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/clusters_empty.svg1
-rw-r--r--app/assets/images/illustrations/convdev/convdev_no_data.svg1
-rw-r--r--app/assets/images/illustrations/convdev/convdev_no_index.svg1
-rw-r--r--app/assets/images/illustrations/convdev/convdev_overview.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_1.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_10.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_2.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_3.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_4.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_5.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_6.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_7.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_8.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_9.svg1
-rw-r--r--app/assets/images/illustrations/logos/go_logo.svg1
-rw-r--r--app/assets/images/illustrations/logos/mattermost_logo.svg1
-rw-r--r--app/assets/images/illustrations/monitoring/getting_started.svg2
-rw-r--r--app/assets/images/illustrations/monitoring/loading.svg2
-rw-r--r--app/assets/images/illustrations/monitoring/unable_to_connect.svg2
-rw-r--r--app/assets/images/illustrations/no_commits.svg1
-rw-r--r--app/assets/images/illustrations/welcome/add_new_group.svg1
-rw-r--r--app/assets/images/illustrations/welcome/add_new_project.svg1
-rw-r--r--app/assets/images/illustrations/welcome/add_new_user.svg1
-rw-r--r--app/assets/images/illustrations/welcome/configure_server.svg1
-rw-r--r--app/assets/images/illustrations/welcome/ee_trial.svg1
-rw-r--r--app/assets/images/illustrations/welcome/globe.svg1
-rw-r--r--app/assets/images/illustrations/welcome/lightbulb.svg1
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js73
-rw-r--r--app/assets/javascripts/behaviors/index.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js3
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js52
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue (renamed from app/assets/javascripts/boards/components/board_card.js)40
-rw-r--r--app/assets/javascripts/boards/components/board_list.js2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js11
-rw-r--r--app/assets/javascripts/boards/models/issue.js18
-rw-r--r--app/assets/javascripts/boards/services/board_service.js10
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js19
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js58
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js12
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js74
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js454
-rw-r--r--app/assets/javascripts/dispatcher.js53
-rw-r--r--app/assets/javascripts/droplab/constants.js2
-rw-r--r--app/assets/javascripts/droplab/drop_down.js29
-rw-r--r--app/assets/javascripts/droplab/hook.js2
-rw-r--r--app/assets/javascripts/dropzone_input.js5
-rw-r--r--app/assets/javascripts/environments/components/container.vue71
-rw-r--r--app/assets/javascripts/environments/components/empty_state.vue42
-rw-r--r--app/assets/javascripts/environments/components/environment.vue268
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue8
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue4
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue128
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue16
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js35
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js31
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue255
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js167
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js7
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/gl_dropdown.js3
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue14
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue8
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js1
-rw-r--r--app/assets/javascripts/init_legacy_filters.js5
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js5
-rw-r--r--app/assets/javascripts/issuable_index.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue18
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue16
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue15
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue15
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue2
-rw-r--r--app/assets/javascripts/jobs/job_details_mediator.js6
-rw-r--r--app/assets/javascripts/jobs/services/job_service.js9
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js22
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js42
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js14
-rw-r--r--app/assets/javascripts/lib/utils/poll.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js9
-rw-r--r--app/assets/javascripts/main.js19
-rw-r--r--app/assets/javascripts/milestone.js85
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js13
-rw-r--r--app/assets/javascripts/new_branch_form.js168
-rw-r--r--app/assets/javascripts/new_commit_form.js54
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue26
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue37
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion.vue28
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue19
-rw-r--r--app/assets/javascripts/notes/components/issue_note.vue12
-rw-r--r--app/assets/javascripts/notes/components/issue_note_body.vue18
-rw-r--r--app/assets/javascripts/notes/components/issue_note_form.vue12
-rw-r--r--app/assets/javascripts/notes/components/issue_notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue (renamed from app/assets/javascripts/notes/components/issue_note_actions.vue)6
-rw-r--r--app/assets/javascripts/notes/components/note_attachment.vue (renamed from app/assets/javascripts/notes/components/issue_note_attachment.vue)2
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue (renamed from app/assets/javascripts/notes/components/issue_note_awards_list.vue)0
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue (renamed from app/assets/javascripts/notes/components/issue_note_edited_text.vue)0
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue (renamed from app/assets/javascripts/notes/components/issue_note_header.vue)0
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue (renamed from app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue)1
-rw-r--r--app/assets/javascripts/notes/index.js4
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js (renamed from app/assets/javascripts/notes/services/issue_notes_service.js)0
-rw-r--r--app/assets/javascripts/notes/stores/actions.js4
-rw-r--r--app/assets/javascripts/notes/stores/getters.js4
-rw-r--r--app/assets/javascripts/notes/stores/index.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue37
-rw-r--r--app/assets/javascripts/project.js7
-rw-r--r--app/assets/javascripts/project_label_subscription.js79
-rw-r--r--app/assets/javascripts/project_new.js272
-rw-r--r--app/assets/javascripts/project_select.js134
-rw-r--r--app/assets/javascripts/project_show.js11
-rw-r--r--app/assets/javascripts/project_variables.js60
-rw-r--r--app/assets/javascripts/projects/ci_cd_settings_bundle.js19
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_setting.vue2
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue51
-rw-r--r--app/assets/javascripts/projects/permissions/components/settings_panel.vue2
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_item.vue25
-rw-r--r--app/assets/javascripts/render_gfm.js20
-rw-r--r--app/assets/javascripts/render_math.js75
-rw-r--r--app/assets/javascripts/render_mermaid.js32
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list.vue89
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue35
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list_item.vue36
-rw-r--r--app/assets/javascripts/repo/components/repo.vue32
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue159
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue54
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue5
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue10
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue6
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue10
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue25
-rw-r--r--app/assets/javascripts/repo/components/repo_tabs.vue4
-rw-r--r--app/assets/javascripts/repo/lib/common/disposable.js14
-rw-r--r--app/assets/javascripts/repo/lib/common/model.js56
-rw-r--r--app/assets/javascripts/repo/lib/common/model_manager.js32
-rw-r--r--app/assets/javascripts/repo/lib/decorations/controller.js43
-rw-r--r--app/assets/javascripts/repo/lib/diff/controller.js71
-rw-r--r--app/assets/javascripts/repo/lib/diff/diff.js30
-rw-r--r--app/assets/javascripts/repo/lib/diff/diff_worker.js10
-rw-r--r--app/assets/javascripts/repo/lib/editor.js79
-rw-r--r--app/assets/javascripts/repo/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/repo/services/index.js4
-rw-r--r--app/assets/javascripts/repo/stores/getters.js4
-rw-r--r--app/assets/javascripts/right_sidebar.js6
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue29
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue40
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue6
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js104
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js102
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js34
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js5
-rw-r--r--app/assets/javascripts/star.js9
-rw-r--r--app/assets/javascripts/subscription.js45
-rw-r--r--app/assets/javascripts/subscription_select.js49
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue104
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue (renamed from app/assets/javascripts/pipelines/components/navigation_tabs.vue)29
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue79
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue163
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue77
-rw-r--r--app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js42
-rw-r--r--app/assets/javascripts/vue_shared/mixins/issuable.js13
-rw-r--r--app/assets/javascripts/zen_mode.js8
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/animations.scss2
-rw-r--r--app/assets/stylesheets/framework/blank.scss9
-rw-r--r--app/assets/stylesheets/framework/buttons.scss45
-rw-r--r--app/assets/stylesheets/framework/common.scss33
-rw-r--r--app/assets/stylesheets/framework/contextual-sidebar.scss19
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss1
-rw-r--r--app/assets/stylesheets/framework/files.scss12
-rw-r--r--app/assets/stylesheets/framework/filters.scss5
-rw-r--r--app/assets/stylesheets/framework/flash.scss12
-rw-r--r--app/assets/stylesheets/framework/gfm.scss1
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss8
-rw-r--r--app/assets/stylesheets/framework/header.scss20
-rw-r--r--app/assets/stylesheets/framework/icons.scss14
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss6
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss43
-rw-r--r--app/assets/stylesheets/framework/mixins.scss8
-rw-r--r--app/assets/stylesheets/framework/new-nav.scss0
-rw-r--r--app/assets/stylesheets/framework/selects.scss18
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss25
-rw-r--r--app/assets/stylesheets/framework/toggle.scss138
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss27
-rw-r--r--app/assets/stylesheets/framework/wells.scss5
-rw-r--r--app/assets/stylesheets/framework/zen.scss8
-rw-r--r--app/assets/stylesheets/pages/builds.scss13
-rw-r--r--app/assets/stylesheets/pages/clusters.scss20
-rw-r--r--app/assets/stylesheets/pages/diff.scss33
-rw-r--r--app/assets/stylesheets/pages/help.scss20
-rw-r--r--app/assets/stylesheets/pages/issuable.scss65
-rw-r--r--app/assets/stylesheets/pages/issues.scss76
-rw-r--r--app/assets/stylesheets/pages/login.scss4
-rw-r--r--app/assets/stylesheets/pages/note_form.scss20
-rw-r--r--app/assets/stylesheets/pages/notes.scss28
-rw-r--r--app/assets/stylesheets/pages/profile.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss118
-rw-r--r--app/assets/stylesheets/pages/repo.scss460
-rw-r--r--app/assets/stylesheets/pages/status.scss4
-rw-r--r--app/assets/stylesheets/pages/wiki.scss6
-rw-r--r--app/controllers/admin/appearances_controller.rb6
-rw-r--r--app/controllers/application_controller.rb34
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/boards/issues_controller.rb1
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/concerns/notes_actions.rb10
-rw-r--r--app/controllers/concerns/preview_markdown.rb1
-rw-r--r--app/controllers/groups/milestones_controller.rb3
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb1
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb8
-rw-r--r--app/controllers/passwords_controller.rb16
-rw-r--r--app/controllers/profiles/passwords_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb75
-rw-r--r--app/controllers/projects/clusters/user_controller.rb39
-rw-r--r--app/controllers/projects/clusters_controller.rb102
-rw-r--r--app/controllers/projects/commit_controller.rb9
-rw-r--r--app/controllers/projects/commits_controller.rb4
-rw-r--r--app/controllers/projects/deployments_controller.rb1
-rw-r--r--app/controllers/projects/environments_controller.rb1
-rw-r--r--app/controllers/projects/group_links_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/labels_controller.rb1
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb3
-rw-r--r--app/controllers/projects/notes_controller.rb1
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb19
-rw-r--r--app/controllers/projects/settings/repository_controller.rb10
-rw-r--r--app/controllers/projects/wikis_controller.rb9
-rw-r--r--app/controllers/projects_controller.rb5
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/controllers/snippets/notes_controller.rb1
-rw-r--r--app/controllers/unicorn_test_controller.rb14
-rw-r--r--app/finders/clusters_finder.rb29
-rw-r--r--app/finders/notes_finder.rb3
-rw-r--r--app/finders/personal_access_tokens_finder.rb1
-rw-r--r--app/finders/runner_jobs_finder.rb22
-rw-r--r--app/finders/users_finder.rb2
-rw-r--r--app/helpers/appearances_helper.rb25
-rw-r--r--app/helpers/application_settings_helper.rb24
-rw-r--r--app/helpers/auto_devops_helper.rb18
-rw-r--r--app/helpers/button_helper.rb56
-rw-r--r--app/helpers/ci_status_helper.rb5
-rw-r--r--app/helpers/commits_helper.rb4
-rw-r--r--app/helpers/diff_helper.rb7
-rw-r--r--app/helpers/emails_helper.rb1
-rw-r--r--app/helpers/issuables_helper.rb1
-rw-r--r--app/helpers/markup_helper.rb11
-rw-r--r--app/helpers/notifications_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/search_helper.rb3
-rw-r--r--app/helpers/tree_helper.rb1
-rw-r--r--app/helpers/visibility_level_helper.rb2
-rw-r--r--app/models/appearance.rb3
-rw-r--r--app/models/application_setting.rb46
-rw-r--r--app/models/blob.rb17
-rw-r--r--app/models/ci/build.rb53
-rw-r--r--app/models/ci/job_artifact.rb36
-rw-r--r--app/models/ci/pipeline.rb74
-rw-r--r--app/models/ci/runner.rb12
-rw-r--r--app/models/clusters/cluster.rb17
-rw-r--r--app/models/clusters/platforms/kubernetes.rb157
-rw-r--r--app/models/clusters/providers/gcp.rb1
-rw-r--r--app/models/commit.rb25
-rw-r--r--app/models/commit_collection.rb44
-rw-r--r--app/models/commit_range.rb8
-rw-r--r--app/models/commit_status.rb16
-rw-r--r--app/models/concerns/artifact_migratable.rb45
-rw-r--r--app/models/concerns/awardable.rb1
-rw-r--r--app/models/concerns/has_variable.rb4
-rw-r--r--app/models/concerns/issuable.rb28
-rw-r--r--app/models/concerns/manual_inverse_association.rb17
-rw-r--r--app/models/concerns/mentionable.rb4
-rw-r--r--app/models/concerns/protected_branch_access.rb18
-rw-r--r--app/models/concerns/protected_ref_access.rb24
-rw-r--r--app/models/concerns/referable.rb8
-rw-r--r--app/models/email.rb1
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/external_issue.rb4
-rw-r--r--app/models/fork_network_member.rb10
-rw-r--r--app/models/group.rb25
-rw-r--r--app/models/identity.rb15
-rw-r--r--app/models/issue.rb8
-rw-r--r--app/models/key.rb9
-rw-r--r--app/models/label.rb6
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/merge_request.rb99
-rw-r--r--app/models/merge_request_diff.rb79
-rw-r--r--app/models/milestone.rb14
-rw-r--r--app/models/namespace.rb18
-rw-r--r--app/models/note.rb20
-rw-r--r--app/models/pages_domain.rb5
-rw-r--r--app/models/project.rb72
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb1
-rw-r--r--app/models/project_services/kubernetes_service.rb6
-rw-r--r--app/models/project_statistics.rb4
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/protected_tag.rb4
-rw-r--r--app/models/protected_tag/create_access_level.rb12
-rw-r--r--app/models/repository.rb66
-rw-r--r--app/models/service.rb2
-rw-r--r--app/models/snippet.rb15
-rw-r--r--app/models/storage/hashed_project.rb1
-rw-r--r--app/models/system_note_metadata.rb10
-rw-r--r--app/models/user.rb82
-rw-r--r--app/models/wiki_page.rb19
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/namespace_policy.rb1
-rw-r--r--app/presenters/clusters/cluster_presenter.rb4
-rw-r--r--app/services/base_count_service.rb44
-rw-r--r--app/services/ci/create_pipeline_service.rb55
-rw-r--r--app/services/ci/fetch_kubernetes_token_service.rb1
-rw-r--r--app/services/ci/register_job_service.rb10
-rw-r--r--app/services/clusters/create_service.rb8
-rw-r--r--app/services/compare_service.rb12
-rw-r--r--app/services/issuable/common_system_notes_service.rb14
-rw-r--r--app/services/issuable/destroy_service.rb9
-rw-r--r--app/services/issuable_base_service.rb33
-rw-r--r--app/services/issues/base_service.rb8
-rw-r--r--app/services/issues/update_service.rb7
-rw-r--r--app/services/labels/promote_service.rb1
-rw-r--r--app/services/merge_requests/base_service.rb22
-rw-r--r--app/services/merge_requests/build_service.rb91
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb24
-rw-r--r--app/services/merge_requests/create_service.rb6
-rw-r--r--app/services/merge_requests/merge_service.rb12
-rw-r--r--app/services/merge_requests/refresh_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb5
-rw-r--r--app/services/milestones/promote_service.rb23
-rw-r--r--app/services/notification_recipient_service.rb13
-rw-r--r--app/services/projects/batch_count_service.rb31
-rw-r--r--app/services/projects/batch_forks_count_service.rb18
-rw-r--r--app/services/projects/batch_open_issues_count_service.rb16
-rw-r--r--app/services/projects/count_service.rb30
-rw-r--r--app/services/projects/forks_count_service.rb13
-rw-r--r--app/services/projects/group_links/destroy_service.rb1
-rw-r--r--app/services/projects/hashed_storage/migrate_attachments_service.rb54
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb70
-rw-r--r--app/services/projects/hashed_storage_migration_service.rb62
-rw-r--r--app/services/projects/import_service.rb20
-rw-r--r--app/services/projects/open_issues_count_service.rb14
-rw-r--r--app/services/projects/open_merge_requests_count_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb38
-rw-r--r--app/services/projects/update_pages_service.rb2
-rw-r--r--app/services/projects/update_service.rb10
-rw-r--r--app/services/protected_branches/legacy_api_create_service.rb (renamed from app/services/protected_branches/api_create_service.rb)4
-rw-r--r--app/services/protected_branches/legacy_api_update_service.rb (renamed from app/services/protected_branches/api_update_service.rb)4
-rw-r--r--app/services/system_hooks_service.rb6
-rw-r--r--app/services/system_note_service.rb14
-rw-r--r--app/services/todo_service.rb1
-rw-r--r--app/services/users/build_service.rb2
-rw-r--r--app/services/users/keys_count_service.rb27
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/uploaders/artifact_uploader.rb39
-rw-r--r--app/uploaders/file_uploader.rb17
-rw-r--r--app/uploaders/job_artifact_uploader.rb46
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb33
-rw-r--r--app/validators/certificate_key_validator.rb1
-rw-r--r--app/validators/certificate_validator.rb1
-rw-r--r--app/validators/cluster_name_validator.rb10
-rw-r--r--app/views/admin/appearances/_form.html.haml38
-rw-r--r--app/views/admin/appearances/preview_sign_in.html.haml (renamed from app/views/admin/appearances/preview.html.haml)0
-rw-r--r--app/views/admin/application_settings/_form.html.haml94
-rw-r--r--app/views/admin/dashboard/index.html.haml4
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/devise/passwords/edit.html.haml4
-rw-r--r--app/views/devise/sessions/_new_base.html.haml2
-rw-r--r--app/views/devise/sessions/new.html.haml6
-rw-r--r--app/views/devise/shared/_links.erb12
-rw-r--r--app/views/devise/shared/_sign_in_link.html.haml2
-rw-r--r--app/views/devise/shared/_signin_box.html.haml4
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml4
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml2
-rw-r--r--app/views/errors/omniauth_error.html.haml2
-rw-r--r--app/views/events/_event.atom.builder7
-rw-r--r--app/views/groups/milestones/index.html.haml1
-rw-r--r--app/views/groups/milestones/new.html.haml1
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/layouts/_flash.html.haml16
-rw-r--r--app/views/layouts/_mailer.html.haml4
-rw-r--r--app/views/layouts/devise.html.haml5
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml18
-rw-r--r--app/views/notify/new_user_email.html.haml2
-rw-r--r--app/views/projects/_issuable_by_email.html.haml30
-rw-r--r--app/views/projects/_md_preview.html.haml26
-rw-r--r--app/views/projects/_readme.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml4
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml8
-rw-r--r--app/views/projects/blob/_header.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml3
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/buttons/_star.html.haml6
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml13
-rw-r--r--app/views/projects/clusters/_banner.html.haml21
-rw-r--r--app/views/projects/clusters/_cluster.html.haml22
-rw-r--r--app/views/projects/clusters/_dropdown.html.haml12
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml12
-rw-r--r--app/views/projects/clusters/_enabled.html.haml15
-rw-r--r--app/views/projects/clusters/_form.html.haml35
-rw-r--r--app/views/projects/clusters/_tabs.html.haml16
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml32
-rw-r--r--app/views/projects/clusters/gcp/_header.html.haml (renamed from app/views/projects/clusters/_header.html.haml)4
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml40
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml (renamed from app/views/projects/clusters/login.html.haml)3
-rw-r--r--app/views/projects/clusters/gcp/new.html.haml10
-rw-r--r--app/views/projects/clusters/index.html.haml23
-rw-r--r--app/views/projects/clusters/new.html.haml17
-rw-r--r--app/views/projects/clusters/new_gcp.html.haml10
-rw-r--r--app/views/projects/clusters/show.html.haml60
-rw-r--r--app/views/projects/clusters/user/_form.html.haml25
-rw-r--r--app/views/projects/clusters/user/_header.html.haml5
-rw-r--r--app/views/projects/clusters/user/_show.html.haml29
-rw-r--r--app/views/projects/clusters/user/new.html.haml11
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml8
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml6
-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/show.html.haml17
-rw-r--r--app/views/projects/forks/_fork_button.html.haml26
-rw-r--r--app/views/projects/forks/new.html.haml17
-rw-r--r--app/views/projects/issues/_by_email_description.html.haml6
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/issues/_issue_by_email.html.haml34
-rw-r--r--app/views/projects/issues/_new_branch.html.haml60
-rw-r--r--app/views/projects/issues/index.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml6
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml9
-rw-r--r--app/views/projects/jobs/show.html.haml4
-rw-r--r--app/views/projects/merge_requests/_by_email_description.html.haml1
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/index.html.haml3
-rw-r--r--app/views/projects/new.html.haml1
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml26
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml2
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tree/_blob_item.html.haml4
-rw-r--r--app/views/projects/tree/_tree_item.html.haml4
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--app/views/projects/wikis/_pages_wiki_page.html.haml2
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml5
-rw-r--r--app/views/projects/wikis/history.html.haml3
-rw-r--r--app/views/projects/wikis/show.html.haml4
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml3
-rw-r--r--app/views/shared/_label.html.haml4
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_notifications.html.haml10
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/issuable/_nav.html.haml5
-rw-r--r--app/views/shared/issuable/nav_links/_all.html.haml6
-rw-r--r--app/views/shared/notes/_note.html.haml3
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/repo/_repo.html.haml1
-rw-r--r--app/views/users/show.html.haml4
-rw-r--r--app/workers/admin_email_worker.rb2
-rw-r--r--app/workers/authorized_projects_worker.rb8
-rw-r--r--app/workers/background_migration_worker.rb31
-rw-r--r--app/workers/build_coverage_worker.rb2
-rw-r--r--app/workers/build_finished_worker.rb2
-rw-r--r--app/workers/build_hooks_worker.rb2
-rw-r--r--app/workers/build_queue_worker.rb2
-rw-r--r--app/workers/build_success_worker.rb2
-rw-r--r--app/workers/build_trace_sections_worker.rb2
-rw-r--r--app/workers/cluster_install_app_worker.rb2
-rw-r--r--app/workers/cluster_provision_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_app_installation_worker.rb2
-rw-r--r--app/workers/concerns/application_worker.rb40
-rw-r--r--app/workers/concerns/dedicated_sidekiq_queue.rb9
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb2
-rw-r--r--app/workers/create_gpg_signature_worker.rb3
-rw-r--r--app/workers/create_pipeline_worker.rb16
-rw-r--r--app/workers/delete_merged_branches_worker.rb3
-rw-r--r--app/workers/delete_user_worker.rb3
-rw-r--r--app/workers/email_receiver_worker.rb6
-rw-r--r--app/workers/emails_on_push_worker.rb3
-rw-r--r--app/workers/expire_build_artifacts_worker.rb4
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb3
-rw-r--r--app/workers/expire_job_cache_worker.rb2
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb2
-rw-r--r--app/workers/git_garbage_collect_worker.rb3
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/finish_import_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_base_data_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_repository_worker.rb2
-rw-r--r--app/workers/gitlab_shell_worker.rb3
-rw-r--r--app/workers/gitlab_usage_ping_worker.rb2
-rw-r--r--app/workers/group_destroy_worker.rb3
-rw-r--r--app/workers/import_export_project_cleanup_worker.rb2
-rw-r--r--app/workers/invalid_gpg_signature_update_worker.rb3
-rw-r--r--app/workers/irker_worker.rb4
-rw-r--r--app/workers/merge_worker.rb3
-rw-r--r--app/workers/namespaceless_project_destroy_worker.rb7
-rw-r--r--app/workers/new_issue_worker.rb3
-rw-r--r--app/workers/new_merge_request_worker.rb3
-rw-r--r--app/workers/new_note_worker.rb3
-rw-r--r--app/workers/pages_worker.rb2
-rw-r--r--app/workers/pipeline_hooks_worker.rb2
-rw-r--r--app/workers/pipeline_metrics_worker.rb2
-rw-r--r--app/workers/pipeline_notification_worker.rb2
-rw-r--r--app/workers/pipeline_process_worker.rb2
-rw-r--r--app/workers/pipeline_schedule_worker.rb4
-rw-r--r--app/workers/pipeline_success_worker.rb2
-rw-r--r--app/workers/pipeline_update_worker.rb2
-rw-r--r--app/workers/post_receive.rb3
-rw-r--r--app/workers/process_commit_worker.rb3
-rw-r--r--app/workers/project_cache_worker.rb3
-rw-r--r--app/workers/project_destroy_worker.rb3
-rw-r--r--app/workers/project_export_worker.rb3
-rw-r--r--app/workers/project_migrate_hashed_storage_worker.rb29
-rw-r--r--app/workers/project_service_worker.rb3
-rw-r--r--app/workers/propagate_service_template_worker.rb3
-rw-r--r--app/workers/prune_old_events_worker.rb2
-rw-r--r--app/workers/reactive_caching_worker.rb3
-rw-r--r--app/workers/remove_expired_group_links_worker.rb2
-rw-r--r--app/workers/remove_expired_members_worker.rb2
-rw-r--r--app/workers/remove_old_web_hook_logs_worker.rb2
-rw-r--r--app/workers/remove_unreferenced_lfs_objects_worker.rb2
-rw-r--r--app/workers/repository_archive_cache_worker.rb2
-rw-r--r--app/workers/repository_check/batch_worker.rb2
-rw-r--r--app/workers/repository_check/clear_worker.rb2
-rw-r--r--app/workers/repository_check/single_repository_worker.rb18
-rw-r--r--app/workers/repository_fork_worker.rb15
-rw-r--r--app/workers/repository_import_worker.rb3
-rw-r--r--app/workers/requests_profiles_worker.rb2
-rw-r--r--app/workers/schedule_update_user_activity_worker.rb2
-rw-r--r--app/workers/stage_update_worker.rb2
-rw-r--r--app/workers/storage_migrator_worker.rb3
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb17
-rw-r--r--app/workers/stuck_import_jobs_worker.rb2
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb2
-rw-r--r--app/workers/system_hook_push_worker.rb3
-rw-r--r--app/workers/trending_projects_worker.rb2
-rw-r--r--app/workers/update_merge_requests_worker.rb3
-rw-r--r--app/workers/update_user_activity_worker.rb3
-rw-r--r--app/workers/upload_checksum_worker.rb3
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb2
-rw-r--r--app/workers/web_hook_worker.rb3
574 files changed, 7233 insertions, 4039 deletions
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json
index d8d173612d5..6befc551263 100644
--- a/app/assets/images/icons.json
+++ b/app/assets/images/icons.json
@@ -1 +1 @@
-{"iconCount":173,"spriteSize":75815,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","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","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","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","pipeline","play","plus-square-o","plus-square","plus","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","spam","spinner","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_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","user","users","volume-up","warning","work"]} \ No newline at end of file
+{"iconCount":179,"spriteSize":81882,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","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","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","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","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","spam","spinner","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_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","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 c8f10628713..74e1c8c22f6 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-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="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 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="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 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="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="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.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="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 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></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-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="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 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></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="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.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 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.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-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="eofirst-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="eosecond-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="eothird-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 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></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="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="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 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="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.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 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-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="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="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 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="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="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.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="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 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></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-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 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></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="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.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 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.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-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="eufirst-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="eusecond-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="euthird-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 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></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="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="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 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="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.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 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/clusters_empty.svg b/app/assets/images/illustrations/clusters_empty.svg
new file mode 100644
index 00000000000..c13228638be
--- /dev/null
+++ b/app/assets/images/illustrations/clusters_empty.svg
@@ -0,0 +1 @@
+<svg height="128" viewBox="0 0 142 128" width="142" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M94 62h20v4H94z" fill="#f0edf8"/><path d="M84.828 84l17.678 17.678-2.828 2.828L82 86.828z" fill="#fee1d3"/><path d="M42.828 24l17.678 17.678-2.828 2.828L40 26.828zM40 101.678L57.678 84l2.828 2.828-17.678 17.678z" fill="#f0edf8"/><g fill="#fee1d3"><path d="M82 41.678L99.678 24l2.828 2.828-17.678 17.678zM28 62h20v4H28z"/><rect height="30" rx="5" width="30" y="49"/></g><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="2" y="51"/><rect fill="#c3b8e3" height="50" rx="10" width="50" x="46" y="39"/><rect height="46" rx="10" stroke="#6b4fbb" stroke-width="4" width="46" x="48" y="41"/><rect fill="#fef0e8" height="30" rx="5" width="30" x="84"/><rect height="26" rx="5" stroke="#fee1d3" stroke-width="4" width="26" x="86" y="2"/><rect fill="#fee1d3" height="30" rx="5" width="30" x="84" y="98"/><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="86" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="112" y="49"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="114" y="51"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28" y="98"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="2"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/convdev_no_data.svg b/app/assets/images/illustrations/convdev/convdev_no_data.svg
new file mode 100644
index 00000000000..b90eddcccfa
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/convdev_no_data.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="360" height="220" viewBox="0 0 360 220"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".02" d="M125 44V24.003C125 18.48 129.483 14 135.005 14h89.99C230.52 14 235 18.477 235 24.003V43h84.992C326.624 43 332 48.372 332 55.002v144.996c0 6.63-5.38 12.002-12.008 12.002h-85.984c-6.632 0-12.008-5.372-12.008-12.002V183h-78v17.002c0 6.626-5.38 11.998-12.008 11.998H46.008C39.376 212 34 206.624 34 200.002V55.998C34 49.372 39.38 44 46.008 44H125z"/><g transform="translate(214 36)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><path fill="#F0EDF8" fill-rule="nonzero" d="M57 111c-11.598 0-21-9.402-21-21s9.402-21 21-21 21 9.402 21 21-9.402 21-21 21zm0-4c9.39 0 17-7.61 17-17s-7.61-17-17-17-17 7.61-17 17 7.61 17 17 17z"/><path fill="#6B4FBB" d="M58 88v-6.997c0-1.11-.895-2.003-2-2.003-1.112 0-2 .897-2 2.003v8.994a1.999 1.999 0 0 0 2.503 1.94c.162.04.33.063.506.063h7.98a2 2 0 0 0 .001-4H58z"/><rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/><path fill="#EEE" d="M21 16c0-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.995 1.995 0 0 1 21 16zm13 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.995 1.995 0 0 1 34 16zm13 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.995 1.995 0 0 1 47 16zm13 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.995 1.995 0 0 1 60 16zm13 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.995 1.995 0 0 1 73 16zm13 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.995 1.995 0 0 1 86 16zm13 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.995 1.995 0 0 1 99 16z"/></g><g transform="translate(118 7)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><g fill-rule="nonzero"><path fill="#F0EDF8" d="M57 112c-12.15 0-22-9.85-22-22s9.85-22 22-22 22 9.85 22 22-9.85 22-22 22zm0-6c8.837 0 16-7.163 16-16s-7.163-16-16-16-16 7.163-16 16 7.163 16 16 16z"/><path fill="#6B4FBB" d="M41.692 105.8A21.93 21.93 0 0 0 57 112c12.15 0 22-9.85 22-22s-9.85-22-22-22v6c8.837 0 16 7.163 16 16s-7.163 16-16 16a15.935 15.935 0 0 1-11.133-4.508l-4.175 4.31z"/></g><path fill="#EEE" d="M8 16c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2H9.998A1.995 1.995 0 0 1 8 16zm13 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.995 1.995 0 0 1 21 16zm13 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.995 1.995 0 0 1 34 16zm13 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.995 1.995 0 0 1 47 16zm13 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.995 1.995 0 0 1 60 16zm13 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.995 1.995 0 0 1 73 16zm13 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.995 1.995 0 0 1 86 16zm13 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.995 1.995 0 0 1 99 16z"/></g><g transform="translate(26 36)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006v147.988A8 8 0 0 0 12.005 168h89.99a8.007 8.007 0 0 0 8.005-8.006V12.006A8 8 0 0 0 101.995 4h-89.99A8.007 8.007 0 0 0 4 12.006zm-4 0C0 5.376 5.377 0 12.005 0h89.99C108.628 0 114 5.37 114 12.006v147.988c0 6.63-5.377 12.006-12.005 12.006h-89.99C5.372 172 0 166.63 0 159.994V12.006z"/><g transform="translate(21 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(69 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(38 42)"><rect width="22" height="4" x="8" fill="#FEE1D3" rx="2"/><rect width="38" height="4" y="12" fill="#FB722E" rx="2"/></g><path fill="#EEE" d="M4 14h106v4H4z"/><path fill="#333" d="M35.724 138h9.696v-2.856h-2.856V122.76h-2.592c-1.08.648-2.136 1.08-3.792 1.392v2.184h2.856v8.808h-3.312V138zm17.736.288c-2.952 0-5.76-2.208-5.76-7.56 0-5.688 2.952-8.256 6.168-8.256 2.016 0 3.48.84 4.44 1.824l-1.848 2.112c-.528-.576-1.488-1.08-2.376-1.08-1.68 0-3.024 1.2-3.144 4.752.792-1.008 2.112-1.608 3.048-1.608 2.616 0 4.536 1.488 4.536 4.704 0 3.168-2.304 5.112-5.064 5.112zm-.072-2.64c1.056 0 1.92-.744 1.92-2.472 0-1.608-.84-2.208-1.992-2.208-.792 0-1.68.432-2.304 1.512.312 2.4 1.32 3.168 2.376 3.168zM63.9 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/convdev_no_index.svg b/app/assets/images/illustrations/convdev/convdev_no_index.svg
new file mode 100644
index 00000000000..4aaf505e0b8
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/convdev_no_index.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="360" height="200" viewBox="0 0 360 200"><g fill="none" fill-rule="evenodd" transform="translate(3 11)"><rect width="110" height="168" x="6" y="8" fill="#000" fill-opacity=".02" rx="10"/><g transform="translate(0 2)"><rect width="110" height="168" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M2 10.006v147.988A8 8 0 0 0 10.005 166h89.99a8.007 8.007 0 0 0 8.005-8.006V10.006A8 8 0 0 0 99.995 2h-89.99A8.007 8.007 0 0 0 2 10.006zm-4 0C-2 3.376 3.377-2 10.005-2h89.99C106.628-2 112 3.37 112 10.006v147.988c0 6.63-5.377 12.006-12.005 12.006h-89.99C3.372 170-2 164.63-2 157.994V10.006z"/><g transform="translate(19 80)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(67 80)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(36 40)"><rect width="22" height="4" x="8" fill="#FEE1D3" rx="2"/><rect width="38" height="4" y="12" fill="#FB722E" rx="2"/></g><path fill="#EEE" d="M2 12h106v4H2z"/><path fill="#333" d="M38.048 127.792c.792 0 1.68-.432 2.28-1.512-.312-2.4-1.296-3.168-2.376-3.168-1.032 0-1.92.744-1.92 2.472 0 1.608.864 2.208 2.016 2.208zm-.552 8.496c-2.016 0-3.504-.864-4.464-1.824l1.872-2.112c.504.576 1.464 1.08 2.352 1.08 1.704 0 3.024-1.2 3.144-4.752-.792 1.008-2.112 1.608-3.048 1.608-2.592 0-4.536-1.488-4.536-4.704 0-3.168 2.304-5.112 5.064-5.112 2.952 0 5.784 2.208 5.784 7.56 0 5.688-2.976 8.256-6.168 8.256zm13.488 0c-3.048 0-5.304-1.704-5.304-4.176 0-1.848 1.152-2.976 2.592-3.744v-.096c-1.176-.888-2.04-1.992-2.04-3.6 0-2.592 2.04-4.2 4.872-4.2 2.784 0 4.632 1.656 4.632 4.176 0 1.464-.936 2.64-1.992 3.336v.096c1.464.792 2.64 1.968 2.64 3.984 0 2.4-2.16 4.224-5.4 4.224zm.96-9.168c.6-.696.936-1.44.936-2.232 0-1.176-.696-1.968-1.848-1.968-.936 0-1.704.576-1.704 1.752 0 1.248 1.056 1.848 2.616 2.448zm-.888 6.72c1.176 0 2.04-.624 2.04-1.896 0-1.344-1.296-1.848-3.216-2.664-.672.624-1.176 1.488-1.176 2.424 0 1.344 1.08 2.136 2.352 2.136zm10.8-3.84c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g><g transform="translate(122)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><g transform="translate(21 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><g transform="translate(69 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><path fill="#FEE1D3" d="M44 44a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 44zM34 56a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 34 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 74 56z"/><rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="21" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="34" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="47" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="60" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/><path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g><g transform="translate(243)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><path fill="#FEE1D3" d="M44 44a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 44zM34 56a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 34 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 74 56z"/><g transform="translate(21 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><g transform="translate(69 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="21" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="34" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="47" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="60" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/><path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/convdev_overview.svg b/app/assets/images/illustrations/convdev/convdev_overview.svg
new file mode 100644
index 00000000000..a06d70812ca
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/convdev_overview.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="208" height="127" viewBox="0 0 208 127" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="58" height="98" y="17" rx="6"/><rect id="b" width="58" height="98" x="3.5" y="17" rx="6"/><rect id="c" width="58" height="98.394" rx="6"/></defs><g fill="none" fill-rule="evenodd" transform="translate(1)"><path fill="#000" fill-opacity=".06" fill-rule="nonzero" d="M16 11.06c0-1.39.56-2.69 1.534-3.635.398-.386.41-1.025.027-1.426a.993.993 0 0 0-1.413-.028A7.075 7.075 0 0 0 14 11.062c0 .556.448 1.007 1 1.007s1-.452 1-1.01zm6.432-5.043h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0H185a4.95 4.95 0 0 1 3.254 1.215.995.995 0 0 0 1.41-.108c.36-.423.312-1.06-.107-1.422A6.944 6.944 0 0 0 185 4h-.568c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zM190 11.932v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008zm0 10.89v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.01v-4.838c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01zm0 10.89v4.84c0 .555.448 1.007 1 1.007s1-.453 1-1.01v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.008zm0 10.888v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008V44.6c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008zm0 10.89v4.84c0 .556.448 1.007 1 1.007s1-.45 1-1.008v-4.84c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01zm0 10.89v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01zm0 10.888v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008zm0 10.89v4.84c0 .556.448 1.007 1 1.007s1-.45 1-1.008v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.007zm0 10.888v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008zm-.24 21.446a5.06 5.06 0 0 1-2.572 2.985 1.01 1.01 0 0 0-.46 1.348c.24.5.84.708 1.336.464a7.06 7.06 0 0 0 3.598-4.178c.17-.53-.12-1.098-.644-1.27a1 1 0 0 0-1.26.65zm-8.063 3.49h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.577-.116a5.009 5.009 0 0 1-3.19-2.3.994.994 0 0 0-1.373-.333c-.472.29-.62.91-.332 1.386.99 1.632 2.6 2.8 4.465 3.215a1 1 0 0 0 1.192-.768 1.005 1.005 0 0 0-.762-1.2zM16 105.292v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01zm0-10.89v-4.84c0-.555-.448-1.007-1-1.007s-1 .452-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.007zm0-10.888v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-10.89v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.008v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008zm0-10.89v-4.838c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.01zm0-11.888v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-9.89v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.007v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-10.888v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008zm0-10.89v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01z"/><g transform="translate(74)"><rect width="58" height="98" y="20" fill="#000" fill-opacity=".02" rx="6"/><use fill="#FFF" xlink:href="#a"/><rect width="56" height="96" x="1" y="18" stroke="#EEE" stroke-width="2" rx="6"/><g transform="translate(16 45.185)"><path fill="#333" d="M.59 33.815h5.655V32.15H4.58v-7.225H3.066c-.63.378-1.246.63-2.212.812v1.274H2.52v5.14H.59v1.665zm10.093.168c-1.778 0-3.094-.994-3.094-2.436 0-1.078.67-1.736 1.51-2.184v-.056c-.685-.518-1.19-1.162-1.19-2.1 0-1.512 1.19-2.45 2.843-2.45 1.624 0 2.702.966 2.702 2.436 0 .854-.546 1.54-1.162 1.946v.055c.854.462 1.54 1.148 1.54 2.324 0 1.4-1.26 2.463-3.15 2.463zm.56-5.348c.35-.406.546-.84.546-1.302 0-.686-.407-1.148-1.08-1.148-.545 0-.993.336-.993 1.022 0 .728.616 1.078 1.526 1.428zm-.518 3.92c.686 0 1.19-.364 1.19-1.106 0-.785-.756-1.08-1.876-1.555-.393.364-.687.868-.687 1.414 0 .783.63 1.245 1.372 1.245zm6.3-2.24c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.063 2.282 2.883 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.462.84-1.764s-.378-1.736-.84-1.736c-.462 0-.84.434-.84 1.736s.378 1.764.84 1.764zm.308 4.816l4.928-9.464h1.19l-4.927 9.463h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.883 2.27-2.883 1.315 0 2.28 1.064 2.28 2.884 0 1.835-.965 2.913-2.28 2.913zm0-1.148c.46 0 .84-.462.84-1.764 0-1.3-.38-1.735-.84-1.735-.463 0-.84.434-.84 1.736 0 1.303.377 1.765.84 1.765z"/><rect width="13" height="2" x="6" y=".815" fill="#FB722E" rx="1"/><path fill="#F0EDF8" d="M3 47.815c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1zm0 6c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1z"/><rect width="20" height="2" x="3" y="6.815" fill="#FEE1D3" rx="1"/></g><g transform="translate(10.81)"><circle cx="18.19" cy="18" r="18" fill="#FFF"/><path fill="#F0EDF8" fill-rule="nonzero" d="M18.19 34c8.837 0 16-7.163 16-16s-7.163-16-16-16-16 7.163-16 16 7.163 16 16 16zm0 2c-9.94 0-18-8.06-18-18s8.06-18 18-18 18 8.06 18 18-8.06 18-18 18z"/><g transform="translate(10 11)"><path fill="#C3B8E3" fill-rule="nonzero" d="M2.19 13.32L5.397 11h7.783a.998.998 0 0 0 1.01-1V3c0-.55-.45-1-1.01-1H3.2a.998.998 0 0 0-1.01 1v10.32zM6.045 13l-3.422 2.476C1.28 16.45.19 15.892.19 14.23V3c0-1.657 1.337-3 3.01-3h9.98a3.004 3.004 0 0 1 3.01 3v7c0 1.657-1.337 3-3.01 3H6.045z"/><rect width="4" height="2" x="5.19" y="4" fill="#6B4FBB" rx="1"/><rect width="6" height="2" x="5.19" y="7" fill="#6B4FBB" rx="1"/></g></g></g><g transform="translate(144.5)"><rect width="58" height="98" x=".5" y="20" fill="#000" fill-opacity=".02" rx="6"/><use fill="#FFF" xlink:href="#b"/><rect width="56" height="96" x="4.5" y="18" stroke="#EEE" stroke-width="2" rx="6"/><g transform="translate(19 46.185)"><path fill="#333" d="M4.01 33.746c1.793 0 3.305-.938 3.305-2.59 0-1.148-.742-1.876-1.764-2.17v-.056c.953-.406 1.485-1.05 1.485-1.974 0-1.554-1.232-2.436-3.066-2.436-1.093 0-1.99.434-2.8 1.134l1.035 1.26c.56-.49 1.036-.784 1.666-.784.7 0 1.093.364 1.093.98 0 .714-.504 1.19-2.1 1.19v1.456c1.932 0 2.394.49 2.394 1.274 0 .672-.574 1.05-1.442 1.05-.756 0-1.414-.378-1.946-.896l-.953 1.302c.644.756 1.652 1.26 3.094 1.26zm4.51-.168h6.257v-1.736h-1.792c-.42 0-1.036.056-1.484.112 1.443-1.512 2.843-3.108 2.843-4.606 0-1.708-1.19-2.828-2.94-2.828-1.274 0-2.1.476-2.982 1.414l1.12 1.106c.45-.476.94-.91 1.583-.91.77 0 1.26.476 1.26 1.344 0 1.26-1.596 2.786-3.864 4.928v1.176zm9.505-3.5c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.064 2.282 2.884 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.462.84-1.764s-.378-1.736-.84-1.736c-.462 0-.84.434-.84 1.736s.378 1.764.84 1.764zm.308 4.816l4.928-9.464h1.19l-4.927 9.464h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.884 2.27-2.884 1.315 0 2.28 1.064 2.28 2.884 0 1.834-.965 2.912-2.28 2.912zm0-1.148c.46 0 .84-.462.84-1.764s-.38-1.736-.84-1.736c-.463 0-.84.434-.84 1.736s.377 1.764.84 1.764z"/><rect width="13" height="2.008" x="7.5" fill="#FB722E" rx="1.004"/><path fill="#F0EDF8" d="M3.5 47.19c0-.556.455-1.005 1.006-1.005h17.988c.556 0 1.006.445 1.006 1.004 0 .553-.455 1.003-1.006 1.003H4.506A1.003 1.003 0 0 1 3.5 47.188zm0 6.023c0-.555.455-1.004 1.006-1.004h17.988c.556 0 1.006.444 1.006 1.003 0 .554-.455 1.004-1.006 1.004H4.506A1.003 1.003 0 0 1 3.5 53.212z"/><rect width="20" height="2.008" x="4" y="6.024" fill="#FEE1D3" rx="1.004"/></g><g transform="translate(14.413)"><circle cx="18.087" cy="18" r="18" fill="#FFF"/><path fill="#F0EDF8" fill-rule="nonzero" d="M18.087 34c8.836 0 16-7.163 16-16s-7.164-16-16-16c-8.837 0-16 7.163-16 16s7.163 16 16 16zm0 2c-9.942 0-18-8.06-18-18s8.058-18 18-18c9.94 0 18 8.06 18 18s-8.06 18-18 18z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M18.087 24a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm0 2c-4.42 0-8-3.582-8-8s3.58-8 8-8a8 8 0 0 1 0 16z"/><path fill="#6B4FBB" d="M19.087 17v-2c0-.556-.448-1-1-1-.557 0-1 .448-1 1v3a.997.997 0 0 0 .998 1h3c.557 0 1-.448 1-1 0-.556-.447-1-1-1h-2z"/></g></g><rect width="58" height="98" x="3" y="20" fill="#000" fill-opacity=".02" rx="6"/><g transform="translate(0 16.754)"><use fill="#FFF" xlink:href="#c"/><rect width="56" height="96.394" x="1" y="1" stroke="#EEE" stroke-width="2" rx="6"/><g transform="translate(16 29.618)"><path fill="#333" d="M3.137 27.84c.462 0 .98-.253 1.33-.883-.182-1.4-.756-1.848-1.386-1.848-.6 0-1.12.433-1.12 1.44 0 .94.505 1.29 1.177 1.29zm-.322 4.955A3.626 3.626 0 0 1 .21 31.73l1.093-1.23c.294.335.854.63 1.372.63.994 0 1.764-.7 1.834-2.773-.463.588-1.233.938-1.78.938-1.51 0-2.645-.868-2.645-2.744 0-1.847 1.344-2.98 2.954-2.98 1.72 0 3.373 1.287 3.373 4.41 0 3.317-1.736 4.815-3.598 4.815zm8.12 0c-1.722 0-3.36-1.288-3.36-4.41 0-3.318 1.722-4.816 3.598-4.816 1.176 0 2.03.49 2.59 1.063l-1.078 1.232c-.308-.336-.868-.63-1.386-.63-.98 0-1.765.7-1.835 2.772.462-.588 1.232-.938 1.778-.938 1.526 0 2.646.867 2.646 2.743 0 1.848-1.345 2.982-2.955 2.982zm-.042-1.54c.616 0 1.12-.434 1.12-1.442 0-.938-.49-1.288-1.162-1.288-.46 0-.98.252-1.343.882.182 1.4.77 1.848 1.386 1.848zm6.132-2.128c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.065 2.282 2.885 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.463.84-1.765 0-1.302-.378-1.736-.84-1.736-.462 0-.84.433-.84 1.735s.378 1.764.84 1.764zm.308 4.815l4.928-9.464h1.19l-4.927 9.465h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.884 2.27-2.884 1.315 0 2.28 1.063 2.28 2.883 0 1.834-.965 2.912-2.28 2.912zm0-1.148c.46 0 .84-.462.84-1.764s-.38-1.736-.84-1.736c-.463 0-.84.434-.84 1.736s.377 1.764.84 1.764z"/><rect width="13" height="2.008" x="6.5" y=".314" fill="#FEE1D3" rx="1.004"/><path fill="#F0EDF8" d="M3 46.627c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1zm0 6c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1z"/><rect width="20" height="2" x="3" y="5.627" fill="#FB722E" rx="1"/></g></g><g transform="translate(10.41)"><circle cx="18.589" cy="18" r="18" fill="#FFF"/><path fill="#F0EDF8" fill-rule="nonzero" d="M18.59 34c8.836 0 16-7.163 16-16s-7.164-16-16-16c-8.837 0-16 7.163-16 16s7.163 16 16 16zm0 2c-9.942 0-18-8.06-18-18s8.058-18 18-18c9.94 0 18 8.06 18 18s-8.06 18-18 18z"/><path fill="#C3B8E3" d="M17.05 19.262h3.367l.248-2.808H17.3l-.25 2.808zm-.177 2.008l-.144 1.627c-.06.662-.646 1.2-1.3 1.2h.25c-.658 0-1.144-.534-1.085-1.2l.144-1.627H13.59a1.001 1.001 0 0 1-1.003-1.004c0-.555.455-1.004 1.002-1.004h1.325l.248-2.808h-1.15a1 1 0 0 1-1.004-1.004 1.01 1.01 0 0 1 1.004-1.004h1.33l.106-1.2c.058-.66.644-1.198 1.298-1.198h-.25c.66 0 1.145.533 1.086 1.2l-.106 1.198h3.365l.107-1.2c.058-.66.644-1.198 1.298-1.198h-.25c.66 0 1.145.533 1.086 1.2l-.106 1.198h1.03c.554 0 1.003.446 1.003 1.004 0 .555-.455 1.004-1 1.004H22.8l-.25 2.808h1.037a1 1 0 0 1 1.002 1.004c0 .554-.456 1.004-1.003 1.004h-1.214l-.144 1.627c-.06.662-.646 1.2-1.3 1.2h.25c-.658 0-1.144-.534-1.085-1.2l.144-1.627H16.87z"/><path fill="#6B4FBB" d="M17.05 19.262l-.177 2.008H14.74l.177-2.008h2.134zm-1.707-4.816h2.135l-.178 2.008h-2.135l.178-2.008zm5.5 0h2.135l-.178 2.008h-2.135l.178-2.008zm1.708 4.816l-.177 2.008H20.24l.177-2.008h2.134z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_1.svg b/app/assets/images/illustrations/convdev/i2p_step_1.svg
new file mode 100644
index 00000000000..67467b1513d
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_1.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M45.688 18.854c-4.869-1.989-10.488-1.975-15.29-.001a20.014 20.014 0 0 0-6.493 4.268 19.798 19.798 0 0 0-4.346 6.381 19.135 19.135 0 0 0-1.525 7.537c0 2.066.33 4.118.983 6.104a20.142 20.142 0 0 0 1.83 3.937 5.983 5.983 0 0 0-2.086 4.538c0 3.309 2.691 6 6 6s6-2.691 6-6-2.691-6-6-6c-.779 0-1.522.154-2.205.425a18.13 18.13 0 0 1-1.642-3.533 17.467 17.467 0 0 1-.881-5.472c0-2.351.459-4.623 1.391-6.814a17.721 17.721 0 0 1 3.88-5.675 18.057 18.057 0 0 1 5.85-3.845c4.329-1.778 9.392-1.79 13.78.002a18.077 18.077 0 0 1 5.843 3.84c3.39 3.34 5.257 7.776 5.257 12.493a17.463 17.463 0 0 1-.878 5.481 17.451 17.451 0 0 1-2.569 4.923c-2.134 2.866-3.818 4.698-5.174 6.173-2.424 2.643-3.98 4.599-4.383 8.384H32.215a1 1 0 1 0 0 2h11.739a1 1 0 0 0 .999-.947c.19-3.645 1.345-5.263 3.934-8.09 1.385-1.506 3.107-3.381 5.304-6.331a19.422 19.422 0 0 0 2.864-5.489c.651-1.98.98-4.04.979-6.109 0-5.256-2.078-10.198-5.856-13.92a20.079 20.079 0 0 0-6.49-4.265M28.761 51.612c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4M40 74h-4a1 1 0 1 0 0 2h4a1 1 0 1 0 0-2M42 70h-8a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2M38 10a1 1 0 0 0 1-1V1a1 1 0 1 0-2 0v8a1 1 0 0 0 1 1M20.828 15.828a.999.999 0 0 0 .707-1.707l-5.656-5.656a.999.999 0 1 0-1.414 1.414l5.656 5.656a.997.997 0 0 0 .707.293M10 33H2a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2M60.12 8.465l-5.656 5.656a.999.999 0 1 0 1.414 1.414l5.656-5.656a.999.999 0 1 0-1.414-1.414M74 33h-8a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2M43 66H33a1 1 0 1 0 0 2h10a1 1 0 1 0 0-2"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_10.svg b/app/assets/images/illustrations/convdev/i2p_step_10.svg
new file mode 100644
index 00000000000..588ecd81414
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_10.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M5 43a1 1 0 1 0 2 0v-4h4a1 1 0 1 0 0-2H7v-4a1 1 0 1 0-2 0v4H1a1 1 0 1 0 0 2h4v4M75 37h-4v-4a1 1 0 1 0-2 0v4h-4a1 1 0 1 0 0 2h4v4a1 1 0 1 0 2 0v-4h4a1 1 0 1 0 0-2M21 38a1 1 0 0 0 .47.848l8 5a.999.999 0 0 0 1.061-1.696L23.887 38l6.644-4.152a1 1 0 1 0-1.061-1.695l-8 5A.998.998 0 0 0 21 38M55 38a1 1 0 0 0-.47-.848l-8-5a.999.999 0 1 0-1.061 1.695L52.113 38l-6.644 4.152a1 1 0 1 0 1.061 1.696l8-5A1 1 0 0 0 55 38M41.803 26.05a1 1 0 0 0-1.256.65l-7 22a1.001 1.001 0 0 0 .953 1.303 1 1 0 0 0 .953-.697l7-22a1.001 1.001 0 0 0-.65-1.256M62 7c3.859 0 7 3.141 7 7v11a1 1 0 1 0 2 0V14c0-4.963-4.04-9-9-9H45.91c-.479-2.833-2.943-5-5.91-5-3.309 0-6 2.691-6 6s2.691 6 6 6c2.967 0 5.431-2.167 5.91-5H62m-22 3c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4M6 26a1 1 0 0 0 1-1V14c0-3.859 3.141-7 7-7h11.09l-3.293 3.293a.999.999 0 1 0 1.414 1.414l5-5a.999.999 0 0 0 0-1.414l-5-5a.999.999 0 1 0-1.414 1.414L25.09 5H14c-4.963 0-9 4.04-9 9v11a1 1 0 0 0 1 1M36 64c-2.967 0-5.431 2.167-5.91 5H14c-3.859 0-7-3.141-7-7V51a1 1 0 1 0-2 0v11c0 4.963 4.04 9 9 9h16.09c.478 2.833 2.942 5 5.91 5 3.309 0 6-2.691 6-6s-2.691-6-6-6m0 10c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4M70 50a1 1 0 0 0-1 1v11c0 3.859-3.141 7-7 7H50.91l3.293-3.293a.999.999 0 1 0-1.414-1.414l-5 5a.999.999 0 0 0 0 1.414l5 5a.997.997 0 0 0 1.414 0 .999.999 0 0 0 0-1.414L50.91 71H62c4.963 0 9-4.04 9-9V51a1 1 0 0 0-1-1"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_2.svg b/app/assets/images/illustrations/convdev/i2p_step_2.svg
new file mode 100644
index 00000000000..4280024c23c
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M42.26 40.44a.989.989 0 0 0 1.109-.877l2.625-22.444a.997.997 0 0 0-.993-1.117h-14a1 1 0 0 0-.994 1.108l3.454 31.575a6.981 6.981 0 0 0-2.46 5.317c0 3.859 3.141 7 7 7s7-3.141 7-7-3.141-7-7-7c-.94 0-1.835.189-2.655.527l-3.23-29.527h11.761L41.383 39.33a1 1 0 0 0 .877 1.11m.741 13.562c0 2.757-2.243 5-5 5s-5-2.243-5-5 2.243-5 5-5 5 2.243 5 5"/><path d="M73.236 23.749a1 1 0 0 0-1.854.75A35.788 35.788 0 0 1 74 38c0 19.851-16.149 36-36 36S2 57.851 2 38 18.149 2 38 2c7.6 0 14.83 2.332 20.965 6.74A5.955 5.955 0 0 0 58 12c0 1.603.624 3.109 1.758 4.242A5.956 5.956 0 0 0 64 18a5.956 5.956 0 0 0 4.242-1.758C69.376 15.109 70 13.603 70 12s-.624-3.109-1.758-4.242A5.956 5.956 0 0 0 64 6a5.943 5.943 0 0 0-3.668 1.259C53.812 2.512 46.104 0 38 0 17.047 0 0 17.047 0 38s17.047 38 38 38 38-17.047 38-38c0-4.93-.93-9.725-2.764-14.251zM64 8c1.068 0 2.072.416 2.828 1.172S68 10.932 68 12s-.416 2.072-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0C60.416 14.072 60 13.068 60 12s.416-2.072 1.172-2.828S62.932 8 64 8z"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_3.svg b/app/assets/images/illustrations/convdev/i2p_step_3.svg
new file mode 100644
index 00000000000..7690f91b420
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_3.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M12 8c0-3.309-2.691-6-6-6S0 4.691 0 8c0 2.967 2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909 0 3.309 2.691 6 6 6s6-2.691 6-6c0-2.967-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.479 5-2.943 5-5.91M2 8c0-2.206 1.794-4 4-4s4 1.794 4 4-1.794 4-4 4-4-1.794-4-4m8 60c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4M21 6h54a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M21 12h35a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M75 24H21a1 1 0 1 0 0 2h54a1 1 0 1 0 0-2M21 32h34a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M75 44H21a1 1 0 1 0 0 2h54a1 1 0 1 0 0-2M21 52h34a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M75 64H21a1 1 0 1 0 0 2h54a1 1 0 1 0 0-2M55 70H21a1 1 0 1 0 0 2h34a1 1 0 1 0 0-2"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_4.svg b/app/assets/images/illustrations/convdev/i2p_step_4.svg
new file mode 100644
index 00000000000..ba21b9e2c3a
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_4.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M67.7 10h-6.751C60.442 4.402 55.728 0 50 0c-6.06 0-11 4.935-11 11s4.935 11 11 11c5.728 0 10.442-4.402 10.949-10H67.7c1.269 0 2.3.987 2.3 2.2v57.6c0 1.213-1.031 2.2-2.3 2.2H8.3C7.031 74 6 73.013 6 71.8V14.2C6 12.987 7.031 12 8.3 12h15.15a1 1 0 1 0 0-2H8.3C5.929 10 4 11.884 4 14.2v57.6C4 74.116 5.929 76 8.3 76h59.4c2.371 0 4.3-1.884 4.3-4.2V14.2c0-2.316-1.929-4.2-4.3-4.2M50 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/><path d="M21.293 29.29a.999.999 0 0 0 0 1.414l12.975 12.975-12.975 12.974a.999.999 0 1 0 1.414 1.414l13.682-13.682a.999.999 0 0 0 0-1.414L22.707 29.29a.999.999 0 0 0-1.414 0M54 59a1 1 0 1 0 0-2H42a1 1 0 1 0 0 2h12"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_5.svg b/app/assets/images/illustrations/convdev/i2p_step_5.svg
new file mode 100644
index 00000000000..3c8f8422a97
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_5.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M48.949 37C48.442 31.402 43.728 27 38 27s-10.442 4.402-10.949 10h-13.05a1 1 0 1 0 0 2h13.05c.507 5.598 5.221 10 10.949 10s10.442-4.402 10.949-10h12.24a1 1 0 1 0 0-2h-12.24M38 47c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/><path d="M73.236 23.749a1 1 0 0 0-1.854.75A35.788 35.788 0 0 1 74 38c0 19.851-16.149 36-36 36S2 57.851 2 38 18.149 2 38 2c7.6 0 14.83 2.332 20.965 6.74A5.955 5.955 0 0 0 58 12c0 1.603.624 3.109 1.758 4.242A5.956 5.956 0 0 0 64 18a5.956 5.956 0 0 0 4.242-1.758C69.376 15.109 70 13.603 70 12s-.624-3.109-1.758-4.242A5.956 5.956 0 0 0 64 6a5.943 5.943 0 0 0-3.668 1.259C53.812 2.512 46.104 0 38 0 17.047 0 0 17.047 0 38s17.047 38 38 38 38-17.047 38-38c0-4.93-.93-9.725-2.764-14.251zM64 8c1.068 0 2.072.416 2.828 1.172S68 10.932 68 12s-.416 2.072-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0C60.416 14.072 60 13.068 60 12s.416-2.072 1.172-2.828S62.932 8 64 8z"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_6.svg b/app/assets/images/illustrations/convdev/i2p_step_6.svg
new file mode 100644
index 00000000000..933860798ad
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_6.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M14.267 7.32l-4.896 5.277-1.702-1.533a.999.999 0 1 0-1.338 1.486l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062a.99.99 0 0 0 .752-.016c.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6a1 1 0 0 0-1.466-1.36M31 9h44a1 1 0 1 0 0-2H31a1 1 0 1 0 0 2M31 15h24a1 1 0 1 0 0-2H31a1 1 0 1 0 0 2"/><path d="M11 0C4.93 0 0 4.935 0 11s4.935 11 11 11 11-4.935 11-11S17.065 0 11 0m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9M14.267 34.32l-4.896 5.277-1.702-1.533a1 1 0 1 0-1.338 1.486l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062a.99.99 0 0 0 .752-.016c.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6a1 1 0 0 0-1.466-1.36M75 34H31a1 1 0 1 0 0 2h44a1 1 0 1 0 0-2M31 42h24a1 1 0 1 0 0-2H31a1 1 0 1 0 0 2"/><path d="M11 27C4.93 27 0 31.935 0 38s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9M14.267 61.32l-4.896 5.277-1.702-1.533a1 1 0 1 0-1.338 1.486l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062a.99.99 0 0 0 .752-.016c.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6a1 1 0 0 0-1.466-1.36"/><path d="M11 54C4.93 54 0 58.935 0 65s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9M75 61H31a1 1 0 1 0 0 2h44a1 1 0 1 0 0-2M55 67H31a1 1 0 1 0 0 2h24a1 1 0 1 0 0-2"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_7.svg b/app/assets/images/illustrations/convdev/i2p_step_7.svg
new file mode 100644
index 00000000000..d97c8f7c2d4
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_7.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M73.236 23.749a1 1 0 1 0-1.854.75A35.788 35.788 0 0 1 74 38c0 19.851-16.149 36-36 36S2 57.851 2 38 18.149 2 38 2c7.6 0 14.83 2.332 20.965 6.74A5.955 5.955 0 0 0 58 12c0 1.603.624 3.109 1.758 4.242A5.956 5.956 0 0 0 64 18a5.956 5.956 0 0 0 4.242-1.758C69.376 15.109 70 13.603 70 12s-.624-3.109-1.758-4.242A5.956 5.956 0 0 0 64 6a5.943 5.943 0 0 0-3.668 1.259C53.812 2.512 46.104 0 38 0 17.047 0 0 17.047 0 38s17.047 38 38 38 38-17.047 38-38c0-4.93-.93-9.725-2.764-14.251zM64 8c1.068 0 2.072.416 2.828 1.172S68 10.932 68 12s-.416 2.072-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0C60.416 14.072 60 13.068 60 12s.416-2.072 1.172-2.828S62.932 8 64 8z"/><path d="M27.19 32.17a.997.997 0 0 0-1.366-.364L13.17 39.132a1 1 0 0 0 0 1.73l12.654 7.326a1 1 0 0 0 1.002-1.73l-11.159-6.461 11.159-6.461a.998.998 0 0 0 .364-1.366M48.808 47.827a1 1 0 0 0 1.366.364l12.654-7.326a1 1 0 0 0 0-1.73l-12.654-7.326a1 1 0 0 0-1.002 1.73L60.331 40l-11.159 6.461a.998.998 0 0 0-.364 1.366M42.71 23.06L31.398 56.29a1 1 0 0 0 1.892.645l11.312-33.23a1 1 0 0 0-1.892-.645"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_8.svg b/app/assets/images/illustrations/convdev/i2p_step_8.svg
new file mode 100644
index 00000000000..919bbeff319
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_8.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M62.44 54.765l-9.912-11.09c.315-3.881.481-7.241.508-10.271-.029-13.871-3.789-23.05-13.413-32.746-.855-.859-2.411-.828-3.294.059-7.594 7.65-11.139 13.934-12.575 22.3a6.94 6.94 0 0 0-4.699 2.039c-1.321 1.321-2.05 3.079-2.05 4.949s.729 3.628 2.051 4.949c1.321 1.322 3.079 2.051 4.949 2.051s3.628-.729 4.949-2.051a6.951 6.951 0 0 0 2.051-4.949 6.955 6.955 0 0 0-2.051-4.949c-.9-.9-2-1.517-3.205-1.824 1.373-7.859 4.764-13.818 11.999-21.11.128-.13.356-.158.456-.059 9.207 9.274 12.805 18.06 12.832 31.33-.026 3.079-.202 6.527-.536 10.54a.997.997 0 0 0 .25.749l10.166 11.379c.062.076.109.23.093.32l-4.547 17.407c-.004.015-.009.036-.079.106a.403.403 0 0 1-.2.106l-3.577.002c-.144-.009-.265-.077-.309-.153l-5.425-10.328a1.002 1.002 0 0 0-.886-.535H30.024c-.371 0-.713.206-.886.535l-5.407 10.303-.069.072a.366.366 0 0 1-.199.105l-3.588.001c-.179-.009-.304-.123-.33-.227l-4.531-17.338a.525.525 0 0 1 .049-.34L25.26 44.682a1 1 0 0 0-1.492-1.332L13.539 54.803c-.448.554-.63 1.312-.474 2.084l4.544 17.396c.253.963 1.146 1.669 2.218 1.719h3.636c.581 0 1.187-.261 1.615-.693.114-.114.286-.286.406-.528l5.144-9.793h14.754l5.16 9.822c.396.697 1.124 1.143 2.01 1.192l3.712-.003a2.396 2.396 0 0 0 1.544-.694c.313-.316.504-.646.598-1.022l4.557-17.451a2.502 2.502 0 0 0-.518-2.066M29.01 30.001c0 1.335-.521 2.591-1.465 3.535s-2.2 1.465-3.535 1.465-2.591-.521-3.535-1.465-1.465-2.2-1.465-3.535.521-2.591 1.465-3.535 2.2-1.465 3.535-1.465 2.591.521 3.535 1.465 1.465 2.2 1.465 3.535"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_9.svg b/app/assets/images/illustrations/convdev/i2p_step_9.svg
new file mode 100644
index 00000000000..2d1b10d430d
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_9.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M68 67c-1.725 0-3.36.541-4.723 1.545A12.998 12.998 0 0 0 52 62c-2.734 0-5.359.853-7.555 2.43L42.159 49h1.228l3.829 7.645c.339.598.962.979 1.724 1.022l2.812-.003a2.07 2.07 0 0 0 1.316-.595c.264-.266.433-.559.514-.882l3.433-13.145a2.138 2.138 0 0 0-.449-1.763l-7.385-8.268c.231-2.875.354-5.376.374-7.641C49.532 14.863 46.684 7.908 39.393.564c-.737-.742-2.072-.715-2.829.044-5.617 5.659-8.309 10.336-9.446 16.463a5.95 5.95 0 0 0-3.36 1.686C22.624 19.891 22 21.397 22 23s.624 3.109 1.758 4.242C24.891 28.376 26.397 29 28 29s3.109-.624 4.242-1.758C33.376 26.109 34 24.603 34 23s-.624-3.109-1.758-4.242a5.952 5.952 0 0 0-3.098-1.648c1.095-5.538 3.637-9.855 8.83-15.14 6.874 6.924 9.561 13.485 9.581 23.392-.021 2.316-.151 4.903-.402 7.91a.999.999 0 0 0 .25.749l7.663 8.572-3.391 13.07-2.695.036-4.081-8.15a1.001 1.001 0 0 0-.895-.553h-12.01c-.379 0-.725.214-.895.553l-4.04 8.114-2.707.015-3.427-13.07 7.671-8.588a1 1 0 0 0-1.492-1.332l-7.7 8.623c-.383.47-.54 1.116-.406 1.787l3.419 13.08c.216.829.98 1.438 1.907 1.48h2.735c.508 0 1.016-.218 1.391-.595.091-.09.242-.241.358-.475l3.804-7.597h1.228l-2.286 15.43a12.914 12.914 0 0 0-7.555-2.43c-4.685 0-8.979 2.53-11.277 6.545a7.943 7.943 0 0 0-4.723-1.545c-4.411 0-8 3.589-8 8a1 1 0 0 0 1 1h74a1 1 0 0 0 1-1c0-4.411-3.589-8-8-8m-36-44a3.973 3.973 0 0 1-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0-.756-.756-1.172-1.76-1.172-2.828s.416-2.072 1.172-2.828 1.76-1.172 2.828-1.172 2.072.416 2.828 1.172 1.172 1.76 1.172 2.828m-29.917 51a6.01 6.01 0 0 1 5.917-5c1.638 0 3.17.652 4.313 1.836a.998.998 0 0 0 1.634-.289 11.011 11.011 0 0 1 10.05-6.547c2.836 0 5.532 1.085 7.593 3.055a1.001 1.001 0 0 0 1.681-.576l2.588-17.479h4.275l2.589 17.479a.999.999 0 1 0 1.681.576 10.945 10.945 0 0 1 7.593-3.055c4.343 0 8.288 2.57 10.05 6.547a.998.998 0 0 0 1.634.289 5.948 5.948 0 0 1 4.313-1.836 6.01 6.01 0 0 1 5.917 5H2.076"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/logos/go_logo.svg b/app/assets/images/illustrations/logos/go_logo.svg
new file mode 100644
index 00000000000..7fd49118006
--- /dev/null
+++ b/app/assets/images/illustrations/logos/go_logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="M14 16.01h1V7.99C15 4.128 11.866.999 8 .999c-3.858 0-7 3.13-7 6.991v8.02h1V7.99c0-3.306 2.691-5.991 6-5.991 3.314 0 6 2.682 6 5.991v8.02M3.48 2.656a2 2 0 1 0-2.155 3.228c.102-.321.226-.631.371-.93a1.001 1.001 0 1 1 1.069-1.599 6.96 6.96 0 0 1 .717-.699m9.04-.002a2 2 0 1 1 2.155 3.23 6.835 6.835 0 0 0-.37-.931 1 1 0 1 0-1.068-1.599 6.96 6.96 0 0 0-.717-.699"/><path d="M5.726 8.04h1.557v.124c0 .283-.033.534-.1.752a1.583 1.583 0 0 1-.33.566c-.35.394-.795.591-1.335.591-.527 0-.979-.19-1.355-.571a1.893 1.893 0 0 1-.564-1.377c0-.547.191-1.01.574-1.391a1.902 1.902 0 0 1 1.396-.574c.295 0 .57.06.825.181.244.12.484.316.72.586l-.405.388c-.309-.412-.686-.618-1.13-.618-.399 0-.733.138-1 .413-.27.27-.405.609-.405 1.015 0 .42.151.766.452 1.037.282.252.587.378.915.378.28 0 .531-.094.754-.283.223-.19.347-.418.373-.683h-.94v-.535m2.884.061c0-.53.194-.986.583-1.367a1.919 1.919 0 0 1 1.396-.571c.537 0 .998.192 1.382.576.386.384.578.845.578 1.384 0 .542-.194 1-.581 1.379a1.944 1.944 0 0 1-1.408.569c-.487 0-.923-.168-1.311-.505-.426-.373-.64-.861-.64-1.465m.574.007c0 .417.14.759.42 1.028.278.269.6.403.964.403.395 0 .729-.137 1-.41.272-.277.408-.613.408-1.01 0-.402-.134-.739-.403-1.01a1.33 1.33 0 0 0-.991-.41c-.392 0-.723.137-.993.41a1.36 1.36 0 0 0-.405 1m-.184 3.918c.525.026.812.063.812.063.271.025.324-.096.116-.273 0 0-.775-.813-1.933-.813-1.159 0-1.923.813-1.923.813-.211.174-.153.3.12.273 0 0 .286-.037.81-.063v.477c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.252.25c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.478m-1-1.023c.552 0 1-.224 1-.5s-.448-.5-1-.5-1 .224-1 .5.448.5 1 .5"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/logos/mattermost_logo.svg b/app/assets/images/illustrations/logos/mattermost_logo.svg
new file mode 100644
index 00000000000..b577c0599aa
--- /dev/null
+++ b/app/assets/images/illustrations/logos/mattermost_logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/monitoring/getting_started.svg b/app/assets/images/illustrations/monitoring/getting_started.svg
index db7a1c2e708..ff783bdd388 100644
--- a/app/assets/images/illustrations/monitoring/getting_started.svg
+++ b/app/assets/images/illustrations/monitoring/getting_started.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="0" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="2" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="4" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="1" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="5" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="matrix(.99619.08716-.08716.99619 19.08-16.813)" rx="10"/><g transform="matrix(.96593.25882-.25882.96593 227.1 57.47)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g transform="translate(24.368 36.951)"><path fill="#d2caea" fill-rule="nonzero" d="m71.785 44.2c.761.296 1.625.099 2.184-.496l35.956-38.34c.756-.806.715-2.071-.091-2.827-.806-.756-2.071-.715-2.827.091l-35.03 37.36-41.888-16.285c-.749-.291-1.6-.106-2.16.471l-26.368 27.16c-.769.793-.751 2.059.042 2.828.793.769 2.059.751 2.828-.042l25.444-26.21 41.911 16.294"/><g fill="#fff"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="matrix(.99619-.08716.08716.99619-12.703 10.717)" rx="10"/><g transform="matrix(.99619.08716-.08716.99619 126.61 137.8)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="m84.67 28.41c18.225 0 33 15.07 33 33.651h-33v-33.651" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="m78.67 66.41h30c1.105 0 2 .895 2 2 0 18.778-15.222 34-34 34-18.778 0-34-15.222-34-34 0-18.778 15.222-34 34-34 1.105 0 2 .895 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28h-29.934c-1.105 0-2-.895-2-2v-29.934c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="matrix(.99619-.08716.08716.99619 30 88.03)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g transform="translate(42 34)"><path fill="#fef0ea" d="m0 13.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391v49.609h-12v-49.609"/><path fill="#fb722e" d="m66 21.406c0-.777.628-1.406 1.4-1.406h9.2c.773 0 1.4.624 1.4 1.406v41.594h-12v-41.594"/><path fill="#6b4fbb" d="m22 1.404c0-.776.628-1.404 1.4-1.404h9.2c.773 0 1.4.624 1.4 1.404v61.6h-12v-61.6"/><path fill="#d2caea" d="m44 39.4c0-.772.628-1.398 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602h-12v-23.602"/></g></g><g fill="#fee8dc"><path d="m6.226 94.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" transform="matrix(.70711.70711-.70711.70711 66.33 22.317)"/><path d="m312.78 53.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 126.1-206.88)"/></g><path fill="#e1dcf1" d="m124.78 12.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711 31.05 90.51)"/><path fill="#d2caea" d="m374.78 244.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711-59.779 335.24)"/></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="b" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="c" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="d" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="e" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="f" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="rotate(5 202.071 210.085)" rx="10"/><g transform="rotate(15 -104.714 891.23)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#d2caea" fill-rule="nonzero" d="M96.153 81.151a2.001 2.001 0 0 0 2.184-.496l35.956-38.34a2 2 0 1 0-2.918-2.736l-35.03 37.36-41.888-16.285a2 2 0 0 0-2.16.471l-26.368 27.16a2 2 0 1 0 2.87 2.786l25.444-26.21 41.911 16.294"/><g fill="#fff" transform="translate(24.368 36.951)"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="rotate(-5 116.372 150.825)" rx="10"/><g transform="rotate(5 -1514.687 1518.752)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#e)" xlink:href="#b"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="M84.67 28.41c18.225 0 33 15.07 33 33.651h-33V28.41" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="M78.67 66.41h30a2 2 0 0 1 2 2c0 18.778-15.222 34-34 34s-34-15.222-34-34 15.222-34 34-34a2 2 0 0 1 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28H76.67a2 2 0 0 1-2-2V38.476c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="rotate(-5 1023.06 -299.524)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#f)" xlink:href="#c"/><path fill="#fef0ea" d="M42 47.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391V97H42V47.391"/><path fill="#fb722e" d="M108 55.406c0-.777.628-1.406 1.4-1.406h9.2a1.4 1.4 0 0 1 1.4 1.406V97h-12V55.406"/><path fill="#6b4fbb" d="M64 35.404c0-.776.628-1.404 1.4-1.404h9.2a1.4 1.4 0 0 1 1.4 1.404v61.6H64v-61.6"/><path fill="#d2caea" d="M86 73.4a1.4 1.4 0 0 1 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602H86V73.4"/></g><g fill="#fee8dc"><path d="M3.592 93.86l-2.454-1.562c-.93-.592-.924-1.554 0-2.143l2.454-1.562 1.562-2.454c.592-.93 1.554-.925 2.143 0l1.562 2.454 2.454 1.562c.93.591.924 1.554 0 2.143L8.86 93.86l-1.562 2.454c-.591.93-1.554.924-2.143 0L3.592 93.86M309.489 52.07l-3.14-1.998c-1.12-.713-1.128-1.863 0-2.581l3.14-2 1.999-3.14c.713-1.12 1.863-1.127 2.58 0l2 3.14 3.14 2c1.12.713 1.128 1.863 0 2.58l-3.14 2-2 3.14c-.712 1.12-1.862 1.128-2.58 0l-1.999-3.14"/></g><path fill="#e1dcf1" d="M128.073 11.066l-1.99 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/><path fill="#d2caea" d="M378.07 243.068l-1.989 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/monitoring/loading.svg b/app/assets/images/illustrations/monitoring/loading.svg
index 6bbd7a6c5b9..1e196fc8ad1 100644
--- a/app/assets/images/illustrations/monitoring/loading.svg
+++ b/app/assets/images/illustrations/monitoring/loading.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="C" width="161" height="100" x="92" y="181" rx="10"/><rect id="E" width="151" height="32" x="20" rx="10"/><rect id="G" width="191" height="62" y="10" rx="10"/><circle id="I" cx="23" cy="41" r="9"/><circle id="4" cx="36.5" cy="36.5" r="36.5"/><circle id="8" cx="262.5" cy="169.5" r="15.5"/><circle id="A" cx="79.5" cy="169.5" r="15.5"/><circle id="K" cx="45" cy="41" r="9"/><circle id="0" cx="30.5" cy="30.5" r="30.5"/><circle id="2" cx="18" cy="34" r="3"/><ellipse id="6" cx="43.5" cy="43.5" rx="43.5" ry="43.5"/><mask id="H" width="191" height="62" x="0" y="0" fill="#fff"><use xlink:href="#G"/></mask><mask id="J" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#I"/></mask><mask id="D" width="161" height="100" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="F" width="151" height="32" x="0" y="0" fill="#fff"><use xlink:href="#E"/></mask><mask id="9" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="1" width="61" height="61" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="B" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="3" width="6" height="6" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="7" width="87" height="87" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="L" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#K"/></mask><mask id="5" width="73" height="73" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(28 2)"><g transform="translate(133 87)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><path stroke="#d2caea" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="m19 32l2-9 5 17 4-12 4 5 6-10 3 5"/><g fill="#fff" stroke="#fb722e"><use stroke-width="4" mask="url(#3)" xlink:href="#2"/><circle cx="44" cy="30" r="2" stroke-width="2"/></g></g><g transform="translate(188 29)"><circle cx="36.5" cy="41.5" r="36.5" fill="#f9f9f9"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><rect width="27" height="4" x="23" y="27" fill="#d2caea" rx="2"/><rect width="10.5" height="4" x="23" y="27" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="36" fill="#d2caea" rx="2"/><rect width="19" height="4" x="23" y="36" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="45" fill="#d2caea" rx="2"/><rect width="7" height="4" x="23" y="45" fill="#6b4fbb" rx="2"/></g><path fill="#eee" fill-rule="nonzero" d="m247 292v1c0 5.519-4.469 9.993-10.01 9.993h-125.99c-5.177 0-9.436-3.927-9.954-8.96 1.348.998 2.957 1.666 4.705 1.883 1.027 1.835 2.992 3.077 5.248 3.077h125.99c2.485 0 4.611-1.497 5.526-3.637 1.796-.675 3.347-1.852 4.48-3.359m1.947-8.962c-.518 5.03-4.774 8.958-9.95 8.958h-131.99c-4.929 0-9.03-3.563-9.851-8.25 1.382.767 2.964 1.216 4.649 1.248 1.037 1.794 2.978 3 5.202 3h131.99c2.255 0 4.219-1.241 5.245-3.076 1.748-.216 3.356-.883 4.705-1.882"/><g transform="translate(79)"><ellipse cx="43.5" cy="47.5" fill="#f9f9f9" rx="43.5" ry="43.5"/><g fill="#fff"><g stroke="#eee"><use stroke-width="8" mask="url(#7)" xlink:href="#6"/><path stroke-width="4" d="m18.595 49c2.515 11.44 12.71 20 24.905 20 14.08 0 25.5-11.417 25.5-25.5 0-12.195-8.56-22.391-20-24.905v15.959c3 1.848 5 5.164 5 8.946 0 5.799-4.701 10.5-10.5 10.5-3.782 0-7.098-2-8.946-5h-15.959" stroke-linejoin="round"/></g><path stroke="#d2caea" stroke-width="4" d="m18 44c-.003-.166-.005-.333-.005-.5 0-14.08 11.417-25.5 25.5-25.5.167 0 .334.002.5.005v15.01c-.166-.008-.332-.012-.5-.012-5.799 0-10.5 4.701-10.5 10.5 0 .168.004.334.012.5h-15.01" stroke-linejoin="round"/></g></g><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill="#eee"><rect width="15" height="2" x="226" y="247" rx="1"/><rect width="15" height="2" x="226" y="242" rx="1"/><rect width="15" height="2" x="226" y="252" rx="1"/></g><rect width="10" height="52" x="118" y="196" fill="#d2caea" rx="2"/><rect width="10" height="47" x="154" y="196" fill="#6b4fbb" rx="2"/><rect width="10" height="37" x="190" y="196" fill="#d2caea" rx="2"/><g fill="#fee8dc"><rect width="10" height="52" x="132" y="185" rx="2"/><rect width="10" height="38" x="168" y="185" rx="2"/></g><rect width="10" height="58" x="204" y="185" fill="#fb722e" rx="2"/><g transform="translate(76 128)"><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#F)" xlink:href="#E"/><use mask="url(#H)" xlink:href="#G"/></g><g fill="#d2caea"><rect width="16" height="4" x="156" y="35" rx="2"/><rect width="16" height="4" x="156" y="43" rx="2"/></g><g fill="#fff" stroke-width="8"><use stroke="#fee8dc" mask="url(#J)" xlink:href="#I"/><use stroke="#fb722e" mask="url(#L)" xlink:href="#K"/></g></g><g fill="#fb722e"><path d="m6.226 220.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 155.43 59.22)"/><path d="m256.23 9.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 79.45-179.36)"/></g><path fill="#fee8dc" d="m312.78 150.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 194.69-178.47)"/><path fill="#6b4fbb" d="m43.778 80.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" opacity=".2" transform="matrix(.70711-.70711.70711.70711-40.761 53.15)"/></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="c" width="161" height="100" x="92" y="181" rx="10"/><rect id="d" width="151" height="32" x="20" rx="10"/><rect id="a" width="191" height="62" y="10" rx="10"/><circle id="b" cx="23" cy="41" r="9"/><circle id="k" cx="36.5" cy="36.5" r="36.5"/><circle id="e" cx="262.5" cy="169.5" r="15.5"/><circle id="g" cx="79.5" cy="169.5" r="15.5"/><circle id="j" cx="45" cy="41" r="9"/><circle id="f" cx="30.5" cy="30.5" r="30.5"/><circle id="h" cx="18" cy="34" r="3"/><ellipse id="i" cx="43.5" cy="43.5" rx="43.5" ry="43.5"/><mask id="t" width="191" height="62" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="u" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="r" width="161" height="100" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><mask id="s" width="151" height="32" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><mask id="p" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><mask id="l" width="61" height="61" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><mask id="q" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask><mask id="m" width="6" height="6" x="0" y="0" fill="#fff"><use xlink:href="#h"/></mask><mask id="o" width="87" height="87" x="0" y="0" fill="#fff"><use xlink:href="#i"/></mask><mask id="v" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#j"/></mask><mask id="n" width="73" height="73" x="0" y="0" fill="#fff"><use xlink:href="#k"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(28 2)"><g transform="translate(133 87)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#l)" xlink:href="#f"/><path stroke="#d2caea" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="M19 32l2-9 5 17 4-12 4 5 6-10 3 5"/><g fill="#fff" stroke="#fb722e"><use stroke-width="4" mask="url(#m)" xlink:href="#h"/><circle cx="44" cy="30" r="2" stroke-width="2"/></g></g><g transform="translate(188 29)"><circle cx="36.5" cy="41.5" r="36.5" fill="#f9f9f9"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#n)" xlink:href="#k"/><rect width="27" height="4" x="23" y="27" fill="#d2caea" rx="2"/><rect width="10.5" height="4" x="23" y="27" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="36" fill="#d2caea" rx="2"/><rect width="19" height="4" x="23" y="36" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="45" fill="#d2caea" rx="2"/><rect width="7" height="4" x="23" y="45" fill="#6b4fbb" rx="2"/></g><path fill="#eee" fill-rule="nonzero" d="M247 292v1c0 5.519-4.469 9.993-10.01 9.993H111c-5.177 0-9.436-3.927-9.954-8.96a9.96 9.96 0 0 0 4.705 1.883 6.008 6.008 0 0 0 5.248 3.077h125.99a6 6 0 0 0 5.526-3.637 10.027 10.027 0 0 0 4.48-3.359m1.947-8.962a10.001 10.001 0 0 1-9.95 8.958h-131.99a10 10 0 0 1-9.851-8.25 9.942 9.942 0 0 0 4.649 1.248 6 6 0 0 0 5.202 3h131.99a6.002 6.002 0 0 0 5.245-3.076 9.943 9.943 0 0 0 4.705-1.882"/><g transform="translate(79)"><ellipse cx="43.5" cy="47.5" fill="#f9f9f9" rx="43.5" ry="43.5"/><g fill="#fff"><g stroke="#eee"><use stroke-width="8" mask="url(#o)" xlink:href="#i"/><path stroke-width="4" d="M18.595 49C21.11 60.44 31.305 69 43.5 69 57.58 69 69 57.583 69 43.5c0-12.195-8.56-22.391-20-24.905v15.959c3 1.848 5 5.164 5 8.946C54 49.299 49.299 54 43.5 54c-3.782 0-7.098-2-8.946-5H18.595" stroke-linejoin="round"/></g><path stroke="#d2caea" stroke-width="4" d="M18 44a27.69 27.69 0 0 1-.005-.5c0-14.08 11.417-25.5 25.5-25.5.167 0 .334.002.5.005v15.01a10.365 10.365 0 0 0-.5-.012c-5.799 0-10.5 4.701-10.5 10.5 0 .168.004.334.012.5h-15.01" stroke-linejoin="round"/></g></g><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#p)" xlink:href="#e"/><use mask="url(#q)" xlink:href="#g"/><use mask="url(#r)" xlink:href="#c"/></g><g fill="#eee"><rect width="15" height="2" x="226" y="247" rx="1"/><rect width="15" height="2" x="226" y="242" rx="1"/><rect width="15" height="2" x="226" y="252" rx="1"/></g><rect width="10" height="52" x="118" y="196" fill="#d2caea" rx="2"/><rect width="10" height="47" x="154" y="196" fill="#6b4fbb" rx="2"/><rect width="10" height="37" x="190" y="196" fill="#d2caea" rx="2"/><g fill="#fee8dc"><rect width="10" height="52" x="132" y="185" rx="2"/><rect width="10" height="38" x="168" y="185" rx="2"/></g><rect width="10" height="58" x="204" y="185" fill="#fb722e" rx="2"/><g fill="#fff" stroke="#eee" stroke-width="8" transform="translate(76 128)"><use mask="url(#s)" xlink:href="#d"/><use mask="url(#t)" xlink:href="#a"/></g><g fill="#d2caea" transform="translate(76 128)"><rect width="16" height="4" x="156" y="35" rx="2"/><rect width="16" height="4" x="156" y="43" rx="2"/></g><g fill="#fff" stroke-width="8" transform="translate(76 128)"><use stroke="#fee8dc" mask="url(#u)" xlink:href="#b"/><use stroke="#fb722e" mask="url(#v)" xlink:href="#j"/></g><g fill="#fb722e"><path d="M3.597 219.858l-2.455-1.562c-.929-.59-.924-1.553 0-2.142l2.455-1.562 1.562-2.455c.59-.929 1.553-.924 2.142 0l1.562 2.455 2.454 1.562c.93.591.925 1.553 0 2.142l-2.454 1.562-1.562 2.455c-.591.929-1.553.924-2.142 0l-1.562-2.455M253.597 8.859l-2.454-1.562c-.93-.592-.925-1.554 0-2.143l2.454-1.562 1.562-2.454c.591-.93 1.554-.925 2.143 0l1.562 2.454 2.454 1.562c.93.591.924 1.554 0 2.143l-2.454 1.562-1.562 2.454c-.592.93-1.554.924-2.143 0l-1.562-2.454" opacity=".2"/></g><path fill="#fee8dc" d="M309.49 149.07l-3.141-1.999c-1.12-.712-1.128-1.863 0-2.58l3.14-2 2-3.14c.712-1.12 1.863-1.128 2.58 0l2 3.14 3.14 2c1.12.712 1.127 1.863 0 2.58l-3.14 2-2 3.14c-.713 1.12-1.863 1.128-2.58 0l-2-3.14"/><path fill="#6b4fbb" d="M47.068 79.067l-1.99 3.126c-.718 1.129-1.88 1.13-2.6 0l-1.99-3.126-3.125-1.99c-1.129-.718-1.131-1.88 0-2.6l3.126-1.989 1.989-3.126c.718-1.129 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.989" opacity=".2"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/monitoring/unable_to_connect.svg b/app/assets/images/illustrations/monitoring/unable_to_connect.svg
index 62537d87d5d..314c052f931 100644
--- a/app/assets/images/illustrations/monitoring/unable_to_connect.svg
+++ b/app/assets/images/illustrations/monitoring/unable_to_connect.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><use id="0" xlink:href="#E"/><use id="2" xlink:href="#E"/><use id="4" xlink:href="#E"/><path id="6" d="m74 93h26v47h-26z"/><path id="8" d="m74 93h26v47h-26z"/><rect id="A" width="65" height="14" x="55" y="135" rx="4"/><rect id="C" width="175" height="118" rx="10"/><rect id="E" width="159" rx="10" height="56"/><rect id="F" width="160" y="2" rx="10" height="56" fill="#f9f9f9"/><mask id="B" width="65" height="14" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="9" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="D" width="175" height="118" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="7" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="3" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="1" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="5" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(1 65)"><g transform="translate(244)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><g fill="#d2caea"><rect width="50" height="4" x="19" y="20" rx="2"/><rect width="50" height="4" x="19" y="34" rx="2"/></g><g transform="translate(0 59)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#fee8dc" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fb722e" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m100 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><rect width="50" height="4" x="19" y="19" fill="#d2caea" rx="2" id="G"/><rect width="50" height="4" x="19" y="33" fill="#d2caea" rx="2" id="H"/></g><g transform="translate(0 118)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><use xlink:href="#G"/><use xlink:href="#H"/></g></g><g transform="translate(163 55)"><g fill="#eee"><rect width="29" height="4" y="29" rx="2"/><rect width="28" height="4" x="55" y="29" rx="2"/></g><g transform="translate(16)"><circle cx="30" cy="30" r="24" fill="#fef0ea"/><g fill="#fb722e"><circle cx="30.5" cy="30.5" r="30.5" opacity=".1"/><circle cx="30.5" cy="30.5" r="19.5" opacity=".1"/></g><circle cx="30.5" cy="30.5" r="13.5" fill="#fff"/><path fill="#fb722e" d="m32.621 30.5l2.481-2.481c.586-.586.58-1.529-.006-2.115-.59-.59-1.533-.589-2.115-.006l-2.481 2.481-2.481-2.481c-.586-.586-1.529-.58-2.115.006-.59.59-.589 1.533-.006 2.115l2.481 2.481-2.481 2.481c-.586.586-.58 1.529.006 2.115.59.59 1.533.589 2.115.006l2.481-2.481 2.481 2.481c.586.586 1.529.58 2.115-.006.59-.59.589-1.533.006-2.115l-2.481-2.481"/></g></g><g transform="translate(0 13)"><rect width="65" height="14" x="55" y="137" fill="#f9f9f9" rx="4"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#7)" xlink:href="#6"/><rect width="175" height="118" y="3" fill="#f9f9f9" rx="10"/><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill-rule="nonzero"><path fill="#eee" d="m163 105v-93h-152v93h152m-156-93.01c0-2.204 1.797-3.99 3.995-3.99h152.01c2.206 0 3.995 1.796 3.995 3.99v93.02c0 2.204-1.797 3.99-3.995 3.99h-152.01c-2.206 0-3.995-1.796-3.995-3.99v-93.02"/><path fill="#d2caea" d="m86 92c-11.598 0-21-9.402-21-21 0-11.598 9.402-21 21-21 11.598 0 21 9.402 21 21 0 11.598-9.402 21-21 21m0-4c9.389 0 17-7.611 17-17 0-9.389-7.611-17-17-17-9.389 0-17 7.611-17 17 0 9.389 7.611 17 17 17"/></g><path fill="#6b4fbb" d="m83 63c0-1.659 1.347-3 3-3 1.657 0 3 1.342 3 3v7.993c0 1.659-1.347 3-3 3-1.657 0-3-1.342-3-3v-7.993m3 18.997c-1.657 0-3-1.343-3-3 0-1.657 1.343-3 3-3 1.657 0 3 1.343 3 3 0 1.657-1.343 3-3 3"/><g fill="#eee"><rect width="134" height="4" x="20" y="30" rx="2"/><rect width="14" height="4" x="20" y="20" rx="2"/><circle cx="87" cy="21" r="5"/></g></g></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><use id="g" xlink:href="#a"/><use id="f" xlink:href="#a"/><use id="h" xlink:href="#a"/><path id="e" d="M74 93h26v47H74z"/><path id="c" d="M74 93h26v47H74z"/><rect id="b" width="65" height="14" x="55" y="135" rx="4"/><rect id="d" width="175" height="118" rx="10"/><rect id="a" width="159" rx="10" height="56"/><rect id="i" width="160" y="2" rx="10" height="56" fill="#f9f9f9"/><mask id="q" width="65" height="14" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="p" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><mask id="r" width="175" height="118" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><mask id="o" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><mask id="k" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><mask id="j" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask><mask id="l" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#h"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="translate(245 65)"><use xlink:href="#i"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#j)" xlink:href="#g"/><g fill-rule="nonzero"><path fill="#fb722e" d="M134 31a2 2 0 1 0 .001-3.999A2 2 0 0 0 134 31m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fee8dc" d="M117 31a2 2 0 1 0 .001-3.999A2 2 0 0 0 117 31m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12m-17-4a2 2 0 1 0 .001-3.999A2 2 0 0 0 100 31m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/></g><g fill="#d2caea"><rect width="50" height="4" x="19" y="20" rx="2"/><rect width="50" height="4" x="19" y="34" rx="2"/></g><g transform="translate(0 59)"><use xlink:href="#i"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#k)" xlink:href="#f"/><g fill-rule="nonzero"><path fill="#fee8dc" d="M134 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 134 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fb722e" d="M117 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 117 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fee8dc" d="M100 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 100 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/></g><rect width="50" height="4" x="19" y="19" fill="#d2caea" rx="2" id="m"/><rect width="50" height="4" x="19" y="33" fill="#d2caea" rx="2" id="n"/></g><g transform="translate(0 118)"><use xlink:href="#i"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#l)" xlink:href="#h"/><g fill-rule="nonzero"><path fill="#fb722e" d="M134 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 134 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fee8dc" d="M117 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 117 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12m-17-4a2 2 0 1 0 .001-3.999A2 2 0 0 0 100 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/></g><use xlink:href="#m"/><use xlink:href="#n"/></g></g><g fill="#eee" transform="translate(164 120)"><rect width="29" height="4" y="29" rx="2"/><rect width="28" height="4" x="55" y="29" rx="2"/></g><g transform="translate(180 120)"><circle cx="30" cy="30" r="24" fill="#fef0ea"/><g fill="#fb722e"><circle cx="30.5" cy="30.5" r="30.5" opacity=".1"/><circle cx="30.5" cy="30.5" r="19.5" opacity=".1"/></g><circle cx="30.5" cy="30.5" r="13.5" fill="#fff"/><path fill="#fb722e" d="M32.621 30.5l2.481-2.481a1.492 1.492 0 0 0-.006-2.115 1.491 1.491 0 0 0-2.115-.006L30.5 28.379l-2.481-2.481a1.492 1.492 0 0 0-2.115.006 1.491 1.491 0 0 0-.006 2.115l2.481 2.481-2.481 2.481a1.492 1.492 0 0 0 .006 2.115c.59.59 1.533.589 2.115.006l2.481-2.481 2.481 2.481c.586.586 1.529.58 2.115-.006.59-.59.589-1.533.006-2.115L32.621 30.5"/></g><g transform="translate(1 78)"><rect width="65" height="14" x="55" y="137" fill="#f9f9f9" rx="4"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#o)" xlink:href="#e"/><rect width="175" height="118" y="3" fill="#f9f9f9" rx="10"/><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#p)" xlink:href="#c"/><use mask="url(#q)" xlink:href="#b"/><use mask="url(#r)" xlink:href="#d"/></g><g fill-rule="nonzero"><path fill="#eee" d="M163 105V12H11v93h152M7 11.99A3.998 3.998 0 0 1 10.995 8h152.01A3.999 3.999 0 0 1 167 11.99v93.02a3.998 3.998 0 0 1-3.995 3.99H10.995A3.999 3.999 0 0 1 7 105.01V11.99"/><path fill="#d2caea" d="M86 92c-11.598 0-21-9.402-21-21s9.402-21 21-21 21 9.402 21 21-9.402 21-21 21m0-4c9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17 7.611 17 17 17"/></g><path fill="#6b4fbb" d="M83 63a3.001 3.001 0 0 1 6 0v7.993a3.001 3.001 0 0 1-6 0V63m3 18.997a3 3 0 1 1 0-6 3 3 0 0 1 0 6"/><g fill="#eee"><rect width="134" height="4" x="20" y="30" rx="2"/><rect width="14" height="4" x="20" y="20" rx="2"/><circle cx="87" cy="21" r="5"/></g></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/no_commits.svg b/app/assets/images/illustrations/no_commits.svg
new file mode 100644
index 00000000000..76fa25156dd
--- /dev/null
+++ b/app/assets/images/illustrations/no_commits.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="M4.01 2h1.102a1 1 0 0 0 0-2H4.01A4.001 4.001 0 0 0 0 4a1 1 0 0 0 2 0c0-1.108.892-2 2.01-2m12.702 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7M164 2c.822 0 1.554.503 1.86 1.254a1 1 0 1 0 1.853-.753 4.01 4.01 0 0 0-3.712-2.5h-2.188a1 1 0 0 0 0 2h2.188m2.01 12.518a1 1 0 0 0 2 0v-5.7a1 1 0 0 0-2 0v5.7m0 11.6a1 1 0 0 0 2 0v-5.7a1 1 0 0 0-2 0v5.7m0 11.6a1 1 0 0 0 2 0v-5.7a1 1 0 0 0-2 0v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72a1 1 0 0 0 0 2h.72a4.001 4.001 0 0 0 4.01-4v-.382a1 1 0 0 0-2 0v.382m-14.325 2a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-8.47 0a2.01 2.01 0 0 1-1.782-1.085 1 1 0 0 0-1.775.923 4.007 4.007 0 0 0 3.556 2.162h2.57a1 1 0 0 0 0-2h-2.57m-2.01-12.136a1 1 0 0 0-2 0v5.7a1 1 0 0 0 2 0v-5.7m0-11.6a1 1 0 0 0-2 0v5.7a1 1 0 0 0 2 0v-5.7m0-11.6a1 1 0 0 0-2 0v5.7a1 1 0 0 0 2 0v-5.7m0-6.664a1 1 0 0 0-2 0v.764a1 1 0 0 0 2 0v-.764" id="a"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="b"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="c"/><path d="M131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9a.998.998 0 0 0-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01a2.998 2.998 0 0 1 2.996 2.999v9a3.003 3.003 0 0 1-2.996 2.999h-22.01A2.998 2.998 0 0 1 129 28.999v-9A3.003 3.003 0 0 1 131.996 17" id="d"/><g transform="translate(0 59)"><use xlink:href="#a"/><circle cx="21" cy="24" r="10"/><use xlink:href="#b"/><use xlink:href="#c"/><use xlink:href="#d"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/add_new_group.svg b/app/assets/images/illustrations/welcome/add_new_group.svg
new file mode 100644
index 00000000000..b10a3ae8812
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/add_new_group.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="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><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="#E1DBF2" fill-rule="nonzero" d="M59.65 32.65H60l-2-2.42-2 2.4-2-2.4-2 2.4-2-2.4-2 2.4-2-2.4-2 2.42h.77C45.57 34.6 46 36.75 46 39c0 2.84-.7 5.5-1.92 7.86 1.97 2.28 4.83 3.64 7.92 3.64 5.8 0 10.5-4.74 10.5-10.6 0-2.8-1.08-5.36-2.85-7.25zM43.18 29.6c2.4-2.1 5.52-3.3 8.82-3.3 7.46 0 13.5 6.1 13.5 13.6S59.46 53.5 52 53.5c-3.68 0-7.1-1.5-9.6-4.04C39.3 53.44 34.44 56 29 56c-9.4 0-17-7.6-17-17s7.6-17 17-17c3.22 0 6.23.9 8.8 2.45 2.13 1.3 3.97 3.05 5.38 5.16zM17 34c-.65 1.54-1 3.23-1 5 0 7.18 5.82 13 13 13s13-5.82 13-13c0-1.77-.35-3.46-1-5h-9c-.53 0-1.04-.2-1.4-.6L29 31.84l-1.6 1.58c-.36.4-.87.6-1.4.6h-9zm21.38-4a12.996 12.996 0 0 0-18.76 0h5.55l2.42-2.4c.74-.8 2-.8 2.8 0l2.4 2.4h5.54z"/><path fill="#6B4FBB" d="M47.6 42.32c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zm8.8 0c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zM25 44h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-1c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/add_new_project.svg b/app/assets/images/illustrations/welcome/add_new_project.svg
new file mode 100644
index 00000000000..4b8dc34c088
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/add_new_project.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 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><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="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#6B4FBB" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/add_new_user.svg b/app/assets/images/illustrations/welcome/add_new_user.svg
new file mode 100644
index 00000000000..d4c184989bf
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/add_new_user.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="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><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="#E1DBF2" d="M44 31l-2.5-3-2.5 3-2.5-3-2.5 3-2.5-3-2.5 3h-2.72c2.65-4.2 7.36-7 12.72-7s10.07 2.8 12.72 7H49l-2.5-3-2.5 3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M39 57c-9.4 0-17-7.6-17-17s7.6-17 17-17 17 7.6 17 17-7.6 17-17 17zm0-4c7.18 0 13-5.82 13-13s-5.82-13-13-13-13 5.82-13 13 5.82 13 13 13z"/><path fill="#6B4FBB" d="M35 45h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/configure_server.svg b/app/assets/images/illustrations/welcome/configure_server.svg
new file mode 100644
index 00000000000..f9dda816f11
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/configure_server.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="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><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="#FEE1D3" fill-rule="nonzero" d="M24.92 35.15a4.012 4.012 0 0 1-.6-5.63l1.26-1.55c1.4-1.72 3.9-2 5.63-.6l.7.56c.7-.4 1.4-.73 2.1-1V26c0-2.2 1.8-4 4-4h2c2.2 0 4 1.8 4 4v.92c.8.28 1.5.62 2.1 1l.7-.55c1.7-1.4 4.3-1.12 5.7.6l1.3 1.55c1.4 1.72 1.2 4.23-.6 5.63l-.7.6c.3.74.4 1.5.5 2.3l.9.2c2.2.5 3.5 2.64 3 4.8L56.4 45c-.5 2.15-2.64 3.5-4.8 3l-.88-.2c-.44.63-.92 1.24-1.46 1.8l.4.82c.9 1.98.1 4.38-1.9 5.35l-1.8.87c-2 .97-4.37.15-5.34-1.84l-.46-.85c-.34.03-.74.05-1.13.05-.4 0-.8-.02-1.2-.05l-.4.85c-.95 2-3.34 2.8-5.33 1.84l-1.8-.87a4.011 4.011 0 0 1-1.83-5.35l.4-.8c-.54-.58-1.02-1.2-1.46-1.83l-.8.2c-2.2.5-4.3-.9-4.8-3l-.4-2c-.5-2.2.85-4.3 3-4.8l.9-.2c.1-.8.3-1.6.5-2.3l-.7-.6zm4.95.77c-.53 1.2-.83 2.47-.87 3.8-.02.9-.66 1.68-1.55 1.9l-2.32.53.45 1.94 2.3-.6c.9-.2 1.8.2 2.23 1 .7 1.1 1.5 2.2 2.5 3 .7.6.9 1.6.5 2.4l-1 2.1 1.8.9 1.1-2.1c.4-.8 1.3-1.3 2.2-1.1.7.1 1.3.2 2 .2s1.3-.1 2-.2c.9-.2 1.8.3 2.2 1.1l1 2.1 1.8-.9-1.2-2c-.4-.8-.2-1.8.5-2.4 1-.85 1.84-1.88 2.45-3.05.4-.82 1.33-1.24 2.2-1.04l2.33.54.45-1.95-2.32-.54c-.9-.2-1.52-.97-1.54-1.88-.03-1.4-.33-2.6-.86-3.8-.4-.9-.2-1.8.5-2.4l1.9-1.5-1.3-1.6-1.8 1.5c-.8.5-1.8.6-2.5 0-1.1-.8-2.3-1.4-3.5-1.7-.9-.2-1.5-1-1.5-1.9V26h-2v2.38c0 .9-.6 1.7-1.5 1.93-1.3.4-2.5 1-3.5 1.7-.8.6-1.8.6-2.5 0l-1.9-1.5-1.26 1.6 1.8 1.5c.7.6.94 1.6.6 2.4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M39 46c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/ee_trial.svg b/app/assets/images/illustrations/welcome/ee_trial.svg
new file mode 100644
index 00000000000..6d0dcf0020c
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/ee_trial.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="330" height="132" viewBox="0 0 330 132"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M174.12 42c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M211 78c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S230.33 4 211 4s-35 15.67-35 35 15.67 35 35 35z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M211.5 51c-6.42 0-12.26-2.84-17.43-8.4a4.008 4.008 0 0 1-.27-5.13C199 30.57 204.92 27 211.5 27s12.5 3.56 17.7 10.47a3.994 3.994 0 0 1-.27 5.12c-5.17 5.53-11 8.4-17.43 8.4zm0-4c5.25 0 10.05-2.34 14.5-7.13-4.5-5.98-9.3-8.87-14.5-8.87-5.2 0-10 2.9-14.5 8.87 4.45 4.8 9.25 7.13 14.5 7.13z"/><path fill="#FC6D26" d="M211 47c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-4c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0-1c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></g><path fill="#000" fill-opacity=".03" d="M88.12 83c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M125 119c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M116 86.34c2.33.83 4 3.05 4 5.66 0 3.3-2.7 6-6 6s-6-2.7-6-6c0-2.6 1.67-4.83 4-5.66V72h4v14.34zM128 66c5.52 0 10 4.48 10 10v12h-4V76c0-3.3-2.7-6-6-6v1.83c0 .55-.45 1-1 1-.24 0-.47-.1-.65-.24l-4.46-3.87c-.46-.36-.5-1-.15-1.4.03-.05.07-.1.1-.12l4.47-3.82c.42-.35 1.05-.3 1.4.1.16.2.25.43.25.66V66zm-14 28c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#FC6D26" fill-rule="nonzero" d="M114 74c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm22 28c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#000" fill-opacity=".03" d="M2.12 52C2.04 53 2 54 2 55c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 71.03 58.42 86 39 86S3.65 71.03 2.12 52z"/><path fill="#EEE" fill-rule="nonzero" d="M39 88C17.46 88 0 70.54 0 49s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 14 39 14 4 29.67 4 49s15.67 35 35 35z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M48 41h-4c0-2.76-2.24-5-5-5s-5 2.24-5 5h-4a9 9 0 0 1 18 0zm-18 0h4v3h-4v-3zm14 0h4v3h-4v-3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 47c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V48c0-.55-.45-1-1-1H30zm0-4h18c2.76 0 5 2.24 5 5v12c0 2.76-2.24 5-5 5H30c-2.76 0-5-2.24-5-5V48c0-2.76 2.24-5 5-5z"/><path fill="#6B4FBB" d="M38 53.73c-.6-.34-1-1-1-1.73 0-1.1.9-2 2-2s2 .9 2 2c0 .74-.4 1.4-1 1.73V55c0 .55-.45 1-1 1s-1-.45-1-1v-1.27z"/><path fill="#000" fill-opacity=".03" d="M254.12 92c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M291 128c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#6B4BBE" fill-rule="nonzero" d="M292 78c5.52 0 10 4.48 10 10 0 2.28-.76 4.43-2.14 6.18-1.03 1.3-.8 3.2.5 4.22 1.3 1.02 3.2.8 4.2-.5 2.22-2.8 3.44-6.26 3.44-9.9 0-8.84-7.16-16-16-16v-3.13c0-.2-.06-.4-.17-.56-.3-.42-.93-.54-1.38-.23l-9.2 6.13c-.1.06-.2.16-.28.27-.3.45-.18 1.08.28 1.38l9.2 6.13c.16.1.35.17.55.17.55 0 1-.45 1-1V78z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M290 100c-5.52 0-10-4.48-10-10 0-2.25.74-4.38 2.1-6.12 1-1.3.77-3.2-.54-4.2-1.3-1.02-3.2-.78-4.2.53A15.796 15.796 0 0 0 274 90c0 8.84 7.16 16 16 16v3.13c0 .55.45 1 1 1 .2 0 .4-.06.55-.17l9.2-6.13c.46-.3.6-.93.28-1.38-.07-.1-.17-.2-.28-.28l-9.2-6.13c-.45-.3-1.08-.2-1.38.27-.1.2-.17.4-.17.6v3.1z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/globe.svg b/app/assets/images/illustrations/welcome/globe.svg
new file mode 100644
index 00000000000..c2daae5f317
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/globe.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 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><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="#E1DBF2" d="M30.24 27.823A14.98 14.98 0 0 0 24 40c0 2.549.636 4.949 1.757 7.051-.297-2.684.644-4.026 2.823-4.026 3.707 0 2.462 5.365 4.473 5.761 2.01.396 4.175.396 4.267 3.29.04 1.257-.265 2.157-.917 2.7a15.095 15.095 0 0 0 8.555-1.006c.035-1.91.303-4.941 2.21-5.61 2.373-.833-.55-1.431.734-3.368 1.17-1.762-3.297-5.2 0-4.832 3.477.388 5.044-.816 6.024-1.456a14.903 14.903 0 0 0-1.373-4.94c-.873.4-2.19.465-3.702-.538-.757-.502-1.084-3.944-2.107-3.944-3.823 0-4.065 3.17-5.994 3.944-1.076.431-4.193 3.773-5.614 3.596-1.126-.14-1.071-4.417-2.45-5.166-1.359-.738-2.174-1.948-2.447-3.633zM39 59c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/lightbulb.svg b/app/assets/images/illustrations/welcome/lightbulb.svg
new file mode 100644
index 00000000000..fce10312085
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/lightbulb.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 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><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="#6B4FBB" d="M33 52h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm1 5h10a2 2 0 1 1 0 4H34a2 2 0 1 1 0-4z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36a8.004 8.004 0 0 1 1.566-3.705c3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846a8.009 8.009 0 0 1 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1a3.997 3.997 0 0 0-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3a3.99 3.99 0 0 0-.784 1.853l-.346 2.36a4.003 4.003 0 0 1-3.942 3.42l-13.08.053a4 4 0 0 1-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268zm-6 0a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
new file mode 100644
index 00000000000..cdea625fc8c
--- /dev/null
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -0,0 +1,73 @@
+import Clipboard from 'clipboard';
+
+function showTooltip(target, title) {
+ const $target = $(target);
+ const originalTitle = $target.data('original-title');
+
+ if (!$target.data('hideTooltip')) {
+ $target
+ .attr('title', title)
+ .tooltip('fixTitle')
+ .tooltip('show')
+ .attr('title', originalTitle)
+ .tooltip('fixTitle');
+ }
+}
+
+function genericSuccess(e) {
+ showTooltip(e.trigger, 'Copied');
+ // Clear the selection and blur the trigger so it loses its border
+ e.clearSelection();
+ $(e.trigger).blur();
+}
+
+/**
+ * Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
+ * See http://clipboardjs.com/#browser-support
+ */
+function genericError(e) {
+ let key;
+ if (/Mac/i.test(navigator.userAgent)) {
+ key = '&#8984;'; // Command
+ } else {
+ key = 'Ctrl';
+ }
+ showTooltip(e.trigger, `Press ${key}-C to copy`);
+}
+
+export default function initCopyToClipboard() {
+ const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
+ clipboard.on('success', genericSuccess);
+ clipboard.on('error', genericError);
+
+ /**
+ * This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting
+ * of plain text or GFM. The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and
+ * `gfm` keys into the `data-clipboard-text` attribute that ClipboardJS reads from.
+ * When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly`
+ * attribute`), sets its value to the value of this data attribute, focusses on it, and finally
+ * programmatically issues the 'Copy' command, this code intercepts the copy command/event at
+ * the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy
+ * data types to the intended values.
+ */
+ $(document).on('copy', 'body > textarea[readonly]', (e) => {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
+
+ const text = e.target.value;
+
+ let json;
+ try {
+ json = JSON.parse(text);
+ } catch (ex) {
+ return;
+ }
+
+ if (!json.text || !json.gfm) return;
+
+ e.preventDefault();
+
+ clipboardData.setData('text/plain', json.text);
+ clipboardData.setData('text/x-gfm', json.gfm);
+ });
+}
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 671532394a9..34e905222b4 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,6 +1,7 @@
import './autosize';
import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm';
+import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
@@ -9,3 +10,4 @@ import './toggler_behavior';
installGlEmojiElement();
initCopyAsGFM();
+initCopyToClipboard();
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index b5500ac116f..6b06344f5ba 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,7 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
/* global EditBlob */
-/* global NewCommitForm */
-
+import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone';
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index ef4093b59e3..20d23162940 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,12 +1,13 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
-/* global BoardService */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
+import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
+import sidebarEventHub from '../sidebar/event_hub';
import './models/issue';
import './models/label';
import './models/list';
@@ -14,7 +15,7 @@ import './models/milestone';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
-import './services/board_service';
+import BoardService from './services/board_service';
import './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
@@ -77,11 +78,16 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
- // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
+ eventHub.$on('newDetailIssue', this.updateDetailIssue);
+ eventHub.$on('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
+ eventHub.$off('newDetailIssue', this.updateDetailIssue);
+ eventHub.$off('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
@@ -112,6 +118,46 @@ $(() => {
methods: {
updateTokens() {
this.filterManager.updateTokens();
+ },
+ updateDetailIssue(newIssue) {
+ const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
+ if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
+ newIssue.setFetchingState('subscriptions', true);
+ BoardService.getIssueInfo(sidebarInfoEndpoint)
+ .then(res => res.json())
+ .then((data) => {
+ newIssue.setFetchingState('subscriptions', false);
+ newIssue.updateData({
+ subscribed: data.subscribed,
+ });
+ })
+ .catch(() => {
+ newIssue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred while fetching sidebar data'));
+ });
+ }
+
+ Store.detail.issue = newIssue;
+ },
+ clearDetailIssue() {
+ Store.detail.issue = {};
+ },
+ toggleSubscription(id) {
+ const issue = Store.detail.issue;
+ if (issue.id === id && issue.toggleSubscriptionEndpoint) {
+ issue.setFetchingState('subscriptions', true);
+ BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
+ .then(() => {
+ issue.setFetchingState('subscriptions', false);
+ issue.updateData({
+ subscribed: !issue.subscribed,
+ });
+ })
+ .catch(() => {
+ issue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred when toggling the notification subscription'));
+ });
+ }
}
},
});
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.vue
index 079fb6438b9..0b220a56e0b 100644
--- a/app/assets/javascripts/boards/components/board_card.js
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,25 +1,11 @@
+<script>
import './issue_card_inner';
+import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardsIssueCard',
- template: `
- <li class="card"
- :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
- :index="index"
- :data-issue-id="issue.id"
- @mousedown="mouseDown"
- @mousemove="mouseMove"
- @mouseup="showIssue($event)">
- <issue-card-inner
- :list="list"
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :update-filters="true" />
- </li>
- `,
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
@@ -56,12 +42,30 @@ export default {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
- Store.detail.issue = {};
+ eventHub.$emit('clearDetailIssue');
} else {
- Store.detail.issue = this.issue;
+ eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list;
}
}
},
},
};
+</script>
+
+<template>
+ <li class="card"
+ :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
+ :index="index"
+ :data-issue-id="issue.id"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)">
+ <issue-card-inner
+ :list="list"
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 6159680f1e6..29aeb8e84aa 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -1,6 +1,6 @@
/* global Sortable */
import boardNewIssue from './board_new_issue';
-import boardCard from './board_card';
+import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 9ae5e270a4b..faa76da964f 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -5,12 +5,13 @@
import Vue from 'vue';
import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
-import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
-import Assignees from '../../sidebar/components/assignees/assignees';
+import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
+import assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
+import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore;
@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
- gl.Subscription.bindAll('.subscription');
},
components: {
+ assigneeTitle,
+ assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
- 'assignee-title': AssigneeTitle,
- assignees: Assignees,
+ subscriptions,
},
});
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 407db176446..81edd95bf2b 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -17,6 +17,12 @@ class ListIssue {
this.assignees = [];
this.selected = false;
this.position = obj.relative_position || Infinity;
+ this.isFetching = {
+ subscriptions: true,
+ };
+ this.isLoading = {};
+ this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
+ this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
@@ -73,6 +79,18 @@ class ListIssue {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
+ updateData(newData) {
+ Object.assign(this, newData);
+ }
+
+ setFetchingState(key, value) {
+ this.isFetching[key] = value;
+ }
+
+ setLoadingState(key, value) {
+ this.isLoading[key] = value;
+ }
+
update (url) {
const data = {
issue: {
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 97e80afa3f8..fa7ddd25e1f 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -2,7 +2,7 @@
import Vue from 'vue';
-class BoardService {
+export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
@@ -88,6 +88,14 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
+
+ static getIssueInfo(endpoint) {
+ return Vue.http.get(endpoint);
+ }
+
+ static toggleIssueSubscription(endpoint) {
+ return Vue.http.post(endpoint);
+ }
}
window.BoardService = BoardService;
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index dc443475952..2cfd6179a25 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -48,6 +48,7 @@ export default class Clusters {
this.toggle = this.toggle.bind(this);
this.installApplication = this.installApplication.bind(this);
+ this.showToken = this.showToken.bind(this);
this.toggleButton = document.querySelector('.js-toggle-cluster');
this.toggleInput = document.querySelector('.js-toggle-input');
@@ -56,6 +57,8 @@ export default class Clusters {
this.creatingContainer = document.querySelector('.js-cluster-creating');
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
+ this.showTokenButton = document.querySelector('.js-show-cluster-token');
+ this.tokenField = document.querySelector('.js-cluster-token');
initSettingsPanels();
this.initApplications();
@@ -97,11 +100,13 @@ export default class Clusters {
addListeners() {
this.toggleButton.addEventListener('click', this.toggle);
+ if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
}
removeListeners() {
this.toggleButton.removeEventListener('click', this.toggle);
+ if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
}
@@ -145,8 +150,18 @@ export default class Clusters {
}
toggle() {
- this.toggleButton.classList.toggle('checked');
- this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
+ this.toggleButton.classList.toggle('is-checked');
+ this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString());
+ }
+
+ showToken() {
+ const type = this.tokenField.getAttribute('type');
+
+ if (type === 'password') {
+ this.tokenField.setAttribute('type', 'text');
+ } else {
+ this.tokenField.setAttribute('type', 'password');
+ }
}
hideAll() {
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
new file mode 100644
index 00000000000..6844d1dbd83
--- /dev/null
+++ b/app/assets/javascripts/clusters/clusters_index.js
@@ -0,0 +1,58 @@
+import Flash from '../flash';
+import { s__ } from '../locale';
+import ClustersService from './services/clusters_service';
+/**
+ * Toggles loading and disabled classes.
+ * @param {HTMLElement} button
+ */
+const toggleLoadingButton = (button) => {
+ if (button.getAttribute('disabled')) {
+ button.removeAttribute('disabled');
+ } else {
+ button.setAttribute('disabled', true);
+ }
+
+ button.classList.toggle('is-loading');
+};
+
+/**
+ * Toggles checked class for the given button
+ * @param {HTMLElement} button
+ */
+const toggleValue = (button) => {
+ button.classList.toggle('is-checked');
+};
+
+/**
+ * Handles toggle buttons in the cluster's table.
+ *
+ * When the user clicks the toggle button for each cluster, it:
+ * - toggles the button
+ * - shows a loading and disables button
+ * - Makes a put request to the given endpoint
+ * Once we receive the response, either:
+ * 1) Show updated status in case of successfull response
+ * 2) Show initial status in case of failed response
+ */
+export default function setClusterTableToggles() {
+ document.querySelectorAll('.js-toggle-cluster-list')
+ .forEach(button => button.addEventListener('click', (e) => {
+ const toggleButton = e.currentTarget;
+ const endpoint = toggleButton.getAttribute('data-endpoint');
+
+ toggleValue(toggleButton);
+ toggleLoadingButton(toggleButton);
+
+ const value = toggleButton.classList.contains('is-checked');
+
+ ClustersService.updateCluster(endpoint, { cluster: { enabled: value } })
+ .then(() => {
+ toggleLoadingButton(toggleButton);
+ })
+ .catch(() => {
+ toggleLoadingButton(toggleButton);
+ toggleValue(toggleButton);
+ Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ });
+ }));
+}
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index 0ac8e68187d..755c2981c2e 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -1,10 +1,7 @@
-import axios from 'axios';
-import setAxiosCsrfToken from '../../lib/utils/axios_utils';
+import axios from '../../lib/utils/axios_utils';
export default class ClusterService {
constructor(options = {}) {
- setAxiosCsrfToken();
-
this.options = options;
this.appInstallEndpointMap = {
helm: this.options.installHelmEndpoint,
@@ -18,7 +15,10 @@ export default class ClusterService {
}
installApplication(appId) {
- const endpoint = this.appInstallEndpointMap[appId];
- return axios.post(endpoint);
+ return axios.post(this.appInstallEndpointMap[appId]);
+ }
+
+ static updateCluster(endpoint, data) {
+ return axios.put(endpoint, data);
}
}
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index 768453b28f1..0d2fe2925d8 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -3,3 +3,4 @@ import './polyfills';
import './jquery';
import './bootstrap';
import './vue';
+import '../lib/utils/axios_utils';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index cb5a9a9f6b5..ff9e4485916 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -10,6 +10,7 @@ import 'core-js/fn/string/from-code-point';
import 'core-js/fn/symbol';
// Browser polyfills
+import 'classlist-polyfill';
import './polyfills/custom_event';
import './polyfills/element';
import './polyfills/event';
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
deleted file mode 100644
index 1f3c7e1772d..00000000000
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
-
-import Clipboard from 'vendor/clipboard';
-
-var genericError, genericSuccess, showTooltip;
-
-genericSuccess = function(e) {
- showTooltip(e.trigger, 'Copied');
- // Clear the selection and blur the trigger so it loses its border
- e.clearSelection();
- return $(e.trigger).blur();
-};
-
-// Safari doesn't support `execCommand`, so instead we inform the user to
-// copy manually.
-//
-// See http://clipboardjs.com/#browser-support
-genericError = function(e) {
- var key;
- if (/Mac/i.test(navigator.userAgent)) {
- key = '&#8984;'; // Command
- } else {
- key = 'Ctrl';
- }
- return showTooltip(e.trigger, "Press " + key + "-C to copy");
-};
-
-showTooltip = function(target, title) {
- var $target = $(target);
- var originalTitle = $target.data('original-title');
-
- if (!$target.data('hideTooltip')) {
- $target
- .attr('title', 'Copied')
- .tooltip('fixTitle')
- .tooltip('show')
- .attr('title', originalTitle)
- .tooltip('fixTitle');
- }
-};
-
-$(function() {
- const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
- clipboard.on('success', genericSuccess);
- clipboard.on('error', genericError);
-
- // This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
- // The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and `gfm` keys into the `data-clipboard-text`
- // attribute that ClipboardJS reads from.
- // When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly` attribute`), sets its value
- // to the value of this data attribute, focusses on it, and finally programmatically issues the 'Copy' command,
- // this code intercepts the copy command/event at the last minute to deconstruct this JSON hash and set the
- // `text/plain` and `text/x-gfm` copy data types to the intended values.
- $(document).on('copy', 'body > textarea[readonly]', function(e) {
- const clipboardData = e.originalEvent.clipboardData;
- if (!clipboardData) return;
-
- const text = e.target.value;
-
- let json;
- try {
- json = JSON.parse(text);
- } catch (ex) {
- return;
- }
-
- if (!json.text || !json.gfm) return;
-
- e.preventDefault();
-
- clipboardData.setData('text/plain', json.text);
- clipboardData.setData('text/x-gfm', json.gfm);
- });
-});
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index bf40eb3ee11..23425672b16 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -2,6 +2,7 @@
import Flash from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
+import { __, sprintf } from './locale';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
@@ -12,28 +13,49 @@ const CREATE_BRANCH = 'create-branch';
export default class CreateMergeRequestDropdown {
constructor(wrapperEl) {
this.wrapperEl = wrapperEl;
+ this.availableButton = this.wrapperEl.querySelector('.available');
+ this.branchInput = this.wrapperEl.querySelector('.js-branch-name');
+ this.branchMessage = this.wrapperEl.querySelector('.js-branch-message');
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
- this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
+ this.createTargetButton = this.wrapperEl.querySelector('.js-create-target');
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
- this.availableButton = this.wrapperEl.querySelector('.available');
+ this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
+ this.refInput = this.wrapperEl.querySelector('.js-ref');
+ this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
this.unavailableButtonArrow = this.unavailableButton.querySelector('.fa');
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
- this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
+ this.branchCreated = false;
+ this.branchIsValid = true;
this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
+ this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
this.createMrPath = this.wrapperEl.dataset.createMrPath;
this.droplabInitialized = false;
+ this.isCreatingBranch = false;
this.isCreatingMergeRequest = false;
+ this.isGettingRef = false;
this.mergeRequestCreated = false;
- this.isCreatingBranch = false;
- this.branchCreated = false;
+ this.refDebounce = _.debounce((value, target) => this.getRef(value, target), 500);
+ this.refIsValid = true;
+ this.refsPath = this.wrapperEl.dataset.refsPath;
+ this.suggestedRef = this.refInput.value;
- this.init();
- }
+ // These regexps are used to replace
+ // a backend generated new branch name and its source (ref)
+ // with user's inputs.
+ this.regexps = {
+ branch: {
+ createBranchPath: new RegExp('(branch_name=)(.+?)(?=&issue)'),
+ createMrPath: new RegExp('(branch_name=)(.+?)(?=&ref)'),
+ },
+ ref: {
+ createBranchPath: new RegExp('(ref=)(.+?)$'),
+ createMrPath: new RegExp('(ref=)(.+?)$'),
+ },
+ };
- init() {
- this.checkAbilityToCreateBranch();
+ this.init();
}
available() {
@@ -41,41 +63,13 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.add('hide');
}
- unavailable() {
- this.availableButton.classList.add('hide');
- this.unavailableButton.classList.remove('hide');
- }
-
- enable() {
- this.createMergeRequestButton.classList.remove('disabled');
- this.createMergeRequestButton.removeAttribute('disabled');
-
- this.dropdownToggle.classList.remove('disabled');
- this.dropdownToggle.removeAttribute('disabled');
- }
-
- disable() {
- this.createMergeRequestButton.classList.add('disabled');
- this.createMergeRequestButton.setAttribute('disabled', 'disabled');
-
- this.dropdownToggle.classList.add('disabled');
- this.dropdownToggle.setAttribute('disabled', 'disabled');
- }
-
- hide() {
- this.wrapperEl.classList.add('hide');
- }
-
- setUnavailableButtonState(isLoading = true) {
- if (isLoading) {
- this.unavailableButtonArrow.classList.add('fa-spinner', 'fa-spin');
- this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
- this.unavailableButtonText.textContent = 'Checking branch availability…';
- } else {
- this.unavailableButtonArrow.classList.remove('fa-spinner', 'fa-spin');
- this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
- this.unavailableButtonText.textContent = 'New branch unavailable';
- }
+ bindEvents() {
+ this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
+ this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
+ this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
+ this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
}
checkAbilityToCreateBranch() {
@@ -107,49 +101,233 @@ export default class CreateMergeRequestDropdown {
});
}
- initDroplab() {
- this.droplab = new DropLab();
+ createBranch() {
+ return $.ajax({
+ method: 'POST',
+ dataType: 'json',
+ url: this.createBranchPath,
+ beforeSend: () => (this.isCreatingBranch = true),
+ })
+ .done((data) => {
+ this.branchCreated = true;
+ window.location.href = data.url;
+ })
+ .fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
+ }
- this.droplab.init(this.dropdownToggle, this.dropdownList, [InputSetter],
- this.getDroplabConfig());
+ createMergeRequest() {
+ return $.ajax({
+ method: 'POST',
+ dataType: 'json',
+ url: this.createMrPath,
+ beforeSend: () => (this.isCreatingMergeRequest = true),
+ })
+ .done((data) => {
+ this.mergeRequestCreated = true;
+ window.location.href = data.url;
+ })
+ .fail(() => new Flash('Failed to create Merge Request. Please try again.'));
+ }
+
+ disable() {
+ this.disableCreateAction();
+
+ this.dropdownToggle.classList.add('disabled');
+ this.dropdownToggle.setAttribute('disabled', 'disabled');
+ }
+
+ disableCreateAction() {
+ this.createMergeRequestButton.classList.add('disabled');
+ this.createMergeRequestButton.setAttribute('disabled', 'disabled');
+
+ this.createTargetButton.classList.add('disabled');
+ this.createTargetButton.setAttribute('disabled', 'disabled');
+ }
+
+ enable() {
+ this.createMergeRequestButton.classList.remove('disabled');
+ this.createMergeRequestButton.removeAttribute('disabled');
+
+ this.createTargetButton.classList.remove('disabled');
+ this.createTargetButton.removeAttribute('disabled');
+
+ this.dropdownToggle.classList.remove('disabled');
+ this.dropdownToggle.removeAttribute('disabled');
+ }
+
+ static findByValue(objects, ref, returnFirstMatch = false) {
+ if (!objects || !objects.length) return false;
+ if (objects.indexOf(ref) > -1) return ref;
+ if (returnFirstMatch) return objects.find(item => new RegExp(`^${ref}`).test(item));
+
+ return false;
}
getDroplabConfig() {
return {
- InputSetter: [{
- input: this.createMergeRequestButton,
- valueAttribute: 'data-value',
- inputAttribute: 'data-action',
- }, {
- input: this.createMergeRequestButton,
- valueAttribute: 'data-text',
- }],
+ addActiveClassToDropdownButton: true,
+ InputSetter: [
+ {
+ input: this.createMergeRequestButton,
+ valueAttribute: 'data-value',
+ inputAttribute: 'data-action',
+ },
+ {
+ input: this.createMergeRequestButton,
+ valueAttribute: 'data-text',
+ },
+ {
+ input: this.createTargetButton,
+ valueAttribute: 'data-value',
+ inputAttribute: 'data-action',
+ },
+ {
+ input: this.createTargetButton,
+ valueAttribute: 'data-text',
+ },
+ ],
};
}
- bindEvents() {
- this.createMergeRequestButton
- .addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ static getInputSelectedText(input) {
+ const start = input.selectionStart;
+ const end = input.selectionEnd;
+
+ return input.value.substr(start, end - start);
+ }
+
+ getRef(ref, target = 'all') {
+ if (!ref) return false;
+
+ return $.ajax({
+ method: 'GET',
+ dataType: 'json',
+ url: this.refsPath + ref,
+ beforeSend: () => {
+ this.isGettingRef = true;
+ },
+ })
+ .always(() => {
+ this.isGettingRef = false;
+ })
+ .done((data) => {
+ const branches = data[Object.keys(data)[0]];
+ const tags = data[Object.keys(data)[1]];
+ let result;
+
+ if (target === 'branch') {
+ result = CreateMergeRequestDropdown.findByValue(branches, ref);
+ } else {
+ result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
+ CreateMergeRequestDropdown.findByValue(tags, ref, true);
+ this.suggestedRef = result;
+ }
+
+ return this.updateInputState(target, ref, result);
+ })
+ .fail(() => {
+ this.unavailable();
+ this.disable();
+ new Flash('Failed to get ref.');
+
+ return false;
+ });
+ }
+
+ getTargetData(target) {
+ return {
+ input: this[`${target}Input`],
+ message: this[`${target}Message`],
+ };
+ }
+
+ hide() {
+ this.wrapperEl.classList.add('hide');
+ }
+
+ init() {
+ this.checkAbilityToCreateBranch();
+ }
+
+ initDroplab() {
+ this.droplab = new DropLab();
+
+ this.droplab.init(
+ this.dropdownToggle,
+ this.dropdownList,
+ [InputSetter],
+ this.getDroplabConfig(),
+ );
+ }
+
+ inputsAreValid() {
+ return this.branchIsValid && this.refIsValid;
}
isBusy() {
return this.isCreatingMergeRequest ||
this.mergeRequestCreated ||
this.isCreatingBranch ||
- this.branchCreated;
+ this.branchCreated ||
+ this.isGettingRef;
}
- onClickCreateMergeRequestButton(e) {
+ onChangeInput(event) {
+ let target;
+ let value;
+
+ if (event.srcElement === this.branchInput) {
+ target = 'branch';
+ value = this.branchInput.value;
+ } else if (event.srcElement === this.refInput) {
+ target = 'ref';
+ value = event.srcElement.value.slice(0, event.srcElement.selectionStart) +
+ event.srcElement.value.slice(event.srcElement.selectionEnd);
+ } else {
+ return false;
+ }
+
+ if (this.isGettingRef) return false;
+
+ // `ENTER` key submits the data.
+ if (event.keyCode === 13 && this.inputsAreValid()) {
+ event.preventDefault();
+ return this.createMergeRequestButton.click();
+ }
+
+ // If the input is empty, use the original value generated by the backend.
+ if (!value) {
+ this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
+ this.createMrPath = this.wrapperEl.dataset.createMrPath;
+
+ if (target === 'branch') {
+ this.branchIsValid = true;
+ } else {
+ this.refIsValid = true;
+ }
+
+ this.enable();
+ this.showAvailableMessage(target);
+ return true;
+ }
+
+ this.showCheckingMessage(target);
+ this.refDebounce(value, target);
+
+ return true;
+ }
+
+ onClickCreateMergeRequestButton(event) {
let xhr = null;
- e.preventDefault();
+ event.preventDefault();
if (this.isBusy()) {
return;
}
- if (e.target.dataset.action === CREATE_MERGE_REQUEST) {
+ if (event.target.dataset.action === CREATE_MERGE_REQUEST) {
xhr = this.createMergeRequest();
- } else if (e.target.dataset.action === CREATE_BRANCH) {
+ } else if (event.target.dataset.action === CREATE_BRANCH) {
xhr = this.createBranch();
}
@@ -163,31 +341,131 @@ export default class CreateMergeRequestDropdown {
this.disable();
}
- createMergeRequest() {
- return $.ajax({
- method: 'POST',
- dataType: 'json',
- url: this.createMrPath,
- beforeSend: () => (this.isCreatingMergeRequest = true),
- })
- .done((data) => {
- this.mergeRequestCreated = true;
- window.location.href = data.url;
- })
- .fail(() => new Flash('Failed to create Merge Request. Please try again.'));
+ onClickSetFocusOnBranchNameInput() {
+ this.branchInput.focus();
}
- createBranch() {
- return $.ajax({
- method: 'POST',
- dataType: 'json',
- url: this.createBranchPath,
- beforeSend: () => (this.isCreatingBranch = true),
- })
- .done((data) => {
- this.branchCreated = true;
- window.location.href = data.url;
- })
- .fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
+ // `TAB` autocompletes the source.
+ static processTab(event) {
+ if (event.keyCode !== 9 || this.isGettingRef) return;
+
+ const selectedText = CreateMergeRequestDropdown.getInputSelectedText(this.refInput);
+
+ // if nothing selected, we don't need to autocomplete anything. Do the default TAB action.
+ // If a user manually selected text, don't autocomplete anything. Do the default TAB action.
+ if (!selectedText || this.refInput.dataset.value === this.suggestedRef) return;
+
+ event.preventDefault();
+ window.getSelection().removeAllRanges();
+ }
+
+ removeMessage(target) {
+ const { input, message } = this.getTargetData(target);
+ const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
+ const messageClasses = ['gl-field-hint', 'gl-field-error-message', 'gl-field-success-message'];
+
+ inputClasses.forEach(cssClass => input.classList.remove(cssClass));
+ messageClasses.forEach(cssClass => message.classList.remove(cssClass));
+ message.style.display = 'none';
+ }
+
+ setUnavailableButtonState(isLoading = true) {
+ if (isLoading) {
+ this.unavailableButtonArrow.classList.add('fa-spin');
+ this.unavailableButtonArrow.classList.add('fa-spinner');
+ this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
+ this.unavailableButtonText.textContent = __('Checking branch availability...');
+ } else {
+ this.unavailableButtonArrow.classList.remove('fa-spin');
+ this.unavailableButtonArrow.classList.remove('fa-spinner');
+ this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
+ this.unavailableButtonText.textContent = __('New branch unavailable');
+ }
+ }
+
+ showAvailableMessage(target) {
+ const { input, message } = this.getTargetData(target);
+ const text = target === 'branch' ? __('Branch name') : __('Source');
+
+ this.removeMessage(target);
+ input.classList.add('gl-field-success-outline');
+ message.classList.add('gl-field-success-message');
+ message.textContent = sprintf(__('%{text} is available'), { text });
+ message.style.display = 'inline-block';
+ }
+
+ showCheckingMessage(target) {
+ const { message } = this.getTargetData(target);
+ const text = target === 'branch' ? __('branch name') : __('source');
+
+ this.removeMessage(target);
+ message.classList.add('gl-field-hint');
+ message.textContent = sprintf(__('Checking %{text} availability…'), { text });
+ message.style.display = 'inline-block';
+ }
+
+ showNotAvailableMessage(target) {
+ const { input, message } = this.getTargetData(target);
+ const text = target === 'branch' ? __('Branch is already taken') : __('Source is not available');
+
+ this.removeMessage(target);
+ input.classList.add('gl-field-error-outline');
+ message.classList.add('gl-field-error-message');
+ message.textContent = text;
+ message.style.display = 'inline-block';
+ }
+
+ unavailable() {
+ this.availableButton.classList.add('hide');
+ this.unavailableButton.classList.remove('hide');
+ }
+
+ updateInputState(target, ref, result) {
+ // target - 'branch' or 'ref' - which the input field we are searching a ref for.
+ // ref - string - what a user typed.
+ // result - string - what has been found on backend.
+
+ const pathReplacement = `$1${ref}`;
+
+ // If a found branch equals exact the same text a user typed,
+ // that means a new branch cannot be created as it already exists.
+ if (ref === result) {
+ if (target === 'branch') {
+ this.branchIsValid = false;
+ this.showNotAvailableMessage('branch');
+ } else {
+ this.refIsValid = true;
+ this.refInput.dataset.value = ref;
+ this.showAvailableMessage('ref');
+ this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
+ pathReplacement);
+ }
+ } else if (target === 'branch') {
+ this.branchIsValid = true;
+ this.showAvailableMessage('branch');
+ this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
+ pathReplacement);
+ } else {
+ this.refIsValid = false;
+ this.refInput.dataset.value = ref;
+ this.disableCreateAction();
+ this.showNotAvailableMessage('ref');
+
+ // Show ref hint.
+ if (result) {
+ this.refInput.value = result;
+ this.refInput.setSelectionRange(ref.length, result.length);
+ }
+ }
+
+ if (this.inputsAreValid()) {
+ this.enable();
+ } else {
+ this.disableCreateAction();
+ }
}
}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index d716218d9a4..1eab5e5c81e 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,12 +1,12 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
import { s__ } from './locale';
-/* global ProjectSelect */
+import projectSelect from './project_select';
import IssuableIndex from './issuable_index';
-/* global Milestone */
+import Milestone from './milestone';
import IssuableForm from './issuable_form';
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
-/* global NewBranchForm */
+import NewBranchForm from './new_branch_form';
/* global NotificationsForm */
/* global NotificationsDropdown */
import groupAvatar from './group_avatar';
@@ -18,16 +18,14 @@ import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
import NamespaceSelect from './namespace_select';
-/* global NewCommitForm */
-/* global NewBranchForm */
+import NewCommitForm from './new_commit_form';
import Project from './project';
import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
/* global ProjectFindFile */
-/* global ProjectNew */
-/* global ProjectShow */
+import ProjectNew from './project_new';
import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
@@ -91,6 +89,8 @@ import Members from './members';
import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select';
import Diff from './diff';
+import ProjectLabelSubscription from './project_label_subscription';
+import ProjectVariables from './project_variables';
(function() {
var Dispatcher;
@@ -187,7 +187,7 @@ import Diff from './diff';
initIssuableSidebar();
break;
case 'dashboard:milestones:index':
- new ProjectSelect();
+ projectSelect();
break;
case 'projects:milestones:show':
case 'groups:milestones:show':
@@ -197,7 +197,7 @@ import Diff from './diff';
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
- new ProjectSelect();
+ projectSelect();
initLegacyFilters();
break;
case 'groups:issues':
@@ -206,7 +206,7 @@ import Diff from './diff';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
- new ProjectSelect();
+ projectSelect();
break;
case 'dashboard:todos:index':
new Todos();
@@ -298,18 +298,21 @@ import Diff from './diff';
break;
case 'projects:snippets:show':
initNotes();
+ new ZenMode();
break;
case 'projects:snippets:new':
case 'projects:snippets:edit':
case 'projects:snippets:create':
case 'projects:snippets:update':
new GLForm($('.snippet-form'), true);
+ new ZenMode();
break;
case 'snippets:new':
case 'snippets:edit':
case 'snippets:create':
case 'snippets:update':
new GLForm($('.snippet-form'), false);
+ new ZenMode();
break;
case 'projects:releases:edit':
new ZenMode();
@@ -317,7 +320,6 @@ import Diff from './diff';
break;
case 'projects:merge_requests:show':
new Diff();
- shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
initIssuableSidebar();
@@ -327,6 +329,8 @@ import Diff from './diff';
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
+
+ shortcut_handler = new ShortcutsIssuable(true);
break;
case 'dashboard:activity':
new gl.Activities();
@@ -339,7 +343,8 @@ import Diff from './diff';
container: '.js-commit-pipeline-graph',
}).bindEvents();
initNotes();
- initChangesDropdown();
+ const stickyBarPaddingTop = 16;
+ initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
break;
case 'projects:commit:pipelines':
@@ -381,6 +386,7 @@ import Diff from './diff';
projectImport();
break;
case 'projects:pipelines:new':
+ case 'projects:pipelines:create':
new NewBranchForm($('.js-new-pipeline-form'));
break;
case 'projects:pipelines:builds':
@@ -484,7 +490,7 @@ import Diff from './diff';
if ($el.find('.dropdown-group-label').length) {
new GroupLabelSubscription($el);
} else {
- new gl.ProjectLabelSubscription($el);
+ new ProjectLabelSubscription($el);
}
});
break;
@@ -519,8 +525,15 @@ import Diff from './diff';
case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels
initSettingsPanels();
+
+ import(/* webpackChunkName: "ci-cd-settings" */ './projects/ci_cd_settings_bundle')
+ .then(ciCdSettings => ciCdSettings.default())
+ .catch((err) => {
+ Flash(s__('ProjectSettings|Problem setting up the CI/CD settings JavaScript'));
+ throw err;
+ });
case 'groups:settings:ci_cd:show':
- new gl.ProjectVariables();
+ new ProjectVariables();
break;
case 'ci:lints:create':
case 'ci:lints:show':
@@ -536,6 +549,7 @@ import Diff from './diff';
new LineHighlighter();
new BlobViewer();
initNotes();
+ new ZenMode();
break;
case 'import:fogbugz:new_user_map':
new UsersSelect();
@@ -548,7 +562,15 @@ import Diff from './diff';
import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch((err) => {
- Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript'));
+ Flash(s__('ClusterIntegration|Problem setting up the cluster'));
+ throw err;
+ });
+ break;
+ case 'projects:clusters:index':
+ import(/* webpackChunkName: "clusters_index" */ './clusters/clusters_index')
+ .then(clusterIndex => clusterIndex.default())
+ .catch((err) => {
+ Flash(s__('ClusterIntegration|Problem setting up the clusters list'));
throw err;
});
break;
@@ -623,7 +645,6 @@ import Diff from './diff';
case 'show':
new Star();
new ProjectNew();
- new ProjectShow();
new NotificationsDropdown();
break;
case 'wikis':
diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js
index 868d47e91b3..673e9bb4c0f 100644
--- a/app/assets/javascripts/droplab/constants.js
+++ b/app/assets/javascripts/droplab/constants.js
@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore';
+const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
// Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
@@ -13,4 +14,5 @@ export {
ACTIVE_CLASS,
TEMPLATE_REGEX,
IGNORE_CLASS,
+ IGNORE_HIDING_CLASS,
};
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
index 3901bb177fe..5eb0a339a1c 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -1,15 +1,18 @@
import utils from './utils';
-import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
+import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
class DropDown {
- constructor(list) {
+ constructor(list, config = {}) {
this.currentIndex = 0;
this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = [];
-
this.eventWrapper = {};
+ if (config.addActiveClassToDropdownButton) {
+ this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
+ }
+
this.getItems();
this.initTemplateString();
this.addEvents();
@@ -42,7 +45,7 @@ class DropDown {
this.addSelectedClass(selected);
e.preventDefault();
- this.hide();
+ if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
const listEvent = new CustomEvent('click.dl', {
detail: {
@@ -67,7 +70,20 @@ class DropDown {
addEvents() {
this.eventWrapper.clickEvent = this.clickEvent.bind(this);
+ this.eventWrapper.closeDropdown = this.closeDropdown.bind(this);
+
this.list.addEventListener('click', this.eventWrapper.clickEvent);
+ this.list.addEventListener('keyup', this.eventWrapper.closeDropdown);
+ }
+
+ closeDropdown(event) {
+ // `ESC` key closes the dropdown.
+ if (event.keyCode === 27) {
+ event.preventDefault();
+ return this.toggle();
+ }
+
+ return true;
}
setData(data) {
@@ -110,6 +126,8 @@ class DropDown {
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
+
+ if (this.dropdownToggle) this.dropdownToggle.classList.add('active');
}
hide() {
@@ -117,6 +135,8 @@ class DropDown {
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
+
+ if (this.dropdownToggle) this.dropdownToggle.classList.remove('active');
}
toggle() {
@@ -128,6 +148,7 @@ class DropDown {
destroy() {
this.hide();
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
+ this.list.removeEventListener('keyup', this.eventWrapper.closeDropdown);
}
static setImagesSrc(template) {
diff --git a/app/assets/javascripts/droplab/hook.js b/app/assets/javascripts/droplab/hook.js
index cf78165b0d8..8a8dcde9f88 100644
--- a/app/assets/javascripts/droplab/hook.js
+++ b/app/assets/javascripts/droplab/hook.js
@@ -3,7 +3,7 @@ import DropDown from './drop_down';
class Hook {
constructor(trigger, list, plugins, config) {
this.trigger = trigger;
- this.list = new DropDown(list);
+ this.list = new DropDown(list, config);
this.type = 'Hook';
this.event = 'click';
this.plugins = plugins || [];
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index b7747ee3f83..c84be42649a 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -36,7 +36,10 @@ export default function dropzoneInput(form) {
$formDropzone.append(divHover);
$formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
- if (!uploadsPath) return;
+ if (!uploadsPath) {
+ $formDropzone.addClass('js-invalid-dropzone');
+ return;
+ }
const dropzone = $formDropzone.dropzone({
url: uploadsPath,
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
new file mode 100644
index 00000000000..3236077c3cf
--- /dev/null
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -0,0 +1,71 @@
+<script>
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import tablePagination from '../../vue_shared/components/table_pagination.vue';
+ import environmentTable from '../components/environments_table.vue';
+
+ export default {
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ environments: {
+ type: Array,
+ required: true,
+ },
+ pagination: {
+ type: Object,
+ required: true,
+ },
+ canCreateDeployment: {
+ type: Boolean,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ components: {
+ environmentTable,
+ loadingIcon,
+ tablePagination,
+ },
+
+ methods: {
+ onChangePage(page) {
+ this.$emit('onChangePage', page);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="environments-container">
+
+ <loading-icon
+ label="Loading environments"
+ v-if="isLoading"
+ size="3"
+ />
+
+ <slot name="emptyState"></slot>
+
+ <div
+ class="table-holder"
+ v-if="!isLoading && environments.length > 0">
+
+ <environment-table
+ :environments="environments"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ />
+
+ <table-pagination
+ v-if="pagination && pagination.totalPages > 1"
+ :change="onChangePage"
+ :pageInfo="pagination"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue
new file mode 100644
index 00000000000..2646f08c8e6
--- /dev/null
+++ b/app/assets/javascripts/environments/components/empty_state.vue
@@ -0,0 +1,42 @@
+<script>
+ export default {
+ name: 'environmentsEmptyState',
+ props: {
+ newPath: {
+ type: String,
+ required: true,
+ },
+ canCreateEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+<template>
+ <div class="blank-state-row">
+ <div class="blank-state-center">
+ <h2 class="blank-state-title js-blank-state-title">
+ {{s__("Environments|You don't have any environments right now.")}}
+ </h2>
+ <p class="blank-state-text">
+ {{s__("Environments|Environments are places where code gets deployed, such as staging or production.")}}
+ <br />
+ <a :href="helpPath">
+ {{s__("Environments|Read more about environments")}}
+ </a>
+ </p>
+
+ <a
+ v-if="canCreateEnvironment"
+ :href="newPath"
+ class="btn btn-create js-new-environment-button">
+ {{s__("Environments|New environment")}}
+ </a>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
deleted file mode 100644
index c039ae85cfb..00000000000
--- a/app/assets/javascripts/environments/components/environment.vue
+++ /dev/null
@@ -1,268 +0,0 @@
-<script>
-import Visibility from 'visibilityjs';
-import Flash from '../../flash';
-import EnvironmentsService from '../services/environments_service';
-import environmentTable from './environments_table.vue';
-import EnvironmentsStore from '../stores/environments_store';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import tablePagination from '../../vue_shared/components/table_pagination.vue';
-import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
-import eventHub from '../event_hub';
-import Poll from '../../lib/utils/poll';
-import environmentsMixin from '../mixins/environments_mixin';
-
-export default {
-
- components: {
- environmentTable,
- tablePagination,
- loadingIcon,
- },
-
- mixins: [
- environmentsMixin,
- ],
-
- data() {
- const environmentsData = document.querySelector('#environments-list-view').dataset;
- const store = new EnvironmentsStore();
-
- return {
- store,
- state: store.state,
- visibility: 'available',
- isLoading: false,
- cssContainerClass: environmentsData.cssClass,
- endpoint: environmentsData.environmentsDataEndpoint,
- canCreateDeployment: environmentsData.canCreateDeployment,
- canReadEnvironment: environmentsData.canReadEnvironment,
- canCreateEnvironment: environmentsData.canCreateEnvironment,
- projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
- projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
- newEnvironmentPath: environmentsData.newEnvironmentPath,
- helpPagePath: environmentsData.helpPagePath,
- isMakingRequest: false,
-
- // Pagination Properties,
- paginationInformation: {},
- pageNumber: 1,
- };
- },
-
- computed: {
- scope() {
- return getParameterByName('scope');
- },
-
- canReadEnvironmentParsed() {
- return convertPermissionToBoolean(this.canReadEnvironment);
- },
-
- canCreateDeploymentParsed() {
- return convertPermissionToBoolean(this.canCreateDeployment);
- },
-
- canCreateEnvironmentParsed() {
- return convertPermissionToBoolean(this.canCreateEnvironment);
- },
- },
-
- /**
- * Fetches all the environments and stores them.
- * Toggles loading property.
- */
- created() {
- const scope = getParameterByName('scope') || this.visibility;
- const page = getParameterByName('page') || this.pageNumber;
-
- this.service = new EnvironmentsService(this.endpoint);
-
- const poll = new Poll({
- resource: this.service,
- method: 'get',
- data: { scope, page },
- successCallback: this.successCallback,
- errorCallback: this.errorCallback,
- notificationCallback: (isMakingRequest) => {
- this.isMakingRequest = isMakingRequest;
- },
- });
-
- if (!Visibility.hidden()) {
- this.isLoading = true;
- poll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- poll.restart();
- } else {
- poll.stop();
- }
- });
-
- eventHub.$on('toggleFolder', this.toggleFolder);
- eventHub.$on('postAction', this.postAction);
- },
-
- beforeDestroy() {
- eventHub.$off('toggleFolder');
- eventHub.$off('postAction');
- },
-
- methods: {
- toggleFolder(folder) {
- this.store.toggleFolder(folder);
-
- if (!folder.isOpen) {
- this.fetchChildEnvironments(folder, true);
- }
- },
-
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- * @return {String}
- */
- changePage(pageNumber) {
- const param = setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
-
- fetchEnvironments() {
- const scope = getParameterByName('scope') || this.visibility;
- const page = getParameterByName('page') || this.pageNumber;
-
- this.isLoading = true;
-
- return this.service.get({ scope, page })
- .then(this.successCallback)
- .catch(this.errorCallback);
- },
-
- fetchChildEnvironments(folder, showLoader = false) {
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
-
- this.service.getFolderContent(folder.folder_path)
- .then(resp => resp.json())
- .then(response => this.store.setfolderContent(folder, response.environments))
- .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
- .catch(() => {
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.');
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
- });
- },
-
- postAction(endpoint) {
- if (!this.isMakingRequest) {
- this.isLoading = true;
-
- this.service.postAction(endpoint)
- .then(() => this.fetchEnvironments())
- .catch(() => new Flash('An error occurred while making the request.'));
- }
- },
-
- successCallback(resp) {
- this.saveData(resp);
-
- // We need to verify if any folder is open to also update it
- const openFolders = this.store.getOpenFolders();
- if (openFolders.length) {
- openFolders.forEach(folder => this.fetchChildEnvironments(folder));
- }
- },
-
- errorCallback() {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.');
- },
- },
-};
-</script>
-<template>
- <div :class="cssContainerClass">
- <div class="top-area">
- <ul
- v-if="!isLoading"
- class="nav-links">
- <li :class="{ active: scope === null || scope === 'available' }">
- <a :href="projectEnvironmentsPath">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li :class="{ active : scope === 'stopped' }">
- <a :href="projectStoppedEnvironmentsPath">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
- <div
- v-if="canCreateEnvironmentParsed && !isLoading"
- class="nav-controls">
- <a
- :href="newEnvironmentPath"
- class="btn btn-create">
- New environment
- </a>
- </div>
- </div>
-
- <div class="environments-container">
- <loading-icon
- label="Loading environments"
- size="3"
- v-if="isLoading"
- />
-
- <div
- class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- You don't have any environments right now.
- </h2>
- <p class="blank-state-text">
- Environments are places where code gets deployed, such as staging or production.
- <br />
- <a :href="helpPagePath">
- Read more about environments
- </a>
- </p>
-
- <a
- v-if="canCreateEnvironmentParsed"
- :href="newEnvironmentPath"
- class="btn btn-create js-new-environment-button">
- New environment
- </a>
- </div>
-
- <div
- class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
-
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- />
- </div>
-
- <table-pagination
- v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation" />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index 6b749814ea4..520c3ac8ace 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,5 +1,6 @@
<script>
import tooltip from '../../vue_shared/directives/tooltip';
+import { s__ } from '../../locale';
/**
* Renders the external url link in environments table.
@@ -18,7 +19,7 @@ export default {
computed: {
title() {
- return 'Open';
+ return s__('Environments|Open');
},
},
};
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 9d25f806c0d..2f0e397aa45 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -432,7 +432,7 @@ export default {
v-if="!model.isFolder"
class="table-mobile-header"
role="rowheader">
- Environment
+ {{s__("Environments|Environment")}}
</div>
<a
v-if="!model.isFolder"
@@ -505,7 +505,7 @@ export default {
<div
role="rowheader"
class="table-mobile-header">
- Commit
+ {{s__("Environments|Commit")}}
</div>
<div
v-if="hasLastDeploymentKey"
@@ -521,7 +521,7 @@ export default {
<div
v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content">
- No deployments yet
+ {{s__("Environments|No deployments yet")}}
</div>
</div>
@@ -531,7 +531,7 @@ export default {
<div
role="rowheader"
class="table-mobile-header">
- Updated
+ {{s__("Environments|Updated")}}
</div>
<span
v-if="canShowDate"
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 1655561cdd3..b45af1a5ebc 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -34,6 +34,7 @@ export default {
:aria-label="title">
<i
class="fa fa-area-chart"
- aria-hidden="true" />
+ aria-hidden="true"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 49dba38edfb..92a596bfd33 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -48,10 +48,10 @@ export default {
:disabled="isLoading">
<span v-if="isLastDeployment">
- Re-deploy
+ {{s__("Environments|Re-deploy")}}
</span>
<span v-else>
- Rollback
+ {{s__("Environments|Rollback")}}
</span>
<loading-icon v-if="isLoading" />
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
new file mode 100644
index 00000000000..2592909734f
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -0,0 +1,128 @@
+<script>
+ import Flash from '../../flash';
+ import { s__ } from '../../locale';
+ import emptyState from './empty_state.vue';
+ import eventHub from '../event_hub';
+ import environmentsMixin from '../mixins/environments_mixin';
+ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+
+ export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ canCreateEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canCreateDeployment: {
+ type: Boolean,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ cssContainerClass: {
+ type: String,
+ required: true,
+ },
+ newEnvironmentPath: {
+ type: String,
+ required: true,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ components: {
+ emptyState,
+ },
+
+ mixins: [
+ CIPaginationMixin,
+ environmentsMixin,
+ ],
+
+ created() {
+ eventHub.$on('toggleFolder', this.toggleFolder);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('toggleFolder');
+ },
+
+ methods: {
+ toggleFolder(folder) {
+ this.store.toggleFolder(folder);
+
+ if (!folder.isOpen) {
+ this.fetchChildEnvironments(folder, true);
+ }
+ },
+
+ fetchChildEnvironments(folder, showLoader = false) {
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
+
+ this.service.getFolderContent(folder.folder_path)
+ .then(resp => resp.json())
+ .then(response => this.store.setfolderContent(folder, response.environments))
+ .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
+ .catch(() => {
+ Flash(s__('Environments|An error occurred while fetching the environments.'));
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
+ });
+ },
+
+ successCallback(resp) {
+ this.saveData(resp);
+
+ // We need to verify if any folder is open to also update it
+ const openFolders = this.store.getOpenFolders();
+ if (openFolders.length) {
+ openFolders.forEach(folder => this.fetchChildEnvironments(folder));
+ }
+ },
+ },
+ };
+</script>
+<template>
+ <div :class="cssContainerClass">
+ <div class="top-area">
+ <tabs
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
+ scope="environments"
+ />
+
+ <div
+ v-if="canCreateEnvironment && !isLoading"
+ class="nav-controls">
+ <a
+ :href="newEnvironmentPath"
+ class="btn btn-create">
+ {{s__("Environments|New environment")}}
+ </a>
+ </div>
+ </div>
+
+ <container
+ :is-loading="isLoading"
+ :environments="state.environments"
+ :pagination="state.paginationInformation"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ @onChangePage="onChangePage"
+ >
+ <empty-state
+ slot="emptyState"
+ v-if="!isLoading && state.environments.length === 0"
+ :new-path="newEnvironmentPath"
+ :help-path="helpPagePath"
+ :can-create-environment="canCreateEnvironment"
+ />
+ </container>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 175cc8f1f72..c04da4b81b7 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -2,12 +2,12 @@
/**
* Render environments table.
*/
-import EnvironmentTableRowComponent from './environment_item.vue';
+import environmentItem from './environment_item.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
- 'environment-item': EnvironmentTableRowComponent,
+ environmentItem,
loadingIcon,
},
@@ -42,19 +42,19 @@ export default {
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="columnheader">
- Environment
+ {{s__("Environments|Environment")}}
</div>
<div class="table-section section-10 environments-deploy" role="columnheader">
- Deployment
+ {{s__("Environments|Deployment")}}
</div>
<div class="table-section section-15 environments-build" role="columnheader">
- Job
+ {{s__("Environments|Job")}}
</div>
<div class="table-section section-25 environments-commit" role="columnheader">
- Commit
+ {{s__("Environments|Commit")}}
</div>
<div class="table-section section-10 environments-date" role="columnheader">
- Updated
+ {{s__("Environments|Updated")}}
</div>
</div>
<template
@@ -86,7 +86,7 @@ export default {
<a
:href="folderUrl(model)"
class="btn btn-default">
- Show all
+ {{s__("Environments|Show all")}}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js
index c0662125f28..2e0a4001b7c 100644
--- a/app/assets/javascripts/environments/environments_bundle.js
+++ b/app/assets/javascripts/environments/environments_bundle.js
@@ -1,10 +1,39 @@
import Vue from 'vue';
-import EnvironmentsComponent from './components/environment.vue';
+import environmentsComponent from './components/environments_app.vue';
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
+import Translate from '../vue_shared/translate';
+
+Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-list-view',
components: {
- 'environments-table-app': EnvironmentsComponent,
+ environmentsComponent,
+ },
+ data() {
+ const environmentsData = document.querySelector(this.$options.el).dataset;
+
+ return {
+ endpoint: environmentsData.environmentsDataEndpoint,
+ newEnvironmentPath: environmentsData.newEnvironmentPath,
+ helpPagePath: environmentsData.helpPagePath,
+ cssContainerClass: environmentsData.cssClass,
+ canCreateEnvironment: convertPermissionToBoolean(environmentsData.canCreateEnvironment),
+ canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
+ canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
+ };
+ },
+ render(createElement) {
+ return createElement('environments-component', {
+ props: {
+ endpoint: this.endpoint,
+ newEnvironmentPath: this.newEnvironmentPath,
+ helpPagePath: this.helpPagePath,
+ cssContainerClass: this.cssContainerClass,
+ canCreateEnvironment: this.canCreateEnvironment,
+ canCreateDeployment: this.canCreateDeployment,
+ canReadEnvironment: this.canReadEnvironment,
+ },
+ });
},
- render: createElement => createElement('environments-table-app'),
}));
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 9add8c3d721..5d2d14c7682 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,10 +1,35 @@
import Vue from 'vue';
-import EnvironmentsFolderComponent from './environments_folder_view.vue';
+import environmentsFolderApp from './environments_folder_view.vue';
+import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
+import Translate from '../../vue_shared/translate';
+
+Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-folder-list-view',
components: {
- 'environments-folder-app': EnvironmentsFolderComponent,
+ environmentsFolderApp,
+ },
+ data() {
+ const environmentsData = document.querySelector(this.$options.el).dataset;
+
+ return {
+ endpoint: environmentsData.endpoint,
+ folderName: environmentsData.folderName,
+ cssContainerClass: environmentsData.cssClass,
+ canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
+ canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
+ };
+ },
+ render(createElement) {
+ return createElement('environments-folder-app', {
+ props: {
+ endpoint: this.endpoint,
+ folderName: this.folderName,
+ cssContainerClass: this.cssContainerClass,
+ canCreateDeployment: this.canCreateDeployment,
+ canReadEnvironment: this.canReadEnvironment,
+ },
+ });
},
- render: createElement => createElement('environments-folder-app'),
}));
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index b155560df9d..27418bad01a 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,168 +1,40 @@
<script>
-import Visibility from 'visibilityjs';
-import Flash from '../../flash';
-import EnvironmentsService from '../services/environments_service';
-import environmentTable from '../components/environments_table.vue';
-import EnvironmentsStore from '../stores/environments_store';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import tablePagination from '../../vue_shared/components/table_pagination.vue';
-import Poll from '../../lib/utils/poll';
-import eventHub from '../event_hub';
-import environmentsMixin from '../mixins/environments_mixin';
-import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
-
-export default {
- components: {
- environmentTable,
- tablePagination,
- loadingIcon,
- },
-
- mixins: [
- environmentsMixin,
- ],
-
- data() {
- const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
- const store = new EnvironmentsStore();
- const pathname = window.location.pathname;
- const endpoint = `${pathname}.json`;
- const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
-
- return {
- store,
- folderName,
- endpoint,
- state: store.state,
- visibility: 'available',
- isLoading: false,
- cssContainerClass: environmentsData.cssClass,
- canCreateDeployment: environmentsData.canCreateDeployment,
- canReadEnvironment: environmentsData.canReadEnvironment,
- // Pagination Properties,
- paginationInformation: {},
- pageNumber: 1,
- };
- },
-
- computed: {
- scope() {
- return getParameterByName('scope');
- },
-
- canReadEnvironmentParsed() {
- return convertPermissionToBoolean(this.canReadEnvironment);
- },
-
- canCreateDeploymentParsed() {
- return convertPermissionToBoolean(this.canCreateDeployment);
- },
-
- /**
- * URL to link in the stopped tab.
- *
- * @return {String}
- */
- stoppedPath() {
- return `${window.location.pathname}?scope=stopped`;
- },
-
- /**
- * URL to link in the available tab.
- *
- * @return {String}
- */
- availablePath() {
- return window.location.pathname;
- },
- },
-
- /**
- * Fetches all the environments and stores them.
- * Toggles loading property.
- */
- created() {
- const scope = getParameterByName('scope') || this.visibility;
- const page = getParameterByName('page') || this.pageNumber;
-
- this.service = new EnvironmentsService(this.endpoint);
-
- const poll = new Poll({
- resource: this.service,
- method: 'get',
- data: { scope, page },
- successCallback: this.successCallback,
- errorCallback: this.errorCallback,
- notificationCallback: (isMakingRequest) => {
- this.isMakingRequest = isMakingRequest;
+ import environmentsMixin from '../mixins/environments_mixin';
+ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+
+ export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ folderName: {
+ type: String,
+ required: true,
+ },
+ cssContainerClass: {
+ type: String,
+ required: true,
+ },
+ canCreateDeployment: {
+ type: Boolean,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
},
- });
-
- if (!Visibility.hidden()) {
- this.isLoading = true;
- poll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- poll.restart();
- } else {
- poll.stop();
- }
- });
-
- eventHub.$on('postAction', this.postAction);
- },
-
- beforeDestroyed() {
- eventHub.$off('postAction');
- },
-
- methods: {
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- */
- changePage(pageNumber) {
- const param = setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
-
- fetchEnvironments() {
- const scope = getParameterByName('scope') || this.visibility;
- const page = getParameterByName('page') || this.pageNumber;
-
- this.isLoading = true;
-
- return this.service.get({ scope, page })
- .then(this.successCallback)
- .catch(this.errorCallback);
- },
-
- successCallback(resp) {
- this.saveData(resp);
- },
-
- errorCallback() {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.');
},
-
- postAction(endpoint) {
- if (!this.isMakingRequest) {
- this.isLoading = true;
-
- this.service.postAction(endpoint)
- .then(() => this.fetchEnvironments())
- .catch(() => new Flash('An error occurred while making the request.'));
- }
+ mixins: [
+ environmentsMixin,
+ CIPaginationMixin,
+ ],
+ methods: {
+ successCallback(resp) {
+ this.saveData(resp);
+ },
},
- },
-};
+ };
</script>
<template>
<div :class="cssContainerClass">
@@ -171,56 +43,23 @@ export default {
v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
- Environments / <b>{{folderName}}</b>
+ {{s__("Environments|Environments")}} / <b>{{folderName}}</b>
</h4>
- <ul class="nav-links">
- <li :class="{ active: scope === null || scope === 'available' }">
- <a
- :href="availablePath"
- class="js-available-environments-folder-tab">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li :class="{ active : scope === 'stopped' }">
- <a
- :href="stoppedPath"
- class="js-stopped-environments-folder-tab">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
- </div>
-
- <div class="environments-container">
-
- <loading-icon
- label="Loading environments"
- v-if="isLoading"
- size="3"
+ <tabs
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
+ scope="environments"
/>
-
- <div
- class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
-
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- />
-
- <table-pagination
- v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation"/>
- </div>
</div>
+
+ <container
+ :is-loading="isLoading"
+ :environments="state.environments"
+ :pagination="state.paginationInformation"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ @onChangePage="onChangePage"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 8f4066e3a6e..7219b076721 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -1,15 +1,174 @@
+/**
+ * Common code between environmets app and folder view
+ */
+
+import Visibility from 'visibilityjs';
+import Poll from '../../lib/utils/poll';
+import {
+ getParameterByName,
+ parseQueryStringIntoObject,
+} from '../../lib/utils/common_utils';
+import { s__ } from '../../locale';
+import Flash from '../../flash';
+import eventHub from '../event_hub';
+
+import EnvironmentsStore from '../stores/environments_store';
+import EnvironmentsService from '../services/environments_service';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import tablePagination from '../../vue_shared/components/table_pagination.vue';
+import environmentTable from '../components/environments_table.vue';
+import tabs from '../../vue_shared/components/navigation_tabs.vue';
+import container from '../components/container.vue';
+
export default {
+
+ components: {
+ environmentTable,
+ container,
+ loadingIcon,
+ tabs,
+ tablePagination,
+ },
+
+ data() {
+ const store = new EnvironmentsStore();
+
+ return {
+ store,
+ state: store.state,
+ isLoading: false,
+ isMakingRequest: false,
+ scope: getParameterByName('scope') || 'available',
+ page: getParameterByName('page') || '1',
+ requestData: {},
+ };
+ },
+
methods: {
saveData(resp) {
const headers = resp.headers;
return resp.json().then((response) => {
this.isLoading = false;
- this.store.storeAvailableCount(response.available_count);
- this.store.storeStoppedCount(response.stopped_count);
- this.store.storeEnvironments(response.environments);
- this.store.setPagination(headers);
+ if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
+ this.store.storeAvailableCount(response.available_count);
+ this.store.storeStoppedCount(response.stopped_count);
+ this.store.storeEnvironments(response.environments);
+ this.store.setPagination(headers);
+ }
});
},
+
+ /**
+ * Handles URL and query parameter changes.
+ * When the user uses the pagination or the tabs,
+ * - update URL
+ * - Make API request to the server with new parameters
+ * - Update the polling function
+ * - Update the internal state
+ */
+ updateContent(parameters) {
+ this.updateInternalState(parameters);
+ // fetch new data
+ return this.service.get(this.requestData)
+ .then(response => this.successCallback(response))
+ .then(() => {
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.errorCallback();
+
+ // restart polling
+ this.poll.restart();
+ });
+ },
+
+ errorCallback() {
+ this.isLoading = false;
+ Flash(s__('Environments|An error occurred while fetching the environments.'));
+ },
+
+ postAction(endpoint) {
+ if (!this.isMakingRequest) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => this.fetchEnvironments())
+ .catch(() => {
+ this.isLoading = false;
+ Flash(s__('Environments|An error occurred while making the request.'));
+ });
+ }
+ },
+
+ fetchEnvironments() {
+ this.isLoading = true;
+
+ return this.service.get(this.requestData)
+ .then(this.successCallback)
+ .catch(this.errorCallback);
+ },
+
+ },
+
+ computed: {
+ tabs() {
+ return [
+ {
+ name: s__('Available'),
+ scope: 'available',
+ count: this.state.availableCounter,
+ isActive: this.scope === 'available',
+ },
+ {
+ name: s__('Stopped'),
+ scope: 'stopped',
+ count: this.state.stoppedCounter,
+ isActive: this.scope === 'stopped',
+ },
+ ];
+ },
+ },
+
+ /**
+ * Fetches all the environments and stores them.
+ * Toggles loading property.
+ */
+ created() {
+ this.service = new EnvironmentsService(this.endpoint);
+ this.requestData = { page: this.page, scope: this.scope };
+
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'get',
+ data: this.requestData,
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: (isMakingRequest) => {
+ this.isMakingRequest = isMakingRequest;
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ this.poll.makeRequest();
+ } else {
+ this.fetchEnvironments();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+
+ eventHub.$on('postAction', this.postAction);
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('postAction');
},
};
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index aff8227c38c..5f2989ab854 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -36,7 +36,12 @@ export default class EnvironmentsStore {
storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => {
const oldEnvironmentState = this.state.environments
- .find(element => element.id === env.latest.id) || {};
+ .find((element) => {
+ if (env.latest) {
+ return element.id === env.latest.id;
+ }
+ return element.id === env.id;
+ }) || {};
let filtered = {};
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 67261c1c9b4..44deab9288e 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -41,7 +41,7 @@ const createFlashEl = (message, type, isInContentWrapper = false) => `
`;
const removeFlashClickListener = (flashEl, fadeTransition) => {
- flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
+ flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
};
/*
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 4e7a6e54f90..7ca783d3af6 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -514,10 +514,11 @@ GitLabDropdown = (function() {
const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
+ const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open');
const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
// Makes indeterminate items effective
- if (this.fullData && hasFilterBulkUpdate) {
+ if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) {
this.parseData(this.fullData);
}
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 356a95c05ca..0cd0c59a275 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -1,4 +1,5 @@
<script>
+import tooltip from '../../vue_shared/directives/tooltip';
import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
@@ -8,6 +9,9 @@ import itemStats from './item_stats.vue';
import itemActions from './item_actions.vue';
export default {
+ directives: {
+ tooltip,
+ },
components: {
identicon,
itemCaret,
@@ -112,10 +116,16 @@ export default {
</a>
</div>
<div
- class="title">
+ class="title namespace-title">
<a
+ v-tooltip
:href="group.relativePath"
- class="no-expand">{{group.fullName}}</a>
+ :title="group.fullName"
+ class="no-expand"
+ data-placement="top"
+ >
+ {{group.name}}
+ </a>
<span
v-if="group.permission"
class="access-type"
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index 7eff19e2e5a..09cb79c1afd 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -4,9 +4,11 @@ import tooltip from '../../vue_shared/directives/tooltip';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
+import Icon from '../../vue_shared/components/icon.vue';
export default {
components: {
+ Icon,
PopupDialog,
},
directives: {
@@ -63,9 +65,9 @@ export default {
:aria-label="editBtnTitle"
data-container="body"
class="edit-group btn no-expand">
- <i
- class="fa fa-cogs"
- aria-hidden="true"/>
+ <icon
+ name="settings">
+ </icon>
</a>
<a
v-tooltip
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 1191e0b895e..ada693afc46 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -14,7 +14,6 @@ export default () => {
});
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
- gl.Subscription.bindAll('.subscription');
new DueDateSelectors();
window.sidebar = new Sidebar();
};
diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js
index 1b265721581..2cbb70220d0 100644
--- a/app/assets/javascripts/init_legacy_filters.js
+++ b/app/assets/javascripts/init_legacy_filters.js
@@ -1,8 +1,7 @@
/* eslint-disable no-new */
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
-/* global SubscriptionSelect */
-
+import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
@@ -11,5 +10,5 @@ export default () => {
new LabelsSelect();
new MilestoneSelect();
issueStatusSelect();
- new SubscriptionSelect();
+ subscriptionSelect();
};
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index af6358953cf..ba2b6737988 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -1,11 +1,10 @@
/* eslint-disable class-methods-use-this, no-new */
/* global MilestoneSelect */
-/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
import issueStatusSelect from './issue_status_select';
-import './subscription_select';
+import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden';
@@ -48,7 +47,7 @@ export default class IssuableBulkUpdateSidebar {
new LabelsSelect();
new MilestoneSelect();
issueStatusSelect();
- new SubscriptionSelect();
+ subscriptionSelect();
}
setupBulkUpdateActions() {
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index 0b123a11a3b..c3e0acdff66 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -28,7 +28,7 @@ export default class IssuableIndex {
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success(response) {
- $('#issue_email').val(response.new_issue_address).focus();
+ $('#issuable_email').val(response.new_address).focus();
},
beforeSend() {
$('.incoming-email-token-reset').text('resetting...');
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e8ac8d3b5bb..5bdc7c99503 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -16,6 +16,10 @@ export default {
required: true,
type: String,
},
+ updateEndpoint: {
+ required: true,
+ type: String,
+ },
canUpdate: {
required: true,
type: Boolean,
@@ -34,6 +38,11 @@ export default {
required: false,
default: true,
},
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
issuableRef: {
type: String,
required: true,
@@ -102,6 +111,11 @@ export default {
required: false,
default: 'issue',
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
const store = new Store({
@@ -234,6 +248,8 @@ export default {
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
+ :can-attach-file="canAttachFile"
+ :enable-autocomplete="enableAutocomplete"
/>
<div v-else>
<title-component
@@ -250,6 +266,8 @@ export default {
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus"
+ :issuable-type="issuableType"
+ :update-url="updateEndpoint"
/>
<edited-component
v-if="hasUpdated"
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 48bad8f1e68..b7559ced946 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -22,6 +22,16 @@
required: false,
default: '',
},
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ updateUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -48,7 +58,7 @@
if (this.canUpdate) {
// eslint-disable-next-line no-new
new TaskList({
- dataType: 'issue',
+ dataType: this.issuableType,
fieldName: 'description',
selector: '.detail-page-description',
});
@@ -95,7 +105,9 @@
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
- v-model="descriptionText">
+ v-model="descriptionText"
+ :data-update-url="updateUrl"
+ >
</textarea>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 0aa1b2c2e31..52fe4ecd08b 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -17,6 +17,16 @@
type: String,
required: true,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
components: {
markdownField,
@@ -36,7 +46,10 @@
</label>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath">
+ :markdown-docs-path="markdownDocsPath"
+ :can-attach-file="canAttachFile"
+ :enable-autocomplete="enableAutocomplete"
+ >
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 8bb5c86d567..0fa19022336 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -41,6 +41,16 @@
required: false,
default: true,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
components: {
lockedWarning,
@@ -83,7 +93,10 @@
<description-field
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath" />
+ :markdown-docs-path="markdownDocsPath"
+ :can-attach-file="canAttachFile"
+ :enable-autocomplete="enableAutocomplete"
+ />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy"
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index 00002709ac6..a363d06d950 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -79,7 +79,7 @@
v-tooltip
v-if="showInlineEditButton && canUpdate"
type="button"
- class="btn-blank btn-edit note-action-button"
+ class="btn btn-default btn-edit btn-svg"
v-html="pencilIcon"
title="Edit title and description"
data-placement="bottom"
diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js
index 3e2658f9fc1..5a216f8fae2 100644
--- a/app/assets/javascripts/jobs/job_details_mediator.js
+++ b/app/assets/javascripts/jobs/job_details_mediator.js
@@ -29,8 +29,8 @@ export default class JobMediator {
this.poll = new Poll({
resource: this.service,
method: 'getJob',
- successCallback: this.successCallback.bind(this),
- errorCallback: this.errorCallback.bind(this),
+ successCallback: response => this.successCallback(response),
+ errorCallback: () => this.errorCallback(),
});
if (!Visibility.hidden()) {
@@ -57,7 +57,7 @@ export default class JobMediator {
successCallback(response) {
this.state.isLoading = false;
- return response.json().then(data => this.store.storeJob(data));
+ return this.store.storeJob(response.data);
}
errorCallback() {
diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js
index eaf1c6e500a..b746489c45c 100644
--- a/app/assets/javascripts/jobs/services/job_service.js
+++ b/app/assets/javascripts/jobs/services/job_service.js
@@ -1,14 +1,11 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '../../lib/utils/axios_utils';
export default class JobService {
constructor(endpoint) {
- this.job = Vue.resource(endpoint);
+ this.job = endpoint;
}
getJob() {
- return this.job.get();
+ return axios.get(this.job);
}
}
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index 45bff245827..7aeeca3b283 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -1,6 +1,22 @@
import axios from 'axios';
import csrf from './csrf';
-export default function setAxiosCsrfToken() {
- axios.defaults.headers.common[csrf.headerKey] = csrf.token;
-}
+axios.defaults.headers.common[csrf.headerKey] = csrf.token;
+
+// Maintain a global counter for active requests
+// see: spec/support/wait_for_requests.rb
+axios.interceptors.request.use((config) => {
+ window.activeVueResources = window.activeVueResources || 0;
+ window.activeVueResources += 1;
+
+ return config;
+});
+
+// Remove the global counter
+axios.interceptors.response.use((config) => {
+ window.activeVueResources -= 1;
+
+ return config;
+});
+
+export default axios;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 195e2ca6a78..33cc807912c 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -190,7 +190,7 @@ export const insertText = (target, text) => {
target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
// Trigger autosave
- $(target).trigger('input');
+ target.dispatchEvent(new Event('input'));
// Trigger autosize
const event = document.createEvent('Event');
@@ -270,46 +270,6 @@ export const parseIntPagination = paginationInformation => ({
});
/**
- * Updates the search parameter of a URL given the parameter and value provided.
- *
- * If no search params are present we'll add it.
- * If param for page is already present, we'll update it
- * If there are params but not for the given one, we'll add it at the end.
- * Returns the new search parameters.
- *
- * @param {String} param
- * @param {Number|String|Undefined|Null} value
- * @return {String}
- */
-export const setParamInURL = (param, value) => {
- let search;
- const locationSearch = window.location.search;
-
- if (locationSearch.length) {
- const parameters = locationSearch.substring(1, locationSearch.length)
- .split('&')
- .reduce((acc, element) => {
- const val = element.split('=');
- // eslint-disable-next-line no-param-reassign
- acc[val[0]] = decodeURIComponent(val[1]);
- return acc;
- }, {});
-
- parameters[param] = value;
-
- const toString = Object.keys(parameters)
- .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
- .join('&');
-
- search = `?${toString}`;
- } else {
- search = `?${param}=${value}`;
- }
-
- return search;
-};
-
-/**
* Given a string of query parameters creates an object.
*
* @example
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 5679b8c9a09..426a81a976d 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -150,3 +150,17 @@ export function timeIntervalInWords(intervalInSeconds) {
}
return text;
}
+
+export function dateInWords(date, abbreviated = false) {
+ if (!date) return date;
+
+ const month = date.getMonth();
+ const year = date.getFullYear();
+
+ const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
+ const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
+
+ const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
+
+ return `${monthName} ${date.getDate()}, ${year}`;
+}
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 65a8cf2c891..7fca80c2fdb 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -3,7 +3,9 @@ import { normalizeHeaders } from './common_utils';
/**
* Polling utility for handling realtime updates.
- * Service for vue resouce and method need to be provided as props
+ * Requirements: Promise based HTTP client
+ *
+ * Service for promise based http client and method need to be provided as props
*
* @example
* new Poll({
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index a1475b92c7e..9280b7f150c 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -55,3 +55,12 @@ export const slugify = str => str.trim().toLowerCase();
*/
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
+/**
+ * Capitalizes first character
+ *
+ * @param {String} text
+ * @return {String}
+ */
+export function capitalizeFirstCharacter(text) {
+ return `${text[0].toUpperCase()}${text.slice(1)}`;
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 0035dd23011..dcc0fa63b63 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -29,7 +29,6 @@ import './commit/image_file';
// lib/utils
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
-import './lib/utils/pretty_time';
import './lib/utils/url_utility';
// behaviors
@@ -45,7 +44,6 @@ import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
-import './copy_to_clipboard';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import './gl_field_error';
@@ -59,11 +57,7 @@ import './line_highlighter';
import initLogoAnimation from './logo';
import './merge_request';
import './merge_request_tabs';
-import './milestone';
import './milestone_select';
-import './namespace_select';
-import './new_branch_form';
-import './new_commit_form';
import './notes';
import './notifications_dropdown';
import './notifications_form';
@@ -71,22 +65,13 @@ import './pager';
import './preview_markdown';
import './project_find_file';
import './project_import';
-import './project_label_subscription';
-import './project_new';
-import './project_select';
-import './project_show';
-import './project_variables';
import './projects_dropdown';
import './projects_list';
import './syntax_highlight';
-import './render_math';
import './render_gfm';
import './right_sidebar';
import './search';
import './search_autocomplete';
-import './smart_interval';
-import './subscription';
-import './subscription_select';
import initBreadcrumbs from './breadcrumb';
import './dispatcher';
@@ -315,6 +300,8 @@ $(function () {
const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) {
- removeFlashClickListener(flashContainer.children[0]);
+ flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
+ removeFlashClickListener(flashEl);
+ });
}
});
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 8f3f1986763..f76a998bf8c 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,54 +1,49 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Sortable */
import Flash from './flash';
-(function() {
- this.Milestone = (function() {
- function Milestone() {
- this.bindTabsSwitching();
+export default class Milestone {
+ constructor() {
+ this.bindTabsSwitching();
- // Load merge request tab if it is active
- // merge request tab is active based on different conditions in the backend
- this.loadTab($('.js-milestone-tabs .active a'));
+ // Load merge request tab if it is active
+ // merge request tab is active based on different conditions in the backend
+ this.loadTab($('.js-milestone-tabs .active a'));
- this.loadInitialTab();
- }
+ this.loadInitialTab();
+ }
+
+ bindTabsSwitching() {
+ return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
+ const $target = $(e.target);
- Milestone.prototype.bindTabsSwitching = function() {
- return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
- const $target = $(e.target);
+ location.hash = $target.attr('href');
+ this.loadTab($target);
+ });
+ }
+ // eslint-disable-next-line class-methods-use-this
+ loadInitialTab() {
+ const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
- location.hash = $target.attr('href');
- this.loadTab($target);
+ if ($target.length) {
+ $target.tab('show');
+ }
+ }
+ // eslint-disable-next-line class-methods-use-this
+ loadTab($target) {
+ const endpoint = $target.data('endpoint');
+ const tabElId = $target.attr('href');
+
+ if (endpoint && !$target.hasClass('is-loaded')) {
+ $.ajax({
+ url: endpoint,
+ dataType: 'JSON',
+ })
+ .fail(() => new Flash('Error loading milestone tab'))
+ .done((data) => {
+ $(tabElId).html(data.html);
+ $target.addClass('is-loaded');
});
- };
-
- Milestone.prototype.loadInitialTab = function() {
- const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
-
- if ($target.length) {
- $target.tab('show');
- }
- };
-
- Milestone.prototype.loadTab = function($target) {
- const endpoint = $target.data('endpoint');
- const tabElId = $target.attr('href');
-
- if (endpoint && !$target.hasClass('is-loaded')) {
- $.ajax({
- url: endpoint,
- dataType: 'JSON',
- })
- .fail(() => new Flash('Error loading milestone tab'))
- .done((data) => {
- $(tabElId).html(data.html);
- $target.addClass('is-loaded');
- });
- }
- };
-
- return Milestone;
- })();
-}).call(window);
+ }
+ }
+}
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
index fed884d5c94..e230a06cd8c 100644
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -1,10 +1,7 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
+import axios from '../../lib/utils/axios_utils';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
-Vue.use(VueResource);
-
const MAX_REQUESTS = 3;
function backOffRequest(makeRequestCallback) {
@@ -32,8 +29,8 @@ export default class MonitoringService {
}
getGraphsData() {
- return backOffRequest(() => Vue.http.get(this.metricsEndpoint))
- .then(resp => resp.json())
+ return backOffRequest(() => axios.get(this.metricsEndpoint))
+ .then(resp => resp.data)
.then((response) => {
if (!response || !response.data) {
throw new Error('Unexpected metrics data response from prometheus endpoint');
@@ -43,8 +40,8 @@ export default class MonitoringService {
}
getDeploymentData() {
- return backOffRequest(() => Vue.http.get(this.deploymentEndpoint))
- .then(resp => resp.json())
+ return backOffRequest(() => axios.get(this.deploymentEndpoint))
+ .then(resp => resp.data)
.then((response) => {
if (!response || !response.deployments) {
throw new Error('Unexpected deployment data response from prometheus endpoint');
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 39fb302b644..77733b67c4d 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,97 +1,93 @@
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
-import RefSelectDropdown from '~/ref_select_dropdown';
+import RefSelectDropdown from './ref_select_dropdown';
-(function() {
- this.NewBranchForm = (function() {
- function NewBranchForm(form, availableRefs) {
- this.validate = this.validate.bind(this);
- this.branchNameError = form.find('.js-branch-name-error');
- this.name = form.find('.js-branch-name');
- this.ref = form.find('#ref');
- new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
- this.setupRestrictions();
- this.addBinding();
- this.init();
+export default class NewBranchForm {
+ constructor(form, availableRefs) {
+ this.validate = this.validate.bind(this);
+ this.branchNameError = form.find('.js-branch-name-error');
+ this.name = form.find('.js-branch-name');
+ this.ref = form.find('#ref');
+ new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
+ this.setupRestrictions();
+ this.addBinding();
+ this.init();
+ }
+
+ addBinding() {
+ return this.name.on('blur', this.validate);
+ }
+
+ init() {
+ if (this.name.length && this.name.val().length > 0) {
+ return this.name.trigger('blur');
}
+ }
- NewBranchForm.prototype.addBinding = function() {
- return this.name.on('blur', this.validate);
+ setupRestrictions() {
+ var endsWith, invalid, single, startsWith;
+ startsWith = {
+ pattern: /^(\/|\.)/g,
+ prefix: "can't start with",
+ conjunction: "or"
};
-
- NewBranchForm.prototype.init = function() {
- if (this.name.length && this.name.val().length > 0) {
- return this.name.trigger('blur');
- }
+ endsWith = {
+ pattern: /(\/|\.|\.lock)$/g,
+ prefix: "can't end in",
+ conjunction: "or"
};
-
- NewBranchForm.prototype.setupRestrictions = function() {
- var endsWith, invalid, single, startsWith;
- startsWith = {
- pattern: /^(\/|\.)/g,
- prefix: "can't start with",
- conjunction: "or"
- };
- endsWith = {
- pattern: /(\/|\.|\.lock)$/g,
- prefix: "can't end in",
- conjunction: "or"
- };
- invalid = {
- pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
- prefix: "can't contain",
- conjunction: ", "
- };
- single = {
- pattern: /^@+$/g,
- prefix: "can't be",
- conjunction: "or"
- };
- return this.restrictions = [startsWith, invalid, endsWith, single];
+ invalid = {
+ pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
+ prefix: "can't contain",
+ conjunction: ", "
+ };
+ single = {
+ pattern: /^@+$/g,
+ prefix: "can't be",
+ conjunction: "or"
};
+ return this.restrictions = [startsWith, invalid, endsWith, single];
+ }
- NewBranchForm.prototype.validate = function() {
- var errorMessage, errors, formatter, unique, validator;
- const indexOf = [].indexOf;
+ validate() {
+ var errorMessage, errors, formatter, unique, validator;
+ const indexOf = [].indexOf;
- this.branchNameError.empty();
- unique = function(values, value) {
- if (indexOf.call(values, value) === -1) {
- values.push(value);
- }
- return values;
- };
- formatter = function(values, restriction) {
- var formatted;
- formatted = values.map(function(value) {
- switch (false) {
- case !/\s/.test(value):
- return 'spaces';
- case !/\/{2,}/g.test(value):
- return 'consecutive slashes';
- default:
- return "'" + value + "'";
- }
- });
- return restriction.prefix + " " + (formatted.join(restriction.conjunction));
- };
- validator = (function(_this) {
- return function(errors, restriction) {
- var matched;
- matched = _this.name.val().match(restriction.pattern);
- if (matched) {
- return errors.concat(formatter(matched.reduce(unique, []), restriction));
- } else {
- return errors;
- }
- };
- })(this);
- errors = this.restrictions.reduce(validator, []);
- if (errors.length > 0) {
- errorMessage = $("<span/>").text(errors.join(', '));
- return this.branchNameError.append(errorMessage);
+ this.branchNameError.empty();
+ unique = function(values, value) {
+ if (indexOf.call(values, value) === -1) {
+ values.push(value);
}
+ return values;
};
-
- return NewBranchForm;
- })();
-}).call(window);
+ formatter = function(values, restriction) {
+ var formatted;
+ formatted = values.map(function(value) {
+ switch (false) {
+ case !/\s/.test(value):
+ return 'spaces';
+ case !/\/{2,}/g.test(value):
+ return 'consecutive slashes';
+ default:
+ return "'" + value + "'";
+ }
+ });
+ return restriction.prefix + " " + (formatted.join(restriction.conjunction));
+ };
+ validator = (function(_this) {
+ return function(errors, restriction) {
+ var matched;
+ matched = _this.name.val().match(restriction.pattern);
+ if (matched) {
+ return errors.concat(formatter(matched.reduce(unique, []), restriction));
+ } else {
+ return errors;
+ }
+ };
+ })(this);
+ errors = this.restrictions.reduce(validator, []);
+ if (errors.length > 0) {
+ errorMessage = $("<span/>").text(errors.join(', '));
+ return this.branchNameError.append(errorMessage);
+ }
+ }
+}
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 04073ef7270..6e152497d20 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,32 +1,28 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
-(function() {
- this.NewCommitForm = (function() {
- function NewCommitForm(form) {
- this.form = form;
- this.renderDestination = this.renderDestination.bind(this);
- this.branchName = form.find('.js-branch-name');
- this.originalBranch = form.find('.js-original-branch');
- this.createMergeRequest = form.find('.js-create-merge-request');
- this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
- this.branchName.keyup(this.renderDestination);
- this.renderDestination();
- }
+export default class NewCommitForm {
+ constructor(form) {
+ this.form = form;
+ this.renderDestination = this.renderDestination.bind(this);
+ this.branchName = form.find('.js-branch-name');
+ this.originalBranch = form.find('.js-original-branch');
+ this.createMergeRequest = form.find('.js-create-merge-request');
+ this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.branchName.keyup(this.renderDestination);
+ this.renderDestination();
+ }
- NewCommitForm.prototype.renderDestination = function() {
- var different;
- different = this.branchName.val() !== this.originalBranch.val();
- if (different) {
- this.createMergeRequestContainer.show();
- if (!this.wasDifferent) {
- this.createMergeRequest.prop('checked', true);
- }
- } else {
- this.createMergeRequestContainer.hide();
- this.createMergeRequest.prop('checked', false);
+ renderDestination() {
+ var different;
+ different = this.branchName.val() !== this.originalBranch.val();
+ if (different) {
+ this.createMergeRequestContainer.show();
+ if (!this.wasDifferent) {
+ this.createMergeRequest.prop('checked', true);
}
- return this.wasDifferent = different;
- };
-
- return NewCommitForm;
- })();
-}).call(window);
+ } else {
+ this.createMergeRequestContainer.hide();
+ this.createMergeRequest.prop('checked', false);
+ }
+ return this.wasDifferent = different;
+ }
+}
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
new file mode 100644
index 00000000000..e6f7ee56ff3
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -0,0 +1,26 @@
+<script>
+ import Icon from '~/vue_shared/components/icon.vue';
+ import Issuable from '~/vue_shared/mixins/issuable';
+
+ export default {
+ mixins: [
+ Issuable,
+ ],
+ components: {
+ Icon,
+ },
+ };
+</script>
+
+<template>
+ <div class="disabled-comment text-center">
+ <span class="issuable-note-warning inline">
+ <icon
+ name="lock"
+ :size="16"
+ class="icon">
+ </icon>
+ <span>This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment.</span>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index 30e02554b65..78986a450c2 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -8,8 +8,8 @@
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
- import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
- import issueDiscussionLockedWidget from './issue_discussion_locked_widget.vue';
+ import noteSignedOutWidget from './note_signed_out_widget.vue';
+ import discussionLockedWidget from './discussion_locked_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import issuableStateMixin from '../mixins/issuable_state';
@@ -22,15 +22,15 @@
noteType: constants.COMMENT,
// Can't use mapGetters,
// this needs to be in the data object because it belongs to the state
- issueState: this.$store.getters.getIssueData.state,
+ issueState: this.$store.getters.getNoteableData.state,
isSubmitting: false,
isSubmitButtonDisabled: true,
};
},
components: {
issueWarning,
- issueNoteSignedOutWidget,
- issueDiscussionLockedWidget,
+ noteSignedOutWidget,
+ discussionLockedWidget,
markdownField,
userAvatarLink,
},
@@ -46,7 +46,7 @@
...mapGetters([
'getCurrentUserLastNote',
'getUserData',
- 'getIssueData',
+ 'getNoteableData',
'getNotesData',
]),
isLoggedIn() {
@@ -59,7 +59,7 @@
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
},
canCreateNote() {
- return this.getIssueData.current_user.can_create_note;
+ return this.getNoteableData.current_user.can_create_note;
},
issueActionButtonTitle() {
if (this.note.length) {
@@ -85,16 +85,16 @@
return this.getNotesData.quickActionsDocsPath;
},
markdownPreviewPath() {
- return this.getIssueData.preview_note_path;
+ return this.getNoteableData.preview_note_path;
},
author() {
return this.getUserData;
},
canUpdateIssue() {
- return this.getIssueData.current_user.can_update;
+ return this.getNoteableData.current_user.can_update;
},
endpoint() {
- return this.getIssueData.create_note_path;
+ return this.getNoteableData.create_note_path;
},
},
methods: {
@@ -119,7 +119,7 @@
data: {
note: {
noteable_type: constants.NOTEABLE_TYPE,
- noteable_id: this.getIssueData.id,
+ noteable_id: this.getNoteableData.id,
note: this.note,
},
},
@@ -207,7 +207,7 @@
},
initAutoSave() {
if (this.isLoggedIn) {
- this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue');
+ this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getNoteableData.id], 'issue');
}
},
initTaskList() {
@@ -240,8 +240,11 @@
<template>
<div>
- <issue-note-signed-out-widget v-if="!isLoggedIn" />
- <issue-discussion-locked-widget v-else-if="!canCreateNote" />
+ <note-signed-out-widget v-if="!isLoggedIn" />
+ <discussion-locked-widget
+ issuable-type="issue"
+ v-else-if="!canCreateNote"
+ />
<ul
v-else
class="notes notes-form timeline">
@@ -266,9 +269,9 @@
<div class="error-alert"></div>
<issue-warning
- v-if="hasWarning(getIssueData)"
- :is-locked="isLocked(getIssueData)"
- :is-confidential="isConfidential(getIssueData)"
+ v-if="hasWarning(getNoteableData)"
+ :is-locked="isLocked(getNoteableData)"
+ :is-confidential="isConfidential(getNoteableData)"
/>
<markdown-field
diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue
index 0f13221b81e..460fde9b62a 100644
--- a/app/assets/javascripts/notes/components/issue_discussion.vue
+++ b/app/assets/javascripts/notes/components/issue_discussion.vue
@@ -4,10 +4,9 @@
import { SYSTEM_NOTE } from '../constants';
import issueNote from './issue_note.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
- import issueNoteHeader from './issue_note_header.vue';
- import issueNoteActions from './issue_note_actions.vue';
- import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
- import issueNoteEditedText from './issue_note_edited_text.vue';
+ import noteHeader from './note_header.vue';
+ import noteSignedOutWidget from './note_signed_out_widget.vue';
+ import noteEditedText from './note_edited_text.vue';
import issueNoteForm from './issue_note_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
@@ -28,10 +27,9 @@
components: {
issueNote,
userAvatarLink,
- issueNoteHeader,
- issueNoteActions,
- issueNoteSignedOutWidget,
- issueNoteEditedText,
+ noteHeader,
+ noteSignedOutWidget,
+ noteEditedText,
issueNoteForm,
placeholderNote,
placeholderSystemNote,
@@ -41,7 +39,7 @@
],
computed: {
...mapGetters([
- 'getIssueData',
+ 'getNoteableData',
]),
discussion() {
return this.note.notes[0];
@@ -50,10 +48,10 @@
return this.discussion.author;
},
canReply() {
- return this.getIssueData.current_user.can_create_note;
+ return this.getNoteableData.current_user.can_create_note;
},
newNotePath() {
- return this.getIssueData.create_note_path;
+ return this.getNoteableData.create_note_path;
},
lastUpdatedBy() {
const { notes } = this.note;
@@ -171,7 +169,7 @@
<div class="timeline-content">
<div class="discussion">
<div class="discussion-header">
- <issue-note-header
+ <note-header
:author="author"
:created-at="discussion.created_at"
:note-id="discussion.id"
@@ -179,8 +177,8 @@
@toggleHandler="toggleDiscussionHandler"
action-text="started a discussion"
class="discussion"
- />
- <issue-note-edited-text
+ />
+ <note-edited-text
v-if="lastUpdatedAt"
:edited-at="lastUpdatedAt"
:edited-by="lastUpdatedBy"
@@ -220,7 +218,7 @@
@cancelFormEdition="cancelReplyForm"
ref="noteForm"
/>
- <issue-note-signed-out-widget v-if="!canReply" />
+ <note-signed-out-widget v-if="!canReply" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
deleted file mode 100644
index e73ec2aaf71..00000000000
--- a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<script>
- export default {
- computed: {
- lockIcon() {
- return gl.utils.spriteIcon('lock');
- },
- },
- };
-
-</script>
-
-<template>
- <div class="disabled-comment text-center">
- <span class="issuable-note-warning">
- <span class="icon" v-html="lockIcon"></span>
- <span>This issue is locked. Only <b>project members</b> can comment.</span>
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue
index 40318f9a600..8c81c5d6df3 100644
--- a/app/assets/javascripts/notes/components/issue_note.vue
+++ b/app/assets/javascripts/notes/components/issue_note.vue
@@ -2,8 +2,8 @@
import { mapGetters, mapActions } from 'vuex';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
- import issueNoteHeader from './issue_note_header.vue';
- import issueNoteActions from './issue_note_actions.vue';
+ import noteHeader from './note_header.vue';
+ import noteActions from './note_actions.vue';
import issueNoteBody from './issue_note_body.vue';
import eventHub from '../event_hub';
@@ -23,8 +23,8 @@
},
components: {
userAvatarLink,
- issueNoteHeader,
- issueNoteActions,
+ noteHeader,
+ noteActions,
issueNoteBody,
},
computed: {
@@ -155,13 +155,13 @@
</div>
<div class="timeline-content">
<div class="note-header">
- <issue-note-header
+ <note-header
:author="author"
:created-at="note.created_at"
:note-id="note.id"
action-text="commented"
/>
- <issue-note-actions
+ <note-actions
:author-id="author.id"
:note-id="note.id"
:access-level="note.human_access"
diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue
index 5f9003bfd87..a16c5f6a785 100644
--- a/app/assets/javascripts/notes/components/issue_note_body.vue
+++ b/app/assets/javascripts/notes/components/issue_note_body.vue
@@ -1,7 +1,7 @@
<script>
- import issueNoteEditedText from './issue_note_edited_text.vue';
- import issueNoteAwardsList from './issue_note_awards_list.vue';
- import issueNoteAttachment from './issue_note_attachment.vue';
+ import noteEditedText from './note_edited_text.vue';
+ import noteAwardsList from './note_awards_list.vue';
+ import noteAttachment from './note_attachment.vue';
import issueNoteForm from './issue_note_form.vue';
import TaskList from '../../task_list';
import autosave from '../mixins/autosave';
@@ -26,9 +26,9 @@
autosave,
],
components: {
- issueNoteEditedText,
- issueNoteAwardsList,
- issueNoteAttachment,
+ noteEditedText,
+ noteAwardsList,
+ noteAttachment,
issueNoteForm,
},
computed: {
@@ -101,20 +101,20 @@
v-model="note.note"
:data-update-url="note.path"
class="hidden js-task-list-field"></textarea>
- <issue-note-edited-text
+ <note-edited-text
v-if="note.last_edited_at"
:edited-at="note.last_edited_at"
:edited-by="note.last_edited_by"
action-text="Edited"
/>
- <issue-note-awards-list
+ <note-awards-list
v-if="note.award_emoji.length"
:note-id="note.id"
:note-author-id="note.author.id"
:awards="note.award_emoji"
:toggle-award-path="note.toggle_award_path"
/>
- <issue-note-attachment
+ <note-attachment
v-if="note.attachment"
:attachment="note.attachment"
/>
diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue
index e2539d6b89d..4d527cb6643 100644
--- a/app/assets/javascripts/notes/components/issue_note_form.vue
+++ b/app/assets/javascripts/notes/components/issue_note_form.vue
@@ -46,8 +46,8 @@
computed: {
...mapGetters([
'getDiscussionLastNote',
- 'getIssueData',
- 'getIssueDataByProp',
+ 'getNoteableData',
+ 'getNoteableDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
]),
@@ -55,7 +55,7 @@
return `#note_${this.noteId}`;
},
markdownPreviewPath() {
- return this.getIssueDataByProp('preview_note_path');
+ return this.getNoteableDataByProp('preview_note_path');
},
markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath');
@@ -129,9 +129,9 @@
class="edit-note common-note-form js-quick-submit gfm-form">
<issue-warning
- v-if="hasWarning(getIssueData)"
- :is-locked="isLocked(getIssueData)"
- :is-confidential="isConfidential(getIssueData)"
+ v-if="hasWarning(getNoteableData)"
+ :is-locked="isLocked(getNoteableData)"
+ :is-confidential="isConfidential(getNoteableData)"
/>
<markdown-field
diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue
index 5c9119644e3..4cfcffa2391 100644
--- a/app/assets/javascripts/notes/components/issue_notes_app.vue
+++ b/app/assets/javascripts/notes/components/issue_notes_app.vue
@@ -14,7 +14,7 @@
export default {
name: 'issueNotesApp',
props: {
- issueData: {
+ noteableData: {
type: Object,
required: true,
},
@@ -56,7 +56,7 @@
actionToggleAward: 'toggleAward',
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
setNotesData: 'setNotesData',
- setIssueData: 'setIssueData',
+ setNoteableData: 'setNoteableData',
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash',
@@ -106,7 +106,7 @@
},
created() {
this.setNotesData(this.notesData);
- this.setIssueData(this.issueData);
+ this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
},
mounted() {
diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index feb3e73194b..45fc6196be4 100644
--- a/app/assets/javascripts/notes/components/issue_note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -5,11 +5,11 @@
import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
export default {
- name: 'issueNoteActions',
+ name: 'noteActions',
props: {
authorId: {
type: Number,
diff --git a/app/assets/javascripts/notes/components/issue_note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue
index 7134a3eb47e..cd9571a4002 100644
--- a/app/assets/javascripts/notes/components/issue_note_attachment.vue
+++ b/app/assets/javascripts/notes/components/note_attachment.vue
@@ -1,6 +1,6 @@
<script>
export default {
- name: 'issueNoteAttachment',
+ name: 'noteAttachment',
props: {
attachment: {
type: Object,
diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index c3a340139e7..c3a340139e7 100644
--- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index 49e09f0ecc5..49e09f0ecc5 100644
--- a/app/assets/javascripts/notes/components/issue_note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 63aa3d777d0..63aa3d777d0 100644
--- a/app/assets/javascripts/notes/components/issue_note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
diff --git a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index 77af3594c1c..45d3c2de355 100644
--- a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -2,7 +2,6 @@
import { mapGetters } from 'vuex';
export default {
- name: 'singInLinksNotes',
computed: {
...mapGetters([
'getNotesDataByProp',
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index e2ea37408cf..8d74c5de5cf 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
const notesDataset = document.getElementById('js-vue-notes').dataset;
return {
- issueData: JSON.parse(notesDataset.issueData),
+ noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: {
lastFetchedAt: notesDataset.lastFetchedAt,
@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
render(createElement) {
return createElement('issue-notes-app', {
props: {
- issueData: this.issueData,
+ noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
},
diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index b51b0cb2013..b51b0cb2013 100644
--- a/app/assets/javascripts/notes/services/issue_notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 6f04aecc9b7..085b18642ba 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -4,7 +4,7 @@ import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
import * as utils from './utils';
import * as constants from '../constants';
-import service from '../services/issue_notes_service';
+import service from '../services/notes_service';
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
@@ -12,7 +12,7 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
-export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data);
+export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 1f0c6af6156..e18b277119e 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -6,8 +6,8 @@ export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData;
export const getNotesDataByProp = state => prop => state.notesData[prop];
-export const getIssueData = state => state.issueData;
-export const getIssueDataByProp = state => prop => state.issueData[prop];
+export const getNoteableData = state => state.noteableData;
+export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
index 8e0c8531bbc..488a9ca38d3 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -15,7 +15,7 @@ export default new Vuex.Store({
// holds endpoints and permissions provided through haml
notesData: {},
userData: {},
- issueData: {},
+ noteableData: {},
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index cd71533ba9d..d520c197407 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -3,7 +3,7 @@ export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
-export const SET_ISSUE_DATA = 'SET_ISSUE_DATA';
+export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA';
export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index c2a08f3d6fe..20f81a430c2 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -66,8 +66,8 @@ export default {
Object.assign(state, { notesData: data });
},
- [types.SET_ISSUE_DATA](state, data) {
- Object.assign(state, { issueData: data });
+ [types.SET_NOTEABLE_DATA](state, data) {
+ Object.assign(state, { noteableData: data });
},
[types.SET_USER_DATA](state, data) {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 5dea4555515..08199b4234a 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -78,11 +78,13 @@
<div class="ci-job-component">
<a
v-tooltip
- v-if="job.status.details_path"
+ v-if="job.status.has_details"
:href="job.status.details_path"
:title="tooltipText"
:class="cssClassJobName"
- data-container="body">
+ data-container="body"
+ class="js-pipeline-graph-job-link"
+ >
<job-name-component
:name="job.name"
@@ -95,7 +97,8 @@
v-tooltip
:title="tooltipText"
:class="cssClassJobName"
- data-container="body">
+ data-container="body"
+ >
<job-name-component
:name="job.name"
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index cf241c8ffed..fe1f3b4246a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -3,15 +3,14 @@
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
- import navigationTabs from './navigation_tabs.vue';
+ import navigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
import {
convertPermissionToBoolean,
getParameterByName,
- historyPushState,
- buildUrlWithCurrentLocation,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
+ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
props: {
@@ -36,6 +35,7 @@
},
mixins: [
pipelinesMixin,
+ CIPaginationMixin,
],
data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
@@ -170,22 +170,8 @@
* - Update the internal state
*/
updateContent(parameters) {
- // stop polling
- this.poll.stop();
+ this.updateInternalState(parameters);
- const queryString = Object.keys(parameters).map((parameter) => {
- const value = parameters[parameter];
- // update internal state for UI
- this[parameter] = value;
- return `${parameter}=${encodeURIComponent(value)}`;
- }).join('&');
-
- // update polling parameters
- this.requestData = parameters;
-
- historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
-
- this.isLoading = true;
// fetch new data
return this.service.getPipelines(this.requestData)
.then((response) => {
@@ -203,14 +189,6 @@
this.poll.restart();
});
},
-
- onChangeTab(scope) {
- this.updateContent({ scope, page: '1' });
- },
- onChangePage(page) {
- /* URLS parameters are strings, we need to parse to match types */
- this.updateContent({ scope: this.scope, page: Number(page).toString() });
- },
},
};
</script>
@@ -235,6 +213,7 @@
<navigation-tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
+ scope="pipelines"
/>
<navigation-controls
@@ -267,9 +246,11 @@
/>
<div
- class="blank-state blank-state-no-icon"
+ class="blank-state-row"
v-if="shouldRenderNoPipelinesMessage">
- <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ <div class="blank-state-center">
+ <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ </div>
</div>
<div
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index ddb78aaeea1..3131e71d9d6 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
-/* global ProjectSelect */
import Cookies from 'js-cookie';
+import projectSelect from './project_select';
export default class Project {
constructor() {
@@ -17,13 +17,14 @@ export default class Project {
$('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
+ const activeText = $this.find('.dropdown-menu-inner-title').text();
e.preventDefault();
$('.is-active', $cloneOptions).not($this).removeClass('is-active');
$this.toggleClass('is-active');
$projectCloneField.val(url);
- $cloneBtnText.text($this.text());
+ $cloneBtnText.text(activeText);
return $('.clone').text(url);
});
@@ -46,7 +47,7 @@ export default class Project {
}
static projectSelectDropdown () {
- new ProjectSelect();
+ projectSelect();
$('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
}
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 0a811627600..b65521b278f 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -1,55 +1,50 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */
+export default class ProjectLabelSubscription {
+ constructor(container) {
+ this.$container = $(container);
+ this.$buttons = this.$container.find('.js-subscribe-button');
-(function(global) {
- class ProjectLabelSubscription {
- constructor(container) {
- this.$container = $(container);
- this.$buttons = this.$container.find('.js-subscribe-button');
-
- this.$buttons.on('click', this.toggleSubscription.bind(this));
- }
+ this.$buttons.on('click', this.toggleSubscription.bind(this));
+ }
- toggleSubscription(event) {
- event.preventDefault();
+ toggleSubscription(event) {
+ event.preventDefault();
- const $btn = $(event.currentTarget);
- const $span = $btn.find('span');
- const url = $btn.attr('data-url');
- const oldStatus = $btn.attr('data-status');
+ const $btn = $(event.currentTarget);
+ const $span = $btn.find('span');
+ const url = $btn.attr('data-url');
+ const oldStatus = $btn.attr('data-status');
- $btn.addClass('disabled');
- $span.toggleClass('hidden');
+ $btn.addClass('disabled');
+ $span.toggleClass('hidden');
- $.ajax({
- type: 'POST',
- url: url
- }).done(() => {
- let newStatus, newAction;
+ $.ajax({
+ type: 'POST',
+ url,
+ }).done(() => {
+ let newStatus;
+ let newAction;
- if (oldStatus === 'unsubscribed') {
- [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
- } else {
- [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
- }
+ if (oldStatus === 'unsubscribed') {
+ [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
+ } else {
+ [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
+ }
- $span.toggleClass('hidden');
- $btn.removeClass('disabled');
+ $span.toggleClass('hidden');
+ $btn.removeClass('disabled');
- this.$buttons.attr('data-status', newStatus);
- this.$buttons.find('> span').text(newAction);
+ this.$buttons.attr('data-status', newStatus);
+ this.$buttons.find('> span').text(newAction);
- this.$buttons.map((button) => {
- const $button = $(button);
+ this.$buttons.map((button) => {
+ const $button = $(button);
- if ($button.attr('data-original-title')) {
- $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
- }
+ if ($button.attr('data-original-title')) {
+ $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
+ }
- return button;
- });
+ return button;
});
- }
+ });
}
-
- global.ProjectLabelSubscription = ProjectLabelSubscription;
-})(window.gl || (window.gl = {}));
+}
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index fd89a1a85c3..ca548d011b6 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
+/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/
import VisibilitySelect from './visibility_select';
@@ -7,153 +7,145 @@ function highlightChanges($elm) {
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
}
-(function() {
- this.ProjectNew = (function() {
- function ProjectNew() {
- this.toggleSettings = this.toggleSettings.bind(this);
- this.$selects = $('.features select');
- this.$repoSelects = this.$selects.filter('.js-repo-select');
- this.$projectSelects = this.$selects.not('.js-repo-select');
-
- $('.project-edit-container').on('ajax:before', (function(_this) {
- return function() {
- $('.project-edit-container').hide();
- return $('.save-project-loader').show();
- };
- })(this));
-
- this.initVisibilitySelect();
-
- this.toggleSettings();
- this.toggleSettingsOnclick();
- this.toggleRepoVisibility();
- }
-
- ProjectNew.prototype.initVisibilitySelect = function() {
- const visibilityContainer = document.querySelector('.js-visibility-select');
- if (!visibilityContainer) return;
- const visibilitySelect = new VisibilitySelect(visibilityContainer);
- visibilitySelect.init();
-
- const $visibilitySelect = $(visibilityContainer).find('select');
- let projectVisibility = $visibilitySelect.val();
- const PROJECT_VISIBILITY_PRIVATE = '0';
-
- $visibilitySelect.on('change', () => {
- const newProjectVisibility = $visibilitySelect.val();
-
- if (projectVisibility !== newProjectVisibility) {
- this.$projectSelects.each((idx, select) => {
- const $select = $(select);
- const $options = $select.find('option');
- const values = $.map($options, e => e.value);
-
- // if switched to "private", limit visibility options
- if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
- if ($select.val() !== values[0] && $select.val() !== values[1]) {
- $select.val(values[1]).trigger('change');
- highlightChanges($select);
- }
- $options.slice(2).disable();
+export default class ProjectNew {
+ constructor() {
+ this.toggleSettings = this.toggleSettings.bind(this);
+ this.$selects = $('.features select');
+ this.$repoSelects = this.$selects.filter('.js-repo-select');
+ this.$projectSelects = this.$selects.not('.js-repo-select');
+
+ $('.project-edit-container').on('ajax:before', () => {
+ $('.project-edit-container').hide();
+ return $('.save-project-loader').show();
+ });
+
+ this.initVisibilitySelect();
+
+ this.toggleSettings();
+ this.toggleSettingsOnclick();
+ this.toggleRepoVisibility();
+ }
+
+ initVisibilitySelect() {
+ const visibilityContainer = document.querySelector('.js-visibility-select');
+ if (!visibilityContainer) return;
+ const visibilitySelect = new VisibilitySelect(visibilityContainer);
+ visibilitySelect.init();
+
+ const $visibilitySelect = $(visibilityContainer).find('select');
+ let projectVisibility = $visibilitySelect.val();
+ const PROJECT_VISIBILITY_PRIVATE = '0';
+
+ $visibilitySelect.on('change', () => {
+ const newProjectVisibility = $visibilitySelect.val();
+
+ if (projectVisibility !== newProjectVisibility) {
+ this.$projectSelects.each((idx, select) => {
+ const $select = $(select);
+ const $options = $select.find('option');
+ const values = $.map($options, e => e.value);
+
+ // if switched to "private", limit visibility options
+ if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
+ if ($select.val() !== values[0] && $select.val() !== values[1]) {
+ $select.val(values[1]).trigger('change');
+ highlightChanges($select);
}
+ $options.slice(2).disable();
+ }
- // if switched from "private", increase visibility for non-disabled options
- if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
- $options.enable();
- if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
- $select.val(values[values.length - 1]).trigger('change');
- highlightChanges($select);
- }
+ // if switched from "private", increase visibility for non-disabled options
+ if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
+ $options.enable();
+ if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
+ $select.val(values[values.length - 1]).trigger('change');
+ highlightChanges($select);
}
- });
+ }
+ });
- projectVisibility = newProjectVisibility;
- }
- });
- };
-
- ProjectNew.prototype.toggleSettings = function() {
- var self = this;
-
- this.$selects.each(function () {
- var $select = $(this);
- var className = $select.data('field')
- .replace(/_/g, '-')
- .replace('access-level', 'feature');
- self._showOrHide($select, '.' + className);
- });
- };
-
- ProjectNew.prototype.toggleSettingsOnclick = function() {
- this.$selects.on('change', this.toggleSettings);
- };
-
- ProjectNew.prototype._showOrHide = function(checkElement, container) {
- var $container = $(container);
-
- if ($(checkElement).val() !== '0') {
- return $container.show();
- } else {
- return $container.hide();
+ projectVisibility = newProjectVisibility;
}
- };
-
- ProjectNew.prototype.toggleRepoVisibility = function () {
- var $repoAccessLevel = $('.js-repo-access-level select');
- var $lfsEnabledOption = $('.js-lfs-enabled select');
- var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
- var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
- var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
-
- this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
- .nextAll()
- .hide();
-
- $repoAccessLevel.off('change')
- .on('change', function () {
- var selectedVal = parseInt($repoAccessLevel.val(), 10);
-
- this.$repoSelects.each(function () {
- var $this = $(this);
- var repoSelectVal = parseInt($this.val(), 10);
-
- $this.find('option').enable();
-
- if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
- $this.val(selectedVal).trigger('change');
- highlightChanges($this);
- }
-
- $this.find("option[value='" + selectedVal + "']").nextAll().disable();
- });
+ });
+ }
+
+ toggleSettings() {
+ this.$selects.each(function () {
+ var $select = $(this);
+ var className = $select.data('field')
+ .replace(/_/g, '-')
+ .replace('access-level', 'feature');
+ ProjectNew._showOrHide($select, '.' + className);
+ });
+ }
+
+ toggleSettingsOnclick() {
+ this.$selects.on('change', this.toggleSettings);
+ }
+
+ static _showOrHide(checkElement, container) {
+ const $container = $(container);
+
+ if ($(checkElement).val() !== '0') {
+ return $container.show();
+ }
+ return $container.hide();
+ }
+
+ toggleRepoVisibility() {
+ var $repoAccessLevel = $('.js-repo-access-level select');
+ var $lfsEnabledOption = $('.js-lfs-enabled select');
+ var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
+ var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
+ var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
+
+ this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
+ .nextAll()
+ .hide();
+
+ $repoAccessLevel
+ .off('change')
+ .on('change', function () {
+ var selectedVal = parseInt($repoAccessLevel.val(), 10);
+
+ this.$repoSelects.each(function () {
+ var $this = $(this);
+ var repoSelectVal = parseInt($this.val(), 10);
+
+ $this.find('option').enable();
+
+ if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
+ $this.val(selectedVal).trigger('change');
+ highlightChanges($this);
+ }
- if (selectedVal) {
- this.$repoSelects.removeClass('disabled');
+ $this.find("option[value='" + selectedVal + "']").nextAll().disable();
+ });
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.removeClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = '';
- }
- } else {
- this.$repoSelects.addClass('disabled');
+ if (selectedVal) {
+ this.$repoSelects.removeClass('disabled');
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.val('false').addClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = 'none';
- containerRegistryCheckbox.checked = false;
- }
+ if ($lfsEnabledOption.length) {
+ $lfsEnabledOption.removeClass('disabled');
+ highlightChanges($lfsEnabledOption);
+ }
+ if (containerRegistry) {
+ containerRegistry.style.display = '';
}
+ } else {
+ this.$repoSelects.addClass('disabled');
- prevSelectedVal = selectedVal;
- }.bind(this));
- };
+ if ($lfsEnabledOption.length) {
+ $lfsEnabledOption.val('false').addClass('disabled');
+ highlightChanges($lfsEnabledOption);
+ }
+ if (containerRegistry) {
+ containerRegistry.style.display = 'none';
+ containerRegistryCheckbox.checked = false;
+ }
+ }
- return ProjectNew;
- })();
-}).call(window);
+ prevSelectedVal = selectedVal;
+ }.bind(this));
+ }
+}
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index bffc85e6315..07a49d1506c 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -2,79 +2,73 @@
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
-(function () {
- this.ProjectSelect = (function () {
- function ProjectSelect() {
- $('.ajax-project-select').each(function(i, select) {
- var placeholder;
- const simpleFilter = $(select).data('simple-filter') || false;
- this.groupId = $(select).data('group-id');
- this.includeGroups = $(select).data('include-groups');
- this.allProjects = $(select).data('all-projects') || false;
- this.orderBy = $(select).data('order-by') || 'id';
- this.withIssuesEnabled = $(select).data('with-issues-enabled');
- this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
+export default function projectSelect() {
+ $('.ajax-project-select').each(function(i, select) {
+ var placeholder;
+ const simpleFilter = $(select).data('simple-filter') || false;
+ this.groupId = $(select).data('group-id');
+ this.includeGroups = $(select).data('include-groups');
+ this.allProjects = $(select).data('all-projects') || false;
+ this.orderBy = $(select).data('order-by') || 'id';
+ this.withIssuesEnabled = $(select).data('with-issues-enabled');
+ this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
- placeholder = "Search for project";
- if (this.includeGroups) {
- placeholder += " or group";
- }
+ placeholder = "Search for project";
+ if (this.includeGroups) {
+ placeholder += " or group";
+ }
- $(select).select2({
- placeholder: placeholder,
- minimumInputLength: 0,
- query: (function (_this) {
- return function (query) {
- var finalCallback, projectsCallback;
- finalCallback = function (projects) {
+ $(select).select2({
+ placeholder: placeholder,
+ minimumInputLength: 0,
+ query: (function (_this) {
+ return function (query) {
+ var finalCallback, projectsCallback;
+ finalCallback = function (projects) {
+ var data;
+ data = {
+ results: projects
+ };
+ return query.callback(data);
+ };
+ if (_this.includeGroups) {
+ projectsCallback = function (projects) {
+ var groupsCallback;
+ groupsCallback = function (groups) {
var data;
- data = {
- results: projects
- };
- return query.callback(data);
+ data = groups.concat(projects);
+ return finalCallback(data);
};
- if (_this.includeGroups) {
- projectsCallback = function (projects) {
- var groupsCallback;
- groupsCallback = function (groups) {
- var data;
- data = groups.concat(projects);
- return finalCallback(data);
- };
- return Api.groups(query.term, {}, groupsCallback);
- };
- } else {
- projectsCallback = finalCallback;
- }
- if (_this.groupId) {
- return Api.groupProjects(_this.groupId, query.term, projectsCallback);
- } else {
- return Api.projects(query.term, {
- order_by: _this.orderBy,
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- membership: !_this.allProjects,
- }, projectsCallback);
- }
+ return Api.groups(query.term, {}, groupsCallback);
};
- })(this),
- id: function(project) {
- if (simpleFilter) return project.id;
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
- });
- },
- text: function (project) {
- return project.name_with_namespace || project.name;
- },
- dropdownCssClass: "ajax-project-dropdown"
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (_this.groupId) {
+ return Api.groupProjects(_this.groupId, query.term, projectsCallback);
+ } else {
+ return Api.projects(query.term, {
+ order_by: _this.orderBy,
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ membership: !_this.allProjects,
+ }, projectsCallback);
+ }
+ };
+ })(this),
+ id: function(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
});
- if (simpleFilter) return select;
- return new ProjectSelectComboButton(select);
- });
- }
-
- return ProjectSelect;
- })();
-}).call(window);
+ },
+ text: function (project) {
+ return project.name_with_namespace || project.name;
+ },
+ dropdownCssClass: "ajax-project-dropdown"
+ });
+ if (simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
+}
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
deleted file mode 100644
index 3a51c1f26ac..00000000000
--- a/app/assets/javascripts/project_show.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife */
-
-(function() {
- this.ProjectShow = (function() {
- function ProjectShow() {}
-
- return ProjectShow;
- })();
-}).call(window);
-
-// I kept class for future
diff --git a/app/assets/javascripts/project_variables.js b/app/assets/javascripts/project_variables.js
index 4ee2e49306d..567c311f119 100644
--- a/app/assets/javascripts/project_variables.js
+++ b/app/assets/javascripts/project_variables.js
@@ -1,43 +1,39 @@
-(() => {
- const HIDDEN_VALUE_TEXT = '******';
- class ProjectVariables {
- constructor() {
- this.$revealBtn = $('.js-btn-toggle-reveal-values');
- this.$revealBtn.on('click', this.toggleRevealState.bind(this));
- }
+const HIDDEN_VALUE_TEXT = '******';
+
+export default class ProjectVariables {
+ constructor() {
+ this.$revealBtn = $('.js-btn-toggle-reveal-values');
+ this.$revealBtn.on('click', this.toggleRevealState.bind(this));
+ }
- toggleRevealState(e) {
- e.preventDefault();
+ toggleRevealState(e) {
+ e.preventDefault();
- const oldStatus = this.$revealBtn.attr('data-status');
- let newStatus = 'hidden';
- let newAction = 'Reveal Values';
+ const oldStatus = this.$revealBtn.attr('data-status');
+ let newStatus = 'hidden';
+ let newAction = 'Reveal Values';
- if (oldStatus === 'hidden') {
- newStatus = 'revealed';
- newAction = 'Hide Values';
- }
+ if (oldStatus === 'hidden') {
+ newStatus = 'revealed';
+ newAction = 'Hide Values';
+ }
- this.$revealBtn.attr('data-status', newStatus);
+ this.$revealBtn.attr('data-status', newStatus);
- const $variables = $('.variable-value');
+ const $variables = $('.variable-value');
- $variables.each((_, variable) => {
- const $variable = $(variable);
- let newText = HIDDEN_VALUE_TEXT;
+ $variables.each((_, variable) => {
+ const $variable = $(variable);
+ let newText = HIDDEN_VALUE_TEXT;
- if (newStatus === 'revealed') {
- newText = $variable.attr('data-value');
- }
+ if (newStatus === 'revealed') {
+ newText = $variable.attr('data-value');
+ }
- $variable.text(newText);
- });
+ $variable.text(newText);
+ });
- this.$revealBtn.text(newAction);
- }
+ this.$revealBtn.text(newAction);
}
-
- window.gl = window.gl || {};
- window.gl.ProjectVariables = ProjectVariables;
-})();
+}
diff --git a/app/assets/javascripts/projects/ci_cd_settings_bundle.js b/app/assets/javascripts/projects/ci_cd_settings_bundle.js
new file mode 100644
index 00000000000..90e418f6771
--- /dev/null
+++ b/app/assets/javascripts/projects/ci_cd_settings_bundle.js
@@ -0,0 +1,19 @@
+function updateAutoDevopsRadios(radioWrappers) {
+ radioWrappers.forEach((radioWrapper) => {
+ const radio = radioWrapper.querySelector('.js-auto-devops-enable-radio');
+ const runPipelineCheckboxWrapper = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox-wrapper');
+ const runPipelineCheckbox = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox');
+
+ if (runPipelineCheckbox) {
+ runPipelineCheckbox.checked = radio.checked;
+ runPipelineCheckboxWrapper.classList.toggle('hide', !radio.checked);
+ }
+ });
+}
+
+export default function initCiCdSettings() {
+ const radioWrappers = document.querySelectorAll('.js-auto-devops-enable-radio-wrapper');
+ radioWrappers.forEach(radioWrapper =>
+ radioWrapper.addEventListener('change', () => updateAutoDevopsRadios(radioWrappers)),
+ );
+}
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
index 80c5d39f736..8fce4c63872 100644
--- a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
@@ -1,5 +1,5 @@
<script>
-import projectFeatureToggle from './project_feature_toggle.vue';
+import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
export default {
props: {
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
deleted file mode 100644
index 2403c60186a..00000000000
--- a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-export default {
- props: {
- name: {
- type: String,
- required: false,
- default: '',
- },
- value: {
- type: Boolean,
- required: true,
- },
- disabledInput: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- model: {
- prop: 'value',
- event: 'change',
- },
-
- methods: {
- toggleFeature() {
- if (!this.disabledInput) this.$emit('change', !this.value);
- },
- },
-};
-</script>
-
-<template>
- <label class="toggle-wrapper">
- <input
- v-if="name"
- type="hidden"
- :name="name"
- :value="value"
- />
- <button
- type="button"
- aria-label="Toggle"
- class="project-feature-toggle"
- data-enabled-text="Enabled"
- data-disabled-text="Disabled"
- :class="{ checked: value, disabled: disabledInput }"
- @click="toggleFeature"
- />
- </label>
-</template>
diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
index 326d9105666..639429baf26 100644
--- a/app/assets/javascripts/projects/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
@@ -1,6 +1,6 @@
<script>
import projectFeatureSetting from './project_feature_setting.vue';
-import projectFeatureToggle from './project_feature_toggle.vue';
+import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external';
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
index fe5179de206..d482a7025de 100644
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
@@ -48,6 +48,27 @@ export default {
}
return this.projectName;
},
+ /**
+ * Smartly truncates project namespace by doing two things;
+ * 1. Only include Group names in path by removing project name
+ * 2. Only include first and last group names in the path
+ * when namespace has more than 2 groups present
+ *
+ * First part (removal of project name from namespace) can be
+ * done from backend but doing so involves migration of
+ * existing project namespaces which is not wise thing to do.
+ */
+ truncatedNamespace() {
+ const namespaceArr = this.namespace.split(' / ');
+ namespaceArr.splice(-1, 1);
+ let namespace = namespaceArr.join(' / ');
+
+ if (namespaceArr.length > 2) {
+ namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
+ }
+
+ return namespace;
+ },
},
};
</script>
@@ -87,9 +108,7 @@ export default {
<div
class="project-namespace"
:title="namespace"
- >
- {{namespace}}
- </div>
+ >{{truncatedNamespace}}</div>
</div>
</a>
</li>
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index bcdc0fd67b8..c91a0d9ba41 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -1,15 +1,15 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
+import renderMath from './render_math';
+import renderMermaid from './render_mermaid';
// Render Gitlab flavoured Markdown
//
-// Delegates to syntax highlight and render math
+// Delegates to syntax highlight and render math & mermaid diagrams.
//
-(function() {
- $.fn.renderGFM = function() {
- this.find('.js-syntax-highlight').syntaxHighlight();
- this.find('.js-render-math').renderMath();
- return this;
- };
+$.fn.renderGFM = function renderGFM() {
+ this.find('.js-syntax-highlight').syntaxHighlight();
+ renderMath(this.find('.js-render-math'));
+ renderMermaid(this.find('.js-render-mermaid'));
+ return this;
+};
- $(() => $('body').renderGFM());
-}).call(window);
+$(() => $('body').renderGFM());
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index 8b3fee49cb9..a759992cd54 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -1,4 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len, no-console */
/* global katex */
// Renders math using KaTeX in any element with the
@@ -8,49 +7,45 @@
//
// <code class="js-render-math"></div>
//
-(function() {
// Only load once
- var katexLoaded = false;
+let katexLoaded = false;
- // Loop over all math elements and render math
- var renderWithKaTeX = function (elements) {
- elements.each(function () {
- var mathNode = $('<span></span>');
- var $this = $(this);
+// Loop over all math elements and render math
+function renderWithKaTeX(elements) {
+ elements.each(function katexElementsLoop() {
+ const mathNode = $('<span></span>');
+ const $this = $(this);
- var display = $this.attr('data-math-style') === 'display';
- try {
- katex.render($this.text(), mathNode.get(0), { displayMode: display });
- mathNode.insertAfter($this);
- $this.remove();
- } catch (err) {
- // What can we do??
- console.log(err.message);
- }
- });
- };
+ const display = $this.attr('data-math-style') === 'display';
+ try {
+ katex.render($this.text(), mathNode.get(0), { displayMode: display });
+ mathNode.insertAfter($this);
+ $this.remove();
+ } catch (err) {
+ throw err;
+ }
+ });
+}
- $.fn.renderMath = function() {
- var $this = this;
- if ($this.length === 0) return;
+export default function renderMath($els) {
+ if (!$els.length) return;
- if (katexLoaded) renderWithKaTeX($this);
- else {
- // Request CSS file so it is in the cache
- $.get(gon.katex_css_url, function() {
- var css = $('<link>',
- { rel: 'stylesheet',
- type: 'text/css',
- href: gon.katex_css_url,
- });
- css.appendTo('head');
+ if (katexLoaded) {
+ renderWithKaTeX($els);
+ } else {
+ $.get(gon.katex_css_url, () => {
+ const css = $('<link>', {
+ rel: 'stylesheet',
+ type: 'text/css',
+ href: gon.katex_css_url,
+ });
+ css.appendTo('head');
- // Load KaTeX js
- $.getScript(gon.katex_js_url, function() {
- katexLoaded = true;
- renderWithKaTeX($this); // Run KaTeX
- });
+ // Load KaTeX js
+ $.getScript(gon.katex_js_url, () => {
+ katexLoaded = true;
+ renderWithKaTeX($els); // Run KaTeX
});
- }
- };
-}).call(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js
new file mode 100644
index 00000000000..41942c04a4e
--- /dev/null
+++ b/app/assets/javascripts/render_mermaid.js
@@ -0,0 +1,32 @@
+// Renders diagrams and flowcharts from text using Mermaid in any element with the
+// `js-render-mermaid` class.
+//
+// Example markup:
+//
+// <pre class="js-render-mermaid">
+// graph TD;
+// A-- > B;
+// A-- > C;
+// B-- > D;
+// C-- > D;
+// </pre>
+//
+
+import Flash from './flash';
+
+export default function renderMermaid($els) {
+ if (!$els.length) return;
+
+ import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => {
+ mermaid.initialize({
+ loadOnStart: false,
+ theme: 'neutral',
+ });
+
+ $els.each((i, el) => {
+ mermaid.init(undefined, el);
+ });
+ }).catch((err) => {
+ Flash(`Can't load mermaid module: ${err}`);
+ });
+}
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list.vue b/app/assets/javascripts/repo/components/commit_sidebar/list.vue
new file mode 100644
index 00000000000..fb862e7bf01
--- /dev/null
+++ b/app/assets/javascripts/repo/components/commit_sidebar/list.vue
@@ -0,0 +1,89 @@
+<script>
+ 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,
+ },
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ toggleCollapsed() {
+ this.$emit('toggleCollapsed');
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="multi-file-commit-panel-section">
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': collapsed,
+ }"
+ >
+ <icon
+ name="list-bulleted"
+ :size="18"
+ css-classes="append-right-default"
+ />
+ <template v-if="!collapsed">
+ {{ title }}
+ <button
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ @click="toggleCollapsed"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-angle-double-right"
+ >
+ </i>
+ </button>
+ </template>
+ </header>
+ <div class="multi-file-commit-list">
+ <list-collapsed
+ v-if="collapsed"
+ />
+ <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>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue
new file mode 100644
index 00000000000..6a0262f271b
--- /dev/null
+++ b/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue
@@ -0,0 +1,35 @@
+<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/repo/components/commit_sidebar/list_item.vue b/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue
new file mode 100644
index 00000000000..742f746e02f
--- /dev/null
+++ b/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue
@@ -0,0 +1,36 @@
+<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/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
index 98117802016..a00e1e9d809 100644
--- a/app/assets/javascripts/repo/components/repo.vue
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -40,20 +40,24 @@ export default {
</script>
<template>
- <div class="repository-view">
- <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}">
- <repo-sidebar/>
- <div
- v-if="isCollapsed"
- class="panel-right"
- >
- <repo-tabs/>
- <component
- :is="currentBlobView"
- />
- <repo-file-buttons/>
- </div>
+ <div
+ class="multi-file"
+ :class="{
+ 'is-collapsed': isCollapsed
+ }"
+ >
+ <repo-sidebar/>
+ <div
+ v-if="isCollapsed"
+ class="multi-file-edit-pane"
+ >
+ <repo-tabs />
+ <component
+ class="multi-file-edit-pane-content"
+ :is="currentBlobView"
+ />
+ <repo-file-buttons />
</div>
- <repo-commit-section v-if="changedFiles.length" />
+ <repo-commit-section />
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index 377e3d65348..d3344d0c8dc 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -1,11 +1,18 @@
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
+import tooltip from '../../vue_shared/directives/tooltip';
+import icon from '../../vue_shared/components/icon.vue';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
-import { n__ } from '../../locale';
+import commitFilesList from './commit_sidebar/list.vue';
export default {
components: {
PopupDialog,
+ icon,
+ commitFilesList,
+ },
+ directives: {
+ tooltip,
},
data() {
return {
@@ -13,6 +20,7 @@ export default {
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
+ collapsed: true,
};
},
computed: {
@@ -23,10 +31,10 @@ export default {
'changedFiles',
]),
commitButtonDisabled() {
- return !this.commitMessage || this.submitCommitsLoading;
+ return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
},
- commitButtonText() {
- return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
+ commitMessageCount() {
+ return this.commitMessage.length;
},
},
methods: {
@@ -77,12 +85,20 @@ export default {
this.submitCommitsLoading = false;
});
},
+ toggleCollapsed() {
+ this.collapsed = !this.collapsed;
+ },
},
};
</script>
<template>
-<div id="commit-area">
+<div
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': collapsed,
+ }"
+>
<popup-dialog
v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')"
@@ -92,78 +108,71 @@ export default {
@toggle="showNewBranchDialog = false"
@submit="makeCommit(true)"
/>
+ <button
+ v-if="collapsed"
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
+ @click="toggleCollapsed"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-angle-double-left"
+ >
+ </i>
+ </button>
+ <commit-files-list
+ title="Staged"
+ :file-list="changedFiles"
+ :collapsed="collapsed"
+ @toggleCollapsed="toggleCollapsed"
+ />
<form
- class="form-horizontal"
- @submit.prevent="tryCommit()">
- <fieldset>
- <div class="form-group">
- <label class="col-md-4 control-label staged-files">
- Staged files ({{changedFiles.length}})
- </label>
- <div class="col-md-6">
- <ul class="list-unstyled changed-files">
- <li
- v-for="(file, index) in changedFiles"
- :key="index">
- <span class="help-block">
- {{ file.path }}
- </span>
- </li>
- </ul>
- </div>
- </div>
- <div class="form-group">
- <label
- class="col-md-4 control-label"
- for="commit-message">
- Commit message
- </label>
- <div class="col-md-6">
- <textarea
- id="commit-message"
- class="form-control"
- name="commit-message"
- v-model="commitMessage">
- </textarea>
- </div>
- </div>
- <div class="form-group target-branch">
- <label
- class="col-md-4 control-label"
- for="target-branch">
- Target branch
- </label>
- <div class="col-md-6">
- <span class="help-block">
- {{currentBranch}}
- </span>
- </div>
- </div>
- <div class="col-md-offset-4 col-md-6">
- <button
- type="submit"
- :disabled="commitButtonDisabled"
- class="btn btn-success">
- <i
- v-if="submitCommitsLoading"
- class="js-commit-loading-icon fa fa-spinner fa-spin"
- aria-hidden="true"
- aria-label="loading">
- </i>
- <span class="commit-summary">
- {{ commitButtonText }}
- </span>
- </button>
- </div>
- <div class="col-md-offset-4 col-md-6">
- <div class="checkbox">
- <label>
- <input type="checkbox" v-model="startNewMR">
- <span>Start a <strong>new merge request</strong> with these changes</span>
- </label>
- </div>
+ class="form-horizontal multi-file-commit-form"
+ @submit.prevent="tryCommit"
+ v-if="!collapsed"
+ >
+ <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"
+ >
+ <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>
- </fieldset>
+ </div>
</form>
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index 1c864b176b1..f37cbd1e961 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -3,19 +3,18 @@
import { mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
+import Editor from '../lib/editor';
export default {
- destroyed() {
- if (this.monacoInstance) {
- this.monacoInstance.destroy();
- }
+ beforeDestroy() {
+ this.editor.dispose();
},
mounted() {
- if (this.monaco) {
+ if (this.editor && monaco) {
this.initMonaco();
} else {
monacoLoader(['vs/editor/editor.main'], () => {
- this.monaco = monaco;
+ this.editor = Editor.create(monaco);
this.initMonaco();
});
@@ -29,47 +28,25 @@ export default {
initMonaco() {
if (this.shouldHideEditor) return;
- if (this.monacoInstance) {
- this.monacoInstance.setModel(null);
- }
+ this.editor.clearEditor();
this.getRawFileData(this.activeFile)
.then(() => {
- if (!this.monacoInstance) {
- this.monacoInstance = this.monaco.editor.create(this.$el, {
- model: null,
- readOnly: false,
- contextmenu: true,
- scrollBeyondLastLine: false,
- });
-
- this.languages = this.monaco.languages.getLanguages();
-
- this.addMonacoEvents();
- }
-
- this.setupEditor();
+ this.editor.createInstance(this.$refs.editor);
})
+ .then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.'));
},
setupEditor() {
if (!this.activeFile) return;
- const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
- const foundLang = this.languages.find(lang =>
- lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
- );
- const newModel = this.monaco.editor.createModel(
- content, foundLang ? foundLang.id : 'plaintext',
- );
+ const model = this.editor.createModel(this.activeFile);
- this.monacoInstance.setModel(newModel);
- },
- addMonacoEvents() {
- this.monacoInstance.onKeyUp(() => {
+ this.editor.attachModel(model);
+ model.onChange((m) => {
this.changeFileContent({
file: this.activeFile,
- content: this.monacoInstance.getValue(),
+ content: m.getValue(),
});
});
},
@@ -99,9 +76,14 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
- v-if="shouldHideEditor"
+ v-show="shouldHideEditor"
v-html="activeFile.html"
>
</div>
+ <div
+ v-show="!shouldHideEditor"
+ ref="editor"
+ >
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
index 5be47d568e7..75787ad6103 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -55,7 +55,7 @@
class="file"
@click.prevent="clickedTreeRow(file)">
<td
- class="multi-file-table-col-name"
+ class="multi-file-table-name"
:colspan="submoduleColSpan"
>
<i
@@ -85,12 +85,11 @@
</td>
<template v-if="!isCollapsed && !isSubmodule">
- <td class="hidden-sm hidden-xs">
+ <td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a
v-if="file.lastCommit.message"
@click.stop
:href="file.lastCommit.url"
- class="commit-message"
>
{{ file.lastCommit.message }}
</a>
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue
index dd948ee84fb..34f0d51819a 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue
@@ -22,12 +22,12 @@ export default {
<template>
<div
v-if="showButtons"
- class="repo-file-buttons"
+ class="multi-file-editor-btn-group"
>
<a
:href="activeFile.rawPath"
target="_blank"
- class="btn btn-default raw"
+ class="btn btn-default btn-sm raw"
rel="noopener noreferrer">
{{ rawDownloadButtonLabel }}
</a>
@@ -38,17 +38,17 @@ export default {
aria-label="File actions">
<a
:href="activeFile.blamePath"
- class="btn btn-default blame">
+ class="btn btn-default btn-sm blame">
Blame
</a>
<a
:href="activeFile.commitsPath"
- class="btn btn-default history">
+ class="btn btn-default btn-sm history">
History
</a>
<a
:href="activeFile.permalink"
- class="btn btn-default permalink">
+ class="btn btn-default btn-sm permalink">
Permalink
</a>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
index d1883299bd9..6ce9267f598 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -32,10 +32,12 @@ export default {
</script>
<template>
-<div class="blob-viewer-container">
+<div>
<div
v-if="!activeFile.renderError"
- v-html="activeFile.html">
+ v-html="activeFile.html"
+ class="multi-file-preview-holder"
+ >
</div>
<div
v-else-if="activeFile.tempFile"
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index 9365b09326f..4ea21913129 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -44,20 +44,16 @@ export default {
</script>
<template>
-<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">
+<div class="ide-file-list">
<table class="table">
<thead>
<tr>
<th
v-if="isCollapsed"
- class="repo-file-options title"
>
- <strong class="clgray">
- {{ projectName }}
- </strong>
</th>
<template v-else>
- <th class="name multi-file-table-col-name">
+ <th class="name multi-file-table-name">
Name
</th>
<th class="hidden-sm hidden-xs last-commit">
@@ -79,7 +75,7 @@ export default {
:key="n"
/>
<repo-file
- v-for="(file, index) in treeList"
+ v-for="file in treeList"
:key="file.key"
:file="file"
/>
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue
index da0714c368c..fb29a60df66 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/repo/components/repo_tab.vue
@@ -36,27 +36,32 @@ export default {
<template>
<li
- :class="{ active : tab.active }"
@click="setFileActive(tab)"
>
<button
type="button"
- class="close-btn"
+ class="multi-file-tab-close"
@click.stop.prevent="closeFile({ file: tab })"
- :aria-label="closeLabel">
+ :aria-label="closeLabel"
+ :class="{
+ 'modified': tab.changed,
+ }"
+ :disabled="tab.changed"
+ >
<i
class="fa"
:class="changedClass"
- aria-hidden="true">
+ aria-hidden="true"
+ >
</i>
</button>
- <a
- href="#"
- class="repo-tab"
+ <div
+ class="multi-file-tab"
+ :class="{active : tab.active }"
:title="tab.url"
- @click.prevent.stop="setFileActive(tab)">
- {{tab.name}}
- </a>
+ >
+ {{ tab.name }}
+ </div>
</li>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue
index 59beae53e8d..ab0bef4f0ac 100644
--- a/app/assets/javascripts/repo/components/repo_tabs.vue
+++ b/app/assets/javascripts/repo/components/repo_tabs.vue
@@ -16,14 +16,12 @@
<template>
<ul
- id="tabs"
- class="list-unstyled"
+ class="multi-file-tabs list-unstyled append-bottom-0"
>
<repo-tab
v-for="tab in openFiles"
:key="tab.id"
:tab="tab"
/>
- <li class="tabs-divider" />
</ul>
</template>
diff --git a/app/assets/javascripts/repo/lib/common/disposable.js b/app/assets/javascripts/repo/lib/common/disposable.js
new file mode 100644
index 00000000000..84b29bdb600
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/disposable.js
@@ -0,0 +1,14 @@
+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/repo/lib/common/model.js b/app/assets/javascripts/repo/lib/common/model.js
new file mode 100644
index 00000000000..23c4811e6c0
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/model.js
@@ -0,0 +1,56 @@
+/* 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 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/repo/lib/common/model_manager.js b/app/assets/javascripts/repo/lib/common/model_manager.js
new file mode 100644
index 00000000000..fd462252795
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/model_manager.js
@@ -0,0 +1,32 @@
+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/repo/lib/decorations/controller.js b/app/assets/javascripts/repo/lib/decorations/controller.js
new file mode 100644
index 00000000000..0954b7973c4
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/decorations/controller.js
@@ -0,0 +1,43 @@
+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/repo/lib/diff/controller.js b/app/assets/javascripts/repo/lib/diff/controller.js
new file mode 100644
index 00000000000..dc0b1c95e59
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/controller.js
@@ -0,0 +1,71 @@
+/* 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/repo/lib/diff/diff.js b/app/assets/javascripts/repo/lib/diff/diff.js
new file mode 100644
index 00000000000..0e37f5c4704
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/diff.js
@@ -0,0 +1,30 @@
+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/repo/lib/diff/diff_worker.js b/app/assets/javascripts/repo/lib/diff/diff_worker.js
new file mode 100644
index 00000000000..e74c4046330
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/diff_worker.js
@@ -0,0 +1,10 @@
+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/repo/lib/editor.js b/app/assets/javascripts/repo/lib/editor.js
new file mode 100644
index 00000000000..db499444402
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/editor.js
@@ -0,0 +1,79 @@
+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),
+ );
+ }
+
+ createInstance(domElement) {
+ if (!this.instance) {
+ this.disposable.add(
+ this.instance = this.monaco.editor.create(domElement, {
+ model: null,
+ readOnly: false,
+ contextmenu: true,
+ scrollBeyondLastLine: false,
+ }),
+ this.dirtyDiffController = new DirtyDiffController(
+ this.modelManager, this.decorationsController,
+ ),
+ );
+ }
+ }
+
+ createModel(file) {
+ return this.modelManager.addModel(file);
+ }
+
+ attachModel(model) {
+ this.instance.setModel(model.getModel());
+ 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;
+ }, {}));
+
+ this.dirtyDiffController.reDecorate(model);
+ }
+
+ clearEditor() {
+ if (this.instance) {
+ this.instance.setModel(null);
+ }
+ }
+
+ dispose() {
+ this.disposable.dispose();
+
+ // dispose main monaco instance
+ if (this.instance) {
+ this.instance = null;
+ }
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/editor_options.js b/app/assets/javascripts/repo/lib/editor_options.js
new file mode 100644
index 00000000000..701affc466e
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/editor_options.js
@@ -0,0 +1,2 @@
+export default [{
+}];
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js
index 2fb45dcb03c..994d325e991 100644
--- a/app/assets/javascripts/repo/services/index.js
+++ b/app/assets/javascripts/repo/services/index.js
@@ -16,6 +16,10 @@ export default {
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());
},
diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js
index 1ed05ac6e35..5ce9f449905 100644
--- a/app/assets/javascripts/repo/stores/getters.js
+++ b/app/assets/javascripts/repo/stores/getters.js
@@ -34,3 +34,7 @@ export const canEditFile = (state) => {
openedFiles.length &&
(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/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index a41548bd694..fa7f6825d7e 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -15,7 +15,7 @@ import Cookies from 'js-cookie';
Sidebar.prototype.removeListeners = function () {
this.sidebar.off('click', '.sidebar-collapsed-icon');
- $('.dropdown').off('hidden.gl.dropdown');
+ this.sidebar.off('hidden.gl.dropdown');
$('.dropdown').off('loading.gl.dropdown');
$('.dropdown').off('loaded.gl.dropdown');
$(document).off('click', '.js-sidebar-toggle');
@@ -25,7 +25,7 @@ import Cookies from 'js-cookie';
const $document = $(document);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
- $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
+ this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
@@ -180,7 +180,7 @@ import Cookies from 'js-cookie';
var $block, sidebar;
sidebar = e.data;
e.preventDefault();
- $block = $(this).closest('.block');
+ $block = $(e.target).closest('.block');
return sidebar.sidebarDropdownHidden($block);
};
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 22a9a34dda3..6ee4d487c0b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,10 +1,12 @@
<script>
import Flash from '../../../flash';
import editForm from './edit_form.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
editForm,
+ Icon,
},
props: {
isConfidential: {
@@ -26,11 +28,8 @@ export default {
};
},
computed: {
- faEye() {
- const eye = this.isConfidential ? 'fa-eye-slash' : 'fa-eye';
- return {
- [eye]: true,
- };
+ confidentialityIcon() {
+ return this.isConfidential ? 'eye-slash' : 'eye';
},
},
methods: {
@@ -49,7 +48,11 @@ export default {
<template>
<div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon">
- <i class="fa" :class="faEye" aria-hidden="true"></i>
+ <icon
+ :name="confidentialityIcon"
+ :size="16"
+ aria-hidden="true">
+ </icon>
</div>
<div class="title hide-collapsed">
Confidentiality
@@ -70,11 +73,21 @@ export default {
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!isConfidential" class="no-value sidebar-item-value">
- <i class="fa fa-eye sidebar-item-icon"></i>
+ <icon
+ name="eye"
+ :size="16"
+ aria-hidden="true"
+ class="sidebar-item-icon inline">
+ </icon>
Not confidential
</div>
<div v-else class="value sidebar-item-value hide-collapsed">
- <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i>
+ <icon
+ name="eye-slash"
+ :size="16"
+ aria-hidden="true"
+ class="sidebar-item-icon inline is-active">
+ </icon>
This issue is confidential
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index c7a6edc7c70..242e826d471 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -18,11 +18,6 @@ export default {
required: true,
type: Function,
},
-
- issuableType: {
- required: true,
- type: String,
- },
},
mixins: [
@@ -39,13 +34,13 @@ export default {
<div class="dropdown open">
<div class="dropdown-menu sidebar-item-warning-message">
<p class="text" v-if="isLocked">
- Unlock this {{ issuableDisplayName(issuableType) }}?
+ Unlock this {{ issuableDisplayName }}?
<strong>Everyone</strong>
will be able to comment.
</p>
<p class="text" v-else>
- Lock this {{ issuableDisplayName(issuableType) }}?
+ Lock this {{ issuableDisplayName }}?
Only
<strong>project members</strong>
will be able to comment.
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index c4b2900e020..04c3a96bf74 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -2,6 +2,7 @@
/* global Flash */
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
+import Icon from '../../../vue_shared/components/icon.vue';
export default {
props: {
@@ -22,11 +23,6 @@ export default {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
},
-
- issuableType: {
- required: true,
- type: String,
- },
},
mixins: [
@@ -35,11 +31,12 @@ export default {
components: {
editForm,
+ Icon,
},
computed: {
- lockIconClass() {
- return this.isLocked ? 'fa-lock' : 'fa-unlock';
+ lockIcon() {
+ return this.isLocked ? 'lock' : 'lock-open';
},
isLockDialogOpen() {
@@ -57,7 +54,7 @@ export default {
discussion_locked: locked,
})
.then(() => location.reload())
- .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}`)));
+ .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
},
},
};
@@ -66,15 +63,16 @@ export default {
<template>
<div class="block issuable-sidebar-item lock">
<div class="sidebar-collapsed-icon">
- <i
- class="fa"
- :class="lockIconClass"
+ <icon
+ :name="lockIcon"
+ :size="16"
aria-hidden="true"
- ></i>
+ class="sidebar-item-icon is-active">
+ </icon>
</div>
<div class="title hide-collapsed">
- Lock {{issuableDisplayName(issuableType) }}
+ Lock {{ issuableDisplayName }}
<button
v-if="isEditable"
class="pull-right lock-edit btn btn-blank"
@@ -98,10 +96,12 @@ export default {
v-if="isLocked"
class="value sidebar-item-value"
>
- <i
+ <icon
+ name="lock"
+ :size="16"
aria-hidden="true"
- class="fa fa-lock sidebar-item-icon is-active"
- ></i>
+ class="sidebar-item-icon inline is-active">
+ </icon>
{{ __('Locked') }}
</div>
@@ -109,10 +109,12 @@ export default {
v-else
class="no-value sidebar-item-value hide-collapsed"
>
- <i
+ <icon
+ name="lock-open"
+ :size="16"
aria-hidden="true"
- class="fa fa-unlock sidebar-item-icon"
- ></i>
+ class="sidebar-item-icon inline">
+ </icon>
{{ __('Unlocked') }}
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 4ad3d469f25..25acc099699 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
+import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
export default {
@@ -21,7 +22,7 @@ export default {
onToggleSubscription() {
this.mediator.toggleSubscription()
.catch(() => {
- Flash('Error occurred when toggling the notification subscription');
+ Flash(__('Error occurred when toggling the notification subscription'));
});
},
},
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index a3a8213d63a..940e1764f3d 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: false,
},
+ id: {
+ type: Number,
+ required: false,
+ },
},
components: {
loadingButton,
@@ -32,7 +36,7 @@ export default {
},
methods: {
toggleSubscription() {
- eventHub.$emit('toggleSubscription');
+ eventHub.$emit('toggleSubscription', this.id);
},
},
};
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
new file mode 100644
index 00000000000..4032f156b15
--- /dev/null
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -0,0 +1,104 @@
+import Vue from 'vue';
+import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
+import SidebarAssignees from './components/assignees/sidebar_assignees';
+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';
+import sidebarParticipants from './components/participants/sidebar_participants.vue';
+import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
+import Translate from '../vue_shared/translate';
+
+Vue.use(Translate);
+
+function mountConfidentialComponent(mediator) {
+ const el = document.getElementById('js-confidential-entry-point');
+
+ if (!el) return;
+
+ const dataNode = document.getElementById('js-confidential-issue-data');
+ const initialData = JSON.parse(dataNode.innerHTML);
+
+ const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
+
+ new ConfidentialComp({
+ propsData: {
+ isConfidential: initialData.is_confidential,
+ isEditable: initialData.is_editable,
+ service: mediator.service,
+ },
+ }).$mount(el);
+}
+
+function mountLockComponent(mediator) {
+ const el = document.getElementById('js-lock-entry-point');
+
+ if (!el) return;
+
+ const dataNode = document.getElementById('js-lock-issue-data');
+ const initialData = JSON.parse(dataNode.innerHTML);
+
+ const LockComp = Vue.extend(LockIssueSidebar);
+
+ new LockComp({
+ propsData: {
+ isLocked: initialData.is_locked,
+ isEditable: initialData.is_editable,
+ mediator,
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
+ },
+ }).$mount(el);
+}
+
+function mountParticipantsComponent() {
+ const el = document.querySelector('.js-sidebar-participants-entry-point');
+
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ sidebarParticipants,
+ },
+ render: createElement => createElement('sidebar-participants', {}),
+ });
+}
+
+function mountSubscriptionsComponent() {
+ const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
+
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ sidebarSubscriptions,
+ },
+ render: createElement => createElement('sidebar-subscriptions', {}),
+ });
+}
+
+function mount(mediator) {
+ const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
+ // Only create the sidebarAssignees vue app if it is found in the DOM
+ // We currently do not use sidebarAssignees for the MR page
+ if (sidebarAssigneesEl) {
+ new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
+ }
+
+ mountConfidentialComponent(mediator);
+ mountLockComponent(mediator);
+ mountParticipantsComponent();
+ mountSubscriptionsComponent();
+
+ new SidebarMoveIssue(
+ mediator,
+ $('.js-move-issue'),
+ $('.js-move-issue-confirmation-button'),
+ ).init();
+
+ new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
+}
+
+export default mount;
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 2650bb725d4..f78287e504b 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,110 +1,12 @@
-import Vue from 'vue';
-import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
-import SidebarAssignees from './components/assignees/sidebar_assignees';
-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';
-import sidebarParticipants from './components/participants/sidebar_participants.vue';
-import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
-import Translate from '../vue_shared/translate';
-
import Mediator from './sidebar_mediator';
-
-Vue.use(Translate);
-
-function mountConfidentialComponent(mediator) {
- const el = document.getElementById('js-confidential-entry-point');
-
- if (!el) return;
-
- const dataNode = document.getElementById('js-confidential-issue-data');
- const initialData = JSON.parse(dataNode.innerHTML);
-
- const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
-
- new ConfidentialComp({
- propsData: {
- isConfidential: initialData.is_confidential,
- isEditable: initialData.is_editable,
- service: mediator.service,
- },
- }).$mount(el);
-}
-
-function mountLockComponent(mediator) {
- const el = document.getElementById('js-lock-entry-point');
-
- if (!el) return;
-
- const dataNode = document.getElementById('js-lock-issue-data');
- const initialData = JSON.parse(dataNode.innerHTML);
-
- const LockComp = Vue.extend(LockIssueSidebar);
-
- new LockComp({
- propsData: {
- isLocked: initialData.is_locked,
- isEditable: initialData.is_editable,
- mediator,
- issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
- },
- }).$mount(el);
-}
-
-function mountParticipantsComponent() {
- const el = document.querySelector('.js-sidebar-participants-entry-point');
-
- if (!el) return;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- sidebarParticipants,
- },
- render: createElement => createElement('sidebar-participants', {}),
- });
-}
-
-function mountSubscriptionsComponent() {
- const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
-
- if (!el) return;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- sidebarSubscriptions,
- },
- render: createElement => createElement('sidebar-subscriptions', {}),
- });
-}
+import mountSidebar from './mount_sidebar';
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
mediator.fetch();
- const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
- // Only create the sidebarAssignees vue app if it is found in the DOM
- // We currently do not use sidebarAssignees for the MR page
- if (sidebarAssigneesEl) {
- new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
- }
-
- mountConfidentialComponent(mediator);
- mountLockComponent(mediator);
- mountParticipantsComponent();
- mountSubscriptionsComponent();
-
- new SidebarMoveIssue(
- mediator,
- $('.js-move-issue'),
- $('.js-move-issue-confirmation-button'),
- ).init();
-
- new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
+ mountSidebar(mediator);
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 2bda5a47791..d4c07a188b3 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -5,19 +5,23 @@ import Store from './stores/sidebar_store';
export default class SidebarMediator {
constructor(options) {
if (!SidebarMediator.singleton) {
- this.store = new Store(options);
- this.service = new Service({
- endpoint: options.endpoint,
- toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
- moveIssueEndpoint: options.moveIssueEndpoint,
- projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
- });
- SidebarMediator.singleton = this;
+ this.initSingleton(options);
}
return SidebarMediator.singleton;
}
+ initSingleton(options) {
+ this.store = new Store(options);
+ this.service = new Service({
+ endpoint: options.endpoint,
+ toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
+ moveIssueEndpoint: options.moveIssueEndpoint,
+ projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
+ });
+ SidebarMediator.singleton = this;
+ }
+
assignYourself() {
this.store.addAssignee(this.store.currentUser);
}
@@ -35,17 +39,21 @@ export default class SidebarMediator {
}
fetch() {
- this.service.get()
+ return this.service.get()
.then(response => response.json())
.then((data) => {
- this.store.setAssigneeData(data);
- this.store.setTimeTrackingData(data);
- this.store.setParticipantsData(data);
- this.store.setSubscriptionsData(data);
+ this.processFetchedData(data);
})
.catch(() => new Flash('Error occurred when fetching sidebar data'));
}
+ processFetchedData(data) {
+ this.store.setAssigneeData(data);
+ this.store.setTimeTrackingData(data);
+ this.store.setParticipantsData(data);
+ this.store.setSubscriptionsData(data);
+ }
+
toggleSubscription() {
this.store.setFetchingState('subscriptions', true);
return this.service.toggleSubscription()
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 3150221b685..73eb25e2333 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -15,6 +15,7 @@ export default class SidebarStore {
participants: true,
subscriptions: true,
};
+ this.isLoading = {};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
@@ -55,6 +56,10 @@ export default class SidebarStore {
this.isFetching[key] = value;
}
+ setLoadingState(key, value) {
+ this.isLoading[key] = value;
+ }
+
addAssignee(assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(assignee);
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 1a8dc085772..d5606e153f6 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,5 +1,6 @@
import Flash from './flash';
import { __, s__ } from './locale';
+import { spriteIcon } from './lib/utils/common_utils';
export default class Star {
constructor() {
@@ -7,16 +8,18 @@ export default class Star {
.on('ajax:success', function handleSuccess(e, data) {
const $this = $(this);
const $starSpan = $this.find('span');
- const $starIcon = $this.find('i');
+ const $startIcon = $this.find('svg');
function toggleStar(isStarred) {
$this.parent().find('.star-count').text(data.star_count);
if (isStarred) {
$starSpan.removeClass('starred').text(s__('StarProject|Star'));
- $starIcon.removeClass('fa-star').addClass('fa-star-o');
+ $startIcon.remove();
+ $this.prepend(spriteIcon('star-o'));
} else {
$starSpan.addClass('starred').text(__('Unstar'));
- $starIcon.removeClass('fa-star-o').addClass('fa-star');
+ $startIcon.remove();
+ $this.prepend(spriteIcon('star'));
}
}
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
deleted file mode 100644
index bb4d68fcd49..00000000000
--- a/app/assets/javascripts/subscription.js
+++ /dev/null
@@ -1,45 +0,0 @@
-class Subscription {
- constructor(containerElm) {
- this.containerElm = containerElm;
-
- const subscribeButton = containerElm.querySelector('.js-subscribe-button');
- if (subscribeButton) {
- // remove class so we don't bind twice
- subscribeButton.classList.remove('js-subscribe-button');
- subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
- }
- }
-
- toggleSubscription(event) {
- const button = event.currentTarget;
- const buttonSpan = button.querySelector('span');
- if (!buttonSpan || button.classList.contains('disabled')) {
- return;
- }
- button.classList.add('disabled');
-
- const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
- const toggleActionUrl = this.containerElm.dataset.url;
-
- $.post(toggleActionUrl, () => {
- button.classList.remove('disabled');
-
- // hack to allow this to work with the issue boards Vue object
- if (document.querySelector('html').classList.contains('issue-boards-page')) {
- gl.issueBoards.boardStoreIssueSet(
- 'subscribed',
- !gl.issueBoards.BoardsStore.detail.issue.subscribed,
- );
- } else {
- buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
- }
- });
- }
-
- static bindAll(selector) {
- [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
- }
-}
-
-window.gl = window.gl || {};
-window.gl.Subscription = Subscription;
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 37e39ce5477..1ab4c2229ca 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,33 +1,24 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
+export default function subscriptionSelect() {
+ $('.js-subscription-event').each((i, element) => {
+ const fieldName = $(element).data('field-name');
-class SubscriptionSelect {
- constructor() {
- $('.js-subscription-event').each(function(i, el) {
- var fieldName;
- fieldName = $(el).data("field-name");
- return $(el).glDropdown({
- selectable: true,
- fieldName: fieldName,
- toggleLabel: (function(_this) {
- return function(selected, el, instance) {
- var $item, label;
- label = 'Subscription';
- $item = instance.dropdown.find('.is-active');
- if ($item.length) {
- label = $item.text();
- }
- return label;
- };
- })(this),
- clicked: function(options) {
- return options.e.preventDefault();
- },
- id: function(obj, el) {
- return $(el).data("id");
+ return $(element).glDropdown({
+ selectable: true,
+ fieldName,
+ toggleLabel(selected, el, instance) {
+ let label = 'Subscription';
+ const $item = instance.dropdown.find('.is-active');
+ if ($item.length) {
+ label = $item.text();
}
- });
+ return label;
+ },
+ clicked(options) {
+ return options.e.preventDefault();
+ },
+ id(obj, el) {
+ return $(el).data('id');
+ },
});
- }
+ });
}
-
-window.SubscriptionSelect = SubscriptionSelect;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
index 4998a47b691..eeb990908f6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
@@ -14,7 +14,7 @@ export default {
statusObj() {
return {
group: this.status,
- icon: `icon_status_${this.status}`,
+ icon: `status_${this.status}`,
};
},
},
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 2e5f9f1088f..4216660da8c 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -6,13 +6,15 @@
Sample configuration:
<icon
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :tooltip-text="tooltipText"
- tooltip-placement="top"
+ name="retry"
+ :size="32"
+ css-classes="top"
/>
*/
+ // only allow classes in images.scss e.g. s12
+ const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
+
export default {
props: {
name: {
@@ -23,7 +25,10 @@
size: {
type: Number,
required: false,
- default: 0,
+ default: 16,
+ validator(value) {
+ return validSizes.includes(value);
+ },
},
cssClasses: {
@@ -43,10 +48,11 @@
},
};
</script>
+
<template>
<svg
:class="[iconSizeClass, cssClasses]">
- <use
+ <use
v-bind="{'xlink:href':spriteHref}"/>
</svg>
</template>
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 16c0a8efcd2..564fc5029af 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -1,4 +1,6 @@
<script>
+ import Icon from '../../../vue_shared/components/icon.vue';
+
export default {
props: {
isLocked: {
@@ -14,12 +16,16 @@
},
},
+ components: {
+ Icon,
+ },
+
computed: {
- iconClass() {
- return {
- 'fa-eye-slash': this.isConfidential,
- 'fa-lock': this.isLocked,
- };
+ warningIcon() {
+ if (this.isConfidential) return 'eye-slash';
+ if (this.isLocked) return 'lock';
+
+ return '';
},
isLockedAndConfidential() {
@@ -30,12 +36,13 @@
</script>
<template>
<div class="issuable-note-warning">
- <i
- aria-hidden="true"
- class="fa icon"
- :class="iconClass"
- v-if="!isLockedAndConfidential"
- ></i>
+ <icon
+ :name="warningIcon"
+ :size="16"
+ class="icon inline"
+ aria-hidden="true"
+ v-if="!isLockedAndConfidential">
+ </icon>
<span v-if="isLockedAndConfidential">
{{ __('This issue is confidential and locked.') }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index a873e00d0f3..15e3d713448 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -25,6 +25,16 @@
type: String,
required: false,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -92,7 +102,7 @@
/*
GLForm class handles all the toolbar buttons
*/
- return new GLForm($(this.$refs['gl-form']), true);
+ return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
@@ -129,6 +139,7 @@
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
+ :can-attach-file="canAttachFile"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 70f5fc1d664..6c575d8eb49 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -50,7 +50,9 @@
<template>
<div class="md-header">
<ul class="nav-links clearfix">
- <li :class="{ active: !previewMarkdown }">
+ <li
+ class="md-header-tab"
+ :class="{ active: !previewMarkdown }">
<a
class="js-write-link"
href="#md-write-holder"
@@ -59,7 +61,9 @@
Write
</a>
</li>
- <li :class="{ active: previewMarkdown }">
+ <li
+ class="md-header-tab"
+ :class="{ active: previewMarkdown }">
<a
class="js-preview-link"
href="#md-preview-holder"
@@ -68,56 +72,52 @@
Preview
</a>
</li>
- <li class="pull-right">
- <div class="toolbar-group">
- <toolbar-button
- tag="**"
- button-title="Add bold text"
- icon="bold" />
- <toolbar-button
- tag="*"
- button-title="Add italic text"
- icon="italic" />
- <toolbar-button
- tag="> "
- :prepend="true"
- button-title="Insert a quote"
- icon="quote" />
- <toolbar-button
- tag="`"
- tag-block="```"
- button-title="Insert code"
- icon="code" />
- <toolbar-button
- tag="* "
- :prepend="true"
- button-title="Add a bullet list"
- icon="list-bulleted" />
- <toolbar-button
- tag="1. "
- :prepend="true"
- button-title="Add a numbered list"
- icon="list-numbered" />
- <toolbar-button
- tag="* [ ] "
- :prepend="true"
- button-title="Add a task list"
- icon="task-done" />
- </div>
- <div class="toolbar-group">
- <button
- v-tooltip
- aria-label="Go full screen"
- class="toolbar-btn js-zen-enter"
- data-container="body"
- tabindex="-1"
- title="Go full screen"
- type="button">
- <icon
- name="screen-full">
- </icon>
- </button>
- </div>
+ <li class="md-header-toolbar">
+ <toolbar-button
+ tag="**"
+ button-title="Add bold text"
+ icon="bold" />
+ <toolbar-button
+ tag="*"
+ button-title="Add italic text"
+ icon="italic" />
+ <toolbar-button
+ tag="> "
+ :prepend="true"
+ button-title="Insert a quote"
+ icon="quote" />
+ <toolbar-button
+ tag="`"
+ tag-block="```"
+ button-title="Insert code"
+ icon="code" />
+ <toolbar-button
+ tag="* "
+ :prepend="true"
+ button-title="Add a bullet list"
+ icon="list-bulleted" />
+ <toolbar-button
+ tag="1. "
+ :prepend="true"
+ button-title="Add a numbered list"
+ icon="list-numbered" />
+ <toolbar-button
+ tag="* [ ] "
+ :prepend="true"
+ button-title="Add a task list"
+ icon="task-done" />
+ <button
+ v-tooltip
+ aria-label="Go full screen"
+ class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
+ data-container="body"
+ tabindex="-1"
+ title="Go full screen"
+ type="button">
+ <icon
+ name="screen-full">
+ </icon>
+ </button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 65fe7bbd94e..ea2509d2839 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -9,6 +9,11 @@
type: String,
required: false,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -41,7 +46,10 @@
are supported
</template>
</div>
- <span class="uploading-container">
+ <span
+ v-if="canAttachFile"
+ class="uploading-container"
+ >
<span class="uploading-progress-container hide">
<i
class="fa fa-file-image-o toolbar-button-icon"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index b930fb116a3..e3e41f8f0ca 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -40,7 +40,7 @@
<button
v-tooltip
type="button"
- class="toolbar-btn js-md hidden-xs"
+ class="toolbar-btn js-md"
tabindex="-1"
data-container="body"
:data-md-tag="tag"
diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index 07befd23500..a2ddd565170 100644
--- a/app/assets/javascripts/pipelines/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -1,11 +1,36 @@
<script>
+ /**
+ * Given an array of tabs, renders non linked bootstrap tabs.
+ * When a tab is clicked it will trigger an event and provide the clicked scope.
+ *
+ * This component is used in apps that handle the API call.
+ * If you only need to change the URL this component should not be used.
+ *
+ * @example
+ * <navigation-tabs
+ * :tabs="[
+ * {
+ * name: String,
+ * scope: String,
+ * count: Number || Undefined,
+ * isActive: Boolean,
+ * },
+ * ]"
+ * @onChangeTab="onChangeTab"
+ * />
+ */
export default {
- name: 'PipelineNavigationTabs',
+ name: 'NavigationTabs',
props: {
tabs: {
type: Array,
required: true,
},
+ scope: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
mounted() {
$(document).trigger('init.scrolling-tabs');
@@ -34,7 +59,7 @@
<a
role="button"
@click="onTabClick(tab)"
- :class="`js-pipelines-tab-${tab.scope}`"
+ :class="`js-${scope}-tab-${tab.scope}`"
>
{{ tab.name }}
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 98f8f32557d..2248699c399 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -17,7 +17,7 @@
* />
*/
import { mapGetters } from 'vuex';
- import issueNoteHeader from '../../../notes/components/issue_note_header.vue';
+ import noteHeader from '~/notes/components/note_header.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
export default {
@@ -29,7 +29,7 @@
},
},
components: {
- issueNoteHeader,
+ noteHeader,
},
computed: {
...mapGetters([
@@ -60,12 +60,12 @@
</div>
<div class="timeline-content">
<div class="note-header">
- <issue-note-header
+ <note-header
:author="note.author"
:created-at="note.created_at"
:note-id="note.id"
:action-text-html="note.note_html"
- />
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
new file mode 100644
index 00000000000..d8d974a2ff7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -0,0 +1,79 @@
+<script>
+ import Pikaday from 'pikaday';
+ import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
+
+ export default {
+ name: 'datePicker',
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: 'Date picker',
+ },
+ selectedDate: {
+ type: Date,
+ required: false,
+ },
+ minDate: {
+ type: Date,
+ required: false,
+ },
+ maxDate: {
+ type: Date,
+ required: false,
+ },
+ },
+ methods: {
+ selected(dateText) {
+ this.$emit('newDateSelected', this.calendar.toString(dateText));
+ },
+ toggled() {
+ this.$emit('hidePicker');
+ },
+ },
+ mounted() {
+ this.calendar = new Pikaday({
+ field: this.$el.querySelector('.dropdown-menu-toggle'),
+ theme: 'gitlab-theme animate-picker',
+ format: 'yyyy-mm-dd',
+ container: this.$el,
+ defaultDate: this.selectedDate,
+ setDefaultDate: !!this.selectedDate,
+ minDate: this.minDate,
+ maxDate: this.maxDate,
+ parse: dateString => parsePikadayDate(dateString),
+ toString: date => pikadayToString(date),
+ onSelect: this.selected.bind(this),
+ onClose: this.toggled.bind(this),
+ });
+
+ this.$el.append(this.calendar.el);
+ this.calendar.show();
+ },
+ beforeDestroy() {
+ this.calendar.destroy();
+ },
+ };
+</script>
+
+<template>
+ <div class="pikaday-container">
+ <div class="dropdown open">
+ <button
+ type="button"
+ class="dropdown-menu-toggle"
+ data-toggle="dropdown"
+ @click="toggled"
+ >
+ <span class="dropdown-toggle-text">
+ {{label}}
+ </span>
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true"
+ >
+ </i>
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
new file mode 100644
index 00000000000..a88e1310131
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -0,0 +1,46 @@
+<script>
+ export default {
+ name: 'collapsedCalendarIcon',
+ props: {
+ containerClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ text: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showIcon: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ methods: {
+ click() {
+ this.$emit('click');
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ :class="containerClass"
+ @click="click"
+ >
+ <i
+ v-if="showIcon"
+ class="fa fa-calendar"
+ aria-hidden="true"
+ >
+ </i>
+ <slot>
+ <span>
+ {{ text }}
+ </span>
+ </slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
new file mode 100644
index 00000000000..9ede5553bc5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
@@ -0,0 +1,109 @@
+<script>
+ import { dateInWords } from '../../../lib/utils/datetime_utility';
+ import toggleSidebar from './toggle_sidebar.vue';
+ import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
+
+ export default {
+ name: 'sidebarCollapsedGroupedDatePicker',
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showToggleSidebar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ minDate: {
+ type: Date,
+ required: false,
+ },
+ maxDate: {
+ type: Date,
+ required: false,
+ },
+ disableClickableIcons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ components: {
+ toggleSidebar,
+ collapsedCalendarIcon,
+ },
+ computed: {
+ hasMinAndMaxDates() {
+ return this.minDate && this.maxDate;
+ },
+ hasNoMinAndMaxDates() {
+ return !this.minDate && !this.maxDate;
+ },
+ showMinDateBlock() {
+ return this.minDate || this.hasNoMinAndMaxDates;
+ },
+ showFromText() {
+ return !this.maxDate && this.minDate;
+ },
+ iconClass() {
+ const disabledClass = this.disableClickableIcons ? 'disabled' : '';
+ return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`;
+ },
+ },
+ methods: {
+ toggleSidebar() {
+ this.$emit('toggleCollapse');
+ },
+ dateText(dateType = 'min') {
+ const date = this[`${dateType}Date`];
+ const dateWords = dateInWords(date, true);
+ const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
+
+ return date ? parsedDateWords : 'None';
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="block sidebar-grouped-item">
+ <div
+ v-if="showToggleSidebar"
+ class="issuable-sidebar-header"
+ >
+ <toggle-sidebar
+ :collapsed="collapsed"
+ @toggle="toggleSidebar"
+ />
+ </div>
+ <collapsed-calendar-icon
+ v-if="showMinDateBlock"
+ :container-class="iconClass"
+ @click="toggleSidebar"
+ >
+ <span class="sidebar-collapsed-value">
+ <span v-if="showFromText">From</span>
+ <span>{{ dateText('min') }}</span>
+ </span>
+ </collapsed-calendar-icon>
+ <div
+ v-if="hasMinAndMaxDates"
+ class="text-center sidebar-collapsed-divider"
+ >
+ -
+ </div>
+ <collapsed-calendar-icon
+ v-if="maxDate"
+ :container-class="iconClass"
+ :show-icon="!minDate"
+ @click="toggleSidebar"
+ >
+ <span class="sidebar-collapsed-value">
+ <span v-if="!minDate">Until</span>
+ <span>{{ dateText('max') }}</span>
+ </span>
+ </collapsed-calendar-icon>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
new file mode 100644
index 00000000000..9c3413377a3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -0,0 +1,163 @@
+<script>
+ import datePicker from '../pikaday.vue';
+ import loadingIcon from '../loading_icon.vue';
+ import toggleSidebar from './toggle_sidebar.vue';
+ import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
+ import { dateInWords } from '../../../lib/utils/datetime_utility';
+
+ export default {
+ name: 'sidebarDatePicker',
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showToggleSidebar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ editable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: 'Date picker',
+ },
+ selectedDate: {
+ type: Date,
+ required: false,
+ },
+ minDate: {
+ type: Date,
+ required: false,
+ },
+ maxDate: {
+ type: Date,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ editing: false,
+ };
+ },
+ components: {
+ datePicker,
+ toggleSidebar,
+ loadingIcon,
+ collapsedCalendarIcon,
+ },
+ computed: {
+ selectedAndEditable() {
+ return this.selectedDate && this.editable;
+ },
+ selectedDateWords() {
+ return dateInWords(this.selectedDate, true);
+ },
+ collapsedText() {
+ return this.selectedDateWords ? this.selectedDateWords : 'None';
+ },
+ },
+ methods: {
+ stopEditing() {
+ this.editing = false;
+ },
+ toggleDatePicker() {
+ this.editing = !this.editing;
+ },
+ newDateSelected(date = null) {
+ this.date = date;
+ this.editing = false;
+ this.$emit('saveDate', date);
+ },
+ toggleSidebar() {
+ this.$emit('toggleCollapse');
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="block">
+ <div class="issuable-sidebar-header">
+ <toggle-sidebar
+ :collapsed="collapsed"
+ @toggle="toggleSidebar"
+ />
+ </div>
+ <collapsed-calendar-icon
+ class="sidebar-collapsed-icon"
+ :text="collapsedText"
+ />
+ <div class="title">
+ {{ label }}
+ <loading-icon
+ v-if="isLoading"
+ :inline="true"
+ />
+ <div class="pull-right">
+ <button
+ v-if="editable && !editing"
+ type="button"
+ class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action"
+ @click="toggleDatePicker"
+ >
+ Edit
+ </button>
+ <toggle-sidebar
+ v-if="showToggleSidebar"
+ :collapsed="collapsed"
+ @toggle="toggleSidebar"
+ />
+ </div>
+ </div>
+ <div class="value">
+ <date-picker
+ v-if="editing"
+ :selected-date="selectedDate"
+ :min-date="minDate"
+ :max-date="maxDate"
+ :label="label"
+ @newDateSelected="newDateSelected"
+ @hidePicker="stopEditing"
+ />
+ <span
+ v-else
+ class="value-content"
+ >
+ <template v-if="selectedDate">
+ <strong>{{ selectedDateWords }}</strong>
+ <span
+ v-if="selectedAndEditable"
+ class="no-value"
+ >
+ -
+ <button
+ type="button"
+ class="btn-blank btn-link btn-secondary-hover-link"
+ @click="newDateSelected(null)"
+ >
+ remove
+ </button>
+ </span>
+ </template>
+ <span
+ v-else
+ class="no-value"
+ >
+ None
+ </span>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
new file mode 100644
index 00000000000..5ae76adad71
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -0,0 +1,30 @@
+<script>
+ export default {
+ name: 'toggleSidebar',
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
+ },
+ },
+ };
+</script>
+
+<template>
+ <button
+ type="button"
+ class="btn btn-blank gutter-toggle btn-sidebar-action"
+ @click="toggle"
+ >
+ <i
+ aria-label="toggle collapse"
+ class="fa"
+ :class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }"
+ ></i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
new file mode 100644
index 00000000000..ddc9ddbc3a3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -0,0 +1,77 @@
+<script>
+ import loadingIcon from './loading_icon.vue';
+
+ export default {
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: Boolean,
+ required: true,
+ },
+ disabledInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ enabledText: {
+ type: String,
+ required: false,
+ default: 'Enabled',
+ },
+ disabledText: {
+ type: String,
+ required: false,
+ default: 'Disabled',
+ },
+ },
+
+ components: {
+ loadingIcon,
+ },
+
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+
+ methods: {
+ toggleFeature() {
+ if (!this.disabledInput) this.$emit('change', !this.value);
+ },
+ },
+ };
+</script>
+
+<template>
+ <label class="toggle-wrapper">
+ <input
+ type="hidden"
+ :name="name"
+ :value="value"
+ />
+ <button
+ type="button"
+ aria-label="Toggle"
+ class="project-feature-toggle"
+ :data-enabled-text="enabledText"
+ :data-disabled-text="disabledText"
+ :class="{
+ 'is-checked': value,
+ 'is-disabled': disabledInput,
+ 'is-loading': isLoading
+ }"
+ @click="toggleFeature"
+ >
+ <loadingIcon class="loading-icon" />
+ </button>
+ </label>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
new file mode 100644
index 00000000000..f94cc670edf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
@@ -0,0 +1,42 @@
+/**
+ * API callbacks for pagination and tabs
+ * shared between Pipelines and Environments table.
+ *
+ * Components need to have `scope`, `page` and `requestData`
+ */
+import {
+ historyPushState,
+ buildUrlWithCurrentLocation,
+} from '../../lib/utils/common_utils';
+
+export default {
+ methods: {
+ onChangeTab(scope) {
+ this.updateContent({ scope, page: '1' });
+ },
+
+ onChangePage(page) {
+ /* URLS parameters are strings, we need to parse to match types */
+ this.updateContent({ scope: this.scope, page: Number(page).toString() });
+ },
+
+ updateInternalState(parameters) {
+ // stop polling
+ this.poll.stop();
+
+ const queryString = Object.keys(parameters).map((parameter) => {
+ const value = parameters[parameter];
+ // update internal state for UI
+ this[parameter] = value;
+ return `${parameter}=${encodeURIComponent(value)}`;
+ }).join('&');
+
+ // update polling parameters
+ this.requestData = parameters;
+
+ historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
+
+ this.isLoading = true;
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/mixins/issuable.js b/app/assets/javascripts/vue_shared/mixins/issuable.js
index 263361587e0..fab0919d96e 100644
--- a/app/assets/javascripts/vue_shared/mixins/issuable.js
+++ b/app/assets/javascripts/vue_shared/mixins/issuable.js
@@ -1,9 +1,14 @@
export default {
- methods: {
- issuableDisplayName(issuableType) {
- const displayName = issuableType.replace(/_/, ' ');
+ props: {
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
- return this.__ ? this.__(displayName) : displayName;
+ computed: {
+ issuableDisplayName() {
+ return this.issuableType.replace(/_/g, ' ');
},
},
};
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index cba7b9227cd..06a86f3b94a 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -71,7 +71,7 @@ export default class ZenMode {
this.active_textarea = this.active_backdrop.find('textarea');
// Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style');
- return this.active_textarea.focus();
+ this.active_textarea.focus();
}
exit() {
@@ -81,7 +81,11 @@ export default class ZenMode {
this.scrollTo(this.active_textarea);
this.active_textarea = null;
this.active_backdrop = null;
- return Dropzone.forElement('.div-dropzone').enable();
+
+ const $dropzone = $('.div-dropzone');
+ if ($dropzone && !$dropzone.hasClass('js-invalid-dropzone')) {
+ Dropzone.forElement('.div-dropzone').enable();
+ }
}
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 66212be1b8f..43b16d3cf7d 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -44,6 +44,7 @@
@import "framework/tabs";
@import "framework/timeline";
@import "framework/tooltips";
+@import "framework/toggle";
@import "framework/typography";
@import "framework/zen";
@import "framework/blank";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 374988bb590..728f9a27aca 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -125,7 +125,7 @@
@include transition(border-color);
}
-.note-action-button .link-highlight,
+.note-action-button,
.toolbar-btn,
.dropdown-toggle-caret {
@include transition(color);
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index 10f9e9b70b0..9982a5779af 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -56,6 +56,12 @@
}
}
+.blank-state-center {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ text-align: center;
+}
+
.blank-state {
padding: 20px;
border: 1px solid $border-color;
@@ -66,7 +72,10 @@
align-items: center;
padding: 50px 30px;
}
+}
+.blank-state,
+.blank-state-center {
.blank-state-icon {
svg {
display: block;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index b2f26cf7159..cdc2aa196dd 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -88,17 +88,6 @@
border-color: $border-dark;
color: $color;
}
-
- svg {
-
- path {
- fill: $color;
- }
-
- use {
- stroke: $color;
- }
- }
}
@mixin btn-green {
@@ -142,6 +131,13 @@
}
}
+@mixin btn-svg {
+ height: $gl-padding;
+ width: $gl-padding;
+ top: 0;
+ vertical-align: text-top;
+}
+
.btn {
@include btn-default;
@include btn-white;
@@ -408,6 +404,7 @@
padding: 0;
background: transparent;
border: 0;
+ border-radius: 0;
&:hover,
&:active,
@@ -417,3 +414,29 @@
box-shadow: none;
}
}
+
+.btn-link.btn-secondary-hover-link {
+ color: $gl-text-color-secondary;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+}
+
+.btn-link.btn-primary-hover-link {
+ color: inherit;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+}
+
+.btn-svg svg {
+ @include btn-svg;
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 5f5b5657a2f..a42fab50db5 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -4,11 +4,41 @@
.cred { color: $common-red; }
.cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; }
+
+.text-plain,
+.text-plain:hover {
+ color: $gl-text-color;
+}
+
.text-secondary {
color: $gl-text-color-secondary;
}
-.underlined-link { text-decoration: underline; }
+.text-primary,
+.text-primary:hover {
+ color: $brand-primary;
+}
+
+.text-success,
+.text-success:hover {
+ color: $brand-success;
+}
+
+.text-danger,
+.text-danger:hover {
+ color: $brand-danger;
+}
+
+.text-warning,
+.text-warning:hover {
+ color: $brand-warning;
+}
+
+.text-info,
+.text-info:hover {
+ color: $brand-info;
+}
+
.hint { font-style: italic; color: $hint-color; }
.light { color: $common-gray; }
@@ -428,6 +458,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
+.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; }
.prepend-top-default { margin-top: $gl-padding !important; }
diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss
index 320f458630a..b73932eb7e1 100644
--- a/app/assets/stylesheets/framework/contextual-sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual-sidebar.scss
@@ -40,12 +40,6 @@
a:hover {
background-color: $link-hover-background;
color: $gl-text-color;
-
- .settings-avatar {
- svg {
- fill: $gl-text-color;
- }
- }
}
.avatar-container {
@@ -138,10 +132,6 @@
color: $gl-text-color-secondary;
}
- svg {
- fill: $gl-text-color-secondary;
- }
-
.nav-item-name {
flex: 1;
}
@@ -224,10 +214,6 @@
&:hover {
color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
}
}
@@ -338,7 +324,6 @@
align-items: center;
svg {
- fill: $gl-text-color-secondary;
margin-right: 8px;
}
@@ -349,10 +334,6 @@
&:hover {
background-color: $border-color;
color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 579bd48fac6..30d5d7a653b 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -1002,6 +1002,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
+ white-space: nowrap;
}
&:hover {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index c2a3cd16e67..609f33582e1 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -364,6 +364,18 @@ span.idiff {
float: none;
}
}
+
+ @media (max-width: $screen-xs-max) {
+ display: block;
+
+ .file-actions {
+ white-space: normal;
+
+ .btn-group {
+ padding-top: 5px;
+ }
+ }
+ }
}
.is-stl-loading {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 74b6b31b07e..cf8165eab5b 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -305,16 +305,11 @@
color: $gl-text-color;
border-color: $dropdown-input-focus-border;
outline: none;
-
- svg {
- fill: $gl-text-color;
- }
}
svg {
height: 14px;
width: 14px;
- fill: $gl-text-color-secondary;
vertical-align: middle;
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index e1b086ebb2b..88ce119ee3a 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -34,8 +34,15 @@
}
}
+ .flash-success {
+ @extend .alert;
+ @extend .alert-success;
+ margin: 0;
+ }
+
.flash-notice,
- .flash-alert {
+ .flash-alert,
+ .flash-success {
border-radius: $border-radius-default;
.container-fluid,
@@ -48,7 +55,8 @@
margin-bottom: 0;
.flash-notice,
- .flash-alert {
+ .flash-alert,
+ .flash-success {
border-radius: 0;
}
}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index 34a35734acc..5621505996d 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -14,6 +14,5 @@
&:hover {
background-color: $user-mention-bg-hover;
- text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index dc591c06c88..db36e27fa74 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -30,10 +30,6 @@
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
-
- svg {
- fill: currentColor;
- }
}
&.line-separator {
@@ -51,10 +47,6 @@
color: $color-200;
> a {
- svg {
- fill: $color-200;
- }
-
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $color-200;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 2218b5705fc..f985a3aea5c 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -235,10 +235,6 @@
opacity: 1;
color: $white-light;
- svg {
- fill: currentColor;
- }
-
&.header-user-dropdown-toggle .header-user-avatar {
border-color: $white-light;
}
@@ -269,14 +265,6 @@
font-size: 20px;
}
}
-
- &.active > a,
- &.dropdown.open > a {
-
- svg {
- fill: currentColor;
- }
- }
}
}
}
@@ -289,10 +277,6 @@
text-decoration: none;
outline: 0;
color: $white-light;
-
- svg {
- fill: currentColor;
- }
}
> a {
@@ -307,10 +291,6 @@
border-radius: $border-radius-default;
height: 32px;
font-weight: $gl-font-weight-bold;
-
- svg {
- fill: currentColor;
- }
}
&.line-separator {
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index ef864e8f6a9..e2084e8f85f 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,15 +1,11 @@
.ci-status-icon-success,
.ci-status-icon-passed {
- color: $green-500;
-
svg {
fill: $green-500;
}
}
.ci-status-icon-failed {
- color: $gl-danger;
-
svg {
fill: $gl-danger;
}
@@ -18,16 +14,12 @@
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
- color: $orange-500;
-
svg {
fill: $orange-500;
}
}
.ci-status-icon-running {
- color: $blue-400;
-
svg {
fill: $blue-400;
}
@@ -36,8 +28,6 @@
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
- color: $gl-text-color;
-
svg {
fill: $gl-text-color;
}
@@ -45,16 +35,12 @@
.ci-status-icon-created,
.ci-status-icon-skipped {
- color: $gray-darkest;
-
svg {
fill: $gray-darkest;
}
}
.ci-status-icon-manual {
- color: $gl-text-color;
-
svg {
fill: $gl-text-color;
}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 6819fd88b7f..78a8e57ddbb 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -27,6 +27,8 @@
}
svg {
+ fill: currentColor;
+
&.s8 { @include svg-size(8px); }
&.s12 { @include svg-size(12px); }
&.s16 { @include svg-size(16px); }
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index ad3bb0e35d1..cd505bcaf6d 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -449,6 +449,12 @@ ul.indent-list {
}
}
+.namespace-title {
+ .tooltip-inner {
+ max-width: 350px;
+ }
+}
+
ul.group-list-tree {
li.group-row {
&.has-description {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index cd6f94fb354..5389eb0a5f2 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -57,6 +57,7 @@
.md-header {
.nav-links {
a {
+ width: 100%;
padding-top: 0;
line-height: 19px;
@@ -72,6 +73,28 @@
}
}
+.md-header-tab {
+ @media(max-width: $screen-xs-max) {
+ flex: 1;
+ width: 100%;
+ border-bottom: 1px solid $border-color;
+ text-align: center;
+ }
+}
+
+.md-header-toolbar {
+ margin-left: auto;
+
+ @media(max-width: $screen-xs-max) {
+ flex: none;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ padding-top: $gl-padding-top;
+ padding-bottom: $gl-padding-top;
+ }
+}
+
.referenced-users {
color: $gl-text-color;
padding-top: 10px;
@@ -126,16 +149,6 @@
}
}
-.toolbar-group {
- float: left;
- margin-right: -5px;
- margin-left: $gl-padding;
-
- &:first-child {
- margin-left: 0;
- }
-}
-
.toolbar-btn {
float: left;
padding: 0 7px;
@@ -158,6 +171,16 @@
}
}
+.toolbar-fullscreen-btn {
+ margin-left: $gl-padding;
+ margin-right: -5px;
+
+ @media(max-width: $screen-xs-max) {
+ margin-left: 0;
+ margin-right: 0;
+ }
+}
+
.atwho-view {
overflow-y: auto;
overflow-x: hidden;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 33012133b66..e12b5aab381 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -130,14 +130,6 @@
background-color: $color-light;
color: $color-dark;
border-color: $color-dark;
-
- svg {
- fill: $color-dark;
- }
- }
-
- svg {
- fill: $color-main;
}
}
diff --git a/app/assets/stylesheets/framework/new-nav.scss b/app/assets/stylesheets/framework/new-nav.scss
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/app/assets/stylesheets/framework/new-nav.scss
+++ /dev/null
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index bb70b270299..dbee7073975 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -134,19 +134,22 @@
}
.select2-search {
- padding: 15px 15px 5px;
+ padding: $grid-size;
.select2-drop-auto-width & {
- padding: 15px 15px 5px;
+ padding: $grid-size;
}
input {
- padding: 2px 25px 2px 5px;
+ padding: $grid-size;
background: $white-light image-url('select2.png');
+ background-clip: content-box;
+ background-origin: content-box;
background-repeat: no-repeat;
- background-position: right 0 bottom 6px;
+ background-position: right 0 bottom 0 !important;
border: 1px solid $input-border;
border-radius: $border-radius-default;
+ line-height: 16px;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
@@ -156,11 +159,16 @@
&.select2-active {
background-color: $white-light;
background-image: image-url('select2-spinner.gif') !important;
+ background-origin: content-box;
background-repeat: no-repeat;
- background-position: right 5px center !important;
+ background-position: right 6px center !important;
background-size: 16px 16px !important;
}
}
+
+ + .select2-results {
+ padding-top: 0;
+ }
}
.select2-results {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 1a19b7320a0..792981fdc48 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -43,11 +43,13 @@
}
.sidebar-collapsed-icon {
- cursor: pointer;
-
.btn {
background-color: $gray-light;
}
+
+ &:not(.disabled) {
+ cursor: pointer;
+ }
}
}
@@ -55,6 +57,10 @@
padding-right: 0;
z-index: 300;
+ .btn-sidebar-action {
+ display: inline-flex;
+ }
+
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
@@ -136,3 +142,18 @@
.issuable-sidebar {
@include new-style-dropdown;
}
+
+.pikaday-container {
+ .pika-single {
+ margin-top: 2px;
+ width: 250px;
+ }
+
+ .dropdown-menu-toggle {
+ line-height: 20px;
+ }
+}
+
+.sidebar-collapsed-icon .sidebar-collapsed-value {
+ font-size: 12px;
+}
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
new file mode 100644
index 00000000000..71765da3908
--- /dev/null
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -0,0 +1,138 @@
+/**
+* Toggle button
+*
+* @usage
+* ### Active and Inactive text should be provided as data attributes:
+* <button type="button" class="project-feature-toggle" data-enabled-text="Enabled" data-disabled-text="Disabled">
+* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
+* </button>
+
+* ### Checked should have `is-checked` class
+* <button type="button" class="project-feature-toggle is-checked" data-enabled-text="Enabled" data-disabled-text="Disabled">
+* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
+* </button>
+
+* ### Disabled should have `is-disabled` class
+* <button type="button" class="project-feature-toggle is-disabled" data-enabled-text="Enabled" data-disabled-text="Disabled" disabled="true">
+* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
+* </button>
+
+* ### Loading should have `is-loading` and an icon with `loading-icon` class
+* <button type="button" class="project-feature-toggle is-loading" data-enabled-text="Enabled" data-disabled-text="Disabled">
+* <i class="fa fa-spinner fa-spin loading-icon"></i>
+* </button>
+*/
+.project-feature-toggle {
+ position: relative;
+ border: 0;
+ outline: 0;
+ display: block;
+ width: 100px;
+ height: 24px;
+ cursor: pointer;
+ user-select: none;
+ background: $feature-toggle-color-disabled;
+ border-radius: 12px;
+ padding: 3px;
+ transition: all .4s ease;
+
+ &::selection,
+ &::before::selection,
+ &::after::selection {
+ background: none;
+ }
+
+ &::before {
+ color: $feature-toggle-text-color;
+ font-size: 12px;
+ line-height: 24px;
+ position: absolute;
+ top: 0;
+ left: 25px;
+ right: 5px;
+ text-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ animation: animate-disabled .2s ease-in;
+ content: attr(data-disabled-text);
+ }
+
+ &::after {
+ position: relative;
+ display: block;
+ content: "";
+ width: 22px;
+ height: 18px;
+ left: 0;
+ border-radius: 9px;
+ background: $feature-toggle-color;
+ transition: all .2s ease;
+ }
+
+ .loading-icon {
+ display: none;
+ font-size: 12px;
+ color: $white-light;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+
+ }
+
+ &.is-loading {
+ &::before {
+ display: none;
+ }
+
+ .loading-icon {
+ display: block;
+
+ &::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+ }
+ }
+
+ &.is-checked {
+ background: $feature-toggle-color-enabled;
+
+ &::before {
+ left: 5px;
+ right: 25px;
+ animation: animate-enabled .2s ease-in;
+ content: attr(data-enabled-text);
+ }
+
+ &::after {
+ left: calc(100% - 22px);
+ }
+ }
+
+ &.is-disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ @media (max-width: $screen-xs-min) {
+ width: 50px;
+
+ &::before,
+ &.is-checked::before {
+ display: none;
+ }
+ }
+
+ @keyframes animate-enabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+
+ @keyframes animate-disabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index d5c6ddbb4a5..1c6e2bf3074 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -195,33 +195,6 @@ summary {
}
}
-// Typography =================================================================
-
-.text-primary,
-.text-primary:hover {
- color: $brand-primary;
-}
-
-.text-success,
-.text-success:hover {
- color: $brand-success;
-}
-
-.text-danger,
-.text-danger:hover {
- color: $brand-danger;
-}
-
-.text-warning,
-.text-warning:hover {
- color: $brand-warning;
-}
-
-.text-info,
-.text-info:hover {
- color: $brand-info;
-}
-
// Prevent datetimes on tooltips to break into two lines
.local-timeago {
white-space: nowrap;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 68824ff8418..735fc4babd7 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -20,6 +20,11 @@
.ref-name {
font-size: 12px;
+
+ &:hover {
+ text-decoration: underline;
+ color: $gl-text-color;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 5a4d3ba0ee9..dbd3144b9b4 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -57,15 +57,7 @@
padding: 5px;
font-size: 36px;
- svg {
- fill: $gl-text-color;
- }
-
&:hover {
color: $black;
-
- svg {
- fill: $black;
- }
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 27b10b536a2..f139f4ab650 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -49,6 +49,7 @@
font-size: 12px;
border-radius: 0;
border: 0;
+ padding: $grid-size;
.bash {
display: block;
@@ -57,14 +58,13 @@
.top-bar {
height: 35px;
- display: flex;
- justify-content: flex-end;
background: $gray-light;
border: 1px solid $border-color;
color: $gl-text-color;
position: sticky;
position: -webkit-sticky;
top: $header-height;
+ padding: $grid-size;
&.affix {
top: $header-height;
@@ -90,9 +90,6 @@
}
.truncated-info {
- margin: 0 auto;
- align-self: center;
-
.truncated-info-size {
margin: 0 5px;
}
@@ -118,7 +115,11 @@
.controllers-buttons {
color: $gl-text-color;
- margin: 0 10px;
+ margin: 0 $grid-size;
+
+ &:last-child {
+ margin-right: 0;
+ }
}
.btn-scroll.animate {
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index e5b9e1f2de6..c303f016ff9 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -8,3 +8,23 @@
// Wait for the Vue to kick-in and render the applications block
min-height: 302px;
}
+
+.clusters-dropdown-menu {
+ max-width: 100%;
+}
+
+@include new-style-dropdown('.clusters-dropdown ');
+
+.clusters-container {
+ .nav-bar-right {
+ padding: $gl-padding-top $gl-padding;
+ }
+
+ .empty-state .svg-content img {
+ width: 145px;
+ }
+
+ .top-area .nav-controls > .btn.btn-add-cluster {
+ margin-right: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index bce94e09367..848d7f144dc 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -628,21 +628,46 @@
}
.diff-file-changes {
- width: 450px;
+ max-width: 560px;
+ width: 100%;
z-index: 150;
@media (min-width: $screen-sm-min) {
left: $gl-padding;
}
- a {
+ .diff-changed-file {
+ display: flex;
padding-top: 8px;
padding-bottom: 8px;
+ min-width: 0;
}
- .diff-changed-file {
+ .diff-file-changed-icon {
+ margin-top: 2px;
+ }
+
+ .diff-changed-file-content {
display: flex;
- align-items: center;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .diff-changed-file-name,
+ .diff-changed-file-path {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .diff-changed-file-path {
+ direction: rtl;
+ color: $gl-text-color-tertiary;
+ }
+
+ .diff-changed-stats {
+ margin-left: auto;
+ white-space: nowrap;
}
}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index dae8ccdef6c..9cc9e11bcd1 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -1,23 +1,3 @@
-.documentation-index {
- h1 {
- margin: 0;
- }
-
- h2 {
- font-size: 20px;
- }
-
- li {
- line-height: 24px;
- color: $document-index-color;
-
- a {
- margin-right: 3px;
- }
- }
-}
-
-
.shortcut-mappings {
font-size: 12px;
color: $help-shortcut-mapping-color;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 760c7c80aff..b33825a506e 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -6,28 +6,20 @@
}
.issuable-warning-icon {
- color: $orange-600;
background-color: $orange-100;
border-radius: $border-radius-default;
- padding: 5px;
margin: 0 $btn-side-margin 0 0;
width: $issuable-warning-size;
height: $issuable-warning-size;
text-align: center;
- &:first-of-type {
- margin-right: $issuable-warning-icon-margin;
+ .icon {
+ fill: $orange-600;
+ vertical-align: text-bottom;
}
-}
-.sidebar-item-icon {
- border-radius: $border-radius-default;
- padding: 5px;
- margin: 0 3px 0 -4px;
-
- &.is-active {
- color: $orange-600;
- background-color: $orange-50;
+ &:first-of-type {
+ margin-right: $issuable-warning-icon-margin;
}
}
@@ -78,14 +70,13 @@
.title {
padding: 0;
- margin-bottom: 16px;
+ margin-bottom: $gl-padding;
border-bottom: 0;
}
.btn-edit {
margin-left: auto;
- // Set height to match title height
- height: 2em;
+ height: $gl-padding * 2;
}
// Border around images in issue and MR descriptions.
@@ -119,6 +110,10 @@
padding: 6px 10px;
border-radius: $label-border-radius;
}
+
+ &:hover .color-label {
+ text-decoration: underline;
+ }
}
&.has-labels {
@@ -183,6 +178,14 @@
color: $gray-darkest;
}
}
+
+ &.assignee {
+ .author_link:hover {
+ .author {
+ text-decoration: underline;
+ }
+ }
+ }
}
.block-first {
@@ -284,10 +287,15 @@
font-weight: $gl-font-weight-normal;
}
- .no-value {
+ .no-value,
+ .btn-secondary-hover-link {
color: $gl-text-color-secondary;
}
+ .btn-secondary-hover-link:hover {
+ color: $gl-link-color;
+ }
+
.sidebar-collapsed-icon {
display: none;
}
@@ -295,6 +303,8 @@
.gutter-toggle {
margin-top: 7px;
border-left: 1px solid $border-gray-normal;
+ padding-left: 0;
+ text-align: center;
}
.title .gutter-toggle {
@@ -367,7 +377,7 @@
fill: $issuable-sidebar-color;
}
- &:hover,
+ &:hover:not(.disabled),
&:hover .todo-undone {
color: $gl-text-color;
@@ -470,7 +480,6 @@
a:not(.btn-retry) {
&:hover {
color: $md-link-color;
- text-decoration: none;
.avatar {
border-color: rgba($avatar-border, .2);
@@ -908,3 +917,21 @@
margin: 0 3px;
}
}
+
+.right-sidebar-collapsed {
+ .sidebar-grouped-item {
+ .sidebar-collapsed-icon {
+ margin-bottom: 0;
+ }
+
+ .sidebar-collapsed-divider {
+ line-height: 5px;
+ font-size: 12px;
+ color: $theme-gray-700;
+
+ + .sidebar-collapsed-icon {
+ padding-top: 0;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8bb68ad2425..8218326ae8f 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -164,12 +164,7 @@ ul.related-merge-requests > li {
}
}
-.issues-footer {
- padding-top: $gl-padding;
- padding-bottom: 37px;
-}
-
-.issue-email-modal-btn {
+.issuable-email-modal-btn {
padding: 0;
color: $gl-link-color;
background-color: transparent;
@@ -203,7 +198,24 @@ ul.related-merge-requests > li {
}
.create-mr-dropdown-wrap {
- @include new-style-dropdown;
+ .branch-message,
+ .ref-message {
+ display: none;
+ }
+
+ .ref::selection {
+ color: $placeholder-text-color;
+ }
+
+ .dropdown {
+ .dropdown-menu-toggle {
+ min-width: 285px;
+ }
+
+ .dropdown-select {
+ width: 285px;
+ }
+ }
.btn-group:not(.hide) {
display: flex;
@@ -214,15 +226,16 @@ ul.related-merge-requests > li {
flex-shrink: 0;
}
- .dropdown-menu {
+ .create-merge-request-dropdown-menu {
width: 300px;
opacity: 1;
visibility: visible;
transform: translateY(0);
display: none;
+ margin-top: 4px;
}
- .dropdown-toggle {
+ .create-merge-request-dropdown-toggle {
.fa-caret-down {
pointer-events: none;
color: inherit;
@@ -230,18 +243,50 @@ ul.related-merge-requests > li {
}
}
+ .droplab-item-ignore {
+ pointer-events: auto;
+ }
+
+ .create-item {
+ cursor: pointer;
+ margin: 0 1px;
+
+ &:hover,
+ &:focus {
+ background-color: $dropdown-item-hover-bg;
+ color: $gl-text-color;
+ }
+ }
+
+ li.divider {
+ margin: 8px 10px;
+ }
+
li:not(.divider) {
+ padding: 8px 9px;
+
+ &:last-child {
+ padding-bottom: 8px;
+ }
+
&.droplab-item-selected {
.icon-container {
i {
visibility: visible;
}
}
+
+ .description {
+ display: block;
+ }
+ }
+
+ &.droplab-item-ignore {
+ padding-top: 8px;
}
.icon-container {
float: left;
- padding-left: 6px;
i {
visibility: hidden;
@@ -249,13 +294,12 @@ ul.related-merge-requests > li {
}
.description {
- padding-left: 30px;
- font-size: 13px;
+ padding-left: 22px;
+ }
- strong {
- display: block;
- font-weight: $gl-font-weight-bold;
- }
+ input,
+ span {
+ margin: 4px 0 0;
}
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index b7985c4dea5..b2250a1ce2f 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -252,6 +252,10 @@
background: $white-light;
}
+ .login-page-broadcast {
+ margin-top: 50px;
+ }
+
.navless-container {
padding: 65px 15px; // height of footer + bottom padding of email confirmation link
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 89f93a92f2e..1e6992cb65e 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -113,6 +113,8 @@
.icon {
margin-right: $issuable-warning-icon-margin;
+ vertical-align: text-bottom;
+ fill: $orange-600;
}
+ .md-area {
@@ -137,12 +139,24 @@
}
}
-.sidebar-item-value {
- .fa {
- background-color: inherit;
+.sidebar-item-icon {
+ border-radius: $border-radius-default;
+ margin: 0 3px 0 -4px;
+ vertical-align: middle;
+
+ &.is-active {
+ fill: $orange-600;
}
}
+.sidebar-collapsed-icon .sidebar-item-icon {
+ margin: 0;
+}
+
+.sidebar-item-value .sidebar-item-icon {
+ fill: $theme-gray-700;
+}
+
.sidebar-item-warning-message {
line-height: 1.5;
padding: 16px;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 9537eeeee97..a6009ab328e 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -208,7 +208,6 @@ ul.notes {
a {
color: $gl-link-color;
- text-decoration: none;
}
p {
@@ -395,6 +394,10 @@ ul.notes {
&:focus,
&:hover {
text-decoration: none;
+
+ .note-header-author-name {
+ text-decoration: underline;
+ }
}
}
@@ -461,6 +464,10 @@ ul.notes {
.system-note-message {
white-space: normal;
}
+
+ a:hover {
+ text-decoration: underline;
+ }
}
/**
@@ -543,14 +550,7 @@ ul.notes {
}
svg {
- height: 16px;
- width: 16px;
- top: 0;
- vertical-align: text-top;
-
- path {
- fill: currentColor;
- }
+ @include btn-svg;
}
.award-control-icon-positive,
@@ -570,10 +570,6 @@ ul.notes {
.link-highlight {
color: $gl-link-color;
fill: $gl-link-color;
-
- svg {
- fill: $gl-link-color;
- }
}
.award-control-icon-neutral {
@@ -788,12 +784,6 @@ ul.notes {
}
}
- svg {
- fill: currentColor;
- height: 16px;
- width: 16px;
- }
-
.loading {
margin: 0;
height: auto;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 28dc71dc641..ac745019319 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -73,7 +73,7 @@
.profile-link-holder {
display: inline;
- a {
+ a:not(.text-link) {
text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index aaad6dbba8e..674588752d2 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -126,93 +126,6 @@
}
}
-.project-feature-toggle {
- position: relative;
- border: 0;
- outline: 0;
- display: block;
- width: 100px;
- height: 24px;
- cursor: pointer;
- user-select: none;
- background: $feature-toggle-color-disabled;
- border-radius: 12px;
- padding: 3px;
- transition: all .4s ease;
-
- &::selection,
- &::before::selection,
- &::after::selection {
- background: none;
- }
-
- &::before {
- color: $feature-toggle-text-color;
- font-size: 12px;
- line-height: 24px;
- position: absolute;
- top: 0;
- left: 25px;
- right: 5px;
- text-align: center;
- overflow: hidden;
- text-overflow: ellipsis;
- animation: animate-disabled .2s ease-in;
- content: attr(data-disabled-text);
- }
-
- &::after {
- position: relative;
- display: block;
- content: "";
- width: 22px;
- height: 18px;
- left: 0;
- border-radius: 9px;
- background: $feature-toggle-color;
- transition: all .2s ease;
- }
-
- &.checked {
- background: $feature-toggle-color-enabled;
-
- &::before {
- left: 5px;
- right: 25px;
- animation: animate-enabled .2s ease-in;
- content: attr(data-enabled-text);
- }
-
- &::after {
- left: calc(100% - 22px);
- }
- }
-
- &.disabled {
- opacity: 0.4;
- cursor: not-allowed;
- }
-
- @media (max-width: $screen-xs-min) {
- width: 50px;
-
- &::before,
- &.checked::before {
- display: none;
- }
- }
-
- @keyframes animate-enabled {
- 0%, 35% { opacity: 0; }
- 100% { opacity: 1; }
- }
-
- @keyframes animate-disabled {
- 0%, 35% { opacity: 0; }
- 100% { opacity: 1; }
- }
-}
-
.project-home-panel,
.group-home-panel {
padding-top: 24px;
@@ -291,14 +204,7 @@
}
svg {
-
- path {
- fill: $layout-link-gray;
- }
-
- use {
- stroke: $layout-link-gray;
- }
+ fill: $layout-link-gray;
}
.fa-caret-down {
@@ -402,6 +308,18 @@
}
}
}
+
+ .clone-dropdown-btn {
+ background-color: $white-light;
+ }
+
+ .clone-options-dropdown {
+ min-width: 240px;
+
+ .dropdown-menu-inner-content {
+ min-width: 320px;
+ }
+ }
}
.project-repo-buttons {
@@ -806,6 +724,7 @@ a.deploy-project-label {
&:hover,
&:focus {
color: $gl-text-color;
+ text-decoration: underline;
}
}
}
@@ -886,10 +805,6 @@ pre.light-well {
font-size: $gl-font-size;
}
- a {
- color: $gl-text-color;
- }
-
.avatar-container,
.controls {
flex: 0 0 auto;
@@ -1208,3 +1123,8 @@ pre.light-well {
border-color: $border-color;
}
}
+
+.issuable-footer {
+ padding-top: $gl-padding;
+ padding-bottom: 37px;
+}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index d93c51d5448..402412eae71 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -35,270 +35,276 @@
}
}
-.repository-view {
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
- color: $almost-black;
+.multi-file {
+ display: flex;
+ height: calc(100vh - 145px);
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+
+ &.is-collapsed {
+ .ide-file-list {
+ max-width: 250px;
+ }
+ }
+}
- .code.white pre .hll {
- background-color: $well-light-border !important;
+.ide-file-list {
+ flex: 1;
+ overflow: scroll;
+
+ .file {
+ cursor: pointer;
}
- .tree-content-holder {
- display: -webkit-flex;
- display: flex;
- min-height: 300px;
+ a {
+ color: $gl-text-color;
}
- .tree-content-holder-mini {
- height: 100vh;
+ th {
+ position: sticky;
+ top: 0;
}
+}
- .panel-right {
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- width: 80%;
- height: 100%;
+.multi-file-table-name,
+.multi-file-table-col-commit-message {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 0;
+}
- .monaco-editor.vs {
- .current-line {
- border: 0;
- background: $well-light-border;
- }
+.multi-file-table-name {
+ width: 350px;
+}
- .line-numbers {
- cursor: pointer;
+.multi-file-table-col-commit-message {
+ width: 50%;
+}
- &:hover {
- text-decoration: underline;
- }
- }
- }
+.multi-file-edit-pane {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ border-left: 1px solid $white-dark;
+ overflow: hidden;
+}
- .blob-no-preview {
- .vertical-center {
- justify-content: center;
- width: 100%;
- }
- }
+.multi-file-tabs {
+ display: flex;
+ overflow: scroll;
+ background-color: $white-normal;
+ box-shadow: inset 0 -1px $white-dark;
- &.blob-editor-container {
- overflow: hidden;
- }
+ > li {
+ position: relative;
+ }
+}
- .blob-viewer-container {
- -webkit-flex: 1;
- flex: 1;
- overflow: auto;
-
- > div,
- .file-content:not(.wiki) {
- display: flex;
- }
-
- > div,
- .file-content,
- .blob-viewer,
- .line-number,
- .blob-content,
- .code {
- min-height: 100%;
- width: 100%;
- }
-
- .line-numbers {
- min-width: 44px;
- }
-
- .blob-content {
- flex: 1;
- overflow-x: auto;
- }
- }
+.multi-file-tab {
+ @include str-truncated(150px);
+ padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
+ background-color: $gray-normal;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ cursor: pointer;
+
+ &.active {
+ background-color: $white-light;
+ border-bottom-color: $white-light;
+ }
+}
- #tabs {
- position: relative;
- flex-shrink: 0;
- display: flex;
- width: 100%;
- padding-left: 0;
- margin-bottom: 0;
- white-space: nowrap;
- overflow-y: hidden;
- overflow-x: auto;
-
- li {
- position: relative;
- background: $gray-normal;
- padding: #{$gl-padding / 2} $gl-padding;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
- cursor: pointer;
-
- &.active {
- background: $white-light;
- border-bottom: 0;
- }
-
- a {
- @include str-truncated(100px);
- color: $gl-text-color;
- vertical-align: middle;
- text-decoration: none;
- margin-right: 12px;
-
- &:focus {
- outline: none;
- }
- }
-
- .close-btn {
- position: absolute;
- right: 8px;
- top: 50%;
- padding: 0;
- background: none;
- border: 0;
- font-size: $gl-font-size;
- transform: translateY(-50%);
- }
-
- .close-icon:hover {
- color: $hint-color;
- }
-
- .close-icon,
- .unsaved-icon {
- color: $gray-darkest;
- }
-
- .unsaved-icon {
- color: $brand-success;
- }
-
- &.tabs-divider {
- width: 100%;
- background-color: $white-light;
- border-right: 0;
- border-top-right-radius: 2px;
- }
- }
- }
+.multi-file-tab-close {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ padding: 0;
+ background: none;
+ border: 0;
+ font-size: $gl-font-size;
+ color: $gray-darkest;
+ transform: translateY(-50%);
+
+ &:not(.modified):hover,
+ &:not(.modified):focus {
+ color: $hint-color;
+ }
- .repo-file-buttons {
- background-color: $white-light;
- padding: 5px 10px;
- border-top: 1px solid $white-normal;
- }
+ &.modified {
+ color: $indigo-700;
+ }
+}
- #binary-viewer {
- height: 80vh;
- overflow: auto;
- margin: 0;
-
- .blob-viewer {
- padding-top: 20px;
- padding-left: 20px;
- }
-
- .binary-unknown {
- text-align: center;
- padding-top: 100px;
- background: $gray-light;
- height: 100%;
- font-size: 17px;
-
- span {
- display: block;
- }
- }
- }
+.multi-file-edit-pane-content {
+ flex: 1;
+ height: 0;
+}
+
+.multi-file-editor-btn-group {
+ padding: $grid-size;
+ border-top: 1px solid $white-dark;
+}
+
+// Not great, but this is to deal with our current output
+.multi-file-preview-holder {
+ height: 100%;
+ overflow: scroll;
+
+ .blob-viewer {
+ height: 100%;
}
- #commit-area {
- background: $gray-light;
- padding: 20px;
+ .file-content.code {
+ display: flex;
- .help-block {
- padding-top: 7px;
- margin-top: 0;
+ i {
+ margin-left: -10px;
}
}
- #view-toggler {
- height: 41px;
- position: relative;
- display: block;
- border-bottom: 1px solid $white-normal;
- background: $white-light;
- margin-top: -5px;
+ .line-numbers {
+ min-width: 50px;
}
- #binary-viewer {
- img {
- max-width: 100%;
- }
+ .file-content,
+ .line-numbers,
+ .blob-content,
+ .code {
+ min-height: 100%;
}
+}
- #sidebar {
- flex: 1;
- height: 100%;
+.multi-file-commit-panel {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 290px;
+ padding: $gl-padding;
+ background-color: $gray-light;
+ border-left: 1px solid $white-dark;
+
+ &.is-collapsed {
+ width: 60px;
+ padding: 0;
+ }
+}
- &.sidebar-mini {
- width: 20%;
- border-right: 1px solid $white-normal;
- overflow: auto;
- }
+.multi-file-commit-panel-section {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
- .table {
- margin-bottom: 0;
- }
+.multi-file-commit-panel-header {
+ display: flex;
+ align-items: center;
+ padding: 0 0 12px;
+ margin-bottom: 12px;
+ border-bottom: 1px solid $white-dark;
- tr {
- .repo-file-options {
- padding: 2px 16px;
- width: 100%;
- }
-
- .title {
- font-size: 10px;
- text-transform: uppercase;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: middle;
- }
-
- .file-icon {
- margin-right: 5px;
- }
-
- td {
- white-space: nowrap;
- }
- }
+ &.is-collapsed {
+ border-bottom: 1px solid $white-dark;
- .file {
- cursor: pointer;
+ svg {
+ margin-left: auto;
+ margin-right: auto;
}
+ }
+}
- a {
- @include str-truncated(250px);
- color: $almost-black;
- }
+.multi-file-commit-panel-collapse-btn {
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-left: auto;
+ font-size: 20px;
+
+ &.is-collapsed {
+ margin-right: auto;
+ }
+}
+
+.multi-file-commit-list {
+ flex: 1;
+ overflow: scroll;
+}
+
+.multi-file-commit-list-item {
+ display: flex;
+ align-items: center;
+}
+
+.multi-file-addition {
+ fill: $green-500;
+}
+
+.multi-file-modified {
+ fill: $orange-500;
+}
+
+.multi-file-commit-list-collapsed {
+ display: flex;
+ flex-direction: column;
+
+ > svg {
+ margin-left: auto;
+ margin-right: auto;
}
}
-.render-error {
- min-height: calc(100vh - 62px);
+.multi-file-commit-list-path {
+ @include str-truncated(100%);
+}
+
+.multi-file-commit-form {
+ padding-top: 12px;
+ border-top: 1px solid $white-dark;
+}
+
+.multi-file-commit-fieldset {
+ display: flex;
+ align-items: center;
+ padding-bottom: 12px;
- p {
- width: 100%;
+ .btn {
+ flex: 1;
}
}
-.multi-file-table-col-name {
- width: 350px;
+.multi-file-commit-message.form-control {
+ height: 80px;
+ resize: none;
+}
+
+.dirty-diff {
+ // !important need to override monaco inline style
+ width: 4px !important;
+ left: 0 !important;
+
+ &-modified {
+ background-color: $blue-500;
+ }
+
+ &-added {
+ background-color: $green-600;
+ }
+
+ &-removed {
+ height: 0 !important;
+ width: 0 !important;
+ bottom: -2px;
+ border-style: solid;
+ border-width: 5px;
+ border-color: transparent transparent transparent $red-500;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100px;
+ height: 1px;
+ background-color: rgba($red-500, .5);
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 25c80e1f950..ade5ddd147b 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -55,10 +55,6 @@
&:not(span):hover {
background-color: rgba($gl-text-color-secondary, .07);
}
-
- svg {
- fill: $gl-text-color-secondary;
- }
}
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index e150f96f3fa..d8fec583121 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -124,7 +124,11 @@
&:hover,
&.active {
- color: $black;
+ text-decoration: none;
+
+ span {
+ text-decoration: underline;
+ }
}
}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index 92df1c8dff0..dd0b38970bd 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -4,8 +4,8 @@ class Admin::AppearancesController < Admin::ApplicationController
def show
end
- def preview
- render 'preview', layout: 'devise'
+ def preview_sign_in
+ render 'preview_sign_in', layout: 'devise'
end
def create
@@ -52,7 +52,7 @@ class Admin::AppearancesController < Admin::ApplicationController
def appearance_params
params.require(:appearance).permit(
:title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
- :updated_by
+ :new_project_guidelines, :updated_by
)
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 3be7aee69bc..ee21d81f23e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
- before_action :authenticate_user_from_personal_access_token!
- before_action :authenticate_user_from_rss_token!
+ before_action :authenticate_sessionless_user!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :check_password_expiration
@@ -97,30 +96,15 @@ class ApplicationController < ActionController::Base
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user
def auth_user
return current_user if current_user.present?
+
return try(:authenticated_user)
end
- def authenticate_user_from_personal_access_token!
- token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
-
- return unless token.present?
-
- user = User.find_by_personal_access_token(token)
+ # This filter handles personal access tokens, and atom requests with rss tokens
+ def authenticate_sessionless_user!
+ user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
- sessionless_sign_in(user)
- end
-
- # This filter handles authentication for atom request with an rss_token
- def authenticate_user_from_rss_token!
- return unless request.format.atom?
-
- token = params[:rss_token].presence
-
- return unless token.present?
-
- user = User.find_by_rss_token(token)
-
- sessionless_sign_in(user)
+ sessionless_sign_in(user) if user
end
def log_exception(exception)
@@ -212,7 +196,11 @@ class ApplicationController < ActionController::Base
end
def check_password_expiration
- if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
+ return if session[:impersonator_id] || !current_user&.allow_password_authentication?
+
+ password_expires_at = current_user&.password_expires_at
+
+ if password_expires_at && password_expires_at < Time.now
return redirect_to new_profile_password_path
end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 10e8e54f402..cde1e284d2d 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -44,6 +44,7 @@ class AutocompleteController < ApplicationController
if @project.blank? && params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
+
group
end
end
@@ -54,6 +55,7 @@ class AutocompleteController < ApplicationController
if params[:project_id].present?
project = Project.find(params[:project_id])
return render_404 unless can?(current_user, :read_project, project)
+
project
end
end
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 737656b3dcc..f8049b20b9f 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -84,6 +84,7 @@ module Boards
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true,
+ sidebar_endpoints: true,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 072dffaff7a..744e448e8df 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -54,7 +54,7 @@ module IssuableActions
end
def destroy
- issuable.destroy
+ Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
TodoService.new.destroy_issuable(issuable, current_user)
name = issuable.human_class_name
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 2b011bc87b0..f3c9251225f 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -150,7 +150,7 @@ module IssuableCollections
when 'MergeRequest'
[
:source_project, :target_project, :author, :assignee, :labels, :milestone,
- head_pipeline: :project, target_project: :namespace, merge_request_diff: :merge_request_diff_commits
+ head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
]
end
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 3c64fd964ff..be2e1b47feb 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -4,7 +4,7 @@ module NotesActions
included do
before_action :set_polling_interval_header, only: [:index]
- before_action :noteable, only: :index
+ before_action :require_noteable!, only: [:index, :create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :note_project, only: [:create]
end
@@ -90,7 +90,7 @@ module NotesActions
if note.persisted?
attrs[:valid] = true
- if noteable.nil? || noteable.discussions_rendered_on_frontend?
+ if noteable.discussions_rendered_on_frontend?
attrs.merge!(note_serializer.represent(note))
else
attrs.merge!(
@@ -191,7 +191,11 @@ module NotesActions
end
def noteable
- @noteable ||= notes_finder.target || render_404
+ @noteable ||= notes_finder.target || @note&.noteable
+ end
+
+ def require_noteable!
+ render_404 unless noteable
end
def last_fetched_at
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 5ce602b55a8..e9b9e9b38bc 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -8,6 +8,7 @@ module PreviewMarkdown
case controller_name
when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
when 'snippets' then { skip_project_check: true }
+ when 'groups' then { group: group }
else {}
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 7a7bcb1a3d2..f013d21275e 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -80,7 +80,8 @@ class Groups::MilestonesController < Groups::ApplicationController
milestones = MilestonesFinder.new(search_params).execute
legacy_milestones = GroupMilestone.build_collection(group, group_projects, params)
- milestones + legacy_milestones
+ @sort = params[:sort] || 'due_date_asc'
+ MilestoneArray.sort(milestones + legacy_milestones, @sort)
end
def milestone
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 510813846a4..567957ba2cb 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -4,6 +4,7 @@ class Import::GitlabProjectsController < Import::BaseController
def new
@namespace = Namespace.find(project_params[:namespace_id])
return render_404 unless current_user.can?(:create_projects, @namespace)
+
@path = project_params[:path]
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 0982a61902b..04b29aa2384 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -51,7 +51,7 @@ class InvitesController < ApplicationController
return if current_user
notice = "To accept this invitation, sign in"
- notice << " or create an account" if current_application_settings.signup_enabled?
+ notice << " or create an account" if current_application_settings.allow_signup?
notice << "."
store_location_for :user, request.fullpath
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 9612b8d8514..e3c18cba1dd 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -54,7 +54,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user
log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed.
- identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml)
+ identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated'
@@ -98,7 +98,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_omniauth
if current_user
# Add new authentication method
- current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider'])
+ current_user.identities
+ .with_extern_uid(oauth['provider'], oauth['uid'])
+ .first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
@@ -138,7 +140,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
- if current_application_settings.signup_enabled?
+ if current_application_settings.allow_signup?
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index fda944adecd..68a52f40342 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -1,6 +1,8 @@
class PasswordsController < Devise::PasswordsController
+ include Gitlab::CurrentSettings
+
before_action :resource_from_email, only: [:create]
- before_action :prevent_ldap_reset, only: [:create]
+ before_action :check_password_authentication_available, only: [:create]
before_action :throttle_reset, only: [:create]
def edit
@@ -25,7 +27,7 @@ class PasswordsController < Devise::PasswordsController
def update
super do |resource|
- if resource.valid? && resource.require_password_creation?
+ if resource.valid? && resource.password_automatically_set?
resource.update_attribute(:password_automatically_set, false)
end
end
@@ -38,11 +40,15 @@ class PasswordsController < Devise::PasswordsController
self.resource = resource_class.find_by_email(email)
end
- def prevent_ldap_reset
- return unless resource&.ldap_user?
+ def check_password_authentication_available
+ if resource
+ return if resource.allow_password_authentication?
+ else
+ return if current_application_settings.password_authentication_enabled?
+ end
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
- alert: "Cannot reset password for LDAP user."
+ alert: "Password authentication is unavailable."
end
def throttle_reset
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index dcfcb855ab5..fa72f67c77e 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -77,7 +77,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
end
def authorize_change_password!
- render_404 if @user.ldap_user?
+ render_404 unless @user.allow_password_authentication?
end
def user_params
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index f28df83d5a5..56df9991fda 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -41,7 +41,7 @@ class Projects::BranchesController < Projects::ApplicationController
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
- redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present?
+ redirect_to_autodeploy = project.empty_repo? && project.deployment_platform.present?
result = CreateBranchService.new(project, current_user)
.execute(branch_name, ref)
diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb
new file mode 100644
index 00000000000..b64f7a2a6bd
--- /dev/null
+++ b/app/controllers/projects/clusters/gcp_controller.rb
@@ -0,0 +1,75 @@
+class Projects::Clusters::GcpController < Projects::ApplicationController
+ before_action :authorize_read_cluster!
+ before_action :authorize_google_api, except: [:login]
+ before_action :authorize_create_cluster!, only: [:new, :create]
+
+ def login
+ begin
+ state = generate_session_key_redirect(gcp_new_namespace_project_clusters_path.to_s)
+
+ @authorize_url = GoogleApi::CloudPlatform::Client.new(
+ nil, callback_google_api_auth_url,
+ state: state).authorize_url
+ rescue GoogleApi::Auth::ConfigMissingError
+ # no-op
+ end
+ end
+
+ def new
+ @cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end
+ end
+
+ def create
+ @cluster = ::Clusters::CreateService
+ .new(project, current_user, create_params)
+ .execute(token_in_session)
+
+ if @cluster.persisted?
+ redirect_to project_cluster_path(project, @cluster)
+ else
+ render :new
+ end
+ end
+
+ private
+
+ def create_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ]).merge(
+ provider_type: :gcp,
+ platform_type: :kubernetes
+ )
+ end
+
+ def authorize_google_api
+ unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ redirect_to action: 'login'
+ end
+ end
+
+ def token_in_session
+ @token_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token]
+ end
+
+ def expires_at_in_session
+ @expires_at_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
+ end
+
+ def generate_session_key_redirect(uri)
+ GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
+ session[key] = uri
+ end
+ end
+end
diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb
new file mode 100644
index 00000000000..d7678512073
--- /dev/null
+++ b/app/controllers/projects/clusters/user_controller.rb
@@ -0,0 +1,39 @@
+class Projects::Clusters::UserController < Projects::ApplicationController
+ before_action :authorize_read_cluster!
+ before_action :authorize_create_cluster!, only: [:new, :create]
+
+ def new
+ @cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_platform_kubernetes
+ end
+ end
+
+ def create
+ @cluster = ::Clusters::CreateService
+ .new(project, current_user, create_params)
+ .execute
+
+ if @cluster.persisted?
+ redirect_to project_cluster_path(project, @cluster)
+ else
+ render :new
+ end
+ end
+
+ private
+
+ def create_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ platform_kubernetes_attributes: [
+ :namespace,
+ :api_url,
+ :token,
+ :ca_cert
+ ]).merge(
+ provider_type: :user,
+ platform_type: :kubernetes
+ )
+ end
+end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 19ae3192044..411c801bf2e 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,52 +1,24 @@
class Projects::ClustersController < Projects::ApplicationController
- before_action :cluster, except: [:login, :index, :new, :new_gcp, :create]
+ before_action :cluster, except: [:index, :new]
before_action :authorize_read_cluster!
- before_action :authorize_create_cluster!, only: [:new, :new_gcp, :create]
- before_action :authorize_google_api, only: [:new_gcp, :create]
+ before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
- def index
- @clusters ||= project.clusters.map { |cluster| cluster.present(current_user: current_user) }
- end
+ STATUS_POLLING_INTERVAL = 10_000
- def login
- begin
- state = generate_session_key_redirect(providers_gcp_new_namespace_project_clusters_url.to_s)
-
- @authorize_url = GoogleApi::CloudPlatform::Client.new(
- nil, callback_google_api_auth_url,
- state: state).authorize_url
- rescue GoogleApi::Auth::ConfigMissingError
- # no-op
- end
+ def index
+ clusters = ClustersFinder.new(project, current_user, :all).execute
+ @clusters = clusters.page(params[:page])
end
def new
end
- def new_gcp
- @cluster = Clusters::Cluster.new.tap do |cluster|
- cluster.build_provider_gcp
- end
- end
-
- def create
- @cluster = Clusters::CreateService
- .new(project, current_user, create_params)
- .execute(token_in_session)
-
- if @cluster.persisted?
- redirect_to project_cluster_path(project, @cluster)
- else
- render :new_gcp
- end
- end
-
def status
respond_to do |format|
format.json do
- Gitlab::PollingInterval.set_header(response, interval: 10_000)
+ Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
render json: ClusterSerializer
.new(project: @project, current_user: @current_user)
@@ -64,10 +36,20 @@ class Projects::ClustersController < Projects::ApplicationController
.execute(cluster)
if cluster.valid?
- flash[:notice] = "Cluster was successfully updated."
- redirect_to project_cluster_path(project, project.cluster)
+ respond_to do |format|
+ format.json do
+ head :no_content
+ end
+ format.html do
+ flash[:notice] = "Cluster was successfully updated."
+ redirect_to project_cluster_path(project, cluster)
+ end
+ end
else
- render :show
+ respond_to do |format|
+ format.json { head :bad_request }
+ format.html { render :show }
+ end
end
end
@@ -84,7 +66,8 @@ class Projects::ClustersController < Projects::ApplicationController
private
def cluster
- @cluster ||= project.clusters.find_by(id: params[:id])&.present(current_user: current_user) || render_404
+ @cluster ||= project.clusters.find(params[:id])
+ .present(current_user: current_user)
end
def create_params
@@ -101,29 +84,24 @@ class Projects::ClustersController < Projects::ApplicationController
end
def update_params
- params.require(:cluster).permit(:enabled)
- end
-
- def authorize_google_api
- unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
- .validate_token(expires_at_in_session)
- redirect_to action: 'login'
- end
- end
-
- def token_in_session
- @token_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_token]
- end
-
- def expires_at_in_session
- @expires_at_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
- end
-
- def generate_session_key_redirect(uri)
- GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
- session[key] = uri
+ if cluster.managed?
+ params.require(:cluster).permit(
+ :enabled,
+ platform_kubernetes_attributes: [
+ :namespace
+ ]
+ )
+ else
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ platform_kubernetes_attributes: [
+ :api_url,
+ :token,
+ :ca_cert,
+ :namespace
+ ]
+ )
end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 494d412b532..6ff96a3f295 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -22,12 +22,7 @@ class Projects::CommitController < Projects::ApplicationController
apply_diff_view_cookie!
respond_to do |format|
- format.html do
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render
- end
- end
+ format.html { render }
format.diff { render text: @commit.to_diff }
format.patch { render text: @commit.to_patch }
end
@@ -112,7 +107,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def commit
- @noteable = @commit ||= @project.commit(params[:id])
+ @noteable = @commit ||= @project.commit_by(oid: params[:id])
end
def define_commit_vars
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 28920877635..026708169f4 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -45,8 +45,7 @@ class Projects::CommitsController < Projects::ApplicationController
private
def set_commits
- render_404 unless request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
-
+ render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
search = params[:search]
@@ -57,6 +56,7 @@ class Projects::CommitsController < Projects::ApplicationController
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
+ @commits = @commits.with_pipeline_status
@commits = prepare_commits_for_rendering(@commits)
end
end
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index 47c312ffddf..1a418d0f15a 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -12,6 +12,7 @@ class Projects::DeploymentsController < Projects::ApplicationController
def metrics
return render_404 unless deployment.has_metrics?
+
@metrics = deployment.metrics
if @metrics&.any?
render json: @metrics, status: :ok
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 29e223a5273..52d528e816e 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -34,6 +34,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available)
.order(:name)
+ @folder = params[:id]
respond_to do |format|
format.html
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index dbc1c8bcc28..f58ee3e9109 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -12,6 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
if group
return render_404 unless can?(current_user, :read_group, group)
+
Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
else
flash[:alert] = 'Please select a group.'
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index dbc9106ba6d..d7a3441a245 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -158,7 +158,8 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create_merge_request
- result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
+ create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
+ result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
@@ -171,6 +172,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue
return @issue if defined?(@issue)
+
# The Sortable default scope causes performance issues when used with find_by
@issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
@note = @project.notes.new(noteable: @issuable)
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 480a2dff262..e0f4710175f 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -111,6 +111,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
+
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project),
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 32759672b6c..293869345bd 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -54,6 +54,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
name = request.headers['X-Gitlab-Lfs-Tmp']
return if name.include?('/')
return unless oid.present? && name.start_with?(oid)
+
name
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 764a9c7111e..1511fc08c89 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -65,7 +65,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
if params[:ref].present?
@ref = params[:ref]
- @commit = @repository.commit("refs/heads/#{@ref}")
+ @commit = @repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end
render layout: false
@@ -76,7 +76,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
if params[:ref].present?
@ref = params[:ref]
- @commit = @target_project.commit("refs/heads/#{@ref}")
+ @commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end
render layout: false
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 7d16e77ef66..9f966889995 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -10,10 +10,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def show
@environment = @merge_request.environments_for(current_user).last
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
- end
+ render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
end
def diff_for_path
@@ -30,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request.merge_request_diff
end
- @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 22de6680511..abe4e5245b1 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -80,7 +80,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def commits
# Get commits from repository
# or from cache if already merged
- @commits = prepare_commits_for_rendering(@merge_request.commits)
+ @commits =
+ prepare_commits_for_rendering(@merge_request.commits.with_pipeline_status)
render json: { html: view_to_html_string('projects/merge_requests/_commits') }
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index ef7d047b1ad..627cb2bd93c 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -76,6 +76,7 @@ class Projects::NotesController < Projects::ApplicationController
def authorize_create_note!
return unless noteable.lockable?
+
access_denied! unless can?(current_user, :create_note, noteable)
end
end
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index abab2e2f0c9..b890818c475 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -6,11 +6,19 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
end
def update
- if @project.update(update_params)
- flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
- redirect_to project_settings_ci_cd_path(@project)
- else
- render 'show'
+ Projects::UpdateService.new(project, current_user, update_params).tap do |service|
+ if service.execute
+ flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
+
+ if service.run_auto_devops_pipeline?
+ CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
+ flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
+ end
+
+ redirect_to project_settings_ci_cd_path(@project)
+ else
+ render 'show'
+ end
end
end
@@ -21,6 +29,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
+ :run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit,
auto_devops_attributes: [:id, :domain, :enabled]
)
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 44de8a49593..d06d18c498b 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -21,14 +21,14 @@ module Projects
def access_levels_options
{
- create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel),
- push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel),
- merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel)
+ create_access_levels: levels_for_dropdown,
+ push_access_levels: levels_for_dropdown,
+ merge_access_levels: levels_for_dropdown
}
end
- def levels_for_dropdown(access_level_type)
- roles = access_level_type.human_access_levels.map do |id, text|
+ def levels_for_dropdown
+ roles = ProtectedRefAccess::HUMAN_ACCESS_LEVELS.map do |id, text|
{ id: id, text: text, before_divider: true }
end
{ roles: roles }
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index f7a9c98629d..292e4158f8b 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -28,6 +28,7 @@ class Projects::WikisController < Projects::ApplicationController
)
else
return render('empty') unless can?(current_user, :create_wiki, @project)
+
@page = WikiPage.new(@project_wiki)
@page.title = params[:id]
@@ -74,7 +75,11 @@ class Projects::WikisController < Projects::ApplicationController
def history
@page = @project_wiki.find_page(params[:id])
- unless @page
+ if @page
+ @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]),
+ total_count: @page.count_versions)
+ .page(params[:page])
+ else
redirect_to(
project_wiki_path(@project, :home),
notice: "Page not found"
@@ -101,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
- @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
+ @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15))
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 2a473ec0cec..3882fa4791d 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -133,11 +133,11 @@ class ProjectsController < Projects::ApplicationController
redirect_to edit_project_path(@project), status: 302, alert: ex.message
end
- def new_issue_address
+ def new_issuable_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
current_user.reset_incoming_email_token!
- render json: { new_issue_address: @project.new_issue_address(current_user) }
+ render json: { new_address: @project.new_issuable_address(current_user, params[:issuable_type]) }
end
def archive
@@ -269,6 +269,7 @@ class ProjectsController < Projects::ApplicationController
def render_landing_page
if can?(current_user, :download_code, @project)
return render 'projects/no_repo' unless @project.repository_exists?
+
render 'projects/empty' if @project.empty_repo?
else
if @project.wiki_enabled?
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index c01be42c3ee..d79108c88fb 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -63,7 +63,7 @@ class SessionsController < Devise::SessionsController
user = User.admins.last
- return unless user && user.require_password_creation?
+ return unless user && user.require_password_creation_for_web?
Users::UpdateService.new(current_user, user: user).execute do |user|
@token = user.generate_reset_token
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
index f9496787b15..c8b4682e6dc 100644
--- a/app/controllers/snippets/notes_controller.rb
+++ b/app/controllers/snippets/notes_controller.rb
@@ -20,6 +20,7 @@ class Snippets::NotesController < ApplicationController
def snippet
PersonalSnippet.find_by(id: params[:snippet_id])
end
+ alias_method :noteable, :snippet
def note_params
super.merge(noteable_id: params[:snippet_id])
diff --git a/app/controllers/unicorn_test_controller.rb b/app/controllers/unicorn_test_controller.rb
deleted file mode 100644
index ed04bd1f77d..00000000000
--- a/app/controllers/unicorn_test_controller.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# :nocov:
-if Rails.env.test?
- class UnicornTestController < ActionController::Base
- def pid
- render plain: Process.pid.to_s
- end
-
- def kill
- Process.kill(params[:signal], Process.pid)
- render plain: 'Bye!'
- end
- end
-end
-# :nocov:
diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb
new file mode 100644
index 00000000000..c13f98257bf
--- /dev/null
+++ b/app/finders/clusters_finder.rb
@@ -0,0 +1,29 @@
+class ClustersFinder
+ def initialize(project, user, scope)
+ @project = project
+ @user = user
+ @scope = scope || :active
+ end
+
+ def execute
+ clusters = project.clusters
+ filter_by_scope(clusters)
+ end
+
+ private
+
+ attr_reader :project, :user, :scope
+
+ def filter_by_scope(clusters)
+ case scope.to_sym
+ when :all
+ clusters
+ when :inactive
+ clusters.disabled
+ when :active
+ clusters.enabled
+ else
+ raise "Invalid scope #{scope}"
+ end
+ end
+end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 02eb983bf55..12157818bcd 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -104,8 +104,7 @@ class NotesFinder
query = @params[:search]
return notes unless query
- pattern = "%#{query}%"
- notes.where(Note.arel_table[:note].matches(pattern))
+ notes.search(query)
end
# Notes changed since last fetch
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index 760166b453f..d975f354a88 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -18,6 +18,7 @@ class PersonalAccessTokensFinder
def by_user(tokens)
return tokens unless @params[:user]
+
tokens.where(user: @params[:user])
end
diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb
new file mode 100644
index 00000000000..52340f94523
--- /dev/null
+++ b/app/finders/runner_jobs_finder.rb
@@ -0,0 +1,22 @@
+class RunnerJobsFinder
+ attr_reader :runner, :params
+
+ def initialize(runner, params = {})
+ @runner = runner
+ @params = params
+ end
+
+ def execute
+ items = @runner.builds
+ items = by_status(items)
+ items
+ end
+
+ private
+
+ def by_status(items)
+ return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
+
+ items.where(status: params[:status])
+ end
+end
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index 1a7e97004fb..edde8022ec9 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -25,7 +25,7 @@ class UsersFinder
end
def execute
- users = User.all
+ users = User.all.order_id_desc
users = by_username(users)
users = by_search(users)
users = by_blocked(users)
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 8ad94d3f723..c037de33c22 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,33 +1,36 @@
module AppearancesHelper
def brand_title
- if brand_item && brand_item.title
- brand_item.title
- else
- 'GitLab Community Edition'
- end
+ brand_item&.title.presence || 'GitLab Community Edition'
end
def brand_image
- if brand_item.logo?
- image_tag brand_item.logo
- else
- nil
- end
+ image_tag(brand_item.logo) if brand_item&.logo?
end
def brand_text
markdown_field(brand_item, :description)
end
+ def brand_new_project_guidelines
+ markdown_field(brand_item, :new_project_guidelines)
+ end
+
def brand_item
@appearance ||= Appearance.current
end
def brand_header_logo
- if brand_item && brand_item.header_logo?
+ if brand_item&.header_logo?
image_tag brand_item.header_logo
else
render 'shared/logo.svg'
end
end
+
+ # Skip the 'GitLab' type logo when custom brand logo is set
+ def brand_header_logo_type
+ unless brand_item&.header_logo?
+ render 'shared/logo_type.svg'
+ end
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index cd1ecaadb85..dccde46fa33 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -3,9 +3,9 @@ module ApplicationSettingsHelper
include Gitlab::CurrentSettings
- delegate :gravatar_enabled?,
- :signup_enabled?,
- :password_authentication_enabled?,
+ delegate :allow_signup?,
+ :gravatar_enabled?,
+ :password_authentication_enabled_for_web?,
:akismet_enabled?,
:koding_enabled?,
to: :current_application_settings
@@ -30,9 +30,9 @@ module ApplicationSettingsHelper
def enabled_project_button(project, protocol)
case protocol
when 'ssh'
- ssh_clone_button(project, 'bottom', append_link: false)
+ ssh_clone_button(project, append_link: false)
else
- http_clone_button(project, 'bottom', append_link: false)
+ http_clone_button(project, append_link: false)
end
end
@@ -177,6 +177,9 @@ module ApplicationSettingsHelper
:ed25519_key_restriction,
:email_author_in_body,
:enabled_git_access_protocol,
+ :gitaly_timeout_default,
+ :gitaly_timeout_medium,
+ :gitaly_timeout_fast,
:gravatar_enabled,
:hashed_storage_enabled,
:help_page_hide_commercial_content,
@@ -203,7 +206,7 @@ module ApplicationSettingsHelper
:metrics_port,
:metrics_sample_interval,
:metrics_timeout,
- :password_authentication_enabled,
+ :password_authentication_enabled_for_web,
:performance_bar_allowed_group_id,
:performance_bar_enabled,
:plantuml_enabled,
@@ -231,6 +234,15 @@ module ApplicationSettingsHelper
:sign_in_text,
:signup_enabled,
:terminal_max_session_time,
+ :throttle_unauthenticated_enabled,
+ :throttle_unauthenticated_requests_per_period,
+ :throttle_unauthenticated_period_in_seconds,
+ :throttle_authenticated_web_enabled,
+ :throttle_authenticated_web_requests_per_period,
+ :throttle_authenticated_web_period_in_seconds,
+ :throttle_authenticated_api_enabled,
+ :throttle_authenticated_api_requests_per_period,
+ :throttle_authenticated_api_period_in_seconds,
:two_factor_grace_period,
:unique_ips_limit_enabled,
:unique_ips_limit_per_user,
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 483b957decb..ec6194d204f 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -8,9 +8,25 @@ module AutoDevopsHelper
!project.ci_service
end
+ def show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project)
+ return false if project.repository.gitlab_ci_yml
+
+ if project&.auto_devops&.enabled.present?
+ !project.auto_devops.enabled && current_application_settings.auto_devops_enabled?
+ else
+ current_application_settings.auto_devops_enabled?
+ end
+ end
+
+ def show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project)
+ return false if project.repository.gitlab_ci_yml
+
+ !project.auto_devops_enabled?
+ end
+
def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain?
- missing_service = !project.kubernetes_service&.active?
+ missing_service = !project.deployment_platform&.active?
if missing_service
params = {
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 48cf30a48ab..d06cf2de2c3 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -56,42 +56,36 @@ module ButtonHelper
end
end
- def http_clone_button(project, placement = 'right', append_link: true)
- klass = 'http-selector'
- klass << ' has-tooltip' if current_user.try(:require_password_creation?) || current_user.try(:require_personal_access_token_creation_for_git_auth?)
-
+ def http_clone_button(project, append_link: true)
protocol = gitlab_config.protocol.upcase
+ dropdown_description = http_dropdown_description(protocol)
+ append_url = project.http_url_to_repo if append_link
+
+ dropdown_item_with_description(protocol, dropdown_description, href: append_url)
+ end
+
+ def http_dropdown_description(protocol)
+ if current_user.try(:require_password_creation_for_git?)
+ _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
+ else
+ _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
+ end
+ end
- tooltip_title =
- if current_user.try(:require_password_creation?)
- _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
- else
- _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
- end
+ def ssh_clone_button(project, append_link: true)
+ dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?)
+ append_url = project.ssh_url_to_repo if append_link
- content_tag (append_link ? :a : :span), protocol,
- class: klass,
- href: (project.http_url_to_repo if append_link),
- data: {
- html: true,
- placement: placement,
- container: 'body',
- title: tooltip_title
- }
+ dropdown_item_with_description('SSH', dropdown_description, href: append_url)
end
- def ssh_clone_button(project, placement = 'right', append_link: true)
- klass = 'ssh-selector'
- klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
+ def dropdown_item_with_description(title, description, href: nil)
+ button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
+ button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
- content_tag (append_link ? :a : :span), 'SSH',
- class: klass,
- href: (project.ssh_url_to_repo if append_link),
- data: {
- html: true,
- placement: placement,
- container: 'body',
- title: _('Add an SSH key to your profile to pull or push via SSH.')
- }
+ content_tag (href ? :a : :span),
+ button_content,
+ class: "#{title.downcase}-selector",
+ href: (href if href)
end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 4dd573c61f1..636316da80a 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -6,11 +6,6 @@
# See 'detailed_status?` method and `Gitlab::Ci::Status` module.
#
module CiStatusHelper
- def ci_status_path(pipeline)
- project = pipeline.project
- project_pipeline_path(project, pipeline)
- end
-
def ci_label_for_status(status)
if detailed_status?(status)
return status.label
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index f9a666fa1e6..f68e2cd3afa 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -63,7 +63,7 @@ module CommitsHelper
# Returns a link formatted as a commit branch link
def commit_branch_link(url, text)
link_to(url, class: 'label label-gray ref-name branch-link') do
- icon('code-fork') + " #{text}"
+ icon('code-fork', class: 'append-right-5') + "#{text}"
end
end
@@ -77,7 +77,7 @@ module CommitsHelper
# Returns a link formatted as a commit tag link
def commit_tag_link(url, text)
link_to(url, class: 'label label-gray ref-name') do
- icon('tag') + " #{text}"
+ icon('tag', class: 'append-right-5') + "#{text}"
end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 4e4a66e8a02..e82136f0177 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -111,6 +111,7 @@ module DiffHelper
def diff_file_old_blob_raw_path(diff_file)
sha = diff_file.old_content_sha
return unless sha
+
project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path))
end
@@ -152,11 +153,11 @@ module DiffHelper
def diff_file_changed_icon(diff_file)
if diff_file.deleted_file? || diff_file.renamed_file?
- "minus"
+ "file-deletion"
elsif diff_file.new_file?
- "plus"
+ "file-addition"
else
- "adjust"
+ "file-modified"
end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 5f11fe62030..878bc9b5c9c 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -24,6 +24,7 @@ module EmailsHelper
def action_title(url)
return unless url
+
%w(merge_requests issues commit).each do |action|
if url.split("/").include?(action)
return "View #{action.humanize.singularize}"
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index a9840d19178..4c60f4b0cd0 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -212,6 +212,7 @@ module IssuablesHelper
def issuable_initial_data(issuable)
data = {
endpoint: issuable_path(issuable),
+ updateEndpoint: "#{issuable_path(issuable)}.json",
canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference,
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 2c85d7d7720..1e4be2d4bcf 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -53,6 +53,7 @@ module MarkupHelper
# text, wrapping anything found in the requested link
fragment.children.each do |node|
next unless node.text?
+
node.replace(link_to(node.text, url, html_options))
end
end
@@ -112,7 +113,13 @@ module MarkupHelper
text = wiki_page.content
return '' unless text.present?
- context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
+ context = {
+ pipeline: :wiki,
+ project: @project,
+ project_wiki: @project_wiki,
+ page_slug: wiki_page.slug,
+ issuable_state_filter_enabled: true
+ }
html =
case wiki_page.format
@@ -221,7 +228,7 @@ module MarkupHelper
data = options[:data].merge({ container: 'body' })
content_tag :button,
type: 'button',
- class: 'toolbar-btn js-md has-tooltip hidden-xs',
+ class: 'toolbar-btn js-md has-tooltip',
tabindex: -1,
data: data,
title: options[:title],
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index fde961e2da4..3e42063224e 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -78,6 +78,7 @@ module NotificationsHelper
# Create hidden field to send notification setting source to controller
def hidden_setting_source_input(notification_setting)
return unless notification_setting.source_type
+
hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index f48d47953e4..4a6b22b5ff6 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -234,11 +234,11 @@ module ProjectsHelper
def show_no_password_message?
cookies[:hide_no_password_message].blank? && !current_user.hide_no_password &&
- ( current_user.require_password_creation? || current_user.require_personal_access_token_creation_for_git_auth? )
+ current_user.require_extra_setup_for_git_auth?
end
def link_to_set_password
- if current_user.require_password_creation?
+ if current_user.require_password_creation_for_git?
link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path
else
link_to s_('CreateTokenToCloneLink|create a personal access token'), profile_personal_access_tokens_path
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index cf28a917fd1..2f57660516d 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -140,7 +140,8 @@ module SearchHelper
placeholder: 'Search or filter results...',
data: {
'username-params' => @users.to_json(only: [:id, :username])
- }
+ },
+ autocomplete: 'off'
}
if @project.present?
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 0e106e2c85d..5b2ea38a03d 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -97,6 +97,7 @@ module TreeHelper
part_path = part if part_path.empty?
next if parts.count > max_links && !parts.last(2).include?(part)
+
yield(part, part_path)
end
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 46867d2d974..c3d5628f241 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -150,6 +150,7 @@ module VisibilityLevelHelper
def restricted_visibility_levels(show_all = false)
return [] if current_user.admin? && !show_all
+
current_application_settings.restricted_visibility_levels || []
end
@@ -159,6 +160,7 @@ module VisibilityLevelHelper
def disallowed_visibility_level?(form_model, level)
return false unless form_model.respond_to?(:visibility_level_allowed?)
+
!form_model.visibility_level_allowed?(level)
end
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index ff15689ecac..76cfe28742a 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -2,9 +2,8 @@ class Appearance < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
+ cache_markdown_field :new_project_guidelines
- validates :title, presence: true
- validates :description, presence: true
validates :logo, file_size: { maximum: 1.megabyte }
validates :header_logo, file_size: { maximum: 1.megabyte }
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 5e16badabec..3117c98c846 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -172,6 +172,27 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ validates :gitaly_timeout_default,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
+ validates :gitaly_timeout_medium,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :gitaly_timeout_medium,
+ numericality: { less_than_or_equal_to: :gitaly_timeout_default },
+ if: :gitaly_timeout_default
+ validates :gitaly_timeout_medium,
+ numericality: { greater_than_or_equal_to: :gitaly_timeout_fast },
+ if: :gitaly_timeout_fast
+
+ validates :gitaly_timeout_fast,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :gitaly_timeout_fast,
+ numericality: { less_than_or_equal_to: :gitaly_timeout_default },
+ if: :gitaly_timeout_default
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
@@ -276,7 +297,8 @@ class ApplicationSetting < ActiveRecord::Base
koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
- password_authentication_enabled: Settings.gitlab['password_authentication_enabled'],
+ password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
+ password_authentication_enabled_for_git: true,
performance_bar_allowed_group_id: nil,
rsa_key_restriction: 0,
plantuml_enabled: false,
@@ -295,10 +317,22 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
+ throttle_unauthenticated_enabled: false,
+ throttle_unauthenticated_requests_per_period: 3600,
+ throttle_unauthenticated_period_in_seconds: 3600,
+ throttle_authenticated_web_enabled: false,
+ throttle_authenticated_web_requests_per_period: 7200,
+ throttle_authenticated_web_period_in_seconds: 3600,
+ throttle_authenticated_api_enabled: false,
+ throttle_authenticated_api_requests_per_period: 7200,
+ throttle_authenticated_api_period_in_seconds: 3600,
two_factor_grace_period: 48,
user_default_external: false,
polling_interval_multiplier: 1,
- usage_ping_enabled: Settings.gitlab['usage_ping_enabled']
+ usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
+ gitaly_timeout_fast: 10,
+ gitaly_timeout_medium: 30,
+ gitaly_timeout_default: 55
}
end
@@ -465,6 +499,14 @@ class ApplicationSetting < ActiveRecord::Base
has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend
end
+ def allow_signup?
+ signup_enabled? && password_authentication_enabled_for_web?
+ end
+
+ def password_authentication_enabled?
+ password_authentication_enabled_for_web? || password_authentication_enabled_for_git?
+ end
+
private
def ensure_uuid!
diff --git a/app/models/blob.rb b/app/models/blob.rb
index ad0bc2e2ead..29e762724e3 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -76,12 +76,24 @@ class Blob < SimpleDelegator
new(blob, project)
end
+ def self.lazy(project, commit_id, path)
+ BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader|
+ project.repository.blobs_at(items.map(&:values)).each do |blob|
+ loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob
+ end
+ end
+ end
+
def initialize(blob, project = nil)
@project = project
super(blob)
end
+ def inspect
+ "#<#{self.class.name} oid:#{id[0..8]} commit:#{commit_id[0..8]} path:#{path}>"
+ end
+
# Returns the data of the blob.
#
# If the blob is a text based blob the content is converted to UTF-8 and any
@@ -95,7 +107,10 @@ class Blob < SimpleDelegator
end
def load_all_data!
- super(project.repository) if project
+ # Endpoint needed: gitlab-org/gitaly#756
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ super(project.repository) if project
+ end
end
def no_highlighting?
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1b2b0d17910..d2402b55184 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1,5 +1,6 @@
module Ci
class Build < CommitStatus
+ prepend ArtifactMigratable
include TokenAuthenticatable
include AfterCommitQueue
include Presentable
@@ -10,9 +11,14 @@ module Ci
belongs_to :erased_by, class_name: 'User'
has_many :deployments, as: :deployable
+
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
+ has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
+ has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
+
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@persisted_environment ||= Environment.find_by(
@@ -31,15 +37,37 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
- scope :with_artifacts, ->() { where.not(artifacts_file: [nil, '']) }
+ scope :with_artifacts, ->() do
+ where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
+ '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id'))
+ end
scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) }
- mount_uploader :artifacts_file, ArtifactUploader
- mount_uploader :artifacts_metadata, ArtifactUploader
+ scope :matches_tag_ids, -> (tag_ids) do
+ matcher = ::ActsAsTaggableOn::Tagging
+ .where(taggable_type: CommitStatus)
+ .where(context: 'tags')
+ .where('taggable_id = ci_builds.id')
+ .where.not(tag_id: tag_ids).select('1')
+
+ where("NOT EXISTS (?)", matcher)
+ end
+
+ scope :with_any_tags, -> do
+ matcher = ::ActsAsTaggableOn::Tagging
+ .where(taggable_type: CommitStatus)
+ .where(context: 'tags')
+ .where('taggable_id = ci_builds.id').select('1')
+
+ where("EXISTS (?)", matcher)
+ end
+
+ mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file
+ mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata
acts_as_taggable
@@ -104,6 +132,7 @@ module Ci
end
before_transition any => [:failed] do |build|
+ next unless build.project
next if build.retries_max.zero?
if build.retries_count < build.retries_max
@@ -243,7 +272,7 @@ module Ci
@merge_request ||=
begin
- merge_requests = MergeRequest.includes(:merge_request_diff)
+ merge_requests = MergeRequest.includes(:latest_merge_request_diff)
.where(source_branch: ref,
source_project: pipeline.project)
.reorder(iid: :desc)
@@ -317,6 +346,7 @@ module Ci
def execute_hooks
return unless project
+
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :job_hooks)
project.execute_services(build_data.dup, :job_hooks)
@@ -324,14 +354,6 @@ module Ci
project.running_or_pending_build_count(force: true)
end
- def artifacts?
- !artifacts_expired? && artifacts_file.exists?
- end
-
- def artifacts_metadata?
- artifacts? && artifacts_metadata.exists?
- end
-
def artifacts_metadata_entry(path, **options)
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
artifacts_metadata.path,
@@ -384,6 +406,7 @@ module Ci
def keep_artifacts!
self.update(artifacts_expire_at: nil)
+ self.job_artifacts.update_all(expire_at: nil)
end
def coverage_regex
@@ -471,11 +494,7 @@ module Ci
private
def update_artifacts_size
- self.artifacts_size = if artifacts_file.exists?
- artifacts_file.size
- else
- nil
- end
+ self.artifacts_size = legacy_artifacts_file&.size
end
def erase_trace!
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
new file mode 100644
index 00000000000..84fc6863567
--- /dev/null
+++ b/app/models/ci/job_artifact.rb
@@ -0,0 +1,36 @@
+module Ci
+ class JobArtifact < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ belongs_to :project
+ belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
+
+ before_save :set_size, if: :file_changed?
+
+ mount_uploader :file, JobArtifactUploader
+
+ enum file_type: {
+ archive: 1,
+ metadata: 2
+ }
+
+ def self.artifacts_size_for(project)
+ self.where(project: project).sum(:size)
+ end
+
+ def set_size
+ self.size = file.size
+ end
+
+ def expire_in
+ expire_at - Time.now if expire_at
+ end
+
+ def expire_in=(value)
+ self.expire_at =
+ if value
+ ChronicDuration.parse(value)&.seconds&.from_now
+ end
+ end
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 19814864e50..eebbf7c4218 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -40,7 +40,6 @@ module Ci
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
- after_initialize :set_config_source, if: :new_record?
after_create :keep_around_commits, unless: :importing?
enum source: {
@@ -149,34 +148,70 @@ module Ci
end
end
- # ref can't be HEAD or SHA, can only be branch/tag name
- scope :latest, ->(ref = nil) do
- max_id = unscope(:select)
- .select("max(#{quoted_table_name}.id)")
- .group(:ref, :sha)
+ scope :internal, -> { where(source: internal_sources) }
- if ref
- where(ref: ref, id: max_id.where(ref: ref))
- else
- where(id: max_id)
- end
+ # Returns the pipelines in descending order (= newest first), optionally
+ # limited to a number of references.
+ #
+ # ref - The name (or names) of the branch(es)/tag(s) to limit the list of
+ # pipelines to.
+ def self.newest_first(ref = nil)
+ relation = order(id: :desc)
+
+ ref ? relation.where(ref: ref) : relation
end
- scope :internal, -> { where(source: internal_sources) }
def self.latest_status(ref = nil)
- latest(ref).status
+ newest_first(ref).pluck(:status).first
end
def self.latest_successful_for(ref)
- success.latest(ref).order(id: :desc).first
+ newest_first(ref).success.take
end
def self.latest_successful_for_refs(refs)
- success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
+ relation = newest_first(refs).success
+
+ relation.each_with_object({}) do |pipeline, hash|
hash[pipeline.ref] ||= pipeline
end
end
+ # Returns a Hash containing the latest pipeline status for every given
+ # commit.
+ #
+ # The keys of this Hash are the commit SHAs, the values the statuses.
+ #
+ # commits - The list of commit SHAs to get the status for.
+ # ref - The ref to scope the data to (e.g. "master"). If the ref is not
+ # given we simply get the latest status for the commits, regardless
+ # of what refs their pipelines belong to.
+ def self.latest_status_per_commit(commits, ref = nil)
+ p1 = arel_table
+ p2 = arel_table.alias
+
+ # This LEFT JOIN will filter out all but the newest row for every
+ # combination of (project_id, sha) or (project_id, sha, ref) if a ref is
+ # given.
+ cond = p1[:sha].eq(p2[:sha])
+ .and(p1[:project_id].eq(p2[:project_id]))
+ .and(p1[:id].lt(p2[:id]))
+
+ cond = cond.and(p1[:ref].eq(p2[:ref])) if ref
+ join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond)
+
+ relation = select(:sha, :status)
+ .where(sha: commits)
+ .where(p2[:id].eq(nil))
+ .joins(join.join_sources)
+
+ relation = relation.where(ref: ref) if ref
+
+ relation.each_with_object({}) do |row, hash|
+ hash[row[:sha]] = row[:status]
+ end
+ end
+
def self.truncate_sha(sha)
sha[0...8]
end
@@ -300,8 +335,10 @@ module Ci
def latest?
return false unless ref
+
commit = project.commit(ref)
return false unless commit
+
commit.sha == sha
end
@@ -327,7 +364,7 @@ module Ci
end
def has_kubernetes_active?
- project.kubernetes_service&.active?
+ project.deployment_platform&.active?
end
def has_stage_seeds?
@@ -469,7 +506,10 @@ module Ci
end
def latest_builds_with_artifacts
- @latest_builds_with_artifacts ||= builds.latest.with_artifacts
+ # We purposely cast the builds to an Array here. Because we always use the
+ # rows if there are more than 0 this prevents us from having to run two
+ # queries: one to get the count and one to get the rows.
+ @latest_builds_with_artifacts ||= builds.latest.with_artifacts.to_a
end
private
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index c6509f89117..dcbb397fb78 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -1,6 +1,7 @@
module Ci
class Runner < ActiveRecord::Base
extend Gitlab::Ci::Model
+ include Gitlab::SQL::Pattern
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
@@ -59,10 +60,7 @@ module Ci
#
# Returns an ActiveRecord::Relation.
def self.search(query)
- t = arel_table
- pattern = "%#{query}%"
-
- where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
+ fuzzy_search(query, [:token, :description])
end
def self.contact_time_deadline
@@ -114,7 +112,7 @@ module Ci
def can_pick?(build)
return false if self.ref_protected? && !build.protected?
- assignable_for?(build.project) && accepting_tags?(build)
+ assignable_for?(build.project_id) && accepting_tags?(build)
end
def only_for?(project)
@@ -173,8 +171,8 @@ module Ci
end
end
- def assignable_for?(project)
- is_shared? || projects.exists?(id: project.id)
+ def assignable_for?(project_id)
+ is_shared? || projects.exists?(id: project_id)
end
def accepting_tags?(build)
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 185d9473aab..55419189282 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -17,8 +17,7 @@ module Clusters
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
- # We have to ":destroy" it today to ensure that we clean also the Kubernetes Integration
- has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
@@ -29,15 +28,9 @@ module Clusters
validates :name, cluster_name: true
validate :restrict_modification, on: :update
- # TODO: Move back this into Clusters::Platforms::Kubernetes in 10.3
- # We need callback here because `enabled` belongs to Clusters::Cluster
- # Callbacks in Clusters::Platforms::Kubernetes will not be called after update
- after_save :update_kubernetes_integration!
-
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
- delegate :update_kubernetes_integration!, to: :platform, allow_nil: true
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
@@ -62,6 +55,10 @@ module Clusters
end
end
+ def created?
+ status_name == :created
+ end
+
def applications
[
application_helm || build_application_helm,
@@ -77,6 +74,10 @@ module Clusters
return platform_kubernetes if kubernetes?
end
+ def managed?
+ !user?
+ end
+
def first_project
return @first_project if defined?(@first_project)
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 6dc1ee810d3..9160a169452 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -1,7 +1,12 @@
module Clusters
module Platforms
class Kubernetes < ActiveRecord::Base
+ include Gitlab::CurrentSettings
+ include Gitlab::Kubernetes
+ include ReactiveCaching
+
self.table_name = 'cluster_platforms_kubernetes'
+ self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] }
belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
@@ -29,19 +34,17 @@ module Clusters
validates :api_url, url: true, presence: true
validates :token, presence: true
- # TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes
- after_destroy :destroy_kubernetes_integration!
+ validate :prevent_modification, on: :update
+
+ after_save :clear_reactive_cache!
alias_attribute :ca_pem, :ca_cert
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true
+ delegate :managed?, to: :cluster, allow_nil: true
- class << self
- def namespace_for_project(project)
- "#{project.path}-#{project.id}"
- end
- end
+ alias_method :active?, :enabled?
def actual_namespace
if namespace.present?
@@ -51,58 +54,138 @@ module Clusters
end
end
- def default_namespace
- self.class.namespace_for_project(project) if project
+ def predefined_variables
+ config = YAML.dump(kubeconfig)
+
+ variables = [
+ { key: 'KUBE_URL', value: api_url, public: true },
+ { key: 'KUBE_TOKEN', value: token, public: false },
+ { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true },
+ { key: 'KUBECONFIG', value: config, public: false, file: true }
+ ]
+
+ if ca_pem.present?
+ variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true }
+ variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+ end
+
+ variables
end
- def kubeclient
- @kubeclient ||= kubernetes_service.kubeclient if manages_kubernetes_service?
+ # Constructs a list of terminals from the reactive cache
+ #
+ # Returns nil if the cache is empty, in which case you should try again a
+ # short time later
+ def terminals(environment)
+ with_reactive_cache do |data|
+ pods = filter_by_label(data[:pods], app: environment.slug)
+ terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }
+ terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
+ end
end
- def update_kubernetes_integration!
- raise 'Kubernetes service already configured' unless manages_kubernetes_service?
+ # Caches resources in the namespace so other calls don't need to block on
+ # network access
+ def calculate_reactive_cache
+ return unless enabled? && project && !project.pending_delete?
- # This is neccesary, otheriwse enabled? returns true even though cluster updated with enabled: false
- cluster.reload
+ # We may want to cache extra things in the future
+ { pods: read_pods }
+ end
+
+ def kubeclient
+ @kubeclient ||= build_kubeclient!
+ end
+
+ private
- ensure_kubernetes_service&.update!(
- active: enabled?,
- api_url: api_url,
- namespace: namespace,
+ def kubeconfig
+ to_kubeconfig(
+ url: api_url,
+ namespace: actual_namespace,
token: token,
- ca_pem: ca_cert
- )
+ ca_pem: ca_pem)
end
- def active?
- manages_kubernetes_service?
+ def default_namespace
+ return unless project
+
+ slug = "#{project.path}-#{project.id}".downcase
+ slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
- private
+ def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ raise "Incomplete settings" unless api_url && actual_namespace
- def enforce_namespace_to_lower_case
- self.namespace = self.namespace&.downcase
+ unless (username && password) || token
+ raise "Either username/password or token is required to access API"
+ end
+
+ ::Kubeclient::Client.new(
+ join_api_url(api_path),
+ api_version,
+ auth_options: kubeclient_auth_options,
+ ssl_options: kubeclient_ssl_options,
+ http_proxy_uri: ENV['http_proxy']
+ )
end
- # TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class
- def manages_kubernetes_service?
- return true unless kubernetes_service&.active?
+ # Returns a hash of all pods in the namespace
+ def read_pods
+ kubeclient = build_kubeclient!
+
+ kubeclient.get_pods(namespace: actual_namespace).as_json
+ rescue KubeException => err
+ raise err unless err.error_code == 404
+
+ []
+ end
+
+ def kubeclient_ssl_options
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
+ end
- kubernetes_service.api_url == api_url
+ def kubeclient_auth_options
+ { bearer_token: token }
end
- def destroy_kubernetes_integration!
- return unless manages_kubernetes_service?
+ def join_api_url(api_path)
+ url = URI.parse(api_url)
+ prefix = url.path.sub(%r{/+\z}, '')
+
+ url.path = [prefix, api_path].join("/")
- kubernetes_service&.destroy!
+ url.to_s
end
- def kubernetes_service
- @kubernetes_service ||= project&.kubernetes_service
+ def terminal_auth
+ {
+ token: token,
+ ca_pem: ca_pem,
+ max_session_time: current_application_settings.terminal_max_session_time
+ }
end
- def ensure_kubernetes_service
- @kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service
+ def enforce_namespace_to_lower_case
+ self.namespace = self.namespace&.downcase
+ end
+
+ def prevent_modification
+ return unless managed?
+
+ if api_url_changed? || token_changed? || ca_pem_changed?
+ errors.add(:base, "cannot modify managed cluster")
+ return false
+ end
+
+ true
end
end
end
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
index ee2e43ee9dd..7fac32466ab 100644
--- a/app/models/clusters/providers/gcp.rb
+++ b/app/models/clusters/providers/gcp.rb
@@ -56,6 +56,7 @@ module Clusters
before_transition any => [:creating] do |provider, transition|
operation_id = transition.args.first
raise ArgumentError.new('operation_id is required') unless operation_id.present?
+
provider.operation_id = operation_id
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6dba154a6ea..6b28d290f99 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -80,10 +80,11 @@ class Commit
@raw = raw_commit
@project = project
+ @statuses = {}
end
def id
- @raw.id
+ raw.id
end
def ==(other)
@@ -108,12 +109,12 @@ class Commit
@link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end
- def to_reference(from_project = nil, full: false)
- commit_reference(from_project, id, full: full)
+ def to_reference(from = nil, full: false)
+ commit_reference(from, id, full: full)
end
- def reference_link_text(from_project = nil, full: false)
- commit_reference(from_project, short_id, full: full)
+ def reference_link_text(from = nil, full: false)
+ commit_reference(from, short_id, full: full)
end
def diff_line_count
@@ -236,11 +237,13 @@ class Commit
end
def status(ref = nil)
- @statuses ||= {}
-
return @statuses[ref] if @statuses.key?(ref)
- @statuses[ref] = pipelines.latest_status(ref)
+ @statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id]
+ end
+
+ def set_status_for_ref(ref, status)
+ @statuses[ref] = status
end
def signature
@@ -358,7 +361,7 @@ class Commit
@deltas ||= raw.deltas
end
- def diffs(diff_options = nil)
+ def diffs(diff_options = {})
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
end
@@ -378,8 +381,8 @@ class Commit
private
- def commit_reference(from_project, referable_commit_id, full: false)
- reference = project.to_reference(from_project, full: full)
+ def commit_reference(from, referable_commit_id, full: false)
+ reference = project.to_reference(from, full: full)
if reference.present?
"#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb
new file mode 100644
index 00000000000..dd93af9df64
--- /dev/null
+++ b/app/models/commit_collection.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# A collection of Commit instances for a specific project and Git reference.
+class CommitCollection
+ include Enumerable
+
+ attr_reader :project, :ref, :commits
+
+ # project - The project the commits belong to.
+ # commits - The Commit instances to store.
+ # ref - The name of the ref (e.g. "master").
+ def initialize(project, commits, ref = nil)
+ @project = project
+ @commits = commits
+ @ref = ref
+ end
+
+ def each(&block)
+ commits.each(&block)
+ end
+
+ # Sets the pipeline status for every commit.
+ #
+ # Setting this status ahead of time removes the need for running a query for
+ # every commit we're displaying.
+ def with_pipeline_status
+ statuses = project.pipelines.latest_status_per_commit(map(&:id), ref)
+
+ each do |commit|
+ commit.set_status_for_ref(ref, statuses[commit.id])
+ end
+
+ self
+ end
+
+ def respond_to_missing?(message, inc_private = false)
+ commits.respond_to?(message, inc_private)
+ end
+
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(message, *args, &block)
+ commits.public_send(message, *args, &block)
+ end
+end
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 84e2e8a5dd5..b93c111dabc 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -89,8 +89,8 @@ class CommitRange
alias_method :id, :to_s
- def to_reference(from_project = nil, full: false)
- project_reference = project.to_reference(from_project, full: full)
+ def to_reference(from = nil, full: false)
+ project_reference = project.to_reference(from, full: full)
if project_reference.present?
project_reference + self.class.reference_prefix + self.id
@@ -99,8 +99,8 @@ class CommitRange
end
end
- def reference_link_text(from_project = nil)
- project_reference = project.to_reference(from_project)
+ def reference_link_text(from = nil)
+ project_reference = project.to_reference(from)
reference = ref_from + notation + ref_to
if project_reference.present?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 6b07dbdf3ea..ee21ed8e420 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -17,6 +17,7 @@ class CommitStatus < ActiveRecord::Base
validates :name, presence: true, unless: :importing?
alias_attribute :author, :user
+ alias_attribute :pipeline_id, :commit_id
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
@@ -103,26 +104,29 @@ class CommitStatus < ActiveRecord::Base
end
after_transition do |commit_status, transition|
+ next unless commit_status.project
next if transition.loopback?
commit_status.run_after_commit do
- if pipeline
+ if pipeline_id
if complete? || manual?
- PipelineProcessWorker.perform_async(pipeline.id)
+ PipelineProcessWorker.perform_async(pipeline_id)
else
- PipelineUpdateWorker.perform_async(pipeline.id)
+ PipelineUpdateWorker.perform_async(pipeline_id)
end
end
- StageUpdateWorker.perform_async(commit_status.stage_id)
- ExpireJobCacheWorker.perform_async(commit_status.id)
+ StageUpdateWorker.perform_async(stage_id)
+ ExpireJobCacheWorker.perform_async(id)
end
end
after_transition any => :failed do |commit_status|
+ next unless commit_status.project
+
commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService
- .new(pipeline.project, nil).execute(self)
+ .new(project, nil).execute(self)
end
end
end
diff --git a/app/models/concerns/artifact_migratable.rb b/app/models/concerns/artifact_migratable.rb
new file mode 100644
index 00000000000..0460439e9e6
--- /dev/null
+++ b/app/models/concerns/artifact_migratable.rb
@@ -0,0 +1,45 @@
+# Adapter class to unify the interface between mounted uploaders and the
+# Ci::Artifact model
+# Meant to be prepended so the interface can stay the same
+module ArtifactMigratable
+ def artifacts_file
+ job_artifacts_archive&.file || legacy_artifacts_file
+ end
+
+ def artifacts_metadata
+ job_artifacts_metadata&.file || legacy_artifacts_metadata
+ end
+
+ def artifacts?
+ !artifacts_expired? && artifacts_file.exists?
+ end
+
+ def artifacts_metadata?
+ artifacts? && artifacts_metadata.exists?
+ end
+
+ def artifacts_file_changed?
+ job_artifacts_archive&.file_changed? || attribute_changed?(:artifacts_file)
+ end
+
+ def remove_artifacts_file!
+ if job_artifacts_archive
+ job_artifacts_archive.destroy
+ else
+ remove_legacy_artifacts_file!
+ end
+ end
+
+ def remove_artifacts_metadata!
+ if job_artifacts_metadata
+ job_artifacts_metadata.destroy
+ else
+ remove_legacy_artifacts_metadata!
+ end
+ end
+
+ def artifacts_size
+ read_attribute(:artifacts_size).to_i +
+ job_artifacts_archive&.size.to_i + job_artifacts_metadata&.size.to_i
+ end
+end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 9adc309a22b..d8394415362 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -98,6 +98,7 @@ module Awardable
def create_award_emoji(name, current_user)
return unless emoji_awardable?
+
award_emoji.create(name: normalize_name(name), user: current_user)
end
diff --git a/app/models/concerns/has_variable.rb b/app/models/concerns/has_variable.rb
index 9585b5583dc..8a241e4374a 100644
--- a/app/models/concerns/has_variable.rb
+++ b/app/models/concerns/has_variable.rb
@@ -16,6 +16,10 @@ module HasVariable
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
+ def key=(new_key)
+ super(new_key.to_s.strip)
+ end
+
def to_runner_variable
{ key: key, value: value, public: false }
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 35090181bd9..5ca4a7086cb 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -122,9 +122,7 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def search(query)
- title = to_fuzzy_arel(:title, query)
-
- where(title)
+ fuzzy_search(query, [:title])
end
# Searches for records with a matching title or description.
@@ -135,10 +133,7 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def full_search(query)
- title = to_fuzzy_arel(:title, query)
- description = to_fuzzy_arel(:description, query)
-
- where(title&.or(description))
+ fuzzy_search(query, [:title, :description])
end
def sort(method, excluded_labels: [])
@@ -255,8 +250,10 @@ module Issuable
participants(user).include?(user)
end
- def to_hook_data(user, old_labels: [], old_assignees: [], old_total_time_spent: nil)
+ def to_hook_data(user, old_associations: {})
changes = previous_changes
+ old_labels = old_associations.fetch(:labels, [])
+ old_assignees = old_associations.fetch(:assignees, [])
if old_labels != labels
changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)]
@@ -270,8 +267,12 @@ module Issuable
end
end
- if old_total_time_spent != total_time_spent
- changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
+ if self.respond_to?(:total_time_spent)
+ old_total_time_spent = old_associations.fetch(:total_time_spent, nil)
+
+ if old_total_time_spent != total_time_spent
+ changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
+ end
end
Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
@@ -345,4 +346,11 @@ module Issuable
def first_contribution?
false
end
+
+ ##
+ # Overriden in MergeRequest
+ #
+ def wipless_title_changed(old_title)
+ old_title != title
+ end
end
diff --git a/app/models/concerns/manual_inverse_association.rb b/app/models/concerns/manual_inverse_association.rb
new file mode 100644
index 00000000000..0fca8feaf89
--- /dev/null
+++ b/app/models/concerns/manual_inverse_association.rb
@@ -0,0 +1,17 @@
+module ManualInverseAssociation
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def manual_inverse_association(association, inverse)
+ define_method(association) do |*args|
+ super(*args).tap do |value|
+ next unless value
+
+ child_association = value.association(inverse)
+ child_association.set_inverse_instance(self)
+ child_association.target = self
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 1db6b2d2fa2..b43eaeaeea0 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -31,11 +31,11 @@ module Mentionable
#
# By default this will be the class name and the result of calling
# `to_reference` on the object.
- def gfm_reference(from_project = nil)
+ def gfm_reference(from = nil)
# "MergeRequest" > "merge_request" > "Merge request" > "merge request"
friendly_name = self.class.to_s.underscore.humanize.downcase
- "#{friendly_name} #{to_reference(from_project)}"
+ "#{friendly_name} #{to_reference(from)}"
end
# The GFM reference to this Mentionable, which shouldn't be included in its #references.
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index fde1cc44afa..e62f42e8e70 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -1,12 +1,6 @@
module ProtectedBranchAccess
extend ActiveSupport::Concern
- ALLOWED_ACCESS_LEVELS ||= [
- Gitlab::Access::MASTER,
- Gitlab::Access::DEVELOPER,
- Gitlab::Access::NO_ACCESS
- ].freeze
-
included do
include ProtectedRefAccess
@@ -14,18 +8,6 @@ module ProtectedBranchAccess
delegate :project, to: :protected_branch
- validates :access_level, presence: true, inclusion: {
- in: ALLOWED_ACCESS_LEVELS
- }
-
- def self.human_access_levels
- {
- Gitlab::Access::MASTER => "Masters",
- Gitlab::Access::DEVELOPER => "Developers + Masters",
- Gitlab::Access::NO_ACCESS => "No one"
- }.with_indifferent_access
- end
-
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index c4f158e569a..80c9f7d4eb4 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -1,13 +1,35 @@
module ProtectedRefAccess
extend ActiveSupport::Concern
+ ALLOWED_ACCESS_LEVELS = [
+ Gitlab::Access::MASTER,
+ Gitlab::Access::DEVELOPER,
+ Gitlab::Access::NO_ACCESS
+ ].freeze
+
+ HUMAN_ACCESS_LEVELS = {
+ Gitlab::Access::MASTER => "Masters".freeze,
+ Gitlab::Access::DEVELOPER => "Developers + Masters".freeze,
+ Gitlab::Access::NO_ACCESS => "No one".freeze
+ }.freeze
+
included do
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
+
+ validates :access_level, presence: true, if: :role?, inclusion: {
+ in: ALLOWED_ACCESS_LEVELS
+ }
end
def humanize
- self.class.human_access_levels[self.access_level]
+ HUMAN_ACCESS_LEVELS[self.access_level]
+ end
+
+ # CE access levels are always role-based,
+ # where as EE allows groups and users too
+ def role?
+ true
end
def check_access(user)
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index 78ac4f324e7..b782e85717e 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -7,7 +7,7 @@ module Referable
# Returns the String necessary to reference this object in Markdown
#
- # from_project - Refering Project object
+ # from - Referring parent object
#
# This should be overridden by the including class.
#
@@ -17,12 +17,12 @@ module Referable
# Issue.last.to_reference(other_project) # => "cross-project#1"
#
# Returns a String
- def to_reference(_from_project = nil, full:)
+ def to_reference(_from = nil, full:)
''
end
- def reference_link_text(from_project = nil)
- to_reference(from_project)
+ def reference_link_text(from = nil)
+ to_reference(from)
end
included do
diff --git a/app/models/email.rb b/app/models/email.rb
index 2da8b050149..d6516761f0a 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -1,5 +1,6 @@
class Email < ActiveRecord::Base
include Sortable
+ include Gitlab::SQL::Pattern
belongs_to :user
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 21a028e351c..bf69b4c50f0 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -138,11 +138,11 @@ class Environment < ActiveRecord::Base
end
def has_terminals?
- project.deployment_service.present? && available? && last_deployment.present?
+ project.deployment_platform.present? && available? && last_deployment.present?
end
def terminals
- project.deployment_service.terminals(self) if has_terminals?
+ project.deployment_platform.terminals(self) if has_terminals?
end
def has_metrics?
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 9ff56f229bc..2aaba2e4c90 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -38,11 +38,11 @@ class ExternalIssue
@project.id
end
- def to_reference(_from_project = nil, full: nil)
+ def to_reference(_from = nil, full: nil)
id
end
- def reference_link_text(from_project = nil)
+ def reference_link_text(from = nil)
return "##{id}" if id =~ /^\d+$/
id
diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb
index 6a9b52a1ef8..eb9417dc34f 100644
--- a/app/models/fork_network_member.rb
+++ b/app/models/fork_network_member.rb
@@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base
belongs_to :forked_from_project, class_name: 'Project'
validates :fork_network, :project, presence: true
+
+ after_destroy :cleanup_fork_network
+
+ private
+
+ def cleanup_fork_network
+ # Explicitly using `#count` makes sure we have the correct number if the
+ # relation was loaded in the fork_network.
+ fork_network.destroy if fork_network.fork_network_members.count == 0
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 8cf632fb566..505e943e464 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -2,6 +2,7 @@ require 'carrierwave/orm/activerecord'
class Group < Namespace
include Gitlab::ConfigHelper
+ include AfterCommitQueue
include AccessRequestable
include Avatarable
include Referable
@@ -50,20 +51,6 @@ class Group < Namespace
Gitlab::Database.postgresql?
end
- # Searches for groups matching the given query.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String
- #
- # Returns an ActiveRecord::Relation.
- def search(query)
- table = Namespace.arel_table
- pattern = "%#{query}%"
-
- where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
- end
-
def sort(method)
if method == 'storage_size_desc'
# storage_size is a virtual column so we need to
@@ -97,7 +84,7 @@ class Group < Namespace
end
end
- def to_reference(_from_project = nil, full: nil)
+ def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}"
end
@@ -303,6 +290,14 @@ class Group < Namespace
"#{parent.full_path}/#{path_was}"
end
+ def group_member(user)
+ if group_members.loaded?
+ group_members.find { |gm| gm.user_id == user.id }
+ else
+ group_members.find_by(user_id: user)
+ end
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/identity.rb b/app/models/identity.rb
index ac8094b610e..ff811e19f8a 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,18 +1,27 @@
class Identity < ActiveRecord::Base
include Sortable
include CaseSensitivity
+
belongs_to :user
validates :provider, presence: true
- validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
+ validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
validates :user_id, uniqueness: { scope: :provider }
+ scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do
- extern_uid = Gitlab::LDAP::Person.normalize_dn(extern_uid) if provider.starts_with?('ldap')
- where(extern_uid: extern_uid, provider: provider)
+ iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider)
end
def ldap?
provider.starts_with?('ldap')
end
+
+ def self.normalize_uid(provider, uid)
+ if provider.to_s.starts_with?('ldap')
+ Gitlab::LDAP::Person.normalize_dn(uid)
+ else
+ uid.to_s
+ end
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b5abc8f57b0..d6ef58d150b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -49,7 +49,6 @@ class Issue < ActiveRecord::Base
scope :public_only, -> { where(confidential: false) }
after_save :expire_etag_cache
- after_commit :update_project_counter_caches, on: :destroy
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
@@ -246,7 +245,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user]
+ if options.key?(:sidebar_endpoints) && project
+ url_helper = Gitlab::Routing.url_helpers
+
+ json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
+ toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self))
+ end
if options.key?(:labels)
json[:labels] = labels.as_json(
diff --git a/app/models/key.rb b/app/models/key.rb
index f119b15c737..a3f8a5d6dc7 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -2,6 +2,7 @@ require 'digest/md5'
class Key < ActiveRecord::Base
include Gitlab::CurrentSettings
+ include AfterCommitQueue
include Sortable
belongs_to :user
@@ -27,8 +28,10 @@ class Key < ActiveRecord::Base
after_commit :add_to_shell, on: :create
after_create :post_create_hook
+ after_create :refresh_user_cache
after_commit :remove_from_shell, on: :destroy
after_destroy :post_destroy_hook
+ after_destroy :refresh_user_cache
def key=(value)
value&.delete!("\n\r")
@@ -76,6 +79,12 @@ class Key < ActiveRecord::Base
)
end
+ def refresh_user_cache
+ return unless user
+
+ Users::KeysCountService.new(user).refresh_cache
+ end
+
def post_destroy_hook
SystemHooksService.new.execute_hooks_for(self, :destroy)
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 899028a01a0..b5bfa6ea2dd 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -165,12 +165,12 @@ class Label < ActiveRecord::Base
#
# Returns a String
#
- def to_reference(from_project = nil, target_project: nil, format: :id, full: false)
+ def to_reference(from = nil, target_project: nil, format: :id, full: false)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- if from_project
- "#{from_project.to_reference(target_project, full: full)}#{reference}"
+ if from
+ "#{from.to_reference(target_project, full: full)}#{reference}"
else
reference
end
diff --git a/app/models/member.rb b/app/models/member.rb
index cbbd58f2eaf..2fe5fda985f 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,4 +1,5 @@
class Member < ActiveRecord::Base
+ include AfterCommitQueue
include Sortable
include Importable
include Expirable
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index f1a5cc73e83..bbc01e9677c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -5,6 +5,8 @@ class MergeRequest < ActiveRecord::Base
include Referable
include IgnorableColumn
include TimeTrackable
+ include ManualInverseAssociation
+ include EachBatch
ignore_column :locked_at,
:ref_fetched
@@ -14,9 +16,28 @@ class MergeRequest < ActiveRecord::Base
belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs
+
has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
+ belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
+ manual_inverse_association :latest_merge_request_diff, :merge_request
+
+ # This is the same as latest_merge_request_diff unless:
+ # 1. There are arguments - in which case we might be trying to force-reload.
+ # 2. This association is already loaded.
+ # 3. The latest diff does not exist.
+ #
+ # The second one in particular is important - MergeRequestDiff#merge_request
+ # is the inverse of MergeRequest#merge_request_diff, which means it may not be
+ # the latest diff, because we could have loaded any diff from this particular
+ # MR. If we haven't already loaded a diff, then it's fine to load the latest.
+ def merge_request_diff(*args)
+ fallback = latest_merge_request_diff if args.empty? && !association(:merge_request_diff).loaded?
+
+ fallback || super
+ end
+
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -31,7 +52,6 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed
- after_commit :update_project_counter_caches, on: :destroy
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -167,6 +187,22 @@ class MergeRequest < ActiveRecord::Base
where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
+ # This is used after project import, to reset the IDs to the correct
+ # values. It is not intended to be called without having already scoped the
+ # relation.
+ def self.set_latest_merge_request_diff_ids!
+ update = '
+ latest_merge_request_diff_id = (
+ SELECT MAX(id)
+ FROM merge_request_diffs
+ WHERE merge_requests.id = merge_request_diffs.merge_request_id
+ )'.squish
+
+ self.each_batch do |batch|
+ batch.update_all(update)
+ end
+ end
+
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def self.work_in_progress?(title)
@@ -181,6 +217,12 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
+ # Verifies if title has changed not taking into account WIP prefix
+ # for merge requests.
+ def wipless_title_changed(old_title)
+ self.class.wipless_title(old_title) != self.wipless_title
+ end
+
def hook_attrs
Gitlab::HookData::MergeRequestBuilder.new(self).build
end
@@ -241,9 +283,9 @@ class MergeRequest < ActiveRecord::Base
if persisted?
merge_request_diff.commit_shas
elsif compare_commits
- compare_commits.reverse.map(&:sha)
+ compare_commits.to_a.reverse.map(&:sha)
else
- []
+ Array(diff_head_sha)
end
end
@@ -322,16 +364,28 @@ class MergeRequest < ActiveRecord::Base
# We use these attributes to force these to the intended values.
attr_writer :target_branch_sha, :source_branch_sha
+ def source_branch_ref
+ return @source_branch_sha if @source_branch_sha
+ return unless source_branch
+
+ Gitlab::Git::BRANCH_REF_PREFIX + source_branch
+ end
+
+ def target_branch_ref
+ return @target_branch_sha if @target_branch_sha
+ return unless target_branch
+
+ Gitlab::Git::BRANCH_REF_PREFIX + target_branch
+ end
+
def source_branch_head
return unless source_project
- source_branch_ref = @source_branch_sha || source_branch
source_project.repository.commit(source_branch_ref) if source_branch_ref
end
def target_branch_head
- target_branch_ref = @target_branch_sha || target_branch
- target_project.repository.commit(target_branch_ref) if target_branch_ref
+ target_project.repository.commit(target_branch_ref)
end
def branch_merge_base_commit
@@ -440,7 +494,7 @@ class MergeRequest < ActiveRecord::Base
def merge_request_diff_for(diff_refs_or_sha)
@merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
- diffs = merge_request_diffs.viewable.select_without_diff
+ diffs = merge_request_diffs.viewable
h[diff_refs_or_sha] =
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
diffs.find_by_diff_refs(diff_refs_or_sha)
@@ -845,7 +899,8 @@ class MergeRequest < ActiveRecord::Base
def compute_diverged_commits_count
return 0 unless source_branch_sha && target_branch_sha
- Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
+ target_project.repository
+ .count_commits_between(source_branch_sha, target_branch_sha)
end
private :compute_diverged_commits_count
@@ -864,28 +919,18 @@ class MergeRequest < ActiveRecord::Base
# Note that this could also return SHA from now dangling commits
#
def all_commit_shas
- if persisted?
- # MySQL doesn't support LIMIT in a subquery.
- diffs_relation =
- if Gitlab::Database.postgresql?
- merge_request_diffs.order(id: :desc).limit(100)
- else
- merge_request_diffs
- end
+ return commit_shas unless persisted?
- column_shas = MergeRequestDiffCommit
- .where(merge_request_diff: diffs_relation)
- .limit(10_000)
- .pluck('sha')
+ diffs_relation = merge_request_diffs
- serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
+ # MySQL doesn't support LIMIT in a subquery.
+ diffs_relation = diffs_relation.recent if Gitlab::Database.postgresql?
- (column_shas + serialised_shas).uniq
- elsif compare_commits
- compare_commits.to_a.reverse.map(&:id)
- else
- [diff_head_sha]
- end
+ MergeRequestDiffCommit
+ .where(merge_request_diff: diffs_relation)
+ .limit(10_000)
+ .pluck('sha')
+ .uniq
end
def merge_commit
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 1eda0f9cbbd..c37aa0a594b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,21 +1,21 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
- include Gitlab::EncodingHelper
+ include ManualInverseAssociation
+ include IgnorableColumn
- # Prevent store of diff if commits amount more then 500
+ # Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100
- # Valid types of serialized diffs allowed by Gitlab::Git::Diff
- VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze
+ ignore_column :st_commits,
+ :st_diffs
belongs_to :merge_request
+ manual_inverse_association :merge_request, :merge_request_diff
+
has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) }
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
- serialize :st_commits # rubocop:disable Cop/ActiveRecordSerialize
- serialize :st_diffs # rubocop:disable Cop/ActiveRecordSerialize
-
state_machine :state, initial: :empty do
state :collected
state :overflow
@@ -29,6 +29,8 @@ class MergeRequestDiff < ActiveRecord::Base
scope :viewable, -> { without_state(:empty) }
+ scope :recent, -> { order(id: :desc).limit(100) }
+
# All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing?
@@ -37,14 +39,6 @@ class MergeRequestDiff < ActiveRecord::Base
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
end
- def self.select_without_diff
- select(column_names - ['st_diffs'])
- end
-
- def st_commits
- super || []
- end
-
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
@@ -126,11 +120,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commit_shas
- if st_commits.present?
- st_commits.map { |commit| commit[:id] }
- else
- merge_request_diff_commits.map(&:sha)
- end
+ merge_request_diff_commits.map(&:sha)
end
def diff_refs=(new_diff_refs)
@@ -194,7 +184,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def latest?
- self == merge_request.merge_request_diff
+ self.id == merge_request.latest_merge_request_diff_id
end
def compare_with(sha)
@@ -205,34 +195,11 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commits_count
- if st_commits.present?
- st_commits.size
- else
- merge_request_diff_commits.size
- end
- end
-
- def utf8_st_diffs
- return [] if st_diffs.blank?
-
- st_diffs.map do |diff|
- diff.each do |k, v|
- diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
- end
- end
+ merge_request_diff_commits.size
end
private
- # Old GitLab implementations may have generated diffs as ["--broken-diff"].
- # Avoid an error 500 by ignoring bad elements. See:
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
- def valid_raw_diff?(raw)
- return false unless raw.respond_to?(:each)
-
- raw.any? { |element| VALID_CLASSES.include?(element.class) }
- end
-
def create_merge_request_diff_files(diffs)
rows = diffs.map.with_index do |diff, index|
diff_hash = diff.to_hash.merge(
@@ -256,9 +223,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def load_diffs(options)
- return Gitlab::Git::DiffCollection.new([]) unless diffs_from_database
-
- raw = diffs_from_database
+ raw = merge_request_diff_files.map(&:to_hash)
if paths = options[:paths]
raw = raw.select do |diff|
@@ -269,23 +234,11 @@ class MergeRequestDiff < ActiveRecord::Base
Gitlab::Git::DiffCollection.new(raw, options)
end
- def diffs_from_database
- return @diffs_from_database if defined?(@diffs_from_database)
-
- @diffs_from_database =
- if st_diffs.present?
- if valid_raw_diff?(st_diffs)
- st_diffs
- end
- elsif merge_request_diff_files.present?
- merge_request_diff_files.map(&:to_hash)
- end
- end
-
def load_commits
- commits = st_commits.presence || merge_request_diff_commits
+ commits = merge_request_diff_commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
- commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
+ CommitCollection
+ .new(merge_request.source_project, commits, merge_request.source_branch)
end
def save_diffs
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 47e6b785c39..c06ee8083f0 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -13,6 +13,7 @@ class Milestone < ActiveRecord::Base
include Referable
include StripAttribute
include Milestoneish
+ include Gitlab::SQL::Pattern
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -73,10 +74,7 @@ class Milestone < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search(query)
- t = arel_table
- pattern = "%#{query}%"
-
- where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
+ fuzzy_search(query, [:title, :description])
end
def filter_by_state(milestones, state)
@@ -162,18 +160,18 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
- def to_reference(from_project = nil, format: :name, full: false)
+ def to_reference(from = nil, format: :name, full: false)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
if project
- "#{project.to_reference(from_project, full: full)}#{reference}"
+ "#{project.to_reference(from, full: full)}#{reference}"
else
reference
end
end
- def reference_link_text(from_project = nil)
+ def reference_link_text(from = nil)
self.title
end
@@ -256,7 +254,7 @@ class Milestone < ActiveRecord::Base
def start_date_should_be_less_than_due_date
if due_date <= start_date
- errors.add(:start_date, "Can't be greater than due date")
+ errors.add(:due_date, "must be greater than start date")
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 4d401e7ba18..901dbf2ba69 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -9,6 +9,7 @@ class Namespace < ActiveRecord::Base
include Routable
include AfterCommitQueue
include Storage::LegacyNamespace
+ include Gitlab::SQL::Pattern
# Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of
@@ -86,10 +87,7 @@ class Namespace < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation
def search(query)
- t = arel_table
- pattern = "%#{query}%"
-
- where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
+ fuzzy_search(query, [:name, :path])
end
def clean_path(path)
@@ -141,7 +139,17 @@ class Namespace < ActiveRecord::Base
def find_fork_of(project)
return nil unless project.fork_network
- project.fork_network.find_forks_in(projects).first
+ if RequestStore.active?
+ forks_in_namespace = RequestStore.fetch("namespaces:#{id}:forked_projects") do
+ Hash.new do |found_forks, project|
+ found_forks[project] = project.fork_network.find_forks_in(projects).first
+ end
+ end
+
+ forks_in_namespace[project]
+ else
+ project.fork_network.find_forks_in(projects).first
+ end
end
def lfs_enabled?
diff --git a/app/models/note.rb b/app/models/note.rb
index f9676361072..340fe087f82 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -14,6 +14,7 @@ class Note < ActiveRecord::Base
include ResolvableNote
include IgnorableColumn
include Editable
+ include Gitlab::SQL::Pattern
module SpecialRole
FIRST_TIME_CONTRIBUTOR = :first_time_contributor
@@ -110,6 +111,7 @@ class Note < ActiveRecord::Base
includes(:author, :noteable, :updated_by,
project: [:project_members, { group: [:group_members] }])
end
+ scope :with_metadata, -> { includes(:system_note_metadata) }
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
@@ -166,10 +168,20 @@ class Note < ActiveRecord::Base
def has_special_role?(role, note)
note.special_role == role
end
+
+ def search(query)
+ fuzzy_search(query, [:note])
+ end
end
def cross_reference?
- system? && matches_cross_reference_regex?
+ return unless system?
+
+ if force_cross_reference_regex_check?
+ matches_cross_reference_regex?
+ else
+ SystemNoteService.cross_reference?(note)
+ end
end
def diff_note?
@@ -382,4 +394,10 @@ class Note < ActiveRecord::Base
def set_discussion_id
self.discussion_id ||= discussion_class.discussion_id(self)
end
+
+ def force_cross_reference_regex_check?
+ return unless system?
+
+ SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.include?(system_note_metadata&.action)
+ end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 43c77f3f2a2..8de42ff9d2e 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -65,6 +65,7 @@ class PagesDomain < ActiveRecord::Base
def expired?
return false unless x509
+
current = Time.new
current < x509.not_before || x509.not_after < current
end
@@ -75,6 +76,7 @@ class PagesDomain < ActiveRecord::Base
def subject
return unless x509
+
x509.subject.to_s
end
@@ -102,6 +104,7 @@ class PagesDomain < ActiveRecord::Base
def validate_pages_domain
return unless domain
+
if domain.downcase.ends_with?(Settings.pages.host.downcase)
self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
end
@@ -109,6 +112,7 @@ class PagesDomain < ActiveRecord::Base
def x509
return unless certificate
+
@x509 ||= OpenSSL::X509::Certificate.new(certificate)
rescue OpenSSL::X509::CertificateError
nil
@@ -116,6 +120,7 @@ class PagesDomain < ActiveRecord::Base
def pkey
return unless key
+
@pkey ||= OpenSSL::PKey::RSA.new(key)
rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
nil
diff --git a/app/models/project.rb b/app/models/project.rb
index 894ded2a9f6..41657c171e2 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -18,6 +18,7 @@ class Project < ActiveRecord::Base
include SelectForProjectAuthorization
include Routable
include GroupDescendant
+ include Gitlab::SQL::Pattern
extend Gitlab::ConfigHelper
extend Gitlab::CurrentSettings
@@ -188,7 +189,6 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
- has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
@@ -233,8 +233,8 @@ class Project < ActiveRecord::Base
validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true
validates :ci_config_path,
- format: { without: /\.{2}/,
- message: 'cannot include directory traversal.' },
+ format: { without: /(\.{2}|\A\/)/,
+ message: 'cannot include leading slash or directory traversal.' },
length: { maximum: 255 },
allow_blank: true
validates :name,
@@ -272,8 +272,9 @@ class Project < ActiveRecord::Base
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
- scope :with_hashed_storage, -> { where('storage_version >= 1') }
- scope :with_legacy_storage, -> { where(storage_version: [nil, 0]) }
+ scope :with_storage_feature, ->(feature) { where('storage_version >= :version', version: HASHED_STORAGE_FEATURES[feature]) }
+ 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) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
@@ -424,32 +425,11 @@ class Project < ActiveRecord::Base
#
# query - The search query as a String.
def search(query)
- ptable = arel_table
- ntable = Namespace.arel_table
- pattern = "%#{query}%"
-
- # unscoping unnecessary conditions that'll be applied
- # when executing `where("projects.id IN (#{union.to_sql})")`
- projects = unscoped.select(:id).where(
- ptable[:path].matches(pattern)
- .or(ptable[:name].matches(pattern))
- .or(ptable[:description].matches(pattern))
- )
-
- namespaces = unscoped.select(:id)
- .joins(:namespace)
- .where(ntable[:name].matches(pattern))
-
- union = Gitlab::SQL::Union.new([projects, namespaces])
-
- where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ fuzzy_search(query, [:path, :name, :description])
end
def search_by_title(query)
- pattern = "%#{query}%"
- table = Project.arel_table
-
- non_archived.where(table[:name].matches(pattern))
+ non_archived.fuzzy_search(query, [:name])
end
def visibility_levels
@@ -581,8 +561,7 @@ class Project < ActiveRecord::Base
if forked?
RepositoryForkWorker.perform_async(id,
forked_from_project.repository_storage_path,
- forked_from_project.full_path,
- self.namespace.full_path)
+ forked_from_project.disk_path)
else
RepositoryImportWorker.perform_async(self.id)
end
@@ -618,7 +597,7 @@ class Project < ActiveRecord::Base
def ci_config_path=(value)
# Strip all leading slashes so that //foo -> foo
- super(value&.sub(%r{\A/+}, '')&.delete("\0"))
+ super(value&.delete("\0"))
end
def import_url=(value)
@@ -760,10 +739,10 @@ class Project < ActiveRecord::Base
end
end
- def to_human_reference(from_project = nil)
- if cross_namespace_reference?(from_project)
+ def to_human_reference(from = nil)
+ if cross_namespace_reference?(from)
name_with_namespace
- elsif cross_project_reference?(from_project)
+ elsif cross_project_reference?(from)
name
end
end
@@ -772,13 +751,14 @@ class Project < ActiveRecord::Base
Gitlab::Routing.url_helpers.project_url(self)
end
- def new_issue_address(author)
+ def new_issuable_address(author, address_type)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
author.ensure_incoming_email_token!
+ suffix = address_type == 'merge_request' ? '+merge-request' : ''
Gitlab::IncomingEmail.reply_address(
- "#{full_path}+#{author.incoming_email_token}")
+ "#{full_path}#{suffix}+#{author.incoming_email_token}")
end
def build_commit_note(commit)
@@ -916,12 +896,10 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
- def deployment_services
- services.where(category: :deployment)
- end
-
- def deployment_service
- @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
+ # TODO: This will be extended for multiple enviroment clusters
+ def deployment_platform
+ @deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes
+ @deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true)
end
def monitoring_services
@@ -1135,7 +1113,11 @@ class Project < ActiveRecord::Base
end
def project_member(user)
- project_members.find_by(user_id: user)
+ if project_members.loaded?
+ project_members.find { |member| member.user_id == user.id }
+ else
+ project_members.find_by(user_id: user)
+ end
end
def default_branch
@@ -1566,9 +1548,9 @@ class Project < ActiveRecord::Base
end
def deployment_variables
- return [] unless deployment_service
+ return [] unless deployment_platform
- deployment_service.predefined_variables
+ deployment_platform.predefined_variables
end
def auto_devops_variables
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 976d85246a8..768f0a7472e 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -51,8 +51,10 @@ class HipchatService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
+
message = create_message(data)
return unless message.present?
+
gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index b487378edd2..1c065e1ddbd 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -176,6 +176,7 @@ class JiraService < IssueTrackerService
def test_settings
return unless client_url.present?
+
# Test settings by getting the project
jira_request { client.ServerInfo.all.attrs }
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 5080acffb3c..b82567ce2b3 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -1,3 +1,8 @@
+##
+# NOTE:
+# We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic.
+# After we've migrated data, we'll remove KubernetesService. This would happen in a few months.
+# If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes.
class KubernetesService < DeploymentService
include Gitlab::CurrentSettings
include Gitlab::Kubernetes
@@ -182,6 +187,7 @@ class KubernetesService < DeploymentService
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err
raise err unless err.error_code == 404
+
[]
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 715b215d1db..17b9d2cf7b4 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -35,7 +35,9 @@ class ProjectStatistics < ActiveRecord::Base
end
def update_build_artifacts_size
- self.build_artifacts_size = project.builds.sum(:artifacts_size)
+ self.build_artifacts_size =
+ project.builds.sum(:artifacts_size) +
+ Ci::JobArtifact.artifacts_size_for(self)
end
def update_storage_size
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 3eecbea8cbf..a0af749a93f 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -76,8 +76,8 @@ class ProjectWiki
# Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages.
- def pages
- wiki.pages.map { |page| WikiPage.new(self, page, true) }
+ def pages(limit: nil)
+ wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
end
# Finds a page within the repository based on a tile
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 89bfc5f9a9c..d28fed11ca8 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -10,7 +10,9 @@ class ProtectedBranch < ActiveRecord::Base
def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected?
- self.matching(ref_name, protected_refs: project.protected_branches).present?
+ refs = project.protected_branches.select(:name)
+
+ self.matching(ref_name, protected_refs: refs).present?
end
def self.default_branch_protected?
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
index f38109c0e52..42a9bcf7723 100644
--- a/app/models/protected_tag.rb
+++ b/app/models/protected_tag.rb
@@ -5,6 +5,8 @@ class ProtectedTag < ActiveRecord::Base
protected_ref_access_levels :create
def self.protected?(project, ref_name)
- self.matching(ref_name, protected_refs: project.protected_tags).present?
+ refs = project.protected_tags.select(:name)
+
+ self.matching(ref_name, protected_refs: refs).present?
end
end
diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb
index c7e1319719d..6b6ab3d8279 100644
--- a/app/models/protected_tag/create_access_level.rb
+++ b/app/models/protected_tag/create_access_level.rb
@@ -1,18 +1,6 @@
class ProtectedTag::CreateAccessLevel < ActiveRecord::Base
include ProtectedTagAccess
- validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
- Gitlab::Access::DEVELOPER,
- Gitlab::Access::NO_ACCESS] }
-
- def self.human_access_levels
- {
- Gitlab::Access::MASTER => "Masters",
- Gitlab::Access::DEVELOPER => "Developers + Masters",
- Gitlab::Access::NO_ACCESS => "No one"
- }.with_indifferent_access
- end
-
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 3a89fa9264b..82af299ec5e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -132,7 +132,8 @@ class Repository
commits = Gitlab::Git::Commit.where(options)
commits = Commit.decorate(commits, @project) if commits.present?
- commits
+
+ CommitCollection.new(project, commits, ref)
end
def commits_between(from, to)
@@ -148,11 +149,14 @@ class Repository
end
raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled|
- if is_enabled
- find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
- else
- find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
- end
+ commits =
+ if is_enabled
+ find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
+ else
+ find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
+ end
+
+ CommitCollection.new(project, commits, ref)
end
end
@@ -213,11 +217,7 @@ class Repository
def branch_exists?(branch_name)
return false unless raw_repository
- @branch_exists_memo ||= Hash.new do |hash, key|
- hash[key] = raw_repository.branch_exists?(key)
- end
-
- @branch_exists_memo[branch_name]
+ branch_names.include?(branch_name)
end
def ref_exists?(ref)
@@ -242,6 +242,7 @@ class Repository
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex
raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
+
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
end
end
@@ -255,7 +256,7 @@ class Repository
end
def diverging_commit_counts(branch)
- root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
+ root_ref_hash = raw_repository.commit(root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
@@ -473,6 +474,11 @@ class Repository
nil
end
+ # items is an Array like: [[oid, path], [oid1, path1]]
+ def blobs_at(items)
+ raw_repository.batch_blobs(items).map { |blob| Blob.decorate(blob, project) }
+ end
+
def root_ref
if raw_repository
raw_repository.root_ref
@@ -662,6 +668,7 @@ class Repository
def next_branch(name, opts = {})
branch_ids = self.branch_names.map do |n|
next 1 if n == name
+
result = n.match(/\A#{name}-([0-9]+)\z/)
result[1].to_i if result
end.compact
@@ -902,19 +909,13 @@ class Repository
end
end
- def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil)
+ def merged_to_root_ref?(branch_or_name)
branch = Gitlab::Git::Branch.find(self, branch_or_name)
if branch
@root_ref_sha ||= commit(root_ref).sha
same_head = branch.target == @root_ref_sha
- merged =
- if pre_loaded_merged_branches
- pre_loaded_merged_branches.include?(branch.name)
- else
- ancestor?(branch.target, @root_ref_sha)
- end
-
+ merged = ancestor?(branch.target, @root_ref_sha)
!same_head && merged
else
nil
@@ -965,6 +966,19 @@ class Repository
run_git(args).first.lines.map(&:strip)
end
+ def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil)
+ unless remote_name
+ remote_name = "tmp-#{SecureRandom.hex}"
+ tmp_remote_name = true
+ end
+
+ add_remote(remote_name, url)
+ set_remote_as_mirror(remote_name, refmap: refmap)
+ fetch_remote(remote_name, forced: forced)
+ 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)
end
@@ -990,10 +1004,6 @@ class Repository
raw_repository.ls_files(actual_ref)
end
- def gitattribute(path, name)
- raw_repository.attributes(path)[name]
- end
-
def copy_gitattributes(ref)
actual_ref = ref || root_ref
begin
@@ -1066,6 +1076,10 @@ class Repository
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end
+ def repository_storage_path
+ @project.repository_storage_path
+ end
+
private
# TODO Generice finder, later split this on finders by Ref or Oid
@@ -1131,10 +1145,6 @@ class Repository
raw_repository.run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip
end
- def repository_storage_path
- @project.repository_storage_path
- end
-
def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki))
end
diff --git a/app/models/service.rb b/app/models/service.rb
index fdd2605e3e3..3c4f1885dd0 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -211,7 +211,7 @@ class Service < ActiveRecord::Base
def async_execute(data)
return unless supported_events.include?(data[:object_kind])
- Sidekiq::Client.enqueue(ProjectServiceWorker, id, data)
+ ProjectServiceWorker.perform_async(id, data)
end
def issue_tracker?
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 9533aa7f555..05a16f11b59 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -9,6 +9,7 @@ class Snippet < ActiveRecord::Base
include Mentionable
include Spammable
include Editable
+ include Gitlab::SQL::Pattern
extend Gitlab::CurrentSettings
@@ -75,11 +76,11 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
- def to_reference(from_project = nil, full: false)
+ def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}"
if project.present?
- "#{project.to_reference(from_project, full: full)}#{reference}"
+ "#{project.to_reference(from, full: full)}#{reference}"
else
reference
end
@@ -135,10 +136,7 @@ class Snippet < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search(query)
- t = arel_table
- pattern = "%#{query}%"
-
- where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
+ fuzzy_search(query, [:title, :file_name])
end
# Searches for snippets with matching content.
@@ -149,10 +147,7 @@ class Snippet < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search_code(query)
- table = Snippet.arel_table
- pattern = "%#{query}%"
-
- where(table[:content].matches(pattern))
+ fuzzy_search(query, [:content])
end
end
end
diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb
index f025f40994e..fae1b64961a 100644
--- a/app/models/storage/hashed_project.rb
+++ b/app/models/storage/hashed_project.rb
@@ -4,7 +4,6 @@ module Storage
delegate :gitlab_shell, :repository_storage_path, to: :project
ROOT_PATH_PREFIX = '@hashed'.freeze
- STORAGE_VERSION = 1
def initialize(project)
@project = project
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 1f9f8d7286b..29035480371 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -1,4 +1,14 @@
class SystemNoteMetadata < ActiveRecord::Base
+ # These notes's action text might contain a reference that is external.
+ # We should always force a deep validation upon references that are found
+ # in this note type.
+ # Other notes can always be safely shown as all its references are
+ # in the same project (i.e. with the same permissions)
+ TYPES_WITH_CROSS_REFERENCES = %w[
+ commit cross_reference
+ close duplicate
+ ].freeze
+
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved
diff --git a/app/models/user.rb b/app/models/user.rb
index ea10e2854d6..38ee4ed50c1 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -7,6 +7,7 @@ class User < ActiveRecord::Base
include Gitlab::ConfigHelper
include Gitlab::CurrentSettings
include Gitlab::SQL::Pattern
+ include AfterCommitQueue
include Avatarable
include Referable
include Sortable
@@ -170,6 +171,7 @@ class User < ActiveRecord::Base
after_save :ensure_namespace_correct
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
+ after_destroy :remove_key_cache
after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
@@ -268,8 +270,7 @@ class User < ActiveRecord::Base
end
def for_github_id(id)
- joins(:identities)
- .where(identities: { provider: :github, extern_uid: id.to_s })
+ joins(:identities).merge(Identity.with_extern_uid(:github, id))
end
# Find a User by their primary email or any associated secondary email
@@ -313,9 +314,6 @@ class User < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search(query)
- table = arel_table
- pattern = User.to_pattern(query)
-
order = <<~SQL
CASE
WHEN users.name = %{query} THEN 0
@@ -325,11 +323,8 @@ class User < ActiveRecord::Base
END
SQL
- where(
- table[:name].matches(pattern)
- .or(table[:email].matches(pattern))
- .or(table[:username].matches(pattern))
- ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
+ fuzzy_search(query, [:name, :email, :username])
+ .reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
end
# searches user by given pattern
@@ -337,16 +332,16 @@ class User < ActiveRecord::Base
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
def search_with_secondary_emails(query)
- table = arel_table
email_table = Email.arel_table
- pattern = "%#{query}%"
- matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern))
+ matched_by_emails_user_ids = email_table
+ .project(email_table[:user_id])
+ .where(Email.fuzzy_arel_match(:email, query))
where(
- table[:name].matches(pattern)
- .or(table[:email].matches(pattern))
- .or(table[:username].matches(pattern))
- .or(table[:id].in(matched_by_emails_user_ids))
+ fuzzy_arel_match(:name, query)
+ .or(fuzzy_arel_match(:email, query))
+ .or(fuzzy_arel_match(:username, query))
+ .or(arel_table[:id].in(matched_by_emails_user_ids))
)
end
@@ -437,7 +432,7 @@ class User < ActiveRecord::Base
username
end
- def to_reference(_from_project = nil, target_project: nil, full: nil)
+ def to_reference(_from = nil, target_project: nil, full: nil)
"#{self.class.reference_prefix}#{username}"
end
@@ -445,6 +440,10 @@ class User < ActiveRecord::Base
skip_confirmation! if bool
end
+ def skip_reconfirmation=(bool)
+ skip_reconfirmation! if bool
+ end
+
def generate_reset_token
@reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
@@ -489,7 +488,11 @@ class User < ActiveRecord::Base
end
def two_factor_u2f_enabled?
- u2f_registrations.exists?
+ if u2f_registrations.loaded?
+ u2f_registrations.any?
+ else
+ u2f_registrations.exists?
+ end
end
def namespace_uniq
@@ -624,21 +627,39 @@ class User < ActiveRecord::Base
end
def require_ssh_key?
- keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
+ count = Users::KeysCountService.new(self).count
+
+ count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
+ end
+
+ def require_password_creation_for_web?
+ allow_password_authentication_for_web? && password_automatically_set?
end
- def require_password_creation?
- password_automatically_set? && allow_password_authentication?
+ def require_password_creation_for_git?
+ allow_password_authentication_for_git? && password_automatically_set?
end
def require_personal_access_token_creation_for_git_auth?
- return false if current_application_settings.password_authentication_enabled? || ldap_user?
+ return false if allow_password_authentication_for_git? || ldap_user?
PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none?
end
+ def require_extra_setup_for_git_auth?
+ require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
+ end
+
def allow_password_authentication?
- !ldap_user? && current_application_settings.password_authentication_enabled?
+ allow_password_authentication_for_web? || allow_password_authentication_for_git?
+ end
+
+ def allow_password_authentication_for_web?
+ current_application_settings.password_authentication_enabled_for_web? && !ldap_user?
+ end
+
+ def allow_password_authentication_for_git?
+ current_application_settings.password_authentication_enabled_for_git? && !ldap_user?
end
def can_change_username?
@@ -883,9 +904,14 @@ class User < ActiveRecord::Base
def post_destroy_hook
log_info("User \"#{name}\" (#{email}) was removed")
+
system_hook_service.execute_hooks_for(self, :destroy)
end
+ def remove_key_cache
+ Users::KeysCountService.new(self).delete_cache
+ end
+
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
DeleteUserWorker.perform_async(deleted_by.id, id, params)
@@ -978,7 +1004,11 @@ class User < ActiveRecord::Base
end
def notification_settings_for(source)
- notification_settings.find_or_initialize_by(source: source)
+ if notification_settings.loaded?
+ notification_settings.find { |notification| notification.source == source }
+ else
+ notification_settings.find_or_initialize_by(source: source)
+ end
end
# Lazy load global notification setting
@@ -1119,6 +1149,7 @@ class User < ActiveRecord::Base
# override, from Devise::Validatable
def password_required?
return false if internal?
+
super
end
@@ -1136,6 +1167,7 @@ class User < ActiveRecord::Base
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
def send_devise_notification(notification, *args)
return true unless can?(:receive_notifications)
+
devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 5f710961f95..bdfef677ef3 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -127,19 +127,24 @@ class WikiPage
@version ||= @page.version
end
- # Returns an array of Gitlab Commit instances.
- def versions
+ def versions(options = {})
return [] unless persisted?
- wiki.wiki.page_versions(@page.path)
+ wiki.wiki.page_versions(@page.path, options)
end
- def commit
- versions.first
+ def count_versions
+ return [] unless persisted?
+
+ wiki.wiki.count_page_versions(@page.path)
+ end
+
+ def last_version
+ @last_version ||= versions(limit: 1).first
end
def last_commit_sha
- commit&.sha
+ last_version&.sha
end
# Returns the Date that this latest version was
@@ -151,7 +156,7 @@ class WikiPage
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
- @page.historical? && versions.first.sha != version.sha
+ @page.historical? && last_version.sha != version.sha
end
# Returns boolean True or False if this instance
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 8af9738d75c..a2518bc1080 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -34,6 +34,8 @@ class GroupPolicy < BasePolicy
rule { admin } .enable :read_group
rule { has_projects } .enable :read_group
+ rule { has_access }.enable :read_namespace
+
rule { developer }.enable :admin_milestones
rule { reporter }.enable :admin_label
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
index 92213f0155e..eb01218eb0a 100644
--- a/app/policies/namespace_policy.rb
+++ b/app/policies/namespace_policy.rb
@@ -8,6 +8,7 @@ class NamespacePolicy < BasePolicy
rule { owner | admin }.policy do
enable :create_projects
enable :admin_namespace
+ enable :read_namespace
end
rule { personal_project & ~can_create_personal_project }.prevent :create_projects
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 01cb59d0d44..a424da5ab24 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -5,5 +5,9 @@ module Clusters
def gke_cluster_url
"https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end
+
+ def can_toggle_cluster?
+ can?(current_user, :update_cluster, cluster) && created?
+ end
end
end
diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb
new file mode 100644
index 00000000000..f2844854112
--- /dev/null
+++ b/app/services/base_count_service.rb
@@ -0,0 +1,44 @@
+# Base class for services that count a single resource such as the number of
+# issues for a project.
+class BaseCountService
+ def relation_for_count
+ raise(
+ NotImplementedError,
+ '"relation_for_count" must be implemented and return an ActiveRecord::Relation'
+ )
+ end
+
+ def count
+ Rails.cache.fetch(cache_key, cache_options) { uncached_count }.to_i
+ end
+
+ def count_stored?
+ Rails.cache.read(cache_key).present?
+ end
+
+ def refresh_cache(&block)
+ Rails.cache.write(cache_key, block_given? ? yield : uncached_count, raw: raw?)
+ end
+
+ def uncached_count
+ relation_for_count.count
+ end
+
+ def delete_cache
+ Rails.cache.delete(cache_key)
+ end
+
+ def raw?
+ false
+ end
+
+ def cache_key
+ raise NotImplementedError, 'cache_key must be implemented and return a String'
+ end
+
+ # subclasses can override to add any specific options, such as
+ # super.merge({ expires_in: 5.minutes })
+ def cache_options
+ { raw: raw? }
+ end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 31a712ccc1b..7b9ea223d26 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -2,27 +2,24 @@ module Ci
class CreatePipelineService < BaseService
attr_reader :pipeline
- SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
+ SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Build,
+ Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::Create].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
- @pipeline = Ci::Pipeline.new(
- source: source,
- project: project,
- ref: ref,
- sha: sha,
- before_sha: before_sha,
- tag: tag_exists?,
- trigger_requests: Array(trigger_request),
- user: current_user,
- pipeline_schedule: schedule,
- protected: project.protected_for?(ref)
- )
-
- command = OpenStruct.new(ignore_skip_ci: ignore_skip_ci,
+ @pipeline = Ci::Pipeline.new
+
+ command = OpenStruct.new(source: source,
+ origin_ref: params[:ref],
+ checkout_sha: params[:checkout_sha],
+ after_sha: params[:after],
+ before_sha: params[:before],
+ trigger_request: trigger_request,
+ schedule: schedule,
+ ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
project: project,
@@ -45,14 +42,6 @@ module Ci
private
- def commit
- @commit ||= project.commit(origin_sha || origin_ref)
- end
-
- def sha
- commit.try(:id)
- end
-
def update_merge_requests_head_pipeline
return unless pipeline.latest?
@@ -76,26 +65,6 @@ module Ci
.created_or_pending
end
- def before_sha
- params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
- end
-
- def origin_sha
- params[:checkout_sha] || params[:after]
- end
-
- def origin_ref
- params[:ref]
- end
-
- def tag_exists?
- project.repository.tag_exists?(ref)
- end
-
- def ref
- @ref ||= Gitlab::Git.ref_name(origin_ref)
- end
-
def pipeline_created_counter
@pipeline_created_counter ||= Gitlab::Metrics
.counter(:pipelines_created_total, "Counter of pipelines created")
diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb
index 44da87cb00c..e73c6ad6780 100644
--- a/app/services/ci/fetch_kubernetes_token_service.rb
+++ b/app/services/ci/fetch_kubernetes_token_service.rb
@@ -34,6 +34,7 @@ module Ci
kubeclient.get_secrets.as_json
rescue KubeException => err
raise err unless err.error_code == 404
+
[]
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index b8db709211a..2ef76e03031 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -22,6 +22,16 @@ module Ci
valid = true
+ if Feature.enabled?('ci_job_request_with_tags_matcher')
+ # pick builds that does not have other tags than runner's one
+ builds = builds.matches_tag_ids(runner.tags.ids)
+
+ # pick builds that have at least one tag
+ unless runner.run_untagged?
+ builds = builds.with_any_tags
+ end
+ end
+
builds.find do |build|
next unless runner.can_pick?(build)
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index 1d407739b21..0471b0f17a2 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -2,9 +2,11 @@ module Clusters
class CreateService < BaseService
attr_reader :access_token
- def execute(access_token)
+ def execute(access_token = nil)
@access_token = access_token
+ raise ArgumentError.new('Instance does not support multiple clusters') unless can_create_cluster?
+
create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end
@@ -25,5 +27,9 @@ module Clusters
@cluster_params = params.merge(user: current_user, projects: [project])
end
+
+ def can_create_cluster?
+ project.clusters.empty?
+ end
end
end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 53f16a236d2..1db91c3c90c 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -1,17 +1,17 @@
require 'securerandom'
-# Compare 2 branches for one repo or between repositories
+# Compare 2 refs for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
- attr_reader :start_project, :start_branch_name
+ attr_reader :start_project, :start_ref_name
- def initialize(new_start_project, new_start_branch_name)
+ def initialize(new_start_project, new_start_ref_name)
@start_project = new_start_project
- @start_branch_name = new_start_branch_name
+ @start_ref_name = new_start_ref_name
end
- def execute(target_project, target_branch, straight: false)
- raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight)
+ def execute(target_project, target_ref, straight: false)
+ raw_compare = target_project.repository.compare_source_branch(target_ref, start_project.repository, start_ref_name, straight: straight)
Compare.new(raw_compare, target_project, straight: straight) if raw_compare
end
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index 92eaa5d5115..3da21bd8b8f 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -41,6 +41,14 @@ module Issuable
end
end
+ def create_wip_note(old_title)
+ return unless issuable.is_a?(MergeRequest)
+
+ if MergeRequest.work_in_progress?(old_title) != issuable.work_in_progress?
+ SystemNoteService.handle_merge_request_wip(issuable, issuable.project, current_user)
+ end
+ end
+
def create_labels_note(old_labels)
added_labels = issuable.labels - old_labels
removed_labels = old_labels - issuable.labels
@@ -49,7 +57,11 @@ module Issuable
end
def create_title_change_note(old_title)
- SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
+ create_wip_note(old_title)
+
+ if issuable.wipless_title_changed(old_title)
+ SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
+ end
end
def create_description_change_note
diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb
new file mode 100644
index 00000000000..0610b401213
--- /dev/null
+++ b/app/services/issuable/destroy_service.rb
@@ -0,0 +1,9 @@
+module Issuable
+ class DestroyService < IssuableBaseService
+ def execute(issuable)
+ if issuable.destroy
+ issuable.update_project_counter_caches
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 39a7299ff60..2c51ac13815 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -169,10 +169,7 @@ class IssuableBaseService < BaseService
change_todo(issuable)
toggle_award(issuable)
filter_params(issuable)
- old_labels = issuable.labels.to_a
- old_mentioned_users = issuable.mentioned_users.to_a
- old_assignees = issuable.assignees.to_a
- old_total_time_spent = issuable.total_time_spent
+ old_associations = associations_before_update(issuable)
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
@@ -193,18 +190,13 @@ class IssuableBaseService < BaseService
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
- Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels)
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_associations[:labels])
end
- handle_changes(
- issuable,
- old_labels: old_labels,
- old_mentioned_users: old_mentioned_users,
- old_assignees: old_assignees
- )
+ handle_changes(issuable, old_associations: old_associations)
new_assignees = issuable.assignees.to_a
- affected_assignees = (old_assignees + new_assignees) - (old_assignees & new_assignees)
+ affected_assignees = (old_associations[:assignees] + new_assignees) - (old_associations[:assignees] & new_assignees)
invalidate_cache_counts(issuable, users: affected_assignees.compact)
after_update(issuable)
@@ -212,9 +204,8 @@ class IssuableBaseService < BaseService
execute_hooks(
issuable,
'update',
- old_labels: old_labels,
- old_assignees: old_assignees,
- old_total_time_spent: old_total_time_spent)
+ old_associations: old_associations
+ )
issuable.update_project_counter_caches if update_project_counters
end
@@ -267,6 +258,18 @@ class IssuableBaseService < BaseService
end
end
+ def associations_before_update(issuable)
+ associations =
+ {
+ labels: issuable.labels.to_a,
+ mentioned_users: issuable.mentioned_users.to_a,
+ assignees: issuable.assignees.to_a
+ }
+ associations[:total_time_spent] = issuable.total_time_spent if issuable.respond_to?(:total_time_spent)
+
+ associations
+ end
+
def has_changes?(issuable, old_labels: [], old_assignees: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 0f711bcc3cf..9f6cfc0f6d3 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,7 +1,7 @@
module Issues
class BaseService < ::IssuableBaseService
- def hook_data(issue, action, old_labels: [], old_assignees: [], old_total_time_spent: nil)
- hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
+ def hook_data(issue, action, old_associations: {})
+ hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
hook_data[:object_attributes][:action] = action
hook_data
@@ -22,8 +22,8 @@ module Issues
issue, issue.project, current_user, old_assignees)
end
- def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: [], old_total_time_spent: nil)
- issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
+ def execute_hooks(issue, action = 'open', old_associations: {})
+ issue_data = hook_data(issue, action, old_associations: old_associations)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
issue.project.execute_hooks(issue_data, hooks_scope)
issue.project.execute_services(issue_data, hooks_scope)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 1b7b5927c5a..d7aa7e2347e 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -14,9 +14,10 @@ module Issues
end
def handle_changes(issue, options)
- old_labels = options[:old_labels] || []
- old_mentioned_users = options[:old_mentioned_users] || []
- old_assignees = options[:old_assignees] || []
+ old_associations = options.fetch(:old_associations, {})
+ old_labels = old_associations.fetch(:labels, [])
+ old_mentioned_users = old_associations.fetch(:mentioned_users, [])
+ old_assignees = old_associations.fetch(:assignees, [])
if has_changes?(issue, old_labels: old_labels, old_assignees: old_assignees)
todo_service.mark_pending_todos_as_done(issue, current_user)
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
index 43b539ded53..997d247be46 100644
--- a/app/services/labels/promote_service.rb
+++ b/app/services/labels/promote_service.rb
@@ -19,6 +19,7 @@ module Labels
# We skipped validations during creation. Let's run them now, after deleting conflicting labels
raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid?
+
new_label
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index d3938b065bc..6b32d65a74b 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -4,22 +4,8 @@ module MergeRequests
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, state, nil)
end
- def create_title_change_note(issuable, old_title)
- removed_wip = MergeRequest.work_in_progress?(old_title) && !issuable.work_in_progress?
- added_wip = !MergeRequest.work_in_progress?(old_title) && issuable.work_in_progress?
- changed_title = MergeRequest.wipless_title(old_title) != issuable.wipless_title
-
- if removed_wip
- SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
- elsif added_wip
- SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
- end
-
- super if changed_title
- end
-
- def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil)
- hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
+ def hook_data(merge_request, action, old_rev: nil, old_associations: {})
+ hook_data = merge_request.to_hook_data(current_user, old_associations: old_associations)
hook_data[:object_attributes][:action] = action
if old_rev && !Gitlab::Git.blank_ref?(old_rev)
hook_data[:object_attributes][:oldrev] = old_rev
@@ -28,9 +14,9 @@ module MergeRequests
hook_data
end
- def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil)
+ def execute_hooks(merge_request, action = 'open', old_rev: nil, old_associations: {})
if merge_request.project
- merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
+ merge_data = hook_data(merge_request, action, old_rev: old_rev, old_associations: old_associations)
merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
merge_request.project.execute_services(merge_data, :merge_request_hooks)
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index bc0e7ad4e39..9622a5c5462 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -1,6 +1,8 @@
module MergeRequests
class BuildService < MergeRequests::BaseService
def execute
+ @issue_iid = params.delete(:issue_iid)
+
self.merge_request = MergeRequest.new(params)
merge_request.compare_commits = []
merge_request.source_project = find_source_project
@@ -8,8 +10,12 @@ module MergeRequests
merge_request.target_branch = find_target_branch
merge_request.can_be_created = branches_valid?
- compare_branches if branches_present?
- assign_title_and_description if merge_request.can_be_created
+ # compare branches only if branches are valid, otherwise
+ # compare_branches may raise an error
+ if merge_request.can_be_created
+ compare_branches
+ assign_title_and_description
+ end
merge_request
end
@@ -18,7 +24,17 @@ module MergeRequests
attr_accessor :merge_request
- delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
+ delegate :target_branch,
+ :target_branch_ref,
+ :target_project,
+ :source_branch,
+ :source_branch_ref,
+ :source_project,
+ :compare_commits,
+ :wip_title,
+ :description,
+ :errors,
+ to: :merge_request
def find_source_project
return source_project if source_project.present? && can?(current_user, :read_project, source_project)
@@ -28,6 +44,7 @@ module MergeRequests
def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project)
+
project.default_merge_request_target
end
@@ -53,10 +70,10 @@ module MergeRequests
def compare_branches
compare = CompareService.new(
source_project,
- source_branch
+ source_branch_ref
).execute(
target_project,
- target_branch
+ target_branch_ref
)
if compare
@@ -105,37 +122,53 @@ module MergeRequests
# more than one commit in the MR
#
def assign_title_and_description
- if match = source_branch.match(/\A(\d+)-/)
- iid = match[1]
- end
+ assign_title_and_description_from_single_commit
+ assign_title_from_issue
- commits = compare_commits
- if commits && commits.count == 1
- commit = commits.first
- merge_request.title = commit.title
- merge_request.description ||= commit.description.try(:strip)
- elsif iid && issue = target_project.get_issue(iid, current_user)
- case issue
- when Issue
- merge_request.title = "Resolve \"#{issue.title}\""
- when ExternalIssue
- merge_request.title = "Resolve #{issue.title}"
- end
+ merge_request.title ||= source_branch.titleize.humanize
+ merge_request.title = wip_title if compare_commits.empty?
+
+ append_closes_description
+ end
+
+ def append_closes_description
+ return unless issue_iid
+
+ closes_issue = "Closes ##{issue_iid}"
+
+ if description.present?
+ merge_request.description += closes_issue.prepend("\n\n")
else
- merge_request.title = source_branch.titleize.humanize
+ merge_request.description = closes_issue
end
+ end
+
+ def assign_title_and_description_from_single_commit
+ commits = compare_commits
+
+ return unless commits&.count == 1
+
+ commit = commits.first
+ merge_request.title ||= commit.title
+ merge_request.description ||= commit.description.try(:strip)
+ end
- if iid
- closes_issue = "Closes ##{iid}"
+ def assign_title_from_issue
+ return unless issue
- if description.present?
- merge_request.description += closes_issue.prepend("\n\n")
- else
- merge_request.description = closes_issue
+ merge_request.title =
+ case issue
+ when Issue then "Resolve \"#{issue.title}\""
+ when ExternalIssue then "Resolve #{issue.title}"
end
- end
+ end
+
+ def issue_iid
+ @issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1)
+ end
- merge_request.title = wip_title if commits.empty?
+ def issue
+ @issue ||= target_project.get_issue(issue_iid, current_user)
end
end
end
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index da39a380451..89dab1dd028 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -1,7 +1,18 @@
module MergeRequests
class CreateFromIssueService < MergeRequests::CreateService
+ def initialize(project, user, params)
+ # branch - the name of new branch
+ # ref - the source of new branch.
+
+ @branch_name = params[:branch_name]
+ @issue_iid = params[:issue_iid]
+ @ref = params[:ref]
+
+ super(project, user)
+ end
+
def execute
- return error('Invalid issue iid') unless issue_iid.present? && issue.present?
+ return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
params[:label_ids] = issue.label_ids if issue.label_ids.any?
@@ -21,20 +32,16 @@ module MergeRequests
private
- def issue_iid
- @isssue_iid ||= params.delete(:issue_iid)
- end
-
def issue
- @issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: issue_iid)
+ @issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: @issue_iid)
end
def branch_name
- @branch_name ||= issue.to_branch_name
+ @branch ||= @branch_name || issue.to_branch_name
end
def ref
- project.default_branch || 'master'
+ @ref || project.default_branch || 'master'
end
def merge_request
@@ -43,6 +50,7 @@ module MergeRequests
def merge_request_params
{
+ issue_iid: @issue_iid,
source_project_id: project.id,
source_branch: branch_name,
target_project_id: project.id,
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 820709583fa..49cf534dc0d 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -35,6 +35,12 @@ module MergeRequests
super
end
+ # expose issuable create method so it can be called from email
+ # handler CreateMergeRequestHandler
+ def create(merge_request)
+ super
+ end
+
private
def update_merge_requests_head_pipeline(merge_request)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 1da4dbd9e96..cedfcb50e09 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -10,6 +10,8 @@ module MergeRequests
attr_reader :merge_request, :source
+ delegate :merge_jid, :state, to: :@merge_request
+
def execute(merge_request)
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project, current_user, params).execute(merge_request)
@@ -27,6 +29,7 @@ module MergeRequests
success
end
end
+ log_info("Merge process finished on JID #{merge_jid} with state #{state}")
rescue MergeError => e
handle_merge_error(log_message: e.message, save_message_on_model: true)
end
@@ -49,7 +52,9 @@ module MergeRequests
def commit
message = params[:commit_message] || merge_request.merge_commit_message
+ log_info("Git merge started on JID #{merge_jid}")
commit_id = repository.merge(current_user, source, merge_request, message)
+ log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
raise MergeError, 'Conflicts detected during merge' unless commit_id
@@ -63,7 +68,9 @@ module MergeRequests
end
def after_merge
+ log_info("Post merge started on JID #{merge_jid} with state #{state}")
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
+ log_info("Post merge finished on JID #{merge_jid} with state #{state}")
if delete_source_branch?
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
@@ -92,6 +99,11 @@ module MergeRequests
@merge_request.update(merge_error: log_message) if save_message_on_model
end
+ def log_info(message)
+ @logger ||= Rails.logger
+ @logger.info("#{merge_request_info} - #{message}")
+ end
+
def merge_request_info
merge_request.to_reference(full: true)
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index fc100580c4f..bf3d4855122 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -35,7 +35,7 @@ module MergeRequests
# target branch manually
def close_merge_requests
commit_ids = @commits.map(&:id)
- merge_requests = @project.merge_requests.preload(:merge_request_diff).opened.where(target_branch: @branch_name).to_a
+ merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request|
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 1f394cacc64..c153872c874 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -22,8 +22,9 @@ module MergeRequests
end
def handle_changes(merge_request, options)
- old_labels = options[:old_labels] || []
- old_mentioned_users = options[:old_mentioned_users] || []
+ old_associations = options.fetch(:old_associations, {})
+ old_labels = old_associations.fetch(:labels, [])
+ old_mentioned_users = old_associations.fetch(:mentioned_users, [])
if has_changes?(merge_request, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb
index bd9cfd4e0ea..2187f26d1ed 100644
--- a/app/services/milestones/promote_service.rb
+++ b/app/services/milestones/promote_service.rb
@@ -6,14 +6,14 @@ module Milestones
check_project_milestone!(milestone)
Milestone.transaction do
- # Destroy all milestones with same title across projects
- destroy_old_milestones(milestone)
-
group_milestone = clone_project_milestone(milestone)
move_children_to_group_milestone(group_milestone)
- # Just to be safe
+ # Destroy all milestones with same title across projects
+ destroy_old_milestones(milestone)
+
+ # Rollback if milestone is not valid
unless group_milestone.valid?
raise_error(group_milestone.errors.full_messages.to_sentence)
end
@@ -35,7 +35,7 @@ module Milestones
end
def move_children_to_group_milestone(group_milestone)
- milestone_ids_for_merge(group_milestone).in_groups_of(100) do |milestone_ids|
+ milestone_ids_for_merge(group_milestone).in_groups_of(100, false) do |milestone_ids|
update_children(group_milestone, milestone_ids)
end
end
@@ -49,7 +49,12 @@ module Milestones
create_service = CreateService.new(group, current_user, params)
- create_service.execute
+ milestone = create_service.execute
+
+ # milestone won't be valid here because of duplicated title
+ milestone.save(validate: false)
+
+ milestone
end
def update_children(group_milestone, milestone_ids)
@@ -65,12 +70,12 @@ module Milestones
@group ||= parent.group || raise_error('Project does not belong to a group.')
end
- def destroy_old_milestones(group_milestone)
- Milestone.where(id: milestone_ids_for_merge(group_milestone)).destroy_all
+ def destroy_old_milestones(milestone)
+ Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all
end
def group_project_ids
- @group_project_ids ||= group.projects.map(&:id)
+ @group_project_ids ||= group.projects.pluck(:id)
end
def raise_error(message)
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index c9f07c140f7..3eb8cfcca9b 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -98,6 +98,12 @@ module NotificationRecipientService
self << [target.participants(user), :participating]
end
+ def add_mentions(user, target:)
+ return unless target.respond_to?(:mentioned_users)
+
+ self << [target.mentioned_users(user), :mention]
+ end
+
# Get project/group users with CUSTOM notification level
def add_custom_notifications
user_ids = []
@@ -227,6 +233,11 @@ module NotificationRecipientService
add_subscribed_users
if [:new_issue, :new_merge_request].include?(custom_action)
+ # These will all be participants as well, but adding with the :mention
+ # type ensures that users with the mention notification level will
+ # receive them, too.
+ add_mentions(current_user, target: target)
+
add_labels_subscribers
end
end
@@ -263,7 +274,7 @@ module NotificationRecipientService
def build!
# Add all users participating in the thread (author, assignee, comment authors)
add_participants(note.author)
- self << [note.mentioned_users, :mention]
+ add_mentions(note.author, target: note)
unless note.for_personal_snippet?
# Merge project watchers
diff --git a/app/services/projects/batch_count_service.rb b/app/services/projects/batch_count_service.rb
new file mode 100644
index 00000000000..178ebc5a143
--- /dev/null
+++ b/app/services/projects/batch_count_service.rb
@@ -0,0 +1,31 @@
+# Service class for getting and caching the number of elements of several projects
+# Warning: do not user this service with a really large set of projects
+# because the service use maps to retrieve the project ids.
+module Projects
+ class BatchCountService
+ def initialize(projects)
+ @projects = projects
+ end
+
+ def refresh_cache
+ @projects.each do |project|
+ service = count_service.new(project)
+ unless service.count_stored?
+ service.refresh_cache { global_count[project.id].to_i }
+ end
+ end
+ end
+
+ def project_ids
+ @projects.map(&:id)
+ end
+
+ def global_count(project)
+ raise NotImplementedError, 'global_count must be implemented and return an hash indexed by the project id'
+ end
+
+ def count_service
+ raise NotImplementedError, 'count_service must be implemented and return a Projects::CountService object'
+ end
+ end
+end
diff --git a/app/services/projects/batch_forks_count_service.rb b/app/services/projects/batch_forks_count_service.rb
new file mode 100644
index 00000000000..e61fe6c86b2
--- /dev/null
+++ b/app/services/projects/batch_forks_count_service.rb
@@ -0,0 +1,18 @@
+# Service class for getting and caching the number of forks of several projects
+# Warning: do not user this service with a really large set of projects
+# because the service use maps to retrieve the project ids
+module Projects
+ class BatchForksCountService < Projects::BatchCountService
+ def global_count
+ @global_count ||= begin
+ count_service.query(project_ids)
+ .group(:forked_from_project_id)
+ .count
+ end
+ end
+
+ def count_service
+ ::Projects::ForksCountService
+ end
+ end
+end
diff --git a/app/services/projects/batch_open_issues_count_service.rb b/app/services/projects/batch_open_issues_count_service.rb
new file mode 100644
index 00000000000..3b0ade2419b
--- /dev/null
+++ b/app/services/projects/batch_open_issues_count_service.rb
@@ -0,0 +1,16 @@
+# Service class for getting and caching the number of issues of several projects
+# Warning: do not user this service with a really large set of projects
+# because the service use maps to retrieve the project ids
+module Projects
+ class BatchOpenIssuesCountService < Projects::BatchCountService
+ def global_count
+ @global_count ||= begin
+ count_service.query(project_ids).group(:project_id).count
+ end
+ end
+
+ def count_service
+ ::Projects::OpenIssuesCountService
+ end
+ end
+end
diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb
index aa034315280..933829b557b 100644
--- a/app/services/projects/count_service.rb
+++ b/app/services/projects/count_service.rb
@@ -1,7 +1,7 @@
module Projects
# Base class for the various service classes that count project data (e.g.
# issues or forks).
- class CountService
+ class CountService < BaseCountService
# The version of the cache format. This should be bumped whenever the
# underlying logic changes. This removes the need for explicitly flushing
# all caches.
@@ -12,26 +12,7 @@ module Projects
end
def relation_for_count
- raise(
- NotImplementedError,
- '"relation_for_count" must be implemented and return an ActiveRecord::Relation'
- )
- end
-
- def count
- Rails.cache.fetch(cache_key) { uncached_count }
- end
-
- def refresh_cache
- Rails.cache.write(cache_key, uncached_count)
- end
-
- def uncached_count
- relation_for_count.count
- end
-
- def delete_cache
- Rails.cache.delete(cache_key)
+ self.class.query(@project.id)
end
def cache_key_name
@@ -44,5 +25,12 @@ module Projects
def cache_key
['projects', 'count_service', VERSION, @project.id, cache_key_name]
end
+
+ def self.query(project_ids)
+ raise(
+ NotImplementedError,
+ '"query" must be implemented and return an ActiveRecord::Relation'
+ )
+ end
end
end
diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb
index 3a0fa84b868..dc6eb19affd 100644
--- a/app/services/projects/forks_count_service.rb
+++ b/app/services/projects/forks_count_service.rb
@@ -1,12 +1,15 @@
module Projects
# Service class for getting and caching the number of forks of a project.
- class ForksCountService < CountService
- def relation_for_count
- @project.forks
- end
-
+ class ForksCountService < Projects::CountService
def cache_key_name
'forks_count'
end
+
+ def self.query(project_ids)
+ # We can't directly change ForkedProjectLink to ForkNetworkMember here
+ # Nowadays, when a call using v3 to projects/:id/fork is made,
+ # the relationship to ForkNetworkMember is not updated
+ ForkedProjectLink.where(forked_from_project: project_ids)
+ end
end
end
diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb
index fbf31214c28..e3a20b4c1e4 100644
--- a/app/services/projects/group_links/destroy_service.rb
+++ b/app/services/projects/group_links/destroy_service.rb
@@ -3,6 +3,7 @@ module Projects
class DestroyService < BaseService
def execute(group_link)
return false unless group_link
+
group_link.destroy
end
end
diff --git a/app/services/projects/hashed_storage/migrate_attachments_service.rb b/app/services/projects/hashed_storage/migrate_attachments_service.rb
new file mode 100644
index 00000000000..f8aaec8a9c0
--- /dev/null
+++ b/app/services/projects/hashed_storage/migrate_attachments_service.rb
@@ -0,0 +1,54 @@
+module Projects
+ module HashedStorage
+ AttachmentMigrationError = Class.new(StandardError)
+
+ class MigrateAttachmentsService < BaseService
+ attr_reader :logger, :old_path, :new_path
+
+ def initialize(project, logger = nil)
+ @project = project
+ @logger = logger || Rails.logger
+ end
+
+ def execute
+ @old_path = project.full_path
+ @new_path = project.disk_path
+
+ origin = FileUploader.dynamic_path_segment(project)
+ project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:attachments]
+ target = FileUploader.dynamic_path_segment(project)
+
+ result = move_folder!(origin, target)
+ project.save!
+
+ if result && block_given?
+ yield
+ end
+
+ result
+ end
+
+ private
+
+ def move_folder!(old_path, new_path)
+ unless File.directory?(old_path)
+ logger.info("Skipped attachments migration from '#{old_path}' to '#{new_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})")
+ return
+ end
+
+ if File.exist?(new_path)
+ logger.error("Cannot migrate attachments from '#{old_path}' to '#{new_path}', target path already exist (PROJECT_ID=#{project.id})")
+ raise AttachmentMigrationError, "Target path '#{new_path}' already exist"
+ end
+
+ # Create hashed storage base path folder
+ FileUtils.mkdir_p(File.dirname(new_path))
+
+ FileUtils.mv(old_path, new_path)
+ logger.info("Migrated project attachments from '#{old_path}' to '#{new_path}' (PROJECT_ID=#{project.id})")
+
+ true
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
new file mode 100644
index 00000000000..7212e7524ab
--- /dev/null
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -0,0 +1,70 @@
+module Projects
+ module HashedStorage
+ class MigrateRepositoryService < BaseService
+ include Gitlab::ShellAdapter
+
+ attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger
+
+ def initialize(project, logger = nil)
+ @project = project
+ @logger = logger || Rails.logger
+ end
+
+ def execute
+ @old_disk_path = project.disk_path
+ has_wiki = project.wiki.repository_exists?
+
+ @old_storage_version = project.storage_version
+ project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
+ project.ensure_storage_path_exists
+
+ @new_disk_path = project.disk_path
+
+ result = move_repository(@old_disk_path, @new_disk_path)
+
+ if has_wiki
+ @old_wiki_disk_path = "#{@old_disk_path}.wiki"
+ result &&= move_repository("#{@old_wiki_disk_path}", "#{@new_disk_path}.wiki")
+ end
+
+ unless result
+ rollback_folder_move
+ project.storage_version = nil
+ end
+
+ project.repository_read_only = false
+ project.save!
+
+ if result && block_given?
+ yield
+ end
+
+ result
+ end
+
+ private
+
+ def move_repository(from_name, to_name)
+ from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
+ to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
+
+ # If we don't find the repository on either original or target we should log that as it could be an issue if the
+ # project was not originally empty.
+ if !from_exists && !to_exists
+ logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
+ return false
+ elsif !from_exists
+ # Repository have been moved already.
+ return true
+ end
+
+ gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ end
+
+ def rollback_folder_move
+ move_repository(@new_disk_path, @old_disk_path)
+ move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki")
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb
index f5945f3b87f..662702c1db5 100644
--- a/app/services/projects/hashed_storage_migration_service.rb
+++ b/app/services/projects/hashed_storage_migration_service.rb
@@ -1,68 +1,22 @@
module Projects
class HashedStorageMigrationService < BaseService
- include Gitlab::ShellAdapter
-
- attr_reader :old_disk_path, :new_disk_path
+ attr_reader :logger
def initialize(project, logger = nil)
@project = project
- @logger ||= Rails.logger
+ @logger = logger || Rails.logger
end
def execute
- return if project.hashed_storage?(:repository)
-
- @old_disk_path = project.disk_path
- has_wiki = project.wiki.repository_exists?
-
- project.storage_version = Storage::HashedProject::STORAGE_VERSION
- project.ensure_storage_path_exists
-
- @new_disk_path = project.disk_path
-
- result = move_repository(@old_disk_path, @new_disk_path)
-
- if has_wiki
- result &&= move_repository("#{@old_disk_path}.wiki", "#{@new_disk_path}.wiki")
- end
-
- unless result
- rollback_folder_move
- return
+ # Migrate repository from Legacy to Hashed Storage
+ unless project.hashed_storage?(:repository)
+ return unless HashedStorage::MigrateRepositoryService.new(project, logger).execute
end
- project.repository_read_only = false
- project.save!
-
- block_given? ? yield : result
- end
-
- private
-
- def move_repository(from_name, to_name)
- from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
- to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
-
- # If we don't find the repository on either original or target we should log that as it could be an issue if the
- # project was not originally empty.
- if !from_exists && !to_exists
- logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
- return false
- elsif !from_exists
- # Repository have been moved already.
- return true
+ # Migrate attachments from Legacy to Hashed Storage
+ unless project.hashed_storage?(:attachments)
+ HashedStorage::MigrateAttachmentsService.new(project, logger).execute
end
-
- gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
- end
-
- def rollback_folder_move
- move_repository(@new_disk_path, @old_disk_path)
- move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki")
- end
-
- def logger
- @logger
end
end
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index c3b11341b4d..f2d676af5c3 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -51,10 +51,13 @@ module Projects
def import_repository
begin
- if project.gitea_import?
- fetch_repository
+ refmap = importer_class.try(:refmap) if has_importer?
+
+ if refmap
+ project.ensure_repository
+ project.repository.fetch_as_mirror(project.import_url, refmap: refmap)
else
- clone_repository
+ gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url)
end
rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
# Expire cache to prevent scenarios such as:
@@ -66,17 +69,6 @@ module Projects
end
end
- def clone_repository
- gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url)
- end
-
- def fetch_repository
- project.ensure_repository
- project.repository.add_remote(project.import_type, project.import_url)
- project.repository.set_remote_as_mirror(project.import_type)
- project.repository.fetch_remote(project.import_type, forced: true)
- end
-
def import_data
return unless has_importer?
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index 3c0d186a73c..a975a06a05c 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -1,15 +1,15 @@
module Projects
# Service class for counting and caching the number of open issues of a
# project.
- class OpenIssuesCountService < CountService
- def relation_for_count
- # We don't include confidential issues in this number since this would
- # expose the number of confidential issues to non project members.
- @project.issues.opened.public_only
- end
-
+ class OpenIssuesCountService < Projects::CountService
def cache_key_name
'open_issues_count'
end
+
+ def self.query(project_ids)
+ # We don't include confidential issues in this number since this would
+ # expose the number of confidential issues to non project members.
+ Issue.opened.public_only.where(project: project_ids)
+ end
end
end
diff --git a/app/services/projects/open_merge_requests_count_service.rb b/app/services/projects/open_merge_requests_count_service.rb
index 2a90f78b90d..77e6448fd5e 100644
--- a/app/services/projects/open_merge_requests_count_service.rb
+++ b/app/services/projects/open_merge_requests_count_service.rb
@@ -1,7 +1,7 @@
module Projects
# Service class for counting and caching the number of open merge requests of
# a project.
- class OpenMergeRequestsCountService < CountService
+ class OpenMergeRequestsCountService < Projects::CountService
def relation_for_count
@project.merge_requests.opened
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 5957f612e84..e5cd6fcdfe3 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -60,21 +60,14 @@ module Projects
# Notifications
project.send_move_instructions(@old_path)
- # Move main repository
- # TODO: check storage type and NOOP when not using Legacy
- unless move_repo_folder(@old_path, @new_path)
- raise TransferError.new('Cannot move project')
- end
-
- # Move wiki repo also if present
- # TODO: check storage type and NOOP when not using Legacy
- move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
+ # Directories on disk
+ move_project_folders(project)
# Move missing group labels to project
Labels::TransferService.new(current_user, @old_group, project).execute
# Move uploads
- Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
+ move_project_uploads(project)
# Move pages
Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
@@ -131,5 +124,30 @@ module Projects
def execute_system_hooks
SystemHooksService.new.execute_hooks_for(project, :transfer)
end
+
+ def move_project_folders(project)
+ return if project.hashed_storage?(:repository)
+
+ # Move main repository
+ unless move_repo_folder(@old_path, @new_path)
+ raise TransferError.new("Cannot move project")
+ end
+
+ # Disk path is changed; we need to ensure we reload it
+ project.reload_repository!
+
+ # Move wiki repo also if present
+ move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
+ end
+
+ def move_project_uploads(project)
+ return if project.hashed_storage?(:attachments)
+
+ Gitlab::UploadsTransfer.new.move_project(
+ project.path,
+ @old_namespace.full_path,
+ @new_namespace.full_path
+ )
+ end
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index d34903c9989..a773222bf17 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -18,7 +18,7 @@ module Projects
@status.enqueue!
@status.run!
- raise 'missing pages artifacts' unless build.artifacts_file?
+ raise 'missing pages artifacts' unless build.artifacts?
raise 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 13e292a18bf..72eecc61c96 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -15,7 +15,7 @@ module Projects
return error("Could not set the default branch") unless project.change_head(params[:default_branch])
end
- if project.update_attributes(params.except(:default_branch))
+ if project.update_attributes(update_params)
if project.previous_changes.include?('path')
project.rename_repo
else
@@ -31,8 +31,16 @@ module Projects
end
end
+ def run_auto_devops_pipeline?
+ params.dig(:run_auto_devops_pipeline_explicit) == 'true' || params.dig(:run_auto_devops_pipeline_implicit) == 'true'
+ end
+
private
+ def update_params
+ params.except(:default_branch, :run_auto_devops_pipeline_explicit, :run_auto_devops_pipeline_implicit)
+ end
+
def renaming_project_with_container_registry_tags?
new_path = params[:path]
diff --git a/app/services/protected_branches/api_create_service.rb b/app/services/protected_branches/legacy_api_create_service.rb
index f2040dfa03a..e358fd0374e 100644
--- a/app/services/protected_branches/api_create_service.rb
+++ b/app/services/protected_branches/legacy_api_create_service.rb
@@ -1,9 +1,9 @@
-# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
+# The branches#protect API still uses the `developers_can_push` and `developers_can_merge`
# flags for backward compatibility, and so performs translation between that format and the
# internal data model (separate access levels). The translation code is non-trivial, and so
# lives in this service.
module ProtectedBranches
- class ApiCreateService < BaseService
+ class LegacyApiCreateService < BaseService
def execute
push_access_level =
if params.delete(:developers_can_push)
diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb
index bdb0e0cc8bf..33176253ca2 100644
--- a/app/services/protected_branches/api_update_service.rb
+++ b/app/services/protected_branches/legacy_api_update_service.rb
@@ -1,9 +1,9 @@
-# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
+# The branches#protect API still uses the `developers_can_push` and `developers_can_merge`
# flags for backward compatibility, and so performs translation between that format and the
# internal data model (separate access levels). The translation code is non-trivial, and so
# lives in this service.
module ProtectedBranches
- class ApiUpdateService < BaseService
+ class LegacyApiUpdateService < BaseService
def execute(protected_branch)
@developers_can_push = params.delete(:developers_can_push)
@developers_can_merge = params.delete(:developers_can_merge)
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 911cc919bb8..690918b4a00 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -1,6 +1,10 @@
class SystemHooksService
def execute_hooks_for(model, event)
- execute_hooks(build_event_data(model, event))
+ data = build_event_data(model, event)
+
+ model.run_after_commit_or_now do
+ SystemHooksService.new.execute_hooks(data)
+ end
end
def execute_hooks(data, hooks_scope = :all)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e946218824c..30a5aab13bf 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -241,14 +241,10 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
- def remove_merge_request_wip(noteable, project, author)
- body = 'unmarked as a **Work In Progress**'
+ def handle_merge_request_wip(noteable, project, author)
+ prefix = noteable.work_in_progress? ? "marked" : "unmarked"
- create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
- end
-
- def add_merge_request_wip(noteable, project, author)
- body = 'marked as a **Work In Progress**'
+ body = "#{prefix} as a **Work In Progress**"
create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
@@ -583,6 +579,10 @@ module SystemNoteService
create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
end
+ def cross_reference?(note_text)
+ note_text =~ /\A#{cross_reference_note_prefix}/i
+ end
+
private
def notes_for_mentioner(mentioner, noteable, notes)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index e694c5761da..575853fd66b 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -208,6 +208,7 @@ class TodoService
def create_todos(users, attributes)
Array(users).map do |user|
next if pending_todos(user, attributes).exists?
+
todo = Todo.create(attributes.merge(user_id: user.id))
user.update_todos_count_cache
todo
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 6f05500adea..61f1568f366 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -34,7 +34,7 @@ module Users
private
def can_create_user?
- (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin?
+ (current_user.nil? && current_application_settings.allow_signup?) || current_user&.admin?
end
# Allowed params for creating a user (admins only)
diff --git a/app/services/users/keys_count_service.rb b/app/services/users/keys_count_service.rb
new file mode 100644
index 00000000000..f82d27eded9
--- /dev/null
+++ b/app/services/users/keys_count_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Users
+ # Service class for getting the number of SSH keys that belong to a user.
+ class KeysCountService < BaseCountService
+ attr_reader :user
+
+ # user - The User for which to get the number of SSH keys.
+ def initialize(user)
+ @user = user
+ end
+
+ def relation_for_count
+ user.keys
+ end
+
+ def raw?
+ # Since we're storing simple integers we don't need all of the additional
+ # Marshal data Rails includes by default.
+ true
+ end
+
+ def cache_key
+ "users/key-count-service/#{user.id}"
+ end
+ end
+end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index cd99e0b90f9..6ebc7c89500 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -63,7 +63,7 @@ class WebHookService
end
def async_execute
- Sidekiq::Client.enqueue(WebHookWorker, hook.id, data, hook_name)
+ WebHookWorker.perform_async(hook.id, data, hook_name)
end
private
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
deleted file mode 100644
index 14addb6cf14..00000000000
--- a/app/uploaders/artifact_uploader.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-class ArtifactUploader < GitlabUploader
- storage :file
-
- attr_reader :job, :field
-
- def self.local_artifacts_store
- Gitlab.config.artifacts.path
- end
-
- def self.artifacts_upload_path
- File.join(self.local_artifacts_store, 'tmp/uploads/')
- end
-
- def initialize(job, field)
- @job, @field = job, field
- end
-
- def store_dir
- default_local_path
- end
-
- def cache_dir
- File.join(self.class.local_artifacts_store, 'tmp/cache')
- end
-
- def work_dir
- File.join(self.class.local_artifacts_store, 'tmp/work')
- end
-
- private
-
- def default_local_path
- File.join(self.class.local_artifacts_store, default_path)
- end
-
- def default_path
- File.join(job.created_at.utc.strftime('%Y_%m'), job.project_id.to_s, job.id.to_s)
- end
-end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index d4ba3a028be..71658df5b41 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -26,11 +26,22 @@ class FileUploader < GitlabUploader
# This is used to build Upload paths dynamically based on the model's current
# namespace and path, allowing us to ignore renames or transfers.
#
- # model - Object that responds to `path_with_namespace`
+ # model - Object that responds to `full_path` and `disk_path`
#
# Returns a String without a trailing slash
- def self.dynamic_path_segment(model)
- File.join(CarrierWave.root, base_dir, model.disk_path)
+ def self.dynamic_path_segment(project)
+ if project.hashed_storage?(:attachments)
+ dynamic_path_builder(project.disk_path)
+ else
+ dynamic_path_builder(project.full_path)
+ end
+ end
+
+ # Auxiliary method to build dynamic path segment when not using a project model
+ #
+ # Prefer to use the `.dynamic_path_segment` as it includes Hashed Storage specific logic
+ def self.dynamic_path_builder(path)
+ File.join(CarrierWave.root, base_dir, path)
end
attr_accessor :model
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
new file mode 100644
index 00000000000..15dfb5a5763
--- /dev/null
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -0,0 +1,46 @@
+class JobArtifactUploader < GitlabUploader
+ storage :file
+
+ def self.local_store_path
+ Gitlab.config.artifacts.path
+ end
+
+ def self.artifacts_upload_path
+ File.join(self.local_store_path, 'tmp/uploads/')
+ end
+
+ def size
+ return super if model.size.nil?
+
+ model.size
+ end
+
+ def store_dir
+ default_local_path
+ end
+
+ def cache_dir
+ File.join(self.class.local_store_path, 'tmp/cache')
+ end
+
+ def work_dir
+ File.join(self.class.local_store_path, 'tmp/work')
+ end
+
+ private
+
+ def default_local_path
+ File.join(self.class.local_store_path, default_path)
+ end
+
+ def default_path
+ creation_date = model.created_at.utc.strftime('%Y_%m_%d')
+
+ File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
+ creation_date, model.job_id.to_s, model.id.to_s)
+ end
+
+ def disk_hash
+ @disk_hash ||= Digest::SHA2.hexdigest(model.project_id.to_s)
+ end
+end
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
new file mode 100644
index 00000000000..4f7f8a63108
--- /dev/null
+++ b/app/uploaders/legacy_artifact_uploader.rb
@@ -0,0 +1,33 @@
+class LegacyArtifactUploader < GitlabUploader
+ storage :file
+
+ def self.local_store_path
+ Gitlab.config.artifacts.path
+ end
+
+ def self.artifacts_upload_path
+ File.join(self.local_store_path, 'tmp/uploads/')
+ end
+
+ def store_dir
+ default_local_path
+ end
+
+ def cache_dir
+ File.join(self.class.local_store_path, 'tmp/cache')
+ end
+
+ def work_dir
+ File.join(self.class.local_store_path, 'tmp/work')
+ end
+
+ private
+
+ def default_local_path
+ File.join(self.class.local_store_path, default_path)
+ end
+
+ def default_path
+ File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s)
+ end
+end
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
index 098b16017d2..8c7bb750339 100644
--- a/app/validators/certificate_key_validator.rb
+++ b/app/validators/certificate_key_validator.rb
@@ -17,6 +17,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator
def valid_private_key_pem?(value)
return false unless value
+
pkey = OpenSSL::PKey::RSA.new(value)
pkey.private?
rescue OpenSSL::PKey::PKeyError
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
index e3d18097f71..5239e70a326 100644
--- a/app/validators/certificate_validator.rb
+++ b/app/validators/certificate_validator.rb
@@ -17,6 +17,7 @@ class CertificateValidator < ActiveModel::EachValidator
def valid_certificate_pem?(value)
return false unless value
+
OpenSSL::X509::Certificate.new(value).present?
rescue OpenSSL::X509::CertificateError
false
diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb
index 13ec342f399..e7d32550176 100644
--- a/app/validators/cluster_name_validator.rb
+++ b/app/validators/cluster_name_validator.rb
@@ -3,11 +3,7 @@
# Custom validator for ClusterName.
class ClusterNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- if record.user?
- unless value.present?
- record.errors.add(attribute, " has to be present")
- end
- elsif record.gcp?
+ if record.managed?
if record.persisted? && record.name_changed?
record.errors.add(attribute, " can not be changed because it's synchronized with provider")
end
@@ -19,6 +15,10 @@ class ClusterNameValidator < ActiveModel::EachValidator
unless value =~ Gitlab::Regex.kubernetes_namespace_regex
record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message)
end
+ else
+ unless value.present?
+ record.errors.add(attribute, " has to be present")
+ end
end
end
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 935787d1a4a..15bda97c3b5 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -1,6 +1,23 @@
= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f|
= form_errors(@appearance)
+ %fieldset.app_logo
+ %legend
+ Navigation bar:
+ .form-group
+ = f.label :header_logo, 'Header logo', class: 'control-label'
+ .col-sm-10
+ - if @appearance.header_logo?
+ = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
+ %hr
+ = f.hidden_field :header_logo_cache
+ = f.file_field :header_logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
+
%fieldset.sign-in
%legend
Sign in/Sign up pages:
@@ -28,27 +45,22 @@
.hint
Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.
- %fieldset.app_logo
+ %fieldset
%legend
- Navigation bar:
+ New project pages:
.form-group
- = f.label :header_logo, 'Header logo', class: 'control-label'
+ = f.label :new_project_guidelines, class: 'control-label'
.col-sm-10
- - if @appearance.header_logo?
- = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- - if @appearance.persisted?
- %br
- = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
- %hr
- = f.hidden_field :header_logo_cache
- = f.file_field :header_logo, class: ""
+ = f.text_area :new_project_guidelines, class: "form-control", rows: 10
.hint
- Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
+ Guidelines parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
.form-actions
= f.submit 'Save', class: 'btn btn-save append-right-10'
- if @appearance.persisted?
- = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ Preview last save:
+ = link_to 'Sign-in page', preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ = link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- if @appearance.updated_at
%span.pull-right
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview_sign_in.html.haml
index 1af7dd5bb67..1af7dd5bb67 100644
--- a/app/views/admin/appearances/preview.html.haml
+++ b/app/views/admin/appearances/preview_sign_in.html.haml
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 3a4d5ce0b5c..a9d0503bc73 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -160,9 +160,22 @@
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
- = f.label :password_authentication_enabled do
- = f.check_box :password_authentication_enabled
- Sign-in enabled
+ = f.label :password_authentication_enabled_for_web do
+ = f.check_box :password_authentication_enabled_for_web
+ Password authentication enabled for web interface
+ .help-block
+ When disabled, an external authentication provider must be used.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :password_authentication_enabled_for_git do
+ = f.check_box :password_authentication_enabled_for_git
+ Password authentication enabled for Git over HTTP(S)
+ .help-block
+ When disabled, a Personal Access Token
+ - if Gitlab::LDAP::Config.enabled?
+ or LDAP password
+ must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
.form-group
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
@@ -719,6 +732,30 @@
Number of Git pushes after which 'git gc' is run.
%fieldset
+ %legend Gitaly Timeouts
+ .form-group
+ = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_default, class: 'form-control'
+ .help-block
+ Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
+ for git fetch/push operations or Sidekiq jobs.
+ .form-group
+ = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_fast, class: 'form-control'
+ .help-block
+ Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
+ If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
+ can help maintain the stability of the GitLab instance.
+ .form-group
+ = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_medium, class: 'form-control'
+ .help-block
+ Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
+
+ %fieldset
%legend Web terminal
.form-group
= f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
@@ -743,5 +780,56 @@
installations. Set to 0 to completely disable polling.
= link_to icon('question-circle'), help_page_path('administration/polling')
+ %fieldset
+ %legend User and IP Rate Limits
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_unauthenticated_enabled do
+ = f.check_box :throttle_unauthenticated_enabled
+ Enable unauthenticated request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_authenticated_api_enabled do
+ = f.check_box :throttle_authenticated_api_enabled
+ Enable authenticated API request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_authenticated_web_enabled do
+ = f.check_box :throttle_authenticated_web_enabled
+ Enable authenticated web request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 2f0143c7eff..a24516355bf 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -45,10 +45,10 @@
.well-segment.admin-well.admin-well-features
%h4 Features
- sign_up = "Sign up"
- %p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
+ %p{ "aria-label" => "#{sign_up}: status " + (allow_signup? ? "on" : "off") }
= sign_up
%span.light.pull-right
- = boolean_to_icon signup_enabled?
+ = boolean_to_icon allow_signup?
- ldap = "LDAP"
%p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
= ldap
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index df2bf27be9d..6d8fad0eb8d 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -99,7 +99,7 @@
%td.build-link
- if project
- = link_to ci_status_path(build.pipeline) do
+ = link_to pipeline_path(build.pipeline) do
%strong= build.pipeline.short_sha
%td.timestamp
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index eb0e6701627..35dafb3e980 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -1,7 +1,7 @@
= render 'devise/shared/tab_single', tab_title:'Change your password'
.login-box
.login-body
- = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'gl-show-field-errors' }) do |f|
+ = form_for(resource, as: resource_name, url: password_path(:user), html: { method: :put, class: 'gl-show-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
= f.hidden_field :reset_password_token
@@ -17,5 +17,5 @@
.clearfix.prepend-top-20
%p
%span.light Didn't receive a confirmation email?
- = link_to "Request a new one", new_confirmation_path(resource_name)
+ = link_to "Request a new one", new_confirmation_path(:user)
= render 'devise/shared/sign_in_link'
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 4095f30c369..41462f503cb 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -11,6 +11,6 @@
= f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me
.pull-right.forgot-password
- = link_to "Forgot your password?", new_password_path(resource_name)
+ = link_to "Forgot your password?", new_password_path(:user)
.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index dd61dcf2a7b..34d4293bd45 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -6,15 +6,15 @@
- else
= render 'devise/shared/tabs_normal'
.tab-content
- - if password_authentication_enabled? || ldap_enabled? || crowd_enabled?
+ - if password_authentication_enabled_for_web? || ldap_enabled? || crowd_enabled?
= render 'devise/shared/signin_box'
-# Signup only makes sense if you can also sign-in
- - if password_authentication_enabled? && signup_enabled?
+ - if allow_signup?
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- - if !password_authentication_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
+ - if !password_authentication_enabled_for_web? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
diff --git a/app/views/devise/shared/_links.erb b/app/views/devise/shared/_links.erb
index 49e99e25c1d..cb934434c28 100644
--- a/app/views/devise/shared/_links.erb
+++ b/app/views/devise/shared/_links.erb
@@ -1,19 +1,19 @@
<%- if controller_name != 'sessions' %>
- <%= link_to "Sign in", new_session_path(resource_name), class: "btn" %><br />
+ <%= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: "btn" %><br />
<% end -%>
-<%- if devise_mapping.registerable? && controller_name != 'registrations' && gitlab_config.signup_enabled %>
- <%= link_to "Sign up", new_registration_path(resource_name) %><br />
+<%- if devise_mapping.registerable? && controller_name != 'registrations' && allow_signup? %>
+ <%= link_to "Sign up", new_registration_path(:user) %><br />
<% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
-<%= link_to "Forgot your password?", new_password_path(resource_name), class: "btn" %><br />
+<%= link_to "Forgot your password?", new_password_path(:user), class: "btn" %><br />
<% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
- <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
+ <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(:user) %><br />
<% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
- <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
+ <%= link_to "Didn't receive unlock instructions?", new_unlock_path(:user) %><br />
<% end -%>
diff --git a/app/views/devise/shared/_sign_in_link.html.haml b/app/views/devise/shared/_sign_in_link.html.haml
index 289bf40f3de..77ef103cc47 100644
--- a/app/views/devise/shared/_sign_in_link.html.haml
+++ b/app/views/devise/shared/_sign_in_link.html.haml
@@ -1,4 +1,4 @@
%p
%span.light
Already have login and password?
- = link_to "Sign in", new_session_path(resource_name)
+ = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes')
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 3b06008febe..6087f4a0b37 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -7,12 +7,12 @@
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
.login-body
= render 'devise/sessions/new_ldap', server: server
- - if password_authentication_enabled?
+ - if password_authentication_enabled_for_web?
.login-box.tab-pane{ id: 'ldap-standard', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
-- elsif password_authentication_enabled?
+- elsif password_authentication_enabled_for_web?
.login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 298604dee8c..2554b2688bb 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -31,4 +31,4 @@
%p
%span.light Didn't receive a confirmation email?
= succeed '.' do
- = link_to "Request a new one", new_confirmation_path(resource_name)
+ = link_to "Request a new one", new_confirmation_path(:user)
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 6d0243a325d..94f19ccd44c 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -5,9 +5,9 @@
- @ldap_servers.each_with_index do |server, i|
%li{ class: active_when(i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- - if password_authentication_enabled?
+ - if password_authentication_enabled_for_web?
%li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
- - if password_authentication_enabled? && signup_enabled?
+ - if allow_signup?
%li
= link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index 212856c0676..1ba6d390875 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,6 +1,6 @@
%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
%a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- - if password_authentication_enabled? && signup_enabled?
+ - if allow_signup?
%li{ role: 'presentation' }
%a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml
index 20b7fa471a0..a2a4c75daad 100644
--- a/app/views/errors/omniauth_error.html.haml
+++ b/app/views/errors/omniauth_error.html.haml
@@ -9,7 +9,7 @@
%p Try logging in using your username or email. If you have forgotten your password, try recovering it
= link_to "Sign in", new_session_path(:user), class: 'btn primary'
- = link_to "Recover password", new_password_path(resource_name), class: 'btn secondary'
+ = link_to "Recover password", new_password_path(:user), class: 'btn secondary'
%hr
%p.light If none of the options work, try contacting a GitLab administrator.
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index e2aec532a9d..38741fe6662 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -5,7 +5,12 @@ xml.entry do
xml.link href: event_feed_url(event)
xml.title truncate(event_feed_title(event), length: 80)
xml.updated event.updated_at.xmlschema
- xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
+
+ # We're deliberately re-using "event.author" here since this data is
+ # eager-loaded. This allows us to re-use the user object's Email address,
+ # instead of having to run additional queries to figure out what Email to use
+ # for the avatar.
+ xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author))
xml.author do
xml.username event.author_username
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index cb4fc69d5b8..f5f621507b8 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -4,6 +4,7 @@
= render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
+ = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestones, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index eca7fb9ddb1..d758e314d41 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title "Milestones"
- page_title "Milestones"
-- header_title group_title(@group, "Milestones", group_milestones_path(@group))
%h3.page-title
New Milestone
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index d0c2e0b1d69..021de4f0caf 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -29,7 +29,7 @@
.row.prepend-top-default
.col-md-8
- .documentation-index
+ .documentation-index.wiki
= markdown(@help_index)
.col-md-4
.panel.panel-default
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index baa8036de10..05ddd0ec733 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,10 +1,8 @@
.flash-container.flash-container-page
- - if alert
- .flash-alert
- %div{ class: (container_class) }
- %span= alert
-
- - elsif notice
- .flash-notice
- %div{ class: (container_class) }
- %span= notice
+ -# We currently only support `alert`, `notice`, `success`
+ - flash.each do |key, value|
+ -# Don't show a flash message if the message is nil
+ - if value
+ %div{ class: "flash-#{key}" }
+ %div{ class: (container_class) }
+ %span= value
diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml
index 983ed22a506..b50537438a9 100644
--- a/app/views/layouts/_mailer.html.haml
+++ b/app/views/layouts/_mailer.html.haml
@@ -10,6 +10,10 @@
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; }
+ .hidden {
+ display: none !important;
+ visibility: hidden !important;
+ }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 52fb46eb8c9..691d2528022 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -4,7 +4,8 @@
%body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
= render "layouts/header/empty"
- = render "layouts/broadcast"
+ .login-page-broadcast
+ = render "layouts/broadcast"
.container.navless-container
.content
= render "layouts/flash"
@@ -14,8 +15,8 @@
.col-sm-7.brand-holder.pull-left
%h1
= brand_title
- - if brand_item
= brand_image
+ - if brand_item&.description?
= brand_text
- else
%h3 Open source software to collaborate on code
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 1eca412aff9..e2407f6a428 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -7,7 +7,7 @@
= link_to root_path, title: 'Dashboard', id: 'logo' do
= brand_header_logo
%span.logo-text.hidden-xs
- = render 'shared/logo_type.svg'
+ = brand_header_logo_type
- if current_user
= render "layouts/nav/dashboard"
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 458b5010d36..7e23f9c1f05 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -73,7 +73,7 @@
= link_to profile_emails_path do
%strong.fly-out-top-item-name
#{ _('Emails') }
- - unless current_user.ldap_user?
+ - if current_user.allow_password_authentication?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path do
.nav-icon-container
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 66146e61263..53a9162b703 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -146,7 +146,7 @@
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters]) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp]) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
= sprite_icon('pipeline')
@@ -154,7 +154,7 @@
CI / CD
%ul.sidebar-sub-level-items
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
#{ _('CI / CD') }
@@ -183,18 +183,18 @@
%span
Environments
+ - if project_nav_tab? :clusters
+ = nav_link(controller: [:clusters, :user, :gcp]) do
+ = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
+ %span
+ Clusters
+
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
= link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
%span
Charts
- - if project_nav_tab? :clusters
- = nav_link(controller: :clusters) do
- = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
- %span
- Cluster
-
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
@@ -266,7 +266,7 @@
Pages
- else
- = nav_link(path: %w[members#show]) do
+ = nav_link(controller: :project_members) do
= link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('users')
diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml
index 6b9b42dcf37..00e1b5faae3 100644
--- a/app/views/notify/new_user_email.html.haml
+++ b/app/views/notify/new_user_email.html.haml
@@ -1,7 +1,7 @@
%p
Hi #{@user['name']}!
%p
- - if Gitlab.config.gitlab.signup_enabled
+ - if current_application_settings.allow_signup?
Your account has been created successfully.
- else
The Administrator created an account for you. Now you are a member of the company GitLab application.
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
new file mode 100644
index 00000000000..749e273b2e2
--- /dev/null
+++ b/app/views/projects/_issuable_by_email.html.haml
@@ -0,0 +1,30 @@
+- name = issuable_type == 'issue' ? 'issue' : 'merge request'
+
+.issuable-footer.text-center
+ %button.issuable-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issuable-email-modal" } }
+ Email a new #{name} to this project
+
+#issuable-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
+ .modal-dialog{ role: "document" }
+ .modal-content
+ .modal-header
+ %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
+ %span{ aria: { hidden: "true" } }= icon("times")
+ %h4.modal-title
+ Create new #{name} by email
+ .modal-body
+ %p
+ You can create a new #{name} inside this project by sending an email to the following email address:
+ .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')
+ %p
+ = render 'by_email_description'
+ %p
+ This is a private email address, generated just for you.
+
+ Anyone who gets ahold of it can create issues or merge requests as if they were you.
+ You should
+ = link_to 'reset it', new_issuable_address_project_path(@project, issuable_type: issuable_type), class: 'incoming-email-token-reset'
+ if that ever happens.
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index f8a2ea18989..2cd5d0c60ea 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -10,25 +10,23 @@
.md-area
.md-header
%ul.nav-links.clearfix
- %li.active
+ %li.md-header-tab.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write
- %li
+ %li.md-header-tab
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview
- %li.pull-right
- .toolbar-group
- = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
- = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
- = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
- = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
- = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
- = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
- = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
- .toolbar-group
- %button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
- = sprite_icon("screen-full")
+ %li.md-header-toolbar
+ = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
+ = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
+ = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
+ = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
+ = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
+ = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
+ = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
+ %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: "Go full screen", data: { container: "body" } }
+ = sprite_icon("screen-full")
.md-write-holder
= yield
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 44aa9eb3826..32901d30b96 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -19,5 +19,5 @@
distributed with computer software, forming part of its documentation.
%p
We recommend you to
- = link_to "add a README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link'
+ = link_to "add a README", add_special_file_path(@project, file_name: 'README.md')
file to the repository and GitLab will render it here instead of this message.
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
index 03be6f15313..1a9ce8d0508 100644
--- a/app/views/projects/artifacts/_tree_directory.html.haml
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -3,6 +3,6 @@
%tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name
= tree_icon('folder', '755', directory.name)
- = link_to path_to_directory do
- %span.str-truncated= directory.name
+ = link_to path_to_directory, class: 'str-truncated' do
+ %span= directory.name
%td
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
index a97ddb3c377..cfb91568061 100644
--- a/app/views/projects/artifacts/_tree_file.html.haml
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -6,12 +6,12 @@
%td.tree-item-file-name
= tree_icon('file', blob.mode, blob.name)
- if external_link
- = link_to path_to_file, class: 'tree-item-file-external-link js-artifact-tree-tooltip',
+ = link_to path_to_file, class: 'tree-item-file-external-link js-artifact-tree-tooltip str-truncated',
target: '_blank', rel: 'noopener noreferrer', title: _('Opens in a new window') do
- %span.str-truncated>= blob.name
+ %span>= blob.name
= icon('external-link', class: 'js-artifact-tree-external-icon')
- else
- = link_to path_to_file do
- %span.str-truncated= blob.name
+ = link_to path_to_file, class: 'str-truncated' do
+ %span= blob.name
%td
= number_to_human_size(blob.size, precision: 2)
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 0be15cc179f..281363d2e01 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -2,7 +2,7 @@
.js-file-title.file-title-flex-parent
= render 'projects/blob/header_content', blob: blob
- .file-actions.hidden-xs
+ .file-actions
= render 'projects/blob/viewer_switcher', blob: blob unless blame
.btn-group{ role: "group" }<
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 6e02ae6c9cc..573050e597d 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -8,8 +8,7 @@
%li{ class: "js-branch-#{branch.name}" }
%div
= link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated ref-name' do
- = icon('code-fork')
- = branch.name
+ = icon('code-fork', class: 'append-right-5') + "#{branch.name}"
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index aade310236e..fb770764364 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -38,7 +38,7 @@
- if @branches.any?
%ul.content-list.all-branches
- @branches.each do |branch|
- = render "projects/branches/branch", branch: branch, merged: @repository.merged_to_root_ref?(branch, @merged_branch_names)
+ = render "projects/branches/branch", branch: branch, merged: @merged_branch_names.include?(branch.name)
= paginate @branches, theme: 'gitlab'
- else
.nothing-here-block
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index c82ae35a685..0a54c736761 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,10 +1,10 @@
- if current_user
= link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project)
- = icon('star')
+ = sprite_icon('star')
%span.starred= _('Unstar')
- else
- = icon('star-o')
+ = sprite_icon('star-o')
%span= s_('StarProject|Star')
.count-with-arrow
%span.arrow
@@ -13,7 +13,7 @@
- else
= link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do
- = icon('star')
+ = sprite_icon('star')
#{ s_('StarProject|Star') }
.count-with-arrow
%span.arrow
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
index 97532f1e2bd..2b3095eb94b 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -1,10 +1,11 @@
- if can?(current_user, :admin_cluster, @cluster)
- .append-bottom-20
- %label.append-bottom-10
- = s_('ClusterIntegration|Google Container Engine')
- %p
- - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
+ - if @cluster.managed?
+ .append-bottom-20
+ %label.append-bottom-10
+ = s_('ClusterIntegration|Google Container Engine')
+ %p
+ - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
.well.form-group
%label.text-danger
diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml
new file mode 100644
index 00000000000..a1cc66eac92
--- /dev/null
+++ b/app/views/projects/clusters/_banner.html.haml
@@ -0,0 +1,21 @@
+%h4= s_('ClusterIntegration|Enable cluster integration')
+.settings-content
+
+ .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
+ = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
+ %p.js-error-reason
+
+ .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
+
+ .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine. Refresh the page to see cluster\'s details')
+
+ %p
+ - if @cluster.enabled?
+ - if can?(current_user, :update_cluster, @cluster)
+ = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is enabled for this project.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is disabled for this project.')
diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml
new file mode 100644
index 00000000000..18ca01d2d49
--- /dev/null
+++ b/app/views/projects/clusters/_cluster.html.haml
@@ -0,0 +1,22 @@
+.gl-responsive-table-row
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Cluster")
+ .table-mobile-content
+ = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern")
+ .table-mobile-content= cluster.environment_scope
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
+ .table-mobile-content= cluster.platform_kubernetes&.actual_namespace
+ .table-section.section-10
+ .table-mobile-header{ role: "rowheader" }
+ .table-mobile-content
+ %button{ type: "button",
+ class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
+ "aria-label": s_("ClusterIntegration|Toggle Cluster"),
+ disabled: !cluster.can_toggle_cluster?,
+ data: { "enabled-text": s_("ClusterIntegration|Active"),
+ "disabled-text": s_("ClusterIntegration|Inactive"),
+ endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
+ = icon("spinner spin", class: "loading-icon")
diff --git a/app/views/projects/clusters/_dropdown.html.haml b/app/views/projects/clusters/_dropdown.html.haml
new file mode 100644
index 00000000000..39188c7ca27
--- /dev/null
+++ b/app/views/projects/clusters/_dropdown.html.haml
@@ -0,0 +1,12 @@
+%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
+
+.dropdown.clusters-dropdown
+ %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
+ %span.dropdown-toggle-text
+ = dropdown_text
+ = icon('chevron-down')
+ %ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
+ %li
+ = link_to(s_('ClusterIntegration|Create cluster on Google Container Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
+ %li
+ = link_to(s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
new file mode 100644
index 00000000000..e629cc58b06
--- /dev/null
+++ b/app/views/projects/clusters/_empty_state.html.haml
@@ -0,0 +1,12 @@
+.row.empty-state
+ .col-xs-12
+ .svg-content= image_tag 'illustrations/clusters_empty.svg'
+ .col-xs-12.text-center
+ .text-content
+ %h4= s_('ClusterIntegration|Integrate cluster automation')
+ - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ %p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
+
+ %p
+ = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
+
diff --git a/app/views/projects/clusters/_enabled.html.haml b/app/views/projects/clusters/_enabled.html.haml
new file mode 100644
index 00000000000..70c677f7856
--- /dev/null
+++ b/app/views/projects/clusters/_enabled.html.haml
@@ -0,0 +1,15 @@
+= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group.append-bottom-20
+ %label.append-bottom-10
+ = field.hidden_field :enabled, { class: 'js-toggle-input'}
+
+ %button{ type: 'button',
+ class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
+ "aria-label": s_("ClusterIntegration|Toggle Cluster"),
+ disabled: !can?(current_user, :update_cluster, @cluster),
+ data: { "enabled-text": s_("ClusterIntegration|Active"), "disabled-text": s_("ClusterIntegration|Inactive"), } }
+
+ - if can?(current_user, :update_cluster, @cluster)
+ .form-group
+ = field.submit _('Save'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml
deleted file mode 100644
index 1f8ae463d0f..00000000000
--- a/app/views/projects/clusters/_form.html.haml
+++ /dev/null
@@ -1,35 +0,0 @@
-.row
- .col-sm-8.col-sm-offset-4
- %p
- - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
-
- = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field|
- = field.hidden_field :provider_type, value: :gcp
- = form_errors(@cluster)
- .form-group
- = field.label :name, s_('ClusterIntegration|Cluster name')
- = field.text_field :name, class: 'form-control'
-
- = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
- .form-group
- = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
- = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :gcp_project_id, class: 'form-control'
-
- .form-group
- = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
- = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
-
- .form-group
- = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
- = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
-
- .form-group
- = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
- = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-2'
-
- .form-group
- = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
diff --git a/app/views/projects/clusters/_tabs.html.haml b/app/views/projects/clusters/_tabs.html.haml
new file mode 100644
index 00000000000..c8120e806fa
--- /dev/null
+++ b/app/views/projects/clusters/_tabs.html.haml
@@ -0,0 +1,16 @@
+.top-area.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon("angle-left")
+ .fade-right= icon("angle-right")
+ %ul.nav-links.scrolling-tabs
+ %li{ class: ('active' if @scope == 'active') }>
+ = link_to project_clusters_path(@project, scope: :active), class: "js-active-tab" do
+ = s_("ClusterIntegration|Active")
+ %span.badge= @active_count
+ %li{ class: ('active' if @scope == 'inactive') }>
+ = link_to project_clusters_path(@project, scope: :inactive), class: "js-inactive-tab" do
+ = s_("ClusterIntegration|Inactive")
+ %span.badge= @inactive_count
+ %li{ class: ('active' if @scope.nil? || @scope == 'all') }>
+ = link_to project_clusters_path(@project), class: "js-all-tab" do
+ = s_("ClusterIntegration|All")
+ %span.badge= @all_count
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
new file mode 100644
index 00000000000..0f6bae97571
--- /dev/null
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -0,0 +1,32 @@
+%p
+ - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
+
+= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+
+ = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
+ .form-group
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :gcp_project_id, class: 'form-control', placeholder: s_('ClusterIntegration|Project ID')
+
+ .form-group
+ = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
+ = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
+
+ .form-group
+ = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
+ = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
+
+ .form-group
+ = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
+ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml
index beb798e7154..cddb53c2688 100644
--- a/app/views/projects/clusters/_header.html.haml
+++ b/app/views/projects/clusters/gcp/_header.html.haml
@@ -1,5 +1,5 @@
-%h4.prepend-top-0
- = s_('ClusterIntegration|Create new cluster on Google Container Engine')
+%h4.prepend-top-20
+ = s_('ClusterIntegration|Enter the details for your cluster')
%p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
new file mode 100644
index 00000000000..3fa9f69708a
--- /dev/null
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -0,0 +1,40 @@
+.form-group
+ %label.append-bottom-10{ for: 'cluster-name' }
+ = s_('ClusterIntegration|Cluster name')
+ .input-group
+ %input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
+ %span.input-group-btn
+ = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'), class: 'btn-default')
+
+= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = form_errors(@cluster)
+ = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
+ .form-group
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ .input-group
+ = platform_kubernetes_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: true
+ %span.input-group-btn
+ = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'btn-default')
+
+ .form-group
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ .input-group
+ = platform_kubernetes_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: true
+ %span.input-group-addon.clipboard-addon
+ = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'btn-blank')
+
+ .form-group
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ .input-group
+ = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: true
+ %span.input-group-btn
+ %button.btn.btn-default.js-show-cluster-token{ type: 'button' }
+ = s_('ClusterIntegration|Show')
+ = clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
+
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index fde030b500b..790ba61fd86 100644
--- a/app/views/projects/clusters/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -3,8 +3,9 @@
.row.prepend-top-default
.col-sm-4
- = render 'sidebar'
+ = render 'projects/clusters/sidebar'
.col-sm-8
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine')
= render 'header'
.row
.col-sm-8.col-sm-offset-4.signin-with-google
diff --git a/app/views/projects/clusters/gcp/new.html.haml b/app/views/projects/clusters/gcp/new.html.haml
new file mode 100644
index 00000000000..9a79480c82f
--- /dev/null
+++ b/app/views/projects/clusters/gcp/new.html.haml
@@ -0,0 +1,10 @@
+- breadcrumb_title "Cluster"
+- page_title _("New Cluster")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'projects/clusters/sidebar'
+ .col-sm-8
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine')
+ = render 'header'
+ = render 'form'
diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml
index 980a0d5f19a..bec512be91c 100644
--- a/app/views/projects/clusters/index.html.haml
+++ b/app/views/projects/clusters/index.html.haml
@@ -1 +1,22 @@
-Hello World!
+- breadcrumb_title "Clusters"
+- page_title "Clusters"
+
+.clusters-container
+ - if @clusters.empty?
+ = render "empty_state"
+ - else
+ .top-area.adjust
+ .nav-text
+ = s_("ClusterIntegration|Clusters can be used to deploy applications and to provide Review Apps for this project")
+ .ci-table.js-clusters-list
+ .gl-responsive-table-row.table-row-header{ role: "row" }
+ .table-section.section-30{ role: "rowheader" }
+ = s_("ClusterIntegration|Cluster")
+ .table-section.section-30{ role: "rowheader" }
+ = s_("ClusterIntegration|Environment pattern")
+ .table-section.section-30{ role: "rowheader" }
+ = s_("ClusterIntegration|Project namespace")
+ .table-section.section-10{ role: "rowheader" }
+ - @clusters.each do |cluster|
+ = render "cluster", cluster: cluster.present(current_user: current_user)
+ = paginate @clusters, theme: "gitlab"
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index 6b321f60212..2e5bc34f64a 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -5,16 +5,9 @@
.col-sm-4
= render 'sidebar'
.col-sm-8
- - if @project.kubernetes_service&.active?
- %h4.prepend-top-0= s_('ClusterIntegration|Cluster management')
+ %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
- %p= s_('ClusterIntegration|A cluster has been set up on this project through the Kubernetes integration page')
- = link_to s_('ClusterIntegration|Manage Kubernetes integration'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
-
- - else
- %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
-
- %p= s_('ClusterIntegration|Create a new cluster on Google Container Engine right from GitLab')
- = link_to s_('ClusterIntegration|Create on GKE'), providers_gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
- %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
- = link_to s_('ClusterIntegration|Add an existing cluster'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
+ %p= s_('ClusterIntegration|Create a new cluster on Google Engine right from GitLab')
+ = link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
+ = link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
diff --git a/app/views/projects/clusters/new_gcp.html.haml b/app/views/projects/clusters/new_gcp.html.haml
deleted file mode 100644
index 48e6b6ae8e8..00000000000
--- a/app/views/projects/clusters/new_gcp.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- breadcrumb_title "Cluster"
-- page_title _("New Cluster")
-
-.row.prepend-top-default
- .col-sm-4
- = render 'sidebar'
- .col-sm-8
- = render 'header'
-
-= render 'form'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index b7671f5e3c4..fe6dacf1f0d 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -1,5 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
-- breadcrumb_title "Cluster"
+- add_to_breadcrumbs "Clusters", project_clusters_path(@project)
+- breadcrumb_title @cluster.id
- page_title _("Cluster")
- expanded = Rails.env.test?
@@ -13,73 +14,32 @@
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } }
-
.js-cluster-application-notice
.flash-container
%section.settings.no-animate.expanded
- %h4= s_('ClusterIntegration|Enable cluster integration')
- .settings-content
-
- .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
- %p.js-error-reason
-
- .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
-
- .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine')
-
- %p
- - if @cluster.enabled?
- - if can?(current_user, :update_cluster, @cluster)
- = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
- - else
- = s_('ClusterIntegration|Cluster integration is enabled for this project.')
- - else
- = s_('ClusterIntegration|Cluster integration is disabled for this project.')
-
- = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
- = form_errors(@cluster)
- .form-group.append-bottom-20
- %label.append-bottom-10
- = field.hidden_field :enabled, { class: 'js-toggle-input'}
-
- %button{ type: 'button',
- class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}",
- 'aria-label': s_('ClusterIntegration|Toggle Cluster'),
- disabled: !can?(current_user, :update_cluster, @cluster),
- data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } }
-
- - if can?(current_user, :update_cluster, @cluster)
- .form-group
- = field.submit _('Save'), class: 'btn btn-success'
+ = render 'banner'
+ = render 'enabled'
.cluster-applications-table#js-cluster-applications
- %section.settings#js-cluster-details
+ %section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('ClusterIntegration|Cluster details')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your cluster')
-
.settings-content
-
- .form_group.append-bottom-20
- %label.append-bottom-10{ for: 'cluster-name' }
- = s_('ClusterIntegration|Cluster name')
- .input-group
- %input.form-control.cluster-name{ value: @cluster.name, disabled: true }
- %span.input-group-addon.clipboard-addon
- = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'))
+ - if @cluster.managed?
+ = render 'projects/clusters/gcp/show'
+ - else
+ = render 'projects/clusters/user/show'
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
- %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project')
+ %p= s_('ClusterIntegration|Manage cluster integration on your GitLab project')
.settings-content
= render 'advanced_settings'
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
new file mode 100644
index 00000000000..4a9bd5186c6
--- /dev/null
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -0,0 +1,25 @@
+= form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+
+ = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
+ .form-group
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
+
+ .form-group
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
+
+ .form-group
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ = platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
+
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Add cluster'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/projects/clusters/user/_header.html.haml
new file mode 100644
index 00000000000..06ac210a06d
--- /dev/null
+++ b/app/views/projects/clusters/user/_header.html.haml
@@ -0,0 +1,5 @@
+%h4.prepend-top-20
+ = s_('ClusterIntegration|Enter the details for your cluster')
+%p
+ - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters').html_safe % { link_to_help_page: link_to_help_page }
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
new file mode 100644
index 00000000000..5931e0b7f17
--- /dev/null
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -0,0 +1,29 @@
+= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+
+ = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
+ .form-group
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
+
+ .form-group
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
+
+ .form-group
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ .input-group
+ = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off'
+ %span.input-group-addon.clipboard-addon
+ %button.js-show-cluster-token.btn-blank{ type: 'button' }
+ = s_('ClusterIntegration|Show')
+
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/user/new.html.haml b/app/views/projects/clusters/user/new.html.haml
new file mode 100644
index 00000000000..68f38f83453
--- /dev/null
+++ b/app/views/projects/clusters/user/new.html.haml
@@ -0,0 +1,11 @@
+- breadcrumb_title "Cluster"
+- page_title _("New Cluster")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'projects/clusters/sidebar'
+ .col-sm-8
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Add an existing cluster')
+ = render 'header'
+ .prepend-top-20
+ = render 'form'
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 8b9c1bbb602..5f607c2ab25 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -67,8 +67,8 @@
- if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info
- .status-icon-container{ class: "ci-status-icon-#{last_pipeline.status}" }
- = link_to project_pipeline_path(@project, last_pipeline.id) do
+ .status-icon-container
+ = link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
= ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') }
= link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id)
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index a66177f20e9..1b91a94a9f8 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -41,6 +41,6 @@
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
- = link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent"
+ = link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent btn-link"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 2de2cf9e38c..dd473ebe580 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -22,9 +22,11 @@
- diff_files.each do |diff_file|
%li
%a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
- = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5")
- %span.diff-file-changes-path.append-right-5= diff_file.new_path
- .pull-right
+ = sprite_icon(diff_file_changed_icon(diff_file), size: 16, css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon append-right-8")
+ %span.diff-changed-file-content.append-right-8
+ %strong.diff-changed-file-name= diff_file.blob.name
+ %span.diff-changed-file-path.prepend-top-5= diff_file.new_path
+ %span.diff-changed-stats
%span.cgreen<
+#{diff_file.added_lines}
%span.cred<
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 5ebeae5c35f..71206f3a386 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -147,7 +147,7 @@
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location.
- - if @project.deployment_services.any?
+ - if @project.deployment_platform.present?
%li Your deployment services will be broken, you will need to manually fix the services after renaming.
= f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project)
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index af564b93dc3..58e89a481a9 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -14,12 +14,12 @@
%p
Otherwise you can start with adding a
= succeed ',' do
- = link_to "README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link'
+ = link_to "README", add_special_file_path(@project, file_name: 'README.md')
a
= succeed ',' do
- = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link'
+ = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE')
or a
- = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link'
+ = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore')
to this project.
%p
You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected.
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index 1bcc955ddc8..d9c9f0ed546 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -5,6 +5,8 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag("environments_folder")
-#environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
+#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,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"css-class" => container_class } }
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 2e85f608823..88f1348da47 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,15 +3,13 @@
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag("common_vue")
= page_specific_javascript_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,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
- "project-environments-path" => project_environments_path(@project),
- "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
"new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class } }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index d7859c9fbeb..add394a6356 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -19,14 +19,15 @@
.environments-container
- if @deployments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any deployments right now.
- %p.blank-state-text
- Define environments in the deploy stage(s) in
- %code .gitlab-ci.yml
- to track deployments here.
- = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+ .blank-state-row
+ .blank-state-center
+ %h2.blank-state-title
+ You don't have any deployments right now.
+ %p.blank-state-text
+ Define environments in the deploy stage(s) in
+ %code .gitlab-ci.yml
+ to track deployments here.
+ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml
new file mode 100644
index 00000000000..8a549d431ee
--- /dev/null
+++ b/app/views/projects/forks/_fork_button.html.haml
@@ -0,0 +1,26 @@
+- avatar = namespace_icon(namespace, 100)
+- can_create_project = current_user.can?(:create_projects, namespace)
+
+- if forked_project = namespace.find_fork_of(@project)
+ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked
+ = link_to project_path(forked_project) do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ = project_identicon(namespace, class: "avatar s100 identicon")
+ - else
+ .avatar-container.s100
+ = image_tag(avatar, class: "avatar s100")
+ %h5.prepend-top-default
+ = namespace.human_name
+- else
+ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: ("disabled" unless can_create_project) }
+ = link_to project_forks_path(@project, namespace_key: namespace.id),
+ method: "POST",
+ class: ("disabled has-tooltip" unless can_create_project),
+ title: (_('You have reached your project limit') unless can_create_project) do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ = project_identicon(namespace, class: "avatar s100 identicon")
+ - else
+ .avatar-container.s100
+ = image_tag(avatar, class: "avatar s100")
+ %h5.prepend-top-default
+ = namespace.human_name
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index e9613534dde..475c6ba4d3d 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -14,22 +14,7 @@
%h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
Click to fork the project
- @namespaces.each do |namespace|
- - avatar = namespace_icon(namespace, 100)
- - can_create_project = current_user.can?(:create_projects, namespace)
- - forked_project = namespace.find_fork_of(@project)
- - fork_path = forked_project ? project_path(forked_project) : project_forks_path(@project, namespace_key: namespace.id)
- .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: [("disabled" unless can_create_project), ("forked" if forked_project)] }
- = link_to fork_path,
- method: "POST",
- class: [("js-fork-thumbnail" unless forked_project), ("disabled has-tooltip" unless can_create_project)],
- title: (_('You have reached your project limit') unless can_create_project) do
- - if /no_((\w*)_)*avatar/.match(avatar)
- = project_identicon(namespace, class: "avatar s100 identicon")
- - else
- .avatar-container.s100
- = image_tag(avatar, class: "avatar s100")
- %h5.prepend-top-default
- = namespace.human_name
+ = render 'fork_button', namespace: namespace
- else
%strong
No available namespaces to fork the project.
diff --git a/app/views/projects/issues/_by_email_description.html.haml b/app/views/projects/issues/_by_email_description.html.haml
new file mode 100644
index 00000000000..f2d58534903
--- /dev/null
+++ b/app/views/projects/issues/_by_email_description.html.haml
@@ -0,0 +1,6 @@
+The subject will be used as the title of the new issue, and the message will be the description.
+
+= link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
+and styling with
+= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
+are supported.
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 483f28c74f2..1eccc0509bd 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -13,5 +13,5 @@
quick_actions_docs_path: help_page_path('user/project/quick_actions'),
notes_path: notes_url,
last_fetched_at: Time.now.to_i,
- issue_data: serialize_issuable(@issue),
+ noteable_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user).to_json } }
diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml
deleted file mode 100644
index 264032a3a31..00000000000
--- a/app/views/projects/issues/_issue_by_email.html.haml
+++ /dev/null
@@ -1,34 +0,0 @@
-.issues-footer.text-center
- %button.issue-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issue-email-modal" } }
- Email a new issue to this project
-
-#issue-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
- .modal-dialog{ role: "document" }
- .modal-content
- .modal-header
- %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
- %span{ aria: { hidden: "true" } }= icon("times")
- %h4.modal-title
- Create new issue by email
- .modal-body
- %p
- You can create a new issue inside this project by sending an email to the following email address:
- .email-modal-input-group.input-group
- = text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-btn
- = clipboard_button(target: '#issue_email')
- %p
- The subject will be used as the title of the new issue, and the message will be the description.
-
- = link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
- and styling with
- = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
- are supported.
-
- %p
- This is a private email address, generated just for you.
-
- Anyone who gets ahold of it can create issues as if they were you.
- You should
- = link_to 'reset it', new_issue_address_project_path(@project), class: 'incoming-email-token-reset'
- if that ever happens.
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 4f78102be0c..331d62cf247 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,34 +1,50 @@
- can_create_merge_request = can?(current_user, :create_merge_request, @project)
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
-- value = can_create_merge_request ? 'Create a merge request' : 'Create a branch'
+- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
- if can?(current_user, :push_code, @project)
- .create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_project_issue_path(@project, @issue), create_mr_path: create_merge_request_project_issue_path(@project, @issue), create_branch_path: project_branches_path(@project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }
+ - can_create_path = can_create_branch_project_issue_path(@project, @issue)
+ - create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
+ - create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
+ - refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
+
+ .create-mr-dropdown-wrap{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
.btn-group.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
.btn-group.available.hide
- %input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: value, data: { action: data_action } }
- %button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }
+ %button.btn.js-create-merge-request.btn-default{ type: 'button', data: { action: data_action } }
+ = value
+
+ %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-default.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
= icon('caret-down')
- %ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
+
+ %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request
- %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
- .menu-item
- .icon-container
- = icon('check')
- .description
- %strong Create a merge request
- %span
- Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'.
- %li.divider.droplab-item-ignore
- %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
- .menu-item
- .icon-container
- = icon('check')
- .description
- %strong Create a branch
- %span
- Creates a branch named after this issue, from '#{@project.default_branch}'.
+ %li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } }
+ .menu-item.droplab-item-ignore-hiding
+ .icon-container.droplab-item-ignore-hiding= icon('check')
+ .description.droplab-item-ignore-hiding Create merge request and branch
+
+ %li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } }
+ .menu-item.droplab-item-ignore-hiding
+ .icon-container.droplab-item-ignore-hiding= icon('check')
+ .description.droplab-item-ignore-hiding Create branch
+ %li.divider
+
+ %li.droplab-item-ignore
+ Branch name
+ %input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
+ %span.js-branch-message.branch-message.droplab-item-ignore
+
+ %li.droplab-item-ignore
+ Source (branch or tag)
+ %input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
+ %span.js-ref-message.ref-message.droplab-item-ignore
+
+ %li.droplab-item-ignore
+ %button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } }
+ Create merge request
+
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index bfaf024428d..193111b4cee 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -2,7 +2,7 @@
- @can_bulk_update = can?(current_user, :admin_issue, @project)
- page_title "Issues"
-- new_issue_email = @project.new_issue_address(current_user)
+- new_issue_email = @project.new_issuable_address(current_user, 'issue')
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
@@ -25,6 +25,6 @@
.issues-holder
= render 'issues'
- if new_issue_email
- = render 'issue_by_email', email: new_issue_email
+ = render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
- else
= render 'shared/empty_states/issues', button_path: new_project_issue_path(@project)
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index b9fec8af4d7..48410ffee21 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -27,9 +27,9 @@
.issuable-meta
- if @issue.confidential
- = icon('eye-slash', class: 'issuable-warning-icon')
+ .issuable-warning-icon.inline= sprite_icon('eye-slash', size: 16, css_class: 'icon')
- if @issue.discussion_locked?
- = icon('lock', class: 'issuable-warning-icon')
+ .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@issue, @project, "Issue")
.issuable-actions.js-issuable-actions
@@ -40,7 +40,7 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can_update_issue
- %li= link_to 'Edit', edit_project_issue_path(@project, @issue)
+ %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit'
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index b5067367802..a71333497e6 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -4,7 +4,7 @@
.sidebar-container
.blocks-container
.block
- %strong.prepend-top-10
+ %strong.inline.prepend-top-8
= @build.name
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post
@@ -44,9 +44,10 @@
%h4.title
Trigger
- %p
- %span.build-light-text Token:
- #{@build.trigger_request.trigger.short_token}
+ - if @build.trigger_request&.trigger&.short_token
+ %p
+ %span.build-light-text Token:
+ #{@build.trigger_request.trigger.short_token}
- if @build.trigger_variables.any?
%p
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 2abd2c9e652..1d0aaa47b60 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -57,13 +57,13 @@
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
- .js-truncated-info.truncated-info.hidden<
+ .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
KiB of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
- .controllers
+ .controllers.pull-right
- if @build.has_trace?
= link_to raw_project_job_path(@project, @build),
title: 'Show complete raw',
diff --git a/app/views/projects/merge_requests/_by_email_description.html.haml b/app/views/projects/merge_requests/_by_email_description.html.haml
new file mode 100644
index 00000000000..8ba251749b8
--- /dev/null
+++ b/app/views/projects/merge_requests/_by_email_description.html.haml
@@ -0,0 +1 @@
+The subject will be used as the source branch name for the new merge request and the target branch will be the default branch for the project.
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 72d5c4961ec..75b3db7e505 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -16,7 +16,7 @@
.issuable-meta
- if @merge_request.discussion_locked?
- = icon('lock', class: 'issuable-warning-icon')
+ .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@merge_request, @project, "Merge request")
.issuable-actions.js-issuable-actions
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 8da2243adef..2ded7484151 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -4,6 +4,7 @@
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- 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'
@@ -25,5 +26,7 @@
.merge-requests-holder
= render 'merge_requests'
+ - if new_merge_request_email
+ = render 'projects/issuable_by_email', email: new_merge_request_email, issuable_type: 'merge_request'
- else
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 0a7880ce4cd..cad7c2e83db 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -18,6 +18,7 @@
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'}.
%p
All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
+ = brand_new_project_guidelines
.col-lg-9.js-toggle-container
%ul.nav-links.gitlab-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 77211099830..ee4fa663b9f 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -13,29 +13,39 @@
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
- .radio
+ .radio.js-auto-devops-enable-radio-wrapper
= form.label :enabled_true do
- = form.radio_button :enabled, 'true'
+ = form.radio_button :enabled, 'true', class: 'js-auto-devops-enable-radio'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
- .radio
+ - if show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(@project)
+ .checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
+ = label_tag 'project[run_auto_devops_pipeline_explicit]' do
+ = check_box_tag 'project[run_auto_devops_pipeline_explicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
+ = s_('ProjectSettings|Immediately run a pipeline on the default branch')
+
+ .radio.js-auto-devops-enable-radio-wrapper
= form.label :enabled_false do
- = form.radio_button :enabled, 'false'
+ = form.radio_button :enabled, 'false', class: 'js-auto-devops-enable-radio'
%strong Disable Auto DevOps
%br
%span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
- .radio
- = form.label :enabled_nil do
- = form.radio_button :enabled, ''
+ .radio.js-auto-devops-enable-radio-wrapper
+ = form.label :enabled_ do
+ = form.radio_button :enabled, '', class: 'js-auto-devops-enable-radio'
%strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
- %br
+ - if show_run_auto_devops_pipeline_checkbox_for_instance_setting?(@project)
+ .checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
+ = label_tag 'project[run_auto_devops_pipeline_implicit]' do
+ = check_box_tag 'project[run_auto_devops_pipeline_implicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
+ = s_('ProjectSettings|Immediately run a pipeline on the default branch')
%p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index ba7d98228c3..e662b877fbb 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -16,7 +16,7 @@
%li prevent pushes from everybody except Masters
%li prevent <strong>anyone</strong> from force pushing to the branch
%li prevent <strong>anyone</strong> from deleting the branch
- %p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
+ %p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches")} and #{link_to "project permissions", help_page_path("user/permissions")}.
- if can? current_user, :admin_project, @project
= content_for :create_protected_branch
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index e764a37bbd7..24baf1cfc89 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -16,7 +16,7 @@
%li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag
- %p Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}.
+ %p Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags")}.
- if can? current_user, :admin_project, @project
= yield :create_protected_tag
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 705a4607ad2..7a68aa16aa4 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -67,7 +67,7 @@
- if koding_enabled? && @repository.koding_yml.blank?
%li.missing
= link_to _('Set up Koding'), add_koding_stack_path(@project)
- - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
+ - if @repository.gitlab_ci_yml.blank? && @project.deployment_platform.present?
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
#{ _('Set up auto deploy') }
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index fd3b8c01b83..da364b58e36 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- @sort ||= sort_value_recently_updated
-- page_title _('TagsPage|Tags')
+- page_title s_('TagsPage|Tags')
- add_to_breadcrumbs("Repository", project_tree_path(@project))
.flex-list{ class: container_class }
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index fd8175e1e01..c51af901699 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -2,8 +2,8 @@
%td.tree-item-file-name
= tree_icon(type, blob_item.mode, blob_item.name)
- file_name = blob_item.name
- = link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do
- %span.str-truncated= file_name
+ = link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do
+ %span= file_name
%td.hidden-xs.tree-commit
%td.tree-time-ago.cgray.text-right
= render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index 56197382a70..af3816fc9f4 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -2,8 +2,8 @@
%td.tree-item-file-name
= tree_icon(type, tree_item.mode, tree_item.name)
- path = flatten_tree(@path, tree_item)
- = link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path do
- %span.str-truncated= path
+ = link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), class: 'str-truncated', title: path do
+ %span= path
%td.hidden-xs.tree-commit
%td.tree-time-ago.text-right
= render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 745a6040488..64cc70053ef 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -11,6 +11,6 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'repo'
-%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+%div{ class: [(container_class unless show_new_repo?), ("limit-container-width" unless fluid_layout)] }
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml
index 0a1ccbc5f1c..efa16d38f84 100644
--- a/app/views/projects/wikis/_pages_wiki_page.html.haml
+++ b/app/views/projects/wikis/_pages_wiki_page.html.haml
@@ -2,4 +2,4 @@
= link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format})
.pull-right
- %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe
+ %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 5b781294d68..2c7551c6f8c 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -6,9 +6,8 @@
- git_access_url = project_wikis_git_access_path(@project)
= link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do
- = succeed '&nbsp;' do
- = icon('cloud-download')
- = _("Clone repository")
+ = icon('cloud-download', class: 'append-right-5')
+ %span= _("Clone repository")
.blocks-container
.block.block-first
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 9ee09262324..969a1677d9a 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -21,7 +21,7 @@
%th= _("Last updated")
%th= _("Format")
%tbody
- - @page.versions.each_with_index do |version, index|
+ - @page_versions.each_with_index do |version, index|
- commit = version
%tr
%td
@@ -37,5 +37,6 @@
%td
%strong
= version.format
+= paginate @page_versions, theme: 'gitlab'
= render 'sidebar'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index de15fc99eda..b3b83cee81a 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -11,8 +11,8 @@
.nav-text
%h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
- = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe
- #{time_ago_with_tooltip(@page.commit.authored_date)}
+ = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe
+ #{time_ago_with_tooltip(@page.last_version.authored_date)}
.nav-controls
= render 'main_links'
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 3d9c90c38fe..fba08092351 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -7,7 +7,7 @@
%span
= enabled_project_button(project, enabled_protocol)
- else
- %a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } }
+ %a#clone-dropdown.btn.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown' } }
%span
= default_clone_protocol.upcase
= icon('caret-down')
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 233d8c95eda..736afa085e8 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -11,6 +11,7 @@
%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>.
%li
- The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
+ 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.
%li
To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}.
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 23a418ad640..3fcc33044e9 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -38,9 +38,9 @@
= link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, data: {confirm: 'Remove this label? Are you sure?'}
.pull-right.hidden-xs.hidden-sm.hidden-md
- = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
+ = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do
view merge requests
- = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do
+ = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do
view open issues
- if current_user
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 6356e9f92cb..f4a4bfaec54 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,3 +1,5 @@
+- show_create = local_assigns.fetch(:show_create, false)
+
- show_new_branch_form = show_new_repo? && 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
diff --git a/app/views/shared/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml
index 9b989c23cab..333dd1a00b4 100644
--- a/app/views/shared/boards/components/sidebar/_notifications.html.haml
+++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml
@@ -1,7 +1,5 @@
- if current_user
- .block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
- %span.issuable-header-text.hide-collapsed.pull-left
- Notifications
- %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- %span
- {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
+ .block.subscriptions
+ %subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions",
+ ":subscribed" => "issue.subscribed",
+ ":id" => "issue.id" }
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 059dd24be6d..321d8767d08 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -9,7 +9,7 @@
.controls.hidden-xs
- if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn" do
- = icon('cogs')
+ = sprite_icon('settings')
= link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do
= icon('sign-out')
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 3f03cc7a275..6d8a4668cec 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -1,6 +1,5 @@
- type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false)
-- issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters
%li{ class: active_when(params[:state] == 'opened') }>
@@ -20,6 +19,4 @@
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)}
- %li{ class: active_when(params[:state] == 'all') }>
- = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
- #{issuables_state_counter_text(type, :all)}
+ = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all)
diff --git a/app/views/shared/issuable/nav_links/_all.html.haml b/app/views/shared/issuable/nav_links/_all.html.haml
new file mode 100644
index 00000000000..d7ad7090a45
--- /dev/null
+++ b/app/views/shared/issuable/nav_links/_all.html.haml
@@ -0,0 +1,6 @@
+- page_context_word = local_assigns.fetch(:page_context_word)
+- counter = local_assigns.fetch(:counter)
+
+%li{ class: active_when(params[:state] == 'all') }>
+ = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
+ #{counter}
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index b6085fd3af0..c978d9e4821 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -31,8 +31,7 @@
.note-header
.note-header-info
%a{ href: user_path(note.author) }
- %span.note-header-author-name
- = sanitize(note.author.name)
+ %span.note-header-author-name= sanitize(note.author.name)
%span.note-headline-light
= note.author.to_reference
%span.note-headline-light
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 52a8fe8bb67..98bfc7c4d36 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -20,7 +20,7 @@
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
.project-details
%h3.prepend-top-0.append-bottom-0
- = link_to project_path(project), class: dom_class(project) do
+ = link_to project_path(project), class: 'text-plain' do
%span.project-full-name
%span.namespace-name
- if project.namespace && !skip_namespace
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
index 5867ea58378..87e8c416194 100644
--- a/app/views/shared/repo/_repo.html.haml
+++ b/app/views/shared/repo/_repo.html.haml
@@ -1,3 +1,4 @@
+- @no_container = true;
#repo{ data: { root: @path.empty?.to_s,
root_url: project_tree_path(project),
url: content_url,
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index cc59f8660fd..4f4e81c705f 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -51,7 +51,7 @@
.cover-desc
- unless @user.public_email.blank?
.profile-link-holder.middle-dot-divider
- = link_to @user.public_email, "mailto:#{@user.public_email}"
+ = link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link'
- unless @user.skype.blank?
.profile-link-holder.middle-dot-divider
= link_to "skype:#{@user.skype}", title: "Skype" do
@@ -66,7 +66,7 @@
= icon('twitter-square')
- unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider
- = link_to @user.short_website_url, @user.full_website_url
+ = link_to @user.short_website_url, @user.full_website_url, class: 'text-link'
- unless @user.location.blank?
.profile-link-holder.middle-dot-divider
= icon('map-marker')
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index c2dc955b27c..bec0a003a1c 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -1,5 +1,5 @@
class AdminEmailWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 55d8d0c69d1..09559e3b696 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -1,6 +1,5 @@
class AuthorizedProjectsWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
# Schedules multiple jobs and waits for them to be completed.
def self.bulk_perform_and_wait(args_list)
@@ -17,11 +16,6 @@ class AuthorizedProjectsWorker
waiter.wait
end
- # Schedules multiple jobs to run in sidekiq without waiting for completion
- def self.bulk_perform_async(args_list)
- Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
- end
-
# Performs multiple jobs directly. Failed jobs will be put into sidekiq so
# they can benefit from retries
def self.bulk_perform_inline(args_list)
diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb
index 45ce49bb5c0..aeb3bc019b9 100644
--- a/app/workers/background_migration_worker.rb
+++ b/app/workers/background_migration_worker.rb
@@ -1,34 +1,5 @@
class BackgroundMigrationWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
-
- # Enqueues a number of jobs in bulk.
- #
- # The `jobs` argument should be an Array of Arrays, each sub-array must be in
- # the form:
- #
- # [migration-class, [arg1, arg2, ...]]
- def self.perform_bulk(jobs)
- Sidekiq::Client.push_bulk('class' => self,
- 'queue' => sidekiq_options['queue'],
- 'args' => jobs)
- end
-
- # Schedules multiple jobs in bulk, with a delay.
- #
- def self.perform_bulk_in(delay, jobs)
- now = Time.now.to_i
- schedule = now + delay.to_i
-
- if schedule <= now
- raise ArgumentError, 'The schedule time must be in the future!'
- end
-
- Sidekiq::Client.push_bulk('class' => self,
- 'queue' => sidekiq_options['queue'],
- 'args' => jobs,
- 'at' => schedule)
- end
+ include ApplicationWorker
# Performs the background migration.
#
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
index cd4af85d047..62b212c79be 100644
--- a/app/workers/build_coverage_worker.rb
+++ b/app/workers/build_coverage_worker.rb
@@ -1,5 +1,5 @@
class BuildCoverageWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
def perform(build_id)
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 52e7d346e74..5efa9180f5e 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -1,5 +1,5 @@
class BuildFinishedWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :processing
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index dedaf2835e6..6705a1c2709 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -1,5 +1,5 @@
class BuildHooksWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :hooks
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index e5ceb9ef715..fc775a84dc0 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -1,5 +1,5 @@
class BuildQueueWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :processing
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index 20ec24bd18a..ec049821ad7 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -1,5 +1,5 @@
class BuildSuccessWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :processing
diff --git a/app/workers/build_trace_sections_worker.rb b/app/workers/build_trace_sections_worker.rb
index 8c57e8f767b..c0f5c144e10 100644
--- a/app/workers/build_trace_sections_worker.rb
+++ b/app/workers/build_trace_sections_worker.rb
@@ -1,5 +1,5 @@
class BuildTraceSectionsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
def perform(build_id)
diff --git a/app/workers/cluster_install_app_worker.rb b/app/workers/cluster_install_app_worker.rb
index 899aed904e4..f771cb4939f 100644
--- a/app/workers/cluster_install_app_worker.rb
+++ b/app/workers/cluster_install_app_worker.rb
@@ -1,5 +1,5 @@
class ClusterInstallAppWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index b01f9708424..1ab4de3b647 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -1,5 +1,5 @@
class ClusterProvisionWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include ClusterQueue
def perform(cluster_id)
diff --git a/app/workers/cluster_wait_for_app_installation_worker.rb b/app/workers/cluster_wait_for_app_installation_worker.rb
index 4bb8c293e5d..d564d5e48bf 100644
--- a/app/workers/cluster_wait_for_app_installation_worker.rb
+++ b/app/workers/cluster_wait_for_app_installation_worker.rb
@@ -1,5 +1,5 @@
class ClusterWaitForAppInstallationWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
new file mode 100644
index 00000000000..9c3bdabc49e
--- /dev/null
+++ b/app/workers/concerns/application_worker.rb
@@ -0,0 +1,40 @@
+Sidekiq::Worker.extend ActiveSupport::Concern
+
+module ApplicationWorker
+ extend ActiveSupport::Concern
+
+ include Sidekiq::Worker
+
+ included do
+ sidekiq_options queue: base_queue_name
+ end
+
+ module ClassMethods
+ def base_queue_name
+ name
+ .sub(/\AGitlab::/, '')
+ .sub(/Worker\z/, '')
+ .underscore
+ .tr('/', '_')
+ end
+
+ def queue
+ get_sidekiq_options['queue'].to_s
+ end
+
+ def bulk_perform_async(args_list)
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
+ end
+
+ def bulk_perform_in(delay, args_list)
+ now = Time.now.to_i
+ schedule = now + delay.to_i
+
+ if schedule <= now
+ raise ArgumentError, 'The schedule time must be in the future!'
+ end
+
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_list, 'at' => schedule)
+ end
+ end
+end
diff --git a/app/workers/concerns/dedicated_sidekiq_queue.rb b/app/workers/concerns/dedicated_sidekiq_queue.rb
deleted file mode 100644
index 132bae6022b..00000000000
--- a/app/workers/concerns/dedicated_sidekiq_queue.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# Concern that sets the queue of a Sidekiq worker based on the worker's class
-# name/namespace.
-module DedicatedSidekiqQueue
- extend ActiveSupport::Concern
-
- included do
- sidekiq_options queue: name.sub(/Worker\z/, '').underscore.tr('/', '_')
- 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 67e36c811de..9a9fbaad653 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -8,7 +8,7 @@ module Gitlab
extend ActiveSupport::Concern
included do
- include Sidekiq::Worker
+ include ApplicationWorker
include GithubImport::Queue
include ReschedulingMethods
include NotifyUponDeath
diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
index 9b5ff17aafa..f371731f68c 100644
--- a/app/workers/create_gpg_signature_worker.rb
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -1,6 +1,5 @@
class CreateGpgSignatureWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(commit_sha, project_id)
project = Project.find_by(id: project_id)
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
new file mode 100644
index 00000000000..00cd7b85b9f
--- /dev/null
+++ b/app/workers/create_pipeline_worker.rb
@@ -0,0 +1,16 @@
+class CreatePipelineWorker
+ include ApplicationWorker
+ include PipelineQueue
+
+ enqueue_in group: :creation
+
+ def perform(project_id, user_id, ref, source, params = {})
+ project = Project.find(project_id)
+ user = User.find(user_id)
+ params = params.deep_symbolize_keys
+
+ Ci::CreatePipelineService
+ .new(project, user, ref: ref)
+ .execute(source, **params)
+ end
+end
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
index f870da4ecfd..07cd1f02fb5 100644
--- a/app/workers/delete_merged_branches_worker.rb
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -1,6 +1,5 @@
class DeleteMergedBranchesWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(project_id, user_id)
begin
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 3340a7be4fe..6c431b02979 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -1,6 +1,5 @@
class DeleteUserWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(current_user_id, delete_user_id, options = {})
delete_user = User.find(delete_user_id)
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 1afa24c8e2a..dd8a6cbbef1 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -1,6 +1,5 @@
class EmailReceiverWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(raw)
return unless Gitlab::IncomingEmail.enabled?
@@ -39,8 +38,7 @@ class EmailReceiverWorker
"You are not allowed to perform this action. If you believe this is in error, contact a staff member."
when Gitlab::Email::NoteableNotFoundError
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
- when Gitlab::Email::InvalidNoteError,
- Gitlab::Email::InvalidIssueError
+ when Gitlab::Email::InvalidRecordError
can_retry = true
e.message
end
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index f5ccc84c160..21da27973fe 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -1,6 +1,5 @@
class EmailsOnPushWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
attr_reader :email, :skip_premailer
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index a27585fd389..87e5dca01fd 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -1,5 +1,5 @@
class ExpireBuildArtifactsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
@@ -8,6 +8,6 @@ class ExpireBuildArtifactsWorker
build_ids = Ci::Build.with_expired_artifacts.pluck(:id)
build_ids = build_ids.map { |build_id| [build_id] }
- Sidekiq::Client.push_bulk('class' => ExpireBuildInstanceArtifactsWorker, 'args' => build_ids )
+ ExpireBuildInstanceArtifactsWorker.bulk_perform_async(build_ids)
end
end
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index 7b59e976492..234b4357cf7 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -1,6 +1,5 @@
class ExpireBuildInstanceArtifactsWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(build_id)
build = Ci::Build
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index 98a7500bffe..a591e2da519 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -1,5 +1,5 @@
class ExpireJobCacheWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :cache
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index 1a0e7f92875..a3ac32b437d 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -1,5 +1,5 @@
class ExpirePipelineCacheWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :cache
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index ec65d3ff65e..8e26275669e 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -1,6 +1,5 @@
class GitGarbageCollectWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
include Gitlab::CurrentSettings
sidekiq_options retry: false
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index 877f88c043f..400396d5755 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -7,7 +7,7 @@ module Gitlab
# been completed this worker will advance the import process to the next
# stage.
class AdvanceStageWorker
- include Sidekiq::Worker
+ include ApplicationWorker
sidekiq_options queue: 'github_importer_advance_stage', dead: false
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
index 45a38927225..7108b531bc2 100644
--- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -3,7 +3,7 @@
module Gitlab
module GithubImport
class RefreshImportJidWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include GithubImport::Queue
# The interval to schedule new instances of this job at.
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 1a09497780a..073d6608082 100644
--- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -4,7 +4,7 @@ module Gitlab
module GithubImport
module Stage
class FinishImportWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
index f8a3684c6ba..5726fbb573d 100644
--- a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
@@ -4,7 +4,7 @@ module Gitlab
module GithubImport
module Stage
class ImportBaseDataWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
index e110b7c1c36..7007754ff2e 100644
--- a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
@@ -4,7 +4,7 @@ module Gitlab
module GithubImport
module Stage
class ImportIssuesAndDiffNotesWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
index 9810ed25cf9..5f4678a595f 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -4,7 +4,7 @@ module Gitlab
module GithubImport
module Stage
class ImportNotesWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
index c531f26e897..1c5a7139802 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
@@ -4,7 +4,7 @@ module Gitlab
module GithubImport
module Stage
class ImportPullRequestsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_repository_worker.rb b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
index aa5762e773d..4d16cef1130 100644
--- a/app/workers/gitlab/github_import/stage/import_repository_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
@@ -4,7 +4,7 @@ module Gitlab
module GithubImport
module Stage
class ImportRepositoryWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index 0ec871e00e1..a0028e41332 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -1,7 +1,6 @@
class GitlabShellWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include Gitlab::ShellAdapter
- include DedicatedSidekiqQueue
def perform(action, *arg)
gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb
index 0a55aab63fd..6dd281b1147 100644
--- a/app/workers/gitlab_usage_ping_worker.rb
+++ b/app/workers/gitlab_usage_ping_worker.rb
@@ -1,7 +1,7 @@
class GitlabUsagePingWorker
LEASE_TIMEOUT = 86400
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index bd8e212e928..f577b310b20 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -1,6 +1,5 @@
class GroupDestroyWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
include ExceptionBacktrace
def perform(group_id, user_id)
diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
index 7957ed807ab..9788c8df3a3 100644
--- a/app/workers/import_export_project_cleanup_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -1,5 +1,5 @@
class ImportExportProjectCleanupWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
index db6b1ea8e8d..6774ab307c6 100644
--- a/app/workers/invalid_gpg_signature_update_worker.rb
+++ b/app/workers/invalid_gpg_signature_update_worker.rb
@@ -1,6 +1,5 @@
class InvalidGpgSignatureUpdateWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(gpg_key_id)
gpg_key = GpgKey.find_by(id: gpg_key_id)
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 3dd14466994..9ae5456be4c 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -2,8 +2,7 @@ require 'json'
require 'socket'
class IrkerWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(project_id, chans, colors, push_data, settings)
project = Project.find(project_id)
@@ -104,6 +103,7 @@ class IrkerWorker
parents = commit.parents
# Return old value if there's no new one
return push_data['before'] if parents.empty?
+
# Or return the first parent-commit
parents[0].id
end
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index 48e2da338f6..ba832fe30c6 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -1,6 +1,5 @@
class MergeWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(merge_request_id, current_user_id, params)
params = params.with_indifferent_access
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
index f1cd1769421..adb25c2a170 100644
--- a/app/workers/namespaceless_project_destroy_worker.rb
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -5,14 +5,9 @@
# The worker will reject doing anything for projects that *do* have a
# namespace. For those use ProjectDestroyWorker instead.
class NamespacelessProjectDestroyWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
include ExceptionBacktrace
- def self.bulk_perform_async(args_list)
- Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
- end
-
def perform(project_id)
begin
project = Project.unscoped.find(project_id)
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index d9a8e892e90..3bc030f9c62 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -1,6 +1,5 @@
class NewIssueWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
include NewIssuable
def perform(issue_id, user_id)
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index 1910c490159..bda2a0ab59d 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -1,6 +1,5 @@
class NewMergeRequestWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
include NewIssuable
def perform(merge_request_id, user_id)
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 926162b8c53..67c54fbf10e 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -1,6 +1,5 @@
class NewNoteWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
# Keep extra parameter to preserve backwards compatibility with
# old `NewNoteWorker` jobs (can remove later)
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 64788da7299..62f733c02fc 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -1,5 +1,5 @@
class PagesWorker
- include Sidekiq::Worker
+ include ApplicationWorker
sidekiq_options queue: :pages, retry: false
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index 30a75ec8435..661c29efe88 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -1,5 +1,5 @@
class PipelineHooksWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :hooks
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index 070943f1ecc..d46d1f122fc 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -1,5 +1,5 @@
class PipelineMetricsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
def perform(pipeline_id)
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
index cdb860b6675..a9a1168a6e3 100644
--- a/app/workers/pipeline_notification_worker.rb
+++ b/app/workers/pipeline_notification_worker.rb
@@ -1,5 +1,5 @@
class PipelineNotificationWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
def perform(pipeline_id, recipients = nil)
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 8c067d05081..07dbf6a971e 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -1,5 +1,5 @@
class PipelineProcessWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :processing
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index d7087f20dfc..c49758878a4 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -1,5 +1,5 @@
class PipelineScheduleWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
@@ -9,7 +9,7 @@ class PipelineScheduleWorker
pipeline = Ci::CreatePipelineService.new(schedule.project,
schedule.owner,
ref: schedule.ref)
- .execute(:schedule, save_on_errors: false, schedule: schedule)
+ .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
schedule.deactivate! unless pipeline.persisted?
rescue => e
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index cb8bb2ffe75..68c40a259e1 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -1,5 +1,5 @@
class PipelineSuccessWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :processing
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index 5fa399dff4c..24a8a9fbed5 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -1,5 +1,5 @@
class PipelineUpdateWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :processing
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index b8f8d3750d9..f2b2c4428d3 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,6 +1,5 @@
class PostReceive
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(gl_repository, identifier, changes)
project, is_wiki = Gitlab::GlRepository.parse(gl_repository)
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index c0c03848a40..52eebe475ec 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -5,8 +5,7 @@
# Consider using an extra worker if you need to add any extra (and potentially
# slow) processing of commits.
class ProcessCommitWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
# project_id - The ID of the project this commit belongs to.
# user_id - The ID of the user that pushed the commit.
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 505ff9e086e..f19bcbf946a 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -1,7 +1,6 @@
# Worker for updating any project specific caches.
class ProjectCacheWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
LEASE_TIMEOUT = 15.minutes.to_i
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index 3be7e686609..1ba854ca4cb 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -1,6 +1,5 @@
class ProjectDestroyWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
include ExceptionBacktrace
def perform(project_id, user_id, params)
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index f13ac9e5db2..c100852374a 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -1,6 +1,5 @@
class ProjectExportWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
include ExceptionBacktrace
sidekiq_options retry: 3
diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb
index ca276d7801c..d01eb744e5d 100644
--- a/app/workers/project_migrate_hashed_storage_worker.rb
+++ b/app/workers/project_migrate_hashed_storage_worker.rb
@@ -1,11 +1,34 @@
class ProjectMigrateHashedStorageWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
+
+ LEASE_TIMEOUT = 30.seconds.to_i
def perform(project_id)
project = Project.find_by(id: project_id)
return if project.nil? || project.pending_delete?
- ::Projects::HashedStorageMigrationService.new(project, logger).execute
+ uuid = lease_for(project_id).try_obtain
+ if uuid
+ ::Projects::HashedStorageMigrationService.new(project, logger).execute
+ else
+ false
+ end
+ rescue => ex
+ cancel_lease_for(project_id, uuid) if uuid
+ raise ex
+ end
+
+ def lease_for(project_id)
+ Gitlab::ExclusiveLease.new(lease_key(project_id), timeout: LEASE_TIMEOUT)
+ end
+
+ private
+
+ def lease_key(project_id)
+ "project_migrate_hashed_storage_worker:#{project_id}"
+ end
+
+ def cancel_lease_for(project_id, uuid)
+ Gitlab::ExclusiveLease.cancel(lease_key(project_id), uuid)
end
end
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 4883d848c53..75c4b8b3663 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -1,6 +1,5 @@
class ProjectServiceWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
sidekiq_options dead: false
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index 6b607451c7a..635a97c99af 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -1,7 +1,6 @@
# Worker for updating any project specific caches.
class PropagateServiceTemplateWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
LEASE_TIMEOUT = 4.hours.to_i
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
index 2b43bb19ad1..5ff62ab1369 100644
--- a/app/workers/prune_old_events_worker.rb
+++ b/app/workers/prune_old_events_worker.rb
@@ -1,5 +1,5 @@
class PruneOldEventsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index 18b8daf4e1e..ef3ddb9024b 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -1,6 +1,5 @@
class ReactiveCachingWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(class_name, id, *args)
klass = begin
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 2a619f83410..7e64c3070a8 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -1,5 +1,5 @@
class RemoveExpiredGroupLinksWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index 31f652e5f9b..d80b3b15840 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -1,5 +1,5 @@
class RemoveExpiredMembersWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb
index 555e1bb8691..87fed42d7ce 100644
--- a/app/workers/remove_old_web_hook_logs_worker.rb
+++ b/app/workers/remove_old_web_hook_logs_worker.rb
@@ -1,5 +1,5 @@
class RemoveOldWebHookLogsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
WEB_HOOK_LOG_LIFETIME = 2.days
diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb
index b80f131d5f7..8daf079fc31 100644
--- a/app/workers/remove_unreferenced_lfs_objects_worker.rb
+++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb
@@ -1,5 +1,5 @@
class RemoveUnreferencedLfsObjectsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
index e47069df189..86a258cf94f 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -1,5 +1,5 @@
class RepositoryArchiveCacheWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index b94d83bd709..76688cf51c1 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -1,6 +1,6 @@
module RepositoryCheck
class BatchWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
RUN_TIME = 3600
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index 85bc9103538..97b89dc3db5 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -1,6 +1,6 @@
module RepositoryCheck
class ClearWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include RepositoryCheckQueue
def perform
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 164586cf0b7..4e3c691e8da 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -1,6 +1,6 @@
module RepositoryCheck
class SingleRepositoryWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include RepositoryCheckQueue
def perform(project_id)
@@ -32,16 +32,14 @@ module RepositoryCheck
end
def git_fsck(repository)
- path = repository.path_to_repo
- cmd = %W(nice git --git-dir=#{path} fsck)
- output, status = Gitlab::Popen.popen(cmd)
+ return false unless repository.exists?
- if status.zero?
- true
- else
- Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}")
- false
- end
+ repository.raw_repository.fsck
+
+ true
+ rescue Gitlab::Git::Repository::GitError => e
+ Gitlab::RepositoryCheckLogger.error(e.message)
+ false
end
def has_pushes?(project)
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 264706e3e23..a07ef1705a1 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,25 +1,24 @@
class RepositoryForkWorker
ForkError = Class.new(StandardError)
- include Sidekiq::Worker
+ include ApplicationWorker
include Gitlab::ShellAdapter
- include DedicatedSidekiqQueue
include ProjectStartImport
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
- def perform(project_id, forked_from_repository_storage_path, source_path, target_path)
+ def perform(project_id, forked_from_repository_storage_path, source_disk_path)
project = Project.find(project_id)
return unless start_fork(project)
Gitlab::Metrics.add_event(:fork_repository,
- source_path: source_path,
- target_path: target_path)
+ source_path: source_disk_path,
+ target_path: project.disk_path)
- result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path,
- project.repository_storage_path, target_path)
- raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result
+ result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_disk_path,
+ project.repository_storage_path, project.disk_path)
+ raise ForkError, "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result
project.repository.after_import
raise ForkError, "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 4e90b137b26..55715c83cb1 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,8 +1,7 @@
class RepositoryImportWorker
ImportError = Class.new(StandardError)
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
include ExceptionBacktrace
include ProjectStartImport
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
index 703b025d76e..55c236e9e9d 100644
--- a/app/workers/requests_profiles_worker.rb
+++ b/app/workers/requests_profiles_worker.rb
@@ -1,5 +1,5 @@
class RequestsProfilesWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/schedule_update_user_activity_worker.rb b/app/workers/schedule_update_user_activity_worker.rb
index 6c2c3e437f3..d9376577597 100644
--- a/app/workers/schedule_update_user_activity_worker.rb
+++ b/app/workers/schedule_update_user_activity_worker.rb
@@ -1,5 +1,5 @@
class ScheduleUpdateUserActivityWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform(batch_size = 500)
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index c301cea5ad6..69f2318d83b 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -1,5 +1,5 @@
class StageUpdateWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include PipelineQueue
enqueue_in group: :processing
diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb
index b48ead799b9..f92421a667d 100644
--- a/app/workers/storage_migrator_worker.rb
+++ b/app/workers/storage_migrator_worker.rb
@@ -1,6 +1,5 @@
class StorageMigratorWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
BATCH_SIZE = 100
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index 269776a1f62..fb26fa4c515 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -1,5 +1,5 @@
class StuckCiJobsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
EXCLUSIVE_LEASE_KEY = 'stuck_ci_builds_worker_lease'.freeze
@@ -39,14 +39,23 @@ class StuckCiJobsWorker
def drop_stuck(status, timeout)
search(status, timeout) do |build|
return unless build.stuck?
+
drop_build :stuck, build, status, timeout
end
end
def search(status, timeout)
- builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago)
- builds.joins(:project).merge(Project.without_deleted).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build|
- yield(build)
+ loop do
+ jobs = Ci::Build.where(status: status)
+ .where('ci_builds.updated_at < ?', timeout.ago)
+ .includes(:tags, :runner, project: :namespace)
+ .limit(100)
+ .to_a
+ break if jobs.empty?
+
+ jobs.each do |job|
+ yield(job)
+ end
end
end
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index f850e459cd9..e0e6d1418de 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -1,5 +1,5 @@
class StuckImportJobsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
IMPORT_JOBS_EXPIRATION = 15.hours.to_i
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index a396c0f27b2..36d2a2e6466 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -1,5 +1,5 @@
class StuckMergeJobsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/system_hook_push_worker.rb b/app/workers/system_hook_push_worker.rb
index e43bbe35de9..ceeaaf8d189 100644
--- a/app/workers/system_hook_push_worker.rb
+++ b/app/workers/system_hook_push_worker.rb
@@ -1,6 +1,5 @@
class SystemHookPushWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(push_data, hook_id)
SystemHooksService.new.execute_hooks(push_data, hook_id)
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
index 0531630d13a..7eb65452a7d 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -1,5 +1,5 @@
class TrendingProjectsWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include CronjobQueue
def perform
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index afc47fc63d6..74bb9993275 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -1,6 +1,5 @@
class UpdateMergeRequestsWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
LOG_TIME_THRESHOLD = 90 # seconds
diff --git a/app/workers/update_user_activity_worker.rb b/app/workers/update_user_activity_worker.rb
index 31bbdb69edb..27ec5cd33fb 100644
--- a/app/workers/update_user_activity_worker.rb
+++ b/app/workers/update_user_activity_worker.rb
@@ -1,6 +1,5 @@
class UpdateUserActivityWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(pairs)
pairs = cast_data(pairs)
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
index 78931f1258f..9222760c031 100644
--- a/app/workers/upload_checksum_worker.rb
+++ b/app/workers/upload_checksum_worker.rb
@@ -1,6 +1,5 @@
class UploadChecksumWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
def perform(upload_id)
upload = Upload.find(upload_id)
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 241ed3901dc..19cdb279aaa 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -1,5 +1,5 @@
class WaitForClusterCreationWorker
- include Sidekiq::Worker
+ include ApplicationWorker
include ClusterQueue
def perform(cluster_id)
diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb
index 713c0228040..dfc3f33ad9d 100644
--- a/app/workers/web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -1,6 +1,5 @@
class WebHookWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
+ include ApplicationWorker
sidekiq_options retry: 4, dead: false