Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/auth_buttons/atlassian_64.pngbin0 -> 1512 bytes
-rw-r--r--app/assets/images/file_icons.svg2
-rw-r--r--app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js7
-rw-r--r--app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue48
-rw-r--r--app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue53
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/getters.js1
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js34
-rw-r--r--app/assets/javascripts/alert_handler.js13
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue42
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue61
-rw-r--r--app/assets/javascripts/alert_management/components/alert_status.vue6
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue53
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue18
-rw-r--r--app/assets/javascripts/alert_management/components/system_notes/system_note.vue3
-rw-r--r--app/assets/javascripts/alert_management/constants.js2
-rw-r--r--app/assets/javascripts/alert_management/details.js9
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql3
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql3
-rw-r--r--app/assets/javascripts/api.js8
-rw-r--r--app/assets/javascripts/authentication/mount_2fa.js19
-rw-r--r--app/assets/javascripts/authentication/u2f/authenticate.js3
-rw-r--r--app/assets/javascripts/authentication/u2f/register.js26
-rw-r--r--app/assets/javascripts/authentication/webauthn/authenticate.js69
-rw-r--r--app/assets/javascripts/authentication/webauthn/error.js28
-rw-r--r--app/assets/javascripts/authentication/webauthn/flow.js24
-rw-r--r--app/assets/javascripts/authentication/webauthn/index.js13
-rw-r--r--app/assets/javascripts/authentication/webauthn/register.js78
-rw-r--r--app/assets/javascripts/authentication/webauthn/util.js120
-rw-r--r--app/assets/javascripts/badges/components/badge.vue9
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue30
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue9
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue1
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_dropdown.vue33
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_item.vue9
-rw-r--r--app/assets/javascripts/batch_comments/components/review_bar.vue1
-rw-r--r--app/assets/javascripts/batch_comments/index.js1
-rw-r--r--app/assets/javascripts/behaviors/autosize.js11
-rw-r--r--app/assets/javascripts/behaviors/index.js22
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js5
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js21
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js3
-rw-r--r--app/assets/javascripts/behaviors/shortcuts.js6
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js99
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js17
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_content.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_embeddable.vue41
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js8
-rw-r--r--app/assets/javascripts/blob/file_template_selector.js8
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue45
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue6
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js1
-rw-r--r--app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue50
-rw-r--r--app/assets/javascripts/blob/suggest_web_ide_ci/index.js20
-rw-r--r--app/assets/javascripts/blob/template_selector.js4
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/dockerfile_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/gitignore_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/license_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/type_selector.js3
-rw-r--r--app/assets/javascripts/blob/utils.js8
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js11
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js15
-rw-r--r--app/assets/javascripts/boards/boards_util.js64
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue18
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue112
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout.vue93
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue63
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue70
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue27
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue60
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue47
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue30
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js4
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue10
-rw-r--r--app/assets/javascripts/boards/components/issuable_title.vue21
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue24
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue7
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue7
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue24
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue10
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue26
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue25
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue16
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue7
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue22
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue79
-rw-r--r--app/assets/javascripts/boards/constants.js3
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js5
-rw-r--r--app/assets/javascripts/boards/index.js82
-rw-r--r--app/assets/javascripts/boards/models/issue.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js2
-rw-r--r--app/assets/javascripts/boards/queries/board_list_create.mutation.graphql10
-rw-r--r--app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql2
-rw-r--r--app/assets/javascripts/boards/queries/board_list_update.mutation.graphql10
-rw-r--r--app/assets/javascripts/boards/queries/group_lists_issues.query.graphql18
-rw-r--r--app/assets/javascripts/boards/queries/issue.fragment.graphql6
-rw-r--r--app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql28
-rw-r--r--app/assets/javascripts/boards/queries/lists_issues.query.graphql39
-rw-r--r--app/assets/javascripts/boards/queries/project_lists_issues.query.graphql18
-rw-r--r--app/assets/javascripts/boards/stores/actions.js270
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js34
-rw-r--r--app/assets/javascripts/boards/stores/getters.js22
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js21
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js135
-rw-r--r--app/assets/javascripts/boards/stores/state.js10
-rw-r--r--app/assets/javascripts/branches/ajax_loading_spinner.js31
-rw-r--r--app/assets/javascripts/ci_lint/components/ci_lint.vue14
-rw-r--r--app/assets/javascripts/ci_lint/components/ci_lint_results.vue9
-rw-r--r--app/assets/javascripts/ci_lint/index.js18
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue1
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js1
-rw-r--r--app/assets/javascripts/ci_variable_list/store/getters.js3
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js2
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue45
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue17
-rw-r--r--app/assets/javascripts/clusters/components/new_cluster.vue34
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue33
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_button.vue8
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue1
-rw-r--r--app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue1
-rw-r--r--app/assets/javascripts/clusters/new_cluster.js19
-rw-r--r--app/assets/javascripts/clusters/stores/new_cluster/index.js12
-rw-r--r--app/assets/javascripts/clusters/stores/new_cluster/state.js3
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue2
-rw-r--r--app/assets/javascripts/commons/jquery.js3
-rw-r--r--app/assets/javascripts/compare_autocomplete.js3
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/dropdown.vue11
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue5
-rw-r--r--app/assets/javascripts/contributors/stores/actions.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue16
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/constants.js8
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/getters.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/state.js2
-rw-r--r--app/assets/javascripts/create_item_dropdown.js8
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue14
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.vue4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.vue7
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js3
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue9
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue16
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js10
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js (renamed from app/assets/javascripts/gl_dropdown.js)252
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js135
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js44
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js42
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/index.js11
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js (renamed from app/assets/javascripts/gl_dropdown/render.js)16
-rw-r--r--app/assets/javascripts/design_management/components/delete_button.vue3
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue92
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue45
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue21
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue49
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue59
-rw-r--r--app/assets/javascripts/design_management/components/design_todo_button.vue168
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue29
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue14
-rw-r--r--app/assets/javascripts/design_management/constants.js1
-rw-r--r--app/assets/javascripts/design_management/graphql.js59
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql5
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql5
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql17
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql1
-rw-r--r--app/assets/javascripts/design_management/index.js2
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue29
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue11
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js283
-rw-r--r--app/assets/javascripts/design_management/utils/design_management_utils.js34
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js8
-rw-r--r--app/assets/javascripts/design_management/utils/tracking.js1
-rw-r--r--app/assets/javascripts/design_management_legacy/components/app.vue3
-rw-r--r--app/assets/javascripts/design_management_legacy/components/delete_button.vue64
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_destroyer.vue66
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_note_pin.vue61
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue297
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue156
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue141
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue70
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_overlay.vue287
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_presentation.vue322
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_scaler.vue65
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_sidebar.vue178
-rw-r--r--app/assets/javascripts/design_management_legacy/components/image.vue110
-rw-r--r--app/assets/javascripts/design_management_legacy/components/list/item.vue174
-rw-r--r--app/assets/javascripts/design_management_legacy/components/toolbar/index.vue126
-rw-r--r--app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue83
-rw-r--r--app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue48
-rw-r--r--app/assets/javascripts/design_management_legacy/components/upload/button.vue58
-rw-r--r--app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue134
-rw-r--r--app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue76
-rw-r--r--app/assets/javascripts/design_management_legacy/constants.js16
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql.js45
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql24
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql8
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql29
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql5
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql9
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql3
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql4
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql21
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql17
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql3
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql21
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql6
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql4
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql31
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql26
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql12
-rw-r--r--app/assets/javascripts/design_management_legacy/index.js61
-rw-r--r--app/assets/javascripts/design_management_legacy/mixins/all_designs.js49
-rw-r--r--app/assets/javascripts/design_management_legacy/mixins/all_versions.js62
-rw-r--r--app/assets/javascripts/design_management_legacy/pages/design/index.vue378
-rw-r--r--app/assets/javascripts/design_management_legacy/pages/index.vue323
-rw-r--r--app/assets/javascripts/design_management_legacy/router/constants.js3
-rw-r--r--app/assets/javascripts/design_management_legacy/router/index.js35
-rw-r--r--app/assets/javascripts/design_management_legacy/router/routes.js44
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/cache_update.js276
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/design_management_utils.js128
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/error_messages.js95
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/tracking.js27
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js1
-rw-r--r--app/assets/javascripts/diffs/components/app.vue231
-rw-r--r--app/assets/javascripts/diffs/components/collapsed_files_warning.vue71
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue14
-rw-r--r--app/assets/javascripts/diffs/components/compare_dropdown_layout.vue6
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue35
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue17
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue52
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue53
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue59
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_row.vue24
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue6
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue7
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue8
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue156
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue3
-rw-r--r--app/assets/javascripts/diffs/components/merge_conflict_warning.vue72
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue28
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue273
-rw-r--r--app/assets/javascripts/diffs/components/settings_dropdown.vue138
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue12
-rw-r--r--app/assets/javascripts/diffs/constants.js6
-rw-r--r--app/assets/javascripts/diffs/diff_file.js1
-rw-r--r--app/assets/javascripts/diffs/i18n.js14
-rw-r--r--app/assets/javascripts/diffs/store/actions.js75
-rw-r--r--app/assets/javascripts/diffs/store/getters.js9
-rw-r--r--app/assets/javascripts/diffs/store/getters_versions_dropdowns.js31
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js18
-rw-r--r--app/assets/javascripts/diffs/store/utils.js80
-rw-r--r--app/assets/javascripts/diffs/utils/uuids.js3
-rw-r--r--app/assets/javascripts/due_date_select.js3
-rw-r--r--app/assets/javascripts/editor/constants.js7
-rw-r--r--app/assets/javascripts/editor/editor_file_template_ext.js7
-rw-r--r--app/assets/javascripts/editor/editor_lite.js113
-rw-r--r--app/assets/javascripts/editor/editor_markdown_ext.js14
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue1
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue33
-rw-r--r--app/assets/javascripts/environments/components/environment_delete.vue7
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue11
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue16
-rw-r--r--app/assets/javascripts/environments/components/environment_pin.vue18
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue24
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue7
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue9
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js22
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue3
-rw-r--r--app/assets/javascripts/environments/index.js21
-rw-r--r--app/assets/javascripts/environments/mixins/canary_callout_mixin.js2
-rw-r--r--app/assets/javascripts/environments/stores/helpers.js1
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue6
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue8
-rw-r--r--app/assets/javascripts/error_tracking/store/details/actions.js1
-rw-r--r--app/assets/javascripts/error_tracking/store/details/getters.js1
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue7
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js5
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue6
-rw-r--r--app/assets/javascripts/frequent_items/index.js12
-rw-r--r--app/assets/javascripts/frequent_items/store/getters.js1
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js22
-rw-r--r--app/assets/javascripts/gpg_badges.js3
-rw-r--r--app/assets/javascripts/grafana_integration/components/grafana_integration.vue7
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql10
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql7
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql)0
-rw-r--r--app/assets/javascripts/groups/components/app.vue1
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue1
-rw-r--r--app/assets/javascripts/groups/components/invite_members_banner.vue76
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue8
-rw-r--r--app/assets/javascripts/groups/components/item_caret.vue6
-rw-r--r--app/assets/javascripts/groups/components/item_stats_value.vue6
-rw-r--r--app/assets/javascripts/groups/components/item_type_icon.vue6
-rw-r--r--app/assets/javascripts/groups/init_invite_members_banner.js23
-rw-r--r--app/assets/javascripts/groups/members/components/app.vue11
-rw-r--r--app/assets/javascripts/groups/members/index.js30
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js3
-rw-r--r--app/assets/javascripts/groups_select.js11
-rw-r--r--app/assets/javascripts/header.js79
-rw-r--r--app/assets/javascripts/helpers/startup_css_helper.js46
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue14
-rw-r--r--app/assets/javascripts/ide/components/branches/item.vue6
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue7
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue1
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue46
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue9
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue10
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue1
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue1
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide.vue3
-rw-r--r--app/assets/javascripts/ide/components/ide_file_row.vue12
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue7
-rw-r--r--app/assets/javascripts/ide/components/ide_status_list.vue7
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue15
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue14
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue6
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue7
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/item.vue6
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue9
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue6
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown_button.vue8
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/button.vue6
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue6
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue12
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue3
-rw-r--r--app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue12
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue11
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue16
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue6
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue13
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue7
-rw-r--r--app/assets/javascripts/ide/components/shared/tokened_input.vue6
-rw-r--r--app/assets/javascripts/ide/components/terminal/empty_state.vue1
-rw-r--r--app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue7
-rw-r--r--app/assets/javascripts/ide/ide_router.js1
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js2
-rw-r--r--app/assets/javascripts/ide/lib/editor.js7
-rw-r--r--app/assets/javascripts/ide/lib/editorconfig/parser.js1
-rw-r--r--app/assets/javascripts/ide/lib/errors.js39
-rw-r--r--app/assets/javascripts/ide/lib/files.js19
-rw-r--r--app/assets/javascripts/ide/lib/schemas/index.js4
-rw-r--r--app/assets/javascripts/ide/lib/schemas/json/index.js8
-rw-r--r--app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js4
-rw-r--r--app/assets/javascripts/ide/lib/schemas/yaml/index.js12
-rw-r--r--app/assets/javascripts/ide/services/gql.js1
-rw-r--r--app/assets/javascripts/ide/services/index.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions.js22
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js14
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js6
-rw-r--r--app/assets/javascripts/ide/stores/getters.js19
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js28
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutation_types.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pane/getters.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js22
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/constants.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/utils.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/router/actions.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/router/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/getters.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js19
-rw-r--r--app/assets/javascripts/ide/stores/utils.js17
-rw-r--r--app/assets/javascripts/ide/sync_router_and_store.js1
-rw-r--r--app/assets/javascripts/ide/utils.js31
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue185
-rw-r--r--app/assets/javascripts/import_projects/components/imported_project_table_row.vue59
-rw-r--r--app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue32
-rw-r--r--app/assets/javascripts/import_projects/components/page_query_param_sync.vue39
-rw-r--r--app/assets/javascripts/import_projects/components/provider_repo_table_row.vue48
-rw-r--r--app/assets/javascripts/import_projects/store/actions.js39
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js13
-rw-r--r--app/assets/javascripts/import_projects/store/mutations.js116
-rw-r--r--app/assets/javascripts/import_projects/store/state.js2
-rw-r--r--app/assets/javascripts/import_projects/utils.js12
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue67
-rw-r--r--app/assets/javascripts/incidents/constants.js3
-rw-r--r--app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql3
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql4
-rw-r--r--app/assets/javascripts/incidents_settings/components/alerts_form.vue23
-rw-r--r--app/assets/javascripts/incidents_settings/constants.js3
-rw-r--r--app/assets/javascripts/incidents_settings/index.js2
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js3
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js4
-rw-r--r--app/assets/javascripts/integrations/edit/components/active_checkbox.vue (renamed from app/assets/javascripts/integrations/edit/components/active_toggle.vue)30
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue1
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue59
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue7
-rw-r--r--app/assets/javascripts/integrations/edit/components/override_dropdown.vue39
-rw-r--r--app/assets/javascripts/integrations/edit/constants.js17
-rw-r--r--app/assets/javascripts/integrations/edit/index.js26
-rw-r--r--app/assets/javascripts/integrations/edit/store/actions.js3
-rw-r--r--app/assets/javascripts/integrations/edit/store/getters.js6
-rw-r--r--app/assets/javascripts/integrations/edit/store/index.js1
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutations.js6
-rw-r--r--app/assets/javascripts/integrations/edit/store/state.js8
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js116
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js2
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_create_root.vue44
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_form.vue127
-rw-r--r--app/assets/javascripts/issuable_form.js1
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue140
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_list_root.vue153
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_tabs.vue53
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/app.vue7
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/item.vue11
-rw-r--r--app/assets/javascripts/issuables_list/service_desk_helper.js55
-rw-r--r--app/assets/javascripts/issue.js21
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue25
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue19
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue35
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue21
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql20
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue42
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue71
-rw-r--r--app/assets/javascripts/issue_show/components/locked_warning.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/pinned_links.vue19
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue30
-rw-r--r--app/assets/javascripts/issue_show/incident.js36
-rw-r--r--app/assets/javascripts/issue_show/issue.js (renamed from app/assets/javascripts/issue_show/index.js)5
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js5
-rw-r--r--app/assets/javascripts/issue_show/utils/parse_data.js14
-rw-r--r--app/assets/javascripts/issue_status_select.js3
-rw-r--r--app/assets/javascripts/issues_list/components/issuable.vue (renamed from app/assets/javascripts/issuables_list/components/issuable.vue)14
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue (renamed from app/assets/javascripts/issuables_list/components/issuables_list_app.vue)2
-rw-r--r--app/assets/javascripts/issues_list/components/jira_issues_list_root.vue (renamed from app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue)2
-rw-r--r--app/assets/javascripts/issues_list/constants.js (renamed from app/assets/javascripts/issuables_list/constants.js)0
-rw-r--r--app/assets/javascripts/issues_list/eventhub.js (renamed from app/assets/javascripts/issuables_list/eventhub.js)0
-rw-r--r--app/assets/javascripts/issues_list/index.js (renamed from app/assets/javascripts/issuables_list/index.js)8
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql (renamed from app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql)0
-rw-r--r--app/assets/javascripts/issues_list/service_desk_helper.js111
-rw-r--r--app/assets/javascripts/jira_connect.js56
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue26
-rw-r--r--app/assets/javascripts/jira_import/utils/jira_import_utils.js2
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue16
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue14
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue9
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue34
-rw-r--r--app/assets/javascripts/jobs/components/log/line.vue2
-rw-r--r--app/assets/javascripts/jobs/components/log/line_header.vue8
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue26
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue22
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue5
-rw-r--r--app/assets/javascripts/jobs/index.js2
-rw-r--r--app/assets/javascripts/labels_select.js9
-rw-r--r--app/assets/javascripts/layout_nav.js13
-rw-r--r--app/assets/javascripts/lib/graphql.js33
-rw-r--r--app/assets/javascripts/lib/utils/axios_startup_calls.js73
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js38
-rw-r--r--app/assets/javascripts/lib/utils/forms.js37
-rw-r--r--app/assets/javascripts/lib/utils/image_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/jquery_at_who.js3
-rw-r--r--app/assets/javascripts/lib/utils/poll.js6
-rw-r--r--app/assets/javascripts/lib/utils/set.js1
-rw-r--r--app/assets/javascripts/lib/utils/simple_poll.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js77
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js75
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js1
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js27
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js1
-rw-r--r--app/assets/javascripts/line_highlighter.js1
-rw-r--r--app/assets/javascripts/logs/stores/getters.js14
-rw-r--r--app/assets/javascripts/main.js31
-rw-r--r--app/assets/javascripts/members.js3
-rw-r--r--app/assets/javascripts/merge_request.js8
-rw-r--r--app/assets/javascripts/merge_request_tabs.js1
-rw-r--r--app/assets/javascripts/milestone_select.js162
-rw-r--r--app/assets/javascripts/milestones/project_milestone_combobox.vue76
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget_form.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/charts/empty_chart.vue1
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue50
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue30
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue48
-rw-r--r--app/assets/javascripts/monitoring/components/embeds/embed_group.vue11
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue1
-rw-r--r--app/assets/javascripts/monitoring/components/refresh_button.vue26
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue7
-rw-r--r--app/assets/javascripts/monitoring/csv_export.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/actions.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/getters.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js1
-rw-r--r--app/assets/javascripts/mr_popover/components/mr_popover.vue2
-rw-r--r--app/assets/javascripts/namespace_select.js4
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue1
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue1
-rw-r--r--app/assets/javascripts/notes.js5
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue51
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue1
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue3
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue13
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue6
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue80
-rw-r--r--app/assets/javascripts/notes/components/note_actions/reply_button.vue18
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue6
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue5
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue14
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue8
-rw-r--r--app/assets/javascripts/notes/constants.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js22
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js3
-rw-r--r--app/assets/javascripts/packages/details/components/additional_metadata.vue2
-rw-r--r--app/assets/javascripts/packages/details/components/app.vue63
-rw-r--r--app/assets/javascripts/packages/details/components/code_instruction.vue63
-rw-r--r--app/assets/javascripts/packages/details/components/composer_installation.vue18
-rw-r--r--app/assets/javascripts/packages/details/components/conan_installation.vue16
-rw-r--r--app/assets/javascripts/packages/details/components/dependency_row.vue2
-rw-r--r--app/assets/javascripts/packages/details/components/maven_installation.vue17
-rw-r--r--app/assets/javascripts/packages/details/components/npm_installation.vue17
-rw-r--r--app/assets/javascripts/packages/details/components/nuget_installation.vue15
-rw-r--r--app/assets/javascripts/packages/details/components/package_history.vue24
-rw-r--r--app/assets/javascripts/packages/details/components/package_title.vue112
-rw-r--r--app/assets/javascripts/packages/details/components/pypi_installation.vue12
-rw-r--r--app/assets/javascripts/packages/details/store/actions.js13
-rw-r--r--app/assets/javascripts/packages/details/store/getters.js4
-rw-r--r--app/assets/javascripts/packages/details/store/index.js6
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list.vue6
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list_app.vue20
-rw-r--r--app/assets/javascripts/packages/list/constants.js1
-rw-r--r--app/assets/javascripts/packages/list/stores/actions.js2
-rw-r--r--app/assets/javascripts/packages/shared/components/package_list_row.vue54
-rw-r--r--app/assets/javascripts/packages/shared/components/package_tags.vue6
-rw-r--r--app/assets/javascripts/packages/shared/components/packages_list_loader.vue58
-rw-r--r--app/assets/javascripts/packages/shared/components/publish_method.vue22
-rw-r--r--app/assets/javascripts/packages/shared/constants.js5
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_previewer.js4
-rw-r--r--app/assets/javascripts/pages/admin/clusters/new/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/cohorts/index.js22
-rw-r--r--app/assets/javascripts/pages/admin/dev_ops_report/index.js27
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue69
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/index.js21
-rw-r--r--app/assets/javascripts/pages/admin/services/index/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue46
-rw-r--r--app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue1
-rw-r--r--app/assets/javascripts/pages/constants.js2
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue36
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js4
-rw-r--r--app/assets/javascripts/pages/groups/clusters/new/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js2
-rw-r--r--app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js3
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue6
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js31
-rw-r--r--app/assets/javascripts/pages/projects/branches/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js47
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/new/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js30
-rw-r--r--app/assets/javascripts/pages/projects/constants.js2
-rw-r--r--app/assets/javascripts/pages/projects/environments/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue4
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js269
-rw-r--r--app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js22
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js4
-rw-r--r--app/assets/javascripts/pages/projects/project.js86
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue7
-rw-r--r--app/assets/javascripts/pages/search/show/index.js6
-rw-r--r--app/assets/javascripts/pages/search/show/search.js6
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue11
-rw-r--r--app/assets/javascripts/pdf/page/index.vue10
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue6
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue1
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue1
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue1
-rw-r--r--app/assets/javascripts/performance_bar/index.js30
-rw-r--r--app/assets/javascripts/performance_bar/performance_bar_log.js28
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js18
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js5
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue91
-rw-r--r--app/assets/javascripts/pipeline_new/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue19
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue65
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue76
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue57
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue35
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue22
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue29
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/stage.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue99
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue19
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue122
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js22
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js4
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/utils.js26
-rw-r--r--app/assets/javascripts/pipelines/utils.js44
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue95
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue1
-rw-r--r--app/assets/javascripts/profile/account/index.js3
-rw-r--r--app/assets/javascripts/project_select.js11
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue38
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue4
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue2
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue1
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue1
-rw-r--r--app/assets/javascripts/projects/project_new.js9
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js3
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue5
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue18
-rw-r--r--app/assets/javascripts/prometheus_alerts/components/reset_key.vue1
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js3
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js2
-rw-r--r--app/assets/javascripts/ref/components/ref_results_section.vue14
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue44
-rw-r--r--app/assets/javascripts/ref_select_dropdown.js4
-rw-r--r--app/assets/javascripts/registry/explorer/components/delete_button.vue1
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue11
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue1
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue4
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue102
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list.vue1
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue2
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue66
-rw-r--r--app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue6
-rw-r--r--app/assets/javascripts/registry/explorer/stores/index.js1
-rw-r--r--app/assets/javascripts/registry/explorer/utils.js1
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue13
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue207
-rw-r--r--app/assets/javascripts/related_issues/components/issue_token.vue115
-rw-r--r--app/assets/javascripts/related_issues/components/related_issuable_input.vue231
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue215
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_list.vue146
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_root.vue247
-rw-r--r--app/assets/javascripts/related_issues/constants.js106
-rw-r--r--app/assets/javascripts/related_issues/index.js27
-rw-r--r--app/assets/javascripts/related_issues/services/related_issues_service.js34
-rw-r--r--app/assets/javascripts/related_issues/stores/related_issues_store.js50
-rw-r--r--app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue7
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue3
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue39
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue2
-rw-r--r--app/assets/javascripts/releases/components/evidence_block.vue4
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue1
-rw-r--r--app/assets/javascripts/releases/components/release_block_assets.vue8
-rw-r--r--app/assets/javascripts/releases/components/release_block_footer.vue9
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue9
-rw-r--r--app/assets/javascripts/releases/components/release_block_metadata.vue9
-rw-r--r--app/assets/javascripts/releases/components/release_block_milestones.vue7
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination.vue20
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_graphql.vue35
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_rest.vue24
-rw-r--r--app/assets/javascripts/releases/mount_edit.js3
-rw-r--r--app/assets/javascripts/releases/mount_index.js17
-rw-r--r--app/assets/javascripts/releases/mount_new.js3
-rw-r--r--app/assets/javascripts/releases/mount_show.js3
-rw-r--r--app/assets/javascripts/releases/queries/all_releases.query.graphql69
-rw-r--r--app/assets/javascripts/releases/stores/index.js3
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/actions.js30
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/index.js8
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/state.js14
-rw-r--r--app/assets/javascripts/releases/util.js89
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/index.js15
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/index.js15
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue6
-rw-r--r--app/assets/javascripts/reports/store/getters.js1
-rw-r--r--app/assets/javascripts/reports/store/index.js15
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue8
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue8
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue1
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue26
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue3
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue19
-rw-r--r--app/assets/javascripts/repository/components/web_ide_link.vue47
-rw-r--r--app/assets/javascripts/repository/graphql.js1
-rw-r--r--app/assets/javascripts/repository/index.js28
-rw-r--r--app/assets/javascripts/repository/log_tree.js13
-rw-r--r--app/assets/javascripts/repository/router.js3
-rw-r--r--app/assets/javascripts/repository/utils/commit.js1
-rw-r--r--app/assets/javascripts/repository/utils/icon.js1
-rw-r--r--app/assets/javascripts/repository/utils/readme.js1
-rw-r--r--app/assets/javascripts/search/state_filter/components/state_filter.vue94
-rw-r--r--app/assets/javascripts/search/state_filter/constants.js39
-rw-r--r--app/assets/javascripts/search/state_filter/index.js34
-rw-r--r--app/assets/javascripts/search_autocomplete.js12
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue1
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue1
-rw-r--r--app/assets/javascripts/set_status_modal/event_hub.js3
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue27
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue20
-rw-r--r--app/assets/javascripts/shared/milestones/form.js1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue37
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue97
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/severity/constants.js41
-rw-r--r--app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql9
-rw-r--r--app/assets/javascripts/sidebar/components/severity/severity.vue42
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue187
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue8
-rw-r--r--app/assets/javascripts/sidebar/event_hub.js4
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js4
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js61
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql7
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js7
-rw-r--r--app/assets/javascripts/snippet/snippet_edit.js1
-rw-r--r--app/assets/javascripts/snippet/snippet_show.js2
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue36
-rw-r--r--app/assets/javascripts/snippets/components/embed_dropdown.vue78
-rw-r--r--app/assets/javascripts/snippets/components/show.vue12
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue7
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_view.vue1
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue7
-rw-r--r--app/assets/javascripts/snippets/components/snippet_visibility_edit.vue50
-rw-r--r--app/assets/javascripts/snippets/constants.js12
-rw-r--r--app/assets/javascripts/snippets/index.js18
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js1
-rw-r--r--app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql5
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js15
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue43
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_drawer.vue32
-rw-r--r--app/assets/javascripts/static_site_editor/components/front_matter_controls.vue57
-rw-r--r--app/assets/javascripts/static_site_editor/components/publish_toolbar.vue16
-rw-r--r--app/assets/javascripts/static_site_editor/services/formatter.js44
-rw-r--r--app/assets/javascripts/static_site_editor/services/image_service.js1
-rw-r--r--app/assets/javascripts/static_site_editor/services/parse_source_file.js66
-rw-r--r--app/assets/javascripts/subscription_select.js3
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js4
-rw-r--r--app/assets/javascripts/tooltips/components/tooltips.vue116
-rw-r--r--app/assets/javascripts/tooltips/index.js120
-rw-r--r--app/assets/javascripts/tracking.js12
-rw-r--r--app/assets/javascripts/ui_development_kit.js3
-rw-r--r--app/assets/javascripts/users_select/index.js197
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue77
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue66
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue106
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue51
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js63
-rw-r--r--app/assets/javascripts/vue_shared/components/actions_button.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/alert_details_table.vue70
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_alert.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/item.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js166
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue115
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue154
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_mentions.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue72
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue36
-rw-r--r--app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue48
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/code_instruction.vue82
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/details_row.vue (renamed from app/assets/javascripts/registry/shared/components/details_row.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/history_item.vue (renamed from app/assets/javascripts/packages/details/components/history_element.vue)7
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue (renamed from app/assets/javascripts/registry/explorer/components/list_item.vue)51
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/metadata_item.vue63
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/title_area.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/resizable_chart/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue64
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js31
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js24
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js28
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/todo_button.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/url_sync.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue118
-rw-r--r--app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js4
-rw-r--r--app/assets/javascripts/vuex_shared/bindings.js1
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/index.js6
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/state.js5
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue56
-rw-r--r--app/assets/javascripts/whats_new/components/trigger.vue19
-rw-r--r--app/assets/javascripts/whats_new/index.js46
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss65
-rw-r--r--app/assets/stylesheets/application.scss4
-rw-r--r--app/assets/stylesheets/components/batch_comments/review_bar.scss122
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss4
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss12
-rw-r--r--app/assets/stylesheets/components/rich_content_editor.scss2
-rw-r--r--app/assets/stylesheets/components/whats_new.scss9
-rw-r--r--app/assets/stylesheets/fontawesome_custom.scss48
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss20
-rw-r--r--app/assets/stylesheets/framework/callout.scss6
-rw-r--r--app/assets/stylesheets/framework/common.scss41
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss88
-rw-r--r--app/assets/stylesheets/framework/files.scss13
-rw-r--r--app/assets/stylesheets/framework/filters.scss12
-rw-r--r--app/assets/stylesheets/framework/flash.scss4
-rw-r--r--app/assets/stylesheets/framework/gfm.scss2
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss431
-rw-r--r--app/assets/stylesheets/framework/header.scss9
-rw-r--r--app/assets/stylesheets/framework/job_log.scss6
-rw-r--r--app/assets/stylesheets/framework/lists.scss17
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss7
-rw-r--r--app/assets/stylesheets/framework/selects.scss6
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss7
-rw-r--r--app/assets/stylesheets/framework/spinner.scss2
-rw-r--r--app/assets/stylesheets/framework/stacked_progress_bar.scss2
-rw-r--r--app/assets/stylesheets/framework/tables.scss34
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss82
-rw-r--r--app/assets/stylesheets/framework/wells.scss4
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/mailer.scss16
-rw-r--r--app/assets/stylesheets/notify.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss16
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss3
-rw-r--r--app/assets/stylesheets/page_bundles/_mixins_and_variables_and_functions.scss21
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss33
-rw-r--r--app/assets/stylesheets/page_bundles/todos.scss (renamed from app/assets/stylesheets/pages/todos.scss)46
-rw-r--r--app/assets/stylesheets/pages/alert_management/details.scss39
-rw-r--r--app/assets/stylesheets/pages/alert_management/severity-icons.scss1
-rw-r--r--app/assets/stylesheets/pages/boards.scss3
-rw-r--r--app/assets/stylesheets/pages/branches.scss4
-rw-r--r--app/assets/stylesheets/pages/builds.scss4
-rw-r--r--app/assets/stylesheets/pages/clusters.scss6
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss29
-rw-r--r--app/assets/stylesheets/pages/dev_ops_report.scss (renamed from app/assets/stylesheets/pages/dev_ops_score.scss)8
-rw-r--r--app/assets/stylesheets/pages/diff.scss8
-rw-r--r--app/assets/stylesheets/pages/environments.scss6
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/graph.scss2
-rw-r--r--app/assets/stylesheets/pages/groups.scss2
-rw-r--r--app/assets/stylesheets/pages/help.scss4
-rw-r--r--app/assets/stylesheets/pages/incident_management_list.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss49
-rw-r--r--app/assets/stylesheets/pages/issues.scss6
-rw-r--r--app/assets/stylesheets/pages/members.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss14
-rw-r--r--app/assets/stylesheets/pages/milestone.scss1
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss6
-rw-r--r--app/assets/stylesheets/pages/packages.scss11
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss19
-rw-r--r--app/assets/stylesheets/pages/profile.scss3
-rw-r--r--app/assets/stylesheets/pages/projects.scss18
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/settings.scss15
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss2
-rw-r--r--app/assets/stylesheets/pages/wiki.scss4
-rw-r--r--app/assets/stylesheets/performance_bar.scss8
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss1912
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss1832
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss2409
-rw-r--r--app/assets/stylesheets/themes/_dark.scss89
-rw-r--r--app/assets/stylesheets/themes/theme_blue.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_dark.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_green.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss204
-rw-r--r--app/assets/stylesheets/themes/theme_indigo.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_light.scss129
-rw-r--r--app/assets/stylesheets/themes/theme_light_blue.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_light_indigo.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_light_red.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_red.scss14
-rw-r--r--app/assets/stylesheets/utilities.scss43
-rw-r--r--app/assets/stylesheets/vendors/atwho.scss6
-rw-r--r--app/assets/stylesheets/vendors/tribute.scss4
-rw-r--r--app/controllers/admin/application_settings_controller.rb3
-rw-r--r--app/controllers/admin/cohorts_controller.rb (renamed from app/controllers/instance_statistics/cohorts_controller.rb)8
-rw-r--r--app/controllers/admin/concerns/authenticates_2fa_for_admin_mode.rb50
-rw-r--r--app/controllers/admin/dev_ops_report_controller.rb13
-rw-r--r--app/controllers/admin/groups_controller.rb6
-rw-r--r--app/controllers/admin/instance_statistics_controller.rb16
-rw-r--r--app/controllers/admin/integrations_controller.rb4
-rw-r--r--app/controllers/admin/plan_limits_controller.rb39
-rw-r--r--app/controllers/admin/runners_controller.rb1
-rw-r--r--app/controllers/admin/services_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb17
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/clusters/clusters_controller.rb1
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb79
-rw-r--r--app/controllers/concerns/integrations_actions.rb11
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/issuable_links.rb41
-rw-r--r--app/controllers/concerns/notes_actions.rb2
-rw-r--r--app/controllers/concerns/redis_tracking.rb47
-rw-r--r--app/controllers/concerns/renders_notes.rb10
-rw-r--r--app/controllers/concerns/send_file_upload.rb38
-rw-r--r--app/controllers/concerns/snippets_actions.rb8
-rw-r--r--app/controllers/concerns/wiki_actions.rb30
-rw-r--r--app/controllers/dashboard/projects_controller.rb7
-rw-r--r--app/controllers/graphql_controller.rb10
-rw-r--r--app/controllers/groups/boards_controller.rb1
-rw-r--r--app/controllers/groups/settings/integrations_controller.rb12
-rw-r--r--app/controllers/instance_statistics/application_controller.rb10
-rw-r--r--app/controllers/instance_statistics/dev_ops_score_controller.rb13
-rw-r--r--app/controllers/invites_controller.rb23
-rw-r--r--app/controllers/jira_connect/app_descriptor_controller.rb64
-rw-r--r--app/controllers/jira_connect/application_controller.rb57
-rw-r--r--app/controllers/jira_connect/events_controller.rb30
-rw-r--r--app/controllers/jira_connect/subscriptions_controller.rb56
-rw-r--r--app/controllers/oauth/jira/authorizations_controller.rb49
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb4
-rw-r--r--app/controllers/passwords_controller.rb6
-rw-r--r--app/controllers/profiles/accounts_controller.rb10
-rw-r--r--app/controllers/profiles/keys_controller.rb21
-rw-r--r--app/controllers/profiles/notifications_controller.rb15
-rw-r--r--app/controllers/profiles/preferences_controller.rb1
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb98
-rw-r--r--app/controllers/profiles/webauthn_registrations_controller.rb10
-rw-r--r--app/controllers/profiles_controller.rb4
-rw-r--r--app/controllers/projects/application_controller.rb4
-rw-r--r--app/controllers/projects/badges_controller.rb1
-rw-r--r--app/controllers/projects/blob_controller.rb12
-rw-r--r--app/controllers/projects/boards_controller.rb1
-rw-r--r--app/controllers/projects/ci/lints_controller.rb42
-rw-r--r--app/controllers/projects/forks_controller.rb13
-rw-r--r--app/controllers/projects/imports_controller.rb6
-rw-r--r--app/controllers/projects/issue_links_controller.rb48
-rw-r--r--app/controllers/projects/issues_controller.rb43
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests/content_controller.rb6
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb28
-rw-r--r--app/controllers/projects/metrics_dashboard_controller.rb31
-rw-r--r--app/controllers/projects/pages_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb27
-rw-r--r--app/controllers/projects/product_analytics_controller.rb4
-rw-r--r--app/controllers/projects/releases_controller.rb3
-rw-r--r--app/controllers/projects/services_controller.rb14
-rw-r--r--app/controllers/projects/static_site_editor_controller.rb18
-rw-r--r--app/controllers/projects/todos_controller.rb3
-rw-r--r--app/controllers/projects/web_ide_schemas_controller.rb25
-rw-r--r--app/controllers/projects/web_ide_terminals_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb6
-rw-r--r--app/controllers/registrations/experience_levels_controller.rb1
-rw-r--r--app/controllers/registrations_controller.rb31
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb2
-rw-r--r--app/controllers/search_controller.rb18
-rw-r--r--app/controllers/sessions_controller.rb15
-rw-r--r--app/controllers/users_controller.rb6
-rw-r--r--app/finders/ci/jobs_finder.rb55
-rw-r--r--app/finders/concerns/finder_methods.rb11
-rw-r--r--app/finders/concerns/merged_at_filter.rb12
-rw-r--r--app/finders/design_management/designs_finder.rb6
-rw-r--r--app/finders/feature_flags_finder.rb44
-rw-r--r--app/finders/fork_targets_finder.rb6
-rw-r--r--app/finders/group_members_finder.rb4
-rw-r--r--app/finders/issuable_finder.rb30
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/labels_finder.rb9
-rw-r--r--app/finders/members_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb4
-rw-r--r--app/finders/packages/group_packages_finder.rb3
-rw-r--r--app/finders/packages/package_finder.rb3
-rw-r--r--app/finders/packages/packages_finder.rb7
-rw-r--r--app/finders/projects_finder.rb1
-rw-r--r--app/finders/user_group_notification_settings_finder.rb48
-rw-r--r--app/finders/users_finder.rb8
-rw-r--r--app/graphql/gitlab_schema.rb4
-rw-r--r--app/graphql/mutations/alert_management/alerts/set_assignees.rb2
-rw-r--r--app/graphql/mutations/alert_management/alerts/todo/create.rb2
-rw-r--r--app/graphql/mutations/alert_management/base.rb1
-rw-r--r--app/graphql/mutations/alert_management/create_alert_issue.rb2
-rw-r--r--app/graphql/mutations/alert_management/update_alert_status.rb2
-rw-r--r--app/graphql/mutations/award_emojis/toggle.rb2
-rw-r--r--app/graphql/mutations/base_mutation.rb4
-rw-r--r--app/graphql/mutations/boards/destroy.rb37
-rw-r--r--app/graphql/mutations/boards/issues/issue_move_list.rb8
-rw-r--r--app/graphql/mutations/boards/lists/base.rb2
-rw-r--r--app/graphql/mutations/boards/lists/create.rb9
-rw-r--r--app/graphql/mutations/ci/base.rb17
-rw-r--r--app/graphql/mutations/ci/pipeline_cancel.rb22
-rw-r--r--app/graphql/mutations/ci/pipeline_destroy.rb22
-rw-r--r--app/graphql/mutations/ci/pipeline_retry.rb27
-rw-r--r--app/graphql/mutations/concerns/mutations/authorizes_project.rb17
-rw-r--r--app/graphql/mutations/design_management/move.rb4
-rw-r--r--app/graphql/mutations/issues/set_severity.rb25
-rw-r--r--app/graphql/mutations/merge_requests/set_wip.rb4
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/delete.rb2
-rw-r--r--app/graphql/mutations/snippets/create.rb13
-rw-r--r--app/graphql/mutations/snippets/update.rb13
-rw-r--r--app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb37
-rw-r--r--app/graphql/resolvers/board_list_issues_resolver.rb10
-rw-r--r--app/graphql/resolvers/concerns/board_issue_filterable.rb24
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb (renamed from app/graphql/resolvers/concerns/issue_resolver_fields.rb)6
-rw-r--r--app/graphql/resolvers/concerns/looks_ahead.rb2
-rw-r--r--app/graphql/resolvers/group_members_resolver.rb19
-rw-r--r--app/graphql/resolvers/issue_status_counts_resolver.rb19
-rw-r--r--app/graphql/resolvers/issues_resolver.rb14
-rw-r--r--app/graphql/resolvers/members_resolver.rb24
-rw-r--r--app/graphql/resolvers/merge_request_pipelines_resolver.rb2
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb15
-rw-r--r--app/graphql/resolvers/metrics/dashboard_resolver.rb3
-rw-r--r--app/graphql/resolvers/namespace_projects_resolver.rb22
-rw-r--r--app/graphql/resolvers/project_members_resolver.rb16
-rw-r--r--app/graphql/resolvers/project_merge_requests_resolver.rb12
-rw-r--r--app/graphql/resolvers/projects_resolver.rb10
-rw-r--r--app/graphql/resolvers/user_starred_projects_resolver.rb17
-rw-r--r--app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb21
-rw-r--r--app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb24
-rw-r--r--app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb2
-rw-r--r--app/graphql/types/alert_management/alert_type.rb2
-rw-r--r--app/graphql/types/award_emojis/award_emoji_type.rb2
-rw-r--r--app/graphql/types/base_enum.rb3
-rw-r--r--app/graphql/types/base_field.rb53
-rw-r--r--app/graphql/types/board_list_type.rb2
-rw-r--r--app/graphql/types/boards/board_issue_input_base_type.rb35
-rw-r--r--app/graphql/types/boards/board_issue_input_type.rb24
-rw-r--r--app/graphql/types/ci/pipeline_config_source_enum.rb2
-rw-r--r--app/graphql/types/ci/pipeline_type.rb10
-rw-r--r--app/graphql/types/concerns/gitlab_style_deprecations.rb31
-rw-r--r--app/graphql/types/current_user_todos.rb24
-rw-r--r--app/graphql/types/design_management/design_at_version_type.rb4
-rw-r--r--app/graphql/types/design_management/design_collection_type.rb2
-rw-r--r--app/graphql/types/design_management/design_type.rb1
-rw-r--r--app/graphql/types/error_tracking/sentry_detailed_error_type.rb2
-rw-r--r--app/graphql/types/error_tracking/sentry_error_collection_type.rb4
-rw-r--r--app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb2
-rw-r--r--app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb2
-rw-r--r--app/graphql/types/error_tracking/sentry_error_type.rb2
-rw-r--r--app/graphql/types/group_member_type.rb2
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/graphql/types/issuable_severity_enum.rb12
-rw-r--r--app/graphql/types/issue_status_counts_type.rb2
-rw-r--r--app/graphql/types/issue_type.rb15
-rw-r--r--app/graphql/types/jira_user_type.rb2
-rw-r--r--app/graphql/types/member_interface.rb19
-rw-r--r--app/graphql/types/merge_request_sort_enum.rb11
-rw-r--r--app/graphql/types/merge_request_type.rb8
-rw-r--r--app/graphql/types/metrics/dashboard_type.rb7
-rw-r--r--app/graphql/types/milestone_type.rb4
-rw-r--r--app/graphql/types/mutation_operation_mode_enum.rb2
-rw-r--r--app/graphql/types/mutation_type.rb5
-rw-r--r--app/graphql/types/permission_types/merge_request.rb4
-rw-r--r--app/graphql/types/project_member_type.rb9
-rw-r--r--app/graphql/types/project_type.rb8
-rw-r--r--app/graphql/types/projects/namespace_project_sort_enum.rb12
-rw-r--r--app/graphql/types/query_type.rb15
-rw-r--r--app/graphql/types/release_asset_link_type.rb12
-rw-r--r--app/graphql/types/release_type.rb4
-rw-r--r--app/graphql/types/todo_type.rb2
-rw-r--r--app/graphql/types/user_type.rb3
-rw-r--r--app/helpers/application_helper.rb8
-rw-r--r--app/helpers/application_settings_helper.rb7
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb7
-rw-r--r--app/helpers/boards_helper.rb4
-rw-r--r--app/helpers/ci/jobs_helper.rb1
-rw-r--r--app/helpers/ci/pipelines_helper.rb33
-rw-r--r--app/helpers/clusters_helper.rb6
-rw-r--r--app/helpers/container_registry_helper.rb8
-rw-r--r--app/helpers/dashboard_helper.rb4
-rw-r--r--app/helpers/deploy_tokens_helper.rb9
-rw-r--r--app/helpers/dev_ops_report_helper.rb (renamed from app/helpers/dev_ops_score_helper.rb)2
-rw-r--r--app/helpers/diff_helper.rb37
-rw-r--r--app/helpers/dropdowns_helper.rb44
-rw-r--r--app/helpers/emails_helper.rb45
-rw-r--r--app/helpers/environments_helper.rb78
-rw-r--r--app/helpers/form_helper.rb23
-rw-r--r--app/helpers/groups/group_members_helper.rb79
-rw-r--r--app/helpers/groups_helper.rb15
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb15
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/helpers/lazy_image_tag_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb6
-rw-r--r--app/helpers/notes_helper.rb6
-rw-r--r--app/helpers/notifications_helper.rb10
-rw-r--r--app/helpers/operations_helper.rb1
-rw-r--r--app/helpers/preferences_helper.rb11
-rw-r--r--app/helpers/projects_helper.rb6
-rw-r--r--app/helpers/releases_helper.rb1
-rw-r--r--app/helpers/search_helper.rb41
-rw-r--r--app/helpers/services_helper.rb28
-rw-r--r--app/helpers/snippets_helper.rb2
-rw-r--r--app/helpers/startup_css_helper.rb7
-rw-r--r--app/helpers/submodule_helper.rb54
-rw-r--r--app/helpers/system_note_helper.rb5
-rw-r--r--app/helpers/tab_helper.rb2
-rw-r--r--app/helpers/tree_helper.rb34
-rw-r--r--app/helpers/user_callouts_helper.rb10
-rw-r--r--app/helpers/whats_new_helper.rb24
-rw-r--r--app/helpers/wiki_helper.rb9
-rw-r--r--app/mailers/devise_mailer.rb4
-rw-r--r--app/mailers/emails/members.rb40
-rw-r--r--app/mailers/emails/profile.rb10
-rw-r--r--app/models/alert_management/alert.rb43
-rw-r--r--app/models/analytics/instance_statistics.rb9
-rw-r--r--app/models/analytics/instance_statistics/measurement.rb31
-rw-r--r--app/models/application_record.rb4
-rw-r--r--app/models/application_setting.rb19
-rw-r--r--app/models/application_setting_implementation.rb7
-rw-r--r--app/models/atlassian/identity.rb26
-rw-r--r--app/models/audit_event.rb12
-rw-r--r--app/models/authentication_event.rb12
-rw-r--r--app/models/blob_viewer/dependency_manager.rb4
-rw-r--r--app/models/blob_viewer/metrics_dashboard_yml.rb26
-rw-r--r--app/models/ci/bridge.rb12
-rw-r--r--app/models/ci/build.rb52
-rw-r--r--app/models/ci/build_pending_state.rb12
-rw-r--r--app/models/ci/build_trace_chunk.rb88
-rw-r--r--app/models/ci/build_trace_chunks/database.rb2
-rw-r--r--app/models/ci/build_trace_chunks/redis.rb4
-rw-r--r--app/models/ci/daily_build_group_report_result.rb2
-rw-r--r--app/models/ci/job_artifact.rb17
-rw-r--r--app/models/ci/pipeline.rb98
-rw-r--r--app/models/ci/pipeline_artifact.rb29
-rw-r--r--app/models/ci/pipeline_enums.rb73
-rw-r--r--app/models/ci/ref.rb2
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/ci_platform_metric.rb39
-rw-r--r--app/models/clusters/agent.rb2
-rw-r--r--app/models/clusters/applications/prometheus.rb6
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb1
-rw-r--r--app/models/clusters/instance.rb2
-rw-r--r--app/models/clusters/providers/aws.rb2
-rw-r--r--app/models/commit.rb6
-rw-r--r--app/models/commit_status.rb15
-rw-r--r--app/models/commit_status_enums.rb32
-rw-r--r--app/models/concerns/admin_changed_password_notifier.rb60
-rw-r--r--app/models/concerns/bulk_member_access_load.rb2
-rw-r--r--app/models/concerns/checksummable.rb6
-rw-r--r--app/models/concerns/ci/artifactable.rb20
-rw-r--r--app/models/concerns/discussion_on_diff.rb2
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb74
-rw-r--r--app/models/concerns/enums/commit_status.rb35
-rw-r--r--app/models/concerns/enums/internal_id.rb (renamed from app/models/internal_id_enums.rb)14
-rw-r--r--app/models/concerns/enums/prometheus_metric.rb91
-rw-r--r--app/models/concerns/from_except.rb37
-rw-r--r--app/models/concerns/from_intersect.rb37
-rw-r--r--app/models/concerns/from_set_operator.rb19
-rw-r--r--app/models/concerns/from_union.rb16
-rw-r--r--app/models/concerns/has_wiki.rb4
-rw-r--r--app/models/concerns/id_in_ordered.rb20
-rw-r--r--app/models/concerns/issuable.rb39
-rw-r--r--app/models/concerns/loaded_in_group_list.rb3
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb2
-rw-r--r--app/models/concerns/milestoneable.rb2
-rw-r--r--app/models/concerns/optimized_issuable_label_filter.rb107
-rw-r--r--app/models/concerns/prometheus_adapter.rb11
-rw-r--r--app/models/concerns/relative_positioning.rb424
-rw-r--r--app/models/concerns/resolvable_discussion.rb1
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb16
-rw-r--r--app/models/concerns/timebox.rb8
-rw-r--r--app/models/cycle_analytics/level_base.rb2
-rw-r--r--app/models/data_list.rb10
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/design_management/design.rb14
-rw-r--r--app/models/design_management/design_collection.rb60
-rw-r--r--app/models/dev_ops_report/card.rb (renamed from app/models/dev_ops_score/card.rb)2
-rw-r--r--app/models/dev_ops_report/idea_to_production_step.rb (renamed from app/models/dev_ops_score/idea_to_production_step.rb)2
-rw-r--r--app/models/dev_ops_report/metric.rb (renamed from app/models/dev_ops_score/metric.rb)2
-rw-r--r--app/models/diff_discussion.rb1
-rw-r--r--app/models/discussion.rb1
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/group.rb23
-rw-r--r--app/models/group_deploy_key.rb4
-rw-r--r--app/models/internal_id.rb2
-rw-r--r--app/models/issuable_severity.rb18
-rw-r--r--app/models/issue.rb42
-rw-r--r--app/models/issue_link.rb38
-rw-r--r--app/models/iteration.rb8
-rw-r--r--app/models/jira_connect_installation.rb22
-rw-r--r--app/models/jira_connect_subscription.rb12
-rw-r--r--app/models/jira_import_state.rb4
-rw-r--r--app/models/lfs_objects_project.rb1
-rw-r--r--app/models/member.rb13
-rw-r--r--app/models/members/group_member.rb9
-rw-r--r--app/models/members_preloader.rb1
-rw-r--r--app/models/merge_request.rb79
-rw-r--r--app/models/merge_request_assignee.rb2
-rw-r--r--app/models/merge_request_diff.rb23
-rw-r--r--app/models/merge_request_diff_file.rb12
-rw-r--r--app/models/merge_request_reviewer.rb6
-rw-r--r--app/models/namespace.rb8
-rw-r--r--app/models/namespace/root_storage_statistics.rb14
-rw-r--r--app/models/note.rb45
-rw-r--r--app/models/operations/feature_flag.rb101
-rw-r--r--app/models/operations/feature_flag_scope.rb62
-rw-r--r--app/models/operations/feature_flags/scope.rb13
-rw-r--r--app/models/operations/feature_flags/strategy.rb94
-rw-r--r--app/models/operations/feature_flags/strategy_user_list.rb12
-rw-r--r--app/models/operations/feature_flags/user_list.rb36
-rw-r--r--app/models/operations/feature_flags_client.rb25
-rw-r--r--app/models/packages/conan/file_metadatum.rb3
-rw-r--r--app/models/packages/package.rb14
-rw-r--r--app/models/packages/pypi/metadatum.rb1
-rw-r--r--app/models/pages/lookup_path.rb32
-rw-r--r--app/models/pages_deployment.rb11
-rw-r--r--app/models/pages_domain.rb6
-rw-r--r--app/models/performance_monitoring/prometheus_dashboard.rb11
-rw-r--r--app/models/product_analytics_event.rb9
-rw-r--r--app/models/project.rb147
-rw-r--r--app/models/project_feature_usage.rb31
-rw-r--r--app/models/project_pages_metadatum.rb1
-rw-r--r--app/models/project_services/chat_message/merge_message.rb16
-rw-r--r--app/models/project_services/ewm_service.rb29
-rw-r--r--app/models/project_services/jira_service.rb32
-rw-r--r--app/models/project_services/prometheus_service.rb10
-rw-r--r--app/models/project_statistics.rb12
-rw-r--r--app/models/project_team.rb34
-rw-r--r--app/models/project_wiki.rb17
-rw-r--r--app/models/prometheus_metric.rb6
-rw-r--r--app/models/prometheus_metric_enums.rb84
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/remote_mirror.rb4
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/resource_iteration_event.rb5
-rw-r--r--app/models/resource_state_event.rb2
-rw-r--r--app/models/security_event.rb4
-rw-r--r--app/models/service.rb352
-rw-r--r--app/models/service_list.rb14
-rw-r--r--app/models/snippet.rb17
-rw-r--r--app/models/snippet_input_action.rb2
-rw-r--r--app/models/snippet_repository.rb4
-rw-r--r--app/models/snippet_statistics.rb2
-rw-r--r--app/models/system_note_metadata.rb3
-rw-r--r--app/models/terraform/state.rb24
-rw-r--r--app/models/terraform/state_version.rb18
-rw-r--r--app/models/timelog.rb1
-rw-r--r--app/models/todo.rb6
-rw-r--r--app/models/user.rb43
-rw-r--r--app/models/user_callout.rb27
-rw-r--r--app/models/user_callout_enums.rb28
-rw-r--r--app/models/vulnerability.rb17
-rw-r--r--app/models/wiki.rb26
-rw-r--r--app/policies/ci/build_policy.rb7
-rw-r--r--app/policies/global_policy.rb5
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/issuable_policy.rb1
-rw-r--r--app/policies/namespace_policy.rb1
-rw-r--r--app/policies/operations/feature_flag_policy.rb7
-rw-r--r--app/policies/project_policy.rb18
-rw-r--r--app/policies/user_policy.rb1
-rw-r--r--app/presenters/alert_management/alert_presenter.rb63
-rw-r--r--app/presenters/alert_management/prometheus_alert_presenter.rb33
-rw-r--r--app/presenters/ci/pipeline_artifacts/code_coverage_presenter.rb25
-rw-r--r--app/presenters/clusters/cluster_presenter.rb2
-rw-r--r--app/presenters/commit_status_presenter.rb3
-rw-r--r--app/presenters/dev_ops_score/metric_presenter.rb2
-rw-r--r--app/presenters/packages/conan/package_presenter.rb71
-rw-r--r--app/presenters/packages/nuget/search_results_presenter.rb2
-rw-r--r--app/presenters/project_presenter.rb13
-rw-r--r--app/presenters/projects/prometheus/alert_presenter.rb25
-rw-r--r--app/serializers/base_serializer.rb2
-rw-r--r--app/serializers/build_coverage_entity.rb5
-rw-r--r--app/serializers/build_details_entity.rb6
-rw-r--r--app/serializers/ci/lint/job_entity.rb15
-rw-r--r--app/serializers/ci/lint/result_entity.rb12
-rw-r--r--app/serializers/ci/lint/result_serializer.rb5
-rw-r--r--app/serializers/cluster_entity.rb4
-rw-r--r--app/serializers/cluster_serializer.rb1
-rw-r--r--app/serializers/diff_file_base_entity.rb24
-rw-r--r--app/serializers/diff_file_entity.rb4
-rw-r--r--app/serializers/environment_entity.rb6
-rw-r--r--app/serializers/fork_namespace_entity.rb6
-rw-r--r--app/serializers/group_group_link_entity.rb26
-rw-r--r--app/serializers/group_group_link_serializer.rb5
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb4
-rw-r--r--app/serializers/issue_sidebar_basic_entity.rb1
-rw-r--r--app/serializers/job_entity.rb3
-rw-r--r--app/serializers/linked_issue_entity.rb37
-rw-r--r--app/serializers/linked_project_issue_entity.rb29
-rw-r--r--app/serializers/linked_project_issue_serializer.rb5
-rw-r--r--app/serializers/merge_request_basic_entity.rb1
-rw-r--r--app/serializers/merge_request_poll_cached_widget_entity.rb10
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb2
-rw-r--r--app/serializers/merge_request_reviewer_entity.rb9
-rw-r--r--app/serializers/merge_request_sidebar_extras_entity.rb4
-rw-r--r--app/serializers/merge_request_widget_entity.rb1
-rw-r--r--app/serializers/note_entity.rb4
-rw-r--r--app/serializers/pipeline_entity.rb4
-rw-r--r--app/serializers/project_note_entity.rb8
-rw-r--r--app/serializers/test_suite_entity.rb2
-rw-r--r--app/services/admin/propagate_integration_service.rb88
-rw-r--r--app/services/admin/propagate_service_template.rb19
-rw-r--r--app/services/alert_management/create_alert_issue_service.rb3
-rw-r--r--app/services/alert_management/process_prometheus_alert_service.rb89
-rw-r--r--app/services/audit_event_service.rb38
-rw-r--r--app/services/auto_merge/base_service.rb15
-rw-r--r--app/services/auto_merge/merge_when_pipeline_succeeds_service.rb2
-rw-r--r--app/services/boards/destroy_service.rb8
-rw-r--r--app/services/boards/issues/move_service.rb4
-rw-r--r--app/services/branches/delete_service.rb2
-rw-r--r--app/services/ci/archive_trace_service.rb1
-rw-r--r--app/services/ci/cancel_user_pipelines_service.rb4
-rw-r--r--app/services/ci/create_downstream_pipeline_service.rb (renamed from app/services/ci/create_cross_project_pipeline_service.rb)28
-rw-r--r--app/services/ci/create_job_artifacts_service.rb4
-rw-r--r--app/services/ci/create_web_ide_terminal_service.rb2
-rw-r--r--app/services/ci/destroy_expired_job_artifacts_service.rb10
-rw-r--r--app/services/ci/destroy_pipeline_service.rb4
-rw-r--r--app/services/ci/generate_coverage_reports_service.rb2
-rw-r--r--app/services/ci/parse_dotenv_artifact_service.rb2
-rw-r--r--app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb9
-rw-r--r--app/services/ci/pipelines/create_artifact_service.rb33
-rw-r--r--app/services/ci/process_pipeline_service.rb10
-rw-r--r--app/services/ci/retry_build_service.rb25
-rw-r--r--app/services/ci/retry_pipeline_service.rb2
-rw-r--r--app/services/ci/update_build_state_service.rb128
-rw-r--r--app/services/ci/update_ci_ref_status_service.rb66
-rw-r--r--app/services/clusters/aws/provision_service.rb1
-rw-r--r--app/services/concerns/admin/propagate_service.rb73
-rw-r--r--app/services/concerns/incident_management/settings.rb4
-rw-r--r--app/services/concerns/incident_management/usage_data.rb18
-rw-r--r--app/services/concerns/measurable.rb2
-rw-r--r--app/services/concerns/merge_requests/removes_refs.rb9
-rw-r--r--app/services/design_management/move_designs_service.rb1
-rw-r--r--app/services/event_create_service.rb28
-rw-r--r--app/services/git/branch_hooks_service.rb12
-rw-r--r--app/services/git/wiki_push_service.rb21
-rw-r--r--app/services/git/wiki_push_service/change.rb6
-rw-r--r--app/services/ide/base_config_service.rb (renamed from app/services/ci/web_ide_config_service.rb)20
-rw-r--r--app/services/ide/schemas_config_service.rb49
-rw-r--r--app/services/ide/terminal_config_service.rb13
-rw-r--r--app/services/incident_management/incidents/create_service.rb15
-rw-r--r--app/services/issuable/common_system_notes_service.rb14
-rw-r--r--app/services/issuable_base_service.rb52
-rw-r--r--app/services/issuable_links/create_service.rb119
-rw-r--r--app/services/issuable_links/destroy_service.rb34
-rw-r--r--app/services/issuable_links/list_service.rb27
-rw-r--r--app/services/issue_links/create_service.rb46
-rw-r--r--app/services/issue_links/destroy_service.rb28
-rw-r--r--app/services/issue_links/list_service.rb18
-rw-r--r--app/services/issue_rebalancing_service.rb71
-rw-r--r--app/services/issues/base_service.rb31
-rw-r--r--app/services/issues/close_service.rb1
-rw-r--r--app/services/issues/create_service.rb16
-rw-r--r--app/services/issues/duplicate_service.rb9
-rw-r--r--app/services/issues/move_service.rb14
-rw-r--r--app/services/issues/related_branches_service.rb2
-rw-r--r--app/services/issues/reopen_service.rb1
-rw-r--r--app/services/issues/update_service.rb4
-rw-r--r--app/services/issues/zoom_link_service.rb1
-rw-r--r--app/services/jira_connect/sync_service.rb43
-rw-r--r--app/services/jira_connect_subscriptions/base_service.rb11
-rw-r--r--app/services/jira_connect_subscriptions/create_service.rb33
-rw-r--r--app/services/lfs/push_service.rb80
-rw-r--r--app/services/merge_requests/base_service.rb32
-rw-r--r--app/services/merge_requests/cleanup_refs_service.rb75
-rw-r--r--app/services/merge_requests/close_service.rb3
-rw-r--r--app/services/merge_requests/create_pipeline_service.rb12
-rw-r--r--app/services/merge_requests/merge_service.rb5
-rw-r--r--app/services/merge_requests/post_merge_service.rb3
-rw-r--r--app/services/merge_requests/refresh_service.rb130
-rw-r--r--app/services/merge_requests/update_service.rb9
-rw-r--r--app/services/metrics/dashboard/custom_metric_embed_service.rb6
-rw-r--r--app/services/notes/create_service.rb9
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/services/packages/composer/create_package_service.rb7
-rw-r--r--app/services/packages/conan/create_package_service.rb5
-rw-r--r--app/services/packages/create_package_service.rb37
-rw-r--r--app/services/packages/maven/create_package_service.rb7
-rw-r--r--app/services/packages/npm/create_package_service.rb17
-rw-r--r--app/services/packages/nuget/create_dependency_service.rb2
-rw-r--r--app/services/packages/nuget/create_package_service.rb4
-rw-r--r--app/services/packages/pypi/create_package_service.rb17
-rw-r--r--app/services/packages/update_tags_service.rb2
-rw-r--r--app/services/pages/delete_service.rb3
-rw-r--r--app/services/product_analytics/build_activity_graph_service.rb13
-rw-r--r--app/services/product_analytics/build_graph_service.rb10
-rw-r--r--app/services/projects/after_rename_service.rb19
-rw-r--r--app/services/projects/alerting/notify_service.rb31
-rw-r--r--app/services/projects/container_repository/gitlab/delete_tags_service.rb35
-rw-r--r--app/services/projects/container_repository/third_party/delete_tags_service.rb2
-rw-r--r--app/services/projects/create_service.rb9
-rw-r--r--app/services/projects/destroy_service.rb36
-rw-r--r--app/services/projects/download_service.rb2
-rw-r--r--app/services/projects/fork_service.rb4
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb39
-rw-r--r--app/services/projects/open_issues_count_service.rb6
-rw-r--r--app/services/projects/propagate_service_template.rb83
-rw-r--r--app/services/projects/transfer_service.rb12
-rw-r--r--app/services/projects/unlink_fork_service.rb31
-rw-r--r--app/services/projects/update_pages_configuration_service.rb3
-rw-r--r--app/services/projects/update_pages_service.rb2
-rw-r--r--app/services/projects/update_remote_mirror_service.rb20
-rw-r--r--app/services/projects/update_service.rb6
-rw-r--r--app/services/prometheus/proxy_service.rb4
-rw-r--r--app/services/quick_actions/interpret_service.rb1
-rw-r--r--app/services/resource_events/change_state_service.rb18
-rw-r--r--app/services/search/global_service.rb8
-rw-r--r--app/services/search/group_service.rb7
-rw-r--r--app/services/search/project_service.rb5
-rw-r--r--app/services/snippets/base_service.rb2
-rw-r--r--app/services/snippets/create_service.rb2
-rw-r--r--app/services/snippets/destroy_service.rb2
-rw-r--r--app/services/snippets/repository_validation_service.rb4
-rw-r--r--app/services/snippets/update_service.rb2
-rw-r--r--app/services/static_site_editor/config_service.rb49
-rw-r--r--app/services/submit_usage_ping_service.rb4
-rw-r--r--app/services/system_note_service.rb12
-rw-r--r--app/services/system_notes/alert_management_service.rb15
-rw-r--r--app/services/system_notes/issuables_service.rb62
-rw-r--r--app/services/todo_service.rb29
-rw-r--r--app/services/two_factor/base_service.rb14
-rw-r--r--app/services/two_factor/destroy_service.rb24
-rw-r--r--app/services/users/build_service.rb1
-rw-r--r--app/services/users/signup_service.rb2
-rw-r--r--app/services/webauthn/authenticate_service.rb61
-rw-r--r--app/services/webauthn/register_service.rb34
-rw-r--r--app/services/wiki_pages/destroy_service.rb9
-rw-r--r--app/services/wiki_pages/update_service.rb8
-rw-r--r--app/uploaders/object_storage.rb46
-rw-r--r--app/uploaders/terraform/versioned_state_uploader.rb13
-rw-r--r--app/validators/feature_flag_strategies_validator.rb95
-rw-r--r--app/validators/feature_flag_user_xids_validator.rb31
-rw-r--r--app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json95
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml25
-rw-r--r--app/views/admin/appearances/_form.html.haml8
-rw-r--r--app/views/admin/appearances/_system_header_footer_form.html.haml2
-rw-r--r--app/views/admin/appearances/preview_sign_in.html.haml2
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml6
-rw-r--r--app/views/admin/application_settings/_diff_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_external_authorization_service_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_gitpod.html.haml30
-rw-r--r--app/views/admin/application_settings/_grafana.html.haml2
-rw-r--r--app/views/admin/application_settings/_package_registry.html.haml50
-rw-r--r--app/views/admin/application_settings/_pages.html.haml4
-rw-r--r--app/views/admin/application_settings/_performance.html.haml2
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml4
-rw-r--r--app/views/admin/application_settings/_realtime.html.haml3
-rw-r--r--app/views/admin/application_settings/_registry.html.haml10
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml2
-rw-r--r--app/views/admin/application_settings/_signin.html.haml2
-rw-r--r--app/views/admin/application_settings/_snowplow.html.haml3
-rw-r--r--app/views/admin/application_settings/_usage.html.haml13
-rw-r--r--app/views/admin/application_settings/ci/_header.html.haml2
-rw-r--r--app/views/admin/application_settings/general.html.haml1
-rw-r--r--app/views/admin/application_settings/integrations.html.haml2
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml2
-rw-r--r--app/views/admin/applications/_form.html.haml5
-rw-r--r--app/views/admin/cohorts/_cohorts_table.html.haml (renamed from app/views/instance_statistics/cohorts/_cohorts_table.html.haml)2
-rw-r--r--app/views/admin/cohorts/index.html.haml7
-rw-r--r--app/views/admin/dashboard/index.html.haml50
-rw-r--r--app/views/admin/dev_ops_report/_callout.html.haml13
-rw-r--r--app/views/admin/dev_ops_report/_card.html.haml (renamed from app/views/instance_statistics/dev_ops_score/_card.html.haml)0
-rw-r--r--app/views/admin/dev_ops_report/_no_data.html.haml7
-rw-r--r--app/views/admin/dev_ops_report/show.html.haml (renamed from app/views/instance_statistics/dev_ops_score/index.html.haml)12
-rw-r--r--app/views/admin/groups/_form.html.haml6
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/health_check/show.html.haml2
-rw-r--r--app/views/admin/instance_statistics/index.html.haml4
-rw-r--r--app/views/admin/projects/_projects.html.haml5
-rw-r--r--app/views/admin/projects/show.html.haml4
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/admin/runners/update.js.haml2
-rw-r--r--app/views/admin/services/_form.html.haml5
-rw-r--r--app/views/admin/services/index.html.haml19
-rw-r--r--app/views/admin/sessions/_two_factor_otp.html.haml2
-rw-r--r--app/views/admin/sessions/two_factor.html.haml4
-rw-r--r--app/views/admin/users/_form.html.haml6
-rw-r--r--app/views/admin/users/_modals.html.haml2
-rw-r--r--app/views/admin/users/_user.html.haml12
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/admin/users/new.html.haml2
-rw-r--r--app/views/admin/users/projects.html.haml8
-rw-r--r--app/views/admin/users/show.html.haml26
-rw-r--r--app/views/authentication/_authenticate.html.haml (renamed from app/views/u2f/_authenticate.html.haml)2
-rw-r--r--app/views/authentication/_register.html.haml36
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml4
-rw-r--r--app/views/ci/variables/_header.html.haml2
-rw-r--r--app/views/ci/variables/_variable_row.html.haml2
-rw-r--r--app/views/clusters/clusters/_banner.html.haml4
-rw-r--r--app/views/clusters/clusters/aws/_new.html.haml2
-rw-r--r--app/views/clusters/clusters/new.html.haml6
-rw-r--r--app/views/clusters/clusters/user/_header.html.haml5
-rw-r--r--app/views/dashboard/projects/index.html.haml5
-rw-r--r--app/views/dashboard/todos/index.html.haml7
-rw-r--r--app/views/devise/mailer/password_change_by_admin.html.haml6
-rw-r--r--app/views/devise/mailer/password_change_by_admin.text.erb5
-rw-r--r--app/views/devise/registrations/new.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml7
-rw-r--r--app/views/devise/shared/_signup_box.html.haml3
-rw-r--r--app/views/doorkeeper/applications/_delete_form.html.haml2
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml3
-rw-r--r--app/views/events/event/_common.html.haml2
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/groups/_group_admin_settings.html.haml4
-rw-r--r--app/views/groups/_import_group_pane.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml36
-rw-r--r--app/views/groups/packages/_legacy_package_list.haml59
-rw-r--r--app/views/groups/packages/index.html.haml3
-rw-r--r--app/views/groups/runners/_index.html.haml2
-rw-r--r--app/views/groups/runners/_runner.html.haml4
-rw-r--r--app/views/groups/settings/ci_cd/_form.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml6
-rw-r--r--app/views/groups/settings/integrations/index.html.haml2
-rw-r--r--app/views/groups/show.html.haml69
-rw-r--r--app/views/help/_shortcuts.html.haml20
-rw-r--r--app/views/import/_project_status.html.haml2
-rw-r--r--app/views/import/fogbugz/new.html.haml5
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml5
-rw-r--r--app/views/import/fogbugz/status.html.haml5
-rw-r--r--app/views/import/google_code/status.html.haml8
-rw-r--r--app/views/import/manifest/_form.html.haml2
-rw-r--r--app/views/instance_statistics/cohorts/index.html.haml15
-rw-r--r--app/views/instance_statistics/dev_ops_score/_callout.html.haml13
-rw-r--r--app/views/instance_statistics/dev_ops_score/_disabled.html.haml14
-rw-r--r--app/views/instance_statistics/dev_ops_score/_no_data.html.haml7
-rw-r--r--app/views/invites/show.html.haml2
-rw-r--r--app/views/jira_connect/subscriptions/index.html.haml28
-rw-r--r--app/views/layouts/_flash.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml14
-rw-r--r--app/views/layouts/_loading_hints.html.haml10
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/_startup_css.haml5
-rw-r--r--app/views/layouts/_startup_css_activation.haml5
-rw-r--r--app/views/layouts/_startup_js.html.haml5
-rw-r--r--app/views/layouts/experiment_mailer.html.haml48
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml8
-rw-r--r--app/views/layouts/header/_default.html.haml6
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml2
-rw-r--r--app/views/layouts/instance_statistics.html.haml6
-rw-r--r--app/views/layouts/jira_connect.html.haml13
-rw-r--r--app/views/layouts/nav/_analytics_link.html.haml4
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml27
-rw-r--r--app/views/layouts/nav/sidebar/_analytics_link.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml11
-rw-r--r--app/views/layouts/nav/sidebar/_instance_statistics.html.haml11
-rw-r--r--app/views/layouts/nav/sidebar/_instance_statistics_links.html.haml25
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml6
-rw-r--r--app/views/notify/_failed_builds.html.haml7
-rw-r--r--app/views/notify/autodevops_disabled_email.text.erb3
-rw-r--r--app/views/notify/disabled_two_factor_email.html.haml6
-rw-r--r--app/views/notify/disabled_two_factor_email.text.erb5
-rw-r--r--app/views/notify/member_invited_email.html.haml2
-rw-r--r--app/views/notify/member_invited_email.text.erb2
-rw-r--r--app/views/notify/member_invited_email_experiment.html.haml12
-rw-r--r--app/views/notify/member_invited_email_experiment.text.erb10
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb4
-rw-r--r--app/views/profiles/accounts/_providers.html.haml6
-rw-r--r--app/views/profiles/accounts/show.html.haml4
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml2
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/_project_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml3
-rw-r--r--app/views/profiles/preferences/_gitpod.html.haml11
-rw-r--r--app/views/profiles/preferences/_integrations.html.haml18
-rw-r--r--app/views/profiles/preferences/_sourcegraph.html.haml36
-rw-r--r--app/views/profiles/preferences/show.html.haml5
-rw-r--r--app/views/profiles/preferences/update.js.erb4
-rw-r--r--app/views/profiles/show.html.haml10
-rw-r--r--app/views/profiles/two_factor_auths/create.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml45
-rw-r--r--app/views/projects/_deletion_failed.html.haml8
-rw-r--r--app/views/projects/_import_project_pane.html.haml3
-rw-r--r--app/views/projects/_issuable_by_email.html.haml6
-rw-r--r--app/views/projects/_merge_request_merge_checks_settings.html.haml4
-rw-r--r--app/views/projects/_merge_request_merge_suggestions_settings.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/_service_desk_settings.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml2
-rw-r--r--app/views/projects/blob/_content.html.haml4
-rw-r--r--app/views/projects/blob/_editor.html.haml1
-rw-r--r--app/views/projects/blob/_pipeline_tour_success.html.haml1
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml14
-rw-r--r--app/views/projects/blob/new.html.haml1
-rw-r--r--app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_loading.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_loading_auxiliary.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_route_map_loading.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_sketch.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_stl.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml14
-rw-r--r--app/views/projects/branches/new.html.haml1
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/ci/lints/_create.html.haml81
-rw-r--r--app/views/projects/ci/lints/_lint_warnings.html.haml12
-rw-r--r--app/views/projects/ci/lints/show.html.haml58
-rw-r--r--app/views/projects/cleanup/_show.html.haml3
-rw-r--r--app/views/projects/commit/_limit_exceeded_message.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml3
-rw-r--r--app/views/projects/cycle_analytics/_overview.html.haml15
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml12
-rw-r--r--app/views/projects/default_branch/_show.html.haml2
-rw-r--r--app/views/projects/diffs/_file.html.haml4
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml121
-rw-r--r--app/views/projects/environments/folder.html.haml2
-rw-r--r--app/views/projects/environments/index.html.haml3
-rw-r--r--app/views/projects/environments/show.html.haml2
-rw-r--r--app/views/projects/feature_flags/_errors.html.haml4
-rw-r--r--app/views/projects/feature_flags/edit.html.haml16
-rw-r--r--app/views/projects/feature_flags/index.html.haml15
-rw-r--r--app/views/projects/feature_flags/new.html.haml14
-rw-r--r--app/views/projects/feature_flags_user_lists/edit.html.haml7
-rw-r--r--app/views/projects/feature_flags_user_lists/new.html.haml8
-rw-r--r--app/views/projects/feature_flags_user_lists/show.html.haml7
-rw-r--r--app/views/projects/find_file/show.html.haml2
-rw-r--r--app/views/projects/forks/error.html.haml30
-rw-r--r--app/views/projects/graphs/show.html.haml2
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_design_management.html.haml23
-rw-r--r--app/views/projects/issues/_issues.html.haml4
-rw-r--r--app/views/projects/issues/_related_issues.html.haml6
-rw-r--r--app/views/projects/issues/_related_issues_block.html.haml5
-rw-r--r--app/views/projects/issues/_tabs.html.haml14
-rw-r--r--app/views/projects/issues/service_desk.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml11
-rw-r--r--app/views/projects/jobs/index.html.haml4
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml6
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml2
-rw-r--r--app/views/projects/merge_requests/conflicts/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml4
-rw-r--r--app/views/projects/merge_requests/diffs/_different_base.html.haml2
-rw-r--r--app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml11
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml4
-rw-r--r--app/views/projects/mirrors/_mirror_repos_push.html.haml2
-rw-r--r--app/views/projects/notes/_actions.html.haml13
-rw-r--r--app/views/projects/packages/packages/_legacy_package_list.html.haml60
-rw-r--r--app/views/projects/packages/packages/index.html.haml3
-rw-r--r--app/views/projects/packages/packages/show.html.haml12
-rw-r--r--app/views/projects/pages/_access.html.haml6
-rw-r--r--app/views/projects/pipelines/_pipeline_warnings.html.haml6
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/views/projects/pipelines/new.html.haml3
-rw-r--r--app/views/projects/pipelines/show.html.haml1
-rw-r--r--app/views/projects/product_analytics/graphs.html.haml4
-rw-r--r--app/views/projects/serverless/functions/index.html.haml11
-rw-r--r--app/views/projects/serverless/functions/show.html.haml9
-rw-r--r--app/views/projects/services/_form.html.haml8
-rw-r--r--app/views/projects/services/prometheus/_custom_metrics.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_metrics.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml6
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml16
-rw-r--r--app/views/projects/show.html.haml25
-rw-r--r--app/views/projects/static_site_editor/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml6
-rw-r--r--app/views/projects/tags/destroy.js.haml2
-rw-r--r--app/views/projects/tags/new.html.haml7
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml13
-rw-r--r--app/views/projects/tree/_tree_row.html.haml2
-rw-r--r--app/views/projects/triggers/_trigger.html.haml2
-rw-r--r--app/views/registrations/welcome.html.haml47
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/results/_blob_data.html.haml2
-rw-r--r--app/views/search/results/_commit.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml23
-rw-r--r--app/views/search/results/_merge_request.html.haml4
-rw-r--r--app/views/search/results/_milestone.html.haml4
-rw-r--r--app/views/search/results/_note.html.haml9
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml2
-rw-r--r--app/views/shared/_broadcast_message.html.haml8
-rw-r--r--app/views/shared/_file_highlight.html.haml2
-rw-r--r--app/views/shared/_group_form.html.haml10
-rw-r--r--app/views/shared/_help_dropdown_forum_link.html.haml2
-rw-r--r--app/views/shared/_no_password.html.haml19
-rw-r--r--app/views/shared/_no_ssh.html.haml2
-rw-r--r--app/views/shared/_old_visibility_level.html.haml2
-rw-r--r--app/views/shared/_outdated_browser.html.haml9
-rw-r--r--app/views/shared/_project_limit.html.haml2
-rw-r--r--app/views/shared/_service_settings.html.haml4
-rw-r--r--app/views/shared/_zen.html.haml2
-rw-r--r--app/views/shared/access_tokens/_table.html.haml4
-rw-r--r--app/views/shared/blob/_markdown_buttons.html.haml20
-rw-r--r--app/views/shared/boards/_show.html.haml16
-rw-r--r--app/views/shared/boards/_switcher.html.haml1
-rw-r--r--app/views/shared/boards/components/sidebar/_milestone.html.haml5
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml1
-rw-r--r--app/views/shared/empty_states/_issues.html.haml2
-rw-r--r--app/views/shared/form_elements/_apply_template_warning.html.haml2
-rw-r--r--app/views/shared/gitpod/_enable_gitpod_modal.html.haml12
-rw-r--r--app/views/shared/icons/_dev_ops_report_no_data.svg (renamed from app/views/shared/icons/_dev_ops_score_no_data.svg)0
-rw-r--r--app/views/shared/icons/_dev_ops_report_no_index.svg (renamed from app/views/shared/icons/_dev_ops_score_no_index.svg)0
-rw-r--r--app/views/shared/icons/_dev_ops_report_overview.svg (renamed from app/views/shared/icons/_dev_ops_score_overview.svg)0
-rw-r--r--app/views/shared/integrations/_form.html.haml7
-rw-r--r--app/views/shared/integrations/_index.html.haml2
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/_close_reopen_report_toggle.html.haml8
-rw-r--r--app/views/shared/issuable/_form.html.haml9
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml176
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml2
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml2
-rw-r--r--app/views/shared/issuable/form/_issue_assignee.html.haml31
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml2
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml18
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml10
-rw-r--r--app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml8
-rw-r--r--app/views/shared/issuable/form/_type_selector.html.haml30
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/members/_invite_group.html.haml2
-rw-r--r--app/views/shared/members/_invite_member.html.haml4
-rw-r--r--app/views/shared/members/_member.html.haml4
-rw-r--r--app/views/shared/milestones/_description.html.haml5
-rw-r--r--app/views/shared/milestones/_milestone.html.haml6
-rw-r--r--app/views/shared/notes/_hints.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml1
-rw-r--r--app/views/shared/promotions/_promote_servicedesk.html.haml2
-rw-r--r--app/views/shared/runners/show.html.haml50
-rw-r--r--app/views/shared/snippets/_form.html.haml7
-rw-r--r--app/views/shared/snippets/_header.html.haml5
-rw-r--r--app/views/shared/web_hooks/_form.html.haml52
-rw-r--r--app/views/shared/wikis/_form.html.haml4
-rw-r--r--app/views/sherlock/file_samples/show.html.haml2
-rw-r--r--app/views/sherlock/queries/show.html.haml2
-rw-r--r--app/views/sherlock/transactions/index.html.haml2
-rw-r--r--app/views/sherlock/transactions/show.html.haml2
-rw-r--r--app/views/u2f/_register.html.haml40
-rw-r--r--app/views/users/show.html.haml18
-rw-r--r--app/workers/all_queues.yml102
-rw-r--r--app/workers/analytics/instance_statistics/count_job_trigger_worker.rb36
-rw-r--r--app/workers/analytics/instance_statistics/counter_job_worker.rb28
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb10
-rw-r--r--app/workers/ci/create_cross_project_pipeline_worker.rb2
-rw-r--r--app/workers/ci/pipelines/create_artifact_worker.rb18
-rw-r--r--app/workers/ci/ref_delete_unlock_artifacts_worker.rb2
-rw-r--r--app/workers/ci_platform_metrics_update_cron_worker.rb16
-rw-r--r--app/workers/concerns/new_issuable.rb2
-rw-r--r--app/workers/delete_diff_files_worker.rb4
-rw-r--r--app/workers/delete_stored_files_worker.rb4
-rw-r--r--app/workers/git_garbage_collect_worker.rb12
-rw-r--r--app/workers/issue_placement_worker.rb49
-rw-r--r--app/workers/issue_rebalancing_worker.rb20
-rw-r--r--app/workers/jira_connect/sync_branch_worker.rb22
-rw-r--r--app/workers/jira_connect/sync_merge_request_worker.rb18
-rw-r--r--app/workers/merge_request_cleanup_refs_worker.rb23
-rw-r--r--app/workers/new_issue_worker.rb4
-rw-r--r--app/workers/new_note_worker.rb8
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb8
-rw-r--r--app/workers/pages_remove_worker.rb17
-rw-r--r--app/workers/pages_transfer_worker.rb20
-rw-r--r--app/workers/pages_update_configuration_worker.rb10
-rw-r--r--app/workers/partition_creation_worker.rb2
-rw-r--r--app/workers/personal_access_tokens/expired_notification_worker.rb2
-rw-r--r--app/workers/pipeline_update_ci_ref_status_worker.rb18
-rw-r--r--app/workers/post_receive.rb23
-rw-r--r--app/workers/propagate_integration_worker.rb8
-rw-r--r--app/workers/propagate_service_template_worker.rb2
-rw-r--r--app/workers/repository_fork_worker.rb4
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb4
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb4
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb2
-rw-r--r--app/workers/trending_projects_worker.rb2
-rw-r--r--app/workers/update_merge_requests_worker.rb2
-rw-r--r--app/workers/upload_checksum_worker.rb2
1891 files changed, 31579 insertions, 15257 deletions
diff --git a/app/assets/images/auth_buttons/atlassian_64.png b/app/assets/images/auth_buttons/atlassian_64.png
new file mode 100644
index 00000000000..548f1c93318
--- /dev/null
+++ b/app/assets/images/auth_buttons/atlassian_64.png
Binary files differ
diff --git a/app/assets/images/file_icons.svg b/app/assets/images/file_icons.svg
index 26ec1a6b388..ec38020f978 100644
--- a/app/assets/images/file_icons.svg
+++ b/app/assets/images/file_icons.svg
@@ -1 +1 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 24 24" id="actionscript" xmlns="http://www.w3.org/2000/svg"><text style="line-height:113.99999857%" x="5.605" y="15.892" transform="scale(.91325 1.095)" font-weight="400" font-size="42.822" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/><path style="line-height:125%" d="M4.744 2.031c-1.157 0-1.994.31-2.51.93-.515.612-.771 1.678-.771 3.197v2.467c0 1.408-.402 2.111-1.201 2.111v2.035c.8 0 1.2.679 1.2 2.036v2.654c0 1.512.26 2.562.78 3.152.52.59 1.355.885 2.502.885V19.43c-.447 0-.77-.151-.97-.453-.195-.303-.292-.815-.292-1.538v-2.267c0-1.807-.404-2.937-1.214-3.395v-.045c.81-.464 1.214-1.581 1.214-3.351V6.025c0-1.283.42-1.925 1.262-1.925V2.03zm14.66 0V4.1c.842 0 1.262.642 1.262 1.925v2.268c0 1.843.402 2.996 1.207 3.46v.046c-.805.442-1.207 1.544-1.207 3.306v2.356c0 .715-.099 1.22-.299 1.516-.2.302-.52.453-.963.453v2.068c1.152 0 1.984-.295 2.494-.885.516-.59.772-1.663.772-3.218V14.84c0-1.379.404-2.069 1.209-2.069v-2.035c-.805 0-1.21-.696-1.21-2.09V6.113c0-1.49-.255-2.54-.77-3.152-.516-.62-1.348-.93-2.495-.93zm-3.054 4.46c-.455 0-.886.057-1.293.173a3.056 3.056 0 0 0-1.078.527c-.308.241-.551.549-.731.924-.18.37-.27.817-.27 1.336 0 .663.165 1.227.493 1.695.33.468.831.864 1.502 1.188.263.125.509.249.736.37.227.12.422.244.586.374.168.13.299.271.394.424a.963.963 0 0 1 .145.521c0 .144-.03.28-.09.405a.9.9 0 0 1-.275.318c-.12.088-.272.158-.455.21a2.34 2.34 0 0 1-.635.075c-.415 0-.825-.083-1.233-.25a3.644 3.644 0 0 1-1.13-.763v2.222a3.68 3.68 0 0 0 1.101.418c.427.093.875.139 1.346.139.459 0 .894-.05 1.305-.152a3.002 3.002 0 0 0 1.09-.5c.31-.237.556-.543.736-.918.183-.38.275-.849.275-1.405 0-.403-.052-.755-.156-1.056a2.542 2.542 0 0 0-.45-.813 3.295 3.295 0 0 0-.704-.633 6.754 6.754 0 0 0-.922-.535 12.4 12.4 0 0 1-.676-.348c-.2-.115-.37-.231-.51-.347a1.502 1.502 0 0 1-.322-.375.91.91 0 0 1-.115-.453c0-.153.033-.288.101-.408a.948.948 0 0 1 .29-.32c.123-.089.275-.156.454-.202a2.18 2.18 0 0 1 .598-.078c.16 0 .326.015.502.043.18.028.36.07.539.13.18.056.354.13.522.218.171.088.329.188.472.304V6.871a4.039 4.039 0 0 0-.957-.285 6.448 6.448 0 0 0-1.185-.096zm-8.774.165l-3.123 9.967h2.094l.605-2.217h3.053l.61 2.217h2.107L9.869 6.656H7.576zm1.072 1.78h.047c.028.347.077.646.145.896l.922 3.35H7.564l.934-3.377c.08-.288.13-.578.15-.87z" font-weight="400" font-size="51.019" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="android" xmlns="http://www.w3.org/2000/svg"><path d="M15 5h-1V4h1m-5 1H9V4h1m5.53-1.84L16.84.85c.19-.19.19-.51 0-.71a.513.513 0 0 0-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.14a.501.501 0 0 0-.7 0c-.2.2-.2.52 0 .71l1.31 1.31C6.97 3.26 6 5 6 7h12c0-2-1-3.75-2.47-4.84M20.5 8A1.5 1.5 0 0 0 19 9.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 20.5 8m-17 0A1.5 1.5 0 0 0 2 9.5v7A1.5 1.5 0 0 0 3.5 18 1.5 1.5 0 0 0 5 16.5v-7A1.5 1.5 0 0 0 3.5 8M6 18a1 1 0 0 0 1 1h1v3.5A1.5 1.5 0 0 0 9.5 24a1.5 1.5 0 0 0 1.5-1.5V19h2v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5V19h1a1 1 0 0 0 1-1V8H6v10z" fill="#c0ca33"/></symbol><symbol viewBox="0 0 24 24" id="angular" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="angular-component" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#0288d1"/></symbol><symbol viewBox="0 0 24 24" id="angular-directive" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ab47bc"/></symbol><symbol viewBox="0 0 24 24" id="angular-guard" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-pipe" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#00897b"/></symbol><symbol viewBox="0 0 24 24" id="angular-resolver" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-routing" xmlns="http://www.w3.org/2000/svg"><path d="M11 10H5L3 8l2-2h6V3l1-1 1 1v1h6l2 2-2 2h-6v2h6l2 2-2 2h-6v6a2 2 0 0 1 2 2H9a2 2 0 0 1 2-2V10z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-service" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ffca28"/></symbol><symbol viewBox="0 0 100 100" id="apiblueprint" xmlns="http://www.w3.org/2000/svg"><title>api-blueprint</title><path d="M50.133 7.521A16.998 16.998 0 0 0 33.135 24.52a16.998 16.998 0 0 0 4.945 11.974L24.861 57.398a16.998 16.998 0 0 0-3.175-.308A16.998 16.998 0 0 0 4.688 74.088a16.998 16.998 0 0 0 16.998 16.998 16.998 16.998 0 0 0 16.998-16.998 16.998 16.998 0 0 0-7.063-13.773l12.576-19.89a16.998 16.998 0 0 0 5.936 1.093 16.998 16.998 0 0 0 6.154-1.155l12.537 19.83a16.998 16.998 0 0 0-7.244 13.895 16.998 16.998 0 0 0 16.998 17 16.998 16.998 0 0 0 16.998-17A16.998 16.998 0 0 0 78.578 57.09a16.998 16.998 0 0 0-2.95.262L62.337 36.327A16.998 16.998 0 0 0 67.13 24.52 16.998 16.998 0 0 0 50.132 7.522z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="applescript" xmlns="http://www.w3.org/2000/svg"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" fill="#78909c"/></symbol><symbol viewBox="0 0 24 24" id="appveyor" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c-.084 0-.165.008-.248.01a10 10 0 0 0-.266.01 9.952 9.952 0 0 0-.754.066 10 10 0 0 0-.148.018 9.855 9.855 0 0 0-.93.177 10 10 0 0 0-.07.02c-.196.049-.392.1-.584.16v.012a10 10 0 0 0-2 .875V3.34c-.02.012-.038.027-.059.039a10 10 0 0 0-.953.635c-.09.067-.172.142-.26.213a10 10 0 0 0-.628.546c-.109.104-.211.211-.315.319a10 10 0 0 0-.476.539c-.1.12-.201.237-.295.361a10 10 0 0 0-.52.766c-.088.143-.17.288-.252.435a10 10 0 0 0-.363.723c-.072.161-.136.327-.2.492a10 10 0 0 0-.269.778c-.02.067-.044.131-.062.199a10 10 0 0 0-.008.027c-.098.364-.166.728-.22 1.09-.012.077-.024.153-.034.23a9.85 9.85 0 0 0-.08 1.182c0 .03-.006.057-.006.086a10 10 0 0 0 .008.148c.001.094-.002.188.002.282l.011.004a10 10 0 0 0 .333 2.158l-.012-.004c.012.047.033.091.047.139a10 10 0 0 0 .322.955c.02.052.037.106.059.158a10 10 0 0 0 .503 1.035c.065.116.14.226.21.34a10 10 0 0 0 .423.64c.092.128.187.252.285.375a10 10 0 0 0 .448.52c.112.123.222.248.341.365a10 10 0 0 0 .803.719 10 10 0 0 0 .01.006c.099.078.207.146.309.22a10 10 0 0 0 .648.442c.138.085.28.163.424.242a10 10 0 0 0 .715.358c.114.051.226.106.343.154a10 10 0 0 0 1.133.389c.016.004.031.01.047.015a10 10 0 0 0 .461.098 10 10 0 0 0 .482.103 10 10 0 0 0 .418.051 10 10 0 0 0 .575.065 10 10 0 0 0 .144.005A10 10 0 0 0 12 22a10 10 0 0 0 .197-.01 10 10 0 0 0 .496-.025 10 10 0 0 0 .49-.043 10 10 0 0 0 .489-.074 10 10 0 0 0 .51-.098 10 10 0 0 0 .47-.12 10 10 0 0 0 .477-.14 10 10 0 0 0 .47-.172 10 10 0 0 0 .481-.197 10 10 0 0 0 .414-.201 10 10 0 0 0 .475-.252 10 10 0 0 0 .39-.238 10 10 0 0 0 .452-.301 10 10 0 0 0 .38-.291 10 10 0 0 0 .385-.315 10 10 0 0 0 .375-.347 10 10 0 0 0 .36-.363 10 10 0 0 0 .293-.334 10 10 0 0 0 .353-.434 10 10 0 0 0 .28-.393 10 10 0 0 0 .263-.4 10 10 0 0 0 .264-.461 10 10 0 0 0 .228-.436 10 10 0 0 0 .195-.437 10 10 0 0 0 .196-.48 10 10 0 0 0 .228-.69 10 10 0 0 0 .028-.094 10 10 0 0 0 .021-.066 10 10 0 0 0 .098-.461 10 10 0 0 0 .103-.482 10 10 0 0 0 .051-.418 10 10 0 0 0 .065-.575 10 10 0 0 0 .005-.144A10 10 0 0 0 22 12a10 10 0 0 0-.01-.197 10 10 0 0 0-.025-.496 10 10 0 0 0-.043-.49 10 10 0 0 0-.074-.489 10 10 0 0 0-.098-.51 10 10 0 0 0-.12-.47 10 10 0 0 0-.14-.477 10 10 0 0 0-.172-.47 10 10 0 0 0-.197-.481 10 10 0 0 0-.201-.414 10 10 0 0 0-.252-.475 10 10 0 0 0-.238-.39 10 10 0 0 0-.301-.452 10 10 0 0 0-.291-.38 10 10 0 0 0-.315-.385 10 10 0 0 0-.347-.375 10 10 0 0 0-.363-.36 10 10 0 0 0-.334-.293 10 10 0 0 0-.434-.353 10 10 0 0 0-.393-.28 10 10 0 0 0-.4-.263 10 10 0 0 0-.461-.264 10 10 0 0 0-.436-.228 10 10 0 0 0-.437-.196 10 10 0 0 0-.48-.195 10 10 0 0 0-.69-.228 10 10 0 0 0-.094-.028 10 10 0 0 0-.066-.021 10 10 0 0 0-.461-.098 10 10 0 0 0-.482-.103 10 10 0 0 0-.418-.051 10 10 0 0 0-.575-.065 10 10 0 0 0-.144-.005A10 10 0 0 0 12 2zm-.016 5.002a5 5 0 0 1 .262.01 5 5 0 0 1 .227.011 5 5 0 0 1 .341.05 5 5 0 0 1 .135.019 5 5 0 0 1 .014.004 5 5 0 0 1 .115.025 5 5 0 0 1 .303.076 5 5 0 0 1 .265.086 5 5 0 0 1 .2.074 5 5 0 0 1 .242.106 5 5 0 0 1 .228.11 5 5 0 0 1 .196.109 5 5 0 0 1 .244.15 5 5 0 0 1 .17.12 5 5 0 0 1 .224.171 5 5 0 0 1 .186.16 5 5 0 0 1 .176.164 5 5 0 0 1 .172.18 5 5 0 0 1 .177.203 5 5 0 0 1 .133.172 5 5 0 0 1 .16.223 5 5 0 0 1 .133.214 5 5 0 0 1 .12.21 5 5 0 0 1 .107.216 5 5 0 0 1 .109.24 5 5 0 0 1 .084.223 5 5 0 0 1 .08.242 5 5 0 0 1 .07.264 5 5 0 0 1 .047.207 5 5 0 0 1 .045.277 5 5 0 0 1 .028.227 5 5 0 0 1 .02.351 5 5 0 0 1 .003.079 5 5 0 0 1-.012.271 5 5 0 0 1-.011.227 5 5 0 0 1-.05.341 5 5 0 0 1-.019.135 5 5 0 0 1-.004.014 5 5 0 0 1-.025.115 5 5 0 0 1-.076.303 5 5 0 0 1-.086.265 5 5 0 0 1-.074.2 5 5 0 0 1-.106.242 5 5 0 0 1-.11.228 5 5 0 0 1-.109.196 5 5 0 0 1-.15.244 5 5 0 0 1-.12.17 5 5 0 0 1-.171.224 5 5 0 0 1-.16.186 5 5 0 0 1-.164.176 5 5 0 0 1-.18.172 5 5 0 0 1-.203.177l-.002.002c-.018.019-.028.035-.047.053l-3.959 5.09-3.05-.979a141.684 141.684 0 0 0 3.177-3.084 5 5 0 0 1-.103-.015 5 5 0 0 1-.149-.024 5 5 0 0 1-.115-.025 5 5 0 0 1-3.57-3.04 5.072 5.072 0 0 1-.206-.661 5 5 0 0 1-.033-.147c-.025-.118-.036-.24-.054-.36-.987.993-1.964 1.993-2.954 3.05l-.98-3.053 5.092-3.957c.043-.044.082-.07.125-.11a5 5 0 0 1 .71-.634c.18-.13.367-.25.561-.356a5 5 0 0 1 .16-.08 4.94 4.94 0 0 1 .516-.222 5 5 0 0 1 .147-.057c.211-.07.43-.123.654-.164a5 5 0 0 1 .172-.027c.236-.035.476-.058.722-.059zM12 9a3 3 0 0 0-.053.002 3 3 0 0 0-.166.01 3 3 0 0 0-.133.011 3 3 0 0 0-.17.026 3 3 0 0 0-.113.021 3 3 0 0 0-.19.05 3 3 0 0 0-.103.03 3 3 0 0 0-.16.057 3 3 0 0 0-.129.053 3 3 0 0 0-.146.072 3 3 0 0 0-.12.063 3 3 0 0 0-.132.082 3 3 0 0 0-.123.08 3 3 0 0 0-.116.088 3 3 0 0 0-.126.105 3 3 0 0 0-.1.094 3 3 0 0 0-.111.111 3 3 0 0 0-.096.107 3 3 0 0 0-.094.116 3 3 0 0 0-.098.136 3 3 0 0 0-.072.11 3 3 0 0 0-.076.133 3 3 0 0 0-.07.132 3 3 0 0 0-.063.14 3 3 0 0 0-.054.14 3 3 0 0 0-.077.228 3 3 0 0 0-.007.026 3 3 0 0 0-.03.138 3 3 0 0 0-.031.149 3 3 0 0 0-.014.11 3 3 0 0 0-.02.183 3 3 0 0 0-.001.052A3 3 0 0 0 9 12a3 3 0 0 0 .002.053 3 3 0 0 0 .01.166 3 3 0 0 0 .011.133 3 3 0 0 0 .026.17 3 3 0 0 0 .021.113 3 3 0 0 0 .05.19 3 3 0 0 0 .03.103 3 3 0 0 0 .057.16 3 3 0 0 0 .053.129 3 3 0 0 0 .072.146 3 3 0 0 0 .063.12 3 3 0 0 0 .082.132 3 3 0 0 0 .08.123 3 3 0 0 0 .088.116 3 3 0 0 0 .105.126 3 3 0 0 0 .094.1 3 3 0 0 0 .111.111 3 3 0 0 0 .107.096 3 3 0 0 0 .116.094 3 3 0 0 0 .136.098 3 3 0 0 0 .11.072 3 3 0 0 0 .133.076 3 3 0 0 0 .132.07 3 3 0 0 0 .135.06 3 3 0 0 0 .153.061 3 3 0 0 0 .216.07 3 3 0 0 0 .004.003 3 3 0 0 0 .026.007 3 3 0 0 0 .138.03 3 3 0 0 0 .149.031 3 3 0 0 0 .11.014 3 3 0 0 0 .183.02 3 3 0 0 0 .011.001 3 3 0 0 0 .041 0A3 3 0 0 0 12 15a3 3 0 0 0 .053-.002 3 3 0 0 0 .166-.01 3 3 0 0 0 .133-.011 3 3 0 0 0 .17-.026 3 3 0 0 0 .113-.021 3 3 0 0 0 .19-.05 3 3 0 0 0 .103-.03 3 3 0 0 0 .16-.057 3 3 0 0 0 .129-.053 3 3 0 0 0 .146-.072 3 3 0 0 0 .12-.063 3 3 0 0 0 .132-.082 3 3 0 0 0 .123-.08 3 3 0 0 0 .116-.088 3 3 0 0 0 .126-.105 3 3 0 0 0 .1-.094 3 3 0 0 0 .111-.111 3 3 0 0 0 .096-.107 3 3 0 0 0 .094-.116 3 3 0 0 0 .098-.136 3 3 0 0 0 .072-.11 3 3 0 0 0 .076-.133 3 3 0 0 0 .07-.132 3 3 0 0 0 .06-.135 3 3 0 0 0 .061-.153 3 3 0 0 0 .07-.216 3 3 0 0 0 .003-.004 3 3 0 0 0 .007-.026 3 3 0 0 0 .03-.138 3 3 0 0 0 .031-.149 3 3 0 0 0 .002-.008 3 3 0 0 0 .012-.101 3 3 0 0 0 .02-.184 3 3 0 0 0 .001-.011 3 3 0 0 0 0-.041A3 3 0 0 0 15 12a3 3 0 0 0-.002-.053 3 3 0 0 0-.01-.166 3 3 0 0 0-.011-.133 3 3 0 0 0-.026-.17 3 3 0 0 0-.021-.113 3 3 0 0 0-.05-.19 3 3 0 0 0-.03-.103 3 3 0 0 0-.057-.16 3 3 0 0 0-.053-.129 3 3 0 0 0-.072-.146 3 3 0 0 0-.063-.12 3 3 0 0 0-.082-.132 3 3 0 0 0-.08-.123 3 3 0 0 0-.088-.116 3 3 0 0 0-.105-.126 3 3 0 0 0-.094-.1 3 3 0 0 0-.111-.111 3 3 0 0 0-.107-.096 3 3 0 0 0-.116-.094 3 3 0 0 0-.136-.098 3 3 0 0 0-.11-.072 3 3 0 0 0-.133-.076 3 3 0 0 0-.132-.07 3 3 0 0 0-.14-.063 3 3 0 0 0-.14-.054 3 3 0 0 0-.228-.077 3 3 0 0 0-.026-.007 3 3 0 0 0-.138-.03 3 3 0 0 0-.149-.031 3 3 0 0 0-.008-.002 3 3 0 0 0-.101-.012 3 3 0 0 0-.184-.02 3 3 0 0 0-.011-.001 3 3 0 0 0-.041 0A3 3 0 0 0 12 9z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 720 720" id="arduino" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="ana" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke-opacity="100%" stroke-width="60" stroke="#00979c" d="M174 30a10.5 10.1 0 0 0 0 280C364 320 344 30 544 30a10.5 10.1 0 0 1 0 280C354 320 374 30 174 30"/><path d="M528 205v-32.8h-32.5v-13.7H528V126h13.9v32.5h32.5v13.7h-32.5V205H528z" text-anchor="middle" fill="#00979c" stroke-width="20" stroke="#00979c" font-family="sans-serif" font-size="167"/><path fill="#00979c" stroke="#00979c" stroke-width="23.6" transform="matrix(1.56 0 0 .64 -366 .528)" d="M321 266v-17.4h53.3V266H321z"/></symbol></defs><title>Layer 1</title><use x="20.063" y="360.85" transform="matrix(.997 0 0 .997 -18.596 -159.19)" xlink:href="#ana"/></symbol><symbol viewBox="0 0 24 24" id="assembly" xmlns="http://www.w3.org/2000/svg"><path d="M1.746 1.566v20.905H5.13v-2.088H3.438V3.656h1.69v-2.09H1.747zm17.219 0v2.09h1.693v16.727h-1.693v2.09h3.383V1.566h-3.383zM15.196 3.988c-.5 0-.93.076-1.29.225-.359.15-.652.372-.877.671-.226.302-.39.673-.494 1.108a6.715 6.715 0 0 0-.155 1.54c0 .573.049 1.083.15 1.528.1.442.264.811.49 1.11.222.298.512.524.872.676.36.153.795.23 1.304.23.518 0 .954-.075 1.308-.224.353-.153.643-.376.869-.671.219-.29.38-.661.484-1.112.104-.454.156-.967.156-1.54 0-.573-.052-1.079-.152-1.515a2.92 2.92 0 0 0-.485-1.106 2.09 2.09 0 0 0-.868-.686c-.354-.155-.79-.234-1.312-.234zm-6.814.12a.941.941 0 0 1-.138.458.849.849 0 0 1-.356.296A1.71 1.71 0 0 1 7.385 5a5.244 5.244 0 0 1-.631.037v1.11H8.19v3.6H6.754v1.188h4.545V9.745H9.894V4.11H8.382zm6.814 1.138c.375 0 .643.176.805.527.161.348.241.933.241 1.756 0 .814-.082 1.399-.247 1.756-.164.356-.43.534-.799.534-.369 0-.636-.178-.8-.534-.165-.357-.248-.941-.248-1.749 0-.829.082-1.415.243-1.763.162-.35.43-.527.805-.527zm-6.33 7.64c-.5 0-.93.073-1.29.223-.359.15-.651.374-.877.673-.225.302-.39.67-.494 1.106a6.715 6.715 0 0 0-.155 1.54c0 .573.05 1.082.15 1.527.1.442.264.814.49 1.112.222.3.514.525.874.677.36.152.793.229 1.302.229.519 0 .954-.076 1.308-.225.354-.153.643-.376.869-.672.22-.29.38-.66.484-1.111.104-.455.156-.967.156-1.54 0-.573-.05-1.079-.15-1.515a2.923 2.923 0 0 0-.487-1.106 2.084 2.084 0 0 0-.867-.686c-.353-.156-.791-.232-1.313-.232zm5.846.119a.941.941 0 0 1-.138.457.85.85 0 0 1-.356.296 1.71 1.71 0 0 1-.503.137 5.245 5.245 0 0 1-.631.037v1.112h1.435v3.597h-1.435v1.189h4.545v-1.189h-1.405v-5.636h-1.512zm-5.846 1.137c.375 0 .643.176.805.527.162.347.241.933.241 1.756 0 .813-.08 1.399-.245 1.755-.164.357-.432.534-.8.534-.37 0-.637-.177-.802-.534-.164-.356-.245-.939-.245-1.746 0-.83.08-1.418.242-1.765.161-.35.43-.527.804-.527z" fill="#ff6e40"/></symbol><symbol viewBox="0 0 24 24" id="aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="api" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apa"/><linearGradient id="apa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#apb"/><linearGradient id="apb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#apc"/><linearGradient id="apc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#apd"/><linearGradient id="apd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#ape"/><linearGradient id="ape" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#apf"/><linearGradient id="apf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apg"/><linearGradient id="apg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="app" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#aph"/><linearGradient id="aph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><g transform="rotate(11.282 -1.694 21.569) scale(.47102)" clip-rule="evenodd" fill="none" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#api)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#apj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#apk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#apl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#apm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#apn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#apo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#app)"/></g></symbol><symbol viewBox="0 0 24 24" id="autohotkey" xmlns="http://www.w3.org/2000/svg"><path d="M5 3c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5zm3.668 3.447a.9.9 0 0 1 .652.256.84.84 0 0 1 .262.625c0 .34-.014.852-.041 1.537-.022.68-.033 1.19-.033 1.53 0 .111-.016.326-.047.644a6.149 6.149 0 0 0-.033.68l2.578-.485c1.007-.179 1.874-.281 2.603-.308.018-.3.048-1.105.088-2.416.01-.345.115-.742.317-1.19.25-.55.533-.826.851-.826.237 0 .448.08.631.236.197.17.295.382.295.637a.775.775 0 0 1-.025.201c-.09.327-.135.612-.135.854 0 .125-.014.32-.041.584-.023.26-.033.453-.033.578 0 .425-.022 1.056-.067 1.893a38.963 38.963 0 0 0-.068 1.892c0 .327.025.816.074 1.465.05.649.074 1.136.074 1.463a.84.84 0 0 1-.261.625.893.893 0 0 1-.65.254 1 1 0 0 1-.686-.254.777.777 0 0 1-.29-.611c0-.327-.015-.818-.046-1.471a39.552 39.552 0 0 1-.041-1.47c0-.256.004-.482.013-.679-.702.032-1.57.142-2.603.33-.86.157-1.719.316-2.578.477-.01.304-.042.812-.096 1.523a22.354 22.354 0 0 0-.066 1.538.84.84 0 0 1-.262.625.893.893 0 0 1-.65.253.898.898 0 0 1-.653-.253.84.84 0 0 1-.262-.625c0-.452.038-1.128.114-2.028.08-.9.12-1.575.12-2.027 0-.573.015-1.436.042-2.586.027-1.155.04-2.017.04-2.59a.84.84 0 0 1 .263-.625.895.895 0 0 1 .65-.256z" fill="#4caf50"/></symbol><symbol viewBox="0 0 24 24" id="autoit" xmlns="http://www.w3.org/2000/svg"><defs id="ardefs8"><style id="arstyle4482">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style><style id="arstyle4510">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style></defs><g id="arg4522" transform="translate(-59.538 -26.404) scale(.0555)"><path d="M12.8 2.133A10.666 10.666 0 0 0 2.136 12.799 10.666 10.666 0 0 0 12.8 23.465a10.666 10.666 0 0 0 10.668-10.666A10.666 10.666 0 0 0 12.8 2.133zm.15 4.713c.456 0 .836.105 1.142.314.306.21.565.469.78.78l6.089 8.812H9.627l1.82-2.506h3.36c.315 0 .589.01.822.03a11.93 11.93 0 0 1-.473-.663 39.13 39.13 0 0 0-.517-.75l-1.748-2.578-4.577 6.467H4.746l6.25-8.813c.204-.281.46-.534.772-.757.31-.224.705-.336 1.181-.336z" transform="matrix(16.89188 0 0 16.89188 1072.761 475.745)" id="arcircle4514" fill="#1976d2" stroke-width=".026"/></g></symbol><symbol viewBox="0 0 213.33333 213.33333" id="babel" xmlns="http://www.w3.org/2000/svg"><path d="M50.22 199.659c-.875-.406-1.261-1.6-.857-2.652.404-1.053.12-1.914-.63-1.914s-1.615.748-1.92 1.663c-.328.983-1.27.302-2.304-1.667-.962-1.831-3.718-5.533-6.126-8.226-9.418-10.535-7.71-27.444 5.432-53.77 12.459-24.96 23.117-39.033 45.966-60.696 30.229-28.66 52.679-46.223 70.587-55.22 10.98-5.518 13.025-5.059 2.778.624-11.004 6.102-11.378 6.359-10.512 7.226.33.33 7.306-2.67 15.504-6.667 15.87-7.737 16.34-7.912 16.34-6.082 0 .652-4.95 3.738-11 6.858-13.062 6.736-12.722 6.48-10.472 7.872 1.117.69 5.428-.582 11.54-3.406 5.367-2.48 10.397-4.508 11.179-4.508 2.755 0-3.928 5.302-11.541 9.157-20.437 10.35-68.937 46.043-68.07 50.097.166.777-5.792 7.639-13.241 15.248-15.257 15.587-26.14 30.002-33.748 44.706-6.379 12.326-7.457 17.734-5.385 26.996 3.482 15.56 11.592 18.366 31.482 10.895 28.228-10.603 45.758-28.704 47.022-48.556.602-9.442-1.317-13.479-8.52-17.93-4.01-2.48-5.268-2.621-12.065-1.365-4.173.771-10.153 2.906-13.289 4.744s-6.455 3.34-7.377 3.34c-.922 0-3.216 1.336-5.096 2.968-1.88 1.633.48-1.13 5.247-6.14 6.82-7.167 7.956-8.9 5.333-8.132-5.208 1.525-10.194 4.33-15.649 8.803-2.76 2.264-.923.175 4.08-4.641 11.565-11.131 21.183-15.97 33.088-16.641 17.097-.966 27.254 5.805 31.964 21.31 2.435 8.017 2.609 10.24 1.353 17.37-1.65 9.361-7.034 21.553-15.593 35.307-4.398 7.067-8.434 11.427-15.588 16.844-9.166 6.94-15.654 11.02-15.654 9.845 0-.295 2.455-2.161 5.455-4.147 8.818-5.835 5.075-5.377-8.326 1.02-6.854 3.27-15.199 6.593-18.542 7.38-7.106 1.675-30.527 3.164-32.846 2.089zm-8.408-19.899c0-1.1-.6-2-1.333-2-.734 0-1.334.9-1.334 2s.6 2 1.333 2c.734 0 1.334-.9 1.334-2zm89.255-8.204c1.53-1.945 2.473-3.845 2.097-4.222-.377-.377-.836-.435-1.02-.13-.182.306-1.787 2.206-3.565 4.223-1.778 2.016-2.571 3.666-1.763 3.666s2.72-1.591 4.25-3.536zm-77.644-1.745c-.82-2.172-1.74-3.7-2.045-3.396-.951.952 1.088 7.345 2.343 7.345.656 0 .522-1.777-.298-3.95zm82.303-27.915c-.837-.837-3.217 2.55-3.184 4.53.012.734.896.178 1.965-1.235 1.07-1.413 1.618-2.896 1.219-3.295zm-66.238-36.904c-1.312-1.312-3.676.702-3.676 3.133 0 2.035.175 2.031 2.254-.047 1.24-1.24 1.88-2.628 1.422-3.086zm39.657.768c4.403-2.196 6.8-3.986 5.333-3.982-2.838.01-16.667 6.028-16.667 7.254 0 1.6 3.717.527 11.333-3.272zm16.667-5.333c0-.733-.9-1.333-2-1.333s-2 .6-2 1.333.9 1.333 2 1.333 2-.6 2-1.333zm-3.334-3.923l5.334-1.104-7.334-.133c-4.033-.073-8.233.45-9.333 1.16-2.539 1.64 3.572 1.682 11.333.077zm35.738-63.976c2.788-1.69 4.765-3.376 4.393-3.748-.947-.947-11.942 5.654-14.237 8.548-1.792 2.258-1.714 2.276 1.44.329a1452.76 1452.76 0 0 1 8.403-5.13z" fill="#ffca28" stroke-width="1.333"/></symbol><symbol viewBox="0 0 400 400" fill-opacity=".05" id="bithound" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.88 0 0 .88 24.121 2.895)" fill="#e53935" fill-opacity="1"><path d="M370.5 207c-1.5-14.8-4.8-29.9-9.5-44-13.5-40.3-38.6-81.6-70.3-110.1-1.4-1.2-6.7-4.4-8.7-3.3-5.2 2.9 4.6 22.8 5.8 26.4 7.4 22 12.1 45.3 6.8 68.3-7.1 30.4-30.4 51.7-61.5 54.3-17.1 1.4-34.3-.5-51.4 1.5-25.6 3-51.7 11.8-68 32.8-1.9 2.4-3.6 5.1-5.2 7.9h-.4c-6.3.7-12.6-2-15.7-3.7-.8-.5-1.6-.9-2.2-1.2-19-10.5-33-34-41.6-53.4-3.9-9-7.2-18.4-9.3-27.9-1-4.3-1.1-8.8-1.3-13.2-.1-2.7.3-6.5-1.2-8.9-3.3-5.2-7.5-.2-8.2 4-1.1 6.9-2.1 13.7-1.8 20.7.5 11.8 3.8 23.5 8 34.5 6.2 16.2 14.9 31.1 26.2 44.4 4.7 5.5 9.7 10.6 15.1 15.3 4.8 4.3 10.9 7.7 14.5 13.2 4.2 6.3 4.9 14.1 4.5 21.4-1 19.3-1.6 37.4 3.9 56.2 4.8 16.7 10.8 33.8 20.8 48.1 5 7.1 11.2 14.6 18 19.9 4.6 3.6 13.3 4 8.3-9.2-11.1-29.3-12.1-59.7 5.2-87.1 14.5-22.8 40.1-43.1 69-39.5 42.5 5.3 72.1 44.3 70 86-.6 11.7-1 21.7-4.7 32.7-1.5 4.4-2.6 10-1.5 14.6 1.8 7.8 10.5 4.9 14.3-.2 10.3-14 21.1-27.6 30.8-42 31.6-47.2 47-101.8 41.3-158.5z"/><path d="M132.4 92.1c.7 2.3 1.4 4.8 1.9 7.5.1 1.1.4 2.3 1 3.4 2.6 6.8 8.9 10.5 14.8 14 3.6 2.2 10.1 4.3 14.1 5.9 5.2 2.1 16.4-.6 21.7-1 12.2-1 23.5-5.3 34.7 1.2-57.4 67.3-3.2 82.3 38.8 49.9 48-37 2.8-124.3 2.8-124.3s-1-6.8-19.2-10.8c-1.7-.9-3.4-1.7-5.1-2.4-18-8.3-34.2 5.3-47.2 16.4-3.8 3.2-7.5 6.4-11.5 9.4-5.4 4-11.2 7.3-17.3 10.2-6.4 3-14 6.4-21.1 6.7-1 0-2.9.2-4.9.6-3.1.3-4.7 1.1-5.4 2.5-1.2 1-2 2.4-1.8 4.2.2 2.5 1.4 4.6 2.7 6.2.4.1.7.3 1 .4z"/></g></symbol><symbol viewBox="0 0 400.00001 399.99999" id="bower" xmlns="http://www.w3.org/2000/svg"><g transform="translate(12.061 33.203) scale(.81733)"><path d="M447.61 200.08c-23.139-22.234-138.85-36.114-175.36-40.154a107.137 107.137 0 0 0 4.517-12.944 146.107 146.107 0 0 1 15.905-5.901c.677 1.997 3.865 9.648 5.682 13.279 73.415 2.025 77.184-54.557 80.17-70.058 2.92-15.157 2.771-29.802 27.953-56.575-37.516-10.933-91.467 16.945-109.54 58.437-6.79-2.545-13.597-4.424-20.328-5.586-4.824-19.46-29.944-73.672-95.863-73.672-83.46 0-174.43 68.853-174.43 185.41 0 97.976 66.891 183.84 104.68 183.84 16.505 0 30.703-12.36 34.036-23.44 2.795 7.597 11.368 31.213 14.184 37.225 4.162 8.89 23.41 16.583 31.833 7.357 10.83 6.017 30.703 9.641 41.534-6.405 20.86 4.412 39.3-8.026 39.702-22.868 10.235-.546 15.256-14.918 13.021-26.363-1.647-8.426-19.248-38.66-26.113-49.098 13.59 11.054 48.013 14.183 52.194.007 21.911 17.198 56.057 8.171 58.765-5.815 26.624 6.917 57.16-8.276 52.146-26.676 42.771-2.958 37.296-48.464 25.296-59.996z" fill="#543729" stroke-width=".973"/><path d="M328.514 103.025c9.212-18.277 20.788-38.234 35.409-50.58-16.093 6.485-31.981 25.873-41.375 46.595a144.914 144.914 0 0 0-14.552-8.132c13.105-27.972 43.555-51.332 77.112-53.157-22.477 20.385-14.498 62.754-32.979 85.183-5.288-5.311-17.43-15.562-23.615-19.909zm-14.53 29.762c.01-.7.272-6.094.763-8.557-1.288-.304-9.3-1.87-13.476-1.772-.304 5.245 2.204 14.17 4.684 19.541 17.075-.358 29.408-5.471 36.667-10.172-6.18-2.88-16.726-5.442-24.745-6.974-.894 1.851-3.097 6.568-3.892 7.934z" fill="#00acee"/><g stroke-width=".973"><path d="M250.54 277.39c.004.024.015.057.018.082-2.165-4.657-4.463-10.314-7.208-17.708 10.688 15.557 44.184 7.533 42.427-6.407 16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455 28 5.4 54.832 10.783 63.256 12.938-5.595 9.123-18.339 15.566-37.549 11.089 10.38 14.14-9.773 31.105-37.844 21.76 6.18 13.883-18.814 26.38-47.22 11.91.361 13.889-35.24 15.488-49.315.143zm55.543-70.194c32.497 2.495 86.238 7.34 119.51 11.997-2.102-10.828-7.844-13.921-25.905-18.772-19.425 2.072-68.706 6.913-93.604 6.776z" fill="#2baf2b"/><path d="M285.78 253.36c16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455-33.103-6.383-67.84-12.788-75.719-13.908 4.78.254 12.702.797 22.59 1.556 24.899.137 74.18-4.704 93.604-6.775-31.452-7.975-95.666-19.613-140.01-22.48-2.055 3.003-5.833 8.097-12.413 13.51-19.403 41.053-54.557 68.34-93.454 68.34-11.335 0-24.018-1.912-38.233-6.456-8.865 9.497-46.661 16.694-77.329 1.641 24.326 56.961 80.74 94.984 143.19 94.984 52.591 0 75.912-53.704 70.808-67.914-1.238-3.45-6.145-14.889-8.891-22.283 10.689 15.556 44.185 7.532 42.429-6.408z" fill="#ffcc2f"/><path d="M253.91 145.27c4.644-2.526 20.69-12.253 35.981-15.908a67.843 67.843 0 0 1-.536-5.12c-10.032 2.403-28.945 10.51-39.784-.661 22.866 6.9 34.283-6.149 51.09-6.149 10.014 0 24.305 2.798 35.57 7.22-9.061-8.37-38.772-33.63-75.558-33.717-8.213 9.957-17.09 31.526-6.764 54.334z" fill="#cecece"/><path d="M115.58 253.33c14.215 4.544 26.898 6.457 38.233 6.457 38.896 0 74.05-27.29 93.454-68.341-14.351 11.978-39.291 22.228-78.241 22.228 34.694-7.866 64.56-25.156 79.753-50.427-10.68-16.998-22.263-54.603 7.07-84.33-4.512-14.497-26.475-52.766-75.095-52.766-84.85 0-155.17 71.001-155.17 166.15 0 22.525 4.547 43.65 12.67 62.664 30.666 15.054 68.462 7.858 77.327-1.64z" fill="#ef5734"/><path d="M141.03 108.45c0 21.644 17.546 39.191 39.19 39.191s39.192-17.548 39.192-39.191c0-21.644-17.548-39.191-39.192-39.191-21.644 0-39.19 17.547-39.19 39.191z" fill="#ffcc2f"/><path d="M156.76 108.45c0 12.958 10.507 23.463 23.463 23.463 12.96 0 23.464-10.506 23.464-23.463 0-12.959-10.504-23.464-23.464-23.464-12.957 0-23.463 10.506-23.463 23.464z" fill="#543729"/><ellipse cx="180.22" cy="98.044" rx="13.673" ry="8.501" fill="#fff"/></g></g></symbol><symbol viewBox="0 0 140 140" id="browserlist" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><path d="M70.314 10.066a59.828 59.828 0 0 0-59.828 59.828 59.828 59.828 0 0 0 59.828 59.828 59.828 59.828 0 0 0 59.828-59.828 59.828 59.828 0 0 0-59.828-59.828zm-4.836 8.785c.496 4.043 1.352 7.322 2.572 10.223 4.779-4.287 10.265-7.546 16.041-9.02-.981 3.938-1.357 7.295-1.261 10.43 6.026-2.314 12.349-3.404 18.3-2.706-3.182 2.413-5.482 4.717-7.128 7.015-2.201 12.074 6.858 20.43 14.779 24.551a5.128 5.128 0 0 1 5.183-3.888 5.128 5.128 0 0 1 3.7 8.435v.002c-.487 1.055-2.002 2.343-3.497 3.219-4.075 2.39-11.172 5.736-20.914 7.39.045 1.214.077 2.453.077 3.747 0 4.817-.485 8.291-1.385 10.699-3.3 13.313-12.648 26.76-24.695 31.95.357-4.083.197-7.485-.402-10.591-5.582 3.218-11.646 5.278-17.623 5.52h-.002c1.785-3.662 2.855-6.878 3.412-9.976-6.347.996-12.727.742-18.377-1.17 2.93-2.732 5.054-5.314 6.673-7.96-6.292-1.344-12.169-3.87-16.766-7.686 3.822-1.544 6.795-3.239 9.3-5.197-5.426-3.517-10.034-7.998-12.972-13.23 4.012-.07 7.321-.568 10.3-1.453-3.786-5.215-6.468-11.032-7.333-16.951 3.861 1.405 7.196 2.133 10.36 2.355-1.662-6.22-2.081-12.605-.768-18.436 3.03 2.634 5.824 4.48 8.63 5.815.678-6.406 2.576-12.52 5.893-17.496 1.926 3.622 3.914 6.391 6.111 8.672 2.93-5.754 6.9-10.798 11.791-14.262zm26.465 19.557c-2.395 5.514-1.665 11.297-.555 18.732a2.138 2.138 0 0 0 .28-4.178 3.419 3.419 0 1 1 .092 6.704c.574 3.882 1.157 8.18 1.421 13.125a67.143 67.143 0 0 0 3.25-.649c6.616-1.487 12.258-3.801 16.871-6.506.45-.264.884-.563 1.276-.867.366-.557.333-.957.035-1.285-4.831-1.245-10.891-4.53-15.258-8.795-4.764-4.653-7.428-10.164-7.412-16.281z" fill="#ffca28" stroke-width=".855"/></symbol><symbol viewBox="0 0 140 140" id="browserlist_light" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><g transform="translate(10.823 10.1)" stroke-width=".855"><circle cx="59.492" cy="59.795" r="59.828" fill="#ffca28"/><path d="M54.656 8.752c-4.89 3.464-8.862 8.508-11.791 14.262-2.198-2.28-4.185-5.05-6.111-8.672-3.318 4.976-5.216 11.09-5.893 17.496-2.807-1.335-5.6-3.18-8.63-5.814-1.314 5.83-.895 12.216.767 18.436-3.164-.223-6.498-.95-10.36-2.356.865 5.92 3.548 11.737 7.333 16.951-2.978.885-6.287 1.383-10.3 1.453 2.939 5.233 7.547 9.714 12.972 13.23-2.505 1.959-5.478 3.654-9.299 5.198 4.596 3.815 10.474 6.341 16.766 7.685-1.62 2.647-3.743 5.228-6.674 7.96 5.65 1.912 12.03 2.166 18.377 1.17-.556 3.098-1.626 6.314-3.412 9.975h.002c5.977-.24 12.042-2.3 17.623-5.52.6 3.108.76 6.51.402 10.593 12.047-5.19 21.395-18.638 24.695-31.951.9-2.408 1.385-5.881 1.385-10.7 0-1.293-.031-2.531-.076-3.745 9.742-1.655 16.839-5.001 20.914-7.39 1.494-.877 3.01-2.165 3.496-3.22v-.002a5.128 5.128 0 0 0-3.7-8.435 5.128 5.128 0 0 0-5.183 3.889c-7.92-4.122-16.98-12.477-14.779-24.551 1.646-2.299 3.947-4.603 7.13-7.016-5.952-.698-12.276.392-18.302 2.707-.095-3.135.28-6.492 1.262-10.43-5.776 1.473-11.262 4.733-16.041 9.02-1.22-2.902-2.076-6.18-2.572-10.223zm26.465 19.557c-.015 6.117 2.648 11.628 7.412 16.281 4.366 4.265 10.426 7.55 15.258 8.795.298.328.331.728-.035 1.285-.392.304-.825.603-1.275.867-4.613 2.704-10.256 5.019-16.871 6.506-1.071.24-2.154.458-3.25.649-.265-4.945-.848-9.243-1.422-13.125a3.419 3.419 0 1 0-.092-6.703 2.138 2.138 0 0 1-.28 4.177c-1.11-7.435-1.84-13.218.555-18.732z" fill="#37474f"/></g></symbol><symbol viewBox="0 0 24 24" id="bucklescript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm14.1 8.858a5.5 5.5 0 0 1 1.26.145c.417.093.778.213 1.082.357v1.723h-.18a3.281 3.281 0 0 0-.959-.603 2.867 2.867 0 0 0-1.155-.247c-.14 0-.277.011-.416.035a1.4 1.4 0 0 0-.395.12.756.756 0 0 0-.291.231.54.54 0 0 0-.123.348c0 .198.065.35.196.456.13.104.376.2.738.288.237.057.466.11.683.164.22.054.455.128.706.222.496.188.86.444 1.095.77.238.32.357.738.357 1.253 0 .737-.271 1.336-.813 1.798-.538.46-1.27.689-2.197.689a5.447 5.447 0 0 1-1.402-.161 6.725 6.725 0 0 1-1.117-.416v-1.794h.183c.344.318.73.563 1.155.734.429.17.839.256 1.233.256.1 0 .235-.01.4-.03.166-.02.3-.055.403-.102a.97.97 0 0 0 .313-.225c.084-.09.127-.223.127-.4a.568.568 0 0 0-.183-.424c-.119-.12-.294-.213-.526-.276-.243-.067-.5-.128-.773-.185a5.523 5.523 0 0 1-.76-.227c-.544-.204-.936-.48-1.177-.828-.237-.351-.357-.786-.357-1.305 0-.697.27-1.265.81-1.703.54-.442 1.235-.663 2.083-.663zm-8.981.135h2.51c.521 0 .903.02 1.143.06.243.041.484.13.721.266.246.144.43.338.548.583.121.24.181.518.181.83 0 .36-.082.68-.247.959a1.697 1.697 0 0 1-.7.642v.04c.423.098.758.298 1.004.603.249.305.373.706.373 1.205 0 .361-.063.686-.19.97-.125.285-.296.52-.516.707a2.31 2.31 0 0 1-.845.472c-.304.094-.69.141-1.159.141H8.12v-7.478zm1.659 1.372v1.582h.262c.263 0 .486-.007.672-.017.185-.01.332-.043.44-.1.15-.077.248-.175.294-.295.046-.124.07-.266.07-.427a.91.91 0 0 0-.083-.371.518.518 0 0 0-.282-.277 1.187 1.187 0 0 0-.456-.086c-.18-.007-.433-.01-.76-.01h-.157zm0 2.873V18.1H9.9c.469 0 .804-.002 1.007-.006.202-.003.39-.046.56-.13a.712.712 0 0 0 .357-.33c.067-.142.099-.302.099-.483 0-.237-.04-.42-.121-.547-.078-.13-.214-.228-.405-.291a1.842 1.842 0 0 0-.538-.072 49.47 49.47 0 0 0-.716-.003h-.366z" fill="#26a69a" stroke-width="1.067"/></symbol><symbol viewBox="0 0 24 24" id="c" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 15.97l.42 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96-1.14-1.27-1.68-2.88-1.68-4.83C6 9.9 6.68 8.13 8 6.89 9.28 5.64 10.92 5 12.9 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.6 2.49-1.04-.34c-.4-.1-.87-.15-1.4-.15-1.15-.01-2.11.36-2.86 1.1-.76.73-1.14 1.85-1.18 3.34.01 1.36.37 2.42 1.08 3.2.71.77 1.7 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.09-.32z" fill="#0277bd"/></symbol><symbol viewBox="0 0 300 300" id="cabal" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -822.52)" fill-rule="evenodd" color="#000"><rect transform="matrix(-.98339 .18149 .60192 .79856 0 0)" x="405.55" y="967.22" width="107.25" height="156.59" rx="12.306" ry="12.31" fill="#2d9bbd"/><rect transform="matrix(-.98528 .17093 -.59175 .80612 0 0)" x="-1156.5" y="1461.9" width="108.34" height="123.15" rx="10.69" ry="12.31" fill="#4a4bcd"/><path d="M52.112 965.158c-1.343 3.515-26.292 23.248-25.744 27.277.548 4.03 29.812 16.023 32.04 19.027s10.545 41.668 13.603 42.5 18.828-31.274 21.548-32.932c2.72-1.658 32.808 2.503 34.15-1.01 1.343-3.515-18.174-35.352-18.721-39.381-.548-4.03 9.732-40.12 7.502-43.125-2.229-3.005-30.06 9.427-33.118 8.594-3.059-.833-26.793-27.3-29.514-25.643-2.72 1.657-.405 41.177-1.747 44.693z" fill="#2e5bc1"/></g></symbol><symbol viewBox="0 0 24 24" id="cake" xmlns="http://www.w3.org/2000/svg"><path d="M12.254 6.621a1.807 1.807 0 0 0 1.808-1.807c0-.344-.09-.66-.262-.932l-1.546-2.684-1.546 2.684a1.72 1.72 0 0 0-.262.932 1.808 1.808 0 0 0 1.808 1.807m4.158 9.04l-.967-.976-.976.976c-1.175 1.166-3.236 1.175-4.42 0l-.959-.976-.994.976a3.134 3.134 0 0 1-3.977.353v4.167a.904.904 0 0 0 .904.904h14.463a.904.904 0 0 0 .904-.904v-4.167a3.134 3.134 0 0 1-3.977-.353m1.265-6.328h-4.52V7.525H11.35v1.808H6.83a2.712 2.712 0 0 0-2.711 2.712v1.392c0 .977.795 1.772 1.771 1.772.489 0 .94-.18 1.248-.515l1.952-1.926 1.908 1.926c.669.669 1.835.669 2.504 0l1.916-1.926 1.944 1.926c.316.334.768.515 1.247.515.976 0 1.78-.795 1.78-1.772v-1.392a2.712 2.712 0 0 0-2.711-2.712z" fill="#ff7043" stroke-width=".904"/></symbol><symbol viewBox="0 0 24 24" id="certificate" xmlns="http://www.w3.org/2000/svg"><path d="M4 3c-1.11 0-2 .89-2 2v10a2 2 0 0 0 2 2h8v5l3-3 3 3v-5h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4m8 2l3 2 3-2v3.5l3 1.5-3 1.5V15l-3-2-3 2v-3.5L9 10l3-1.5V5M4 5h5v2H4V5m0 4h3v2H4V9m0 4h5v2H4v-2z" fill="#ff5722"/></symbol><symbol viewBox="0 0 24 24" id="changelog" xmlns="http://www.w3.org/2000/svg"><path d="M11 7v5.11l4.71 2.79.79-1.28-4-2.37V7m0-5C8.97 2 5.91 3.92 4.27 6.77L2 4.5V11h6.5L5.75 8.25C6.96 5.73 9.5 4 12.5 4a7.5 7.5 0 0 1 7.5 7.5 7.5 7.5 0 0 1-7.5 7.5c-3.27 0-6.03-2.09-7.06-5h-2.1c1.1 4.03 4.77 7 9.16 7 5.24 0 9.5-4.25 9.5-9.5A9.5 9.5 0 0 0 12.5 2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="clojure" xmlns="http://www.w3.org/2000/svg"><path d="M3.355 1.78c-.845 0-1.525.68-1.525 1.525v17.441c0 .845.68 1.525 1.525 1.525h17.442c.845 0 1.525-.68 1.525-1.525V3.305c0-.845-.68-1.526-1.525-1.526H3.355zm6.168 2.572h1.963l6.368 14.931H15.93l-3.38-8.086-3.349 8.086H7.21l4.346-10.38-2.032-4.551z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="cmake" xmlns="http://www.w3.org/2000/svg"><path d="M11.99 2.965L2.977 20.999l9.874-8.47-.863-9.564z" fill="#1e88e5"/><path d="M12.007 2.963l.002.29 1.312 14.498-.001.006.023.26 7.362 2.979h.416l-.158-.311-.114-.228h-.002l-8.84-17.494z" fill="#e53935"/><path d="M8.607 16.11L2.98 20.995h17.743v-.016L8.607 16.11z" fill="#7cb342"/></symbol><symbol class="bfmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate" xmlns="http://www.w3.org/2000/svg"><path class="bfsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#eee" stroke-width="2.849"/></symbol><symbol class="bgmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate_light" xmlns="http://www.w3.org/2000/svg"><path class="bgsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#455a64" stroke-width="2.849"/></symbol><symbol viewBox="0 0 24 24" id="coffee" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="coldfusion" xmlns="http://www.w3.org/2000/svg"><rect transform="rotate(90)" x="2.283" y="-21.86" width="19.487" height="19.487" ry="0" fill="#0d3858" stroke="#4dd0e1" stroke-width=".7"/><text x="6.653" y="16.426" fill="#4dd0e1" font-family="Calibri" font-size="29.001" font-weight="bold" letter-spacing="0" stroke-width=".725" word-spacing="0" style="line-height:1.25"><tspan x="6.653" y="16.426" font-family="'Segoe UI'" font-size="10.634" font-weight="normal">C<tspan font-size="11.844">f</tspan></tspan></text></symbol><symbol viewBox="0 0 24 24" id="conduct" xmlns="http://www.w3.org/2000/svg"><path d="M10 17l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9m-6-6a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#cddc39"/></symbol><symbol viewBox="0 0 24 24" id="console" xmlns="http://www.w3.org/2000/svg"><path d="M20 19V7H4v12h16m0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16m-7 14v-2h5v2h-5m-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z" fill="#ff7043"/></symbol><symbol viewBox="0 0 24 24" id="contributing" xmlns="http://www.w3.org/2000/svg"><path d="M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="cpp" xmlns="http://www.w3.org/2000/svg"><path d="M10.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C1.56 15.77 1 14.16 1 12.21c.05-2.31.72-4.08 2-5.32C4.32 5.64 5.96 5 7.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M11 11h2V9h2v2h2v2h-2v2h-2v-2h-2v-2m7 0h2V9h2v2h2v2h-2v2h-2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="credits" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v2H3V3m4 4h10v2H7V7m-4 4h18v2H3v-2m4 4h10v2H7v-2m-4 4h18v2H3v-2z" fill="#9ccc65"/></symbol><symbol viewBox="0 0 200 200" id="crystal" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" stroke-width="1.153" fill="#cfd8dc"/></symbol><symbol viewBox="0 0 200 200" id="crystal_light" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" fill="#37474f" stroke-width="1.153"/></symbol><symbol viewBox="0 0 24 24" id="csharp" xmlns="http://www.w3.org/2000/svg"><path d="M11.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C2.56 15.77 2 14.16 2 12.21c.05-2.31.72-4.08 2-5.32C5.32 5.64 6.96 5 8.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M13.89 19l.61-4H13l.34-2h1.5l.32-2h-1.5L14 9h1.5l.61-4h2l-.61 4h1l.61-4h2l-.61 4H22l-.34 2h-1.5l-.32 2h1.5L21 15h-1.5l-.61 4h-2l.61-4h-1l-.61 4h-2m2.95-6h1l.32-2h-1l-.32 2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="css" xmlns="http://www.w3.org/2000/svg"><path d="M5 3l-.65 3.34h13.59L17.5 8.5H3.92l-.66 3.33h13.59l-.76 3.81-5.48 1.81-4.75-1.81.33-1.64H2.85l-.79 4 7.85 3 9.05-3 1.2-6.03.24-1.21L21.94 3H5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="css-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#42a5f5"/><path d="M4.676 3l-.488 2.51h10.211l-.33 1.623H3.864l-.496 2.502H13.58l-.57 2.863-4.119 1.36-3.569-1.36.248-1.232H3.06l-.593 3.005 5.898 2.254 6.8-2.254.902-4.53.18-.91L17.406 3H4.675z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 33 33" id="cucumber" xmlns="http://www.w3.org/2000/svg"><title>cucumber-mark-transparent-pips</title><g transform="translate(0 -5)" fill="none" fill-rule="evenodd"><path d="M-4-1h40v40H-4z"/><path d="M16.641 7.092c-7.028 0-12.714 5.686-12.714 12.714 0 6.187 4.435 11.327 10.288 12.471v3.64C21.824 34.77 28.561 28.73 29.063 20.8c.303-4.772-2.076-9.644-6.09-12.01a10.575 10.575 0 0 0-1.455-.728l-.243-.097c-.223-.082-.448-.175-.68-.242a12.614 12.614 0 0 0-3.954-.632zm2.62 4.707a1.387 1.387 0 0 0-1.213.485c-.233.31-.379.611-.534.923-.466 1.087-.31 2.251.388 3.105 1.087-.233 2.01-.927 2.475-2.014a2.45 2.45 0 0 0 .243-1.02c.048-.824-.634-1.404-1.359-1.479zm-5.654.073c-.708.068-1.382.63-1.382 1.407 0 .31.087.709.243 1.02.466 1.086 1.46 1.78 2.546 2.013.621-.854.782-2.018.316-3.105-.155-.311-.3-.617-.534-.85a1.364 1.364 0 0 0-1.188-.485zm-3.809 3.735c-1.224.063-1.77 1.602-.752 2.402.31.233.612.403.922.559 1.087.466 2.344.306 3.275-.316-.233-1.009-1.023-1.936-2.11-2.402-.388-.155-.703-.243-1.092-.243-.087-.009-.161-.004-.243 0zm11.961 4.708a3.551 3.551 0 0 0-2.013.582c.233 1.01 1.023 1.936 2.11 2.401.389.156.705.244 1.093.244 1.397.077 2.08-1.65.994-2.427-.31-.233-.611-.379-.922-.534a3.354 3.354 0 0 0-1.262-.266zm-10.603.072a3.376 3.376 0 0 0-1.261.267c-.389.155-.69.325-.923.558-1.009.854-.33 2.48 1.068 2.402.388 0 .782-.087 1.092-.243 1.087-.465 1.859-1.392 2.014-2.401a3.474 3.474 0 0 0-1.99-.582zm3.931 2.378c-1.087.233-2.009.927-2.475 2.014-.155.31-.243.684-.243.995-.077 1.32 1.724 2.028 2.5 1.02.233-.312.378-.613.534-.923.466-1.01.306-2.174-.316-3.106zm2.887.073c-.621.854-.781 2.019-.315 3.106.155.31.3.615.534.848.854.932 2.65.243 2.572-.921 0-.31-.088-.71-.243-1.02-.466-1.087-1.46-1.78-2.547-2.013z" fill="#4caf50" stroke-width=".776"/></g></symbol><symbol id="cuda" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style>.bust0{fill:#76b900}</style><title>NVIDIA-Logo</title><path id="buEye_Mark" class="bust0" d="M76.362 75.199V64.116c1.095-.068 2.19-.137 3.284-.137 30.377-.958 50.286 26.135 50.286 26.135s-21.483 29.83-44.539 29.83c-3.079 0-6.089-.48-8.962-1.438v-33.66c11.836 1.436 14.23 6.636 21.277 18.471l15.804-13.273s-11.562-15.12-30.992-15.12c-2.053-.068-4.105.069-6.158.274m0-36.67v16.556l3.284-.205c42.213-1.437 69.784 34.618 69.784 34.618s-31.608 38.45-64.516 38.45c-2.873 0-5.678-.274-8.483-.753v10.262c2.326.274 4.72.48 7.046.48 30.65 0 52.817-15.668 74.3-34.14 3.558 2.874 18.13 9.784 21.14 12.794-20.388 17.104-67.937 30.856-94.893 30.856-2.6 0-5.062-.137-7.525-.41v14.436h116.44V38.532zm0 79.977v8.757C48.038 122.2 40.17 92.712 40.17 92.712s13.615-15.05 36.192-17.514v9.579h-.068c-11.836-1.437-21.14 9.646-21.14 9.646s5.268 18.678 21.209 24.082M26.077 91.481S42.839 66.714 76.43 64.115v-9.03C39.213 58.094 7.057 89.565 7.057 89.565s18.199 52.68 69.305 57.47v-9.579c-37.492-4.652-50.286-45.975-50.286-45.975z" fill="#8bc34a" stroke-width=".684"/></symbol><symbol viewBox="0 0 24 24" id="dart" xmlns="http://www.w3.org/2000/svg"><title>Dart</title><path d="M12.486 1.385a.978.978 0 0 0-.682.281l-.01.007-6.387 3.692 6.371 6.372v.004l7.659 7.659 1.46-2.63-5.265-12.64-2.456-2.457a.972.972 0 0 0-.69-.288z" fill="#00ca94"/><path d="M5.422 5.35L1.73 11.733l-.007.01a.967.967 0 0 0 .006 1.371l3.059 3.061 11.963 4.706 2.704-1.502-.073-.073-.018.002-7.5-7.512h-.01L5.423 5.35z" fill="#1565c0"/><path d="M5.405 5.353l6.518 6.525h.01l7.502 7.51 2.855-.544.005-8.449-3.016-2.955c-.66-.647-1.675-1.064-2.695-1.202l.002-.032-11.181-.853z" fill="#1565c0"/><path d="M5.414 5.361l6.521 6.522v.009l7.506 7.506-.546 2.855h-8.448l-2.954-3.017c-.647-.66-1.064-1.676-1.2-2.696l-.033.003L5.414 5.36z" fill="#00ee94"/></symbol><symbol viewBox="0 0 24 24" id="database" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.58 3 4 4.79 4 7s3.58 4 8 4 8-1.79 8-4-3.58-4-8-4M4 9v3c0 2.21 3.58 4 8 4s8-1.79 8-4V9c0 2.21-3.58 4-8 4s-8-1.79-8-4m0 5v3c0 2.21 3.58 4 8 4s8-1.79 8-4v-3c0 2.21-3.58 4-8 4s-8-1.79-8-4z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="diff" xmlns="http://www.w3.org/2000/svg"><path d="M3 1c-1.11 0-2 .89-2 2v11c0 1.11.89 2 2 2h2v-2H3V3h11v2h2V3c0-1.11-.89-2-2-2H3m6 6c-1.11 0-2 .89-2 2v2h2V9h2V7H9m4 0v2h1v1h2V7h-3m5 0v2h2v11H9v-2H7v2c0 1.11.89 2 2 2h11c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-2m-4 5v2h-2v2h2c1.11 0 2-.89 2-2v-2h-2m-7 1v3h3v-2H9v-1H7z" fill="#42a5f5"/></symbol><symbol id="docker" viewBox="0 0 41 34.5" xmlns="http://www.w3.org/2000/svg"><style id="bystyle2">.byst0{fill:#fff}.byst1{clip-path:url(#bySVGID_4_)}</style><g id="byg34" transform="translate(.292 1.9)" fill="#0087c9"><g id="byg32"><g id="byg30"><title id="bytitle4">Group 3</title><g id="byg28"><g id="byg26"><g id="byg9"><path id="bySVGID_1_" class="byst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="byg24"><defs id="bydefs12"><path id="bySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><clipPath id="bySVGID_4_"><use xlink:href="#bySVGID_2_" id="byuse14" width="100%" height="100%" overflow="visible"/></clipPath><g class="byst1" clip-path="url(#bySVGID_4_)" id="byg22"><g id="byg20"><g id="byg18"><path id="bySVGID_3_" class="byst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol viewBox="0 0 24 24" id="document" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m9 16v-2H6v2h9m3-4v-2H6v2h12z" fill="#42a5f5"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone_light" xmlns="http://www.w3.org/2000/svg"><g fill="#424242" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol viewBox="0 0 3473 3473" id="editorconfig" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" xmlns="http://www.w3.org/2000/svg"><defs id="ccdefs4"><style id="ccstyle2">.ccfil2{fill:#020202}.ccfil0{fill:#e3e3f8}.ccfil5{fill:#efefef}.ccfil6{fill:#faf1f1}.ccfil3{fill:#fdf2f2}.ccfil1{fill:#fdfdfd}.ccfil4{fill:#fef3f3}</style></defs><g id="ccLayer_x0020_1" transform="matrix(.8945 0 0 .8945 138.649 275.985)"><g id="cc_631799120"><g id="ccg11"><path class="ccfil0" d="M967 1895c46-30 84-105 61-158-63 27-60 89-61 158z" id="ccpath7" fill="#e3e3f8"/><path class="ccfil0" d="M1679 2067c50-16 98-72 71-130-39 27-64 64-71 130z" id="ccpath9" fill="#e3e3f8"/></g><g id="ccg21"><path class="ccfil1" d="M280 2895c0 63 16 131 60 155 162 91 730 20 923-23 101-23 183-98 278-139 214-93 369-168 540-293 124-91 321-347 342-500l-169-38c-4 172-43 211-196 251-103 28-304 34-409 16-139-23-202-96-265-179-122-162 27-275-166-286-203 249-561 70-718 45-67 97-224 727-222 871 97-33 158 3 245 37 308 119 39 224-84 193-84-20-110-75-159-110z" id="ccpath13" fill="#fdfdfd"/><path class="ccfil1" d="M683 1458c125 24 236 76 342 129 173 86 204 74 220 194 2 22-2 34 61 54 106 33-61-26 223-25 169 1 556 69 681 148 52 33 42 75 218 70-2-207-57-516-138-706-99-230-230-265-497-351-156-50-614-105-756-17-133 83-158 182-282 356-36 51-49 90-72 148z" id="ccpath15" fill="#fdfdfd"/><path class="ccfil1" d="M1784 1883c100 41-5 306-144 242-45-127 62-199 91-256-60-9-231-36-282-17-66 25-81 166-47 232 160 314 867 247 792 3-30-99-58-115-159-149-81-27-162-55-251-55z" id="ccpath17" fill="#fdfdfd"/><path class="ccfil1" d="M527 1848c80 77 261 89 378 95 15-155 28-271 152-262 61 83 29 181-35 244 109-1 172-83 156-202-92-66-371-198-511-217-39 42-135 272-140 342z" id="ccpath19" fill="#fdfdfd"/></g><path class="ccfil2" d="M339 2838c66-6 238 44 252 100-107 13-243 3-252-100zm-59 57c49 35 75 90 159 110 123 31 392-74 84-193-87-34-148-70-245-37-2-144 155-774 222-871 157 25 515 204 718-45 193 11 44 124 166 286 63 83 126 156 265 179 105 18 306 12 409-16 153-40 192-79 196-251l169 38c-21 153-218 409-342 500-171 125-326 200-540 293-95 41-177 116-278 139-193 43-761 114-923 23-44-24-60-92-60-155zm1399-828c7-66 32-103 71-130 27 58-21 114-71 130zm105-184c89 0 170 28 251 55 101 34 129 50 159 149 75 244-632 311-792-3-34-66-19-207 47-232 51-19 222 8 282 17-29 57-136 129-91 256 139 64 244-201 144-242zm-817 12c1-69-2-131 61-158 23 53-15 128-61 158zm-440-47c5-70 101-300 140-342 140 19 419 151 511 217 16 119-47 201-156 202 64-63 96-161 35-244-124-9-137 107-152 262-117-6-298-18-378-95zm-100-80c-37-102-37-261 120-274l-80 223c-21 48-21 37-40 51zm256-310c23-58 36-97 72-148 124-174 149-273 282-356 142-88 600-33 756 17 267 86 398 121 497 351 81 190 136 499 138 706-176 5-166-37-218-70-125-79-512-147-681-148-284-1-117 58-223 25-63-20-59-32-61-54-16-120-47-108-220-194-106-53-217-105-342-129zm1770-49c-19-63 16-59 77-102 35-25 63-51 106-75 161-90 461-105 589 2 52 43 137 127 124 237-27 219-177 339-300 439-125 102-333 207-548 137-18-44-4-323-25-426-19-92-9-102 44-157 156-162 494-280 686-141 81 60 58 83 100 129 52-56-45-244-403-232-243 8-348 198-450 189zM997 840c5-139 133-427 261-527 155-120 317-233 555-98 59 33 56 50 62 132 5 79-2 108-22 172-158 510-290 217-796 338 19-166 163-314 243-391 137-133 236-219 442-191 57 95 63 155-6 266-92 148-115 139-101 240 72-18 94-88 127-158 201-420-91-471-270-394-120 51-334 287-404 429-14 28-29 64-42 95zm792 21c21-125 145-156 145-541 0-166-204-315-471-204-229 94-264 166-386 350-115 174-111 365-210 526-29 46-55 62-87 108-23 34-40 77-63 117-47 77-95 133-133 225-120 3-221 5-233 129-16 170 64 212 64 276-1 69-281 765-203 1180 22 114 97 115 217 129 289 35 664 23 923-81l470-225c119-67 319-194 408-287 63-65 96-120 150-197 74-108 76-106 92-253 98 18 281 61 342 114-7 69-41 36-41 98 39 1 104-48 120-102-41-60-84-50-143-98 47-37 132-54 197-81 140-58 379-234 438-394 47-129 12-344-64-428-80-88-266-133-418-133-181 0-368 130-514 186-56-49-60-105-101-159-47-64-353-224-499-255z" id="ccpath23" fill="#020202"/><path class="ccfil3" d="M2453 1409c102 9 207-181 450-189 358-12 455 176 403 232-42-46-19-69-100-129-192-139-530-21-686 141-53 55-63 65-44 157 21 103 7 382 25 426 215 70 423-35 548-137 123-100 273-220 300-439 13-110-72-194-124-237-128-107-428-92-589-2-43 24-71 50-106 75-61 43-96 39-77 102z" id="ccpath25" fill="#fdf2f2"/><path class="ccfil4" d="M997 840l49-87c13-31 28-67 42-95 70-142 284-378 404-429 179-77 471-26 270 394-33 70-55 140-127 158-14-101 9-92 101-240 69-111 63-171 6-266-206-28-305 58-442 191-80 77-224 225-243 391 506-121 638 172 796-338 20-64 27-93 22-172-6-82-3-99-62-132-238-135-400-22-555 98-128 100-256 388-261 527z" id="ccpath27" fill="#fef3f3"/><path class="ccfil5" d="M427 1768c19-14 19-3 40-51l80-223c-157 13-157 172-120 274z" id="ccpath29" fill="#efefef"/><path class="ccfil6" d="M591 2938c-14-56-186-106-252-100 9 103 145 113 252 100z" id="ccpath31" fill="#faf1f1"/></g></g></symbol><symbol viewBox="0 0 24 24" id="elixir" xmlns="http://www.w3.org/2000/svg"><path d="M12.431 22.383c-3.86 0-6.99-3.64-6.99-8.13 0-3.678 2.774-8.172 4.916-10.91 1.014-1.295 2.931-2.321 2.931-2.321s-.982 5.238 1.683 7.318c2.365 1.847 4.105 4.25 4.105 6.363 0 4.232-2.784 7.68-6.645 7.68z" fill="#9575cd" stroke-width="1.256"/></symbol><symbol viewBox="0 0 323.00001 322.99999" id="elm" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.8053 0 0 .8053 30.106 31.524)"><path fill="#f0ad00" d="M160.8 153.865l68.028-68.03H92.77z"/><path fill="#7fd13b" d="M160.983 5.098H12.033l68.524 68.525H229.51z"/><path fill="#7fd13b" stroke-width=".974" d="M243.906 88.021l74.136 74.137-74.474 74.475-74.137-74.137z"/><path fill="#60b5cc" d="M318.2 145.045V5.098H178.252z"/><path fill="#5a6378" d="M152.164 162.499L3.4 13.733v297.533z"/><path fill="#f0ad00" d="M252.205 245.27l65.995 65.996v-131.99z"/><path fill="#60b5cc" d="M160.8 171.134L12.034 319.899h297.53z"/></g></symbol><symbol viewBox="0 0 24 24" id="email" xmlns="http://www.w3.org/2000/svg"><path d="M20 8l-8 5-8-5V6l8 5 8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 30 30" id="erlang" xmlns="http://www.w3.org/2000/svg"><path style="line-height:1.25;-inkscape-font-specification:'Wide Latin'" d="M5.217 4.367c-.048.052-.097.1-.144.153C2.697 7.182 1.51 10.798 1.51 15.366c0 4.418 1.156 7.862 3.46 10.34h19.414c2.553-1.152 4.127-3.43 4.127-3.43l-3.147-2.52-1.454 1.381c-.866.773-.845.931-2.314 1.78-1.496.674-3.04.966-4.634.966-2.516 0-4.423-.909-5.723-2.059-1.286-1.15-1.985-4.511-2.097-6.68l17.458.067-.182-1.472s-.847-7.129-2.542-9.372zm8.76.846c1.565 0 3.22.535 3.96 1.471.742.937.932 1.667.974 3.524H9.12c.111-1.955.436-2.81 1.372-3.697.937-.888 2.03-1.298 3.484-1.298z" font-weight="400" font-size="48" font-family="Wide Latin" letter-spacing="0" word-spacing="0" fill="#f44336" stroke-width=".97"/></symbol><symbol viewBox="0 0 299.99999 300.00001" id="eslint" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-2.88 18.438) scale(1.0344)"><path d="M97.021 99.016l48.432-27.962c1.212-.7 2.706-.7 3.918 0l48.433 27.962a3.92 3.92 0 0 1 1.959 3.393v55.924a3.924 3.924 0 0 1-1.959 3.394l-48.433 27.962c-1.212.7-2.706.7-3.918 0l-48.432-27.962a3.92 3.92 0 0 1-1.959-3.394v-55.924a3.922 3.922 0 0 1 1.959-3.393" fill="#7986cb"/><path d="M273.34 124.49L215.473 23.82c-2.102-3.64-5.985-6.325-10.188-6.325H89.545c-4.204 0-8.088 2.685-10.19 6.325L21.488 124.27c-2.102 3.641-2.102 8.236 0 11.877l57.867 99.847c2.102 3.64 5.986 5.501 10.19 5.501h115.74c4.203 0 8.087-1.805 10.188-5.446l57.867-100.01c2.104-3.639 2.104-7.907.001-11.547m-47.917 48.41c0 1.48-.891 2.849-2.174 3.59l-73.71 42.527a4.194 4.194 0 0 1-4.17 0l-73.767-42.527c-1.282-.741-2.179-2.109-2.179-3.59V87.847c0-1.481.884-2.849 2.167-3.59l73.707-42.527a4.185 4.185 0 0 1 4.168 0l73.772 42.527c1.283.741 2.186 2.109 2.186 3.59z" fill="#3f51b5"/></g></symbol><symbol viewBox="0 0 24 24" id="exe" xmlns="http://www.w3.org/2000/svg"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14m0 14V8H5v10h14z" fill="#e64a19"/></symbol><symbol viewBox="0 0 24 24" id="favicon" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.45 4.73L5.82 21 12 17.27z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="file" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 400 400" id="firebase" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 103)"><path d="M72.55 208.77l44.456-292.29 56.209 90.445L195.49-37.57 330.6 209.28z" fill="#ffa712"/><path d="M195.7 276.73l134.9-67.45-46.5-224.83L72.55 208.77z" fill="#fcca3f"/><path d="M173.22 6.932L72.56 208.772l136.35-144.58z" fill="#f6820c"/></g></symbol><symbol viewBox="0 0 24 24" id="flash" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cma"><stop offset="0" stop-color="#d92f3c"/><stop offset="1" stop-color="#791223"/></linearGradient><linearGradient xlink:href="#cma" id="cmb" x1="2.373" y1="12.027" x2="21.86" y2="12.027" gradientUnits="userSpaceOnUse" gradientTransform="translate(-.09 -24.144)"/></defs><rect width="19.487" height="19.487" x="2.283" y="-21.86" transform="rotate(90)" ry="0" fill="url(#cmb)"/><path style="line-height:125%" d="M16.802 5.768l-.013.002a6.43 6.43 0 0 0-1.182.192 5.062 5.062 0 0 0-1.494.718c-.428.323-.817.72-1.17 1.191-.34.48-.682 1.032-1.022 1.66-.12.228-.233.424-.35.636v.002h-.004l-1.34 2.394-.005-.002c-.238.443-.461.847-.665 1.198a4.358 4.358 0 0 1-.716.94 2.79 2.79 0 0 1-.907.594c-.072.027-.161.042-.242.063h-.989v2.414h.989v-.002a6.427 6.427 0 0 0 1.185-.192 5.062 5.062 0 0 0 1.494-.718 5.94 5.94 0 0 0 1.171-1.191c.34-.48.681-1.033 1.021-1.66.12-.228.235-.425.353-.637l.006.002.003-.005.037-.066h2.53v.002h1.124v-2.408h-.33v-.001h-1.98c.22-.407.432-.789.621-1.115.214-.37.452-.682.717-.94a2.79 2.79 0 0 1 .906-.594c.07-.027.16-.041.239-.061h.992V8.18h-.002V5.77h-.977v-.002z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol class="cnflow-logo" viewBox="0 0 299.99999 300" id="flow" xmlns="http://www.w3.org/2000/svg"><title>Flow logo</title><path d="M38.75 33.427l77.461 77.47H54.436l61.145 61.16H38.437l93.462 93.478v-77.158l.01-.01v-77.47h-.01V66.982h46.691l20.394 20.393H153.57v76.531h22.05l24.474 24.473h-15.806l-.01-.01v.01h-31.665l-.01-.01v.01h-.313l.313.313v77.148h109.149l-39.2-39.2v-15.806l8.465 8.466v-77.37h-15.682l.017-38.191 30.09 30.086V56.362h-64.874l-22.94-22.934H113.71z" fill="#fbc02d" fill-opacity=".976" stroke-width=".955" class="cnflow-logo-mark"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="coa" x1="-388.15%" x2="237.68%" y1="-144.18%" y2="430.41%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cob" x1="72.945%" x2="-97.052%" y1="84.424%" y2="-147.7%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="coc" x1="-283.88%" x2="287.54%" y1="-693.6%" y2="101.71%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cod" x1="-821.19%" x2="101.99%" y1="-469.05%" y2="288.24%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coe" x1="-140.36%" x2="419.01%" y1="-230.93%" y2="261.98%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cof" x1="191.08%" x2="20.358%" y1="253.95%" y2="20.403%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cog" x1="-388.09%" x2="237.67%" y1="-173.85%" y2="518.99%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#coa"/><linearGradient id="coj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cob"/><linearGradient id="cok" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#coc"/><linearGradient id="col" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cod"/><linearGradient id="com" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#coe"/><linearGradient id="con" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cof"/><linearGradient id="coo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cog"/><linearGradient id="cop" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#coh"/><linearGradient id="coh" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#coi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#coj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cok)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#col)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#com)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#con)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#coo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cop)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia-open" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cpi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpa"/><linearGradient id="cpa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cpb"/><linearGradient id="cpb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#cpc"/><linearGradient id="cpc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cpd"/><linearGradient id="cpd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#cpe"/><linearGradient id="cpe" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cpf"/><linearGradient id="cpf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpg"/><linearGradient id="cpg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpp" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#cph"/><linearGradient id="cph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#cpi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#cpj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cpk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#cpl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#cpm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#cpn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#cpo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cpp)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#cddc39" fill-rule="nonzero"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#cddc39"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00acc1" fill-rule="nonzero"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00acc1"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#e57373" fill-rule="nonzero"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" stroke-width=".644"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#e57373"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" fill-rule="evenodd" stroke-width=".644"/></symbol><symbol id="folder-docker" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><defs id="cydefs10"><path id="cySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><path id="cypath2" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><style id="cystyle2">.cyst0{fill:#fff}.cyst1{clip-path:url(#cySVGID_4_)}</style><g id="cyg34" transform="translate(8.319 9.626) scale(.39491)" fill="#b3e5fc"><g id="cyg32"><g id="cyg30"><title id="cytitle4">Group 3</title><g id="cyg28"><g id="cyg26"><g id="cyg9"><path id="cySVGID_1_" class="cyst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="cyg24"><clipPath id="cySVGID_4_"><use id="cyuse14" width="100%" height="100%" xlink:href="#cySVGID_2_"/></clipPath><g id="cyg22" class="cyst1" clip-path="url(#cySVGID_4_)"><g id="cyg20"><g id="cyg18"><path id="cySVGID_3_" class="cyst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docker-open" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="cza"><use width="100%" height="100%" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#SVGID_2_"/></clipPath></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><g transform="matrix(.3949 0 0 .39489 8.319 9.626)" fill="#b3e5fc"><title>Group 3</title><path class="czst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/><g class="czst1" clip-path="url(#cza)"><path class="czst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#01579b" fill-rule="nonzero"/><style>.dcst0{fill:#1173b6}.st1{fill:#585d67}</style><path class="dcst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#01579b"/><path class="ddst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M21.132 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.217 1.217m.608-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M21.133 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.216 1.217m.609-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ffca28" fill-rule="nonzero"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.411 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill-rule="nonzero" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ffca28"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.412 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#c0ca33" fill-rule="nonzero"/><path d="M17.39 12.544a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#c0ca33"/><path d="M17.391 12.543a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.036 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.968 4.968 0 0 1-2.679 2.203m-.155-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.1-1.238h2.894c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399h-1.806a4.902 4.902 0 0 1 2.672-2.202c-.37.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.049.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.94 4.94 0 0 1 2.679 2.202m-4.281-3.712a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.037 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.967 4.967 0 0 1-2.68 2.203m-.154-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.099-1.238h2.895c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399H13.06a4.902 4.902 0 0 1 2.672-2.202c-.371.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.05.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.941 4.941 0 0 1 2.679 2.202M17.34 9.322a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M16.473 13.927c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a17.015 17.015 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359a5.558 5.558 0 0 0-.203.604c.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.569-1.053c-.21-.372-.435-.702-.639-1.032-.38-.022-.78-.022-1.2-.022-.422 0-.823 0-1.202.022-.204.33-.428.66-.639 1.032l-.569 1.053.57 1.054c.21.372.434.702.638 1.032.38.021.78.021 1.201.021.421 0 .822 0 1.201-.02.204-.331.428-.661.639-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.245-9.284c-.436-.267-1.405.14-2.522 1.194.366.414.724.864 1.06 1.334.577.057 1.146.14 1.686.253.359-1.503.225-2.535-.224-2.78m-.492 4.03l.204.358c.077-.203.154-.407.203-.604-.19-.042-.4-.077-.618-.112l.211.358m1.018-4.95c1.033.589 1.145 2.141.71 3.953 1.784.527 3.069 1.398 3.069 2.584 0 1.187-1.285 2.058-3.07 2.585.436 1.812.324 3.364-.709 3.954-1.025.59-2.423-.085-3.77-1.37-1.35 1.285-2.747 1.96-3.78 1.37-1.025-.59-1.137-2.142-.702-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.203 6.538c.239.527.45 1.054.625 1.588 1.475-.443 2.303-1.075 2.303-1.588 0-.512-.828-1.144-2.303-1.587a15.81 15.81 0 0 1-.625 1.587m-7.136 0a15.806 15.806 0 0 1-.625-1.587c-1.474.443-2.303 1.075-2.303 1.587 0 .513.829 1.145 2.303 1.588.176-.534.387-1.06.625-1.588m6.321 1.588l-.21.358c.217-.035.428-.07.617-.113-.049-.196-.126-.4-.203-.604l-.204.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.522 1.194.45-.246.583-1.278.224-2.781-.54.112-1.11.196-1.685.253-.337.47-.695.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.113.049.196.126.4.203.604l.204-.359m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M16.473 13.928c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a16.997 16.997 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359c-.077.204-.154.408-.203.604.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.57-1.053c-.21-.372-.434-.702-.638-1.032-.38-.022-.78-.022-1.201-.022-.421 0-.822 0-1.2.022-.205.33-.43.66-.64 1.032l-.569 1.053.569 1.054c.21.372.435.702.64 1.032.378.021.779.021 1.2.021.421 0 .822 0 1.2-.02.205-.33.43-.661.64-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.244-9.284c-.435-.267-1.404.14-2.52 1.194.364.414.723.864 1.06 1.334.575.057 1.144.14 1.685.253.358-1.503.225-2.535-.225-2.78m-.491 4.03l.203.358c.078-.203.155-.407.204-.604-.19-.042-.4-.077-.618-.112l.21.358m1.02-4.95c1.032.589 1.144 2.141.708 3.953 1.784.527 3.07 1.398 3.07 2.584 0 1.187-1.286 2.058-3.07 2.585.436 1.812.323 3.364-.709 3.954-1.025.59-2.423-.085-3.771-1.37-1.348 1.285-2.746 1.96-3.778 1.37-1.026-.59-1.138-2.142-.703-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.204 6.538c.24.527.45 1.054.625 1.588 1.475-.443 2.304-1.075 2.304-1.588 0-.512-.829-1.144-2.304-1.587a15.81 15.81 0 0 1-.625 1.587m-7.135 0a15.808 15.808 0 0 1-.625-1.587c-1.475.443-2.303 1.075-2.303 1.587 0 .513.828 1.145 2.303 1.588.176-.534.386-1.06.625-1.588m6.32 1.588l-.21.358c.218-.035.428-.07.618-.113a5.56 5.56 0 0 0-.204-.604l-.203.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.521 1.194.45-.246.583-1.278.225-2.781-.54.112-1.11.196-1.685.253-.338.47-.696.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.112.049.197.126.4.203.604l.204-.358m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#fbc02d" fill-rule="nonzero"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.434h-1.217v8.518a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#fbc02d"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.433h-1.217v8.519a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#4caf50" fill-rule="nonzero"/><g fill="#c8e6c9" transform="translate(2.065 -.225) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#4caf50"/><g fill="#c8e6c9" fill-rule="evenodd" transform="translate(2.064 -.224) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1e88e5" fill-rule="nonzero"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1e88e5"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.811 8.52l-5.988 5.506-3.346-2.522-1.383.805 3.298 3.03-3.298 3.032 1.383.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.622v6.396l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.81 8.52l-5.988 5.506-3.346-2.522-1.384.805 3.3 3.03-3.3 3.032 1.384.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.621v6.397l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><g transform="translate(8.459 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688"/><g transform="translate(8.458 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#03a9f4" fill-rule="nonzero"/><g transform="translate(9.192 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#03a9f4"/><g transform="translate(9.193 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol viewBox="0 0 24 24" id="font" xmlns="http://www.w3.org/2000/svg"><path d="M9.62 12L12 5.67 14.37 12M11 3L5.5 17h2.25l1.12-3h6.25l1.13 3h2.25L13 3h-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 500 500" id="fsharp" xmlns="http://www.w3.org/2000/svg"><path d="M235.906 36.66L21.963 250.601l213.943 213.943v-84.36L106.209 250.487l129.697-129.696z" fill="#378bba" stroke-width="14.706"/><path d="M235.906 156.614l-93.622 93.62 93.622 93.622z" fill="#378bba" stroke-width="15.006"/><path d="M263.417 36.64L477.36 250.583 263.417 464.526v-84.36l129.696-129.697-129.696-129.696z" fill="#30b9db" stroke-width="14.706"/></symbol><symbol viewBox="0 0 152.99 160.01" id="fusebox" xmlns="http://www.w3.org/2000/svg"><defs id="fkdefs4"><style id="fkstyle2">.fkcls-1{fill:#fff}.fkcls-2{fill:#515151}.fkcls-3{fill:#1d79bf}.fkcls-4{fill:#383838}</style></defs><title id="fktitle6">Asset 3</title><g id="fkLayer_2" data-name="Layer 2" transform="matrix(.87285 0 0 .87285 10.17 10.175)"><g id="fkFuse_Box" data-name="Fuse Box"><g id="fkLOGO"><path class="fkcls-1" id="fkpolygon8" fill="#fff" d="M76.56 2.19l74.22 24.93-7.7 87.77-65.41 42.66-69.79-43.93-5.7-86.13z"/><path class="fkcls-2" d="M77.69 160L5.87 114.81 0 26 76.55 0 153 25.67l-7.94 90.4zM9.88 112.43l67.77 42.66 63.45-41.39 7.47-85.13-72-24.18L4.36 28.95z" id="fkpath10" fill="#515151"/><path class="fkcls-3" id="fkpolygon12" fill="#1d79bf" d="M76.4 148.8V61.68l66.93-29.82-5.99 78.77z"/><path id="fkF" class="fkcls-4" fill="#383838" d="M76.4 148.8l-60.35-37.39L9.63 31.8 76.4 61.68z"/><path class="fkcls-1" d="M25.58 52.73l.54 15.93 37.35 18.18.12 14.69-37-18.21 1.64 37.1-14.56-9-5.05-80.55 67.79 30.82v15.46z" id="fkpath15" fill="#fff"/><path class="fkcls-1" d="M135.91 90.77c-.08 13.12-6.33 26.59-16.77 33.12l-42.8 27.93V61.71l42.27-18.84c5.16-2.41 9.51-1.43 12.4 3.11 1.9 3 2.89 7.23 2.86 12.21A35.69 35.69 0 0 1 129.34 76c4.29 2 6.66 6.55 6.57 14.77zM123 63.76c0-4.64-2-6.93-4.92-5.45l-29 14.48L89 90l29.44-15.59c2.5-1.32 4.56-5.91 4.56-10.65zM125.15 96c0-5.71-2.42-8.24-6.55-5.93L89 106.64v19.58l29.34-17.46c4.43-2.64 6.79-7.27 6.81-12.76z" id="fkpath17" fill="#fff"/><path id="fkTOP" class="fkcls-4" fill="#383838" d="M76.4 8.82L9.71 31.77l109.77 2.38-84.02 9.21L76.4 61.68l20.76-9.25-27.73-1.37 49.78-8.46 24.12-10.74z"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="git" xmlns="http://www.w3.org/2000/svg"><path d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2 2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2 2 2 0 0 0 2-2 2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82z" fill="#e64a19"/></symbol><symbol viewBox="0 0 494 455" id="gitlab" xmlns="http://www.w3.org/2000/svg"><title>logo</title><defs><path id="fma" d="M0 1173.3h2000V0H0v1173.3z"/></defs><g transform="matrix(.88256 0 0 -.88256 -286.767 742.766)" fill="none" fill-rule="evenodd"><mask id="fmb" fill="#fff"><use width="100%" height="100%" xlink:href="#fma"/></mask><g mask="url(#fmb)"><g transform="translate(358.67 358.67)"><path d="M492.532 195.445l-27.559 84.815-54.617 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.619-168.1h-181.37l-54.62 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.617-168.1-27.557-84.815a18.775 18.775 0 0 1 6.82-20.992l238.51-173.29 238.51 173.29a18.777 18.777 0 0 1 6.82 20.992" fill="#fc6d26"/><path d="M247.2 1.16l90.684 279.1h-181.37z" fill="#e24329"/><path d="M247.201 1.16l-90.684 279.09H29.427z" fill="#fc6d26"/><path d="M29.422 280.256L1.862 195.44a18.774 18.774 0 0 1 6.822-20.991L247.194 1.16z" fill="#fca326"/><path d="M29.422 280.26h127.09l-54.619 168.1c-2.81 8.65-15.047 8.65-17.856 0z" fill="#e24329"/><path d="M247.2 1.16l90.684 279.09h127.09z" fill="#fc6d26"/><path d="M464.98 280.256l27.559-84.815a18.774 18.774 0 0 0-6.821-20.991L247.208 1.16z" fill="#fca326"/><path d="M464.97 280.26H337.88l54.619 168.1c2.81 8.65 15.047 8.65 17.856 0z" fill="#e24329"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="go" xmlns="http://www.w3.org/2000/svg"><path d="M10.575 1.695c-2.634 0-4.756 2.453-4.756 5.502v4.6l-.027-.003v4.71c0 3.05 2.123 5.502 4.757 5.502h2.286c2.634 0 4.757-2.453 4.757-5.502v-4.6a5.1 5.1 0 0 0 .026.003v-4.71c0-3.049-2.122-5.502-4.756-5.502h-2.287z" fill="#73cddc"/><rect width="2.289" height="3.335" x="-1.178" y="6.092" ry="1.125" transform="matrix(.4849 -.87457 .85979 .51065 0 0)" fill="#73cddc"/><rect width="2.297" height="3.39" x="10.261" y="-15.076" ry="1.143" transform="matrix(.44646 .8948 -.89204 .45195 0 0)" fill="#73cddc"/><circle cx="9.267" cy="5.13" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><circle cx="14.214" cy="5.116" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><ellipse cx="8.039" cy="5.051" rx=".792" ry=".901" fill="#030d18"/><path d="M11.792 9.556l.763.138a.403.689 0 0 1 .008.138.403.689 0 0 1-.402.69.403.689 0 0 1-.404-.69.403.689 0 0 1 .035-.276z" fill="#fff" stroke="#fff" stroke-width=".155"/><ellipse cx="8.51" cy="5.365" rx=".138" ry=".166" fill="#fff"/><ellipse cx="12.945" cy="5.189" rx=".792" ry=".901" fill="#030d18"/><ellipse cx="13.414" cy="5.446" rx=".138" ry=".166" fill="#fff"/><ellipse cx="-12.982" cy="-3.409" rx=".708" ry="1.026" transform="rotate(-129.403)" fill="#f6d2a1" stroke-width=".4"/><path d="M11.772 9.553l-.757.135a.4.672 0 0 0-.008.135.4.672 0 0 0 .4.672.4.672 0 0 0 .4-.672.4.672 0 0 0-.035-.27z" fill="#fff" stroke="#fff" stroke-width=".153"/><ellipse cx="1.841" cy="-21.563" rx=".707" ry="1.026" transform="scale(1 -1) rotate(50.597)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="-17.281" cy="-21.784" rx=".864" ry="1.27" transform="matrix(.3054 -.95222 -.97065 -.2405 0 0)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="22.885" cy="2.587" rx=".864" ry="1.27" transform="matrix(.22652 .974 .95652 -.29167 0 0)" fill="#f6d2a1" stroke-width=".4"/><path d="M10.708 8.392a.594.594 0 0 0-.594.597v.115c0 .331.264.598.594.598h.386a.973.772 0 0 1 .697-.235.973.772 0 0 1 .698.235h.334c.33 0 .594-.267.594-.598V8.99a.595.595 0 0 0-.594-.597h-2.115z" fill="#f6d2a1" stroke="#657075" stroke-width=".1"/><ellipse cx="11.734" cy="8.203" rx="1.208" ry=".68" fill="#030d18" stroke="#fff" stroke-width=".162"/></symbol><symbol viewBox="0 0 24 24" id="gradle" xmlns="http://www.w3.org/2000/svg"><path d="M21.718 5.503c-.731-1.315-2.04-1.708-2.963-1.727-1.133-.023-2.065.605-1.888 1.017.037.088.25.55.38.741.19.275.527.064.646 0 .353-.187.73-.248 1.16-.198.409.048.954.3 1.319 1.001.859 1.652-1.794 5.05-5.114 2.697-3.32-2.353-6.548-1.574-8.01-1.1-1.462.475-2.135.952-1.556 2.055.785 1.498.524 1.038 1.285 2.28 1.21 1.97 3.856-.908 3.856-.908-1.972 2.906-3.662 2.204-4.31 1.188a15.864 15.864 0 0 1-1.038-1.97c-4.993 1.76-3.642 9.534-3.642 9.534h2.48c.632-2.862 2.892-2.757 3.28 0h1.892c1.673-5.59 5.914 0 5.914 0h2.466c-.69-3.812 1.388-5.01 2.697-7.246 1.31-2.235 2.551-4.969 1.146-7.364zm-6.362 7.362c-1.304-.426-.837-1.723-.837-1.723s1.139.368 2.68.87c-.09.403-.856 1.175-1.843.853z" fill="#0097a7" stroke-width=".47"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 300 300" id="graphcool" xmlns="http://www.w3.org/2000/svg"><path d="M246.886 107.727c-12.237-6.892-27.616 2.1-30.081 3.646l-52.834 29.965c-7.8-6.196-18.914-5.933-26.412.625-7.499 6.558-9.24 17.537-4.14 26.094 5.102 8.556 15.588 12.246 24.923 8.768 9.335-3.478 14.852-13.129 13.111-22.937l52.688-29.9.321-.196c3.464-2.188 11.5-5.462 15.256-3.34 2.706 1.524 4.252 6.629 4.376 14.148h-.066v66.092a17.313 17.313 0 0 1-8.635 14.95l-75.739 43.755a17.312 17.312 0 0 1-17.261 0l-75.74-43.756a17.312 17.312 0 0 1-8.634-14.95V113.22c.01-6.165 3.3-11.86 8.634-14.95l68.549-39.562c6.522 7.482 17.451 9.25 26 4.206s12.283-15.468 8.886-24.794c-3.397-9.327-12.962-14.904-22.751-13.27-9.79 1.636-17.022 10.02-17.204 19.944L59.397 85.632a31.932 31.932 0 0 0-15.978 27.588v87.454a31.933 31.933 0 0 0 15.927 27.602l75.74 43.755a31.934 31.934 0 0 0 31.846 0l75.74-43.755a31.933 31.933 0 0 0 15.927-27.58V137.12h.05c.373-14.913-3.616-24.794-11.762-29.389z" fill="#27ae60" stroke="#27ae60" stroke-width="7.883622079999999"/></symbol><symbol viewBox="0 0 400 400" id="graphql" xmlns="http://www.w3.org/2000/svg"><path d="M67.008 293.022l-13.143-7.588L200.282 31.839l13.143 7.588z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M50.855 265.174H343.69v15.177H50.855z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M203.122 358.269L56.649 273.7l7.589-13.143 146.472 84.568zm127.24-220.407L183.889 53.293l7.589-13.143 146.472 84.568z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M64.278 137.803l-7.588-13.142 146.472-84.568 7.588 13.143z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M327.661 293.025L181.244 39.43l13.143-7.589 146.417 253.596zM62.466 114.597h15.176v169.136H62.466zm254.528 0h15.176v169.136h-15.176z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M200.538 351.845l-6.628-11.481L321.3 266.812l6.629 11.48z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M352.284 288.67c-8.777 15.268-28.342 20.48-43.61 11.703-15.268-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.36 8.869 20.57 28.342 11.703 43.61M97.574 141.567c-8.778 15.268-28.343 20.48-43.61 11.703-15.269-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.268 8.869 20.479 28.342 11.702 43.61M42.353 288.67c-8.777-15.268-3.566-34.741 11.702-43.61 15.268-8.776 34.741-3.565 43.61 11.703 8.776 15.268 3.565 34.741-11.703 43.61-15.36 8.776-34.833 3.565-43.61-11.703m254.71-147.103c-8.776-15.268-3.565-34.741 11.703-43.61 15.268-8.776 34.742-3.565 43.61 11.703 8.777 15.268 3.566 34.741-11.702 43.61-15.268 8.776-34.833 3.565-43.61-11.703m-99.745 236.608c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907c0 17.554-14.262 31.907-31.907 31.907m0-294.206c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907-14.262 31.907-31.907 31.907" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/></symbol><symbol viewBox="0 0 24 24" id="groovy" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.982a10.119 10.119 0 0 0-10.12 10.12A10.119 10.119 0 0 0 12 22.22 10.119 10.119 0 0 0 22.12 12.1 10.119 10.119 0 0 0 12 1.983zm1.254 2.422c.91 0 1.647.261 2.213.78.571.518.857 1.188.857 2.013 0 .889-.319 1.673-.959 2.35-.64.677-1.376 1.015-2.207 1.015-.486 0-.89-.119-1.213-.357-.317-.238-.476-.532-.476-.88 0-.212.06-.4.181-.563.127-.164.274-.246.438-.246.159 0 .238.092.238.277 0 .164.06.29.182.38.121.09.261.136.42.136.423 0 .828-.29 1.215-.866.391-.582.587-1.202.587-1.863 0-.465-.151-.844-.453-1.135-.301-.296-.69-.445-1.166-.445-.714 0-1.406.318-2.078.953-.666.635-1.211 1.47-1.635 2.506-.417 1.031-.627 2.014-.627 2.945 0 .857.185 1.54.555 2.047.37.503.863.754 1.477.754 1.037 0 2.027-.734 2.974-2.2l1.493-.212c.185-.026.277.018.277.135 0 .053-.072.28-.215.681-.143.402-.337 1.074-.586 2.016.82-.476 1.455-1.003 1.904-1.58v.914c-.36.418-1.046.888-2.062 1.412-.212 1.407-.682 2.493-1.406 3.26-.725.772-1.54 1.16-2.444 1.16-.433 0-.775-.102-1.023-.303-.243-.2-.365-.477-.365-.832 0-.984.955-1.94 2.865-2.865.2-.714.395-1.356.586-1.928-.333.482-.817.907-1.451 1.278-.635.37-1.225.554-1.77.554-.889 0-1.628-.383-2.22-1.15-.588-.772-.881-1.748-.881-2.928 0-1.243.333-2.42 1-3.531a7.747 7.747 0 0 1 2.625-2.674c1.084-.672 2.134-1.008 3.15-1.008zM12.03 16.592c-1.375.687-2.062 1.365-2.062 2.031 0 .354.169.533.508.533.666 0 1.184-.856 1.554-2.564z" fill="#26c6da"/></symbol><symbol viewBox="0 0 24 24" id="gulp" xmlns="http://www.w3.org/2000/svg"><path d="M8.37 15.94a596.238 596.238 0 0 1-.482-4.982c.002-.042-.225-.077-.505-.077h-.508V8.95h3.966V5.198l1.871-1.124c1.14-.685 1.978-1.125 2.144-1.125.4 0 .866.506.866.939 0 .19-.057.422-.127.517-.07.095-.722.53-1.45.966l-1.321.792-.029 1.393-.028 1.393h3.972v1.932h-.98l-.495 4.983-.495 4.983H8.854l-.485-4.906z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="h" xmlns="http://www.w3.org/2000/svg"><path d="M16.745 19.818h-3.007v-5.882q0-2.381-1.736-2.381-.869 0-1.438.663-.56.662-.56 1.718v5.882H6.988V4.533h3.016v6.508h.037q1.186-1.802 3.193-1.802 3.511 0 3.511 4.239z" stroke-width=".478" fill="#0277bd"/></symbol><symbol viewBox="0 0 253.6 253.6" id="hack" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-29.243 -29.515) scale(1.2301)"><path fill="#607d8b" d="M69.496 159.551v52.576l51.77-52.576zM123.507 41.523l-54.01 52.755v55.084l54.01-54.009z"/><path fill="#eceff1" d="M130.023 95.663v51.501l52.128-51.5z"/><path fill="#607d8b" d="M185.465 101.867l-55.442 55.174v55.083l55.442-55.262z"/><path fill="#ffa000" d="M73.068 154.283l50.427.09v-50.248z"/></g></symbol><symbol viewBox="0 0 300 300.00001" id="haml" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 165.6)"><path d="M78.42-132.307c-12.047-.302-26.924 5.998-26.924 5.998l49.195 99.791L74.605 85.005c23.81 20.134 50.07 10.504 50.07 10.504L136.76 9.212c1.526 1.446 3.146 2.77 4.777 3.995 5.244 3.714 10.925 6.553 16.606 8.738 5.68 2.185 11.583 3.933 17.482 5.244 3.933.874 7.645 1.53 11.578 1.967-1.748 3.933-2.84 8.083-2.621 12.672 0 .437.22.873.656 1.092h.217c4.152 2.185 8.521 3.934 13.328 5.027 4.589.874 9.615 1.312 14.422.656 5.026-.655 10.051-2.623 13.984-5.9 3.933-3.278 6.774-7.648 8.522-12.237l.219-.218v-.217l.656-5.899v-.22c2.185-1.311 4.37-2.621 6.555-4.37 2.622-2.184 5.025-4.589 6.773-7.648 1.748-3.059 2.84-6.774 2.621-10.488-.218-3.496-1.53-6.99-3.06-10.049-1.53-3.059-3.495-5.901-5.68-8.523-4.37-5.026-9.614-9.176-15.295-12.454-5.462-3.496-11.581-6.338-17.7-8.304l-2.404-.656-1.962-.655c-1.311-.437-2.406-1.092-3.498-1.53-2.185-1.31-3.717-2.622-4.809-4.37-2.185-3.278-2.403-8.301-1.31-13.545.218-1.311.656-2.623 1.093-3.934a96.064 96.064 0 0 0 1.31-4.152c.314-1.412.51-2.829.598-4.402l29.203-25.553c-2.275-8.404-27.488-17.158-27.488-17.158l-74.931 63.726-43.243-81.584c-1.553-.35-3.218-.527-4.94-.57zm107.682 73.14c-.449 2.336-.647 4.795-.647 7.258.219 3.715 1.311 7.87 3.715 11.366 2.403 3.496 5.68 6.117 8.957 7.646a29.663 29.663 0 0 0 5.027 1.967l2.623.654 2.184.438c5.68 1.53 11.142 3.714 16.168 6.554 5.025 2.84 9.833 6.337 13.766 10.27s6.992 8.959 7.43 13.984c.218 3.496-.22 6.118-1.313 8.303-1.093 2.404-2.84 4.588-4.807 6.555-.874.874-1.966 1.747-2.84 2.402a27.11 27.11 0 0 0-.654-5.898c-.219-1.093-.438-1.966-.875-3.059-.437-.874-.872-1.966-1.965-2.621-.218 0-.44-.001-.44.217-1.31 3.277-3.494 6.12-5.898 8.086-2.403 1.966-5.462 2.84-8.521 3.058-3.06.219-6.338-.436-9.616-1.31-3.277-.874-6.552-1.968-9.83-3.06l-.439-.22c-.656-.218-1.526.002-1.963.44-1.748 2.185-3.06 4.149-4.59 6.334a58.435 58.435 0 0 0-2.84 5.027c-3.933-1.53-7.649-2.841-11.582-4.37-5.462-2.186-10.925-4.37-15.95-6.991-5.245-2.404-10.268-5.246-14.638-8.524-3.15-2.363-6.062-4.845-8.185-7.681l2.404-17.172z" fill="#f4511e" stroke-width="0" stroke-linejoin="round"/></g></symbol><symbol viewBox="0 0 24 24" id="handlebars" xmlns="http://www.w3.org/2000/svg"><path d="M8.55 10.32c-2.753 0-4.202 3.48-5.793 3.48-.98 0-1.126-.677-1.126-.915 0-.332.236-.706.564-.706.59 0 .414.77.414.77s.798-.555.272-1.298c-.42-.595-1.31-.623-1.92-.17-.617.458-1.057 1.146-.853 2.287.1.551.468 1.35 1.233 1.805.764.455 1.925.566 2.335.566 2.194 0 4.342-1.633 6.639-2.322a5.513 5.513 0 0 1 1.497-.222 6.19 6.19 0 0 1 1.92.226c2.296.689 4.444 2.323 6.638 2.323.41 0 1.57-.11 2.335-.566.765-.455 1.132-1.256 1.231-1.807.204-1.14-.235-1.829-.853-2.287-.61-.453-1.497-.423-1.918.172-.526.743.27 1.297.27 1.297s-.176-.77.414-.77c.329 0 .565.373.565.705 0 .238-.147.914-1.126.914-1.592 0-3.04-3.478-5.794-3.478-2.565 0-3.076 1.177-3.462 1.718-.004.005-.005.011-.008.016-.005-.006-.007-.013-.012-.02-.386-.54-.896-1.717-3.461-1.717z" fill="#ff7043" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 300.00001 300" id="haskell" xmlns="http://www.w3.org/2000/svg"><g stroke-width="2.422"><path d="M23.928 240.5l59.94-89.852-59.94-89.855h44.955l59.94 89.855-59.94 89.852z" fill="#ef5350"/><path d="M83.869 240.5l59.94-89.852-59.94-89.855h44.955l119.88 179.71h-44.95l-37.46-56.156-37.468 56.156z" fill="#ffa726"/><path d="M228.72 188.08l-19.98-29.953h69.93v29.956h-49.95zm-29.97-44.924l-19.98-29.953h99.901v29.953z" fill="#ffee58"/></g></symbol><symbol viewBox="0 0 210 210" id="haxe" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -87)"><path fill="#f68712" stroke-width=".221" d="M42.78 191.545l63.431-63.43 63.431 63.43-63.431 63.431z"/><path d="M42.8 191.592L31.193 148.28 19.59 104.97 62.9 116.575l43.311 11.605-31.706 31.706z" fill="#fab20b" stroke-width=".266"/><path d="M105.956 128.111l-43.19-11.544-43.177-11.597 22.927.185 23.228.294 20.264 11.36z" fill="#fbc707" stroke-width=".265"/><path d="M19.59 104.97l11.596 43.176 11.545 43.19-11.303-19.948-11.36-20.263-.294-23.228z" fill="#fff200" stroke-width=".265"/><path d="M106.23 128.133l43.312-11.605 43.311-11.605-11.605 43.31-11.605 43.312-31.706-31.706z" fill="#f47216" stroke-width=".266"/><path d="M169.711 191.289l11.545-43.19 11.597-43.176-.185 22.927-.294 23.228-11.36 20.263z" fill="#f1471d" stroke-width=".265"/><path d="M192.853 104.923l-43.176 11.597-43.19 11.544 19.947-11.303 20.264-11.36 23.228-.293z" fill="#fbc707" stroke-width=".265"/><path d="M169.643 191.545l11.605 43.31 11.605 43.312-43.311-11.605-43.311-11.606 31.706-31.705z" fill="#f25c19" stroke-width=".266"/><path d="M106.487 255.025l43.19 11.544 43.176 11.598-22.927-.185-23.228-.294-20.264-11.36z" fill="#f68712" stroke-width=".265"/><path d="M192.853 278.167l-11.597-43.176-11.545-43.19 11.303 19.947 11.36 20.264.294 23.228z" fill="#f1471d" stroke-width=".265"/><path d="M106.211 254.976l-43.31 11.605-43.312 11.605 11.605-43.31L42.8 191.563l31.706 31.706z" fill="#f89c0e" stroke-width=".266"/><path d="M42.731 191.82l-11.545 43.19-11.597 43.176.185-22.927.294-23.228 11.36-20.263z" fill="#fff200" stroke-width=".265"/><path d="M19.59 278.186l43.175-11.597 43.19-11.544-19.947 11.303-20.264 11.36-23.228.293z" fill="#f25c19" stroke-width=".265"/></g></symbol><symbol viewBox="0 0 144 152" id="heroku" xmlns="http://www.w3.org/2000/svg"><path d="M118.68 13.279H26.865c-6.337 0-11.476 5.139-11.476 11.476V129.32c0 6.338 5.139 11.477 11.476 11.477h91.813c6.338 0 11.477-5.14 11.477-11.477V24.755c0-6.337-5.139-11.476-11.477-11.476zM44.08 121.669V96.165l14.346 12.752zm44.632 0v-38.08c-.063-2.976-1.496-6.551-7.97-6.551-12.966 0-27.51 6.52-27.654 6.586l-9.008 4.08V32.407h12.752v36.201c6.366-2.072 15.266-4.321 23.91-4.321 7.882 0 12.6 3.099 15.17 5.698 5.484 5.547 5.56 12.613 5.551 13.43v38.255zm3.188-68.54H79.149c5.011-6.576 8.158-13.496 9.564-20.723h12.751c-.86 7.243-3.796 14.187-9.563 20.722z" fill="#6963b9"/></symbol><symbol viewBox="0 0 24 24" id="hpp" xmlns="http://www.w3.org/2000/svg"><path d="M9.757 19.818H6.751v-5.882q0-2.381-1.737-2.381-.868 0-1.438.663-.56.662-.56 1.718v5.882H0V4.533h3.016v6.508h.037Q4.24 9.239 6.247 9.239q3.51 0 3.51 4.239z" stroke-width=".478" fill="#0277bd"/><path d="M13.073 11.448v2h-2v2h2v2h2v-2h2v-2h-2v-2zm7 0v2h-2v2h2v2h2v-2h2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="html" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.56l4.07-1.13.55-6.1H9.38L9.2 8.3h7.6l.2-1.99H7l.56 6.01h6.89l-.23 2.58-2.22.6-2.22-.6-.14-1.66h-2l.29 3.19L12 17.56M4.07 3h15.86L18.5 19.2 12 21l-6.5-1.8L4.07 3z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" id="http" xmlns="http://www.w3.org/2000/svg"><path d="M16.046 13.784c.074-.613.13-1.225.13-1.856s-.056-1.244-.13-1.856h3.137c.148.594.241 1.215.241 1.856a7.65 7.65 0 0 1-.241 1.856m-4.78 5.16c.557-1.03.984-2.144 1.281-3.304h2.738a7.452 7.452 0 0 1-4.019 3.304m-.232-5.16H9.828a12.314 12.314 0 0 1-.149-1.856c0-.631.056-1.253.149-1.856h4.343c.084.603.149 1.225.149 1.856 0 .63-.065 1.243-.149 1.856M12 19.315c-.77-1.113-1.393-2.348-1.773-3.675h3.545c-.38 1.327-1.002 2.562-1.773 3.675m-3.712-11.1h-2.71a7.353 7.353 0 0 1 4.01-3.304c-.557 1.03-.975 2.144-1.3 3.304m-2.71 7.425h2.71c.325 1.16.743 2.274 1.3 3.304a7.433 7.433 0 0 1-4.01-3.304m-.761-1.856a7.65 7.65 0 0 1-.241-1.856c0-.64.093-1.262.241-1.856h3.137c-.074.612-.13 1.225-.13 1.856 0 .63.056 1.243.13 1.856m4.046-9.253c.77 1.114 1.393 2.357 1.773 3.684h-3.545c.38-1.327 1.002-2.57 1.773-3.684m6.422 3.684h-2.738a14.523 14.523 0 0 0-1.28-3.304 7.412 7.412 0 0 1 4.018 3.304m-6.423-5.568c-5.132 0-9.28 4.176-9.28 9.28a9.28 9.28 0 0 0 9.28 9.282 9.28 9.28 0 0 0 9.281-9.281A9.28 9.28 0 0 0 12 2.647z" fill="#e53935" stroke-width=".928"/></symbol><symbol viewBox="0 0 24 24" id="image" xmlns="http://www.w3.org/2000/svg"><path d="M13.009 9.202h5.368l-5.368-5.368v5.368M6.177 2.37h7.808l5.856 5.856v11.711a1.952 1.952 0 0 1-1.952 1.952H6.178a1.951 1.951 0 0 1-1.952-1.952V4.322c0-1.083.868-1.952 1.952-1.952m0 17.567h11.71V12.13l-3.903 3.903-1.952-1.951-5.856 5.855M8.13 9.202a1.952 1.952 0 0 0-1.952 1.952 1.952 1.952 0 0 0 1.952 1.952 1.952 1.952 0 0 0 1.952-1.952A1.952 1.952 0 0 0 8.13 9.202z" fill="#26a69a" stroke-width=".976"/></symbol><symbol viewBox="0 0 512 512" id="ionic" xmlns="http://www.w3.org/2000/svg"><g fill="#4f8ff7"><path d="M423.592 132.804A31.855 31.855 0 0 0 429 115c0-17.675-14.33-32-32-32a31.853 31.853 0 0 0-17.805 5.409C344.709 63.015 302.11 48 256 48 141.125 48 48 141.125 48 256c0 114.877 93.125 208 208 208 114.873 0 208-93.123 208-208 0-46.111-15.016-88.71-40.408-123.196zM391.83 391.832c-17.646 17.646-38.191 31.499-61.064 41.174-23.672 10.012-48.826 15.089-74.766 15.089-25.94 0-51.095-5.077-74.767-15.089-22.873-9.675-43.417-23.527-61.064-41.174s-31.5-38.191-41.174-61.064C68.982 307.096 63.905 281.94 63.905 256c0-25.94 5.077-51.095 15.089-74.767 9.674-22.873 23.527-43.417 41.174-61.064s38.191-31.5 61.064-41.174c23.673-10.013 48.828-15.09 74.768-15.09 25.939 0 51.094 5.077 74.766 15.089a191.221 191.221 0 0 1 37.802 21.327A31.853 31.853 0 0 0 365 115c0 17.675 14.327 32 32 32 5.293 0 10.28-1.293 14.678-3.568a191.085 191.085 0 0 1 21.327 37.801c10.013 23.672 15.09 48.827 15.09 74.767 0 25.939-5.077 51.096-15.09 74.768-9.675 22.873-23.527 43.418-41.175 61.064z"/><circle cx="256.003" cy="256" r="96"/></g></symbol><symbol viewBox="0 0 24 24" id="java" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="javascript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v18H3V3m4.73 15.04c.4.85 1.19 1.55 2.54 1.55 1.5 0 2.53-.8 2.53-2.55v-5.78h-1.7V17c0 .86-.35 1.08-.9 1.08-.58 0-.82-.4-1.09-.87l-1.38.83m5.98-.18c.5.98 1.51 1.73 3.09 1.73 1.6 0 2.8-.83 2.8-2.36 0-1.41-.81-2.04-2.25-2.66l-.42-.18c-.73-.31-1.04-.52-1.04-1.02 0-.41.31-.73.81-.73.48 0 .8.21 1.09.73l1.31-.87c-.55-.96-1.33-1.33-2.4-1.33-1.51 0-2.48.96-2.48 2.23 0 1.38.81 2.03 2.03 2.55l.42.18c.78.34 1.24.55 1.24 1.13 0 .48-.45.83-1.15.83-.83 0-1.31-.43-1.67-1.03l-1.38.8z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="javascript-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#ffca28"/><path d="M2.444 2.506h14.135v14.136H2.444V2.506m3.714 11.811c.315.668.935 1.218 1.995 1.218 1.178 0 1.987-.629 1.987-2.003V8.993H8.805v4.508c0 .675-.275.848-.707.848-.455 0-.644-.314-.856-.683l-1.084.651m4.697-.14c.392.769 1.185 1.358 2.426 1.358 1.257 0 2.199-.652 2.199-1.854 0-1.107-.636-1.602-1.767-2.089l-.33-.141c-.573-.243-.816-.408-.816-.801 0-.322.243-.573.636-.573.377 0 .628.165.856.573l1.028-.683c-.432-.754-1.044-1.045-1.884-1.045-1.186 0-1.948.754-1.948 1.752 0 1.083.636 1.594 1.594 2.002l.33.141c.613.267.974.432.974.888 0 .377-.354.652-.903.652-.652 0-1.029-.338-1.312-.81l-1.083.63z" fill="#ffca28"/></symbol><symbol viewBox="0 0 180 180" id="jenkins" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="gia"><path transform="scale(1 -1)" fill="#37474f" d="M.899-144.42h144.42V0H.899z"/></clipPath></defs><g transform="matrix(1.0691 0 0 -1.0691 9.4 166.143)" clip-path="url(#gia)"><g fill-rule="evenodd"><path d="M107.96 30.661l-12.506-1.876-16.883-1.876-10.943-.312-10.629.312-8.13 2.502-7.19 7.815-5.628 15.945-1.25 3.44-7.504 2.5-4.377 7.191-3.126 10.317 3.44 9.067 8.128 2.814 6.565-3.127 3.127-6.878 3.752.626 1.25 1.563-1.25 7.19-.313 9.068 1.876 12.505-.074 7.143 5.701 9.114 10.005 7.19 17.508 7.504 19.383-2.814 16.883-12.193 7.817-12.505 5.002-9.067 1.25-22.51-3.752-19.384-6.877-17.195-6.566-9.066" fill="#f0d6b7"/><path d="M97.334-23.425l-44.709-1.876v-7.503l3.752-26.262-1.876-2.19-31.264 10.63-2.19 3.752-3.126 35.328-7.19 21.26-1.563 5.002 25.01 17.195 7.818 3.127 6.877-8.441 5.94-5.315 6.88-2.188 3.125-.938L68.57 1.899l2.814-3.44 7.19 2.502-5.002-9.693 27.2-12.818-3.439-1.876" fill="#335061"/><path d="M23.238 85.687l8.128 2.814 6.566-3.127 3.127-6.878 3.751.626.938 3.751-1.876 7.19 1.876 17.197-1.563 9.379 5.627 6.565 12.193 9.692-3.44 4.69-17.194-8.442-7.191-5.627-4.064-8.754-6.253-8.442-1.876-10.005 1.251-10.63" fill="#6d6b6d"/><path d="M36.055 115.07s4.69 11.567 23.448 17.195c18.759 5.628.938 4.065.938 4.065l-20.321-7.817-7.817-7.816-3.438-6.253 7.19.626M26.676 87.875s-6.566 21.886 18.446 25.012l-.938 3.752-17.195-4.065-5.003-16.257 1.251-10.63 3.439 2.188" fill="#dcd9d8"/></g><g fill="#f7e4cd"><path d="M36.681 58.799l4.094 3.966s1.847-.214 2.16-2.402c.312-2.19 1.25-21.886 14.693-32.516 1.227-.97-10.004 1.564-10.004 1.564L37.62 45.042M94.209 64.739s.729 9.477 3.28 8.748c2.553-.729 2.553-3.28 2.553-3.28s-6.198-4.01-5.833-5.468" fill-rule="evenodd"/><path d="M120.16 99.442s-5.153-1.088-5.628-5.628c-.474-4.54 5.628-.938 6.566-.625M82.327 99.129s-6.879-.938-6.879-5.314c0-4.378 7.817-4.065 10.005-2.19"/><g fill-rule="evenodd"><path d="M39.807 78.808s-11.881 7.191-13.131.312c-1.25-6.877-4.065-11.88 1.876-19.07l-4.064 1.25-3.752 9.691-1.25 9.38 7.19 7.504 8.129-.626 4.69-3.751.312-4.69M45.435 98.504s5.315 27.512 32.203 32.827c22.136 4.375 33.765-.938 38.142-5.94 0 0-19.696 23.447-38.455 16.257-18.759-7.191-32.514-20.322-32.202-28.762.532-14.377.313-14.382.313-14.382M117.97 122.27s-9.066.312-9.38-7.817c0 0 0-1.25.625-2.5 0 0 7.192 8.129 11.568 3.751"/><path d="M78.268 111.1s-1.56 12.477-12.199 5.223c-6.878-4.69-6.252-11.255-5.002-12.505s.91-3.77 1.862-2.04c.952 1.728.638 7.356 4.078 8.918 3.439 1.564 9.077 3.31 11.26.404"/></g></g><g fill="#49728b" fill-rule="evenodd"><path d="M48.874 26.597L19.486 13.466s12.193-48.46 5.94-63.467l-4.377 1.563-.313 18.446-8.128 35.015-3.44 9.692 30.639 20.633 9.067-8.753M51.896-.206l4.17-5.087v-18.76h-5.003s-.625 13.132-.625 14.696c0 1.563.624 7.19.624 7.19M52-26.866l-14.069-.625 4.065-2.813L52-31.868"/></g><g fill-rule="evenodd"><path d="M100.15-23.739l11.567.313 2.814-28.764-11.881-1.563-2.5 30.014" fill="#335061"/><path d="M103.27-23.739l17.508.938s7.19 18.133 7.19 19.07c0 .939 6.253 26.263 6.253 26.263l-14.069 14.694-2.813 2.501-7.504-7.503V3.148l-6.565-26.887" fill="#335061"/><path d="M111.09-21.55l-10.942-2.188 1.563-8.755c4.064-1.876 10.943 3.127 10.943 3.127M111.4 33.162l21.885-16.257.626 7.503-16.57 15.32-5.94-6.566" fill="#49728b"/><path d="M62.85-85.332l-6.473 26.266-3.22 19.38-.531 14.385 29.296 1.56 18.226.003-1.658-32.83 2.814-25.324-.312-4.69-23.76-1.876-14.382 3.126" fill="#fff"/><path d="M96.083-23.426s-1.563-32.515 3.127-55.65c0 0-9.38-5.94-23.136-7.503l26.262.938 3.126 1.875-3.752 51.273-.938 10.944" fill="#dcd9d8"/><path d="M115.06-49.691l12.193 3.44 23.135 1.25 3.44 10.629-6.254 18.446-7.19.938-10.005-3.127-9.599-4.686-5.095.935-3.972-1.56" fill="#fff"/><path d="M114.84-43.435s8.128 3.751 9.38 3.438L120.78-22.8l4.065 1.563s2.814-16.257 2.814-18.133c0 0 17.507-.938 19.07-.938 0 0 3.752 7.191 2.814 14.694l3.44-10.005.312-5.628-5.002-7.503-5.627-1.25-9.38.312-3.126 4.064-10.943-1.563-3.44-1.25" fill="#dcd9d8"/></g><path d="M102.56-21.241L95.682-3.733l-7.19 10.317s1.562 4.377 3.75 4.377h7.192l6.878-2.501-.625-11.568-3.127-18.134" fill="#fff"/><path d="M103.9-15.297S95.145 1.585 95.145 4.086c0 0 1.563 3.752 3.752 2.814 2.19-.938 6.879-3.439 6.879-3.439v5.94l-10.63 2.19-7.19-.939 12.193-28.763 2.5-.313" fill="#dcd9d8" fill-rule="evenodd"/><path d="M65.664 25.968l-8.661.942-8.13 2.501v-2.814l3.972-4.38 12.506-5.627" fill="#fff"/><path d="M51.689 25.031s9.693-4.065 12.819-3.127l.311-3.748-8.752 1.872-5.316 3.752.938 1.251" fill="#dcd9d8" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43" fill="#d33833" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669" fill="#d33833" fill-rule="evenodd"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695" fill="#d33833" fill-rule="evenodd"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M102.87 10.649s-2.19 3.127-.626 4.065c1.564.938 3.127 0 4.065 1.563s0 2.501.313 4.377 1.877 2.189 3.44 2.501c1.562.313 5.94.938 6.565-.625l-1.876 5.627-3.752 1.25-11.88-6.877-.626-3.44v-6.877M70.041.331c-.376 4.88-.773 9.752-1.215 14.626-.662 7.279 1.748 6.009 8.057 6.009.964 0 5.933-1.15 6.289-1.876 1.705-3.483-2.851-2.709 1.964-5.335 4.065-2.216 11.246 1.346 9.603 6.273-.919 1.095-4.789.341-6.176 1.06l-7.327 3.8c-3.108 1.612-10.29 3.962-13.603 1.709-8.395-5.71.53-19.974 3.524-25.93" fill="#ef3d3a" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M78.268 111.1c-8.521 1.985-12.755-3.566-15.338-9.323-2.306.559-1.389 3.695-.806 5.294 1.525 4.194 7.672 9.778 12.694 9.02 2.161-.325 5.086-2.301 3.45-4.99M119.79 101.4l.404-.016c1.926-4 3.593-8.238 6.022-11.769-1.628-3.79-12.322-7.144-12.157-.338 2.313 1.01 6.305.206 8.356 1.497-1.186 3.254-2.897 6.024-2.625 10.626M82.63 101.29c1.827-3.35 2.422-6.868 5.019-9.4 1.17-1.14 3.444-2.529 2.316-5.698-.263-.747-2.189-2.414-3.3-2.741-4.06-1.2-13.521-.248-10.317 4.814 3.358-.157 7.871-2.18 10.38.257-1.927 3.081-5.363 9.177-4.098 12.768M118.26 67.253c-6.113-3.927-12.93-8.197-22.947-7.207-2.14 1.86-2.956 6.002-.877 8.737 1.082-1.861.402-5.284 3.419-5.799 5.684-.972 12.299 3.477 16.387 5.032 2.535 4.275-.219 5.847-2.503 8.597-4.675 5.636-10.947 12.622-10.72 21.06 1.89 1.37 2.053-2.092 2.325-2.722 2.44-5.714 8.585-13.021 13.07-17.912 1.1-1.205 2.914-2.36 3.115-3.157.582-2.315-1.513-5.09-1.27-6.63M37.668 71.387c-1.916 1.094-2.372 5.91-4.622 6.048-3.215.195-2.629-6.25-2.616-10.018-2.213 2.009-2.602 8.194-.976 11.37-1.853.91-2.68-1.003-3.708-1.677 1.32 9.595 14.036 4.45 11.922-5.723M122.15 63.257c-2.846-5.417-6.871-11.382-15.222-11.555-.17 1.75-.3 4.411.009 5.464 6.384.614 10.325 3.863 15.212 6.091M82.149 59.745c5.326-2.8 15.114-3.102 22.353-2.89.388-1.586.379-3.545.394-5.48-9.305-.463-20.307 1.84-22.747 8.37M81.136 54.523c3.683-9.247 16.341-8.182 27.016-7.927-.47-1.2-1.489-2.62-2.755-3.132-3.42-1.392-12.855-2.448-17.604.074-3.011 1.601-4.946 5.219-6.596 7.34-.797 1.024-4.765 3.64-.06 3.645"/></g><path d="M117.82 3.516c-4.322-7.402-8.457-15.005-13.585-21.534 2.15 6.32 3.07 16.9 3.394 24.965 4.498 2.105 8.349-.474 10.191-3.43" fill="#81b0c4" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M141.07-23.089c-4.839-.969-8.239-5.671-12.959-5.37 2.594 3.658 7.14 5.2 12.959 5.37M143.21-30.661c-3.944-.417-8.576-1.055-12.577-.726 1.894 2.892 9.19 1.894 12.577.726M144.58-37.19c-4.433-.096-9.942-.008-14.155.346 2.492 2.677 11.28.993 14.155-.346"/></g><g fill-rule="evenodd"><path d="M109.48-55.057c.636-5.567 2.843-11.207 2.566-17.304-2.45-.827-3.858-1.55-7.142-1.545-.232 5.181-.925 13.102-.718 18.041 1.615-.107 3.997 1.154 5.294.808" fill="#dcd9d8"/><path d="M102.33 26.985c-2.226-1.453-4.121-3.267-6.259-4.818-4.74-.235-7.327.328-10.81 3.05.057.219.407.121.42.39 5.075-2.262 11.524.92 16.648 1.378" fill="#f0d6b7"/><path d="M75.694-7.603c1.394 6.04 6.857 9.17 11.817 12.497 5.12-6.498 8.234-14.855 11.663-22.92-8.102 2.443-16.38 6.406-23.481 10.423" fill="#81b0c4"/><path d="M104.18-55.865c-.207-4.94.486-12.86.718-18.041 3.283-.004 4.691.718 7.142 1.545.276 6.096-1.93 11.737-2.566 17.304-1.298.346-3.679-.914-5.294-.808zm-51.13 28.09c2.165-19.906 5.301-36.639 11.054-54.266 12.766-3.876 28.157-4.214 39.441-.716-2.072 9.948-1.167 22.06-2.378 32.677-.912 7.98-.447 16.009-1.698 24.15-13.673 2.844-33 .665-46.418-1.845zm49.651 1.72c-.115-8.549.383-16.982 1.036-25.542 3.282.493 5.51.822 8.56 1.49-.99 8.241-.869 17.514-2.886 24.804-2.332-.023-4.385.027-6.71-.752zm16.653 1.378c-1.558.357-3.372.014-4.86-.015.7-6.969 2.397-14.659 2.995-21.974 2.342-.073 3.593 1.032 5.52 1.403.102 6.421-.562 15.268-3.655 20.586zm25.215-23.038c4.882 1.186 7.952 7.165 6.586 13.305-.916 4.127-2.548 11.898-4.295 14.538-1.29 1.953-4.79 4.51-7.584 2.72-4.545-2.91-12.552-3.755-15.867-7.278 1.662-5.534 2.178-13.135 2.864-20.146 5.678-.354 12.665 1.562 17.387-.471-3.297-1.068-7.575-1.077-10.423-2.633 2.328-1.125 7.778-.897 11.332-.035zM99.17-18.025c-3.43 8.063-6.543 16.42-11.663 22.918-4.96-3.327-10.423-6.456-11.817-12.497 7.1-4.017 15.379-7.98 23.481-10.422zm8.453 24.971c-.325-8.065-1.245-18.644-3.395-24.965 5.128 6.53 9.263 14.132 13.585 21.534-1.842 2.957-5.693 5.536-10.19 3.431zm-9.582 3.405c-1.943.21-3.592-2.233-6.117-1.177-.58-.64-1.105-1.333-1.695-1.958 5.579-6.723 8.114-16.262 12.423-24.163 2.312 7.59 2.045 15.904 2.555 24.188-3.177-.201-4.94 2.873-7.166 3.11zm-6.161 8.132c-.208-2.303.328-3.056.791-5.695 7.57-2.367 6.248 10.388-.791 5.695zm-8.394 2.755c-3.261 1.782-8.161 3.723-12.374 4.527-5.222.999-4.732-7.123-4.51-11.968.173-3.836 2.168-7.893 3.035-10.441.406-1.19.498-2.453 1.515-2.69 1.798-.418 7.73 1.954 9.42 2.875 3.575 1.95 6.348 5.045 9.384 7.123.04 1.011.078 2.021.119 3.032-1.826.91-3.935 1.555-6.615 1.673 1.818.914 4.492.901 6.148 1.989.016.405.033.81.047 1.21-3.024.234-4.176 1.58-6.17 2.67zm-31.152 5.659c-2.707-2.748 7.592-6.494 10.871-6.696-.018 1.739.991 3.378.788 4.626-3.895.684-9.013.232-11.66 2.07zm33.345-1.29c-.013-.27-.363-.172-.42-.39 3.482-2.722 6.07-3.285 10.81-3.05 2.137 1.551 4.033 3.365 6.259 4.818-5.124-.458-11.574-3.64-16.648-1.379zm30.606-9.282c-.146 3.053-.948 9.332-2.835 10.431-3.961 2.312-11.002-4.668-13.984-5.732.324-.934.86-1.674.901-2.868 1.764.434 3.912.137 5.44-.615-1.767-.198-3.727-.185-4.897-1.027-.429-1.239.105-2.927-.18-4.647 4.196-1.184 8.989-1.814 14.294-1.97 1.032 1.341 1.383 3.896 1.261 6.429zM47.777 24.24c-.85.606-6.6 8.087-7.388 7.777-10.405-4.103-20.134-11.199-28.828-17.91 8.29-17.787 11.635-39.579 12.227-60.582 9.496-4.441 17.836-10.844 30.722-11.512-1.491 10.55-2.852 19.962-3.699 29.895-3.237 1.365-7.882-.062-10.913.423-.025 3.651 4.628 1.6 5.015 4.054.292 1.858-2.56 1.998-1.631 4.923 2.368-.861 3.612-2.763 6.138-3.477 2.309 5.05-.032 13.985.3 18.205.064.792.397 4.39 2.172 3.759 1.57-.559-.09-9.569.082-13.563.157-3.68-.444-7.242 1.046-9.552a355.817 355.817 0 0 0 38.576 3.16c-2.964 1.272-6.485 2.475-10.345 4.651-2.093 1.18-8.69 3.635-9.293 5.622-.964 3.167 2.528 4.855 3.125 7.57-6.285-3.428-7.511 3.286-8.998 8.042-1.347 4.308-2.114 7.526-2.445 10.01-5.414 2.581-11.203 5.195-15.863 8.505zm63.009 6.872c8.67 4.204 10.232-15.711 6.834-22.127.525-1.914 2.331-2.646 3.069-4.366-4.838-8.667-10.211-16.756-15.148-25.32 3.672 2.286 8.917.409 13.238 2.12 1.58.624 2.722 4.24 3.918 7.133 3.29 7.958 6.743 17.99 8.28 25.586.346 1.73 1.292 5.5 1.08 7.04-.378 2.758-4.12 4.803-6.022 6.508-3.506 3.15-5.714 5.921-9.371 8.866-1.483-2.189-4.666-3.66-5.878-5.44zM27.95 107.99c-4.13-4.545-3.266-13.062-2.766-19.121 7.467 4.697 17.377-.372 17.284-8.36 3.565.094 1.332 4.452.687 7.259-2.107 9.169 3.55 19.13.256 27.516-6.395-.485-11.649-3.097-15.46-7.294zm29.558 26.38c-9.352-2.65-21.337-9.446-25.18-17.847 2.976.432 5.041 1.933 7.977 2.119 1.11.072 2.563-.466 3.838-.148 2.54.63 4.685 6.327 6.602 8.447 1.868 2.07 4.114 2.954 5.651 4.841.988.477 2.448.444 2.504 1.927-.428.457-.879.806-1.392.66zm48.681-2.493c-9.707 5.477-26.136 9.596-36.462 4.449-8.331-4.155-19.593-11.027-23.433-19.737 3.587-8.405-1.062-16.106-1.36-24.64-.157-4.54 2.139-8.504 2.315-13.446-1.228-2.025-4.978-2.275-7.574-2.136-.873 4.372-2.403 9.287-6.906 9.78-6.371.697-11.03-4.576-11.319-10.085-.342-6.48 4.978-17.22 12.517-16.475 2.913.287 3.629 3.207 6.802 3.177 1.72-3.432-2.653-4.51-3.103-6.964-.117-.634.363-3.112.642-4.274 1.37-5.658 4.422-12.982 7.427-17.29 3.814-5.464 11.307-6.288 19.37-6.823 1.44 3.101 6.743 2.846 10.2 2.035-4.143 1.64-7.993 5.617-11.185 9.137-3.665 4.039-7.378 8.371-7.566 13.65 6.927-9.61 12.65-18.003 25.246-22.23 9.53-3.196 20.662 1.465 27.986 6.608 3.039 2.137 4.853 5.529 7.013 8.634 8.082 11.626 11.854 28.219 11.024 44.303-.342 6.633-.327 13.244-2.552 17.706-2.326 4.666-10.193 8.84-14.8 4.62-.853 4.537 3.83 7.344 9.331 5.71-3.922 5.063-8.039 11.145-13.614 14.29zm18.084-149.66c7.585 3.77 21.757 10.149 26.512-.014 1.755-3.746 3.814-10.079 4.723-13.946 1.284-5.456-1.392-16.923-7-18.754-4.953-1.617-10.733-1.518-16.7-.32-.702.585-1.484 1.603-2.03 2.665-4.261.165-8.25-.229-11.615-1.98.319-3.15-1.812-3.656-3.81-4.305-1.48-5.872 2.963-13.541 1.9-18.896-.76-3.815-5.453-4.405-8.902-5.118-.113-2.12.15-3.89.386-5.683-.789-2.907-4.327-4.561-7.679-4.967-11.029-1.326-27.775-1.922-38.384 1.893-2.96 7.261-5.292 16.093-7.758 24.384-10.346-1.105-18.715 4.464-26.603 8.113-2.731 1.266-6.51 1.964-7.53 4.138-.99 2.105-.584 6.14-.83 9.95-.625 9.733-1.16 19.12-3.73 29.086-1.154 4.472-3.165 8.418-4.568 12.727C9.358 5.184 7.092 10.12 6.5 14.1c-.877 5.903 4.681 6.232 8.235 8.79 5.494 3.954 9.806 6.142 15.756 9.711 1.762 1.057 7.077 3.733 7.681 4.966 1.202 2.443-2.062 5.888-2.935 7.803-1.38 3.03-2.1 5.602-2.298 8.59-4.992.789-8.775 3.76-11.06 7.109-3.781 5.543-6.403 15.798-3.132 23.599.257.614 1.536 1.822 1.725 2.765.372 1.858-.7 4.329-.768 6.305-.343 10.14 1.716 18.875 8.541 21.932 2.771 11.038 12.688 14.71 22.032 20.195 3.493 2.05 7.343 3.36 11.32 4.824 14.263 5.25 36.15 4.261 47.987-4.692 5.02-3.797 13.044-11.813 15.914-17.617 7.58-15.323 7.042-40.931 1.74-59.571-.712-2.503-1.746-6.181-3.19-9.187-1.006-2.1-4.134-6.3-3.754-8.153.391-1.916 7.132-7.034 8.577-8.428 2.603-2.51 7.548-5.843 7.948-9.012.43-3.372-1.485-7.984-2.456-11.238-3.245-10.858-6.412-20.895-10.091-30.576" fill="#231f20"/><path d="M73.674 57.38c.411.548 2.674 1.38 5.84-.144 0 0-3.752-.626-3.44-6.881l-1.564.313s-1.615 5.672-.836 6.712" fill="#f7e4cd"/><path d="M101.09 3.617a1.72 1.72 0 1 0-3.44.001 1.72 1.72 0 0 0 3.44-.001M102.81-4.355a1.72 1.72 0 1 0-3.44 0 1.72 1.72 0 0 0 3.44 0" fill="#1d1919"/></g><g><rect transform="matrix(.8 0 0 -.8 0 144)" x="16.854" y="177.38" width="70.412" height="4.12" rx=".983" ry=".983"/><rect transform="scale(1 -1)" x="78.502" y="-2.097" width="50.037" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="13.483" y="-3.697" width="54.831" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="83.296" y="-3.697" width="45.243" height="3.296" rx=".786" ry=".786"/></g></g></symbol><symbol viewBox="0 0 24 24" id="json" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#fbc02d"/></symbol><symbol viewBox="0 0 50 50" id="julia" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" stroke-width="5.673"><circle cx="13.497" cy="281.632" r="9.555" fill="#bc342d"/><circle cx="36.081" cy="281.632" r="9.555" fill="#864e9f"/><circle cx="24.722" cy="262.389" r="9.555" fill="#328a22"/></g></symbol><symbol viewBox="0 0 64 64" id="karma" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -233)"><path d="M38.556 288.413l-20.29-26.687 9.532-7.246 20.29 26.686h-.001.002l5.527 7.247z" fill="#359b8b" stroke-width=".173"/><path d="M35.681 241.172L24.92 255.327v-14.13H12.947v13.817l7.84 33.235h4.132v-13.147l.003.003 20.29-26.686-.008-.006 5.504-7.24H35.84v.12z" fill="#3cbeae" stroke-width=".206"/></g></symbol><symbol viewBox="0 0 24 24" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7 14a2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2 2 2 0 0 1-2 2m5.65-4A5.99 5.99 0 0 0 7 6a6 6 0 0 0-6 6 6 6 0 0 0 6 6 5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4H12.65z" fill="#26a69a"/></symbol><symbol viewBox="0 0 24 24" id="kivy" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.89 0 0 1.89 -12.157 -11.429)" fill="#90a4ae"><path d="M7.026 8.63v4.474l1.928-1.928a.437.437 0 0 0 0-.619zM9.38 16.072v-4.474l-1.927 1.927a.437.437 0 0 0 0 .62zM18.576 10.412l-5.346.564-.017.018 2.39 2.39zM9.922 8.502s.023 3.304-.003 4.452c-.02.856.371 1.114.746 1.507.538.564 1.599 1.57 1.599 1.57a.53.53 0 0 0 .75 0l1.843-1.844a.53.53 0 0 0 0-.75z"/></g></symbol><symbol viewBox="0 0 24 24" id="kl" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:#3aaae1}.b{fill:#fdfeff}</style></defs><title>kl</title><path d="M12.033 1.737c-.25-.003-.5.11-.729.337C8.225 5.15 5.15 8.227 2.078 11.31c-.144.144-.229.346-.341.521v.41c.16.223.294.474.485.666a3259.51 3259.51 0 0 0 8.936 8.937c.193.192.443.325.666.486h.41c.205-.142.436-.256.609-.428 3.046-3.041 6.09-6.083 9.133-9.127.47-.47.472-1.005.006-1.472l-9.218-9.217c-.23-.23-.48-.347-.731-.35zm-1.062 4.545l1.386.832c.702.422 1.403.846 2.109 1.262a.544.544 0 0 1 .04.026l.016.013.017.013c.061.056.089.123.088.224a510.281 510.281 0 0 0 0 3.794.463.463 0 0 1-.007.094c-.015.069-.054.103-.142.109a.464.464 0 0 1-.044.002c-.045-.002-.09-.002-.136-.003-.323-.006-.648-.001-.998-.001v-.527-1.34-.671-.003l.004-.668c0-.147-.039-.231-.17-.308-.893-.528-1.78-1.066-2.67-1.6-.051-.03-.101-.065-.173-.111l.001-.003h-.001zm.362 3.39c.068-.003.119.043.173.138.085.148.174.293.264.44l.015.025c.096.154.194.31.292.47l-1.915 1.176c-.337.207-.673.417-1.014.617-.113.067-.154.143-.154.277.01.977.01 1.955.014 2.932V16H7.7V16h-.002c-.004-.053-.014-.112-.014-.17-.005-1.25-.006-2.501-.015-3.751 0-.142.045-.222.164-.294a467.13 467.13 0 0 0 3.353-2.054l.016-.01a.606.606 0 0 1 .032-.017l.016-.008a.308.308 0 0 1 .033-.013l.012-.004a.157.157 0 0 1 .028-.005l.01-.001zm5.677 3.126l.314.54.346.594v.001c-.158.094-.298.178-.438.259l-3.097 1.798c-.106.062-.189.071-.3.01l-.893-.496-1.524-.843-.895-.493c-.035-.02-.068-.044-.129-.085h.001l.137-.25.495-.902 1.446.795c.442.243.886.483 1.323.734.121.07.212.072.334 0 .894-.525 1.792-1.043 2.689-1.563.057-.034.118-.061.191-.1z" fill="#29b6f6" stroke-width=".041"/></symbol><symbol viewBox="0 0 24 24" id="kotlin" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="gpb"><stop offset="0" stop-color="#cb55c0"/><stop offset="1" stop-color="#f28e0e"/></linearGradient><linearGradient id="gpa"><stop offset="0" stop-color="#0296d8"/><stop offset="1" stop-color="#8371d9"/></linearGradient><linearGradient xlink:href="#gpa" id="gpc" x1="1.725" y1="22.67" x2="22.185" y2="1.982" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/><linearGradient xlink:href="#gpb" id="gpd" x1="1.869" y1="22.382" x2="22.798" y2="3.377" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/></defs><path d="M3.307 3.003v18.048h18.05v-.03L16.88 16.51l-4.48-4.515 4.48-4.515 4.443-4.477H3.307z" fill="url(#gpc)"/><path d="M12.538 3.003l-9.23 9.23v8.818h.083l9.032-9.032-.025-.024 4.48-4.515 4.444-4.477h-8.784z" fill="url(#gpd)"/></symbol><symbol viewBox="0 0 240 240" id="laravel" xmlns="http://www.w3.org/2000/svg"><path d="M216.05 119.036c-1.433.343-24.945 6.673-24.945 6.673l-19.227-28.622c-.537-.828-.99-1.656.359-1.849 1.345-.196 23.195-4.477 24.182-4.723.99-.245 1.837-.536 3.053 1.267 1.21 1.8 17.836 24.626 18.464 25.506.627.877-.447 1.41-1.883 1.748m-4.101 49.326c.588 1.003 1.176 1.64-.67 2.367-1.843.73-62.243 22.847-63.418 23.39-1.173.546-2.092.73-3.607-1.637-1.51-2.362-21.16-39.264-21.16-39.264l64.03-18.075c1.876-.644 2.317-.405 3.103.822 1.074 1.68 21.143 31.403 21.726 32.4m-103.7-21.087c-.78.202-37.566 9.733-39.525 10.22-1.965.485-1.965.246-2.188-.49-.226-.727-43.728-98.053-44.333-99.271-.605-1.214-.574-2.177 0-2.177.571 0 34.734-3.313 35.944-3.383 1.207-.07 1.08.205 1.526 1.033l49.025 91.818c.84 1.58 1.239 1.81-.452 2.248m94.588-59.77c-3.5-4.58-5.2-3.751-7.357-3.41-2.154.336-27.277 4.915-30.194 5.449-2.918.536-4.758 1.803-2.963 4.53 1.597 2.422 18.113 27.824 21.751 33.42l-65.663 17.066L66.18 49.832c-2.075-3.342-2.507-4.514-7.236-4.28-4.735.23-40.969 3.495-43.55 3.731-2.58.233-5.416 1.479-2.835 8.09 2.583 6.612 43.734 102.82 44.88 105.62 1.149 2.803 4.128 7.345 11.11 5.527 7.157-1.871 31.969-8.894 45.52-12.742 7.163 14.07 21.77 42.619 24.473 46.707 3.607 5.459 6.089 4.56 11.626 2.738 4.325-1.42 67.65-26.129 70.502-27.4 2.855-1.273 4.613-2.184 2.685-5.275-1.419-2.28-18.124-26.558-26.876-39.26 5.993-1.733 27.305-7.888 29.575-8.557 2.646-.779 3.008-2.19 1.572-3.94-1.436-1.755-21.293-28.72-24.79-33.296z" fill="#ff5722" stroke="#ff5722" stroke-width="8.852" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="less" xmlns="http://www.w3.org/2000/svg"><path d="M13.696 2.999V5h2.002v5a2 2 0 0 0 1.999 2 2 2 0 0 0-2 2v5h-2v2h2a2 2 0 0 0 2-2v-4a2 2 0 0 1 2-2h1V11h-1a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2.001zm-.03 12.766v.47a1 1 0 0 0 .03-.236 1 1 0 0 0-.03-.234zM10.566 21v-2.001H8.565v-5a2 2 0 0 0-2-2 2 2 0 0 0 2-2V5h2.001v-2H8.565a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-.999V13h1a2 2 0 0 1 2 2v3.999A2 2 0 0 0 8.564 21zm.03-12.766v-.47a1 1 0 0 0-.03.236 1 1 0 0 0 .03.234z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="lib" xmlns="http://www.w3.org/2000/svg"><path d="M19 7H9V5h10m-4 10H9v-2h6m4-2H9V9h10m1-7H8a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2M4 6H2v14a2 2 0 0 0 2 2h14v-2H4V6z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 40 40" id="livescript" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -257)" fill="#317eac"><path stroke-width="3.299" d="M5.419 260.18h3.685v34.207H5.419z"/><path stroke-width="3.299" d="M37.074 288.197v3.685H2.867v-3.685z"/><path stroke-width="2.894" d="M29.612 265.658l2.004 2.005L7.428 291.85l-2.004-2.005z"/><path stroke-width="2.325" d="M10.73 262.471h2.835v22.08H10.73z"/><path stroke-width="2.063" d="M15.36 262.519h2.835v17.382H15.36z"/><path stroke-width="1.77" d="M19.99 262.471h2.835v12.802H19.99z"/><path stroke-width="1.422" d="M24.526 262.491h2.835v8.254h-2.835z"/><path stroke-width="1.128" d="M28.783 262.463h2.835v5.197h-2.835z"/><path stroke-width="2.325" d="M34.801 286.545v-2.835h-22.08v2.835z"/><path stroke-width="2.063" d="M34.753 281.914v-2.835H17.371v2.835z"/><path stroke-width="1.77" d="M34.801 277.284v-2.835H21.999v2.835z"/><path stroke-width="1.422" d="M34.781 272.749v-2.835h-8.254v2.835z"/><path stroke-width="1.128" d="M34.809 268.492v-2.835h-5.197v2.835z"/></g></symbol><symbol viewBox="0 0 24 24" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="lua" xmlns="http://www.w3.org/2000/svg"><circle cx="12.203" cy="12.102" r="10.322" fill="none" stroke="#42a5f5"/><path d="M12.33 5.746a6.483 6.381 0 0 0-6.482 6.381 6.483 6.381 0 0 0 6.482 6.38 6.483 6.381 0 0 0 6.484-6.38 6.483 6.381 0 0 0-6.484-6.38zm1.86 1.916a2.329 2.292 0 0 1 2.33 2.293 2.329 2.292 0 0 1-2.33 2.291 2.329 2.292 0 0 1-2.329-2.29 2.329 2.292 0 0 1 2.328-2.294z" fill="#42a5f5" fill-rule="evenodd"/><ellipse cy="4.615" cx="19.631" rx="2.329" ry="2.292" fill="#42a5f5" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="markdown" xmlns="http://www.w3.org/2000/svg"><path d="M2 16V8h2l3 3 3-3h2v8h-2v-5.17l-3 3-3-3V16H2m14-8h3v4h2.5l-4 4.5-4-4.5H16V8z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" preserveAspectRatio="xMidYMid" id="markojs" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -120.96)" stroke-width=".984"><path d="M4.002 126.482c-.655 1.07-1.32 2.14-1.976 3.21-.655 1.06-1.308 2.142-1.963 3.212l.002.002-.002.002c.655 1.07 1.308 2.15 1.963 3.211.655 1.07 1.32 2.141 1.976 3.211h3.33c-.664-1.07-1.318-2.14-1.974-3.21-.653-1.069-1.307-2.145-1.961-3.214.654-1.068 1.308-2.146 1.961-3.215a601.93 601.93 0 0 1 1.974-3.209z" fill="#2196f3"/><path d="M3.999 126.482l-.002.002c.655 1.07 1.31 2.15 1.964 3.212.655 1.07 1.32 2.14 1.974 3.21h3.331c-.664-1.07-1.319-2.14-1.974-3.21-.653-1.068-1.306-2.146-1.96-3.214z" fill="#26a69a"/><path d="M15.203 126.482l.002.002c-.655 1.07-1.31 2.15-1.965 3.212-.655 1.07-1.319 2.14-1.974 3.21h-3.33c.664-1.07 1.318-2.14 1.973-3.21.654-1.069 1.307-2.146 1.961-3.214z" fill="#8bc34a"/><path d="M11.874 126.484c.664 1.07 1.318 2.14 1.974 3.21.653 1.068 1.307 2.146 1.961 3.214-.654 1.069-1.308 2.145-1.961 3.213-.656 1.07-1.31 2.14-1.974 3.21h3.33c.655-1.07 1.319-2.14 1.974-3.21.655-1.06 1.31-2.14 1.966-3.21l-.002-.003.002-.002c-.656-1.07-1.311-2.152-1.966-3.213-.655-1.07-1.319-2.138-1.974-3.209z" fill="#ffc107"/><path d="M16.74 126.482c.665 1.07 1.319 2.14 1.974 3.21.654 1.068 1.306 2.146 1.96 3.214-.654 1.069-1.306 2.145-1.96 3.213-.655 1.07-1.31 2.141-1.974 3.211h3.33c.656-1.07 1.32-2.14 1.974-3.21.655-1.062 1.31-2.141 1.966-3.212l-.002-.002.002-.002c-.655-1.07-1.31-2.152-1.966-3.213-.655-1.07-1.318-2.138-1.973-3.209z" fill="#f44336"/></g></symbol><symbol viewBox="0 0 23 24" id="mathematica" xmlns="http://www.w3.org/2000/svg"><path d="M11.512 1.523l-.073.025-.46.794-.454.763-1.217 2.09H9.29L5.435 3.5l-.1-.047h-.018v.092l.025.163v.086l.132 1.226v.082l.032.252v.082l.22 2.137v.075l.018.082v.06l-2.348.507-.04.015-.457.1-.025.01h-.042l-1.096.244-.04.007-.17.036v.082l.018.01 1.859 2.086.053.052.114.132.804.909v.005l-.053.05-.22.257-2.564 2.875-.01.007v.082l.071.006.295.075 1.697.366v.006l2.139.472h.015v.047l-.036.252v.08l-.046.412v.082l-.036.244v.082l-.045.412v.08l-.05.41v.08l-.036.244v.082l-.046.412v.082l-.05.407v.082l-.032.248V20l-.05.407v.104h.037l3.642-1.6.294-.134h.018l.177.312.539.911.015.032.854 1.465.16.262.404.695.007.022h.092l.005-.022.017-.025.56-.947.014-.042.6-1.033.316-.539.644-1.091.05.013 3.906 1.721h.035v-.085l-.138-1.32v-.082l-.032-.244v-.082l-.035-.245v-.085l-.033-.244v-.081l-.032-.245v-.082l-.032-.244v-.085l-.035-.245v-.082l-.032-.245v-.082l-.033-.244v-.085l-.025-.17v-.053l1.632-.354.043-.008.458-.107h.028v-.01l.23-.05.03-.01h.042l.382-.09.025-.01h.043l.194-.05h.033l1.015-.23.07-.007v-.064l-.015-.013-1.19-1.342-.028-.028-.197-.22-1.428-1.604v-.006l.295-.323.4-.457 2.148-2.408.015-.01v-.065l-.035-.008-1.288-.28-.372-.084-.047-.01-2.481-.544v-.045l.432-4.265v-.02h-.042l-.302.135-.01.014h-.025l-3.307 1.45-.297.135h-.015l-2.028-3.483-.099-.145-.014-.045zm-.001 1.114l1.365 2.323.34.592-.008.025-1.18 1.511-.517.66-.012-.01-.258-.335-.04-.05-1.397-1.787.03-.063 1.378-2.365.287-.491zm4.908 2.039l-.007.025-.168.225-.538.066zm-9.817.004l.053.02.677.3h-.499l-.224-.3zM16.947 5l-.123 1.248-.113-.928.226-.307zm-9.26.156l.053.024.705.309-.757-.175zm7.388.116l.02.168-1.318.403.003-.003.16-.071 1.015-.444zM9.669 6.388l.944 1.204v.01L9.483 7.2zm3.55.172l.21.682-.234.084-.089.022-.702.255.008-.022.776-.982zm-5 .836l.986.356.898.312.048.02 1.054.373.011 3.086-.362-.117-.67-.224-.081-.038-.735-.245-.77-.256-.29-.1-.011-.255-.032-1.195-.01-.287-.015-.894-.013-.297zm6.583 0l-.011.227-.028.9-.008.303-.032 1.475-.01.262-.337.117-.734.245-.77.256-.712.245-.355.117.01-3.086 1.632-.578zm.585.437l.09.735.79-.097-.915 1.302-.018.006.01-.183.018-.877zm-9.451.536l.152.22 1.447 2.049-2.607.968-.05.015-1.972-2.214-.28-.312.003-.01.115-.018.424-.1.14-.021.337-.078.042-.01zm11.146.003l3.284.713.029.01-.022.025-1.954 2.192-.277.312-.092-.036-2.564-.95.475-.681.152-.216zM6.787 8.52h.86l.036 1.258-.013-.006-.763-1.078zm1.358 2.625l.152.06.77.252.712.245.746.247.49.167-.065.092-1.723 2.334-1.015-.302-.082-.017-.035-.015-1.902-.56.938-1.22.981-1.277zm6.73 0l.033.006 1.787 2.327.132.17-.128.036-.032.014-2.196.642-.105.032-.564.17-.018-.003-1.053-1.44-.174-.239-.547-.726-.007-.018.469-.16.769-.254.713-.245.77-.252zm-7.766.305l-.007.02-.405.523-.291-.291.657-.245zm8.802 0l.043.007.578.212.714.27-.661.394-.375-.479-.03-.042-.262-.342zm-10.843.75l-.67.668.355-.397.207-.23zm12.911.016l.068.025.045.042.554.627.042.043.204.228-.255.135zm-6.473.265l.022.015 1.38 1.872.032.05.343.465.008.031-.088.117-.422.629-.047.074-.245.343-.97 1.43-.013.007-1.18-1.72-.096-.16-.493-.708-.008-.037 1.618-2.191.007-.01zm7.827 1.194l.565.633.063.082-.272-.093-.037-.013zm-15.785.148l.297.299-.637.218-.152.05.038-.058zm13.224.47l-.855.448.346.66-.185-.058-.27-.088-1.092-.348.012-.01zm-9.687.255l1.222.356-.006.007-.458.145-.443.135-.032.01-.49.157zm-2.765.048l.318.32 2.007.517-.567.18-.055.004-2.103-.469-.744-.156.007-.006zm14.966.205l.548.188v.003l-.457.1-.043.014-1.069.23zm-10.23.507l.007.227.01.347.025 1.363.025.691-.007.255-.24.107-2.863 1.255.032-.372.033-.255.017-.227.031-.256.037-.407.045-.42.018-.23.032-.251.032-.412.05-.414.013-.14 1.455-.457.003-.014.301-.098zm4.908 0l1.245.39v.014l.312.1 1.146.362.022.23.03.255.043.408.04.42.017.23.033.251.032.412.042.325.078.848-.078-.04-3.025-1.322-.004-.305.06-2.368zm-4.295.617l.015.007.067.107.6.875-.64.531-.034-1.438zm3.671 0h.008l-.005.06-.02.678-.005.214-.479-.223zm-2.888 3.605l.763.915.001.37-.017-.006-.025-.05-.464-.791-.012-.018zm1.53.61l.184.083-.343.586-.018.007.002-.532z" fill="#f44336" fill-rule="evenodd" stroke="#f44336" stroke-width=".7747499999999999" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 720 720" id="matlab" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><path d="M209.247 329.98L52.368 387.638l121.325 85.822 96.752-95.804-61.198-47.674z" fill="#4db6ac" fill-rule="evenodd" stroke-width=".3"/><path d="M480.193 71.446c-13.123 1.784-9.565 1.013-28.4 16.09-18.008 14.418-69.925 100.347-97.673 129.256-24.688 25.722-34.46 12.199-60.102 33.661-25.68 21.494-65.273 64.464-65.273 64.464l63.978 47.32L394.15 222.754c23.948-32.932 23.694-37.266 36.744-71.82 6.384-16.907 17.76-29.9 27.756-45.809 12.488-19.874 30.186-34.855 21.543-33.68z" fill="#00897b" fill-rule="evenodd" stroke-width=".3"/><path d="M478.206 69.796c-31.268-.189-62.068 137.245-115.56 242.691-54.543 107.519-162.235 176.82-162.235 176.82 18.156 8.243 34.681 4.91 54.236 23.394 13.375 16.164 52.09 95.976 75.174 146.117 0 0 18.964-10.297 42.994-27.695 24.03-17.397 53.124-41.896 73.384-70.3 26.883-37.692 47.897-61.043 65.703-75.271 17.806-14.23 32.404-19.336 46.458-20.54 50.238-4.305 124.582 85.792 124.582 85.792S527.267 70.09 478.206 69.796z" fill="#ffb74d" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 24 24" id="merlin" xmlns="http://www.w3.org/2000/svg"><text style="line-height:1.25;-inkscape-font-specification:'Century Gothic Bold'" x="1.953" y="21.178" transform="scale(.99582 1.0042)" font-weight="700" font-size="30.255" font-family="Century Gothic" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-width=".756"><tspan x="1.953" y="21.178" style="-inkscape-font-specification:'Century Gothic Bold'" font-size="22.745">M</tspan></text></symbol><symbol viewBox="0 0 192 191.99999" id="mocha" xmlns="http://www.w3.org/2000/svg"><title>Mocha Logo</title><g transform="translate(-354.75 -262.42) scale(4.835)" fill="#a1887f"><path d="M103.6 69.6c0-.5-.4-1-1-1H83.8c-.5 0-1 .4-1 1 0 3.4.5 15.1 5.5 20.8.2.2.4.3.7.3h8.4c.3 0 .5-.1.7-.3 5-5.6 5.5-17.3 5.5-20.8zm-7.4 18.2h-5.9c-.3 0-.5-.1-.7-.3-3.4-4-3.8-12-3.9-14.8 0-.5.4-1 1-1h13.2c.5 0 1 .4 1 1 0 2.8-.5 10.7-3.9 14.8-.3.2-.5.3-.8.3zM95.1 66.6s3.6-2.1 1.4-5.9c-1.3-2-1.9-3.7-1.4-4.4-1.3 1.6-3.5 3.3-1.1 6.9.8.9 1.2 2.8 1.1 3.4zM91.1 66.9s2.4-1.4.9-4c-.9-1.3-1.3-2.5-.9-2.9-.9 1.1-2.3 2.2-.7 4.7.5.5.7 1.8.7 2.2z"/><path d="M99.3 78.5c-.4 2.7-1.2 5.8-2.9 7.8-.2.2-.4.3-.6.3h-5c-.2 0-.5-.1-.6-.3-1.2-1.5-2-3.5-2.5-5.6 0 0 5.8.8 9.1-.4 2.4-.9 2.5-1.8 2.5-1.8z"/></g></symbol><symbol viewBox="0 0 24 24" id="movie" xmlns="http://www.w3.org/2000/svg"><path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V4h-4z" fill="#ff9800"/></symbol><symbol viewBox="0 0 24 24" id="music" xmlns="http://www.w3.org/2000/svg"><path d="M16 9V7h-4v5.5c-.42-.31-.93-.5-1.5-.5A2.5 2.5 0 0 0 8 14.5a2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 2.5-2.5V9h3m-4-7a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z" fill="#ef5350"/></symbol><symbol viewBox="0 0 24 24" id="mxml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#ffa726"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#ab47bc" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#26c6da" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#e53935" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#9ccc65" stroke-width="12.914"/></symbol><symbol viewBox="0 0 24 24" id="nim" xmlns="http://www.w3.org/2000/svg"><path d="M4.464 15.75L2.288 3.78l5.985 7.617L12.08 3.78l3.809 7.617 5.985-7.617-2.177 11.97H4.464m15.234 3.264a1.088 1.088 0 0 1-1.088 1.088H5.553a1.088 1.088 0 0 1-1.089-1.088v-1.089h15.234z" stroke-width="1.088" fill="#ffca28"/></symbol><symbol viewBox="0 0 500 500" id="nix" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-1.965 36.302)" stroke-width=".395"><path d="M135.59 415.7c0-.295-2.752-5.283-6.116-11.084-3.364-5.801-6.116-10.776-6.116-11.055s9.514-16.889 21.143-36.912c11.629-20.022 21.323-36.798 21.542-37.279.346-.76-1.608-4.363-14.896-27.466-8.412-14.625-15.294-26.785-15.294-27.023 0-.5 24.46-43.501 25.206-44.31.414-.45.592-.384 1.078.395.32.513 16.876 29.256 36.791 63.87 62.62 108.85 74.852 130.01 75.41 130.46.3.242.544.554.544.694 0 .14-11.836.21-26.302.154-23.023-.09-26.313-.175-26.393-.694-.11-.714-27.662-48.825-28.86-50.392-.746-.978-.906-1.035-1.426-.51-.688.696-28.954 49.323-29.49 50.733l-.365.96h-13.229c-10.896 0-13.229-.095-13.229-.538zm167.58-125.61c-.134-.216 1.188-2.863 2.938-5.882 6.924-11.944 84.291-145.75 96.491-166.88 7.143-12.371 13.142-22.465 13.333-22.433.363.062 25.861 43.105 25.861 43.655 0 .174-6.761 11.952-15.026 26.173-8.46 14.557-14.932 26.104-14.81 26.421.185.483 4.564.564 30.213.564h29.996l.958 1.48c.526.814 3.296 5.547 6.155 10.518 2.859 4.971 5.45 9.29 5.756 9.597.706.705.704.724-.16 1.572-.395.388-3.36 5.323-6.587 10.965-3.228 5.643-6.056 10.387-6.285 10.543-.23.156-19.695.171-43.256.034l-42.84-.249-.804 1.15c-.441.632-7.504 12.736-15.696 26.897l-14.892 25.747H339.03c-8.517 0-20.015.116-25.55.259-6.55.168-10.15.121-10.309-.135zM169.42 132.23c-56.373-.055-102.5-.182-102.5-.282 0-.1 5.617-10.132 12.481-22.294l12.481-22.112h30.332c27.113 0 30.332-.065 30.332-.611 0-.336-6.659-12.228-14.797-26.427-8.139-14.199-14.797-25.917-14.797-26.04 0-.123 2.682-4.853 5.96-10.51s6.003-10.578 6.055-10.934c.086-.586 1.376-.648 13.572-.648 7.413 0 13.463.143 13.446.317-.017.174.222.707.531 1.184.31.476 9.763 16.937 21.007 36.578 11.244 19.64 20.71 36.022 21.036 36.4.554.647 2.549.691 31.428.691h30.837l12.896 22.145c7.093 12.18 12.8 22.301 12.682 22.492-.118.19-4.776.303-10.352.249-5.575-.054-56.26-.143-112.63-.198z" fill="#5075c1"/><path d="M25.289 203.14c-6.098 10.563-6.69 11.711-6.225 12.078.283.224 3.18 5.044 6.44 10.712 3.261 5.668 6.017 10.355 6.124 10.417.106.061 13.585.153 29.95.204 16.367.052 29.994.23 30.285.399.472.273-1.08 3.094-14.637 26.574L62.06 289.793l12.907 21.865c7.1 12.026 12.982 21.906 13.068 21.956.086.05 23.257-39.831 51.492-88.624 11.352-19.617 21.214-36.64 30.37-52.442 23.308-40.452 30.68-53.468 30.73-54.132-1.097-.11-6.141-.187-13.006-.216-3.945-.01-7.82-.02-12.75-.002l-25.341.092-15.42 26.706c-14.256 24.693-15.445 26.663-16.278 26.86l-.024.037c-.011.003-1.62-.001-1.825 0-4.29.062-20.453.063-40.226-.01-22.632-.082-41.615-.125-42.183-.096-.568.03-1.147-.03-1.29-.132-.142-.102-3.29 5.066-6.996 11.485zm205.16-190.3c-.123.149 5.62 10.392 12.761 22.763 12.199 21.131 89.393 155.03 96.276 167 1.502 2.613 2.92 4.803 3.443 5.348.9-1.249 3.531-5.63 7.954-13.219a1342.88 1342.88 0 0 1 10.049-17.76l6.606-11.443c.692-1.403.754-1.818.653-2.117-.162-.48-6.904-12.332-14.982-26.337-8.078-14.005-14.824-25.849-14.991-26.32a.73.73 0 0 1-.009-.366l-.426-.913L359.42 72.5c3.69-6.307 6.425-11.042 9.47-16.29 9.159-15.948 12.037-21.189 11.896-21.55-.126-.324-2.7-4.83-5.72-10.017-3.021-5.185-5.845-10.148-6.275-11.026-.483-.987-.734-1.364-1.1-1.456-.054.014-.083.018-.145.035-.42.112-5.454.195-11.189.185-5.734-.01-11.22.024-12.188.073l-1.76.089-14.997 25.978c-12.824 22.212-15.084 25.964-15.595 25.883-.024-.004-.15-.189-.235-.301-.109.066-.2.09-.272.05-.255-.148-7.143-11.902-15.306-26.119l-14.36-25.016c-.115-.186-.444-.744-.457-.752-.477-.275-50.502.287-50.737.57zm-18.646 283.09c-.047.109-.026.262.042.48.329 1.05 25.338 43.735 25.772 43.985.207.119 14.178.239 31.05.266 26.651.044 30.75.152 31.234.832.308.43 9.988 17.214 21.513 37.296s21.152 36.627 21.394 36.767c.242.14 5.927.243 12.633.23 6.706-.013 12.401.099 12.657.246.132.076.382-.141.852-.795l6.008-10.406c5.234-9.065 6.62-11.684 6.294-11.888-.575-.36-15.597-26.643-23.859-41.482-3.09-5.45-5.37-9.516-5.441-9.774-.195-.712-.065-.822 1.156-.98 1.956-.252 57.397-.057 58.07.205.238.092.79-.569 2.594-3.497 1.866-3.067 5.03-8.524 11-18.866 7.22-12.505 13.044-22.784 12.942-22.843-.102-.059-.771-.051-1.489.016l-.046.001c-4.452.204-33.918.203-149.74.025-38.96-.06-69.786-.09-71.912-.072-1.121.01-2.095.076-2.66.172a.25.25 0 0 0-.062.083z" fill="#7db7e1"/></g></symbol><symbol viewBox="0 0 24 24" id="nodejs" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.85c-.27 0-.55.07-.78.2l-7.44 4.3c-.48.28-.78.8-.78 1.36v8.58c0 .56.3 1.08.78 1.36l1.95 1.12c.95.46 1.27.47 1.71.47 1.4 0 2.21-.85 2.21-2.33V8.44c0-.12-.1-.22-.22-.22H8.5c-.13 0-.23.1-.23.22v8.47c0 .66-.68 1.31-1.77.76L4.45 16.5a.26.26 0 0 1-.11-.21V7.71c0-.09.04-.17.11-.21l7.44-4.29c.06-.04.16-.04.22 0l7.44 4.29c.07.04.11.12.11.21v8.58c0 .08-.04.16-.11.21l-7.44 4.29c-.06.04-.16.04-.23 0L10 19.65c-.08-.03-.16-.04-.21-.01-.53.3-.63.36-1.12.51-.12.04-.31.11.07.32l2.48 1.47c.24.14.5.21.78.21s.54-.07.78-.21l7.44-4.29c.48-.28.78-.8.78-1.36V7.71c0-.56-.3-1.08-.78-1.36l-7.44-4.3c-.23-.13-.5-.2-.78-.2M14 8c-2.12 0-3.39.89-3.39 2.39 0 1.61 1.26 2.08 3.3 2.28 2.43.24 2.62.6 2.62 1.08 0 .83-.67 1.18-2.23 1.18-1.98 0-2.4-.49-2.55-1.47a.226.226 0 0 0-.22-.18h-.96c-.12 0-.21.09-.21.22 0 1.24.68 2.74 3.94 2.74 2.35 0 3.7-.93 3.7-2.55 0-1.61-1.08-2.03-3.37-2.34-2.31-.3-2.54-.46-2.54-1 0-.45.2-1.05 1.91-1.05 1.5 0 2.09.33 2.32 1.36.02.1.11.17.21.17h.97c.05 0 .11-.02.15-.07.04-.04.07-.1.05-.16C17.56 8.82 16.38 8 14 8z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 300 300" id="nodemon" xmlns="http://www.w3.org/2000/svg"><title>nodemon</title><path d="M149.868 20.62c-2.124 0-4.25.55-6.154 1.648L41.899 81.083a12.306 12.306 0 0 0-6.15 10.652v117.633a12.29 12.29 0 0 0 6.152 10.646l101.815 58.766h.001a12.282 12.282 0 0 0 12.291 0l101.84-58.766a12.29 12.29 0 0 0 6.153-10.652V91.738a12.31 12.31 0 0 0-6.146-10.652L156.015 22.27a12.302 12.302 0 0 0-6.153-1.648zM83.303 70.93s11.789 33.031 35.477 31.934l27.74-15.961a7.348 7.348 0 0 1 3.414-.99h.641a7.233 7.233 0 0 1 3.404.99l27.738 15.961c23.69 1.094 35.475-31.934 35.475-31.934 5.233 23.154 1.06 38.641-5.924 48.942l4.541 2.614h.002c2.321 1.327 3.734 3.795 3.737 6.49l-.12 95.811a3.724 3.724 0 0 1-1.855 3.227 3.624 3.624 0 0 1-3.735 0L177.1 206.971c-2.311-1.363-3.742-3.818-3.742-6.48v-44.763a7.44 7.44 0 0 0-3.737-6.465l-15.642-9.01a7.28 7.28 0 0 0-3.715-1.01 7.378 7.378 0 0 0-3.742 1.01l-15.648 9.01c-2.316 1.323-3.729 3.798-3.729 6.467v44.762c0 2.663-1.413 5.1-3.738 6.48l-36.748 21.041a3.571 3.571 0 0 1-3.71 0c-1.173-.65-1.864-1.887-1.864-3.224l-.137-95.812a7.483 7.483 0 0 1 3.74-6.49l4.541-2.615c-6.982-10.302-11.16-25.79-5.925-48.942z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 990 990" id="npm" xmlns="http://www.w3.org/2000/svg"><defs><style>.hncls-1{fill:#cb3837}.cls-2{fill:#fff}</style></defs><title>n</title><path class="hncls-1" d="M113.26 876.74V113.27h763.47v763.47zm143.59-620.4v476.18h240.61V355.63h140.21v376.96h95.457V256.34z" fill="#e53935" stroke-width=".771"/></symbol><symbol id="nunjucks" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.host0{fill:#388e3c}</style><path class="host0" d="M11.2 21.1H8.1l-2.3-7.9v7.9H2.7V2.9h3.1l2.3 7.4V2.9h3.1zM21.3 19.2c0 1-.8 1.9-1.9 1.9h-4.8c-1 0-1.9-.8-1.9-1.9v-3.8l3.2-.7V18h2.3V7.2h3.1v12z"/></symbol><symbol viewBox="0 0 150 150.00001" id="ocaml" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.76136 0 0 .76136 11.616 19.98)"><path d="M83.02 101.645l.023-.062c-.035-.159-.047-.195-.024.062z" fill="none" stroke-width="1.028"/><linearGradient id="hpa" gradientUnits="userSpaceOnUse" x1="-696.735" y1="97.7" x2="-696.735" y2="142.997" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M82.313 138.79c-.471-1.004-1.904-3.621-2.624-4.46-1.562-1.828-1.927-1.966-2.386-4.275-.799-4.02-2.913-11.31-5.405-16.341-1.286-2.596-3.426-4.777-5.385-6.66-1.71-1.652-5.565-4.431-6.237-4.294-6.296 1.257-8.249 7.432-11.21 12.323-1.638 2.705-3.374 5.007-4.665 7.885-1.192 2.646-1.087 5.577-3.128 7.849-2.093 2.333-3.454 4.814-4.48 7.829-.194.574-.747 6.596-1.348 8.015l9.357-.659c8.719.594 6.2 3.936 19.81 3.208l21.487-.665c-.666-1.97-1.584-4.25-1.938-4.991-.599-1.248-1.352-3.69-1.848-4.763z" fill="url(#hpa)" stroke-width="1.028"/><linearGradient id="hpb" gradientUnits="userSpaceOnUse" x1="-666.972" y1="142.12" x2="-666.972" y2="142.12" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><linearGradient id="hpc" gradientUnits="userSpaceOnUse" x1="-675.228" y1="-1.28" x2="-675.228" y2="142.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M109.553 94.296c-1.652 1.193-4.88 4.06-11.902 5.145-3.152.487-6.1.527-9.335.365-1.584-.076-3.077-.157-4.665-.177-.936-.008-4.074-.107-3.919.193l-.349.871c.054.287.169 1.004.2 1.177.129.704.165 1.265.192 1.912.048 1.331-.11 2.719-.043 4.062.141 2.787 1.175 5.326 1.306 8.137.143 3.13 1.69 6.442 3.188 8.998.569.973 1.434 1.084 1.811 2.283.442 1.373.024 2.83.239 4.293.842 5.675 2.477 11.606 5.032 16.728.018.043.038.09.06.128 3.156-.53 6.318-1.665 10.418-2.271 7.517-1.115 17.972-.54 24.688-1.17 16.993-1.597 26.216 6.97 41.478 3.459V22.459c0-11.84-9.594-21.438-21.435-21.438H19.239C7.4 1.021-2.197 10.62-2.197 22.458v46.774c3.067-1.11 7.479-7.635 8.861-9.222 2.419-2.775 2.858-6.315 4.062-8.544 2.743-5.078 3.215-8.57 9.451-8.57 2.907 0 4.061.67 6.027 3.31 1.368 1.834 3.731 5.224 4.837 7.49 1.277 2.615 3.357 6.153 4.272 6.867.677.53 1.35.928 1.976 1.163 1.012.38 1.848-.316 2.525-.855.863-.687 1.235-2.088 2.035-3.957 1.152-2.696 2.408-5.926 3.122-7.054 1.237-1.949 1.658-4.261 2.993-5.381 1.97-1.652 4.54-1.768 5.246-1.908 3.957-.781 5.755 1.906 7.704 3.645 1.276 1.138 3.019 3.432 4.256 6.507.967 2.4 2.199 4.622 2.714 6.008.497 1.339 1.725 3.484 2.453 6.055.661 2.336 2.43 4.125 3.102 5.235 0 0 1.029 2.882 7.285 5.516 1.357.572 4.1 1.501 5.736 2.096 2.718.988 5.351.86 8.704.458 2.391 0 3.686-3.462 4.772-6.234.643-1.639 1.259-6.334 1.678-7.667.406-1.297-.544-2.3.265-3.437.946-1.327 1.508-1.399 2.054-3.129 1.172-3.704 7.95-3.89 11.761-3.89 3.176 0 2.772 3.083 8.16 2.028 3.086-.605 6.059.398 9.335 1.265 2.758.732 5.352 1.566 6.906 3.385 1.005 1.178 3.5 7.08.958 7.331.244.3.423.84.88 1.135-.566 2.226-3.03.64-4.4.355-1.845-.383-3.147.057-4.952.856-3.085 1.374-7.598 1.214-10.286 3.452-2.281 1.898-2.277 6.133-3.34 8.507-.002-.001-2.955 7.6-9.402 12.248z" fill="url(#hpc)" stroke-width="1.028"/><linearGradient id="hpd" gradientUnits="userSpaceOnUse" x1="-735.137" y1="90.833" x2="-735.137" y2="141.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M38.247 105.09c-1.467-.15-2.83-.317-4.256-.605-2.662-.536-5.57-1.06-8.193-1.688-1.592-.385-6.895-2.263-8.048-2.792-2.702-1.246-4.496-4.63-6.609-4.282-1.348.22-2.662.682-3.5 2.042-.685 1.11-.917 3.016-1.391 4.294-.55 1.485-1.5 2.87-2.331 4.284-1.53 2.595-4.282 4.941-5.468 7.469-.239.52-.45 1.101-.649 1.708V144.415a48.57 48.57 0 0 1 4.45.96c11.955 3.19 14.872 3.46 26.598 2.119l1.1-.146c.897-1.867 1.59-8.227 2.171-10.195.454-1.51 1.077-2.712 1.313-4.253.223-1.463-.02-2.858-.146-4.188-.329-3.332 2.427-4.522 3.742-7.384 1.186-2.589 1.871-5.535 2.853-8.181.941-2.54 2.41-6.13 4.918-7.408-.305-.355-5.237-.518-6.554-.65z" fill="url(#hpd)" stroke-width="1.028"/></g></symbol><symbol viewBox="0 0 24 24" id="pdf" xmlns="http://www.w3.org/2000/svg"><path d="M14 9h5.5L14 3.5V9M7 2h8l6 6v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m4.93 10.44c.41.9.93 1.64 1.53 2.15l.41.32c-.87.16-2.07.44-3.34.93l-.11.04.5-1.04c.45-.87.78-1.66 1.01-2.4m6.48 3.81c.18-.18.27-.41.28-.66.03-.2-.02-.39-.12-.55-.29-.47-1.04-.69-2.28-.69l-1.29.07-.87-.58c-.63-.52-1.2-1.43-1.6-2.56l.04-.14c.33-1.33.64-2.94-.02-3.6a.853.853 0 0 0-.61-.24h-.24c-.37 0-.7.39-.79.77-.37 1.33-.15 2.06.22 3.27v.01c-.25.88-.57 1.9-1.08 2.93l-.96 1.8-.89.49c-1.2.75-1.77 1.59-1.88 2.12-.04.19-.02.36.05.54l.03.05.48.31.44.11c.81 0 1.73-.95 2.97-3.07l.18-.07c1.03-.33 2.31-.56 4.03-.75 1.03.51 2.24.74 3 .74.44 0 .74-.11.91-.3m-.41-.71l.09.11c-.01.1-.04.11-.09.13h-.04l-.19.02c-.46 0-1.17-.19-1.9-.51.09-.1.13-.1.23-.1 1.4 0 1.8.25 1.9.35M8.83 17c-.65 1.19-1.24 1.85-1.69 2 .05-.38.5-1.04 1.21-1.69l.48-.31m3.02-6.91c-.23-.9-.24-1.63-.07-2.05l.07-.12.15.05c.17.24.19.56.09 1.1l-.03.16-.16.82-.05.04z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="perl" xmlns="http://www.w3.org/2000/svg"><path d="M12 14c-1 0-3 1-3 2 0 2 3 2 3 2v-1a1 1 0 0 1-1-1 1 1 0 0 1 1-1v-1m0 5s-4-.5-4-2.5c0-3 3-3.75 4-3.75V11.5c-1 0-5 1.5-5 4.5 0 4 5 4 5 4v-1M10.07 7.03l1.19.53c.43-2.44 1.58-4.06 1.58-4.06-.43 1.03-.71 1.88-.89 2.55C13.16 3.55 15.61 2 15.61 2a15.916 15.916 0 0 0-2.64 3.53c1.58-1.68 3.77-2.78 3.77-2.78-2.69 1.72-3.9 4.45-4.2 5.21l.55.08c0 .52 0 1 .25 1.38C14.1 11.31 18 11.47 18 16s-4.03 6-6.17 6C9.69 22 5 21.03 5 16s4.95-5.07 5.83-7.08c.12-.38-.76-1.89-.76-1.89z" fill="#9575cd"/></symbol><symbol viewBox="0 0 24 24" id="php" xmlns="http://www.w3.org/2000/svg"><path d="M12 18.08c-6.63 0-12-2.72-12-6.08s5.37-6.08 12-6.08S24 8.64 24 12s-5.37 6.08-12 6.08m-5.19-7.95c.54 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.58 1.09-.28.22-.71.33-1.29.33h-.87l.53-2.76h.99m-3.5 5.55h1.44l.34-1.75h1.23c.54 0 .98-.06 1.33-.17.35-.12.67-.31.96-.58.24-.22.43-.46.58-.73.15-.26.26-.56.31-.88.16-.78.05-1.39-.33-1.82-.39-.44-.99-.65-1.82-.65H4.59l-1.28 6.58m7.25-8.33l-1.28 6.58h1.42l.74-3.77h1.14c.36 0 .6.06.71.18.11.12.13.34.07.66l-.57 2.93h1.45l.59-3.07c.13-.62.03-1.07-.27-1.36-.3-.27-.85-.4-1.65-.4h-1.27L12 7.35h-1.44M18 10.13c.55 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.57 1.09-.29.22-.72.33-1.3.33h-.85l.5-2.76h1m-3.5 5.55h1.44l.34-1.75h1.22c.55 0 1-.06 1.35-.17.35-.12.65-.31.95-.58.24-.22.44-.46.58-.73.15-.26.26-.56.32-.88.15-.78.04-1.39-.34-1.82-.36-.44-.99-.65-1.82-.65h-2.75l-1.29 6.58z" fill="#1E88E5"/></symbol><symbol viewBox="0 0 79 78" id="postcss" xmlns="http://www.w3.org/2000/svg"><title>postcss-logo-symbol</title><g transform="translate(5.48 5.52) scale(.85425)" fill="#e53935" fill-rule="evenodd" stroke="#e53935" stroke-width="1.519"><path d="M15.447 32.623c.106.08.29.132.106.29-.132.184-.29.342-.395.553-.105.185-.184.237-.342.106.21-.343.42-.66.63-.95zM68.342 60.24c0 .078.026.13.026.21.053-.105.053-.158.08-.21zm0 .236v-.026zm-5.368 10.277l-4.58-25.402c-.078-.025-.183-.077-.368-.13.053.105.08.184.106.263.13-.026.184-.026.236-.052 0-.026 0-.052.027-.08l4.58 25.404zm-4.737-31.12c-.026.078-.026.158-.026.237 0-.08 0-.16.028-.238zm.026.526c-.026 0-.026 0-.052-.028v.026c.028.026.028.026.054 0zm-.052.21v-.185c-.077.026-.156.026-.262.053.132.05.264.078.264.13z"/><path d="M78.71 33.967c-.052-1.028-.078-2.056-.184-3.083-.184-1.397-.368-2.82-.684-4.19-.237-1.133-.63-2.214-1.026-3.294-.5-1.265-1-2.556-1.632-3.768-1.026-1.95-2.368-3.69-3.605-5.508-.818-1.16-1.87-2.108-2.66-3.294-.447-.685-1.105-1.264-1.763-1.79-1.053-.845-2.158-1.61-3.263-2.347a32.525 32.525 0 0 0-2.58-1.634c-.71-.397-1.473-.713-2.21-1.056-.842-.395-1.658-.87-2.605-1.054-.238-.05-.448-.13-.685-.21-.605-.21-1.184-.447-1.79-.632-.92-.29-1.815-.632-2.763-.87C50.342 1 49.394.843 48.446.71 47.394.555 46.316.5 45.262.397a26.83 26.83 0 0 0-2.026-.184C42.236.16 41.21.16 40.21.134c-.5-.027-1.026-.08-1.526-.053-.763.026-1.526.105-2.29.21-.736.08-1.473.21-2.183.317-.867.105-1.735.158-2.604.264-.816.106-1.658.264-2.473.396-.29.053-.58.158-.87.21-.63.132-1.288.185-1.92.396-1.13.344-2.263.74-3.368 1.16-1.027.422-2.027.87-3 1.397-1 .552-1.948 1.21-2.895 1.844a45.325 45.325 0 0 0-2.66 1.923c-.84.66-1.63 1.397-2.394 2.135-.42.42-.763.922-1.158 1.396-.657.765-1.315 1.502-1.947 2.293-.524.66-1 1.344-1.5 2.03-.893 1.21-1.656 2.502-2.366 3.794-.29.527-.553 1.054-.816 1.58-.395.79-.816 1.555-1.184 2.372-.264.554-.474 1.16-.632 1.766-.367 1.292-.736 2.61-1.078 3.9-.316 1.16-.395 2.372-.42 3.558-.027 1.054.078 2.082.183 3.136.027.264-.13.58.184.79-.105.29-.026.45.13.5-.182.29.08.476-.024.74-.027.052.08.157.13.236 0 .08-.025.185 0 .264.028.237.133.474.133.738 0 .184.157.395.21.58.026.078 0 .21-.053.263-.158.184-.132.342.105.448.133.342.08.5.054.66.052.236-.027.315 0 .368.21.422.29.896.315 1.37 0 .106.053.212.106.343.026 0 0 .5 0 .5.13-.078.237-.104.368-.157.08.342.158.66.263.95.132.21.132.314.08.34.105.474.157.922.34 1.37 0-.5-.05-1-.13-1.475.368.132.684.263.895.263.027-.08.053-.184.08-.237-.158-.157-.29-.394-.448-.552.053.21 0 .29 0 .37-.105-.054-.237-.107-.368-.16.105-.13.21-.263.368-.42 0-.238-.13-.45-.5-.423.158-.052.316-.13.5-.184.29-.157-.026-.447-.026-.816.026-.447-.237-.895-.316-1.37-.132-.737-.105-1.844-.184-2.582-.158-.132-.29.21-.316.237.08.632.158 1.264.21 1.897-.157-.527-.263-1.107-.394-1.74-.027.185-.053.264-.053.37-.13.13-.026.29.053.474-.184-.08-.395-.052-.395-.052v.738c-.262-.264-.34-.474-.473-.66-.052-.21-.08-.42-.13-.63.05-.133 0-.212 0-.29a15.968 15.968 0 0 1-.08-.634c.026-.026-.026-.42-.026-.42.21.025.343.05.474.05-.263-.34-.08-.552.027-.763.053-.106.237-.13.29-.238.21-.395.553-.71.553-1.212 0-.237.08-.5.105-.738.053-.448.105-.896.13-1.344.054-.58 0-1.16.133-1.713.212-.92.475-1.843.764-2.766.21-.66.448-1.29.71-1.95.395-1.028.764-2.056 1.264-3.03.71-1.424 1.526-2.794 2.316-4.19.5-.87 1.026-1.687 1.58-2.53.525-.817 1.05-1.66 1.657-2.425a21.452 21.452 0 0 1 2.79-2.978c1.053-.948 2.053-1.923 3.184-2.793a32.218 32.218 0 0 1 4.685-3.005c1.343-.71 2.737-1.266 4.132-1.793.895-.342 1.868-.5 2.79-.79 1.052-.343 2.105-.5 3.21-.527.71-.027 1.395-.106 2.105-.185.632-.05 1.263-.104 1.948-.183-.08.105-.106.158-.132.21-.288.422-.604.844-.894 1.265-.237.343-.5.712-.737 1.054-.422.555-.87 1.108-1.264 1.688-.605.87-1.158 1.766-1.79 2.635-.63.843-1.315 1.634-1.973 2.45-.868 1.134-1.684 2.293-2.552 3.426-.79 1.08-1.63 2.11-2.394 3.19-.684.947-1.29 1.95-1.948 2.923-.973 1.45-1.947 2.872-2.92 4.322a271.93 271.93 0 0 1-2.316 3.294c-.053.08-.132.104-.21.157-.21.342-.21.527-.29.685-.21.395-.42.79-.658 1.16-.132.21-.316.394-.474.605-.026-.316.42-.474.21-.87-.13.212-.263.396-.394.607l-.316.63c.105.08.29.133.105.29-.08.133-.158.29-.237.423a.954.954 0 0 0 .29-.264c0 .29-.158.526-.29.763-.105.21-.368.37-.552.527.026.027.21.106.237.132.237-.08.316-.21.343-.132.08-.105.158-.184.184-.263.104-.264.262-.474.525-.58.106-.053.184-.132.263-.21.79-.818 1.606-1.608 2.316-2.478 1.106-1.345 2.106-2.74 3.16-4.11.446-.58.973-1.16 1.446-1.714.078.606.026 1.185 0 1.74-.08.974-.132 1.95-.21 2.95-.027.395 0 .79-.027 1.186 0 .105-.08.184-.08.29 0 .263.08.553.08.817-.08.975-.186 1.923-.265 2.898-.027.21.078.422.13.607-.13 1.422.16 2.925-.078 4.427.184-.29.237-.474.237-.658.025-.158 0-.316 0-.5v-.264c.025-.475.13-.975.078-1.45-.053-.527-.053-1.027.053-1.528.053-.21-.026-.474.106-.738v.395c-.026 1.5.027 3.003-.183 4.505-.027.132.08.37-.21.343-.238.474.052.817-.21 1.08-.054.053.05.29.077.448-.106.317-.106.317.052.343.026.58.08 1.106.105 1.66.42-1 .21-2.03.396-3.058.026.422.053.844.026 1.29 0 .687-.026 1.345-.052 2.03 0 .132-.027.264-.053.396-.08.37-.105.738-.237 1.08-.105.264-.052.66-.052.975v1.003c.105.448-.027.685.052.948-.08.265-.105.344-.08.423l.08.395c.527-.053.29.343.5.553-.158.212-.105.29-.105.397 0 .237-.025.448-.052.685 0 .606-.026 1.212-.026 1.792 0 .08.026.157.026.236 0 .054-.026.74-.026.74.053.078 0 .157-.08.236-.025 0-.104-3.347-.104-3.347h-.395c-.052 1.58.08 3.003-.21 4.48-.316.025-.42.078-.764.078-.816 0-1.632 0-2.448.026-.974 0-1.92.026-2.895.026-.472 0-.972.054-1.446.054-.632 0-1.29-.08-1.92-.08-.975 0-1.922.08-2.896.106-.71.026-1.42.026-2.13.053-.475.025-.95.05-1.422.104-.21.026-.395.105-.658.184-.08 0-.263-.026-.42 0-.265.053-.5.21-.765.264-.395.08-.5.184-.448.58v.263c-.026.052.58-.08.58-.08-.054 0-.08.158-.16.29.212-.08.343-.132.475-.184.395.185.737.08 1.052.16 1.026.262 2.078.37 3.13.473.685.053 1.343.08 2.027.105.973.053 1.947.106 2.92.106.816 0 1.606-.08 2.42-.08 1.13 0 2.264.052 3.395.08.237 0 .5-.028.763-.028h1.92c1.712-.052 3.422-.08 5.133-.13.975-.028 1.975-.08 2.948-.107l3-.08c1.158-.026 2.316-.026 3.448-.05.868 0 1.71-.03 2.58-.055.972-.026 1.972-.105 2.946-.157.527-.027 1.054-.08 1.58-.132.632-.052 1.29-.13 1.92-.157.948-.054 1.922-.08 2.87-.133 1.184-.078 2.368-.183 3.578-.21 1.106-.052 2.237-.026 3.343-.052.974-.027 1.948-.08 2.948-.106l1.66-.08s1.104-.026 1.657-.08c.947-.052 1.894-.157 2.842-.183.604-.027 1.21 0 1.815-.027.973-.026 1.973-.08 2.947-.08.367 0 .762.054 1.236.08-.21.185-.342.29-.5.422.105.026.21.08.316.132a.71.71 0 0 1-.42.13c-.054.133-.107.186-.16.45h.474c-.184 0-.342.237-.526.395-.21-.054-.395 0-.5.29.184.104.158.183.132.29-.316.104-.553.21-.42.552-.107.052-.238.105-.37.184-.13.21-.368.263-.316.553.106.025.21.08.29.104-.132.053-.263.132-.395.184-.473.29-.262.422-.157.554-.08.053-.158.105-.237.132.052.237.13.29.157.29a9.3 9.3 0 0 0-.395.316c-.08.237-.185.342-.29.5s-.158.37-.29.527c-.552.607-.947 1.32-1.657 1.793-.264.185-.5.422-.737.66-.474.447-.895.948-1.395 1.37a29.595 29.595 0 0 1-2.052 1.554 151.56 151.56 0 0 1-2.604 1.792c-.474.315-1 .552-1.5.842s-.974.554-1.474.843c-.316.21-.606.5-.948.66-.868.37-1.79.685-2.684 1.028-.87.37-1.5.685-2.158.922-.605.21-1.237.37-1.868.5-.21.054-.448 0-.685.027-.448.08-.895.186-1.343.238-1.158.158-2.316.264-3.473.422-.685.08-1.343.21-2.027.29-.473.026-.973-.026-1.447-.026-.342 0-.71.08-1.053.027-.552-.08-1.105-.21-1.658-.316-.13-.026-.316-.08-.42-.026-.21.106-.396-.052-.607 0-.13.027-.262-.08-.394-.08-.106-.025-.238.028-.37 0-.29-.078-.552-.183-.87-.157-.313.026-.63-.132-.97-.21-.475-.106-.92-.21-1.396-.317a2.38 2.38 0 0 1-.525-.237c-.685 0-1.133-.026-1.554-.185-.368-.13-.71-.315-1.105-.262-.104.026-.183-.026-.29-.026-.08-.106-.157-.317-.235-.317-.526.027-.842-.42-1.29-.553-.236-.08-.42-.343-.657-.422-.58-.237-1.052-.737-1.71-.816-.21-.027-.42-.132-.658-.21.08.104.13.183.21.262-.763-.37-1.473-.79-2.184-1.186-.104-.026-.183-.13-.262-.184l-.71-.474c-.395.08-.553-.08-.66-.132-.71-.5-1.525-.817-2.21-1.37-.29-.238-.63-.396-.84-.686-.37-.448-.817-.764-1.317-1.027-.394-.21-.762-.448-1.13-.685-.185-.132-.37-.29-.37-.58 0-.185-.078-.37-.315-.264-.105-.158-.21-.342-.342-.395-.316-.13-.526-.37-.763-.58s-.42-.5-.71-.605c-.527-.21-.843-.658-1.158-1.027-.738-.87-1.396-1.82-2.08-2.74-.053-.08-.158-.133-.237-.212.105.29.237.527.368.79-.262-.105-.446-.29-.604-.474-.027.027 1.815 3.057 1.815 3.057.16.237.29.475.448.712a.813.813 0 0 1-.79-.422c-.236-.42-.5-.684-1.026-.63a4.588 4.588 0 0 1-.13-.58c-.107 0-.185 0-.37-.027.37.58.685 1.08 1.027 1.66-.133-.08-.21-.132-.265-.158.473.5.815 1.133 1.42 1.45.132.605.816.895.974 1.475-.13-.027-.238-.053-.37-.08-.21-.263-.447-.526-.683-.816.052.184.13.342.236.474.316.395.606.79.974 1.133.132.134.316.187.316.424.21.105.29.13.368.13.054.16-.025.397.29.344.21.395.42.395.71.264.343.343.528.37.764.16 0 .13.026.262.026.368.105-.053.08-.132.08-.264.13.105.21.158.262.21.263.37.5.712.868 1.002.5.422.948.87 1.42 1.265.922.765 1.95 1.398 2.975 1.977 1.264.712 2.475 1.476 3.764 2.16 1.552.818 3.21 1.372 4.92 1.767.632.132 1.237.263 1.87.42.55.16 1.104.397 1.657.528.842.185 1.71.343 2.552.5.183.027.37.054.58.08.235.053.524-.053.577.027.132.21.237.104.395.078.184-.053.395-.053.605-.053.737.026 1.447.184 2.184.132.16 0 .396-.133.528.13.236-.105.368-.105.473-.13.028.236 0 .236-.05.262-.054.026-.133.053-.238.132.947.184 1.842.21 2.63 0 1.37.105 2.554-.053 3.686-.448.105.132.184.316.342.053.052-.08.184-.107.29-.133.236-.053.526-.158.736-.08.238.08.317-.13.5-.13.317 0 .606-.027.896-.08.158-.026.316-.105.5-.158a1.285 1.285 0 0 0-.58-.133c.317-.158.606-.29.896-.42-.053.078-.106.183-.21.183h.367c-.08 0-.185.237-.316.395.946-.237 1.814-.448 2.657-.66-.29-.552.315-.367.526-.684-.263.08-.526.158-.79.21.895-.447 1.816-.842 2.71-1.237-.13.158-.29.237-.525.37.158.025.263.025.342.05.42.133.316-.262.447-.5.5 0 .71-.078.947-.158.263-.08.526-.158.79-.263.42-.184.815-.42 1.236-.63.08-.028.21 0 .316 0 .29-.186.394-.344.473-.318.37.053.63-.08.736-.42.184-.133.316-.238.447-.318.578-.316 1.13-.632 1.71-.948.21 0 .316 0 .368-.027.344-.16.66-.342.975-.527a2.258 2.258 0 0 1-.263-.13c.262-.054.34-.08.5-.133.63-.74 1.5-1.24 2.157-1.82.29-.026.29-.105.29-.157.104-.132.21-.29.34-.396.58-.527 1.21-.975 1.737-1.528a37.16 37.16 0 0 0 2.184-2.374c.63-.738 1.264-1.475 1.79-2.292.737-1.133 1.368-2.293 2.026-3.48.474-.842.895-1.685 1.37-2.528.05-.08.157-.185.236-.185.71-.08 1.422-.13 2.106-.21.158-.026.342-.13.5-.21-.08-.132-.132-.29-.21-.422-.106-.16-.264-.29-.37-.45-.104-.13-.183-.29-.262-.447-.08-.13-.158-.236-.237-.37a9.7 9.7 0 0 1-.45-.894c-.026-.08-.08-.21-.052-.29.474-1.027.658-2.134 1.105-3.162.447-1.054.58-2.24.79-3.373.184-1.08.29-2.16.42-3.24.08-.764.185-1.502.21-2.266.16-1.212.106-2.346.08-3.48-.026-1-.08-2.028-.13-3.03zM12.685 66.405c-.184-.21-.342-.448-.526-.658l.08-.08c.287.317.577.633.866.976-.158-.08-.342-.132-.42-.238zm.42.238c.08-.027.16-.027.238-.053.08.132.132.29.21.448-.368-.027-.552-.185-.447-.395zm27.37 10.883v-.08c.5-.052.973-.105 1.473-.157v.077c-.5.08-.973.13-1.473.158zm6.63-.685c-.367.08-.762.133-1.13.186-.132.026-.29.158-.342-.08-.053.027-.106.027-.158.054.13.394.447.078.71.236-.58.08-1.13.132-1.684.21v-.052c.16-.026.343-.053.5-.08v-.078a7.743 7.743 0 0 0-.79-.053c-.077 0-.183.106-.262.132-.105.026-.21.053-.342.053-.447.026-.894.026-1.316.052-.027 0-.08-.026-.106-.026v-.08c1.763-.236 3.5-.473 5.263-.71.027.052.027.105.053.157-.158 0-.263.055-.395.08zm.396-.262c.606-.08 1.16-.132 1.738-.21-1.21.342-1.605.394-1.737.21zM24.58 23.374c.84-1.16 1.71-2.32 2.552-3.505.263-.345.473-.714.736-1.056.08-.106.185-.158.316-.264l-.026-.05c.105-.133.21-.24.263-.344.134-.21.213-.448.318-.685a.385.385 0 0 1 .105-.103c.37.184.37-.21.5-.343.237-.264.474-.553.684-.817.158-.21.316-.395.448-.632.026-.08-.053-.21-.08-.317h-.078c.08-.052.158-.13.237-.184.026 0 .026 0 .052-.026.158-.238.316-.475.474-.686.315-.42.657-.842 1.025-1.21-.052.13-.105.263-.158.368.027 0 .027.027.053.027.316-.422.658-.817.974-1.24-.027-.025-.053-.052-.08-.052-.13.132-.236.264-.368.396-.026-.027-.052-.053-.08-.053.265-.343.528-.685.79-1.08.053.08.106.184.21.395.107-.263.212-.447.29-.632-.078.08-.183.158-.262.238l-.08-.08.474-.71c.5-.712 1-1.45 1.5-2.162.185-.263.42-.474.58-.738.5-1 1.29-1.792 1.894-2.714.132-.184.316-.342.474-.5.13-.16.237-.106.342.026.71.896 1.42 1.818 2.13 2.714.528.66 1.054 1.29 1.554 1.976.605.844 1.184 1.687 1.79 2.53.684.975 1.368 1.95 2.026 2.95 1 1.477 1.947 2.953 2.947 4.428.737 1.08 1.474 2.135 2.184 3.215h-1.344c-1.236-.025-2.5-.13-3.736-.078-1.684.08-3.394.264-5.078.396-2.132.185-4.29.21-6.42.21-.765 0-1.528.107-2.29.16-.922.052-1.817.105-2.738.13-1.08.054-2.13.08-3.21.107-.606.026-1.237 0-1.895 0zm30.183 12.12v.238c-.026 0-.052.027-.105.027-.105-.37-.21-.766-.342-1.135-.263-.765-.553-1.53-1.027-2.214-.528-.737-1-1.5-1.528-2.265-.13-.185-.316-.343-.474-.5-.553-.607-1.106-1.24-1.816-1.687a21.485 21.485 0 0 0-3.29-1.688 7.374 7.374 0 0 1-.92-.474h.63l4.5-.08c.974-.025 1.922-.025 2.895-.078.236 0 .368.08.5.29.236.395.473.79.736 1.186.027.052.08.13.08.21 0 .58 0 1.186.026 1.766.025.606.08 1.186.104 1.792 0 .606-.053 1.238-.026 1.87.027.897.053 1.82.053 2.74zM26.447 26.67c1.237-.053 2.42-.132 3.632-.185.945-.053 1.92-.08 2.866-.132.395-.025.764-.05 1.158 0-.42.212-.842.423-1.21.686-.474.316-.92.737-1.395 1.08-.475.342-.896.764-1.29 1.212-.5.605-1.053 1.132-1.58 1.712-.37.422-.79.817-1.105 1.265-.447.58-.842 1.21-1.263 1.87.132-2.504.29-4.98.184-7.51zm17.185 25.35c-.843.21-1.71.448-2.58.553-.736.106-1.5.08-2.263.08a25.42 25.42 0 0 1-2.028-.08c-.763-.078-1.526-.157-2.263-.5-.633-.29-1.29-.553-1.92-.87-.634-.316-1.265-.684-1.74-1.264-.34-.423-.815-.765-1.236-1.134.08.316.263.58.553.764-.132.158-.316.08-.58-.343-.078.053-.157.08-.21.106.08-.185.158-.37.237-.527-.105-.21-.237-.448-.342-.66-.21-.342-.42-.71-.605-1.053-.053-.08-.053-.158-.105-.237a5.893 5.893 0 0 1-.37-.475c-.21-.315-.394-.657-.657-.974 0 .08.027.158.027.264-.027 0-.053.026-.053.026l-.554-1.344c-.026 0-.026 0-.052.026l.473 1.74c-.026 0-.052.025-.08.025-.077-.104-.156-.21-.21-.34-.052-.212-.21-.212-.34-.133-.08.053-.133.237-.106.316.185.448.395.896.606 1.344.052.158.105.29.184.448.027.053.106.105.106.184.106.21.185.42.316.606.237.316.5.632.737.948.235.316.445.66.656.975.026.053.105.053.13.08.133.395.58.684.896.526.08.606.737.817 1 1.397a11.957 11.957 0 0 1-.763-.343c-.027.026-.027.052-.054.105.316.158.632.316.92.5.265.16.528.317.765.5.316.29.685.45 1.13.554a.282.282 0 0 0-.05-.107c.736.343 1.5.712 2.078 1-2.737.054-5.658.107-8.685.16 0-.5-.026-.975-.026-1.476 0-.21.052-.395.025-.606-.08-1.21-.08-2.424-.237-3.61-.157-1.264-.157-2.503-.13-3.77.025-.683-.027-1.394-.054-2.08 0-.922 0-1.82.028-2.74 0-.132.053-.237.106-.37h.08c.025.054 0 .133.05.16.08.08.212.21.265.184.157-.106.394-.21.447-.37.13-.315.184-.658.184-.974 0-.236.106-.394.21-.553.054-.08.08-.158.133-.263-.105-.08-.21-.132-.342-.237.106-.29.08-.633.475-.79.052-.027.052-.16.08-.238.025-.213.05-.45.078-.66.052.08.08.105.13.157a.42.42 0 0 1 .054-.08c0-.104-.026-.315 0-.315.316-.053.184-.395.342-.553.025-.028-.027-.107-.027-.16 0-.052 0-.13.026-.13.367-.08.315-.475.552-.66.08-.053.105-.13.21-.263.21.368-.158.553-.184.816.446-.263.578-.895.315-1.08.105-.08.21-.184.29-.29.29-.316.604-.606.868-.922.185-.236.29-.526.474-.763.106-.132.316-.237.474-.317.474-.262.92-.552 1.21-1 .053-.053.132-.105.21-.158.08-.053.238-.053.264-.132.027-.052-.052-.184-.105-.263.104-.053.21-.158.42-.264-.08.158-.105.264-.158.37l.13.13c.238-.184.606-.394.843-.552 0-.025-.132-.13-.132-.13-.157.08-.394.21-.63.316.05-.08.05-.132.08-.158.367-.237.735-.474 1.13-.66.92-.42 1.842-.842 2.763-1.237.158-.08.37-.026.553-.026.078 0 .13 0 .21-.026.42-.132.842-.264 1.263-.37.183-.052.393-.078.58-.078.787.025 1.577.025 2.366.078.342.026.658.105.974.21a9.88 9.88 0 0 1 1.184.5c.447.24.868.502 1.29.792.763.5 1.473 1.054 2.236 1.502.737.448 1.316 1.054 1.79 1.74.58.816 1.237 1.554 1.5 2.555l.394 1.74c.08.316.264.632.185 1-.133.66-.238 1.345-.343 2.004-.052.265-.105.53-.078.79.05.82-.265 1.53-.58 2.268-.106.237-.264.475-.395.738a.798.798 0 0 0 .21.106l.237-.474c.027 0 .027 0 .053.027-.132.368-.237.764-.37 1.133-.314.817-.63 1.66-1.025 2.45-.21.448-.58.817-.842 1.24-.262.368-.473.763-.736 1.106-.237.29-.473.58-.79.79-.71.527-1.447 1.054-2.21 1.476-.473.29-1.026.448-1.552.58zm-14.027-1.4l-.026.027c-.055-.026-.134-.052-.186-.105l-.632-.95c-.052-.078-.08-.157-.052-.262.29.448.58.87.895 1.29zm16.37 3.61c1.183-.5 2.157-1.21 3.05-2.028.133-.132.264-.263.422-.37 1.106-.684 1.92-1.633 2.658-2.687.842-1.212 1.395-2.582 2.08-3.873a2.73 2.73 0 0 1 .157-.29c-.053 3.004.29 5.955.684 8.933-2.973.105-6 .21-9.052.316zm26.683-.79c-.026.053-.08.106-.105.16-.027-.054-.027-.133-.053-.24-.158.423-.5.212-.737.212-1.42.027-2.868.027-4.29.027-1.368 0-2.762 0-4.13.024-.448 0-.922.105-1.37.132-1.078.052-2.157.08-3.236.105-.08 0-.158-.13-.29-.236a1.81 1.81 0 0 1-.158.237c-.028-.052-.08-.104-.133-.183-.026.08-.053.158-.08.21H58c-.053-.368-.158-.71-.158-1.08 0-.79.08-1.58.105-2.372.027-.368 0-.71 0-1.054.106.08.185.133.29.21.052-.103.105-.182.158-.26 0 0-.053-.028-.106-.08.05-.027.104-.08.104-.106.026-.08.08-.158.08-.21 0-.185-.054-.343-.08-.5.026 0 .052 0 .08-.028l.157.79h.08c-.106-.183.236-.342-.053-.552-.026-.027.026-.185.026-.264-.08-.157-.13-.315-.21-.526.026-.026.105-.053.184-.08-.105-.052-.184-.104-.263-.13.263-.238.263-.37.026-.633.054-.025.106-.025.106-.05 0-.238 0-.475-.052-.71-.053-.266.08-.58-.316-.74a.79.79 0 0 0 .105.21s-.08.027-.158.08c-.342-.317-.13-.74-.21-1.213.184.053.316.106.447.16-.053-.186-.184-.397-.263-.634h-.107v-1.74c0 .027.184.027.29.054 0-.027.025-.053.025-.08-.08-.105-.185-.21-.29-.342l.053-.053c-.21-.262-.105-.63-.105-.71V39.4c.264.264-.13.606.264.764v-.263h-.027c-.026-.395-.026-.79-.052-1.186h-.052c-.027.054-.027.08-.054.133h-.052l.158-6.298c.263.342.552.66.736 1 .606 1.108 1.395 2.057 2.132 3.058.632.87 1.21 1.818 1.79 2.714.71 1.08 1.394 2.16 2.105 3.24a81.41 81.41 0 0 0 1.63 2.426c.5.71 1.028 1.396 1.554 2.082.446.606.92 1.212 1.367 1.818.527.738 1.053 1.475 1.58 2.187.262.368.552.737.84 1.106.16.21.396.37.554.5-.025 0-.052 0-.104-.026.08.105.13.184.184.237.29.158.316.316.158.554zM74 46.854v-.185c0 .052.026.13 0 .184zm.895-11.62c-.027 0-.184-.16-.21-.186-.027.08 0 .158-.053.264-.027-.078-.21-.052-.21-.13-.027.368.157.737.13 1.106.08-.053.395-.08.474-.158.027.026.08.052.106.052-.527.396-.395.79-.158 1.24.052.104.21.315.052.526-.052.053.027.21.053.343h.077v.05l-.237.08c-.052-.08-.367-.236-.367-.37v1.346c.263.08.263.448.368.633a.768.768 0 0 0 .107-.21l.027.024c-.027.158-.053.316-.106.475-.052.236-.105.447-.13.684 0 .026.05.08.05.105-.288.66-.13 1.396-.235 2.08-.08.5 0 1.03-.053 1.556-.054.448-.16.922-.264 1.37-.027.08-.08.105-.21.158.052-.316.026-.527-.027-.817-.028 0-.37-.184-.397-.184 0 .37.21.87.29 1.29-.08-.026-.395-.21-.42-.21-.054.316-.054.738-.08 1.08-.027.264-.263.5-.29.79 0 .16.184.264.158.528h.21c0-.526.238-1 .238-1.554h.078c.027.053.106.106.08.132-.053.29-.16.606-.132.896 0 .158.13.316.08.5-.054.16-.08.317-.107.554-.027-.132-.053-.184-.053-.263-.026 0-.263-.027-.29-.027-.026.158.185.316.158.448-.026.026-.052.026-.105.053l-.868-1.266c-.686-1-1.37-2.003-2.054-3.03a6.312 6.312 0 0 1-.475-.79 37.09 37.09 0 0 0-2.71-4.033c-.762-.974-1.37-2.03-2.08-3.055-.656-.975-1.314-1.924-1.972-2.9-.237-.315-.526-.605-.737-.948-.683-1.08-1.29-2.187-1.972-3.267-.58-.897-1.21-1.767-1.816-2.636-.21-.29-.42-.607-.632-.923a.37.37 0 0 1-.052-.182c-.053-.58-.106-1.16-.132-1.713 0-.527.053-1.054.053-1.608v-.474c0-.132.025-.237.025-.37.025-.025.052-.078.078-.104-.763 0-1.553-.028-2.316 0-.5.025-.763-.186-1.105-.555-1-1.133-1.737-2.424-2.605-3.636a162.42 162.42 0 0 0-2.5-3.427c-.685-.922-1.37-1.818-2.053-2.74-.764-1.054-1.5-2.108-2.29-3.162a381.983 381.983 0 0 0-2.895-3.794c-.45-.58-.95-1.133-1.45-1.74.343.054.66.106.975.133l1.264.08c.947.077 1.894.13 2.84.26.79.107 1.58.265 2.396.396 1.738.29 3.448.765 5.106 1.318.974.316 1.92.738 2.87 1.133 2.13.87 4.157 1.924 6.157 3.03.63.343 1 .896 1.472 1.397.685.712 1.37 1.423 2.027 2.16.762.87 1.472 1.766 2.21 2.662.657.79 1.34 1.58 2 2.372.21.237.37.527.552.79.42.633.895 1.24 1.263 1.924.262.502.42 1.082.604 1.635.262.817.526 1.607.79 2.424.183.606.34 1.24.472 1.87.106.423.08.87.21 1.29.16.556 0 1.16.16 1.715.025.053.05.132.078.185.105.104.184.21.026.368-.025.026-.025.13 0 .21.054-.052.08-.105.133-.184 0 .053.025.08.025.105 0 .104-.027.21 0 .315 0 .052.052.13.078.184.053-.054.105-.08.21-.16.237.897.264 1.793.264 2.715 0 .87.157 1.74-.21 2.583.078-.29-.106-.555-.027-.818z"/><path d="M58.08 45.482c.025 0 .052.027.052.027l-.027-.03c0-.025 0-.025-.026 0zm4.157 26.036c-.29.21-.58.395-.948.474-.028-.026-.028-.053-.054-.08.29-.184.605-.368.895-.553.027.05.08.104.106.157zM12.895 35.81c.29-.367.58-.736.894-1.105.025.026.235.08.262.105-.29.37-.685.87-.974 1.265-.054-.053-.133-.237-.185-.264zM5.42 48.725c-.21-.448-.42-.923-.63-1.37a.91.91 0 0 1 .236-.106c.29.42.42.92.632 1.37 0 0-.21.105-.237.105zm6.712-12.65c-.158.238-.316.502-.474.74-.026-.028-.316.104-.342.078.158-.237.552-.66.71-.896.027.026.053.053.106.08zM59.422 72.6c.025 0 .025-.026.052-.026.184.026.394.052.605.052-.344.237-.555.21-.66-.026zm-47.24-35.418c.028-.08.08-.158.133-.237.052 0 .13-.027.13-.027.107-.184.107-.316.212-.474-.026-.026-.053-.026-.08-.053-.157.108-.315.24-.473.345.053.052.053.08.053.132-.21-.027-.29.08-.395.368-.026.08-.158.106-.29.21-.026.054-.052.186-.105.317l.027.028c-.053.053-.132.08-.132.08-.158.157-.342.29-.5.447-.026.08-.052.158-.052.237.185-.184.5-.527.737-.738l.027.027c.105-.158.184-.316.29-.474.025.026.025.052.052.08-.08.21-.158.446-.237.657-.055.026-.134.08-.134.053-.105.08-.184.184-.29.263l-.473.316c-.263.237-.526.447-.816.685-.184.29-.368.553-.58.896.317-.08.396.053.37.317.368.052.395-.237.5-.448.026-.054.053-.16.105-.186.237-.21.5-.394.763-.605.053-.053.053-.16.053-.238 0-.026-.133-.026-.212-.053.237-.264.58-.71.816-1 .132-.08.263-.186.263-.265-.026-.29.158-.368.37-.474-.106-.08-.133-.157-.133-.183z"/><path d="M12.71 36.892c-.105.184-.21.342-.315.527l-.158-.08c-.105.605-.474 1.132-.842 1.237.105.053.21.106.29.08.078-.027.13-.16.183-.238l.71-1.028.238-.396-.105-.105zM3.948 48.46c.132 0 .264.026.42.026 0-.105.133-.08.133-.184h.08c0 .132.026.237.026.37h-.552c-.027-.027-.132-.186-.106-.212zm-.21-1.212c-.08-.08-.21-.158-.21-.237-.027-.104.052-.235.13-.367.054.184.08.342.132.527-.027.025-.053.052-.053.078zm.658-1.687c.105.266.21.556.316.82a.798.798 0 0 0-.21.105c-.105-.264-.237-.554-.342-.817a.652.652 0 0 1 .237-.106zm58.58 25.194c.13-.052.288-.08.5-.13-.238.183-.422.315-.58.473-.027-.026-.053-.053-.08-.053.053-.105.106-.184.16-.29zM30.63 15.074c.157-.106.29-.185.447-.29l.052.052c-.16.21-.29.42-.475.685-.026-.183-.026-.29-.053-.42-.026 0 0 0 .027-.026zm7.71 13.333c.237-.106.474-.21.763-.343-.026.158-.026.264-.026.37a.927.927 0 0 0-.264-.054c-.158.027-.448.238-.58.264-.025 0 .106-.21.106-.237zm19.74 22.346c.052.263.552.395.052.658.08.055.157.08.236.134a.2.2 0 0 1-.052.106c-.053.025-.158.078-.21.05-.027 0-.08-.104-.08-.157 0-.237.027-.474.053-.79z"/></g></symbol><symbol viewBox="0 0 24 24" id="powerpoint" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M8 11v2h1v6H8v1h4v-1h-1v-2h2a3 3 0 0 0 3-3 3 3 0 0 0-3-3H8m5 2a1 1 0 0 1 1 1 1 1 0 0 1-1 1h-2v-2h2z" fill="#d14524"/></symbol><symbol viewBox="0 0 67.47 70" id="powershell" xmlns="http://www.w3.org/2000/svg"><path d="M18.545 12.4c-3.014 0-6.08 2.34-6.873 5.248L1.91 53.438c-.793 2.908.996 5.248 4.01 5.248h42.887c3.014 0 6.08-2.34 6.873-5.248l9.761-35.79c.794-2.908-.993-5.248-4.007-5.248h-42.89zm4.848 6.243c.652.04 1.29.33 1.76.86l7.96 9.013-3.957 3.246 3.957-3.244 4.832 5.47c.037.042.06.088.094.131.026.034.057.06.082.096.02.028.032.057.05.086.057.087.105.176.15.267.028.06.055.117.08.178a2.546 2.546 0 0 1 .171.764c.005.073.01.146.008.219-.002.09-.01.178-.021.267a2.53 2.53 0 0 1-.036.217 2.56 2.56 0 0 1-.07.252c-.024.076-.048.15-.08.224a2.547 2.547 0 0 1-.111.22 2.503 2.503 0 0 1-.133.218 2.546 2.546 0 0 1-.147.187c-.058.07-.118.137-.185.202-.027.026-.048.057-.076.082-.037.032-.077.054-.116.084-.038.03-.07.065-.11.093L16.8 52.271a2.552 2.552 0 0 1-3.563-.626 2.553 2.553 0 0 1 .63-3.563l18.349-12.853-3.06-3.467-7.839-8.873a2.549 2.549 0 0 1 .225-3.608 2.546 2.546 0 0 1 1.85-.638zm22.441 28.214c1.377 0 2.255 1.083 1.969 2.43-.287 1.347-1.627 2.433-3.004 2.434l-9.957.006c-1.378 0-2.256-1.083-1.969-2.43.287-1.347 1.626-2.433 3.004-2.434l9.957-.006z" fill="#03a9f4" stroke-width="5.342" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 210 210" id="prettier" xmlns="http://www.w3.org/2000/svg"><title>prettier-icon-dark</title><g transform="matrix(.9 0 0 .9 10.5 10.5)" fill="none" fill-rule="evenodd"><rect fill="#56B3B4" x="165" y="40" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="200" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="135" y="120" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="75" y="120" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="120" width="50" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="160" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="80" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="65" y="20" width="110" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="20" width="40" height="10" rx="5"/><rect fill="#F7BA3E" x="55" y="180" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="55" y="60" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="180" width="30" height="10" rx="5"/><rect fill="#F7BA3E" x="15" y="60" width="30" height="10" rx="5"/><rect fill="#56B3B4" x="95" y="100" width="90" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="100" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="100" width="20" height="10" rx="5"/><rect fill="#BF85BF" x="105" y="40" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="40" width="80" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="140" width="100" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="140" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="135" y="60" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="135" y="80" width="60" height="10" rx="5"/><rect fill="#56B3B4" x="15" width="130" height="10" rx="5"/></g></symbol><symbol viewBox="0 0 80 80" id="protractor" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="hxa"><path transform="scale(1 -1)" fill="#564b55" stroke-width="27.224" d="M-2.983-69.251h69.412v67.108H-2.983z"/></clipPath></defs><g transform="matrix(1.13039 0 0 -1.13039 5.714 82.137)" clip-path="url(#hxa)"><g transform="scale(.1)"><path d="M1180.54 92.324c-5.53 0-9.93-1.797-13.23-5.39-3.29-3.614-5.22-8.594-5.81-14.97h36.02c0 6.583-1.47 11.622-4.4 15.126-2.93 3.496-7.12 5.234-12.58 5.234zm2.84-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.88 6.21-8.83 14.824-8.83 25.84 0 11.101 2.73 19.922 8.21 26.464 5.45 6.524 12.81 9.805 22.02 9.805 8.63 0 15.46-2.851 20.48-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.45c.21-8.086 2.26-14.22 6.12-18.418 3.89-4.18 9.34-6.29 16.38-6.29 7.42 0 14.76 1.563 22 4.669V34.14c-3.68-1.602-7.18-2.746-10.48-3.438-3.28-.684-7.24-1.035-11.89-1.035M1272.34 30.918v44.57c0 5.606-1.28 9.805-3.82 12.559-2.56 2.773-6.56 4.16-12.02 4.16-7.2 0-12.49-1.953-15.84-5.851-3.34-3.895-5.03-10.32-5.03-19.286V30.918h-10.42v68.887h8.47l1.71-9.422h.5c2.14 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.15 2.804 12.88 2.804 8.29 0 14.54-2.011 18.73-6.015 4.19-3.985 6.28-10.391 6.28-19.192V30.918h-10.43M1328.96 38.406c7.1 0 12.27 1.938 15.48 5.813 3.22 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.44 6.25-15.56 6.25-6.11 0-10.79-2.383-14.04-7.129-3.26-4.746-4.88-11.472-4.88-20.136 0-8.797 1.61-15.45 4.84-19.93 3.23-4.484 7.97-6.723 14.22-6.723zm20.85 1.762h-.56c-4.83-7.004-12.02-10.5-21.62-10.5-9.01 0-16.03 3.066-21.04 9.238-5 6.153-7.5 14.922-7.5 26.27 0 11.355 2.51 20.176 7.54 26.465 5.03 6.289 12.03 9.433 21 9.433 9.34 0 16.5-3.398 21.49-10.195h.81l-.43 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.38 9.25M1434.91 38.27c1.85 0 3.63.136 5.34.421 1.72.274 3.09.547 4.1.84v-7.976c-1.15-.559-2.81-.996-5.01-1.36-2.18-.351-4.17-.527-5.94-.527-13.32 0-19.97 7.012-19.97 21.055V91.71h-9.88v5.027l9.88 4.336 4.38 14.707h6.04V99.805h20V91.71h-20V51.16c0-4.15.98-7.333 2.96-9.56 1.97-2.206 4.67-3.331 8.1-3.331M1463.81 65.43c0-8.809 1.76-15.508 5.27-20.118 3.53-4.609 8.69-6.906 15.53-6.906s12.01 2.297 15.56 6.875c3.53 4.602 5.3 11.301 5.3 20.149 0 8.75-1.77 15.41-5.3 19.953-3.55 4.539-8.77 6.824-15.69 6.824-6.82 0-11.99-2.246-15.47-6.73-3.46-4.48-5.2-11.16-5.2-20.047zm52.47 0c0-11.23-2.83-20-8.48-26.309-5.66-6.309-13.47-9.453-23.44-9.453-6.17 0-11.64 1.445-16.42 4.336-4.78 2.89-8.46 7.031-11.06 12.45-2.59 5.401-3.88 11.73-3.88 18.976 0 11.23 2.8 19.968 8.41 26.242 5.61 6.258 13.4 9.402 23.38 9.402 9.64 0 17.3-3.222 22.97-9.62 5.69-6.415 8.52-15.087 8.52-26.024M1591.71 92.324c-5.54 0-9.94-1.797-13.23-5.39-3.3-3.614-5.24-8.594-5.81-14.97h36c0 6.583-1.46 11.622-4.39 15.126-2.93 3.496-7.13 5.234-12.57 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.89 6.21-8.83 14.824-8.83 25.84 0 11.101 2.74 19.922 8.2 26.464 5.46 6.524 12.81 9.805 22.04 9.805 8.62 0 15.45-2.851 20.48-8.523 5.03-5.676 7.54-13.157 7.54-22.461v-6.613h-47.45c.21-8.086 2.25-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.36-6.29 7.43 0 14.77 1.563 22.01 4.669V34.14c-3.69-1.602-7.17-2.746-10.46-3.438-3.3-.684-7.27-1.035-11.91-1.035M1683.5 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12.01 4.16-7.2 0-12.48-1.953-15.83-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M1740.11 38.406c7.12 0 12.28 1.938 15.49 5.813 3.21 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.43 6.25-15.56 6.25-6.12 0-10.8-2.383-14.05-7.129-3.24-4.746-4.88-11.472-4.88-20.136 0-8.797 1.64-15.45 4.85-19.93 3.22-4.484 7.96-6.723 14.21-6.723zm20.87 1.762h-.57c-4.82-7.004-12.03-10.5-21.62-10.5-9.01 0-16.02 3.066-21.03 9.238-5 6.153-7.52 14.922-7.52 26.27 0 11.355 2.52 20.176 7.55 26.465 5.02 6.289 12.02 9.433 21 9.433 9.34 0 16.5-3.398 21.48-10.195h.83l-.44 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.37 9.25M1846.07 38.27c1.85 0 3.64.136 5.36.421 1.7.274 3.07.547 4.08.84v-7.976c-1.13-.559-2.8-.996-5-1.36-2.2-.351-4.18-.527-5.94-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.86v5.027l9.86 4.336 4.4 14.707h6.04V99.805H1855V91.71h-19.98V51.16c0-4.15.98-7.333 2.95-9.56 1.97-2.206 4.68-3.331 8.1-3.331M1894.26 92.324c-5.53 0-9.94-1.797-13.22-5.39-3.31-3.614-5.25-8.594-5.83-14.97h36.01c0 6.583-1.45 11.622-4.38 15.126-2.95 3.496-7.13 5.234-12.58 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.1 9.297-5.9 6.21-8.84 14.824-8.84 25.84 0 11.101 2.73 19.922 8.2 26.464 5.47 6.524 12.81 9.805 22.03 9.805 8.63 0 15.46-2.851 20.49-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.46c.22-8.086 2.26-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.37-6.29 7.42 0 14.75 1.563 22 4.669V34.14c-3.7-1.602-7.17-2.746-10.47-3.438-3.28-.684-7.25-1.035-11.9-1.035M1983.36 49.727c0-6.426-2.4-11.368-7.18-14.844-4.77-3.477-11.47-5.215-20.11-5.215-9.13 0-16.26 1.445-21.37 4.336v9.687a51.32 51.32 0 0 1 10.65-3.964c3.79-.977 7.45-1.457 10.97-1.457 5.46 0 9.64.87 12.57 2.609 2.95 1.738 4.41 4.394 4.41 7.95 0 2.694-1.17 4.98-3.5 6.894-2.32 1.914-6.85 4.152-13.6 6.757-6.41 2.383-10.97 4.473-13.67 6.25-2.71 1.778-4.72 3.81-6.04 6.067-1.31 2.254-1.98 4.96-1.98 8.113 0 5.606 2.29 10.04 6.86 13.281 4.57 3.25 10.84 4.883 18.79 4.883 7.42 0 14.66-1.515 21.74-4.531l-3.71-8.496c-6.9 2.851-13.17 4.277-18.79 4.277-4.94 0-8.67-.77-11.18-2.324-2.52-1.543-3.78-3.691-3.78-6.406 0-1.844.48-3.418 1.42-4.707.95-1.309 2.46-2.54 4.56-3.711 2.09-1.184 6.11-2.871 12.07-5.086 8.16-2.98 13.69-5.98 16.55-8.996 2.87-3.02 4.32-6.809 4.32-11.367M2021.28 38.27c1.85 0 3.64.136 5.35.421 1.71.274 3.09.547 4.09.84v-7.976c-1.14-.559-2.81-.996-5.01-1.36-2.18-.351-4.18-.527-5.93-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.87v5.027l9.87 4.336 4.4 14.707h6.02V99.805h20V91.71h-20V51.16c0-4.15 1-7.333 2.97-9.56 1.98-2.206 4.67-3.331 8.1-3.331M2053.61 30.918h-10.42v68.887h10.42zm-11.31 87.559c0 2.39.59 4.14 1.76 5.253 1.18 1.106 2.65 1.661 4.42 1.661 1.67 0 3.1-.567 4.32-1.7 1.22-1.132 1.82-2.871 1.82-5.214 0-2.344-.6-4.09-1.82-5.247-1.22-1.16-2.65-1.726-4.32-1.726-1.77 0-3.24.566-4.42 1.726-1.17 1.157-1.76 2.903-1.76 5.247M2121.59 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.49l1.69-9.422h.5c2.15 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.16 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2159.29 77.742c0-4.812 1.35-8.465 4.08-10.926 2.72-2.48 6.51-3.71 11.37-3.71 10.19 0 15.28 4.953 15.28 14.831 0 10.344-5.16 15.532-15.47 15.532-4.9 0-8.67-1.32-11.31-3.965-2.63-2.649-3.95-6.555-3.95-11.762zm-5.67-58.387c0-3.73 1.58-6.55 4.72-8.488 3.14-1.922 7.65-2.879 13.52-2.879 8.75 0 15.24 1.309 19.45 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.15 6.32-3.45 7.754-2.31 1.457-6.65 2.168-13.01 2.168h-12.51c-4.74 0-8.43-1.12-11.06-3.386-2.65-2.266-3.97-5.508-3.97-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.23-3.39 3.15-5.754.91-2.371 1.37-5.039 1.37-8.02 0-6.746-2.29-12.128-6.91-16.152-4.61-4.012-10.93-6.023-18.98-6.023-2.05 0-3.98.156-5.78.5-4.45-2.356-6.67-5.305-6.67-8.871 0-1.883.77-3.282 2.34-4.176 1.54-.902 4.21-1.36 7.97-1.36h12.2c7.46 0 13.19-1.574 17.19-4.707 4-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2192.38 2.004 2183.46 0 2171.72 0c-9 0-15.95 1.68-20.82 5.027-4.88 3.352-7.34 8.079-7.34 14.211 0 4.18 1.35 7.813 4.03 10.88 2.68 3.046 6.45 5.116 11.32 6.21-1.77.8-3.24 2.031-4.44 3.711-1.19 1.68-1.78 3.633-1.78 5.84 0 2.52.66 4.707 2.01 6.602 1.34 1.882 3.44 3.71 6.34 5.468-3.56 1.465-6.46 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.26 13.37 6.79 17.452 4.52 4.082 10.93 6.133 19.22 6.133 3.6 0 6.86-.429 9.75-1.27h23.82M2284.61 91.71h-17.54V30.919h-10.43v60.793h-12.31v4.707l12.31 3.766v3.839c0 16.922 7.4 25.391 22.19 25.391 3.65 0 7.93-.73 12.82-2.195l-2.7-8.364c-4.03 1.301-7.46 1.946-10.31 1.946-3.93 0-6.85-1.309-8.73-3.926-1.89-2.617-2.84-6.816-2.84-12.598v-4.472h17.54V91.71M2302.87 65.43c0-8.809 1.76-15.508 5.28-20.118 3.52-4.609 8.7-6.906 15.52-6.906 6.84 0 12.02 2.297 15.57 6.875 3.54 4.602 5.3 11.301 5.3 20.149 0 8.75-1.76 15.41-5.3 19.953-3.55 4.539-8.78 6.824-15.69 6.824-6.83 0-11.99-2.246-15.46-6.73-3.48-4.48-5.22-11.16-5.22-20.047zm52.48 0c0-11.23-2.82-20-8.47-26.309-5.67-6.309-13.48-9.453-23.46-9.453-6.15 0-11.62 1.445-16.4 4.336-4.77 2.89-8.47 7.031-11.06 12.45-2.59 5.401-3.9 11.73-3.9 18.976 0 11.23 2.81 19.968 8.43 26.242 5.6 6.258 13.4 9.402 23.38 9.402 9.63 0 17.28-3.222 22.97-9.62 5.68-6.415 8.51-15.087 8.51-26.024M2403.79 101.074c3.07 0 5.8-.254 8.22-.761l-1.43-9.676c-2.86.633-5.37.933-7.55.933-5.58 0-10.33-2.261-14.3-6.785-3.95-4.531-5.94-10.156-5.94-16.902V30.918h-10.43v68.887h8.62l1.19-12.754h.5c2.56 4.48 5.63 7.949 9.23 10.37 3.61 2.423 7.56 3.653 11.89 3.653M2500.33 69.766l-10.68 28.476c-1.39 3.594-2.81 8.028-4.28 13.262-.93-4.024-2.24-8.438-3.96-13.262l-10.81-28.476zm14.77-38.848l-11.44 29.227h-36.83l-11.32-29.227h-10.81l36.34 92.273h8.98l36.13-92.273h-11.05M2583.07 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2620.76 77.742c0-4.812 1.36-8.465 4.08-10.926 2.73-2.48 6.53-3.71 11.37-3.71 10.2 0 15.28 4.953 15.28 14.831 0 10.344-5.15 15.532-15.45 15.532-4.91 0-8.68-1.32-11.32-3.965-2.64-2.649-3.96-6.555-3.96-11.762zm-5.66-58.387c0-3.73 1.57-6.55 4.71-8.488 3.15-1.922 7.65-2.879 13.53-2.879 8.75 0 15.23 1.309 19.44 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.14 6.32-3.45 7.754-2.31 1.457-6.64 2.168-13 2.168h-12.51c-4.74 0-8.43-1.12-11.07-3.386-2.63-2.266-3.96-5.508-3.96-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.22-3.39 3.14-5.754.92-2.371 1.38-5.039 1.38-8.02 0-6.746-2.3-12.128-6.92-16.152-4.61-4.012-10.92-6.023-18.97-6.023-2.05 0-3.99.156-5.78.5-4.46-2.356-6.67-5.305-6.67-8.871 0-1.883.78-3.282 2.33-4.176 1.55-.902 4.21-1.36 7.98-1.36h12.2c7.46 0 13.18-1.574 17.18-4.707 4.01-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2653.87 2.004 2644.94 0 2633.2 0c-9 0-15.95 1.68-20.83 5.027-4.88 3.352-7.33 8.079-7.33 14.211 0 4.18 1.35 7.813 4.02 10.88 2.69 3.046 6.47 5.116 11.32 6.21-1.77.8-3.23 2.031-4.43 3.711-1.19 1.68-1.79 3.633-1.79 5.84 0 2.52.66 4.707 2.01 6.602 1.35 1.882 3.45 3.71 6.35 5.468-3.56 1.465-6.47 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.25 13.37 6.79 17.452 4.52 4.082 10.92 6.133 19.21 6.133 3.62 0 6.86-.429 9.75-1.27h23.83M2692.7 99.805V55.117c0-5.605 1.27-9.805 3.83-12.566 2.56-2.766 6.57-4.145 12.01-4.145 7.2 0 12.47 1.965 15.81 5.903 3.33 3.945 4.99 10.379 4.99 19.304v36.192h10.44V30.918h-8.62l-1.5 9.25h-.58c-2.13-3.41-5.1-5.988-8.88-7.793-3.8-1.809-8.13-2.707-12.99-2.707-8.37 0-14.65 1.992-18.81 5.977-4.18 3.964-6.26 10.351-6.26 19.101v45.059h10.56M2760.61 30.918h10.43v97.805h-10.43zM2810.67 38.27c6.5 0 11.6 1.789 15.31 5.343 3.71 3.575 5.56 8.555 5.56 14.961v6.23l-10.44-.448c-8.3-.286-14.27-1.583-17.94-3.868-3.66-2.273-5.5-5.82-5.5-10.644 0-3.781 1.14-6.64 3.42-8.613 2.29-1.973 5.48-2.961 9.59-2.961zm23.57-7.352l-2.07 9.805h-.51c-3.44-4.305-6.86-7.227-10.27-8.77-3.42-1.523-7.68-2.285-12.8-2.285-6.83 0-12.17 1.758-16.05 5.273-3.87 3.528-5.81 8.536-5.81 15.032 0 13.906 11.12 21.199 33.37 21.875l11.7.359v4.277c0 5.418-1.17 9.395-3.5 11.985-2.32 2.566-6.03 3.855-11.15 3.855-5.74 0-12.24-1.758-19.49-5.273l-3.21 7.988c3.4 1.836 7.11 3.281 11.16 4.324a47.81 47.81 0 0 0 12.16 1.575c8.23 0 14.3-1.817 18.27-5.461 3.96-3.66 5.93-9.5 5.93-17.54V30.919h-7.73M2893.6 101.074c3.07 0 5.8-.254 8.25-.761l-1.46-9.676c-2.84.633-5.35.933-7.54.933-5.56 0-10.33-2.261-14.3-6.785-3.96-4.531-5.93-10.156-5.93-16.902V30.918h-10.44v68.887h8.61l1.19-12.754h.5c2.57 4.48 5.65 7.949 9.25 10.37 3.6 2.423 7.56 3.653 11.87 3.653M2901.63 6.727c-3.94 0-7.04.558-9.31 1.691v9.121c2.97-.84 6.08-1.25 9.31-1.25 4.14 0 7.3 1.25 9.45 3.77 2.16 2.507 3.24 6.132 3.24 10.859v91.895h10.69V31.797c0-7.95-2.01-14.121-6.04-18.496-4.02-4.383-9.8-6.574-17.34-6.574M2999.96 55.371c0-8.086-2.93-14.394-8.8-18.918-5.87-4.52-13.83-6.785-23.88-6.785-10.9 0-19.27 1.406-25.14 4.219v10.3c3.77-1.59 7.88-2.847 12.31-3.765 4.45-.93 8.85-1.399 13.21-1.399 7.12 0 12.49 1.36 16.09 4.063 3.59 2.695 5.4 6.465 5.4 11.277 0 3.196-.63 5.805-1.91 7.832-1.29 2.024-3.42 3.907-6.42 5.625-2.99 1.711-7.56 3.664-13.67 5.84-8.55 3.059-14.66 6.692-18.32 10.871-3.66 4.2-5.51 9.668-5.51 16.407 0 7.089 2.68 12.714 7.99 16.914 5.32 4.191 12.36 6.289 21.12 6.289 9.13 0 17.54-1.68 25.2-5.032l-3.32-9.304c-7.59 3.183-14.96 4.785-22.13 4.785-5.66 0-10.07-1.223-13.26-3.652-3.19-2.43-4.78-5.809-4.78-10.118 0-3.191.59-5.8 1.76-7.832 1.17-2.031 3.14-3.886 5.95-5.597 2.78-1.688 7.04-3.563 12.79-5.625 9.63-3.426 16.26-7.118 19.89-11.063 3.62-3.937 5.43-9.043 5.43-15.332M741.648 375.406h30c28.965 0 50.227 5.039 63.774 15.117 13.531 10.079 20.32 25.821 20.32 47.247 0 19.832-6.074 34.628-18.191 44.402-12.141 9.758-31.028 14.641-56.692 14.641h-39.211zm172.192 64.246c0-36.062-11.809-63.691-35.434-82.898-23.621-19.219-57.234-28.82-100.847-28.82h-35.911V198.73h-56.445v345.329h99.438c43.14 0 75.457-8.829 96.961-26.465 21.496-17.637 32.238-43.614 32.238-77.942M1099.26 464.691c11.17 0 20.39-.789 27.63-2.371l-5.43-51.718c-7.88 1.894-16.07 2.832-24.57 2.832-22.2 0-40.19-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.514v261.227h43.464l7.32-46.055h2.83c8.66 15.594 19.96 27.95 33.9 37.09 13.93 9.141 28.93 13.699 45 13.699M1206.88 329.82c0-60.308 22.28-90.465 66.85-90.465 44.08 0 66.13 30.157 66.13 90.465 0 59.688-22.21 89.512-66.61 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.95-75.972-32.83-99.898-21.89-23.945-52.35-35.918-91.41-35.918-24.41 0-45.97 5.508-64.7 16.543-18.75 11.016-33.16 26.836-43.23 47.48-10.08 20.625-15.11 44.551-15.11 71.793 0 42.364 10.86 75.43 32.58 99.2 21.73 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.05-24.328 33.06-57.121 33.06-98.379M1558.11 238.887c13.54 0 27.07 2.129 40.62 6.386v-41.816c-6.13-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.59 0-78.88 27.715-78.88 83.144v140.778h-35.68v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.48-9.57 26.34-9.57M1783.44 464.691c11.17 0 20.38-.789 27.62-2.371l-5.43-51.718c-7.88 1.894-16.06 2.832-24.56 2.832-22.2 0-40.2-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.52v261.227h43.46l7.34-46.055h2.82c8.66 15.594 19.95 27.95 33.9 37.09 13.92 9.141 28.93 13.699 45 13.699M1925.05 236.523c20.15 0 36.32 5.625 48.52 16.895 12.21 11.25 18.31 27.051 18.31 47.344v22.676l-33.54-1.407c-26.13-.937-45.16-5.312-57.04-13.105-11.89-7.793-17.82-19.727-17.82-35.781 0-11.661 3.45-20.665 10.39-27.051 6.91-6.387 17.32-9.571 31.18-9.571zm82.66-37.793l-11.11 36.387h-1.87c-12.62-15.918-25.29-26.738-38.04-32.48-12.74-5.742-29.13-8.633-49.13-8.633-25.67 0-45.7 6.934-60.1 20.801-14.41 13.847-21.62 33.457-21.62 58.808 0 26.934 10 47.246 30 60.934 19.99 13.691 50.45 21.172 91.41 22.441l45.09 1.414v13.938c0 16.699-3.88 29.16-11.68 37.441-7.79 8.262-19.88 12.383-36.25 12.383-13.39 0-26.23-1.953-38.5-5.891a294.638 294.638 0 0 1-35.44-13.933l-17.94 39.668c14.17 7.41 29.68 13.035 46.52 16.894 16.85 3.868 32.77 5.789 47.72 5.789 33.22 0 58.31-7.246 75.22-21.726 16.94-14.492 25.4-37.246 25.4-68.262V198.73h-39.68M2220.04 194.004c-39.52 0-69.55 11.543-90.1 34.609-20.55 23.067-30.82 56.172-30.82 99.321 0 43.925 10.74 77.707 32.23 101.339 21.5 23.614 52.56 35.418 93.18 35.418 27.56 0 52.35-5.117 74.41-15.359l-16.78-44.641c-23.46 9.133-42.82 13.704-58.1 13.704-45.19 0-67.79-29.993-67.79-89.981 0-29.293 5.63-51.305 16.89-66.031 11.26-14.707 27.76-22.09 49.48-22.09 24.72 0 48.11 6.152 70.15 18.437v-48.417c-9.92-5.84-20.5-10-31.76-12.52-11.26-2.52-24.93-3.789-40.99-3.789M2451.52 238.887c13.54 0 27.08 2.129 40.63 6.386v-41.816c-6.15-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.6 0-78.9 27.715-78.9 83.144v140.778h-35.66v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.47-9.57 26.33-9.57M2585.92 329.82c0-60.308 22.28-90.465 66.84-90.465 44.09 0 66.15 30.157 66.15 90.465 0 59.688-22.22 89.512-66.62 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.94-75.972-32.83-99.898-21.89-23.945-52.36-35.918-91.4-35.918-24.42 0-45.98 5.508-64.72 16.543-18.74 11.016-33.14 26.836-43.22 47.48-10.07 20.625-15.12 44.551-15.12 71.793 0 42.364 10.87 75.43 32.59 99.2 21.74 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.04-24.328 33.06-57.121 33.06-98.379M2972.33 464.691c11.18 0 20.38-.789 27.63-2.371l-5.43-51.718c-7.87 1.894-16.05 2.832-24.57 2.832-22.2 0-40.19-7.246-53.96-21.731-13.78-14.48-20.67-33.301-20.67-56.453V198.73h-55.51v261.227h43.46l7.33-46.055h2.83c8.66 15.594 19.96 27.95 33.89 37.09 13.94 9.141 28.94 13.699 45 13.699" fill="#100f0d"/><path d="M610.11 372.83c0-170.584-138.257-308.862-308.846-308.862-170.602 0-308.846 138.278-308.846 308.863 0 170.576 138.244 308.846 308.846 308.846 170.59 0 308.846-138.27 308.846-308.846" fill="#e53935" stroke-width="1.029"/><path d="M460.694 521.792l-105.04.958-61.415 61.415-72.096-47.883 12.445-12.438-29.207.26-99.129-166.817H67.357l24.39-24.402-24.57-41.363L294.66 64.049c2.192-.04 4.399-.08 6.603-.08 170.416 0 308.585 138.055 308.846 308.408L460.694 521.792" fill="#d51c2f" stroke-width="1.029"/><path d="M149.093 350.258c0 84.048 68.13 152.151 152.171 152.151 84.028 0 152.139-68.103 152.139-152.151zm342.063-7.017v14.046h44.015c-1.75 59.337-25.556 113.104-63.54 153.419L438.75 477.81l-9.925 9.94 32.875 32.887c-40.314 37.983-94.081 61.79-153.41 63.527l-.015-44.003h-14.035v44.003c-59.34-1.737-113.096-25.556-153.41-63.527l32.887-32.887-9.945-9.92-32.883 32.875c-37.975-40.315-61.781-94.082-63.53-153.419h44.002l-.008-14.034H67.176v-51.511h468.176v51.5h-44.196" fill="#f5f5f5" stroke-width="1.029"/></g></g></symbol><symbol id="pug" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#c1272d}.hyst1{fill:#efcca3}.st2{fill:#ed1c24}.hyst3{fill:#ccac8d}.hyst4{fill:#fff}.st5{fill:#ff931e}.st6{fill:#ffb81e}.hyst7{fill:#56332b}.hyst8{fill:#442823}.hyst9{fill:#7f4a41}.hyst10{fill:#331712}.st11{fill:#fc6}.st12{fill:#ccc}.st13{fill:#b3b3b3}.st14{fill:#989898}.st15{fill:#323232}.st16{fill:#1e1e1e}.st17{fill:#4c4c4c}.st18{fill:#e6e6e6}.st19{fill:#606060}</style><path class="hyst1" d="M107.4 50.9c-.2-4.4.4-8.3-1.6-11.6-4.8-8.2-16.8-13-40.8-13v.7h-.5.5v-.7c-24 0-36.6 4.8-41.4 13.1-1.9 3.4-1.7 7.2-2 11.6-.2 3.5-1.8 7.2-1.1 11.2.8 5.2 1.1 10.4 1.9 15.2.6 3.9 6 7.2 6.5 10.9 1.4 10.2 12 14.9 36 14.9v.8h-.6.7v-.8c24 0 34.2-4.7 35.5-14.9.5-3.8 5.5-7 6.1-10.9.8-4.8 1.1-10 1.9-15.2.7-4-.9-7.8-1.1-11.3z"/><path class="hyst3" d="M64.6 54.5c4.3.1 7.3 2.8 10.1 5.3 3.3 2.9 8.9 4.9 11.2 7.4 2.3 2.5 5.3 5 6.4 8.9 1.1 3.9 1.4 8.9 1.4 10.2 0 1.3.7 1 2.7 0 4.7-2.3 9.9-8.5 9.9-8.5-.6 3.9-5.7 7.4-6.2 11.1C98.9 99.1 89 104 64.5 104h-.1.6"/><path class="hyst3" d="M80.4 46.7c.9 3.1 4.1 13.6-2.1 10.1 0 0 2.6 1.5 4.2 7.2 1.7 5.7 5.8 6.4 5.8 6.4s6.7 1.3 11.7-3c4.2-3.6 4.9-10 3.1-14.9-1.8-4.8-5-6.3-9.7-7.3-4.7-1.1-14.1-2-13 1.5z"/><circle cx="92.3" cy="58.1" r="8.8"/><circle class="hyst4" cx="90" cy="54.2" r="2.3"/><path class="hyst1" d="M78.9 57.7s7.9 5.4 12.2 10.7c4.3 5.3 4.2 6.3 4.2 6.3l-3.1 1.4s-4.4-8.3-9.8-11.4c-5.5-3.1-6.1-5.7-6.1-5.7l2.6-1.3z"/><path class="hyst3" d="M64.9 54.5c-4.3.1-7.5 2.8-10.4 5.3-3.3 2.9-9.1 4.9-11.4 7.4-2.3 2.5-5.4 5-6.5 8.9-1.1 3.9-1.5 8.9-1.5 10.2 0 1.3.2 1.4-2.7 0-4.7-2.2-9.9-8.5-9.9-8.5.6 3.9 5.7 7.4 6.2 11.1C30.1 99.1 40 104 64.5 104h.5"/><path class="hyst7" d="M88.1 71.4C83.3 65.5 75.6 60 64.9 60h-.1c-10.7 0-18.4 5.5-23.2 11.4-5 6.1-4.6 8.5-4.6 14.3 0 21 7.4 15 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.7 12.3-17.3.1-5.8.4-8.4-4.6-14.5z"/><path class="hyst8" d="M64.4 65.2s-.7 9.7-2.1 11.6l2.6-.6-.5-11z"/><path class="hyst8" d="M65.1 65.2s.7 9.7 2.1 11.6l-2.6-.6.5-11z"/><path class="hyst7" d="M56.7 62.9c-1-2.3 2.6-6 8.3-6.1 5.7 0 9.3 3.7 8.3 6.1-1 2.4-4.6 3.1-8.3 3.2-3.6-.1-7.3-.8-8.3-3.2z"/><path d="M65 65.2c0-.4 3.4-.5 5.2-1.7 0 0-3.7 1.2-4.5.7-.8-.4-1-1.6-1-1.6s-.3 1.2-.9 1.6c-.7.4-4.9-.7-4.9-.7s5.6 1.4 5.6 1.7c0 .3-.1 1.3-.1 2 0 2.5 0 8.7.4 9.2.6.9.4-6.7.4-9.2-.1-.8-.1-1.6-.2-2z"/><path class="hyst9" d="M65.2 78.6c1.7 0 4.7 1.2 7.4 3.1-2.6-2.9-5.7-4.9-7.4-4.9-1.8 0-5.6 2.2-8.3 5.4 2.8-2.2 6.4-3.6 8.3-3.6z"/><path class="hyst8" d="M64.5 96.3c-3.8 0-7.5-1.2-10.9-2.1-.7-.2-1.4.3-2.1.1-6.3-2-11.4-5.4-14.5-9.7v1c0 21 7.4 15.1 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.6 12.3-17.4 0-.8 0-1.6.1-2.3-2.9 4.7-8.2 8.4-14.8 10.6-.6.2-2-.3-2.6-.2-3.6 1.2-6.8 2.5-10.9 2.5z"/><path class="hyst8" d="M55 85s-2.5 7.5-.8 10.8l-2.3-1s1.7-7.6 3.1-9.8zM74.8 85s2.5 7.5.8 10.8l2.3-1s-1.8-7.6-3.1-9.8z"/><path class="hyst3" d="M48.6 46.7c-.9 3.1-4.1 13.6 2.1 10.1 0 0-2.6 1.5-4.2 7.2s-5.8 6.4-5.8 6.4-6.7 1.3-11.7-3c-4.2-3.6-4.9-10-3.1-14.9s5-6.3 9.7-7.3c4.7-1.1 14-2 13 1.5z"/><path d="M64.9 76.8c2.7 0 11.1 5.8 11.2 12.9v-.4c0-7.4-6.8-13.3-11.2-13.3-4.4 0-11.2 6-11.2 13.3v.4c.1-7.1 8.5-12.9 11.2-12.9z"/><ellipse transform="rotate(-14.465 66.712 61.468)" class="hyst10" cx="66.7" cy="61.5" rx=".8" ry="1.5"/><ellipse transform="rotate(17.235 62.371 61.462)" class="hyst10" cx="62.4" cy="61.5" rx=".8" ry="1.5"/><circle cx="37.2" cy="58.1" r="8.8"/><circle class="hyst4" cx="39.5" cy="54.2" r="2.3"/><path class="hyst9" d="M67.5 58.2c0-.1-2.3 1-2.9 1.1-.6-.1-2.9-1.2-2.9-1.1h5.8z"/><path class="hyst1" d="M50 57.7s-7.9 5.4-12.2 10.7c-4.3 5.3-4.2 6.3-4.2 6.3l3.1 1.4s4.4-8.3 9.8-11.4 6.1-5.7 6.1-5.7L50 57.7z"/><path class="hyst3" d="M32.7 41.7S30 49.1 24 52.2c0 0 9.4-1.1 8.7-10.5zM95.8 41.7s2.7 7.4 8.7 10.5c0 0-9.4-1.1-8.7-10.5zM78.7 55.5s-5.9-6.2-13.8-6.4h.1.1c-8 .2-13.8 6.4-13.8 6.4 6.9-4.8 12.8-4.7 13.8-4.7-.1 0 6.7-.1 13.6 4.7zM71.8 42.5s-3-4.2-7-4.3h.2c-3 .1-6.9 4.3-6.9 4.3 3.4-3.3 6.9-3.2 6.9-3.2s3.3-.1 6.8 3.2zM37.2 73.2s-4.7 2.3-8.1.9H29c-3-1.7-4.5-6.8-4.5-6.8s3 9 12.7 5.9zM92 73.2s4.7 2.3 8.1.9c4-1.7 4.6-6.8 4.6-6.8s-3 9-12.7 5.9z"/><path class="hyst3" d="M42.6 41.2c2.6-.5 6.9-.6 10.3.5 4.3 1.5.8 7 1.7 7.3.9.3 2.1-3.8 10.1-3.4 8.1.4 9 4 10.1 3.4s-1.1-10 11-7.8c0 0-12.7-3.4-12.1 5.8 0 0-7.3-5.6-17.5-.6.1 0 2.7-8.6-13.6-5.2zM86.9 41.2c.2 0 .3.1.4.1.1 0-.1-.1-.4-.1zM86.9 41.2zM39.1 28.9S28.3 42.5 26.7 47.7c-1.6 5.3-2.8 27-4.2 30.1l-5-21.4 9.2-22.3 12.4-5.2zM89.9 28.9s10.8 13.6 12.4 18.8c1.6 5.3 2.8 27 4.2 30.1l5-21.4-9.2-22.3-12.4-5.2z"/><path class="hyst7" d="M89.4 28.9s11.6 9.7 15 20.9c3.4 11.2 2 24.8 4.6 26.5 3.7 2.4 7.9-11.9 9.3-13.4 2.2-2.4 9.5-8.5 10-9.6.5-1.1-14.8-17.8-21.5-21.1-8.1-3.8-18.1-4.1-17.4-3.3z"/><path class="hyst8" d="M99.3 34.9s13.7 17.5 13.5 39.3l5.5-11.2c-.1 0-4.9-14.3-19-28.1z"/><path class="hyst7" d="M39.1 28.9s-11.6 9.7-15 20.9-2 24.8-4.6 26.5c-3.7 2.4-7.9-11.9-9.3-13.4C8 60.5.7 54.4.2 53.3-.3 52.2 15 35.5 21.7 32.2c8.1-3.8 18.1-4.1 17.4-3.3z"/><path class="hyst8" d="M29.2 34.9S15.5 52.4 15.7 74.2L10.3 63s4.8-14.3 18.9-28.1z"/><path class="hyst3" d="M21.8 74.6s1 5.4 2.6 7.1.5-1.3.5-1.3-1.7-.9-1.4-7.8-1.7 2-1.7 2zM107.1 74.6s-1 5.4-2.6 7.1-.5-1.3-.5-1.3 1.7-.9 1.4-7.8 1.7 2 1.7 2z"/><g><circle class="hyst8" cx="54.5" cy="70.5" r=".8"/><circle class="hyst8" cx="49.9" cy="75.3" r=".8"/><circle class="hyst8" cx="48.4" cy="70.5" r=".8"/></g><g><circle class="hyst8" cx="74" cy="70.5" r=".8"/><circle class="hyst8" cx="78.6" cy="75.3" r=".8"/><circle class="hyst8" cx="80.1" cy="70.5" r=".8"/></g></symbol><symbol viewBox="0 0 50 50" id="puppet" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" fill="#fbc02d"><path stroke-width=".283" d="M11.559 249.467h13.587v13.587H11.559zM27.435 265.056h13.587v13.587H27.435zM11.559 281.074h13.587v13.587H11.559z"/><path stroke-width=".256" d="M16.62 251.615l18.305 18.305-3.236 3.236-18.305-18.305z"/><path stroke-width=".256" d="M37.834 271.331L19.53 289.636l-3.237-3.237 18.305-18.304z"/></g></symbol><symbol viewBox="0 0 100 99.999997" id="purescript" xmlns="http://www.w3.org/2000/svg"><path clip-path="url(#SVGID_2_)" d="M98.079 38.548L79.22 19.68l-5.087 5.088L90.447 41.09 74.134 57.41l5.087 5.087 18.858-18.86a3.59 3.59 0 0 0 1.055-2.55 3.578 3.578 0 0 0-1.055-2.54M25.483 42.794l-5.09-5.089L1.53 56.568a3.566 3.566 0 0 0-1.05 2.545c0 .961.373 1.863 1.05 2.542L20.394 80.52l5.089-5.086L9.162 59.113z" fill="#42a5f5" stroke-width="1.192"/><path clip-path="url(#SVGID_2_)" transform="matrix(1.19175 0 0 1.19175 -306.84 -629.047)" fill="#42a5f5" d="M281.841 551.736l6.461 6.037h28.379l-6.461-6.037zM288.302 566.861l-6.463 6.035h28.381l6.463-6.035zM281.838 581.982l6.464 6.035h28.381l-6.463-6.035z"/></symbol><symbol viewBox="0 0 24 24" id="python" xmlns="http://www.w3.org/2000/svg"><path d="M19.14 7.5A2.86 2.86 0 0 1 22 10.36v3.78A2.86 2.86 0 0 1 19.14 17H12c0 .39.32.96.71.96H17v1.68a2.86 2.86 0 0 1-2.86 2.86H9.86A2.86 2.86 0 0 1 7 19.64v-3.75a2.85 2.85 0 0 1 2.86-2.85h5.25a2.85 2.85 0 0 0 2.85-2.86V7.5h1.18m-4.28 11.79c-.4 0-.72.3-.72.89 0 .59.32.71.72.71a.71.71 0 0 0 .71-.71c0-.59-.32-.89-.71-.89m-10-1.79A2.86 2.86 0 0 1 2 14.64v-3.78A2.86 2.86 0 0 1 4.86 8H12c0-.39-.32-.96-.71-.96H7V5.36A2.86 2.86 0 0 1 9.86 2.5h4.28A2.86 2.86 0 0 1 17 5.36v3.75a2.85 2.85 0 0 1-2.86 2.85H8.89a2.85 2.85 0 0 0-2.85 2.86v2.68H4.86M9.14 5.71c.4 0 .72-.3.72-.89 0-.59-.32-.71-.72-.71-.39 0-.71.12-.71.71s.32.89.71.89z"/><path d="M9.264 22.379c-.895-.24-1.581-.799-1.947-1.582-.228-.489-.237-.606-.238-2.957-.001-2.745.057-3.074.666-3.785.193-.226.568-.517.833-.648.47-.23.579-.239 3.839-.288 3.131-.048 3.386-.065 3.814-.264.626-.291 1.07-.687 1.4-1.247.27-.46.278-.522.311-2.29l.034-1.82.932.051c1.075.058 1.504.211 2.098.748.853.77.869.841.869 3.957 0 2.434-.02 2.783-.18 3.075a3.365 3.365 0 0 1-1.337 1.33l-.517.273-3.95.031-3.951.031.068.274c.037.151.164.377.282.503.209.224.262.229 2.433.229h2.22v1.05c0 1.653-.394 2.437-1.54 3.072l-.545.302-2.644.018c-1.455.01-2.782-.018-2.95-.063zm6.12-1.692c.22-.222.253-.325.206-.675-.07-.523-.278-.73-.732-.73-.467 0-.672.217-.735.78-.042.372-.012.496.163.672.3.3.77.28 1.097-.047z" fill="#fc0" stroke="#fc0" stroke-width=".102"/><path d="M9.349 22.38c-.911-.15-1.936-1.074-2.176-1.963-.073-.273-.101-1.279-.079-2.868.033-2.317.047-2.473.27-2.926.13-.263.401-.623.603-.8.674-.592.87-.63 3.484-.675 4.399-.076 4.927-.166 5.705-.967.642-.662.706-.9.774-2.883l.061-1.784.951.055c.523.031 1.11.122 1.304.204.54.225 1.358 1.042 1.472 1.47.153.572.243 3.18.16 4.617-.071 1.23-.093 1.327-.395 1.78-.193.288-.577.647-.966.903l-.647.425-3.922.008c-2.157.004-3.942.028-3.966.052-.115.115.354.82.587.883.14.038 1.181.073 2.314.079l2.06.01v.91c0 1.739-.326 2.446-1.454 3.162l-.631.4-2.543-.011c-1.398-.007-2.733-.043-2.966-.081zm5.98-1.718c.285-.256.313-.328.251-.658-.09-.483-.301-.682-.722-.682-.436 0-.625.193-.715.73-.065.384-.044.453.2.663.358.308.595.295.985-.053z" fill="#fdd835" stroke-width=".102"/><path d="M4.281 17.396c-.88-.215-1.714-.935-2.024-1.747-.149-.389-.168-.804-.142-3.041.027-2.26.054-2.638.215-2.962.259-.519.851-1.092 1.392-1.346.437-.206.632-.217 4.408-.245l3.95-.03-.067-.275a1.367 1.367 0 0 0-.282-.504c-.21-.224-.263-.23-2.433-.23h-2.22l.002-1.143c.003-1.338.157-1.795.84-2.493.746-.763 1.103-.838 4.025-.838 2.961 0 3.28.06 4.067.768.37.333.572.621.728 1.037.201.539.213.735.183 3.072-.035 2.777-.045 2.824-.78 3.598-.787.829-.76.824-4.59.883-3.812.06-3.797.057-4.61.806-.765.706-.917 1.2-.964 3.133l-.04 1.653-.677-.01c-.371-.007-.813-.045-.98-.086zM9.59 5.551c.237-.204.286-.326.286-.72 0-.547-.201-.763-.71-.763-.502 0-.765.248-.765.724 0 .492.141.782.439.902.345.14.444.12.75-.143z" fill="#3c78aa"/></symbol><symbol viewBox="0 0 24 24" id="r" xmlns="http://www.w3.org/2000/svg"><path d="M11.956 4.05c-5.694 0-10.354 3.106-10.354 6.947 0 3.396 3.686 6.212 8.531 6.813v2.205h3.53V17.82c.88-.093 1.699-.259 2.475-.497l1.43 2.692h3.996l-2.402-4.048c1.936-1.263 3.147-3.034 3.147-4.97 0-3.841-4.659-6.947-10.354-6.947m1.584 2.712c4.349 0 7.558 1.45 7.558 4.753 0 1.77-.952 3.013-2.505 3.779a1.081 1.081 0 0 1-.228-.156c-.373-.165-.994-.352-.994-.352s3.085-.227 3.085-3.302-3.23-3.127-3.23-3.127h-7.092v7.413c-2.64-.766-4.462-2.392-4.462-4.255 0-2.63 3.52-4.753 7.868-4.753m.156 4.12h2.143s.983-.05.983.974c0 1.004-.983 1.004-.983 1.004h-2.143v-1.977m-.031 4.566h.952c.186 0 .28.052.445.207.135.103.28.3.404.476-.57.073-1.17.104-1.801.104z" fill="#1976d2" stroke-width="1.035"/></symbol><symbol viewBox="0 0 24 24" id="raml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="razor" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 11.91c-.11-2.21-1.75-3.54-3.73-3.54h-.08c-2.29 0-3.55 1.8-3.55 3.84 0 2.29 1.53 3.74 3.54 3.74 2.25 0 3.72-1.65 3.83-3.59m-3.81-5.97c1.53 0 2.97.68 4.02 1.74 0-.51.33-.89.83-.89h.11c.74 0 .89.7.89.92v7.9c-.04.52.54.78.87.44 1.27-1.29 2.78-6.69-.79-9.81-3.33-2.92-7.8-2.44-10.18-.8-2.52 1.74-4.14 5.61-2.57 9.22 1.71 3.95 6.61 5.13 9.52 3.95 1.48-.59 2.15 1.4.65 2.05-2.34.99-8.77.89-11.78-4.32-2.03-3.52-1.93-9.71 3.46-12.92C10.81 1.42 16.24 2.1 19.5 5.5c3.45 3.6 3.25 10.3-.1 12.91-1.51 1.18-3.76.03-3.74-1.7l-.02-.56a5.611 5.611 0 0 1-3.99 1.66C8.63 17.81 6 15.15 6 12.13c0-3.05 2.63-5.74 5.65-5.74z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="react" xmlns="http://www.w3.org/2000/svg"><path d="M12 10.11c1.03 0 1.87.84 1.87 1.89 0 1-.84 1.85-1.87 1.85-1.03 0-1.87-.85-1.87-1.85 0-1.05.84-1.89 1.87-1.89M7.37 20c.63.38 2.01-.2 3.6-1.7-.52-.59-1.03-1.23-1.51-1.9a22.7 22.7 0 0 1-2.4-.36c-.51 2.14-.32 3.61.31 3.96m.71-5.74l-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16l-.3-.51m6.54-.76l.81-1.5-.81-1.5c-.3-.53-.62-1-.91-1.47C13.17 9 12.6 9 12 9c-.6 0-1.17 0-1.71.03-.29.47-.61.94-.91 1.47L8.57 12l.81 1.5c.3.53.62 1 .91 1.47.54.03 1.11.03 1.71.03.6 0 1.17 0 1.71-.03.29-.47.61-.94.91-1.47M12 6.78c-.19.22-.39.45-.59.72h1.18c-.2-.27-.4-.5-.59-.72m0 10.44c.19-.22.39-.45.59-.72h-1.18c.2.27.4.5.59.72M16.62 4c-.62-.38-2 .2-3.59 1.7.52.59 1.03 1.23 1.51 1.9.82.08 1.63.2 2.4.36.51-2.14.32-3.61-.32-3.96m-.7 5.74l.29.51c.11-.29.22-.58.29-.86-.27-.06-.57-.11-.88-.16l.3.51m1.45-7.05c1.47.84 1.63 3.05 1.01 5.63 2.54.75 4.37 1.99 4.37 3.68 0 1.69-1.83 2.93-4.37 3.68.62 2.58.46 4.79-1.01 5.63-1.46.84-3.45-.12-5.37-1.95-1.92 1.83-3.91 2.79-5.38 1.95-1.46-.84-1.62-3.05-1-5.63-2.54-.75-4.37-1.99-4.37-3.68 0-1.69 1.83-2.93 4.37-3.68-.62-2.58-.46-4.79 1-5.63 1.47-.84 3.46.12 5.38 1.95 1.92-1.83 3.91-2.79 5.37-1.95M17.08 12c.34.75.64 1.5.89 2.26 2.1-.63 3.28-1.53 3.28-2.26 0-.73-1.18-1.63-3.28-2.26-.25.76-.55 1.51-.89 2.26M6.92 12c-.34-.75-.64-1.5-.89-2.26-2.1.63-3.28 1.53-3.28 2.26 0 .73 1.18 1.63 3.28 2.26.25-.76.55-1.51.89-2.26m9 2.26l-.3.51c.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.29.51m-2.89 4.04c1.59 1.5 2.97 2.08 3.59 1.7.64-.35.83-1.82.32-3.96-.77.16-1.58.28-2.4.36-.48.67-.99 1.31-1.51 1.9M8.08 9.74l.3-.51c-.31.05-.61.1-.88.16.07.28.18.57.29.86l.29-.51m2.89-4.04C9.38 4.2 8 3.62 7.37 4c-.63.35-.82 1.82-.31 3.96a22.7 22.7 0 0 1 2.4-.36c.48-.67.99-1.31 1.51-1.9z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="readme" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="reason" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm5.119 8.993h2.798c.382 0 .71.025.985.075.275.05.534.159.774.326.244.168.435.386.577.654.145.265.218.598.218 1 0 .552-.112 1.001-.335 1.35-.22.348-.536.638-.947.87l2.16 3.203H12.31l-1.763-2.742h-.77v2.742H8.12v-7.478zm6.594 0h4.676v1.447h-3.018v1.29h2.802v1.447h-2.802v1.848h3.018v1.446h-4.676v-7.478zM9.778 13.37v2.014h.513c.266 0 .49-.014.67-.044.18-.03.329-.1.45-.207a.96.96 0 0 0 .253-.34c.055-.128.082-.297.082-.508 0-.187-.034-.35-.1-.483a.698.698 0 0 0-.343-.317 1.086 1.086 0 0 0-.395-.095 6.012 6.012 0 0 0-.526-.02h-.604z" fill="#f44336" stroke-width="1.067"/></symbol><symbol viewBox="0 0 172 193" id="restql" xmlns="http://www.w3.org/2000/svg"><title>Group</title><g transform="translate(14.767 16.713) scale(.82795)" fill="none"><path d="M171.39 55.799c-.975-6.147-4.673-11.642-10.15-14.805L96.381 3.546C93.217 1.72 89.615.756 85.964.756s-7.253.964-10.415 2.788L10.69 40.992A20.896 20.896 0 0 0 .272 59.035v74.89a20.894 20.894 0 0 0 10.416 18.042l64.859 37.446c3.165 1.827 6.767 2.791 10.417 2.791s7.252-.964 10.415-2.79l64.859-37.445c5.479-3.166 9.178-8.66 10.152-14.808zm-16.516 85.147L90.017 178.39a8.104 8.104 0 0 1-8.108 0l-64.857-37.444a8.109 8.109 0 0 1-4.053-7.021v-74.89a8.109 8.109 0 0 1 4.053-7.021l64.857-37.446c1.254-.725 2.654-1.086 4.054-1.086s2.8.361 4.054 1.086l64.857 37.446a8.106 8.106 0 0 1 4.053 7.021v74.89a8.109 8.109 0 0 1-4.053 7.021z" fill="#83e8c2"/><path d="M158.93 59.035a8.109 8.109 0 0 0-4.053-7.021L90.02 14.568c-1.254-.725-2.654-1.086-4.054-1.086s-2.8.361-4.054 1.086L17.055 52.014a8.106 8.106 0 0 0-4.053 7.021v74.89a8.109 8.109 0 0 0 4.053 7.021l64.857 37.444a8.104 8.104 0 0 0 8.108 0l64.857-37.444a8.109 8.109 0 0 0 4.053-7.021zm-46.766 31.681c.119-.069.242-.118.365-.149.044-.012.088-.01.131-.018.076-.012.152-.029.228-.029l.015.001c.02.001.038.005.059.006.093.005.184.019.273.04l.1.03c.077.025.15.057.223.095.028.014.057.027.084.043.094.057.184.122.263.199.007.008.013.017.021.024.07.071.133.15.188.235.018.029.033.059.05.09.04.072.072.148.099.229a1.512 1.512 0 0 1 .081.46v16.209l-3.278 1.893a1.548 1.548 0 0 0-.678.83 1.533 1.533 0 0 0-.098.514v3.785l-14.038 8.104-.01.004a1.55 1.55 0 0 1-.354.146c-.045.012-.09.011-.135.018-.074.012-.15.029-.225.029l-.014-.001c-.02-.001-.039-.005-.059-.006a1.463 1.463 0 0 1-.273-.041c-.034-.008-.066-.019-.1-.03a1.318 1.318 0 0 1-.223-.094c-.029-.015-.057-.027-.084-.044a1.45 1.45 0 0 1-.263-.198c-.009-.008-.015-.019-.023-.027a1.495 1.495 0 0 1-.185-.232c-.019-.029-.034-.06-.051-.09a1.422 1.422 0 0 1-.098-.229 1.702 1.702 0 0 1-.033-.101 1.487 1.487 0 0 1-.048-.358l-.001-.002v-20.053a1.446 1.446 0 0 1 .727-1.255zM85.24 31.369a1.449 1.449 0 0 1 1.452 0l45.741 26.41a1.45 1.45 0 0 1 0 2.512l-17.366 10.027a1.457 1.457 0 0 1-1.452 0l-15.49-8.943 1.727-.996a1.552 1.552 0 0 0 0-2.688l-13.111-7.57c-.239-.139-.508-.207-.775-.207s-.535.068-.775.207l-3.278 1.893-14.038-8.104a1.451 1.451 0 0 1 0-2.513zM57.59 47.558c.251 0 .501.065.726.194l15.489 8.942-1.727.997a1.552 1.552 0 0 0 0 2.688l1.727.996-15.488 8.943a1.457 1.457 0 0 1-1.452 0L39.499 60.291a1.45 1.45 0 0 1 0-2.512l17.366-10.027c.225-.129.475-.194.725-.194zm-9.56 92.328c-.241 0-.489-.062-.724-.196l-17.365-10.026a1.45 1.45 0 0 1-.726-1.256V75.59c0-.847.694-1.453 1.452-1.453.242 0 .49.062.724.197l17.366 10.025c.449.26.726.738.726 1.257v17.886l-1.727-.997a1.552 1.552 0 0 0-2.327 1.344v15.139c0 .555.295 1.067.775 1.344l3.278 1.894v16.209a1.45 1.45 0 0 1-1.452 1.451zm29.828 14.929a1.452 1.452 0 0 1-2.177 1.257l-17.365-10.026a1.452 1.452 0 0 1-.726-1.257v-17.885l1.726.996c.25.145.515.211.773.211.811 0 1.554-.648 1.554-1.555v-1.993l15.489 8.942c.449.26.726.738.726 1.257zm0-32.768c0 .127-.02.246-.049.36-.009.035-.021.067-.032.101-.026.08-.059.157-.099.229-.017.03-.032.061-.05.09a1.48 1.48 0 0 1-.188.235l-.021.025a1.51 1.51 0 0 1-.264.199c-.026.016-.055.028-.082.043a1.597 1.597 0 0 1-.324.124 1.362 1.362 0 0 1-.278.041c-.018.001-.036.006-.055.006l-.015.001c-.077 0-.155-.018-.233-.03-.043-.007-.084-.005-.125-.017a1.484 1.484 0 0 1-.366-.149l-14.035-8.104v-3.784a1.545 1.545 0 0 0-.776-1.343l-3.276-1.892V91.976c0-.127.02-.246.049-.361.009-.034.021-.066.032-.1a1.33 1.33 0 0 1 .099-.229c.017-.03.032-.062.051-.091.054-.084.116-.163.187-.234l.021-.025c.079-.076.168-.142.263-.199.027-.016.056-.029.084-.043a1.476 1.476 0 0 1 .601-.166c.019 0 .036-.005.055-.005l.015-.001c.078 0 .157.018.236.03.04.007.081.005.122.017.124.031.246.08.366.149l17.361 10.023a1.456 1.456 0 0 1 .726 1.259zm-9.984-45.373a1.448 1.448 0 0 1-.544-.55 1.466 1.466 0 0 1 0-1.413c.121-.219.303-.41.544-.55l14.038-8.104 3.277 1.892c.48.276 1.071.276 1.551 0l3.278-1.893 14.038 8.105a1.45 1.45 0 0 1 0 2.513L86.691 86.7a1.447 1.447 0 0 1-1.452 0zm74.842 51.733c0 .518-.276.997-.726 1.256l-45.741 26.409a1.452 1.452 0 0 1-2.177-1.257v-20.053c0-.519.277-.997.727-1.257l15.488-8.941v1.992c0 .906.743 1.555 1.553 1.555.26 0 .523-.066.774-.21l13.11-7.57a1.55 1.55 0 0 0 .776-1.344v-3.784l14.038-8.105a1.452 1.452 0 0 1 2.177 1.257v20.052zm0-32.764c0 .519-.276.997-.726 1.256l-15.489 8.943v-1.993c0-.906-.744-1.554-1.554-1.554a1.519 1.519 0 0 0-.773.21l-1.727.996V85.616c0-.519.277-.997.727-1.257l17.365-10.025c.234-.135.482-.197.724-.197.758 0 1.453.606 1.453 1.453z" fill="#111d5a"/><g fill="#83e8c2"><path d="M59.402 90.568zM94.485 123.06zM94.771 123.29zM77.775 122.51zM77.072 123.33zM77.418 123.09zM77.856 122.05zM76.749 123.45zM94.119 122.41zM77.131 133.51l-15.489-8.942v1.993c0 .906-.743 1.555-1.554 1.555a1.53 1.53 0 0 1-.773-.211l-1.726-.996v17.885c0 .519.276.997.726 1.257l17.365 10.026a1.452 1.452 0 0 0 2.177-1.257v-20.053a1.454 1.454 0 0 0-.726-1.257zM94.25 122.74zM110.28 111.42zM94.494 100.98c.088-.089.189-.168.303-.232l17.365-10.026-17.365 10.026a1.392 1.392 0 0 0-.303.232zM77.627 122.83zM58.027 90.936zM58.374 90.693zM59.044 90.521l-.015.001c.083-.001.167.015.251.029-.079-.012-.158-.03-.236-.03zM57.819 91.195zM58.696 90.568zM57.589 91.977zM76.043 123.46zM57.67 91.516zM75.677 123.31l-14.035-8.11zM76.401 123.5l.015-.001c-.082.001-.166-.016-.248-.029.078.012.156.03.233.03zM112.16 90.716zM77.662 101.27zM113.64 90.734zM96.237 123.31zM113.33 90.597zM112.89 90.52c-.075 0-.151.018-.228.029.081-.014.162-.029.242-.028l-.014-.001zM141.26 74.137c-.241 0-.489.062-.724.197l-17.365 10.025c-.449.26-.727.738-.727 1.257v17.885l1.727-.996c.25-.145.515-.211.773-.21.81 0 1.554.647 1.554 1.554v1.993l15.489-8.943a1.45 1.45 0 0 0 .726-1.256V75.59c0-.847-.695-1.453-1.453-1.453zM112.96 90.526zM95.523 123.5c.074 0 .15-.018.225-.029-.08.013-.159.028-.238.028l.013.001zM95.451 123.5zM85.238 86.7zM95.078 123.43zM141.26 106.9c-.241 0-.489.062-.724.196l-14.038 8.105v3.784c0 .555-.296 1.067-.776 1.344l-13.11 7.57c-.251.144-.515.21-.774.21-.81 0-1.553-.648-1.553-1.555v-1.992l-15.488 8.941c-.449.26-.727.738-.727 1.257v20.053a1.452 1.452 0 0 0 2.177 1.257l45.741-26.409a1.45 1.45 0 0 0 .726-1.256v-20.053a1.454 1.454 0 0 0-1.454-1.452zM67.871 41.396a1.451 1.451 0 0 0 0 2.513l14.038 8.104 3.278-1.893c.24-.139.508-.207.775-.207s.536.068.775.207l13.111 7.57a1.552 1.552 0 0 1 0 2.688l-1.727.996 15.49 8.943a1.457 1.457 0 0 0 1.452 0l17.366-10.027a1.45 1.45 0 0 0 0-2.512l-45.741-26.41a1.449 1.449 0 0 0-1.452 0zM39.497 57.779a1.45 1.45 0 0 0 0 2.512l17.366 10.027a1.457 1.457 0 0 0 1.452 0l15.488-8.943-1.727-.996a1.552 1.552 0 0 1 0-2.688l1.727-.997-15.489-8.942a1.458 1.458 0 0 0-1.451 0zM49.481 138.43v-16.209l-3.278-1.894a1.55 1.55 0 0 1-.775-1.344v-15.139c0-.906.743-1.555 1.554-1.554.259 0 .523.065.773.21l1.727.997V85.611a1.45 1.45 0 0 0-.726-1.257L31.39 74.33a1.436 1.436 0 0 0-.724-.197c-.758 0-1.452.606-1.452 1.453v52.817c0 .518.276.997.726 1.256l17.365 10.026a1.45 1.45 0 0 0 2.176-1.255zM114.34 108.18l-3.278 1.893 3.278-1.893V91.971zM114.11 91.193zM114.16 91.283z"/></g><g fill="#de5941"><path d="M94.494 100.98a1.45 1.45 0 0 0-.424 1.023v20.053l.001.002c0 .126.02.244.048.358.01.034.021.066.033.101.026.08.059.156.098.229.017.03.032.061.051.09.055.084.115.162.185.232.009.009.015.02.023.027.079.077.169.142.263.198.027.017.055.029.084.044a1.46 1.46 0 0 0 .596.165c.02.001.039.005.059.006.079 0 .158-.016.238-.028.045-.007.09-.006.135-.018.119-.031.238-.08.354-.146l.01-.004 14.038-8.104v-3.785c0-.18.04-.35.098-.514.122-.343.353-.643.678-.83l3.278-1.893V91.977c0-.127-.021-.246-.049-.361-.009-.033-.021-.065-.032-.099a1.266 1.266 0 0 0-.099-.229c-.017-.031-.032-.061-.05-.09a1.425 1.425 0 0 0-.188-.235l-.021-.024a1.41 1.41 0 0 0-.263-.199c-.027-.016-.056-.029-.084-.043a1.509 1.509 0 0 0-.323-.125 1.591 1.591 0 0 0-.273-.04c-.021-.001-.039-.005-.059-.006-.08-.001-.161.015-.242.028-.043.008-.087.006-.131.018-.123.031-.246.08-.365.149l-17.365 10.026a1.447 1.447 0 0 0-.302.233zM77.13 100.74L59.769 90.717a1.424 1.424 0 0 0-.366-.149c-.041-.012-.082-.01-.122-.017-.084-.015-.168-.03-.251-.029-.019 0-.036.005-.055.005-.095.005-.188.02-.278.041-.034.009-.065.02-.099.03a1.406 1.406 0 0 0-.224.095c-.028.014-.057.027-.084.043a1.515 1.515 0 0 0-.263.199l-.021.025c-.07.071-.133.15-.187.234-.019.029-.034.061-.051.091-.04.073-.072.149-.099.229a1.463 1.463 0 0 0-.081.461v16.206l3.276 1.892a1.547 1.547 0 0 1 .776 1.343v3.784l14.035 8.104c.119.068.242.117.366.149.041.012.082.01.125.017.082.014.166.03.248.029.019 0 .037-.005.055-.006.095-.004.188-.019.278-.041.034-.008.065-.019.099-.029.077-.025.152-.058.225-.095.027-.015.056-.027.082-.043.095-.058.185-.123.264-.199l.021-.025c.07-.071.133-.15.188-.235.018-.029.033-.06.05-.09.04-.072.072-.149.099-.229a1.448 1.448 0 0 0 .081-.461v-20.047a1.456 1.456 0 0 0-.726-1.259zM86.689 86.7l17.365-10.026a1.45 1.45 0 0 0 0-2.513l-14.038-8.105-3.278 1.893a1.556 1.556 0 0 1-1.551 0l-3.277-1.892-14.038 8.104c-.241.14-.423.331-.544.55a1.466 1.466 0 0 0 0 1.413c.121.218.303.41.544.55L85.238 86.7a1.447 1.447 0 0 0 1.451 0z"/></g></g></symbol><symbol viewBox="0 0 24 24" id="riot" xmlns="http://www.w3.org/2000/svg"><defs><path d="M13.26 3.04l.58.05.54.07.52.09.49.11.46.13.44.14.41.16.39.17.36.19.33.21.32.22.29.23.26.25.22.22.2.22.19.24.17.24.15.25.15.26.12.27.12.28.1.29.08.31.07.31.05.32.04.34.02.35.01.37v.05l-.02.51-.05.49-.09.48-.13.45-.15.43-.19.4-.22.39-.26.37-.28.34-.31.33-.33.3-.37.28-.39.27-.41.24-.44.22L21 21h-7.04l-3.48-5.14H9.17V21H3V3h9.01l.64.01.61.03zm-4.09 8.52h2.66l.99-.11.75-.35.47-.55.16-.74v-.05l-.17-.75-.47-.54-.74-.32-.96-.11H9.17v3.52z" id="ija"/></defs><use xlink:href="#ija" fill="#ff1744"/><use xlink:href="#ija" fill-opacity="0" stroke="#000" stroke-opacity="0"/></symbol><symbol viewBox="0 0 24 24" id="robot" xmlns="http://www.w3.org/2000/svg"><path d="M12.05 2.804a1.787 1.787 0 0 1 1.788 1.788c0 .661-.357 1.242-.893 1.546v1.135h.893a6.256 6.256 0 0 1 6.256 6.256h.894a.894.894 0 0 1 .893.893v2.681a.894.894 0 0 1-.893.894h-.894v.894a1.787 1.787 0 0 1-1.787 1.787H5.795a1.787 1.787 0 0 1-1.787-1.787v-.894h-.894a.894.894 0 0 1-.894-.894v-2.68a.894.894 0 0 1 .894-.894h.894a6.256 6.256 0 0 1 6.255-6.256h.894V6.138a1.773 1.773 0 0 1-.894-1.546 1.787 1.787 0 0 1 1.788-1.788m-4.022 9.83a2.234 2.234 0 0 0-2.234 2.235 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.234-2.234 2.234 2.234 0 0 0-2.234-2.234m8.043 0a2.234 2.234 0 0 0-2.234 2.234 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.235-2.234 2.234 2.234 0 0 0-2.235-2.234z" fill="#ff5722" stroke-width=".894"/></symbol><symbol viewBox="100 100 800 800" id="rollup" xmlns="http://www.w3.org/2000/svg"><style>.ilst0{fill:url(#ilXMLID_4_)}.ilst1{fill:url(#ilXMLID_5_)}.ilst2{fill:url(#ilXMLID_8_)}.ilst3{fill:url(#ilXMLID_9_)}.ilst4{fill:url(#ilXMLID_11_)}.ilst5{opacity:.3;fill:url(#ilXMLID_16_)}</style><g id="ilXMLID_14_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_4_" x1="444.47" x2="598.47" y1="526.05" y2="562.05" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_15_" class="ilst0" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_4_)"/></g><g id="ilXMLID_2_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_5_" x1="420.38" x2="696.38" y1="475" y2="689" gradientUnits="userSpaceOnUse"><stop stop-color="#BF3338" offset="0"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_10_" class="ilst1" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_5_)"/></g><linearGradient id="ilXMLID_8_" x1="429.39" x2="469.39" y1="517.16" y2="559.16" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_3_" class="ilst2" d="M329.82 813.46c15.58-8.903 122.41-220.34 227.02-320.5s117.96-66.771 60.094-175.83c0 0-221.46 310.49-301.58 464.06" fill="url(#ilXMLID_8_)" stroke-width="1.113"/><g id="ilXMLID_7_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_9_" x1="502.11" x2="490.11" y1="589.46" y2="417.46" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_12_" class="ilst3" d="M373 537c134.4-247.1 152-272 222-272 36.8 0 73.9 16.6 97.9 46.1-32.7-52.7-90.6-88-156.9-89H307.7c-4.8 0-8.7 3.9-8.7 8.7V691c13.6-35.1 36.7-85.3 74-154z" fill="url(#ilXMLID_9_)"/></g><linearGradient id="ilXMLID_11_" x1="450.12" x2="506.94" y1="514.21" y2="552.85" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FBB040" offset="0"/><stop stop-color="#FB8840" offset="1"/></linearGradient><path id="ilXMLID_6_" class="ilst4" d="M556.84 492.96c-104.61 100.16-211.44 311.6-227.02 320.5s-41.732 10.016-55.643-5.564c-14.801-16.582-37.837-43.401 86.802-272.65 149.57-274.99 169.15-302.7 247.05-302.7 40.953 0 82.24 18.473 108.95 51.302 1.447 2.337 2.893 4.785 4.34 7.233-45.738-47.074-145.23-57.98-169.93-.222-25.373 59.204 42.622 125.08 72.335 119.85 37.837-6.677-6.677-93.48-6.677-93.48 57.757 108.95 44.403 75.563-60.205 175.72z" fill="url(#ilXMLID_11_)" stroke-width="1.113"/><linearGradient id="ilXMLID_16_" x1="508.33" x2="450.33" y1="295.76" y2="933.76" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFF" offset="0"/><stop stop-color="#FFF" stop-opacity="0" offset="1"/></linearGradient><path id="ilXMLID_13_" class="ilst5" d="M373.22 547.49c149.57-274.99 169.15-302.7 247.05-302.7 33.719 0 67.661 12.575 93.48 35.277-26.708-30.492-66.326-47.519-105.72-47.519-77.9 0-97.486 27.71-247.05 302.7-124.64 229.25-101.6 256.07-86.802 272.65 2.114 2.337 4.563 4.34 7.122 6.01-13.02-18.919-18.807-62.877 91.922-266.42z" fill="url(#ilXMLID_16_)" opacity=".3" stroke-width="1.113"/></symbol><symbol viewBox="0 0 24 24" id="ruby" xmlns="http://www.w3.org/2000/svg"><path d="M16 9h3l-5 7m-4-7h4l-2 8M5 9h3l2 7m5-12h2l2 3h-3m-5-3h2l1 3h-4M7 4h2L8 7H5m1-5L2 8l10 14L22 8l-4-6H6z" fill="#f44336"/></symbol><symbol viewBox="0 0 144 144" id="rust" xmlns="http://www.w3.org/2000/svg"><path d="M68.252 26.206a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0M25.766 58.451a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m84.97.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m-74.661 4.88a3.252 3.252 0 0 0 1.651-4.29l-1.58-3.574h6.214v28.01H29.823a43.847 43.847 0 0 1-1.42-16.738zm25.994.688v-8.256h14.798c.764 0 5.397.883 5.397 4.347 0 2.877-3.553 3.908-6.475 3.908zm-20.203 44.452a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m52.769.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m1.101-8.076a3.246 3.246 0 0 0-3.856 2.498l-1.787 8.342a43.847 43.847 0 0 1-36.566-.175l-1.787-8.342a3.246 3.246 0 0 0-3.854-2.497l-7.365 1.581a43.847 43.847 0 0 1-3.808-4.488h35.834c.406 0 .676-.074.676-.443V84.527c0-.369-.27-.442-.676-.442h-10.48V76.05h11.335c1.035 0 5.532.296 6.97 6.045.45 1.768 1.44 7.519 2.116 9.36.674 2.065 3.417 6.19 6.34 6.19h18.501a43.847 43.847 0 0 1-4.06 4.7zm19.898-33.468a43.847 43.847 0 0 1 .093 7.612h-4.499c-.45 0-.631.296-.631.737v2.066c0 4.863-2.742 5.92-5.145 6.19-2.288.258-4.825-.958-5.138-2.358-1.35-7.593-3.6-9.214-7.152-12.016 4.409-2.8 8.996-6.93 8.996-12.457 0-5.97-4.092-9.729-6.881-11.572-3.914-2.58-8.246-3.096-9.415-3.096H39.336A43.847 43.847 0 0 1 63.867 28.52l5.484 5.753a3.243 3.243 0 0 0 4.59.105l6.137-5.869a43.847 43.847 0 0 1 30.017 21.38l-4.201 9.487a3.256 3.256 0 0 0 1.652 4.29zm10.477.154l-.143-1.467 4.327-4.036c.88-.82.55-2.472-.574-2.891l-5.532-2.068-.433-1.428 3.45-4.792c.704-.974.058-2.53-1.127-2.724l-5.833-.949-.7-1.31 2.45-5.38c.502-1.095-.43-2.496-1.636-2.45l-5.92.206-.935-1.135 1.36-5.766c.275-1.17-.913-2.36-2.084-2.085l-5.765 1.359-1.136-.935.207-5.92c.046-1.198-1.357-2.135-2.45-1.637l-5.379 2.452-1.31-.703-.95-5.833c-.193-1.183-1.75-1.83-2.723-1.128l-4.796 3.45-1.425-.432-2.068-5.532c-.42-1.127-2.072-1.452-2.89-.576l-4.036 4.33-1.467-.143-3.117-5.036c-.63-1.02-2.318-1.02-2.946 0l-3.117 5.036-1.467.143-4.037-4.33c-.819-.876-2.47-.551-2.89.576l-2.069 5.532-1.426.432-4.795-3.45c-.974-.703-2.53-.055-2.723 1.128l-.951 5.833-1.31.703-5.379-2.452c-1.093-.5-2.496.439-2.45 1.637l.206 5.92-1.136.935-5.765-1.36c-1.171-.272-2.36.915-2.086 2.086l1.358 5.766-.933 1.135-5.92-.206c-1.193-.035-2.134 1.355-1.637 2.45l2.453 5.38-.703 1.31-5.832.949c-1.185.192-1.827 1.75-1.128 2.724l3.45 4.792-.433 1.428-5.532 2.068c-1.123.42-1.452 2.07-.574 2.891l4.328 4.036-.143 1.467-5.035 3.116c-1.02.63-1.02 2.318 0 2.946l5.035 3.117.143 1.467-4.328 4.037c-.878.818-.549 2.468.574 2.89l5.532 2.068.433 1.428-3.45 4.793c-.701.976-.056 2.532 1.129 2.723l5.831.948.703 1.312-2.453 5.378c-.5 1.093.444 2.5 1.638 2.451l5.917-.207.935 1.136-1.358 5.768c-.275 1.168.915 2.355 2.086 2.08l5.765-1.357 1.137.932-.207 5.921c-.046 1.199 1.357 2.136 2.45 1.636l5.379-2.45 1.31.702.95 5.83c.193 1.187 1.75 1.829 2.725 1.13l4.792-3.453 1.427.435 2.069 5.53c.42 1.123 2.072 1.454 2.89.574l4.037-4.328 1.467.146 3.117 5.035c.628 1.016 2.316 1.018 2.946 0l3.117-5.035 1.467-.146 4.036 4.328c.818.88 2.47.549 2.89-.574l2.068-5.53 1.428-.435 4.793 3.453c.974.699 2.53.055 2.722-1.13l.952-5.83 1.31-.703 5.378 2.451c1.093.5 2.493-.435 2.45-1.636l-.206-5.92 1.135-.933 5.765 1.357c1.171.275 2.36-.912 2.085-2.08l-1.358-5.768.932-1.136 5.92.207c1.194.048 2.138-1.358 1.636-2.451l-2.45-5.378.7-1.312 5.833-.948c1.187-.19 1.831-1.747 1.127-2.723l-3.45-4.793.433-1.428 5.532-2.068c1.125-.422 1.454-2.072.574-2.89l-4.327-4.037.143-1.467 5.035-3.117c1.02-.628 1.021-2.315.001-2.946z" fill="#ff7043" stroke-width="1.146"/></symbol><symbol viewBox="0 0 500 500" id="sass" xmlns="http://www.w3.org/2000/svg"><path d="M422.676 96.573c-12.192-47.839-91.508-63.557-166.575-36.892-44.68 15.877-93.029 40.786-127.81 73.311-41.349 38.675-47.943 72.328-45.216 86.395 9.583 49.622 77.585 82.069 105.535 106.126v.144c-8.246 4.05-68.565 34.584-82.684 65.799-14.893 32.932 2.372 56.556 13.804 59.742 35.424 9.859 71.764-7.866 91.311-37.01 18.853-28.12 17.28-64.422 9.086-82.487 11.3-2.976 24.476-4.314 41.218-2.36 47.248 5.52 56.517 35.017 54.747 47.366-1.77 12.35-11.681 19.14-14.998 21.186-3.317 2.045-4.326 2.766-4.05 4.287.405 2.215 1.94 2.137 4.758 1.652 3.894-.656 24.804-10.042 25.709-32.828 1.14-28.933-26.587-61.302-75.684-60.45-20.216.354-32.933 2.268-42.123 5.69-.681-.774-1.363-1.547-2.084-2.307-30.35-32.382-86.46-55.285-84.088-98.824.866-15.823 6.372-57.5 107.817-108.052 83.104-41.415 149.637-30.009 161.135-4.76 16.427 36.08-35.554 103.137-121.858 112.812-32.88 3.684-50.198-9.059-54.498-13.804-4.536-4.995-5.204-5.218-6.909-4.287-2.753 1.533-1.01 5.938 0 8.574 2.583 6.712 13.15 18.603 31.176 24.515 15.863 5.205 54.459 8.063 101.156-9.99 52.283-20.255 93.12-76.523 81.125-123.548zM200.213 340.34c3.92 14.5 3.487 28.016-.564 40.248a65.289 65.289 0 0 1-3.225 7.97c-3.12 6.477-7.316 12.534-12.442 18.132-15.653 17.069-37.507 23.532-46.88 18.092-10.122-5.874-5.048-29.944 13.083-49.11 19.52-20.636 47.602-33.903 47.602-33.903l-.039-.079 2.465-1.35z" fill="#ec407a" stroke="#ec407a" stroke-width="16.286552999999998"/></symbol><symbol viewBox="0 0 300 300" id="sbt" xmlns="http://www.w3.org/2000/svg"><path d="M105.46 209.517c-7.875 0-13.452-7.521-13.452-15.37v-.327c0-7.848 5.578-13.735 13.452-13.735h164.05c1.476-4.905 2.625-11.446 3.281-17.986h-137.81c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h137.31c-.82-6.54-1.969-13.081-3.773-17.986h-104.01c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h91.87c-21.327-37.607-60.864-61.315-106.14-61.315-67.918 0-123.04 54.448-123.04 122.3 0 67.856 55.122 123.28 123.04 123.28 46.59 0 87.112-25.507 107.95-63.114h-152.73z" fill="#0277bd" stroke-width="1.638"/></symbol><symbol viewBox="0 0 256 256" id="scala" xmlns="http://www.w3.org/2000/svg"><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M59.607 50.647l149.097-21.982v49.488L59.607 100.135zM59.593 114.08L208.69 92.098v49.488L59.593 163.568zM59.587 177.358l149.097-21.982v49.488L59.587 226.846z"/><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M62.425 91.414l95.605 30.923-2.832 8.757-95.605-30.922zM113.084 61.13l95.604 30.922-2.832 8.757-95.605-30.922zM62.425 154.79l95.605 30.922-2.833 8.758-95.604-30.923zM113.097 124.408l95.604 30.923-2.832 8.757-95.605-30.922z"/></symbol><symbol viewBox="0 0 24 24" id="settings" xmlns="http://www.w3.org/2000/svg"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="shaderlab" xmlns="http://www.w3.org/2000/svg"><path d="M9.11 17H6.5l-4.91-5L6.5 7h2.61l1.31-2.26L17.21 3l1.87 6.74L17.77 12l1.31 2.26L17.21 21l-6.79-1.74L9.11 17m.14-.25l5.13 1.38L11.42 13H5.5l3.75 3.75m6.87.38L17.5 12l-1.38-5.13L13.15 12l2.97 5.13M9.25 7.25L5.5 11h5.92l2.96-5.13-5.13 1.38z" fill="#1976d2"/></symbol><symbol viewBox="0 0 24 24" id="slim" xmlns="http://www.w3.org/2000/svg"><path d="M6.959 2.5a4.605 4.605 0 0 0-4.615 4.615v9.957a4.605 4.605 0 0 0 4.615 4.615h9.957a4.605 4.605 0 0 0 4.615-4.615V7.115A4.605 4.605 0 0 0 16.916 2.5zm4.938 2.691a6.811 6.811 0 0 1 6.81 6.813H13.43L9.938 7.287l.699 4.717H5.086a6.811 6.811 0 0 1 6.81-6.813z" fill="#f57f17"/></symbol><symbol id="smarty" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.iust0{fill:#ffce00}</style><path class="iust0" d="M9.14 20.606c0 .556.398.953.954.953h3.812c.556 0 .953-.397.953-.953v-.953H9.141zM12 2.5c-3.653 0-6.671 3.018-6.671 6.671 0 2.303 1.112 4.289 2.859 5.48v2.144c0 .556.397.953.953.953h5.718c.556 0 .953-.397.953-.953V14.65c1.747-1.191 2.86-3.177 2.86-5.48 0-3.653-3.019-6.671-6.672-6.671zm2.7 10.563l-.794.555v2.224h-3.812v-2.224l-.794-.555A4.712 4.712 0 0 1 7.235 9.17 4.78 4.78 0 0 1 12 4.405a4.78 4.78 0 0 1 4.765 4.765 4.712 4.712 0 0 1-2.065 3.892z"/></symbol><symbol viewBox="0 0 200 200" id="snyk" xmlns="http://www.w3.org/2000/svg"><title>Group 2</title><g transform="translate(15.255 18.22) scale(1.8477)" fill="none" fill-rule="evenodd"><path d="M65.161 24.997c-1.656 5.974-5.255 23.587-5.255 23.587s-6.618-2.464-14.148-2.476h-.055c-.413.002-.822.012-1.23.026v41.649h6.677v.003h5.815v-.003h20.858c.111-8.177-2.036-27.066-2.036-27.066-1.088-2.279.46-7.668.46-7.668-8.869-9.092-11.086-28.051-11.086-28.051zm-3.357 43.958c5.476 0 1.381 4.64.9 5.168H52.35c.944-1.18 4.504-5.168 9.453-5.168z" fill="#607d8b" stroke-width="1.6"/><path d="M26.366 24.995s-2.217 18.961-11.087 28.053c0 0 1.548 5.391.46 7.669 0 0-2.15 18.895-2.038 27.066h19.273v.003h7.079v-.003h5.744V46.107h-.025c-7.532.013-14.151 2.478-14.151 2.478s-3.6-17.615-5.255-23.59zm3.264 43.96c4.95 0 8.51 3.987 9.452 5.168H28.73c-.479-.528-4.573-5.168.9-5.168z" fill="#90a4ae" stroke-width="1.6"/><g transform="translate(23.76 77.45) scale(1.5998)"><g transform="translate(17.526)"><path d="M7.357.06H.177v.075C.177 2.64 2.345 4.67 4.89 4.67 7.431 4.67 9.6 2.64 9.6.135V.059z" fill="#455a64"/><path d="M1.972.06v.075a2.692 2.692 0 1 0 5.386 0V.059z" fill="#fff"/><path d="M5.496.06H4.234c-.012 0-.023.005-.034.007.157.033.243.388.21.624a.721.721 0 0 1-.71.617c.102.471.487.85.997.922a1.188 1.188 0 0 0 1.35-1.007C6.112.743 5.881.06 5.495.06z" fill="#37474f"/></g><path d="M7.552.06H.372v.075c0 2.505 2.17 4.535 4.712 4.535 2.544 0 4.712-2.03 4.712-4.535V.059z" fill="#455a64"/><path d="M2.168.06v.075a2.692 2.692 0 1 0 5.385 0V.059z" fill="#fff"/><path d="M5.692.06H4.428c-.01 0-.022.005-.032.007.156.033.242.388.21.624a.72.72 0 0 1-.712.617c.104.471.488.85.999.922A1.187 1.187 0 0 0 6.24 1.223C6.308.743 6.078.06 5.69.06z" fill="#37474f"/></g><path d="M25.514-.27l-4.202 7.697C19.838 10.17 6.858 34.465 6.858 43.243v.516L12.8 59.573c-.8 7.258-2.203 21.643-1.78 28.21h5.73c-.354-3.787.648-17.008 1.903-28.25l.076-.677-1.075-2.892c3.694-3.868 6.285-9.193 8.073-14.261l.174 1.235 5.869 9.629 2.291-.983c.058-.024 5.935-2.523 11.643-2.523 5.672 0 11.646 2.5 11.702 2.525l2.29.976 5.86-9.626.23-1.608c1.769 5.117 4.358 10.536 8.07 14.49l-1.127 3.035.076.678c1.259 11.286 2.266 24.564 1.916 28.252h5.677c.406-6.567-1.05-20.952-1.848-28.208l5.838-15.817v-.514c0-8.779-12.876-33.074-14.347-35.816L65.923-.27l-5.897 41.229-2.723 4.478c-2.628-.882-7.1-2.11-11.603-2.11-4.498 0-8.94 1.225-11.557 2.108l-2.722-4.476-2.07-14.452a.832.832 0 0 0 .006-.071l-.016-.004zm-3.166 18.39l1.206 8.407c-.46 3.143-2.561 15.47-8.198 23.24l-2.598-6.99c.325-4.554 5.067-15.462 9.59-24.656zm46.763 0c4.523 9.194 9.267 20.104 9.592 24.657L76.166 49.6c-6.09-8.553-8-22.459-8.166-23.73z" fill="#607d8b" stroke-width="1.6"/></g></symbol><symbol viewBox="0 0 24 24" id="solidity" xmlns="http://www.w3.org/2000/svg"><path d="M5.8 14.05l6.253 8.61 6.252-8.61-6.254 3.807z" fill="#0288d1" stroke-width="4.553" stroke-linejoin="round"/><path d="M12.051 1.347L5.8 11.833l6.252 3.807 6.254-3.807z" fill="#0288d1" stroke-width="5.025" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 120 120" id="sonar" xmlns="http://www.w3.org/2000/svg"><style>.a,.b{fill:#fff}.b{stroke:#fff;stroke-miterlimit:10}</style><path d="M115.45 23.033S97.961 33.27 97.534 33.412c-.427.284-.852.57-1.137.854-1.422 1.421-1.848 3.41-1.422 5.26.285.852.711 1.849 1.422 2.56.711.71 1.564 1.137 2.559 1.422 1.848.426 3.84 0 5.262-1.422.426-.427.709-.853.851-1.28l.143-.427 2.56-4.692zm-39.102 9.242c-27.441 0-31.99 13.08-31.99 29.29 0 3.838.569 7.962-1.99 11.942-3.84 5.972-8.957 5.828-10.236 5.828-1.706 0-7.962-.993-8.246-2.841h.994c6.682 0 11.658-5.404 11.658-12.655v-2.56h-5.686c-4.123 0-7.82 1.849-10.238 5.12-2.417-3.271-6.113-5.12-10.236-5.12h-5.83v2.56c0 7.11 5.688 12.795 12.797 12.795h1.848c0 4.124 5.687 20.332 47.63 20.332 16.352 0 40.665-2.843 40.665-33.697 0-5.829-1.848-11.23-4.691-15.78-.996.284-1.992.568-3.13.568a8.92 8.92 0 0 1-8.956-8.957c0-.995.141-1.991.425-2.986-4.265-2.702-8.53-3.838-14.787-3.838z" fill="#1e88e5" stroke-width="1.422"/></symbol><symbol viewBox="0 0 412 395" id="stylelint" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-white</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#cfd8dc" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 412 395" id="stylelint_light" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-black</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#546e7a" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 200.00001 200.00001" id="stylus" xmlns="http://www.w3.org/2000/svg"><path d="M126.814 155.9c14.64-17.51 16.362-35.595 5.024-69.18-7.177-21.24-19.09-37.602-10.334-50.807 9.329-14.065 29.135-.43 12.63 18.371l3.301 2.297c19.806 2.296 29.566-24.83 14.783-32.58C113.179 3.621 79.02 42.803 94.09 88.156c6.458 19.232 15.5 39.613 8.18 55.83-6.314 13.923-18.514 22.103-26.695 22.39-17.079.862-5.74-38.32 13.922-48.08 1.722-.861 4.162-2.01 1.866-4.88-24.256-2.727-38.464 8.468-46.645 24.112-23.825 45.497 45.21 62.29 82.095 18.371z" fill="#c0ca33" stroke-width="1.435"/></symbol><symbol viewBox="0 0 24 24" id="swc" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="jba"><stop offset="0" stop-color="#791223"/><stop offset="1" stop-color="#d92f3c"/></linearGradient><linearGradient xlink:href="#jba" id="jbb" x1="12.356" y1="21.559" x2="12.356" y2="2.949" gradientUnits="userSpaceOnUse"/></defs><path d="M6 3c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6 3 6.5V19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.5c0-.5-.17-.93-.46-1.27l-1.39-1.68C18.88 3.21 18.47 3 18 3H6zm-.07 1h12l.94 1H5.12l.81-1z" fill="url(#jbb)"/><path style="line-height:125%" d="M11.053 11.918h-.008c-.244.022-.475.054-.676.11a2.9 2.9 0 0 0-.856.412 3.399 3.399 0 0 0-.67.683 9.36 9.36 0 0 0-.586.95c-.07.131-.134.244-.201.365v.001h-.002l-.768 1.372-.003-.001c-.136.253-.264.485-.38.686-.123.212-.26.39-.411.539a1.599 1.599 0 0 1-.52.34c-.04.016-.092.024-.138.036h-.567v1.383H5.834v-.001c.245-.02.477-.053.679-.11a2.9 2.9 0 0 0 .856-.411c.245-.185.469-.413.67-.683.195-.275.39-.591.585-.95.07-.131.135-.244.202-.366l.004.001.002-.002.02-.038H10.948v-1.378h-.19v-.001H9.624c.125-.234.246-.452.355-.64.123-.21.259-.39.41-.538.152-.148.325-.26.52-.34.04-.015.091-.024.136-.035h.57V13.3h-.002v-1.381h-.56v-.001z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol viewBox="0 0 24 24" id="swift" xmlns="http://www.w3.org/2000/svg"><path d="M17.09 19.72c-2.36 1.36-5.59 1.5-8.86.1A13.807 13.807 0 0 1 2 14.5c.67.55 1.46 1 2.3 1.4 3.37 1.57 6.73 1.46 9.1 0-3.37-2.59-6.24-5.96-8.37-8.71-.45-.45-.78-1.01-1.12-1.51 8.28 6.05 7.92 7.59 2.41-1.01 4.89 4.94 9.43 7.74 9.43 7.74.16.09.25.16.36.22.1-.25.19-.51.26-.78.79-2.85-.11-6.12-2.08-8.81 4.55 2.75 7.25 7.91 6.12 12.24-.03.11-.06.22-.05.39 2.24 2.83 1.64 5.78 1.35 5.22-1.21-2.39-3.48-1.65-4.62-1.17z" fill="#fe5e2f"/></symbol><symbol viewBox="0 0 24 24" id="table" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5m4 7.5h-4v2h1l-2 1.67L10 13h1v-2H7v2h1l3 2.5L8 18H7v2h4v-2h-1l2-1.67L14 18h-1v2h4v-2h-1l-3-2.5 3-2.5h1v-2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 200 200" id="terraform" xmlns="http://www.w3.org/2000/svg"><g transform="translate(177.03 -58.705) scale(.92881)" fill="#5c6bc0" stroke="#b0aff5" stroke-linejoin="round"><g stroke-width=".288"><path transform="skewY(26.439) scale(.89541 1)" d="M-203.8 170.95h64.714v51.88H-203.8zM-124.37 171.04h64.714v51.88h-64.714zM-124.37 236.09h64.714v51.88h-64.714z"/></g><path transform="skewY(-22.59) scale(-.92328 1)" stroke-width=".284" d="M-19.172 128.27h62.76v51.88h-62.76z"/></g></symbol><symbol viewBox="0 0 24 24" id="test-js" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="test-jsx" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="test-ts" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tex" xmlns="http://www.w3.org/2000/svg"><g font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-linejoin="miter"><text style="line-height:125%" x="9.914" y="364.919"><tspan x="9.914" y="364.919" font-size="287.5">T</tspan></text><text style="line-height:125%" x="136.374" y="435.558"><tspan x="136.374" y="435.558" font-size="287.5">E</tspan></text><text style="line-height:125%" x="307.819" y="361.201"><tspan x="307.819" y="361.201" font-size="287.5">X</tspan></text></g></symbol><symbol viewBox="0 0 24 24" id="todo" xmlns="http://www.w3.org/2000/svg"><path d="M3 5h6v6H3V5m2 2v2h2V7H5m6 0h10v2H11V7m0 8h10v2H11v-2m-6 5l-3.5-3.5 1.41-1.41L5 17.17l4.59-4.58L11 14l-6 6z" fill="#42a5f5"/></symbol><symbol id="travis" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style id="jkstyle2">.jkst0{fill:#cb3349}.jkst1{fill:#f4edae}.jkst2{fill:#e6ccad}.jkst3{fill:#656c67}.jkst4{fill:#e5caa3}.jkst5{fill:#c7b39a}.jkst6{fill:#ebd599}.jkst7{fill:#2d3136}.jkst8{fill:#edf6fa}.jkst9{opacity:.8}.jkst10{opacity:.75;fill:#ebd599}</style><g id="jkg99" transform="translate(11.017 12.484) scale(.8858)"><g id="jkg10"><path class="jkst0" d="M47.781 86.572s-31.118 21.903-32.335 30.247l2.335-.48S55.045 91.64 84.584 88.628l.669-3.749z" id="jkpath4" fill="#cb3349"/><path class="jkst0" d="M96.629 83.442l-24.511 17.385 1.325 1.063c.999-.806 43.539-13.798 43.539-13.798l8.969-5.623c-6.018.749-29.322.973-29.322.973z" id="jkpath6" fill="#cb3349"/><path class="jkst0" d="M117.932 104.469c17.405 0 43.495-17.046 43.495-17.046l-8.434-1.605c-.417.417-13.6-.462-13.6-.462l-6.258-1.738-14.951 17.036-1.217 2.956c1.075-.437.965.859.965.859z" id="jkpath8" fill="#cb3349"/></g><path class="jkst0" d="M174.728 158.832l-5.377 1.514-24.843-.537-15.541-12.085-18.784 4.7-21.726-1.88-12.166 13.294-22.828 6.819-11.398-3.534-.574-.494 5.116 12.527s11.588 12.424 18.061 13.885c6.472 1.461 18.165-.105 26.935-1.463 8.769-1.357 15.764-4.489 18.582-9.603 2.818-5.117 3.236-6.578 3.236-6.578s8.353 11.797 15.556 13.155c7.203 1.357 28.605-5.952 28.605-5.952s13.051-3.549 15.346-8.038c2.297-4.489 8.353-19.209 8.353-19.209zM44.456 169.038l-.361-.166-2.013-1.736z" id="jkpath12" fill="#cb3349"/><g id="jkg97"><path class="jkst1" d="M195.832 70.085a48.125 48.125 0 0 0-.21-2.009 26.472 26.472 0 0 0-.215-1.424c-1.793-1.509-3.831-2.851-5.952-4.071-2.299-1.343-4.704-2.546-7.159-3.663-2.438-1.15-4.942-2.191-7.461-3.207a134.313 134.313 0 0 0-3.798-1.477c-1.269-.495-2.55-.956-3.835-1.424 2.697.447 5.366 1.059 8.015 1.741 1.723.446 3.437.945 5.14 1.477-12.112-31.655-41.07-52.27-72.687-52.27-31.622 0-60.577 20.615-72.686 52.27a109.044 109.044 0 0 1 5.137-1.477c2.653-.682 5.323-1.294 8.018-1.741-1.289.468-2.567.929-3.84 1.424-1.267.472-2.536.967-3.798 1.477-2.519 1.016-5.016 2.057-7.46 3.207-2.45 1.117-4.857 2.32-7.156 3.663-2.121 1.219-4.157 2.562-5.957 4.071-.075.457-.151.951-.21 1.424a51.768 51.768 0 0 0-.21 2.009 51.354 51.354 0 0 0-.177 4.061 59.216 59.216 0 0 0 .5 8.11c.37 2.692.864 5.366 1.595 7.951.36 1.295.768 2.572 1.24 3.808.237.617.495 1.225.764 1.816.134.294.274.585.413.864l.172.328c.199.101.408.204.607.3l1.204.575c.671.305 1.6.746 2.368 1.09.043-.037.086-.075.123-.114l-2.235-8.513c.474-.13 4.718-1.225 12.032-2.617a38.816 38.816 0 0 1-1.772-.381c-1.665-.414-3.309-.919-4.899-1.564a22.415 22.415 0 0 1-2.309-1.115c-.742-.426-1.472-.908-2.037-1.548 8.036 2.622 24.64 1.434 39.399-.091 13.499-1.391 27.029-2.293 40.63-2.32 13.602.027 27.137.929 40.63 2.32 14.766 1.525 31.37 2.713 39.405.091-.564.64-1.293 1.123-2.035 1.548a22.5 22.5 0 0 1-2.308 1.115c-1.592.645-3.234 1.15-4.899 1.564-.247.059-.496.113-.743.166 8.02 1.488 12.689 2.697 13.188 2.831l-2.138 8.11c.43-.194.864-.381 1.29-.574l1.202-.575c.2-.097.403-.199.607-.3l.166-.328c.146-.279.286-.57.419-.864.27-.591.528-1.199.764-1.816a42.235 42.235 0 0 0 1.241-3.808c.731-2.585 1.225-5.259 1.595-7.951.345-2.685.526-5.398.501-8.11a50.874 50.874 0 0 0-.179-4.059z" id="jkpath14" fill="#f4edae"/><path class="jkst2" d="M116.787 182.661c-1.064.16-2.128.295-3.186.375-.682.033-1.404.102-2.059.102l-.242.005c.822-1.837 1.446-3.26 1.919-4.339.963 1.08 2.188 2.417 3.568 3.857z" id="jkpath16" fill="#e6ccad"/><path class="jkst2" d="M119.101 185.018c3.304 3.272 7.398 5.146 11.904 5.479-7.569 3.074-14.702 4.26-20.197 4.63-5.478.367-11.032-.279-16.474-1.771.456-.082.79-.14 1.193-.189.447-.054 10.206-1.327 14.605-7.868l.413.009 1.08-.009c.731 0 1.395-.06 2.094-.087a43.69 43.69 0 0 0 4.878-.703c.167.171.333.338.504.509z" id="jkpath18" fill="#e6ccad"/><path class="jkst3" d="M128.464 87.071a98.82 98.82 0 0 1-1.048 1.343c-1.933 2.444-4.614 5.57-7.794 8.627a369.585 369.585 0 0 0-11.404-.177c-6.46 0-12.655.171-18.537.457 8.311-3.449 18.296-6.818 29.109-8.842a113.323 113.323 0 0 1 9.674-1.408z" id="jkpath20" fill="#656c67"/><path class="jkst3" d="M79.821 90.792c-2.966 2.084-6.317 4.744-9.566 7.971a360.155 360.155 0 0 0-21.567 2.81c9.207-4.232 19.713-8.127 31.133-10.781z" id="jkpath22" fill="#656c67"/><path class="jkst3" d="M181.48 107.969l-3.384 23.679-16.212 11.355-42.283-4.807-6.365-20.961a1.383 1.383 0 0 0-1.108-.971c-1.567-.253-2.953-.382-4.108-.382-1.16 0-2.541.129-4.115.382-.522.086-.95.461-1.106.971l-6.209 20.45-42.047 9.357-16.662-11.672-3.283-26.572c.715-.404 1.441-.806 2.176-1.209 1.031-.222 2.191-.457 3.475-.704l3.094 25.073c.048.392.264.741.586.967l11.462 8.032a1.425 1.425 0 0 0 1.101.213l34.57-7.692c.119-.027.237-.069.344-.124a1.39 1.39 0 0 0 .682-.827l6.225-20.498c1.67-.43 5.947-1.429 9.706-1.429 3.749 0 8.03.999 9.701 1.429l6.225 20.498c.161.532.624.912 1.176.977l34.57 3.927c.335.037.677-.05.952-.242l11.469-8.025c.31-.22.52-.566.573-.946l3.062-21.421c2.301.444 4.224.846 5.733 1.172z" id="jkpath24" fill="#656c67"/><path class="jkst3" d="M185.751 93.119l-2.976 11.29c-6.086-1.342-19.456-3.975-37.654-5.747 5.946-2.535 12-5.715 17.531-9.69 10.829 1.53 18.78 3.169 23.099 4.147z" id="jkpath26" fill="#656c67"/><g id="jkg32"><path class="jkst4" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath28" fill="#e5caa3"/><path class="jkst4" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath30" fill="#e5caa3"/></g><g id="jkg38"><path class="jkst5" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath34" fill="#c7b39a"/><path class="jkst5" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath36" fill="#c7b39a"/></g><path class="jkst2" d="M187.481 115.502c.508.419.911 1.504.456 6.558-.559 6.188-3.16 17.049-4.771 18.8-1.778.344-5.505-.064-7.778-.595.393-1.559.505-2.306.822-3.9l3.975-2.781c.317-.22.526-.566.58-.941l2.778-19.466c1.686.912 3.421 1.899 3.938 2.325z" id="jkpath40" fill="#e6ccad"/><path class="jkst2" d="M40.937 140.908c.199.704.408 1.407.624 2.1-2.139.628-6.495 1.23-8.465.886-1.633-1.645-4.679-12.966-5.345-18.978-.543-4.871-.162-5.924.333-6.334.575-.483 2.728-1.708 4.593-2.707l2.519 20.449c.048.393.257.741.586.967z" id="jkpath42" fill="#e6ccad"/><path class="jkst2" d="M121.347 141.194l-.151 1.305s-4.581 4.248-11.956 5.199c-7.375.95-13.171-3.582-13.171-3.582.242.788.586 2.567 2.256 4.086a53.184 53.184 0 0 0-6.313-.393c-.804 0-1.616.023-2.401.061-4.539.237-10.924 7.1-15.414 14.014-2.203.697-9.089 2.883-17.06 5.237-7.44-10.309-11.098-20.842-11.469-21.932l.005-.006c-.15-.419-.301-.839-.441-1.268l1.913 1.338v.005l4.726 3.309 1.58 1.101c.236.167.515.253.794.253.102 0 .204-.011.305-.031l43.435-9.67a1.385 1.385 0 0 0 1.025-.95l6.194-20.39c1.069-.145 2.008-.22 2.814-.22.801 0 1.746.075 2.815.22l6.374 20.997c.162.532.624.919 1.171.977z" id="jkpath44" fill="#e6ccad"/><path class="jkst2" d="M170.926 140.066l1.402-.984c-.232.973-.484 1.94-.747 2.896-1.949 6.248-4.25 11.774-6.805 16.656-.565.039-1.161.061-1.8.061-1.972 0-3.986-.167-6.215-.371-3.868-.355-10.007-1.058-11.946-1.283-1.67-1.332-7.385-5.873-12.14-9.615-.187-.151-.348-.291-.505-.42-.837-.708-1.789-1.513-3.717-1.513-1.751 0-4.308.638-10.489 2.508 3.212-2.401 3.233-5.5 3.233-5.5l.151-1.305 40.748 4.629a1.41 1.41 0 0 0 .955-.241l4.094-2.868z" id="jkpath46" fill="#e6ccad"/><path class="jkst6" d="M140.937 54.337c.124 3.625.033 10.194-1.655 16.345a1.335 1.335 0 0 0 0 .704 259.298 259.298 0 0 0-6.446-.591c2.412-5.054 2.938-10.436 3.052-12.332 1.852-1.317 3.696-2.896 5.049-4.126z" id="jkpath48" fill="#ebd599"/><path class="jkst6" d="M79.456 58.462c.112 1.896.638 7.267 3.046 12.317-2.149.171-4.297.37-6.441.596a1.328 1.328 0 0 0 0-.694c-1.686-6.139-1.772-12.714-1.654-16.345 1.353 1.231 3.19 2.81 5.049 4.126z" id="jkpath50" fill="#ebd599"/><path class="jkst7" d="M151.835 125.675c-2.89-1.396-6.059.377-11.828.484-4.292.151-7.896.198-8.132-6.543-.237-6.747 2.513-12.326 6.805-12.478 4.292-.15 8.207 5.2 8.735 11.931.145 1.854-.06 3.207-.521 4.21 3.996-.477 4.899 2.235 4.941 2.396zm-13.488-9.878a2.203 2.203 0 0 0 2.154-2.235 2.186 2.186 0 0 0-2.235-2.153 2.194 2.194 0 0 0 .081 4.388z" id="jkpath52" fill="#2d3136"/><circle transform="rotate(-1.049 138.093 113.428)" class="jkst8" cx="138.307" cy="113.602" id="jkellipse54" r="2.194" fill="#edf6fa"/><path class="jkst7" d="M83.484 120.953c.063 6.747-3.509 6.339-7.806 6.381-.435.011-.848.016-1.258.022-.482.011-.944.016-1.39.005-4.168-.005-6.833-.194-9.19 1.079.058-.145 1.09-2.461 4.835-3.4-.414-.914-.673-2.181-.742-3.937-.257-6.741 3.9-12.269 8.197-12.306 4.292-.042 7.289 5.411 7.354 12.156zm-6.634-3.529a2.195 2.195 0 1 0-.122-4.388 2.195 2.195 0 0 0 .122 4.388z" id="jkpath56" fill="#2d3136"/><circle transform="rotate(-1.473 76.78 115.216)" class="jkst8" cx="76.79" cy="115.23" id="jkellipse58" r="2.195" fill="#edf6fa"/><g class="jkst9" id="jkg64" opacity=".8"><path class="jkst6" d="M50.691 75.155s.667-8.692 2.03-12.023c.702-1.717 4.996-2.81 8.276-3.591 3.278-.78 8.508-2.342 9.524 2.264 1.015 4.606 2.653 7.963 3.746 9.446l-1.404-18.97-22.562 5.464-1.484 16.786.703 1.327 1.171-.703" id="jkpath60" fill="#ebd599"/><path class="jkst6" d="M164.855 75.155s-.666-8.692-2.029-12.023c-.703-1.717-4.997-2.81-8.275-3.591-3.28-.78-8.51-2.342-9.526 2.264-1.013 4.606-2.654 7.963-3.748 9.446l1.407-18.97 22.562 5.464 1.483 16.786-.703 1.327-1.171-.703" id="jkpath62" fill="#ebd599"/></g><path class="jkst10" d="M132.965 18.378s-.598 45.49-11.224 45.49h-14.875-12.752c-10.626 0-11.484-45.47-11.484-45.47l-5.22 15.438.085 21.183 3.707 2.947 1.685 9.096 2.357 5.307 45.482.084 2.105-3.791 1.769-6.4.254-4.043 5.023-14.341z" id="jkpath66" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M166.429 60.794s2.187 15.692 7.974 18.522c5.788 2.829 0 0 0 0l-8.103-2.444z" id="jkpath68" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M48.908 60.794s-2.187 15.692-7.975 18.522c-5.788 2.829 0 0 0 0l8.104-2.444z" id="jkpath70" opacity=".75" fill="#ebd599"/><path class="jkst7" d="M167.987 76.8c2.755.902 5.526 1.858 8.036 3.325-1.343-.532-2.729-.913-4.126-1.257a70.385 70.385 0 0 0-4.201-.924c-2.82-.531-5.65-.982-8.498-1.327-2.841-.37-5.687-.682-8.546-.924-2.858-.241-5.709-.483-8.573-.65-11.446-.704-22.924-.88-34.41-.892-11.483.006-22.962.221-34.409.897-2.862.166-5.715.409-8.572.651-2.857.241-5.71.548-8.546.923-2.847.345-5.678.796-8.498 1.327-1.407.264-2.81.57-4.206.919-1.391.344-2.783.725-4.126 1.257 2.509-1.466 5.28-2.427 8.041-3.331.232-.075.467-.139.703-.214-.015-.059-.032-.113-.043-.177-.048-.317-1.069-7.859.709-18.645.086-.516.456-.935.962-1.075l2.917-.831c.634-22.625 9.952-33.266 10.243-33.594-8.326 13.397-8.25 29.286-8.106 32.986l18.128-5.152c.016-.005.026-.005.042-.01.076-.016.151-.027.226-.032.021 0 .049-.006.075-.006a1.19 1.19 0 0 1 .297.027c.015 0 .031.011.053.016.075.016.145.042.224.075.033.016.054.033.086.049.058.033.119.07.177.112.016.011.034.016.049.033l.032.032c.016.016.037.027.054.044.012.016.494.493 1.262 1.209-.182-5.973.102-23.108 8.262-37.31-.172.498-6.646 19.428-4.415 40.645.724.58 1.486 1.149 2.229 1.649.359.247.58.655.585 1.09.006.07.161 6.833 3.148 12.586.042.086.074.177.102.268 7.429-.505 14.878-.709 22.312-.714 7.436.005 14.88.22 22.307.731.027-.097.06-.193.109-.285 2.986-5.753 3.142-12.516 3.142-12.586.01-.436.231-.843.591-1.09.741-.5 1.493-1.069 2.224-1.649 2.234-21.217-4.24-40.147-4.411-40.645 8.153 14.201 8.444 31.336 8.262 37.31a62.536 62.536 0 0 0 1.261-1.209c.016-.016.039-.027.053-.044.012-.01.018-.021.033-.032.016-.016.033-.022.049-.033.06-.042.119-.079.177-.118.028-.01.054-.027.081-.043.081-.033.155-.059.236-.08.016 0 .033-.011.049-.011.096-.021.2-.032.296-.027.027 0 .049.006.07.006.075.005.156.016.231.032.012.006.028.006.042.01l18.129 5.152c.146-3.7.221-19.59-8.104-32.986.289.328 9.609 10.969 10.237 33.594l2.922.831c.499.14.875.559.962 1.075 1.777 10.786.752 18.328.708 18.645-.01.065-.026.124-.042.182.239.07.47.139.707.215zm-3.297-.968c.14-1.207.789-7.809-.591-16.801l-20.52-5.833c.184 3.475.265 11.012-1.707 18.199a1.619 1.619 0 0 1-.101.258c.203.021.408.037.606.064 5.769.661 11.511 1.584 17.189 2.83 1.712.398 3.426.823 5.124 1.283zm-25.409-5.151c1.688-6.15 1.779-12.72 1.655-16.345-1.353 1.23-3.197 2.809-5.049 4.125-.114 1.896-.64 7.278-3.052 12.332 2.149.173 4.298.366 6.446.591a1.33 1.33 0 0 1 0-.703zm-56.78.098c-2.408-5.05-2.934-10.422-3.046-12.317-1.858-1.316-3.696-2.895-5.049-4.125-.119 3.631-.032 10.206 1.654 16.345.065.237.058.473 0 .694 2.145-.227 4.292-.425 6.441-.597zm-8.933.864a1.65 1.65 0 0 1-.098-.247c-1.975-7.187-1.889-14.723-1.712-18.199L51.244 59.03c-1.38 8.982-.736 15.583-.597 16.797 1.703-.462 3.411-.887 5.131-1.284 2.835-.628 5.693-1.154 8.556-1.638 2.869-.478 5.747-.843 8.626-1.192.205-.027.404-.042.608-.07z" id="jkpath72" fill="#2d3136"/><g id="jkXMLID_1_"><g id="jkg78"><path class="jkst7" d="M129.293 18.973v17.025h-12.068v-4.974h-2.72v22.981h4.109v12.85H97.505v-12.85h4.092v-22.98h-2.711v4.974h-12.06V18.973zm-3.626 13.408v-9.789H90.443v9.789h4.816v-4.974h9.964v30.225h-4.1v5.606h13.865v-5.606h-4.1V27.407h9.964v4.974z" id="jkpath74" fill="#2d3136"/><path class="jkst0" id="jkpolygon76" fill="#cb3349" d="M101.123 57.632h4.1V27.407h-9.964v4.974h-4.816v-9.79h35.224v9.79h-4.816v-4.974h-9.964v30.225h4.1v5.606h-13.864z"/></g></g><path class="jkst3" d="M30.694 93.119c1.759-.399 4.136-.907 7.051-1.47a104.37 104.37 0 0 0-6.222 4.597z" id="jkpath83" fill="#656c67"/><path class="jkst5" d="M95.111 139.78s.492 3.165-3.938 4.519c-4.428 1.355-32.482 9.716-35.682 9.263-3.199-.451-11.319-5.874-11.319-5.874l-1.969-7.004 12.016 7.492z" id="jkpath85" fill="#c7b39a"/><path class="jkst5" d="M120.242 139.167s-.354 3.182 4.131 4.345c4.484 1.161 32.875 8.295 36.05 7.704 3.176-.591 11.053-6.361 11.053-6.361l1.663-7.084-11.045 6.588z" id="jkpath87" fill="#c7b39a"/><path class="jkst5" d="M28.412 133.956s3.887 7.775 10.166 5.083l4.485 1.645-.448 3.29-9.419 1.195-2.541-1.494z" id="jkpath89" fill="#c7b39a"/><path class="jkst5" d="M187.551 131.822s-6.353 8.115-12.632 5.424l-2.019 1.302.448 3.289 9.419 1.196 2.54-1.495z" id="jkpath91" fill="#c7b39a"/><path class="jkst5" d="M89.279 192.904s23.03 11.611 49.106-4.188l-8.374-.571s-18.272 7.232-32.738 3.235z" id="jkpath93" fill="#c7b39a"/><path class="jkst7" d="M112.626 171.509l1.594 1.899c.036.046 3.577 4.26 7.906 8.552 2.879 2.853 6.357 4.297 10.343 4.297 1.361 0 2.791-.175 4.235-.523 1.34-.326 2.796-.673 4.287-1.03 5.384-1.287 11.482-2.749 14.438-3.577.585-.166 1.238-.315 1.925-.472 3.935-.909 9.329-2.163 12.187-7.889 2.149-4.297 5.047-9.874 7.197-13.961-1.863.859-3.816 1.79-5.203 2.52-2.138 1.123-4.938 1.667-8.558 1.667-2.152 0-4.266-.181-6.605-.389-4.675-.43-12.586-1.361-12.667-1.372l-.606-.067-.478-.383c-.071-.052-7.003-5.575-12.606-9.981-.227-.186-.434-.358-.621-.513-.59-.503-.59-.503-.942-.503-1.797 0-7.02 1.62-18.462 5.167l-.703.223-.689-.26c-.078-.026-7.585-2.81-16.581-2.81-.736 0-1.47.019-2.185.056-.901.046-5.958 2.448-12.425 12.68l-.419.657-.741.238c-.107.037-11.238 3.63-23.042 7.005l-.766.218-.725-.337c-.077-.031-4.696-2.174-9.091-4.194 2.397 3.541 5.462 7.958 8.159 11.422 4.711 6.067 10.649 11.674 22.034 11.674 1.428 0 2.945-.088 4.503-.265 11.581-1.309 14.563-1.837 16.168-2.117.543-.092.973-.171 1.522-.238.088-.011 9.571-1.237 12.232-7.206 2.744-6.134 3.298-7.595 3.319-7.651l.968-2.583s.12-.669.317-.877c0 .005 0 .005.005.005l.019.016c.305.219.757.902.757.902zM40.499 55.71c-2.516 1.014-5.016 2.06-7.46 3.209-2.449 1.119-4.856 2.32-7.155 3.66-2.121 1.222-4.157 2.563-5.954 4.076-.077.455-.149.952-.211 1.423a51.357 51.357 0 0 0-.388 6.068c-.026 2.713.16 5.426.502 8.112.372 2.692.864 5.369 1.594 7.952a41.963 41.963 0 0 0 1.243 3.804c.233.623.492 1.228.762 1.818.134.294.274.585.413.864l.172.326c.201.104.409.207.605.3l1.206.574c.673.311 1.6.751 2.366 1.093.046-.037.088-.078.124-.114l-2.231-8.511c.471-.129 4.717-1.227 12.032-2.619a33.744 33.744 0 0 1-1.775-.379 36.704 36.704 0 0 1-4.898-1.563 22.857 22.857 0 0 1-2.309-1.119c-.741-.425-1.471-.905-2.035-1.547 8.035 2.624 24.637 1.433 39.398-.088 13.501-1.393 27.028-2.293 40.628-2.325 13.6.031 27.138.931 40.63 2.325 14.77 1.522 31.374 2.713 39.406.088-.564.642-1.293 1.122-2.034 1.547-.739.42-1.522.782-2.309 1.119a36.965 36.965 0 0 1-4.903 1.563c-.244.056-.492.114-.741.166 8.02 1.486 12.689 2.697 13.186 2.832l-2.138 8.107c.43-.192.864-.377 1.288-.574l1.207-.574c.196-.094.404-.196.606-.3l.166-.326c.144-.279.284-.57.419-.864.27-.591.528-1.196.767-1.818.471-1.231.879-2.51 1.236-3.804.731-2.583 1.228-5.26 1.595-7.952.346-2.686.528-5.4.502-8.112a52.755 52.755 0 0 0-.176-4.059 51.573 51.573 0 0 0-.213-2.009 29.83 29.83 0 0 0-.213-1.423c-1.797-1.513-3.831-2.853-5.954-4.076-2.299-1.34-4.704-2.541-7.159-3.66-2.438-1.149-4.943-2.195-7.46-3.209a140.105 140.105 0 0 0-3.801-1.476c-1.267-.491-2.552-.956-3.835-1.423 2.696.445 5.369 1.06 8.013 1.739 1.724.446 3.444.948 5.141 1.481-12.11-31.658-41.07-52.272-72.685-52.272-31.622 0-60.576 20.614-72.684 52.272a107.832 107.832 0 0 1 5.135-1.481c2.651-.678 5.322-1.294 8.02-1.739-1.29.466-2.568.931-3.842 1.423-1.268.47-2.535.967-3.799 1.475zm159.43 18.316a53.972 53.972 0 0 1-.258 8.733 55.462 55.462 0 0 1-1.619 8.605c-.4 1.414-.86 2.811-1.404 4.198a38.295 38.295 0 0 1-.89 2.071c-.161.341-.331.678-.523 1.025l-.284.512a8.975 8.975 0 0 1-.348.574l-.294.457-.461.237c-.492.254-.895.445-1.342.653l-1.298.585a88.22 88.22 0 0 1-2.62 1.065c-.611.239-1.15.457-1.662.674l-1.444 5.487c-.036-.009-.471-.12-1.283-.315l-.078.574c1.594.833 4.726 2.522 5.793 3.403 2.148 1.775 2.299 4.587 1.823 9.841-.244 2.697-1.139 7.946-2.381 12.767-2.144 8.298-3.283 9.273-4.753 9.649-.746.192-1.894.383-3.008.383-2.266 0-5.353.063-7.429-.439-.533 1.888-2.055 6.812-5.068 12.962.151-.073.3-.135.435-.207 3.717-1.952 10.861-5.064 11.162-5.199l5.643-2.452-2.89 5.435c-.067.118-6.264 11.773-10.059 19.383-3.769 7.538-10.835 9.179-15.065 10.151-.637.151-1.241.291-1.733.425-3.035.854-9.18 2.319-14.599 3.623-.064.016-.13.033-.197.042a64.057 64.057 0 0 1-10.955 5.411c-14.568 5.518-29.923 5.208-43.844.092a647.05 647.05 0 0 1-9.193 1.097 45.12 45.12 0 0 1-4.985.291c-13.264 0-20.294-6.736-25.425-13.331-5.493-7.062-12.212-17.546-12.497-17.985L31 158.426l6.585 2.961c3.152 1.419 12.524 5.757 15.205 7 .217-.061.43-.124.642-.186-4.457-6.357-8.112-13.605-10.695-21.634-2.195.662-5.576 1.175-8.206 1.175-.961 0-1.822-.072-2.484-.228-1.471-.336-3.148-1.754-5.431-9.795-1.325-4.668-2.314-9.764-2.603-12.387-.57-5.121-.466-7.864 1.662-9.636 1.283-1.071 5.611-3.344 6.507-3.809l-.192-1.58c-13.75 8.08-21.991 15.22-22.157 15.366L0 134.302l7.005-11.047c5.544-8.755 11.948-15.832 17.84-21.284-.244-.098-.471-.196-.71-.294l-1.299-.585a34.907 34.907 0 0 1-1.34-.653l-.461-.237-.295-.457c-.166-.249-.238-.388-.347-.574l-.29-.512c-.181-.347-.358-.684-.518-1.025a30.878 30.878 0 0 1-.89-2.071 44.74 44.74 0 0 1-1.404-4.198 54.745 54.745 0 0 1-1.62-8.605 54.664 54.664 0 0 1-.259-8.733c.078-1.455.218-2.909.419-4.354.104-.725.213-1.45.358-2.17.15-.734.296-1.418.518-2.221l.155-.564.404-.317c2.294-1.802 4.768-3.163 7.284-4.369a78.87 78.87 0 0 1 6.311-2.616c5.943-16.493 16.162-31.118 29.591-41.311C74.337 5.57 90.664 0 107.671 0s33.334 5.57 47.218 16.106c13.43 10.193 23.649 24.819 29.588 41.307a78.282 78.282 0 0 1 6.316 2.62c2.515 1.206 4.99 2.567 7.283 4.369l.404.317.156.564c.227.803.372 1.487.517 2.221.146.72.26 1.445.357 2.17.203 1.443.348 2.897.419 4.352zm-11.995 48.031c.456-5.052.058-6.139-.455-6.554-.513-.43-2.247-1.412-3.935-2.329l-2.779 19.464a1.39 1.39 0 0 1-.58.942l-3.977 2.781c-.315 1.593-.429 2.345-.817 3.903 2.273.528 5.999.938 7.775.595 1.612-1.748 4.214-12.61 4.768-18.802zm-5.161-17.648l2.977-11.29c-4.318-.978-12.27-2.615-23.1-4.148-5.53 3.976-11.582 7.155-17.53 9.691 18.199 1.771 31.57 4.406 37.653 5.747zm-4.68 27.237l3.385-23.676a240.127 240.127 0 0 0-5.731-1.169l-3.059 21.422a1.415 1.415 0 0 1-.575.943l-11.472 8.023c-.27.192-.616.28-.947.243l-34.572-3.929a1.391 1.391 0 0 1-1.176-.973l-6.227-20.5c-1.668-.431-5.949-1.43-9.696-1.43-3.764 0-8.041.999-9.708 1.43l-6.228 20.5a1.388 1.388 0 0 1-1.025.947l-34.572 7.692a1.483 1.483 0 0 1-.306.033 1.36 1.36 0 0 1-.792-.25l-11.467-8.029a1.396 1.396 0 0 1-.585-.968l-3.091-25.072c-1.284.249-2.443.487-3.479.703-.734.405-1.46.809-2.174 1.213l3.281 26.568 16.666 11.675 42.047-9.354 6.207-20.449a1.389 1.389 0 0 1 1.108-.975c1.574-.253 2.95-.382 4.116-.382 1.153 0 2.536.129 4.105.382.528.083.957.461 1.108.975l6.366 20.956 42.282 4.808zm-8.07-4.411l2.992-20.948c-8.439-1.536-20.78-3.394-35.897-4.554-13.647 4.707-25.077 6.108-25.766 6.155l-.797.057c4.353.374 8.454 1.544 8.66 1.605.452.135.804.481.944.933l6.186 20.366 33.138 3.764zm2.303 11.845l-1.404.983-3.779 2.651-4.095 2.868c-.279.192-.621.28-.954.243l-40.746-4.633-2.966-.337a1.39 1.39 0 0 1-1.171-.977l-6.377-20.998c-1.066-.145-2.014-.219-2.81-.219-.809 0-1.751.073-2.817.219l-6.192 20.392a1.383 1.383 0 0 1-1.025.946l-43.435 9.672c-.103.02-.206.03-.305.03-.279 0-.559-.083-.798-.253l-1.578-1.098-4.726-3.307v-.011l-1.91-1.335c.135.43.289.85.441 1.268l-.006.006c.368 1.092 4.028 11.622 11.467 21.929a873.96 873.96 0 0 0 17.057-5.234c4.488-6.917 10.877-13.777 15.418-14.014a51.12 51.12 0 0 1 2.402-.061c2.221 0 4.344.16 6.31.393-1.671-1.517-2.013-3.298-2.256-4.085 0 0 5.793 4.53 13.17 3.584 7.378-.953 11.959-5.204 11.959-5.204s-.021 3.102-3.236 5.503c6.182-1.869 8.739-2.511 10.489-2.511 1.931 0 2.883.808 3.717 1.519.161.129.322.268.507.419a3519.302 3519.302 0 0 1 12.141 9.614c1.936.227 8.075.926 11.943 1.283 2.23.201 4.245.372 6.217.372.637 0 1.233-.026 1.797-.063 2.558-4.88 4.857-10.411 6.808-16.653.261-.96.516-1.928.743-2.901zm-15.034-51.593c-.01-.006-.02-.012-.031-.012a551.624 551.624 0 0 0-9.826-.651 905.6 905.6 0 0 0-13.667-.668 72.95 72.95 0 0 1-1.574 2.225c-2.479 3.355-7.398 9.51-13.704 14.729 8.926-1.6 24.409-5.56 37.803-14.905.336-.238.668-.486.999-.718zm-29.876.926c.377-.471.729-.926 1.044-1.34-3.281.331-6.512.808-9.67 1.408-10.814 2.024-20.801 5.389-29.11 8.837a383.259 383.259 0 0 1 18.54-.455c3.908 0 7.708.067 11.404.176 3.179-3.056 5.861-6.182 7.792-8.626zm3.587 102.085c-4.503-.332-8.598-2.205-11.903-5.477a271.86 271.86 0 0 0-.502-.512 44.25 44.25 0 0 1-4.881.704c-.698.026-1.361.087-2.091.087l-1.083.011-.413-.011c-4.396 6.539-14.159 7.813-14.605 7.87-.403.046-.734.103-1.191.186 5.442 1.491 10.996 2.138 16.474 1.77 5.492-.367 12.627-1.558 20.195-4.628zm-17.4-7.461a45.604 45.604 0 0 0 3.184-.378 138.958 138.958 0 0 1-3.568-3.857 398.441 398.441 0 0 1-1.92 4.339h.243c.658.001 1.378-.071 2.061-.104zm-3.354-78.632c1.827-1.103 3.582-2.366 5.249-3.712a422.33 422.33 0 0 0-7.278-.072c-10.137 0-19.606.415-28.189 1.061-8.61 4.209-13.875 7.672-13.998 7.76l-8.268 5.514 5.679-8.149a52.452 52.452 0 0 1 2.956-3.857c-9.536 1.066-17.477 2.329-23.41 3.422l3.038 24.632 10.453 7.321 33.184-7.378 6.212-20.464c.104-.337.331-.621.627-.793.098-.063.202-.109.315-.14.192-.052 3.51-.999 7.336-1.465zm3.816-18.788c-2.31-.036-4.623-.057-6.933-.062h-.005c-3.39.005-6.787.041-10.189.109l-6.269 2.971c-.005.005-.041.021-.088.048-.942.46-9.174 4.613-16.919 12.021 6.943-3.65 17.146-8.418 29.153-12.115a144.186 144.186 0 0 1 11.25-2.972zM70.251 98.761c3.251-3.225 6.605-5.886 9.567-7.967-11.415 2.651-21.923 6.543-31.128 10.778a360.846 360.846 0 0 1 21.561-2.811zm2.159-9.949a150.122 150.122 0 0 1 11.813-2.796c-5.798.212-11.6.481-17.393.808-3.366.186-6.715.414-10.065.667-1.678.129-3.345.263-5.007.445-.476.046-.942.098-1.418.16-4.369 2.614-21.127 13.134-32.631 26.889 11.179-7.769 30.654-19.443 54.701-26.173zm-30.85 54.197a68.861 68.861 0 0 1-.621-2.102l-5.162-3.612a1.391 1.391 0 0 1-.586-.969l-2.516-20.449c-1.864.999-4.017 2.225-4.592 2.707-.497.409-.875 1.46-.336 6.332.668 6.01 3.712 17.333 5.348 18.979 1.968.347 6.327-.258 8.465-.886zm-3.815-51.36a229.005 229.005 0 0 0-7.051 1.47l.829 3.127a103.93 103.93 0 0 1 6.222-4.597z" id="jkpath95" fill="#2d3136"/></g></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tune" xmlns="http://www.w3.org/2000/svg"><path d="M6.85 2.852h-2v6h2v-6m12 0h-2v10h2v-10m-16 10h2v8h2v-8h2v-2h-6v2m12-6h-2v-4h-2v4h-2v2h6v-2m-4 14h2v-10h-2v10m4-6v2h2v4h2v-4h2v-2h-6z" fill="#fbc02d" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 50 50" id="twig" xmlns="http://www.w3.org/2000/svg"><path d="M9.727 47.556c-.125-.223-.297-2.168-.183-2.087.034.025.171.267.304.537.132.27.282.487.332.482.123-.011.075-1.196-.1-2.454-.331-2.398-1.176-4.435-2.358-5.69-.2-.212-.344-.4-.319-.419.093-.067 1.327.843 1.842 1.359.293.293.735.825.981 1.181.328.474.465.618.51.534.078-.147-.21-9.903-.376-12.701-.074-1.255.063-1.023.61 1.035 1.064 4.006 1.858 7.922 2.342 11.55.086.637.173 1.172.195 1.19.022.016.092.001.157-.034.888-.483 1.524-.667 2.55-.736.727-.048.945.062.35.178-1.15.222-1.99 1.013-2.344 2.201-.315 1.061-.327 2.707-.024 3.434.152.366.037.426-1.067.56-.716.088-.977.096-1.202.037-.356-.092-1.118-.098-1.195-.008-.031.036-.243.066-.47.066-.38 0-.423-.017-.535-.215zm1.974-3.233c.152-.205.072-.41-.204-.522-.225-.09-.263-.088-.437.025-.21.137-.252.43-.08.554.18.13.607.096.72-.057zm1.248.086a.763.763 0 0 0 .214-.203c.241-.33-.352-.622-.745-.366-.406.265.08.785.531.569zm2.288 3.094c-.033-.039.117-.387.334-.775.216-.387.411-.665.433-.618.07.152-.201 1.28-.33 1.372-.15.108-.354.117-.437.02zM8.2 47.092c-.29-.343-.221-.434.14-.182.176.123.321.263.321.31 0 .165-.279.087-.46-.128zm8.649-.145c0-.053.102-.18.227-.282.25-.204.312-.113.143.207-.095.18-.37.236-.37.075zm8.065-.827c-.243-.025-.48-.088-.527-.141-.11-.125-.114-3.043-.004-3.043.045 0 .132.149.193.331.127.38.228.42.31.124.094-.337.065-3.472-.039-4.297-.449-3.55-1.865-6.124-4.342-7.89-1.086-.774-2.653-1.436-4.047-1.711-.764-.15-.522-.224.598-.182 2.364.089 4.167.706 5.847 2.001a11.046 11.046 0 0 1 2.32 2.502c.453.682.64.854.64.584 0-.07.063-.882.139-1.805.679-8.26 2.396-15.1 4.984-19.86 1.86-3.422 5.108-6.817 7.885-8.244 1.397-.718 2.539-.988 4.02-.952.933.023 1.01.036 1.77.307a6.822 6.822 0 0 1 1.363.662c.612.407 1.309 1.004 1.235 1.058-.026.018-.343-.165-.705-.407-2.657-1.771-5.062-1.52-7.12.742-1.108 1.22-2.651 3.53-3.634 5.443-2.828 5.503-4.541 11.464-5.291 18.413-.163 1.509-.282 3.76-.195 3.703.032-.022.266-.52.518-1.108 1.597-3.723 3.578-6.428 5.79-7.908.672-.449 1.612-.904 1.715-.83.022.016-.172.22-.432.454-1.957 1.754-3.248 3.76-4.232 6.572-.938 2.68-1.366 5.588-1.368 9.3-.002 1.741.188 4.385.366 5.101.125.505.08.546-.585.546-.55 0-2.306.138-3.416.27-.414.05-.817.04-1.609-.036-.58-.056-1.129-.119-1.218-.14-.165-.037-.18-.014-.2.302-.01.186-.098.203-.728.139zm2.507-6.725c.294-.11.375-.22.375-.517 0-.63-1.309-.706-1.524-.088-.074.211.13.51.42.616.297.108.413.106.73-.011zm2.369-.052c.277-.222.318-.364.174-.611-.4-.691-1.755-.307-1.428.404.121.266.299.35.738.354.227 0 .387-.045.516-.147zm3.011 6.681c-.027-.05.088-.268.256-.484.879-1.135 1.22-1.544 1.284-1.544.04 0 .056.037.036.082l-.423.964c-.212.485-.445.924-.519.977-.169.122-.57.125-.634.005zm2.446-.596c0-.121.853-.683.896-.59.018.04-.056.209-.166.376-.168.259-.238.305-.464.305-.164 0-.266-.035-.266-.091zm-13.04-.124c-.177-.159-.493-.656-.462-.725.018-.038.248.1.512.309.264.207.457.405.428.438-.075.088-.371.074-.478-.022z" fill="#9bb92f" stroke-width=".078"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript" xmlns="http://www.w3.org/2000/svg"><path d="M49 51h408v408H49V51zm246.669 314.879l19.463-1.702c.922 7.8 3.067 14.199 6.435 19.198 3.368 4.998 8.597 9.04 15.688 12.124 7.09 3.085 15.067 4.627 23.93 4.627 7.87 0 14.819-1.17 20.845-3.51 6.027-2.34 10.512-5.548 13.455-9.625 2.942-4.077 4.413-8.526 4.413-13.348 0-4.892-1.418-9.164-4.254-12.816-2.836-3.651-7.516-6.718-14.039-9.2-4.183-1.63-13.436-4.165-27.759-7.604s-24.355-6.683-30.099-9.732c-7.445-3.899-12.993-8.739-16.644-14.517-3.652-5.779-5.478-12.249-5.478-19.41 0-7.871 2.234-15.227 6.701-22.069 4.467-6.842 10.99-12.036 19.569-15.581 8.58-3.546 18.116-5.318 28.61-5.318 11.557 0 21.75 1.861 30.577 5.584 8.828 3.722 15.617 9.199 20.368 16.432 4.75 7.232 7.303 15.421 7.657 24.568l-19.782 1.489c-1.064-9.856-4.662-17.301-10.795-22.335-6.133-5.034-15.191-7.551-27.174-7.551-12.479 0-21.573 2.286-27.281 6.86-5.707 4.573-8.561 10.086-8.561 16.538 0 5.602 2.021 10.21 6.062 13.826 3.971 3.617 14.34 7.321 31.109 11.115 16.769 3.793 28.273 7.108 34.513 9.944 9.076 4.183 15.776 9.483 20.101 15.9 4.325 6.417 6.488 13.809 6.488 22.175 0 8.296-2.375 16.113-7.126 23.452-4.751 7.338-11.575 13.046-20.474 17.123-8.898 4.077-18.913 6.116-30.045 6.116-14.11 0-25.933-2.056-35.47-6.169-9.537-4.112-17.017-10.299-22.441-18.559-5.424-8.26-8.278-17.602-8.562-28.025zm-65.728 50.094V278.454h51.583v-18.399H157.938v18.399h51.37v137.519h20.633z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript-def" xmlns="http://www.w3.org/2000/svg"><path d="M457 459H49V51h408v408zM69 71v368h368V71H69z" fill="#0288d1"/><text x="342.219" y="344.544" font-family="ArialMT" font-size="12" fill="#0288d1" transform="translate(-6058.94 -5838) scale(18.1514)"><tspan style="-inkscape-font-specification:sans-serif" font-family="sans-serif" font-weight="400">TS</tspan></text></symbol><symbol viewBox="0 0 24 24" id="url" xmlns="http://www.w3.org/2000/svg"><path d="M16 6h-3v1.9h3a4.1 4.1 0 0 1 4.1 4.1 4.1 4.1 0 0 1-4.1 4.1h-3V18h3a6 6 0 0 0 6-6c0-3.32-2.69-6-6-6M3.9 12A4.1 4.1 0 0 1 8 7.9h3V6H8a6 6 0 0 0-6 6 6 6 0 0 0 6 6h3v-1.9H8c-2.26 0-4.1-1.84-4.1-4.1M8 13h8v-2H8v2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="verilog" xmlns="http://www.w3.org/2000/svg"><path d="M17.282 17.08H6.718V6.513h10.564m4.226 4.226V8.627h-2.113V6.514c0-1.173-.95-2.113-2.113-2.113H15.17V2.288h-2.113v2.113h-2.112V2.288H8.83v2.113H6.718c-1.173 0-2.113.94-2.113 2.113v2.113H2.492v2.113h2.113v2.113H2.492v2.113h2.113v2.113a2.113 2.113 0 0 0 2.113 2.113H8.83v2.113h2.113v-2.113h2.112v2.113h2.113v-2.113h2.113a2.113 2.113 0 0 0 2.113-2.113v-2.113h2.113v-2.113h-2.113V10.74m-6.339 2.113h-2.112V10.74h2.112m2.113-2.113H8.831v6.34h6.338z" fill="#ff7043" stroke-width="1.056"/></symbol><symbol viewBox="0 0 24 23.999999" id="vfl" xmlns="http://www.w3.org/2000/svg"><defs><style>.jra{fill:#f05223}.jrb{fill:url(#jra)}</style><radialGradient id="jra" cx="205.45" cy="208.29" r="225.35" gradientTransform="matrix(.04556 0 0 .0456 2.888 2.88)" gradientUnits="userSpaceOnUse"><stop stop-color="#ffd104" offset="0"/><stop stop-color="#faa60e" offset=".35"/><stop stop-color="#f05023" offset="1"/></radialGradient></defs><title>houdinibadge</title><g stroke-width=".046"><path class="jra" d="M19.97 3H4.03A1.03 1.031 0 0 0 3 4.031v4.135C4.548 6.977 6.563 6.21 8.948 6.21c5.107.003 8.35 3.574 8.348 8.081 0 3.13-1.46 5.485-3.746 6.71h6.42A1.03 1.031 0 0 0 21 19.968V4.031a1.03 1.031 0 0 0-1.03-1.03z" fill="#f4511e"/><path class="jrb" d="M3 17.722v2.247A1.03 1.031 0 0 0 4.03 21h1.837C4.474 20.21 3.49 19 3 17.722z" fill="url(#jra)"/><path class="jra" d="M8.948 8.231c-2.586-.09-4.598.86-5.948 2.264v3.163c.918-2.654 3.447-3.87 5.565-3.85 2.647.027 4.689 2.025 4.7 4.284.012 2.159-.892 3.748-3.33 4.14-1.33.213-3.411-.567-3.318-2.578.046-1.037.854-1.622 1.777-1.58-.905 1.213.293 2.102 1.139 1.921 1.048-.224 1.475-1.156 1.475-1.878 0-.762-.718-1.994-2.498-1.951-2.204.052-3.591 1.639-3.638 3.602-.056 2.468 2.253 4.091 4.622 4.121 3.48.046 5.543-2.24 5.539-5.586-.005-3.029-2.434-5.946-6.085-6.072z" fill="#f05223"/></g></symbol><symbol viewBox="0 0 24 24" id="virtual" xmlns="http://www.w3.org/2000/svg"><path d="M21 14H3V4h18m0-2H3c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h7l-2 3v1h8v-1l-2-3h7a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 281.25 281.25" id="visualstudio" xmlns="http://www.w3.org/2000/svg"><path d="M196.18 101.74l-52.778 42.444 52.778 40.889V101.74m-136.67 110l-30-18.889v-100L62.843 81.74l47.778 37 96.666-89.222 44.444 27.778v172.22l-55.555 22.222-85.111-81.555-51.555 41.555m3.333-48.889l20.667-19.111-20.667-19.778z" fill="#ab47bc" stroke-width="11.111"/></symbol><symbol viewBox="0 0 300 300" id="vscode" xmlns="http://www.w3.org/2000/svg"><defs><style>.icon-canvas-transparent{fill:#f6f6f6;opacity:0}.icon-white{fill:#fff}</style></defs><title>BrandVisualStudioCode</title><path d="M218.62 29.953l-105.41 96.92L54.301 82.47 29.955 96.64l58.068 53.359-58.068 53.359 24.346 14.212 58.909-44.402 105.41 96.878 51.424-24.976V54.93zm0 63.744v112.6l-74.719-56.302z" fill="#2196f3" stroke-width="17.15"/></symbol><symbol viewBox="0 0 24 24" id="vue" xmlns="http://www.w3.org/2000/svg"><path d="M1.821 4.15l10.21 17.618L22.24 4.235V4.15h-7.692L12.113 8.33 9.691 4.15H1.82z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179H5.937z" fill="#35495e"/></symbol><symbol viewBox="0 0 420 419" id="watchman" xmlns="http://www.w3.org/2000/svg"><g stroke="#fff" stroke-linecap="round" stroke-linejoin="bevel"><path d="M166.95 145.32a93.935 123.23 0 0 1 92.934 3.263" fill="none" stroke-width="18.467"/><path d="M162.92 137.96L44.63 256.25a174.07 173.93 0 0 0 5.705 16.486l123.68-123.68-11.096-11.096zM266.54 144.04l-11.096 11.096 117.16 117.16a174.07 173.93 0 0 0 5.691-16.5l-111.76-111.76zm170.65 170.65v22.193l17.1 17.1 11.096-11.098-28.195-28.195z" fill="#fff" stroke-width="1.963"/><path d="M167.52 273.36a93.935 123.23 0 0 1 92.934-3.263" fill="none" stroke-width="18.467"/><path d="M49.516 144.56a174.07 173.93 0 0 0-.809 2.213 174.07 173.93 0 0 0-4.757 14.344 174.07 173.93 0 0 0-.016.055l119.56 119.56 11.098-11.096-125.07-125.07zM454.87 64.703l-17.668 17.668v22.191l28.764-28.764-11.096-11.096zm-80.984 80.984l-117.86 117.86 11.098 11.096 112.18-112.18a174.07 173.93 0 0 0-5.416-16.777z" fill="#fff" stroke-width="1.963"/></g><image x="21.229" y="20.262" width="378" height="377.1" preserveAspectRatio="none" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaQAAAGjCAYAAABjSWGNAAAgAElEQVR4AeydB3hUVdrH/+fOpNMF JAFUmivFXtZuIBRRQUUTil1RV3et6Lr6rSu6rg3B1dXVtXeBCCioCASIimLDhkFsgAIJSAkhPZmZ 8z3vjReGMJlMueeWmfc88MzMLaf8zp3855zznvcV4MQEkoxA7sVr01PrUrv4pdZNBNA1AHQRkJ0g ZAcJrYOA7Aipv8/Q6JgUKdBkG0iZISDSDVwSaAfAY3z+/bVSAD56L+mfwA5INAqgSkrUCKA2IOQO IcQOIUU5pNwhNZQLia2Q2OyH3OJBxqaiwk4VzfLlj0wg4QmIhG8hNzDJCEgxJH9DjgfevgGBAyCx vxSyh5DoAWA/CHQH0MEFUOoBbJbAeiHEBiED6wMSv0DgF02In1J2Vq+ZP78fXcOJCSQMARakhOnK 5GrICaO3tE3P8B0kERgkgP6Qop8E+gqgL4Bdo5gEphKAwHpIrIHAjxL4DpCrAo1yVfHsHhsSuN3c tAQmwIKUwJ2bGE2TYsjY33qLQOBwCHk4JA6HhgE08kmM9ilpRQUgvwPE1xD4MiC0r2R67dfFz/eq U1IaZ8oETCLAgmQSSM7GHAIj8td38sN7HAROkpDHATgMTWs15hSQvLn4JLBaQHwqEPgQQiwvmpHz XfLi4JY7kQALkhN7JYnqlDd2w4GQnuMk5AkCOBHAQQD4ubTgGZDAFg1ieQCBj6Dhg7SKmhW8LmUB eC6iRQL8xW8RDZ8wn4AUQ8eVHib9YjCELj4nAOhqfjmcY4wE6iGxAkIsCwDvSel5v7iwa1WMefFt TCBqAixIUSPjG6IhkDtmQw/N4xkKTQ4TEkNZgKKhZ/u1DZD4CAKLIMSik/p3WzF5sgjYXiuuQMIS YEFK2K61p2GTJ0tt2Xebj5EyMArAab+vAdlTGS7VVAI0xSeAd6QQb2uBtIW8V8pUvJwZz9XzM2AG AdpoKqpTh3uEdlYA8gwBdDEjX87D0QQaALlMCLzha5Rz2NTc0X3lmsrxCMk1XeWsig6/YFOWr0Ge JoBzICWNhNo6q4ZcGwsJkFOKT4TUZiOgzSqate8aC8vmohKIAAtSAnWm6qaMHPljWl2bzBGapk2A lKMBZKguk/N3JYHPpMCrHuGbuWj6fqWubAFX2hYCLEi2YHdToVIMKSg7SQAXABgDoJObas91tZWA H5DvCeCV2rrUwg/ndqm0tTZcuOMJsCA5vovsqeDwszd19af4LwbEZQAOtKcWXGoCEaiCwAwB7emi Gd0+TqB2cVNMJMCCZCJMt2dFFnIflJQNh8BEgKbkRIrb28T1dySBlULIpzyBwCsLCntud2QNuVK2 EGBBsgW7swqlvUIer+cSQE7UPWI7q3pcm8QlUAtgtibFU4sKu70PCJm4TeWWRUKABSkSSgl5DTkt 3XSqJuVfJDAiRFyfhGw1N8qxBH4AxJP+hrpnit/otcOxteSKKSXAgqQUr/My18216wMXCOB6AH9w Xg25RklOoEoI8bzw+R5ZNKvnj0nOIumaz4KUJF0+YsyWbL/Xdx0gr5BAxyRpNjfTvQQCkPIdDdqD iwqz33NvM7jm0RBgQYqGlguvbfKmrd2MJrPtNBc2gauc5ASElJ8EIO4/eWD2m+xLL7EfBhakBO3f wfmlR2oCtwI4G4CWoM3kZiURASmwGpAPVLTPeXnFk6IxiZqeNE1lQUqwrs4bW3Y0AoF/QIjT2Vdh gnUuN8cgsFYK3LNPoPzFwsKBDcZBfnU/ARYk9/eh3oIh+WXHaELeIZs8bCdIq7gZTCAsgXUQuK9T oPw5FqawnFxzkgXJNV0VuqJD8zcdDCHvlvpG1tDX8FEmkOAE1gohJp/Yv9vLvMbk7p5mQXJp/w0Z u7mPkP7JACbwGpFLO5GrbS4BiRII/H3xzOw3eZOtuWityo0FySrSJpWTO760s8cv7wQEeVVINSlb zoYJJBAB+bGQ4uaiwpxlCdSopGgKC5JLuvm4/PUZmcJ7NSD/DqCDS6rN1WQCdhKY45f+vxYX9vzJ zkpw2ZETYEGKnJVNV1L4h9JxAuI+9jNnUxdwsW4mQFZ4//E31N/NLomc340sSA7uo2H5Gw7zC+3f AjjFwdXkqjEBNxDYKgVuPbl/9rNs+ODc7mJBcmDf5J61toMnNfVfgLgCgNeBVeQqMQF3EpD4XGja NRyTyZndx4LkqH6RYkj+pouEkFMAdHZU1bgyTCBxCEgIPFvvabx52av7lydOs9zfEhYkh/Th0HM2 95Ye//8ADHVIlbgaTCDRCWwWENcWzcyemegNdUv7WJBs7qncXOnVum66XoBMuZFpc3W4eCaQdAQE xFyfz//n4tk9NiRd4x3WYBYkGzuEjBak0J6WwJE2VoOLZgJMANgJIW49qX+3J9jowb7HgQXJBva0 pyhLeO+QkJPYaMGGDuAimUDLBJYJgSuKZuR81/IlfEYVARYkVWRbyHfouLJcBPCUhOzbwiV8mAkw AXsJ1EOKe3Z07HYvh7mwtiNYkCzinXvx2nRvTdq9EriOw0JYBJ2LYQLxEfgCUlywuDB7VXzZ8N2R EmBBipRUHNcNGVt6hJB4CcCAOLLhW5kAE7CeQB1tqF0yI/thdtiqHj4LkkLG+fnSUy7KbpWQ/wBE isKiOGsmwASUEpBLPBouXji9+3qlxSR55ixIih6A3DEbemhe7WV2+6MIMGfLBKwnsA3AxMUzc96w vujkKJEFSUE/D87fOFoT4lkA+yjInrNkAkzAVgLyv/7MhknFz/eqs7UaCVg4C5KJnTpy5I9p9W3b TBGQf2HDBRPBclZMwHkEVnr8KFg4K2e186rm3hqxIJnUd0PGlO4vPHgdAkeZlCVnwwSYgLMJVEHI yxfP6D7d2dV0T+1YkEzoq6H5ZadLIV/gKToTYHIWTMBlBIQQj6bsrLpp/vx+9S6ruuOqy4IUR5fo VnRa2Z1S4jaeoosDJN/KBNxP4DMhcW5RYc6v7m+KfS1gQYqRvR6zKCXtVQiMjDELvo0JMIEEIiCB LR4p8hcVZr+XQM2ytCmapaUlSGF5+WUDvKnpn7EYJUiHcjOYgAkEBNAlIAKLho7deI0J2SVlFjxC irLbh+SXni0EXgTQJspb+XImwASShICAeC6lsuoqXleKrsNZkKLglVdQeiuAf/F6URTQ+FImkLwE lnkatXMWzun2W/IiiK7lLEgR8KL9RQ1tsyia60URXM6XMAEmwAQMAmsDHjFq6WvZJcYBfm2ZAAtS y2z0M7njSzt7/JgD4MRWLuXTTIAJMIFQBCqkkAVLZnRfGOokH9tNgI0adrPY611u/vq+Xr9YzmK0 Fxo+wASYQOQE2gsp3h4ytnRi5Lck55UsSC30+5D8smM8wvMhB9JrARAfZgJMIBoCXiHxVN7YjXcC kmemWiDHYEKAyRu76QzIALkDyQpxmg8xASbABGImQBZ4vt+6XVFcLHwxZ5KgN7IgNevYvPyNl0EI MmDwNDvFH5kAE2AC5hCQmJ9Zh3PnzcupMSfDxMiFp+yC+nFIQdlNEOIpFqMgKPyWCTAB8wkIjKxJ xyLy+GJ+5u7NkUdIet9JkVdQRvuLaJ8RJybABJiAVQS+9kvvqcWFXTdZVaCTy0l6QZo8WWrLVpU9 JoE/ObmjuG5MgAkkJgEB8ZNPw7Di6dnrErOFkbcqqQWJvHVvF2UU2fXCyJHxlUyACTAB0wn86pf+ vOLCnj+ZnrOLMkzaNaTcXOndLja9zGLkoqeVq8oEEpfAfh7hfW/4OaUHJW4TW29ZUo6Q8vNLUreh 42tCYEzriPgKJsAEmIBlBDZDiiGLC7NXWVaigwpKOkEiMSoXnQol5GgH9QNXhQkwASagE6C4SprU 8ooKu61MNiRJNWVH03Q0MmIxSrbHnNvLBNxDgOIqSREoGjq2tL97am1OTZNmhEQGDNtE6asCosAc dJwLE2ACTEAlAVkGIXMXz+jxg8pSnJR3UoyQfreme57FyEmPHteFCTCB8ARENqS2mJw8h78ucc4m gSBJUS7KHgVwfuJ0G7eECTCBJCHQwyM8i3LHbOiRDO1NeEEaMrbsHt70mgyPMreRCSQsgQM8Hu3d kfllXRK2hb83LKEFaUjBxluExN8SvRO5fUyACSQ4AYGBjQjMG5q/vX0itzRhBSlvbOmVAuLeRO48 bhsTYALJQ0AK8UcpamfT1pVEbXVCCtLg/I2jIfEYgKSxIkzUB5TbxQSYQDABMWS76Phsogb5SzhB Gjxu0x81IV7jEBLBDzG/ZwJMIIEInJc3tuyhBGrPrqYklCCReaQIBOYByNzVQn7DBJgAE0g0AhLX Dc0vTbj18YSZ0iILlEaBjyRk0tjsJ9p3jNvDBJhAVAQkpBy/uLD7jKjucvDFCTFCokW+Bsg3WIwc /KRx1ZgAEzCbgIAQz9IyhdkZ25VfAgiSFPoin8DxdkHkcpkAE2ACNhHI1AKBuYmycdb1gjS0oOz/ AJxn08PAxTIBJsAE7CbQ1ePV3kmEPUquFqS8/LJzJXCX3U8Dl88EmAATsJnAwVLUv+R2c3CPzRBj Ln5o/qaDIeRbABJ2k1jMcPhGJsAEkpHAH3oPrNTWlkxb6tbGu9LKLu/sDfsgRfsEQB+3gud6MwEm wAQUEJBC4NyiGTmzFeStPEvXTdlRKAmkaLTxlcVI+ePBBTABJuAyAkJKvKDPILms4lRd1wlSudj0 LwDDXMiaq8wEmAATsIJAG6kFZrnRyMFVgkQ+6iTkX63oUS6DCTABJuBaAhL9Aqhznc871wjS0HM2 99aEeIEdprr2K8IVZwJMwEICQmBMXsGmGy0sMu6iXGHUkHvx2nRPddpHEDg87hZzBkyACTCBpCEg G4UUQ4oKc5a5ocmuGCF5a9IeYjFyw+PEdWQCTMBZBESKFGKGbpnsrIqFrI3jBWno2NIxHII8ZN/x QSbABJhABARkDlI8z7lh06yjBWnImNL9IfF0BMT5EibABJgAE2iRgByVl192bYunHXLCsWtItN9o O8reBztNdcijwtVgAkzA5QTqhSaPK5re/UuntsOxI6RyUXo7i5FTHxuuFxNgAi4kkBaQ4tVRo0od G8DUkYI0JL/sGAlBXrw5MQEmwASYgEkEhMRBtZnifpOyMz0bx03ZDb9gU5a/PvAFgANNby1nyASY ABNgAlIKeeqSGd0XOg2F40ZI/nr/AyxGTntMuD5MgAkkEAEhpHh2RP76Tk5rk6MEacjYjcMBcZXT IHF9mAATYAIJRqC7X3gedVqbHDNld8LoLW3T0xu/BbCf0yBxfZgAE2ACiUhASoxZUpgzxyltc8wI KT29cQqLkVMeC64HE2ACyUBACPnYiRN+6eiUtjpCkIbll50C4AqnQOF6MAEmwASSg4DITvN7pzml rbZP2ZFNfE0GvuGAe055JLgeTIAJJB0BiZGLC3Petbvdto+QajPEP1iM7H4MuHwmwASSmoDA407Y MGurIOWNX3+IRMBV8TqS+qHlxjMBJpCoBA6oyRB32N042wRp8mSpwe99AhApdkPg8pkAE2ACTEDe OHjshkPt5GCbIC0r2XQlII+zs/FcNhNgAkyACewi4NWkeEIfLOw6ZO0bWwRpZH5ZFynkPdY2lUtj AkyACTCB8ATEse9/V3Zp+GvUnbVFkBqaxKiDumZxzkyACTABJhATAYl7cs9aa8vfZ8sFadi40qMA 2KbAMXUQ38QEmAATSBICAuiipabfZUdzLRYkKQIB+R8AFpdrB1oukwkwASbgTgIC8uph+RsOs7r2 lgrDkPxNFwHiWKsbyeUxASbABJhAVAQ8AWgPRXWHCRdb5qnhd48MPwDobkK9OQsmwASYABNQTEAI eVbRjO5vKi5mV/aWjZBq0sVNLEa7uPMbJsAEmIDjCUgpHjjyCmnZXlFLBGnEmC3ZEPJmx9PnCjIB JsAEmEAwgQM7lJddHXxA5XtLBMmX0vhPAG1UNoTzZgJMgAkwAQUEBG63KkSFckEafk7pQZC4SAEm zpIJMAEmwATUE9gn1Z/yV/XFWGB+7feARkdeKxrDZTABJsAEmID5BITENXnjN+9rfs575qh0hPT7 Jthz9iySPzEBJsAEmIDLCGSJQODvquusVJACAZC/OstMy1XD4vyZABNgAslKQEp5xfD8Tb1Utl+Z IA0pKD0ZwDCVlee8mQATYAJMwDICqX7NTwFVlSVlgiQgbQ/2pIwaZ8wEmAATSEYCUpw/ZOzmPqqa rkSQ8saVngSIIaoqzfkyASbABJiALQS8Aj5la0lKBAkBOdkWVFwoE2ACTIAJqCUgxflDz9ncW0Uh pgvS0PzSE3l0pKKrOE8mwASYgCMIeOFRY3FnuiBB4FZHIONKMAEmwASYgBICEvK84eM29jQ7c1MF KW/8+kMkMNLsSnJ+TIAJMAEm4CgCqb6AuMHsGpkqSPB7yL0E7zsyu5c4PybABJiAwwgI4PIR+es7 mVkt0wQpd1zZAQDGmlk5zosJMAEmwAQcS6CNT3j/bGbtTBMkLYAb2WedmV3DeTEBJsAEnE5AXntc /voMs2ppitPT3LPWdhCQl5hVKc6HCdhNwOsVaN9WQ1amQFaGQEaGhox0gdQUAaEBaali19x0Q6PU j1OdG30S/gDQ0CDh9wPVtQFUVUlUVgdQXRNATa20u2lcPhMwk0DnLOE5H8BTZmRqiiBpqekTAcnx jszoEc7DUgIkLL3396JvrxT03T8FPXK8yNnXgy77eJTUo7pG4tdSH9Zv9OGXDT78vK4Rq39uRFV1 QEl5nCkTUE1AAtcD8mlAxP1rK24DhNxc6fV0LfsZwH6qG875M4F4CXTqoOHQAWk4dGAqBv0hFQf0 NOU3WbzV0gXqu58asXJ1A75YWY/NW/xx58kZMAGrCAjg1KKZOQviLS/ub6O3a9mZksUo3n7g+xUR SEkROKR/Kv54eJr+v3t23I+8kpr27O4F/R9+StN0/MZNPnzxTQM++7oeK76pR31D3D8+ldSbM2UC REBKXAUgbkGKe4SUl1+6FAK53C1MwCkEaK3n2CPSMfiEdBx/VLpTqhVXPZZ9WocPP6vDxyvq9fWo uDLjm5mA+QQCwu/pVzRr3zXxZB2XIOXllw2AkCXxVIDvZQJmEKCR0PFHpWHw8Rk48ZjEEKGWuJAw LXq/Fp98WY/GRh45tcSJj1tLQEDcXzQz+2/xlBqfIBVsfBQQptqhx9MYvjf5CPQ5IAUjB2fgrFOz kq/xAN4qqsE7i2vww5rGpGw/N9pRBLZ2kuXdCwsHNsRaq5gFadSo0syaDGwE0CHWwvk+JhALAbKM yzsxA6OGZ6Jfr5RYski4e0iQ5i2qwdIPa1FXz6OmhOtglzRISHleUWH3V2OtbsyCNLRg46US4plY C+b7mEC0BPbt4sGoYZkYdybvMAjHbtbb1Zi7qAYby3zhLuNzTMB8AhIfLC7MoWjhMaWYBElKeeCM udULXp1TeQDtq+DEBFQSGHRQKkYPy8SQE03bEK6yuo7J+73ldZi7sBpfr4p5BsUxbeGKuIOApkE+ PLnTjQMOSv93LDWOWpBIjAB8T4XNW1iDp1/bCRalWNDzPa0ROO7IdH1a7pjD0lq7lM+HIfDNqgZ9 xFT8UW2Yq/gUE4iPAE2fP35fZyOTvwsh/mV8iPQ1FkG6F8AuSwoWpUhR83WREsg9PkMfER0yIDXS W/i6CAjQxtt5C6t1Cz3JExsREONLIiXQe78UPDlllxgZt3mFEFHt8I5KkKSU5E9lr4lpFiWDP7/G Q+CUY2lElIXDBrIQxcMxknsfeqoCbxfVRHIpX8MEwhJoQYzontOEEPPD3tzsZLSCdCqAkAWQKP3v 5Z1s4dMMMH9snQBNyZHFHE3RcbKOwKofGnTLPNrTxIkJxEIgjBhRdjOFEFGFJIpWkF4EcEFLFSdR eviZipZO83EmsAeBA3unYPTwTJw6OHOP4/zBWgLkmohMxskbBCcmECkB8gP59INdWru8vRBiZ2sX GecjFiQpJf3VqDZubOmVRaklMnzcINCxvaabbp9zuns3s1IYCWMdJjNDoLau6XNKCnaFojDa65ZX GimRVd53P/ImW7f0mV31pC0Y40a30Wc2WqnDJUKI51u5ZtfpaASJhl7Td90Z5g2LUhg4SXxKCGD8 WW1w6bi2jqZAISHWl/qwcZMf5OR0yzY/tu8IoKIygIqdgV1CFK4RXg/QsYMHnTtq6NTRg25dPdi/ hxf75XhBZuxOTrSPqfCtamzdHtV6tJObxHUzkUAUYkSlLhJCDI+0+GgE6U0AoyPNmEUpUlLJcR15 Vrj1Guc59aA/urRP59vVDbr7nTW/+pT7hyNh7pHtxYADU3BQ31TdiKNnjvO8kD8/sxKvzK6KSICT 4ynmVkYpRgawbCHEJuNDuNeIBElK2RHA9nAZhTrHohSKSnIdoz+8Z52a6Shfcx9/UY9PvqjDFysb 9BGQE3qka2cPjjg4DUcflgayNnRSuu/RHSj6gA0fnNQndtQlRjGiql4jhHg0kjpHKkgUnvzZSDJs fg2LUnMiyfP5vDFtcMlYZ0zP0R/UpR/V4qtvGxwfW4g8lx91SBpOOCYNp+Y6w+CDnLjSd/nnX3h9 KXm+wbtb2r6thosL2kayZrT7pt3vioUQg3d/bPldpIL0NtmUt5xN+DMsSuH5JNpZp3hYIKuxhe/V 6kHu3BqmgcSJggtS4D4nxHZ66fUqvPh6JU/jJdqXNkx7sjIFJo5vF6sYGTnvK4T4zfjQ0murgiSl bA9gR0sZRHqcRSlSUu69zuMBLhvXDgWj7bOeW7fBh/lLavQpJjJASKTUrq2GYSdl4KqL2tnerH8+ VI73PmYzcds7QnEFTBIjquWVQognW6tuJII0HkDM7sSDK8CiFEwjsd4POzkDt/zZPqMFGg3NXViD L1bWJxbYEK0ho4hDB6TqXi3sXG+a/U41XplTpVsehqgmH3I5ARPFiEgsFEKMaA1JJII0E0B+axlF ep5FKVJS7riOwoVfPLYtzjnNnlHRzHnVmLeoGmWbk9NEOWdfjy5M+WfYw5+e0keeqdB/DLjjieVa RkKA9tZdPiHuabrmRXUQQoT1nNCqIAUCcqYQ5gkS1ZBFqXk/ufOznaOiFwsrMXt+DaqqE2taLtYn gf6AnD0yyzYjkjcX1GD2/GqOwRRrBzrsvusuax/vmlGoFh0phPgi1AnjWKuCNOutquvHnJ71kHGD Wa8sSmaRtD4fWiuiX0/n2vCr/IXCSsycW+14Sznre6WpRK9XYMxpmbjiPHvWmZ54aSdef6tVhy52 4eFyIyCgSIyoZE0IEdbPPHnvDpu2+f5y7uqfGo/PO8nc4Gh/6JOCju09+OTLxJ/zDwvYZSfJ0uvZ aV0w4EBrvQ2QEP3ffeX63iF/cs7ORfSkBAJAyfeN+tpOba3EkYdYG0vqqEPT0KG9B6Wb/dhZyaPX iDrNQRepEqM/3bK14sWnMqesWnVnfILUa9Ckh0s3+Tuv/qkRLEoOenJsqArtKbpuIhldWpdmzK3C P6aU47Ov6sFCFDl38rNX8kMjXn2jGo0+icMHWSdM9GPzrFOzUF0r2S9e5F1m+5WqxOiav28lLyjp dahatGbV1F/DNTTsCGnI2M19hJR3UQbk14tFKRzKxD3X94AUfVOclc5Q58yvxgOPV2Dph3U8PRfH o0UjppXfNWDGvGpoAjjYQj96Rx+ahvbtPPh5nU93PhtHM/hWxQRUidH1d2zDqh9+30wtsGltydQl 4ZoSVpB6DbxxrADOMDIgUfplgw+nHMfTdwaTRH+lX7p33dwR/XqnWNLU5Svq8PgLO/HGuzU85WMi cRpdfvltAxYva9o71L+fNVOuB/VNAVkA/rYtgJ/WsZcHE7vUtKyuubQ9Ro8w3yMIiRH5iAxKaWtL pj4d9Hmvt2GNGobkl84SAmOa30V7H26/gdzbmZvY0MFcnvHmRhswrTTnfvQ5EiJnLoiTFdthA9NA 01H7dfcip5sHbTI1tMnSdMwNjVIfBZBn8A2lPtAG3a9K6rH2170CLMfbLabcT/uYRg/PwinHWec3 j4wdyOiBk3MIkBidaY0YUaP9XunvuqCwZ4t+UVsUpPx86dkuyrYCCLnbUZUokfnof54Na6runN5M 0JrQegMFzjvpj9b8saJ1ohdmVoH+qDsppaUKDD4hA8NPzsAhA2IbUWwvD+CDT+vwzmJn+oEbNSzT 0nXBjz6v04MB0pogJ3sJqBKjG+7YhpV7jox2N1SK/MWF2a/vPrDnuxYFKS9/w3EQ2kd7Xr7nJxal PXkkwieaXrnyAmtMht9bTt4VqvXwD05iRzvUzz29DS44t42p1fr863p9Ayn9UXZS0jToa4QTzja3 veHa+NyMptAW4a7hc+oIqBKj2+7djk/D/dgQeHLxjJwrW2pZi2tIvQbedJEQGNLSjXSc1pNUrCnR vDMthn7KJuHh8Jt+jqboLjjXGu/cT76yU18r2rzFOTbcNCKiP8r33baP7prHbMA53bz6iOsPfVJR XRvAxjJntJ0s8mh9icJy0B6zvr3UrxfSKJy+4zSlSdF3OVlH4M8Xt1MSDqZVMWpqYoe1JVMfaam1 LY6QhuaXLpACEUX645FSS3jdcZz2FNHUDXleUJ30Hf3vVDsmDhG1lzaTjh1tvZcDGimR/z0aOTkp 0ZoC/YK2Kt37nx1YvIzjLVnBm8SIPHqYnSIUI71YKf09lhT23BiqDiFHSLm50ivaVP0XQEQT5zxS CoXWHcdGDc/EnZM6os/+6n8VP/x0BV6aVYXKKmdsmKTRwLgz22DaHftYuk/HeDIoSuzQkzJ0I4kd lQE4ZbT4/c+NmPVONVJThCUboGmt0uMR+KpkD4ssAxO/mkTACWJETdEgVqxZNW1lqGaFFKRex151 tJDy6lA3tHSMRaklMs49ftn4tpg4Qf16EVnO0eZWChXuhETesmmt7N93ddajtNpdpwN6pmBEbia6 dfVie7kfW7fbL9iNjdBHbr+W+nGyBRFsDx63vWsAACAASURBVOmfqntuIevE6hqewjP7mVQlRpOn lmP5iihH+EJsXVsy9a1QbQwpSH37T5oAgWGhbgh3jEUpHB3nnOvYXsOV57eDFRtdyWKSgrrV1tn/ R4aEiPZVPfqvziAXN05LfQ5IwWl5mejU0YMt2wIor7BfmNat9+mjWlpfG/SHiCZMYsZKJvW0zWB9 qR9ULidzCKgSI4qJ9cEnMRnoZK0tmfpYqNaFXEPKKyibC8hRoW6I5BivKUVCyZ5rjjksDffc2kl5 4bQ2MnNuFTY5wGiBhIj+0N9wuXXrImYApvW2eQur9T1NZuQXbx4nHJ2OO28yf/9hqHqRN/cXX68K dYqPRUHgTxeocYIcb4DGVCm6zi/M3tK8KSFHSL0GTnpYADGvfPFIqTlmZ3ymxcxbrwm5rczUCj79 WiWeea0SVQ6YeqHQ3/+7vwsorLrbElmbjh6RpW++JWG321np+tKm0VJ6qoaBikdLhw5MA0XI/ea7 BvicYYzotscHThUjAtkoRPG6kqk/Noe6lyANz9/USwp5W/MLo/3MohQtMbXXTxzfFpeOU2vSvWRZ LR5+Zifo1e405MQMkH8uFRZFVreN3PzQVGN6uoYNZfavsaxYWY9fNvpwyrFqrTIP6puqm+GvXN2I Tb+xKkXz3CkTo3+bE7peCPnz2pJpxc3btJcgHXDwDacC4tzmF8bymUUpFmrm3pOWJnD1Re2Vxy4i bwsPP70Tv2219w8HucK59rL2utFC1857Pd7mwrU4NxqV0BoLWb+tXe9DXb1963L03SaHrRlpAqr9 4tEot6IyALL+49Q6AVViNOVxMs+Pac0oRKVFw9qSqS81P7HXN7b3oJsmAji2+YWxfmZRipVc/PfR lM+F57bF6UPNd5wYXLsHn6jQg+YFH7P6PcVposXb8We1Qbcuez3WVldHaXmDDkpFwag2ujB9v6YR ZBFnRyKHrZ99XY/ynQEce4TaKdE/Hp6u7xejDbycWiYwcUJbFIw23+MGidGCYlNnPjpflP/gA8XF e8ZH2suoIa9g43JAmCZIBjo2dDBIWPOad2KG8vUicoY7Y16VrdMpZC1HfvdIkJI1vTK7Cq+9UWXr iClnXw/yz2ijIuz1Ht1Khh5Pv7rTEVabe1TMAR9IjGhfndlJgRjpVfT40X/hrJzVwfXd46ckOVSt FVX/BmD6LkkeKQVjV/ueHkrVgfTojyB5bq6qtmfaiJydXj6haR8VbTBN5kR7eMjlEfXEdz82gmIg WZ0qq6Ue/ZnqQF7RVSUa9Xfr6sGOioDt08Oq2hhLvm4TI72NQi5vvkF2D0HKOfjKgwBcHwuQSO5h UYqEUnzXkPHChflqjRfuf2yHvpM/vprGdnf/vim4dFw7XH1RO9CGUjsTTR9R8Luff/FhQ5kfASn1 zZ121YmE4PwxbdDogx5M0w5h+mZVA35c68OQE9QZPPTaLwWnDs7E9h0BikRqF27HlKtKjMizyjtL TJ2m24OZEFi7pmTaouCDe0zZ5Y0tPQ8SLwdfoOL9iNwM3HyV+ebHyR66QtUGOOMZoCm619+2xw9d v14p+nTQaUPUrocZbW3p1fDYTYEEySlp89Q2S9MNSM4bY/7USfOyWvtMDmxnvV1tS+j3lBShj2DH nBbz7pHWmqeff3lWFZ6fWRnRtYl4kSoxeuSZCt3PokpmUorFSwqzhwaXsccIqffASRcBOD74AhXv KaTx5q1+0EY7M1Oyegnvso8Hl09ohzNPVfflnzm3Gv99YaflfugO6OnVR3yTrmwPEiW7Erk9euqV nfr+KtqP01KimE7kk418wdEIhabT7EpHHpKGC85pq6+30FSelYnaTgYPZAlI9VCVaOqW9iuFDXmg qnCb8724oK0+VWt2NawQI6qzEGi/tmTqA8H133OEVFBKw6c9FCv4YrPf80gpfqL0B2/a5H3izyhM DnZEcqV1IfJArvoXdphm66dKvm/QfynG6o2a3DSRqfa4s+wfMVE/vrmgOuTIrjUO8Zy3IuAjjd4L 36pC6WZ7tx3Ewymae0mMzj/H/GfKKjEy2iok9i8qzPl112fjDb3mFWwsBUR28DHV71WJ0pz51Xjs +cQOl2yFJd3N/9ymx8pR/RwY+dOUF5luF4xWN9ozymrt9YH/7sDC98yZQ9+3iwdnjshCwSj72/XQ UxV4u6imteabep6CAF51oZrQB8EVjSYMQvB9bnqfKGKkM5cYubgw512D/64puxMn/NLRG/DcbZyw 6lXV9B1t1mvbRkOihkomx6g3XKHONxv5orvlX9vx68aWp6fMfkbox8kjd3dW7pamtXrTH2zyTk7P plmJPFiv+KYeRR/U6lN5FIPKrkRulAb8IVWfygs3/Whm/Wi9jb6LZHBxxMHqpvDyTsrAth0B/Jig xg6qxOh/L+3EnPnW/kjRny8hvlpbMnW58aztEqQDD/rrURC41Dhh5SuLUnS06aGk0BGqEnldePyF naD1EKsSmS3/5RJ1AhtJO2hK6//u367UcovMo8kwYulHTUYRqr0ctNTunH29GHx8Bsj4wMrNpt+u brJKpLJVJRJcenLJ4i+REhnKXKTAgpbEqPCtaptQiQ1rS6bONQrfJUi9Dp40EsAZxgmrX1mUIiNO bkFUrkfQw0lB9KxMNNq74jz1cZlaahO1+dZ7t+um0i1dY/ZxcpRKI4YPP6unxV0c2Nseg42DD0rV nbeSAYJViUZlbxXVIC1NA4WcUJHIBD4zQ8Pn31jXLhXtMPIkMbpkrPk/Qu0VI2qdrF9bMu0Zo527 BKn3oEkXmOkyyCggmlcWpfC0/nJJO6WL/HdOK8f8peasmYRvye6zfQ9IsSykwe5Sm96RR3ISom+/ t9YCLbgeFPPo4y/qdXGiUOoUE8nqRKO01FSBL1ZaN6Kg+FiffFkP8rWoKs4STYt2bO9ByQ+Nlo72 ze6/xBUjIiXarS2Zep/BbJcg9Rk46RoA/YwTdr2yKO1Nnhb6aUGYFsVVJLJQuu/RHVi52ro/SEY7 aPqxn8Wjg+dmkBCV61M6ofYSGXWz8pWixH74WR2+WtUACoZn9aZf8o9nbFy3st0kghSm5OjD1Kwr 0QisXRtND3hIG2ndllSJ0bPTKzFjrl3TdHv0QlrvQ/76xNpvH9Qrs0uQeg2c9A8Anfe41KYPLEq7 we/X3YsJZ6nzETb7nWrQ2okdsXbItFvFBund9PZ890JhJf7vvnJ9zcQOLwZ71ib0p81b/Hj/kzpQ yIWMdIH9e1jnFql9O49pVoWhWxf6KO2RIiexZDWqItEPnjOGZeplbCxzj1m4KjGiH2SvzrF2Wj5c v2oy8Maakmnr6RpdkJp82FVOBcQugQqXgRXnWJSAwwel4rF7OiubZ6fQ4k+/at8ud/rCDein3tqM /O7d/sB2fP51gy1eC2L5vlD8n/eW12HVj43IzBCwwl8f+Yhb9UOjLXt5SCjeXlyL9FSh7Hknwftt WwA/rbNvijbSZyH/jCxMnGD+uiqJEX0fHJWE9t6akqlfU510Aeox8KoDpMCNjqokoJvdqvDo4AaT 8NzjM3D3X9WFGievC9PftPfBJFdH7dtqyh676W9UYfK0cny8ot62EA3xNo42epJF3k/rfPo2hpxu akdM6ekCxcvNinkTXeuNdSUKRKgqIi15hSfvEbSu5NREYnTlBUkiRmTWAJSsLZm6lPpDf7r98PcF 9nDa4Ji+MmJwmD21Y0QSdeLmWfJQoNJb9z8p6qNNf3SMB8vrgbJf/TPnVYOmIrdud8/0jMGlpdeP Pq8D/acfKqOHZYJc5qhIJx6Trlv92bm29uTLO1G+w6/kjzIxu+L8droFnhN94CWbGFF/aEAf41nW BUlqYn9h3ZYTo+yIX5NJlFQ9kAbsa2/fhlU/WG+8YJRvvHbsYP7sMDkSJQ8dm7YkjhAZvIzX4o9q Qf8piuqo4Vkg7+dmp306emwXc9oXQ/14x40dzW6enh+53UlPE3oIFSUFxJCpqu8+OaB13DRdEB8p sb/xUf+r0HvQjWcC4hTjoBNfk2FNidZUVMwbU3+SJR25VdlQZp73gXieE/pjkD/KHF9cNBp64L8V IH9zZLGVDIlCXsxfUqOviZgdnHDRB7Uod4BFGnkJ+fDzemiK9mmRWTgZcnz6pf17lc4ckYmrLzZ/ Y7grvKELYG3J1Ifoe6uPkITUekp9b7Ozv8qJPFJS5RKEetSJfv0aTJjCp82VJLQ//2JCZs5+9Fus 3btLa7CguAan5WXihsvN/4PWYsEWnfh5XaPuZd7nB+iPttmJ8iTBe+H1Sj3on9n5R5IfRTy+5lLz +84VYtQEKCc3V3qLi4VPHyEdMOCma4RA70jg2X1NIo6UKKYJuc5RkWio/uQr9lnStdQm2ogZT7hl cm1EFkO0sZQTdN9t5GFjZ6XEMYfHt6eHhN4JIySjX/1+6KMYVcYOtFcpI03Tpwgrdlr7PJEYXXtZ UosRdbOW0rby6Z+/nbZTN3ESQvY0Ot8NrzRSomiGZicydCDLLyuT7groTDVi9NQrlfofbSvbw2XZ R4AMEd54txpTHt9hXyUUlkzGDuRdQ0UaNTwTz0ztAnKlZFVSJUZkPetEg41wXGUA+9F5w+a2R7iL nXhu3qIaUOwOs5OVokTid+4Zarwv/PupCpCTVE5MIJEIvPZGFR58wvzvvcHooTv3gdlrckbewa8q xcjOvYXBbYzmfUBqetgjbdSoUpqYNX9yNpraxHgthUhwqyjRnLFheh5j81u87Z8PlevOK1u8gE8w ARcToHUzitOlKt11c0cMO1mN1wiqM4VZUTFNRyMjN4oRMZEIdKNXrTrTY2lAPrMfIpWiRNNpZidy B3PdZe2VLNBSXW+6axve+9iejY1ms+L8mEBLBChkxgXX/qYbtbR0TTzHb/lzB9AoxuykKiCpm8WI GAsNXejVK4Xs7OQ9SJE8ECRKlMz+1WFMpz3xkjmRZ7MyBSaObwearzY7kbXZ7Hersd7CgHpmt4Hz YwLRECjb7Mdjz1cgINVY4NHfE/JcMdMkJ6QsRmF6V4ocOuvVAgFXj5CMJjpdlLp29mD8mWqcpJIY Pf3aTlBUUk5MIF4CNbXWWprFU18yB//PsxXw+SQorpbZieJ0kfd18vsYT1IlRq+/Ve3aabpmPHXH 3l4BdEmUP2NOFaVuXTwYO1qNGJFVFXnr5sQEzCIg3aNHu5r8+Is7dR91tLnc7ERRWj2aiNlyjdw9 me36jNpIYmTW7I3ZzKLPT+xL93ghRQe4fc4uqPVOE6Xe+6XgySlqonok1gMZ1In8lgnEQID2pdXU Slx+nvmRVcnVkNeLqEcjpxybjr9f1yGG1oS/JfG++1KHpEnR9CZ88911lkRJhfUdrSlFY+jQr5c6 MaJFzMT5deSu54tr61wCtNVh2pNqzMJpI3c0338So9tvMN8XX+KJkf486aA0IPEEiZpntyhRBM7H 71MzMiKXIG4173TunzKuWaIQeGdxDf4xpVxJcyL9UapKjMgNWIL+EG0aIQkI8yVcyaMQfaZ2iRIF 1vv3nftEX+EI7qBpCbftwo6gWXwJEzCVAIXquO4favYqkSiF8+hysqKRkRN9UprYaem5F69NJ08N 5jtSMrGW8WZltSgdc1gaptyuRoz+99JOR7uRj7ev+H4mYCaBku8bcOmNW5TsVaJN7aEcotL3/x8K pukSXIyaur06q4NXAubv/jTzqTIhLxIlSqr3KdHDeM+taqK8PvxMhZIvlgl4OQsm4FgCFMLi+cIm /3dm7/8zPIXTd5OSqu9/UogRgFQEMryQSHdosFhTH3LVovTFynplYnT/Yzuw6P1aU3lwZkwgWQiQ B+9HnlWzgdYQueUr6pR8/5NFjOhZDABtvBCIz1e9i55qlaJkeHUwG8fkqeVY9im7AjKbK+eXXATI EzptoA0EpOk+JEmUDGEyk+qbC2rw2PPJs8fQ70EmBehL6DWk5g+IKlFqXo4ZnynC66df2R/N0oy2 cB5MwAkE6A88xVdS9QPSrDaSGJGAJlMSgUAaCVLSjJCMznWDKN04eRu++a7BqDK/MgEmYBIBMpuu b5BQ4dXBjComoxgRN02KtmRll24GRLflQaLk1OHw9XewGLnteeL6uosAbZ+g/XxOS8kqRkY/0Agp aRMtGHo9wJUKwkzECvWKm7diza+Nsd7O9zEBJhAhAdrP1+iTuGSs+a6GIqzCHpeRk+Rkm6YLBiAR aJPUgkQwCt+q1pnYLUr0ML65sBrr1vuC+4jfMwEmoJDAK7Or0NAgbf9RSt9/w3xcYXMdnbUAPCRI zvh5YCMqu0WJHsbpc6uweYvfRgpcNBNITgL0/acwFuG8L6gkw2K0my4JEq0jJX2yS5ToYXx5dhW2 lbMYJf1DyABsI0DT936/NH3zfGsNYjHak1DST9kF47BalOhh5MB6wT3A75mAfQTI0Ims71TELgrV Khajvanw6KgZExIl8hmnOrEYqSbM+TOB6AksKK7FPx9S4yk8uDb0/f/fy+r/zgSX6Yb3JEgujA+p Fq1qUWIxUtt/nDsTiIfAex/XKRUl4/tfV58osbrjob3nvSRITZ4H9zye1J+yMgVy9lU3m0luRkYO yUxqxtx4JuBkAocMUOcvgL7/+WeYH2rdyTwjrZu6v7qR1sBh15EYTRzfTolvquCmUuRJEWR2Hnwu Gd5npFPrOTEB5xG47rL2yr//FBKdEsc2293/EvCzIO3mAavEyCiS9j4JAcyc17QXyjieDK8eXr1M hm52VRszMwQun6D+x6gBhUXJINH0KqBV0Z8FdiUNWC5GRldccX47x/rUMurIr0wg0Ql0bK9ZKkYG TxKliwuSfiuogQM0Qkp6d9JWj4x20f/9DbkuSfEKHr43BxPmcy0vCIehw6eiIdCtiwdjR7dRPk3X Up14pNREJiBkJQlScvk4b/ZU0DDdijWjZsXu9ZEeSq8XePpVtjHZC06IA7SJkRMTiJdA7/1S8OSU zvFmE/f99P0nv3rkyihZk9S0eg0yuUdIVs4Zt/agjTuzDSZO4OF7a5z4PBMwg0D/fs4QI6MtNFPi 1JAYRh1Vvnr8qNEgkncNyQprmmg7kESJLPA4MQG7CGRkJL4F5CEDUvGfu+0fGTXv42QWJQ2o0gSQ lNuFnShGxsNJ0SztcvRo1IFf3UugPs64jhr9VUjgdPxR6Zh2xz6ObWGyilIDtNqkXENyshgZ35Kz R2aB/jAkc3wUgwW/RkegsZHX11oiNuzkDNzy5w4tnXbMcRIlSkm1ppRVvUOTkOodNzmmmwFVYvT6 73GVzGzqmSMy9fqmpyX2L1YzmXFeTKAlAqOHZyoTI4pAa3YiUco/I8vsbJ2aX13x873qvIDY4dQa ml0vVWJ0273b8elXTdbzNN1mZiI3I5QK36pC6WYOUWEmW84reQgUjMoC7flTkcgZK/m/o2SMbMwq xwgcakQiMCtfB+aj65AmZHIIkhVi9MRLO6FipESi9OIjXXFQ3xQHPkdcJSbgbAIXntvGEjGi6bWX Z5lvtk2ilAQjJX2mToOQCT9CskKMjK8kiZIqV0CP/qszjj5MndNHow38ygQShQBto7gwX81Witsf 2D0yMniRbzoWJYNGNK9NAyNNAluiuc1t11opRgabJ1/eielvmv9LifK/99ZOGHJChlEUvzIBJtAC Ado+QdsoVKS/3bMdy1eE9rqmUpTMXhJQwSa2POVmuk8LaFpZbBk4/y47xMigQh4XVPxSovxvu7YD Rg3j8BUGa35lAs0JXHNpe6j64339Hdvw+dfhPa6pEiUSWTLOSMC0ldrkFVLobxKtgXaKkcGSHkpy B2L2Qiflf93E9qANjDPnJp+ncIMvvzKB5gQ6ddBwwTltlfmlu/KWrfh5XWPzYkN+pu8/JcNXXciL Yjh47WXt9bso5HrCJCFLqS3erBp/WU2CzQA5QYyMB4UWOn0+4PLzzJ/HvuK8dsjK0KDC5NSoP78y AbcQ+EOfFDx2jxrvCxTldfa71Vi/0RcVDhIl8lFp9tRhoomSDDQtHWnz5uWQzCaM1DpJjIwnd8bc KvzvJTUOMcj31VUXqjFnNerPr0zA6QTI+4JKMZo+typqMTKY0fS9ijVlEqVEmb4T0DYRLyNM2gYD nptfad7Y2LdjZjuC9xnFmi/tI3jkGTWO1c85PQvU9pQU3kAba//wfe4lcFpeJu66uaOSBtDI6OnX dmLzlvj2AKoUpTOGun9NSRMB3ZZBjxgrpVgvhDxQSY9alCn9QSbPBmanux4q37XpNd68ac63vkHi 5qvMd11CbScXZLPnV2N9aXTTCvG2i+9nAnYRoDhGKqbDqT1vLqjBo89VQJrkickILWP29N31l7fX 16oXFNfa1Q1xlys0/EqZ6CMkoQXWx52jjRmoEiPagf3+7zuwzWoePTQkcioSjQ6fe6gLDh/Ee5VU 8OU8nUWAjIVUidGc+dW6H0mzxMggp2qkRD9yR+S61hjA37ApRx8hNU3ZSbhWkFSKkeEOxHiYzHol kbv5n9vNym6vfKbc3glDT3Ltw7lXe/gAE2hO4OqL2imLHUTeVh57Xs2aL7WDREmFRxcXi1JpcbHQ p3V0QZJC6MOl5p3u9M9uFCOD6Zff1uNPt2zFO4vV2JP87S8dcM5p5vrVM+ruhFdaxD7qEB4JOqEv rKxD504e3eHwGEXP9qtzqkDeVlQnVW7GXClKQQMifQ0JkL8C7loQd7MYGQ/7T+sa8eLrVfD7ocQY 46qL2qFDew3PvGa+J2KjDXa9nnB0Ouh/8Ue1mLuoBt+sijMIkF0NMbFcjwf6jxBVTkRNrGpMWR06 IBVTFcYxou0TVoZ7MITP7A28JEqNPmDJMpesKQUNiHRB8gY8P/pFIKaHxI6bEkGMDG5bt/vx3xeb fpGpsBAcf1YbZKQLPPqc+l99RpusfM09PgP0f+F7tZi3sBrf/RTZpkUr66i6LCEAGjEksvk/rY/Q H1pVibZl2OFRW5Uo3XZNB/h9cpcXclXczMlXrjHy8dCbIwZOqawVlbcCQv9snHTiayKJkcE3EAA+ +bIebdto6N8v1Ths2utBfVPRsb0Hm7b4UbHTGT88AhIgsTQr9TkgBWT+26mjB1u2BVBe4Yx2mtW+ UPmQEJ11ahZ0p7uHmjd9+eaCauxwyHNC7R53Vhtcc0mTd4JQHOI99u+nKvDGu2qmziOp2+ff1CMz Q8OAA8397p9yXAZ+2eDT/0dSD7uuEZDPrymZ9iWVrwvQqlV3yt4DbzofgHPj+gL6XhsVpt3B8Uzs 6hQq97Ov1DyYlDftYj9zRBbW/OrDr1HuNlfBhKYUzj2jjel7pw7snaL7+WvXVmsS4MrEFKbTh2bi v/d2xjGHmydERj/TNHJdvUm2zkamMb6SN5ILzjXvh0vzakyeWo6iD+yf2iJRUvGD1BWipImpa7+d qtsx7BoR9Rk4aSSAfs07zCmfE3FkFIotPZgej8Ah/c39tWSURdNbldUSqx0wtdW/Xwp65vy+jGlU 0KRXGhWSAGdladj0mx87qxJDmIafkoHrr1DnXLeqOoDnZ6rxVB9N12ZmCPzpwvagTd+q0rW3b8OK b8I7SVVVdqh86QdpMoqSP+C9bd2qKfpDt0uQeg2adAyAY0OBsvtYsoiRwfmrkgb9F+qRiqzIjjks TRc9KsfOJCFw8rHpSqswoF+qPq2Vnq5hQ5kP1TXO+OUfbaPph8S1l7bX14q6dNr1tY02m1avX/5F vel771ottNkFBx+UiosK2mLkYPM3ulNR5H2BRkY0neW0pFKUflzr078DDmvzzqWF3f5u1GnXk917 0KT9AZxhnHDKa7KJkcG95IdG/LY1gOOPVvMHm0ZgNK21YmW9aTvRjbpH+kpThxeca77T2VDlD/xD qm6BlpoisHa9zzFTUqHqGnzsxGPS8eeLm+L67Ntl19c1+BJT39Pifumm+NzkxFMhcoMzeVJH9NpP TXRk8r7wyLMVqKl17g8TVaJEcdRoZmSjjf0b4tn4Zm3J1KeM47ue8D4DJmVA4FLjhBNek1WMDPZk Fk4PUJ6iTa40rUW/trduD2BbufVTWrQLnkYsVkbBHXRQKgpGNa1d/bCmEY0ONcr74xFpuPrC9nro gpx91UxrGs+Z8frdj436pk3js9Wv5HlBpck6bUb97wvusDZVJUr0t8RhorR4bcnUN4xnbZcg9Rt4 fVVAaLcYJ+x+/csl7fSpFrPr4RQDhkjbRb9mPvi0DgJCN0yI9L5Ir+vbKwW0QL55qx8/r7N+CoP+ CLZv58FBfdX8Im6JA00LkZUfrddRHWgvmBPSUYem4U8XtsPFBW3RI9saITLa/ez0Sqz5xfpngJwC k+cF+qGgKlGwzKddth+PREnFd8NJoiQhX1tbMu1Do993CdLPqx6q7T3wxisBYc0cilGDEK80RXH2 SPMXM+9/bAeWfBg67HCIajjm0I6KAFaubkCbTE2JKFFDaZMppa9t2GBK0Tc7tPMoa1u4jqSpSwrh QRM4q35oBJng25EOGZCKyye0xcQJ7ZQZeoRr18x51SicZ32wx4EHpuLisW11k/1w9Yvn3JMv78Qr c+w31IilDZ9+mdiipEnt32tWTf3RYLNLkOhArwE3jxQCvY2TdryqEqMpj+/QN0/a0SYzyiQzaZV7 laiOhw5M09eVSJSsHDHQ1N2n9GuwrfUjJaNvDhuYpk+PEWea0rBKmPr3TcGl49rpI4QDelo7SjTa To5En/h9c7ZxzIrXkUOawkb03l9du+/9zw68VWTfHiMzOKoUpa9WNcQdWiOeNgrg1jWrpu6Ky7OH IPUeNOkQAMfHU0A896oUIze7Zg9mSsN4r1fgYEVm4bSuRCOGb75r0PfxBJet+j198eoaJFRZF0ZS /yMObhKm2jqJ739uVGbw0Wf/FN2SjEIH0KZeuxK5ynnyFetdS106ri2uPF9tYMnrbt+m/4izi62Z 5aoSpRG5mfji2wb8ttWWOettiwtzCjVkfQAAIABJREFU/i+Y056CNHBSVwBjgi+w6j2LUeSkv/y2 AVWKjQGGn5Jpy36lku8b8frb1boQqBLdSEjTWs4F57RFTZ3U15giuSeSa2jf1QXntMHNV3cAbeK1 Ky1fUYfHX9iJtxU5922pXZ06aLj8vHYw239bcHlk1v23e7Y70cQ5uJpRv1clSqcOtkeUpBTL166a +mIwiD0Eqc+gSWRz9OfgC6x4z2IUPWVaiCevC7Q/RVWi/UoZGZrlmwdp2oxEd96iGgT8AFnG2ZVI mC48ty12VjWNmGKtR86+Hpx/Tlvcek0HJe6hIq0X+fp7bnqlbk1ntfkvmbBTmHHyGqIqGaEjGhqc a9YdT9tJlMgNmNkM7RAlTcjZa0qmLQrmsYcgHTngwfJaUXUTAHVPTHDppH6KDBhozShRpumaIdv1 kfbxqLTAo4Jo0blrZ3tMw8l9DU0nLHivVl/Tor1EdiVy0UPCtG1HAD+uidxWvMs+TUL09+s7mu6r LFoW0/5XgUeeqcDPNljSnX9OG9D0pMr0YmElnplu/fSjyjaFypvWkhNBlITEY2tWTVsZ3Ma9Yk7k FWxcDghLPDawGAV3RezvyckmsSRHmyoT/TGjMOx2pe7ZXowelqnUnUwkbaNRBnkWX/R+bYtrTB3b a/pGXHIManci56E0NWd29NNI2kWbry/KbwsVPiiDy3/46Qp9RB18LNHfX3dZeyVha668ZSt+Xhf5 j65YOQuBAUUzcr4Lvn9vQRpb+m9IXBd8kYr3LEbmU504vq3uGdn8nHfnSFMihsv83Uetfdeze5Mw qdgaEG1LSJRopEpGEB4N6NhBQ98DUkBTfXYnCjlCnrvtECJqO7mF+scNHZVj+L/7tieM8UK0sFSJ 0hU3b8WaX5WKUsVJA7I7TZ68Z9yjEIK0cRykeC1aMNFcz2IUDa3orqXF4j9doNZ6iUK70wjBbl94 ZKlGMaTI3Qyn3QTI/Y9hGLL7qLXvaGMvTdOpTGS88PLsKmwrt8VCTGXTosrbpaK0aPHMnOHNG7rH GhKd7HPQLTXQpLIREotR8y4w9zNt7ly3wQdyO68qHdDDC7LCa2gEvv3ePgetFPPo4y/q9bAdZApv p/m0KtbR5EuRgW+9dzu+/V7pL9uwVdqvu1f3MpE/Su30MW3kJTdANDJN9kTfQRWb5unH3rJP69XE FhPylbUl04qb991eIyS6YEhB6W8C6NL84ng/sxjFSzDy+7t38+Lc07OUzDEH14KcVU5/swpbttn/ K5W8HdAak0rLw+C2O+X9C4WVeO2Navh89v5xpj9g9GtddbIruqvqdsWTf1amwMTx7ZR835VM3wlt 1OIZ3d5q3ua9Rkh0Qe+BN51EMd2aXxzPZxajeOhFf29lVUCfV8/KND8SZXBtyAcdCd9v2wIgZ7B2 ps1b/Hj/kzqQp3QK206/1hM5kX82GhF9sbLBMs8SoXimpQndKSo5R1Wd/jGlPOGtZ2NhSE6CVY6U Fr5Xq+99jKVuoe5Jlbj+p1VT97KQakGQbuwJiGGhMorlGDlOHHOa+UP4ZDDtjoV38D3kJ67BB5AH ApXp+KPS9TDMX5XUg8KT25nKNvtRvLwOZA1Hgd5UBQG0q43kXeH2B7brU5VWungK1V4ajf7v/s6g uFMqE60X3fVQue7WSWU5bs5bpSileIW+FGBSPLHvFhbmTAnFOqQg9Rn0Vz8gJ4a6IdpjtMCuIuoj mbLOX2J/6OFoedhx/berG/S9J4MVbqKldg04MFXf/PnLRp8jgp9RXJ+lH9Xhp3U+PRJnTjd3j5im v1GFO6eVY/mKekeEzZg4oa3ug0/1M134VjUee36na4MrquYTnD+JEq0jZ2WY64iZNuKmmidKs9eW TN1ruo7aEVKQ9usyZZOWVTUJQFw/e0iMVLgIof0wtIufU+QE1pf68M6SWqSlqgljEVyTU47NAE3j kLcFJyRqe9EHtfi11I8O7TRYEejOzHbTAv49/9mBDz6pc0RgwWOPSMNVF7XHqbnqrRtpi8FLr7vT U7eZz0A0eTU0Sn00QwJipkcHs0RJSPlQ8w2xRvtCCtK6dXcG+gyYdAoE+hgXRvuqUozs3JwZLQcn XU9RMmmXN8WfoXhAKtOgP6Tqng0oTLRTQkWvW+/TvT5s2uJHp44ePTihSgbx5j3r7Wrc/98KFH9U 65jRAe11u3Zie0tiNd3yr+1Y/AHPgsTyHNHUGlnbOlGU/H55/brvpoWMlBhSkAjAAQMn7S8EhsQC g8UoFmrW3UMjF4oSe9xRasKjB7eEzM8zMzR8/Z29C+/BdSLXOfOX1FjGILjsSN6/8W41pjxeoY/q yDjFCYnWCGktmLxDq07U/kl3bQeNbDnFTkC1KJERRdQRlwXWLH29+90ttapFQeo98AYfhLispRtb Os5i1BIZZx3/cW2jbpGmKhJtcGv1taUxbWyLShtcl+D3xOClWbSxMoDjjlQvzsFlh3pPcXumPlGh W5FV7HSGENH+LgoaSBGcaSuB6vTcjErQfiq7jTVUt9Oq/FWKEu19ilqUBApbWj8iJi0K0lEDp5bV iqprAUT8TWUxsuoxM6ecHTubTMPT0zRY4biUotKSb7MNZT49tIU5rYg/F3KWSsK0s1KCnKhancik lox0SJBos69T0ojcDDx+b2dLng1qM5l0v2NxOAynsFZZDxKlX0t9GGNyFG5aU4pWlATEA2tKppa0 1N6QG2ONi/MKSmcDONv4HO6VxSgcHeefOy0vEzdeoX5To0Hi6dcqQVZjTkvkqJb2VV2p2P0StZvW huYuqsE3NoSND8edfAWeOTxTubNeow40RUcjI/rDyUkdgd77peDJKZ1NL4BM8p9+LSIryIBX+rss KOy5vaVKtDhCohv6DLipEwROb+lm4ziLkUHCva80fbXkwzp4NHMtc1oiQvuiaCqPDC2ctlZAZrOv zKlCQ4Oa/VsffV6nmzFPf7Pa1vDRofrmgnPb4I4bO4IiB1uR6IfJs9Mro1+LsKJyCVYGjb7JFRB5 1DAzGSOllasbQLHMwqTPFxX2eCTMeYQdIeXmr+/rEZ4fw2XAYhSOjjvP0Y57CmNuVZozv1p3Bkqe FpyWPB6A9m+dPjQzbstEmpKjX5M//2KvR4tQjIeckKH/oVJtfWmU7RQHvUZ9kulV5Ujp4WcqwqG8 Z/HMPUOWN784rCDRxXkFpd8DOLD5jfSZxSgUlcQ4RtE9J09SHzogmBYFWCPvzQHnLKMEVw8d2ms4 8uA0HH5wKg7okYIe2R60ydL2uMb4QKbltFb2/U+NesTdVT80wOc8vdVDZYwengmasrUq0b6qJ18O afVrVRWSvhw7RElInFRUmLMsHPxIBOlBALRJdo/EYrQHjoT8QPuVaDf+OQrcPoUDZncgwHB1a36O XBNlpGtITxfw+yRq6qTugbqx0dnrISSk487MwrgzrRsJE7up/yMPK7ypvflzZMfnfr1S8Ph9ataU QoyUtneS2V0LC0XYn2Vh15AIUu+BN9H8wkXBwFiMgmkk7nsaqZAvvPKdARx7RMTGlnED+eMR6Ti4 f6rulYCC3zk50Zw5hUCg/UJVNRL1DdKxIzziqGnAhLPb4P7/64RBijdHB/cbGS7c8eAOfPOdM7x3 BNctWd9v3xHAF9824NTB5o6OaU2JQqzTJvygNOutwnazgj6HfNuqIHU64cGN6XW7zb9ZjEJyTOiD P/zciIXv18LjESDv3lak7K5e5B6Xgb69UnQT8dLNYX9YWVElV5dB1oMU4v4/d3fG4YOsNW0n9z/P z6zi2EUOfIJ+2+q3RJSkEHevLZm6qjUErU7ZUQZDC0pnSiCfpm9UDPEp0Nbsd6pbqyufdwABKyLS hmqmU02kQ9XVScdIiGh96IbLrTPpN9pPBhxvLqwGuWzi5GwCNFr+9537mF5Jipf2n2d3NAqZ0aWo sFNYiwcqPCJByhu7cdzE8e1eUyFGHGzL9GdAeYb79/DirBHqg/+FasiyT+t0x7orvtljOiDUpUl9 jIRoxCmZuOkq64WIwD/1SiVmzHXePrOkfihaabwqUVr6Ue36ISdk7tdK8frpiASp9LfGU7O7eOdH kmE017AYRUPLeddSWJGrLmxnS8U+/6ZeN6H+8LM6W8p3aqFkpj5qWJbu6seOOvKoyA7q5pV5+KBU TLnd/JESgH5CiJ9aq2lEgiSlfA3AuNYyi+Y8i1E0tJx7bbcuHj3ECK1P2JHW/NqoC9O7xbVwumWb Sj5tszR9H9Gl49RHbW2pHbRW9PpbPPXeEh+3HD/msDTcc2sns6vbTQixubVMIxWkCwG80FpmkZ5n MYqUlHuus9r1UCgy09+swluLakB7gJIl0X4S2nk/api5llLR8CMLOgqi58SNzdG0g6/dTcBkUSoW QgzenXvL7yIVpA4AylvOJvIzLEaRs3LblakpAhcXtEXBaHtGSwavT76ox4L3an6PrOrs/UBGnaN5 peCHucel47QhmZY5Pm2pfryvqCUy7j9uoihdI4R4NBIiEQkSZSSlfBPA6EgybekaFqOWyCTW8aMO SdN/tZN3b7sTWfkUfVCD1T81QrpYm8hI4ZD+qcg7McNSrwot9R+NRskHnVO9arRUbz4eHQGTRClb CLEpkpKjEaSxAKZHkmmoa1iMQlFJ7GMFo7Jwxfn2GD2EIkvrGxQGfNWPDa4QJxKhAf1SccIx6SCW TkhLljV5KP92NW9wdUJ/WFEH+mF5500xuxFbJIQYHmk9oxEkmqSOacWSxSjS7ki869q20XDe2W10 wwcnta7og1p8+mU9Vqysh1OC4RGfrEyBwwam4Y9HpOlTck5i9uhzO0HrRZySj8Apx6bj9htiEqVL hBDPR0osYkGiDKWULwK4INLM6ToWo2hoJe61hw5IxejhWTjlOPun8ZpTXrfBh5XfNej/v1/TiNJN PstGUB3bayBXKxQgkTwoWOUJozmDcJ9fnVOFFwo5ims4RslwLkZRai+EiNiTbrSCdCqAiPcjsRgl w2MaXRuHnZyBW/5MNjLOTuRzjTwMbNzkQ+kmP0o3+0ARdmMZTZGT2k4dNHTu6EGPHA96ZnuxXw+v 7mm7a+dWvXfZBmreoqZwGWRaz4kJEIEoRalQCFEQDbloBYm+PRH5AWExiqYbkutaWhspGNUGl59n 356ZWIn7/UBFZUDf80SOVBt9TZYSXo8Atcv4nJmu6Y5MMzMFaI+QmxJZKc5dVA165cQEmhOIQpRO F0K80/z+cJ+jEiTKSEp5L4C/hcuUxSgcHT5nEKCRw4Sz2oCilHJyBoEHn6jAu0s5PIQzesO5tYhw psMrRPhwE81bGIsg/QHA6uYZGZ/Zh5VBgl8jJdC+rYb8M7Iw7iwWpkiZmX0dGSy8uaDasrUzs+vP +VlPYERuBm6+qsXp9/uEELdGW6uoBYkKkFJ+AODE5oU9N6MSr8xmh4rNufDnyAjQesroYZksTJHh MuUqms14/W0WIlNgJmEmYUTpICEERRuPKsUqSJcCeCa4JBajYBr8Ph4CZHlGUWp5xBQPxfD3khDN nl8NWhPjxATiIRBClJYJIU6KJc9YBYl26e0aCrEYxYKe72mNAIXZPuvUTN0dUWvX8vnICDz8dAXe XlzDHhYiw8VXRUigmSidJ4R4NcJb97gsJkGiHKSU1wJ4+LHnd2LOfN4stwdV/mAqAa9X4PS8DFxz qT2xfUxtjA2Zfb2qAXMXVuP9j+t4jcgG/slSZJssbdurj+57aVaWmBtrm2MWJCrw7oe3n7j0wzpa T+LEBJQTILPqIw9Jw+jhmTj+KOdtsFUOIMoCFhTXYt6iat2PX5S38uVMIGoCAuL+opnZYS2wW8s0 LkGizPPyS5dCILe1gvg8EzCTABlAnDE0ExPOZsu85lzJ6ek7S2qwoyLQ/BR/ZgKqCASE39OvaNa+ a+IpwARBKjsXQhbGUwm+lwnESsDrAY45PB3DT8nAicck76jpvY/r9FhQX5XU87RcrA8T3xczAQEx t2hm9pkxZ/D7jXELUm6u9Hq6lv0MIKKY6fFWmO9nAi0RoP1Mg0/I0GMFDTootaXLEub4N6sasPjD Wn1tqLKKR0MJ07EubIgATi2ambMg3qrHLUhUgSEFZTcJyCnxVobvZwJmEdinowcn/TEdJx+brscR Mitfu/MhH3sffV6H95bXYcs2ttm2uz+4fJ3AqsUzswcBIu6IY6YIUu5Zazt4UtPWA+AJfX5CHUeA zMePPCQVfzw8HUcflgba5+SmRNNxH6+ow8df1INHQm7queSoqwCuKJqZ85QZrTVFkKgiQwrKHhGQ 15hRKc6DCagk0LO7F4f2T8XB/VP10VOXfZzjcZsct67+sRG0FvTFtw1Y84u7I92q7EfO2xEEttZI /37LC3vWmlEb0wRpeP6mXn4R+AGA14yKcR5MwCoC7dtpOLB3CvockIJ+vVLQM8eD7vt6kZZm2tcj ZFPKNvvx60Yfftnow/c/NYBiMW36jafhQsLig84kIMUdiwuz7zKrcqZ+4/IKSl8GcJ5ZleN8mICd BMhIonMnD9q1FaBpP4p+2yZT6EKV4hWgDbspv//8CgRoszjg+X2wVVsndW8IdQ0S9fUSOysD+nTb zqqAbo69dbsfPtYeO7uXy46fQJVX+vdfUNhze/xZNeVg7mjG438Afs8EAKYKnVmN5XyYQDQEaPqM /nNiAkxgbwISeMpMMaISTF3dXfxaz29EFBFl924iH2ECTIAJMAEXEGjwavIhs+tpqiBR5aSG+8yu JOfHBJgAE2ACziEgIF5ZOL07WVabmkwXpMXTcz4A5BJTa8mZMQEmwASYgFMI+ODX7lZRGdMFiSop Ie5UUVnOkwkwASbABGwmIOTL8fqsa6kFSgRpycyc9wEsbalQPs4EmAATYAKuJODzBwL/UlVzJYJE lZXAZFWV5nyZABNgAkzAFgKvFBf2/ElVycoE6fdR0iJVFed8mQATYAJMwFICDR6pKV2OUSZIhEnT cFvTYMlSaFwYE2ACTIAJmE5APrWwsNta07MNylCpIC2anvM5gFlB5fFbJsAEmAATcB+BGq8vVdna kYFDqSBRIR4/bgfgMwrkVybABJgAE3AXASnwyILZXcpU11q5IC2clbMaAi+obgjnzwSYABNgAkoI bGvwND6gJOdmmSoXJCrP25hCo6TqZmXzRybABJgAE3A4ASlw97JX9y+3opqWCBIN9SQER5S1oke5 DCbABJiAWQQEfqxon/2YWdm1lo8lgkSVyKqlEOeitLUK8XkmwASYABNwBgEZwC0rnhSNVtXGMkGa Ny+nRorA361qGJfDBJgAE2ACsROQwHtLCnPmxJ5D9HdaJkhUtZP757wACTIF58QEmAATYALOJeD3 yMD1VlfP8kB6eWPLjoaUH5sdi8lqcFweE2ACTCBRCUghH18yo/vVVrfP0hESNW7xjOzPADxrdUO5 PCbABJgAE2idgAS2BOobyMuO5clyQaIWpkpBjd1heWu5QCbABJgAEwhPQOC24jd62fL32RZBml+Y vQVC/F94KnyWCTABJsAErCQgpPzk5P7Zts1g2SJIBPik/t2eAPS1JCt5c1lMgAkwASYQmoDPr8kr J08WgdCn1R+13KghuEmDx244VJMaWd15g4/zeybABJgAE7CYgJBTFs/o/leLS92jONtGSFSLpTN6 fA2IaXvUiD8wASbABJiA1QTWeVI9SmMdRdIgWwWJKphZKwmC0hgbkYDga5gAE2ACyUpACnH1wpe6 2e5v1HZBIg8Omha4nAP5JetXgdvNBJiAzQReXjIje77NddCLt12QqBaLpvdYDOBJJwDhOjABJsAE kojAJq/0X+eU9jpCkAhGXV3KzQB+dQoYrgcTYAJMINEJSImrFxT23O6UdjpGkD6c26VSCslTd055 MrgeTIAJJDgBMd1q56mtAXWMIFFFl8zovhCQj7dWaT7PBJgAE2AC8RAQpV7p+3M8Oai411GCRA30 pHnIDv4HFY3lPJkAE2ACTABSQF7qpKk6o08cJ0hkehjQtAsB+IxK8isTYAJMgAmYRUD+t2hmzgKz cjMzH8cJEjVu6fRun0DIe8xsKOfFBJgAE0h2AlJgdWatsNUbQ7g+cKQgUYX9m3P+CYmPwlWezzEB JsAEmEDEBBoAnEd7PyO+w+ILHStIxcXCJ/2YIIByi5lwcUyACTCBxCMg8dclM3K+cHLDHCtIBG3J 7JxfpBRXOBkg140JMAEm4AICby8uzH7E6fV0tCARvMWF2a8L4Amng+T6MQEmwAScSUCU+j24GBDS mfXbXSvHCxJVtVr6b4TEl7urze+YABNgAkwgAgI+aHJc8Ws5WyO41vZLXCFIywt71krNkw+gwnZi XAEmwASYgEsISIhbF0/P+cAl1YUrBIlgLpmx789CyIvYK7hbHi2uJxNgAjYTmLNkZrepNtchquJd I0jUqqIZ3d+EkA9G1UK+mAkwASaQbAQEfhQy/RI3rBsFd42rBIkq3imQc6uUgsJVcGICTIAJMIG9 CVSLgHZOUWEn1y1xuE6QCguFPw0YD4Ff9u4HPsIEmAATSGoCUgAXFRV2W+lGCq4TJII8vzB7SwCB MwE4dsexGx8GrjMTYALuJiCBfxXNzJnl1la4UpAI9tIZPb6WQlzMRg5uffS43kyACZhKQMq3Th6Q fYepeVqcmbC4PNOLyysoux2Qd5meMWfIBJgAE3APgZVCpp/kxnWjYMSuFyRAiryCspfIaWBww/g9 E2ACTCAZCEhgS8AXOKJ4do8Nbm+va6fsdoMXspMsvxSQH+8+xu+YABNgAklBoEZq2qhEECPqrQQY ITU9dCPzy7o0CnwkIfsmxWPIjWQCTCDZCUgJed6Smd1fSxQQCTBCauoKsrzzSd9IGr4mSudwO5gA E2ACLREQErclkhhROxNmhGR02uBxm/6oBQJLAGQax/iVCTABJpBIBKSQjy+Z0f3qRGoTtSVhRkhG x1D484CU4ynorHGMX5kAE2ACCUTglX0COdckUHt2NSXhBIlatrSw+1wJ/IX3KO3qZ37DBJhAQhCQ S8iIizzWJERzmjUi4absgts3NL/0b1Lg3uBj/J4JMAEm4FICnwqZPtzte43CsU/IEZLR4KLCnPsE xP3GZ35lAkyACbiUwKpUKc5IZDGifknoEVLTg6dvnH0cwJUufRC52kyACSQzAYFf/I2BExNlr1G4 rkzoEVJTw2njbPafAbwSDgSfYwJMgAk4kMAGf8A/NBnEiNgnwQip6RHLz5eebaL0VQFR4MCHjqvE BJgAE2hGQJZByNzFM3r80OxEwn5MGkGiHjzyCpnSvrxsuhAYk7A9yg1jAkzA9QRog78mcErRjJzv XN+YKBqQBFN2u2mseFI07oPy8QJi7u6j/I4JMAEm4BwCuhhJLS/ZxIh6IKkEiRpcWDiwoaPcni8l ZjvnEeSaMAEmwAR0ApulRwx2a8TXePswqabsgmHl5kqvZ99NL0PKscHH+T0TYAJMwB4CotTjl3kL Z+Wstqd8+0tNuhGSgby4WPg6BbpRDKWXjWP8ygSYABOwicB6v/SdksxiRNyTVpCo8eR+o5PMpjDo /7PpIeRimQATSHICAuInvyZOLi7s+VOSo0ges+/wHS3FkLFl9wiJv4W/js8yASbABEwkIMU3fnhG FBd23WRirq7NKqlHSLt7TcglM3JuFVL8lR2y7qbC75gAE1BIQOKj+pSGXBaj3YyT1qhhN4I93w0Z WzpRSDwBwLPnGf7EBJgAEzCNwLuZtThn3rycGtNyTICMWJBCdOKQgo2jBASFBc4KcZoPMQEmwARi JiAgnivv0O1K2hcZcyYJeiMLUgsdS5FnRSAwTwBdWriEDzMBJsAEoiQg/7l4Zs4dgJBR3pgUl7Mg henm3Pz1fb3CO19C9g1zGZ9iAkyACbRGwC+Aq4pm5jzV2oXJfJ4FqZXezx1f2tnjxxwAJ7ZyKZ9m AkyACYQiUCGAsUUzcxaEOsnHdhNgK7vdLEK+K34tZ2tqZfVQ3kAbEg8fZAJMIDyBtQGPOIHFKDwk 4yyPkAwSEbzmFZTeCuBfyRS2IwIsfAkTYAIhCYjlnkZx1sI53X4LeZoP7kWABWkvJOEPDMkvPVsI vAigTfgr+SwTYAJJS0DI51N31vxp/vx+9UnLIIaGsyDFAG342E2DAlLOYWOHGODxLUwgsQn4hJA3 Fs3o/p/Ebqaa1rEgxcj1xAm/dEzzpbwK4NQYs+DbmAATSCACehwjTRQUTc8uTqBmWdoUNmqIEfey V/cv7ySzzwBwL7sbihEi38YEEoWAxOfw4WgWo/g6lEdI8fHT7x6aX3a6FPIFAPuYkB1nwQSYgIsI CCEeTdlZdROvF8XfaSxI8TPUcxiev6mXH4GZEDjKpCw5GybABJxNoAoCf1o8I+cVZ1fTPbXjKTuT +mphYbe1qVXVJ9KvJZ7CMwkqZ8MEnEtgpcePo1mMzO0gHiGZy1PPbejYjWdKKZ7hKTwFcDlLJmAz ASnk44GMhhuLn+9VZ3NVEq54FiRFXZo7ZkMPzau9LIBTFBXB2TIBJmAtge1SYuKSwhxyJcZJAQGe slMAlbIsnt1jwz4yOw8Q/wAku5lXxJmzZQIWEVgqJA5nMVJLm0dIavnquQ8bV3pUIACywhtgQXFc BBNgAuYRqBPAbScOyH548mQRMC9bzikUARakUFQUHDsuf31Gpua5FxLXsi88BYA5SyZgNgGJLwFx /uLC7FVmZ835hSbAghSai7KjeQVlgwXw5P+3d26xVVRRGP7/PbW2oqYgSIGC8a6QoPLgJYKppYL1 Fi+p0AcUrw/6oDHxHhU1XuOLiYmJqVGjRvAIGjERQ9GCGNEgmGgUlYhSORRRqQhKy5lZZho1EUs9 9zMz5386ObPXWnutb03yZ86Zvbe2HSoZYgUWgUIJDBj48K8NjQ/rVNdCUebmL0HKjVdRrAeflliz ALBbANQUJaiCiIAIFEzAgA/DPS8CAAAGbElEQVRovF5PRQWjzCuABCkvbMVxap275RQL2AlgWnEi KooIiECeBHYCvGvG5Man9V9RngSL4CZBKgLEQkI0N1uNO7z3ZsIeAFBfSCz5ioAI5EOAS/2Mf0P4 Zmw+3vIpHgEJUvFYFhSp9bJtRwUueIa0mQUFkrMIiEC2BLbB7KYVqQmLsnWQXWkJSJBKyzfH6MaW Oen5ND4OYHSOzjIXARHIjoABfK7GMre+k5r4S3YusioHAQlSOSjnOMfs9p5RGboHAV6vlx5yhCdz ERiewDrS3di1qHHN8GYarQQBCVIlqGc55+BLDz6fBDEjSxeZiYAIDE3gJxrunj5lXKdeWhgaUBSu SpCi0IVhczC2XJ6eS/BRAJOGNdWgCIjAvgQGQDzl9/c/2P3GkX37Dup7tAhIkKLVj/1mE65dGgHv JiNuB9CwX0MNiIAI/E3gdd/827pTEzf+fUGf0SYgQYp2f/6TXXNHerTn2/0ArgN4wH8MdEEEqpwA zT6C5+7QceLxuxEkSPHr2WDGze09x3j0FgDoAKBd22PaR6VdVAJfkLina9G41wFaUSMrWFkISJDK grl0k8zs6JmKjHsI5AWlm0WRRSDCBIjvYbxvlDW+lErRj3CmSu1/CEiQ/gdQXIZb5/SebkFwL4i2 uOSsPEWgQAKbaXx0JH55NpWaMlBgLLlHgIAEKQJNKGYKLe1bTyXtXgDnFzOuYolAhAh8R8MjI7Hj eQlRhLpShFQkSEWAGMUQ4aGAvo87SVys/5ii2CHllAeBrwh7fEfD+Bd1LEQe9GLgIkGKQZMKSfHs 9i3HO8dbYZgHoLaQWPIVgQoR+JjEY9NPHPeGFrVWqANlmlaCVCbQlZ7mnLmbxwfm3QzjdVrHVOlu aP4sCAQElhn4xIpXx72Xhb1MEkBAgpSAJuZSwqx5vSP8Absy3OUYwHG5+MpWBMpAYDdgL4D25IpF TV+XYT5NESECEqQINaOcqSxYYG7lF73nerAbDZgNwCvn/JpLBP5FgPjGjM8EA3s6tcXPv8hU1RcJ UlW1e+hiW9vTkwLyKsKu1n55QzPS1ZIQ6AfwGh07uxY2rtRi1pIwjlVQCVKs2lXaZMOnplVf9s5G YNeSdqG2Jiot7yqO/hkMz9bAf1HnEVXxXTBE6RKkIaDoEjCzY9tY8zPzSV4Dw7FiIgIFEtgF8FWY 37ki1fRhgbHknlACEqSENrZ4ZYXHX2ydQeIKGi41YGTxYitSwgkEAFbC7OXa2rrU2y8ftjPh9aq8 AglIkAoEWE3ubW3fHDhwyIg2g3UQvBBAfTXVr1qzJGBYC+AV52UWLl84KZ2ll8xEABIk3QR5ETjz ou2H1B04cB7Jyww4D8CIvALJKQkEDLCPaG4JAre4a/HYb5NQlGooPwEJUvmZJ27G8PDAg+iFr45f YkAbgTGJK1IF7UsgA9gqI5cGe4PXupc0/bCvgb6LQK4EJEi5EpP9sATa28372UufxoAXwHg+aFOH ddBgnAj8BGAZzN4i6pd1pUb9GqfklWv0CUiQot+jWGfYfOkPTTU1nGVw5xhspp6eYtXO8EiHNQSW +84tH+2PXavzhmLVv9glK0GKXcvim3C4zmn1hvRJ8F2rITgL5HTtqxepfvowrIfDKgZ8NwNvZXfq 8F2RylDJJJqABCnR7Y12ceHPezu89FTzeRaIGQBOBzAh2lknKrvdMKwDsdrI9/v/qFn9wZtjfktU hSomVgQkSLFqV/KTbWnvmeCcdxrMzjDwVAAnAzg0+ZWXvMKMARsIfGLAGs+CNXu3T/i8u5uZks+s CUQgSwISpCxByaxSBIwtc348CgimATaNNihQk7Xn3rD92EmzL438FMT6gO7Tg3cHny1dOv73Yb00 KAIVJiBBqnADNH1+BMJ1ULUH+ZO9wJ9ixAk0d7yZHQ3iaAB1+UWNlZcBCF+13mjARho2mLPPXcAN Xanxm2NViZIVgb8ISJB0KySMgHHW3HSTH7hjBp+iDEcYrYmGpsHvxMSY/AQY7oS9zYAewLYQDD83 G7DJkRsz9f2bup8/ck/CmqdyqpyABKnKb4BqLL95/qa62j21Y3xzjQDHGvwxNDfKYA2ObDCzBpg1 GFy9ozWY8QA4Oxhm9QT/efraz75+vxMIxQTh9gUg+mDYS2CXGcKxPwJaH8k+GnfArC8g+hyw3Rx+ 9PZie+DqtmqNTzXemar5T7boKrYfCqI6AAAAAElFTkSuQmCC"/></symbol><symbol viewBox="0 0 24 24" id="webpack" xmlns="http://www.w3.org/2000/svg"><path d="M19.376 15.988l-7.709 4.45-7.708-4.45V7.087l7.708-4.45 7.709 4.45z" fill="#fff" fill-opacity=".785" stroke-width="0"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18.21 0 .41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.939v2.104h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07l7-3.94zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#1c78c0"/></symbol><symbol viewBox="0 0 24 24" id="wolframlanguage" xmlns="http://www.w3.org/2000/svg"><title>wolframLanguage</title><g transform="scale(.12121)" fill="none" fill-rule="evenodd"><circle cx="99.197" cy="98.946" r="83.28" fill="#212121" stroke-width=".841"/><path d="M182.529 98.828a83.406 83.406 0 0 1-39.14 70.721.064.064 0 0 1-.038.019l-28.62-35.665 23.71 2.612s11.385 1.177 13.978 0c2.373-.938 15.175-18.963 15.175-18.963s-36.75-23.23-49.312-36.032c1.434-21.575-1.656-50.269-1.656-50.03-9.251 9.234-10.429 10.669-19.68 19.203-4.028-13.04-5.923-17.547-9.95-30.588-12.104 9.95-21.337 26.799-27.977 46.48a78.68 78.68 0 0 0-4.23 5.094 109.774 109.774 0 0 0-2.667 3.66 114.558 114.558 0 0 0-5.132 8.002 172.555 172.555 0 0 0-3.403 6.051c-7.706 14.475-14.034 31.066-19.515 46.001a.858.858 0 0 1-.092-.184c-14.988-30.912-9.502-67.85 13.822-93.072 23.325-25.223 59.723-33.575 91.71-21.045 31.988 12.53 53.029 43.382 53.017 77.736z" fill="#e53935"/><path d="M101.452 69.178s-1.416-8.295-2.373-11.367c6.401-6.18 7.357-7.118 13.52-13.04.477 11.845.238 18.006-.479 32.481-3.55-3.568-10.668-8.074-10.668-8.074zm-27.737 40.778s-6.64-4.029-11.624-4.728c1.435-3.329 5.223-7.596 6.18-8.773-1.913.699-15.653 6.86-17.087 12.084a74.804 74.804 0 0 1 11.385 3.79 35.993 35.993 0 0 0-8.774 20.158s21.815-3.33 38.185-1.196c.283.168.609.251.938.24l8.534.239 27.111 45.136.221.35c-.037.018-.055.037-.073.037-51.133 18.485-88.085-15.543-95.976-27.443.034-.102.058-.206.074-.313 7.1-30.017 15.855-65.939 30-76.552 7.356-12.82 9.49-31.783 22.751-41.734 3.33 9.951 8.553 30.588 12.103 40.539 15.653 15.652 39.361 35.094 55.234 43.15 1.656.956 3.79 7.596 3.79 7.596l-6.401 8.056-68.276-6.879a54.462 54.462 0 0 0-4.58-.183 86.848 86.848 0 0 0-14.144 1.36c3.311-8.295 10.43-14.935 10.43-14.935zm22.054-8.774c3.789-.46 7.817.956 12.323 3.568 4.267-1.195 4.745-1.434 9.013-2.612-5.463-4.028-11.386-8.295-19.442-7.118a47.249 47.249 0 0 0-1.894 6.162z" fill="#fff" stroke-width=".936"/></g></symbol><symbol viewBox="0 0 24 24" id="word" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M7 13l1.5 7h2l1.5-3 1.5 3h2l1.5-7h1v-2h-4v2h1l-.9 4.2L13 15h-2l-1.1 2.2L9 13h1v-2H6v2h1z" fill="#01579b"/></symbol><symbol viewBox="0 0 24 24" id="xaml" xmlns="http://www.w3.org/2000/svg"><path d="M18.93 12l-3.47 6H8.54l-3.47-6 3.47-6h6.92l3.47 6m4.84 0l-4.04 7L18 18l3.46-6L18 6l1.73-1 4.04 7M.23 12l4.04-7L6 6l-3.46 6L6 18l-1.73 1-4.04-7z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="xml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="yaml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="yang" xmlns="http://www.w3.org/2000/svg"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8 8 8 0 0 0 8 8 4 4 0 0 1-4-4 4 4 0 0 1 4-4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 2.5A1.5 1.5 0 0 1 13.5 8 1.5 1.5 0 0 1 12 9.5 1.5 1.5 0 0 1 10.5 8 1.5 1.5 0 0 1 12 6.5m0 8a1.5 1.5 0 0 0-1.5 1.5 1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5 1.5 1.5 0 0 0-1.5-1.5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 289.99999 290.00001" id="yarn" xmlns="http://www.w3.org/2000/svg"><path d="M250.733 218.418c-12.39 2.943-18.661 5.653-33.993 15.641-24.004 15.487-50.176 22.688-50.176 22.688s-2.168 3.252-8.44 4.723c-10.84 2.633-51.647 4.878-55.364 4.956-9.988.077-16.105-2.555-17.809-6.66-5.188-12.388 7.434-17.809 7.434-17.809s-2.788-1.703-4.414-3.252c-1.471-1.47-3.02-4.413-3.484-3.33-1.936 4.724-2.943 16.261-8.13 21.45-7.125 7.2-20.598 4.8-28.573.619-8.75-4.646.62-15.564.62-15.564s-4.724 2.788-8.518-2.942c-3.407-5.266-6.582-14.248-5.73-25.32 1.084-12.777 15.176-25.011 15.176-25.011s-2.477-18.661 5.653-37.787c7.356-17.422 27.179-31.437 27.179-31.437s-16.648-18.352-10.454-35c4.027-10.84 5.653-10.763 6.97-11.227 4.645-1.781 9.136-3.717 12.466-7.356 16.648-17.964 37.864-14.557 37.864-14.557s9.911-30.431 19.203-24.469c2.865 1.859 13.163 24.778 13.163 24.778s10.996-6.426 12.235-4.026c6.659 12.931 7.433 37.632 4.49 52.654-4.955 24.778-17.344 38.096-22.3 46.459-1.161 1.936 13.319 8.053 22.456 33.373 8.44 23.152.929 42.587 2.245 44.756.232.387.31.542.31.542s9.679.774 29.114-11.228c10.376-6.427 22.688-13.628 36.703-13.783 13.55-.232 14.247 15.719 4.104 18.12z" fill="#2c8ebb" stroke-width=".774"/></symbol><symbol viewBox="0 0 24 24" id="zip" xmlns="http://www.w3.org/2000/svg"><path d="M14 17h-2v-2h-2v-2h2v2h2m0-6h-2v2h2v2h-2v-2h-2V9h2V7h-2V5h2v2h2m5-4H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#afb42b"/></symbol></svg> \ No newline at end of file
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 24 24" id="actionscript" xmlns="http://www.w3.org/2000/svg"><text style="line-height:113.99999857%" x="5.605" y="15.892" transform="scale(.91325 1.095)" font-weight="400" font-size="42.822" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/><path style="line-height:125%" d="M4.744 2.031c-1.157 0-1.994.31-2.51.93-.515.612-.771 1.678-.771 3.197v2.467c0 1.408-.402 2.111-1.201 2.111v2.035c.8 0 1.2.679 1.2 2.036v2.654c0 1.512.26 2.562.78 3.152.52.59 1.355.885 2.502.885V19.43c-.447 0-.77-.151-.97-.453-.195-.303-.292-.815-.292-1.538v-2.267c0-1.807-.404-2.937-1.214-3.395v-.045c.81-.464 1.214-1.581 1.214-3.351V6.025c0-1.283.42-1.925 1.262-1.925V2.03zm14.66 0V4.1c.842 0 1.262.642 1.262 1.925v2.268c0 1.843.402 2.996 1.207 3.46v.046c-.805.442-1.207 1.544-1.207 3.306v2.356c0 .715-.099 1.22-.299 1.516-.2.302-.52.453-.963.453v2.068c1.152 0 1.984-.295 2.494-.885.516-.59.772-1.663.772-3.218V14.84c0-1.379.404-2.069 1.209-2.069v-2.035c-.805 0-1.21-.696-1.21-2.09V6.113c0-1.49-.255-2.54-.77-3.152-.516-.62-1.348-.93-2.495-.93zm-3.054 4.46c-.455 0-.886.057-1.293.173a3.056 3.056 0 0 0-1.078.527c-.308.241-.551.549-.731.924-.18.37-.27.817-.27 1.336 0 .663.165 1.227.493 1.695.33.468.831.864 1.502 1.188.263.125.509.249.736.37.227.12.422.244.586.374.168.13.299.271.394.424a.963.963 0 0 1 .145.521c0 .144-.03.28-.09.405a.9.9 0 0 1-.275.318c-.12.088-.272.158-.455.21a2.34 2.34 0 0 1-.635.075c-.415 0-.825-.083-1.233-.25a3.644 3.644 0 0 1-1.13-.763v2.222a3.68 3.68 0 0 0 1.101.418c.427.093.875.139 1.346.139.459 0 .894-.05 1.305-.152a3.002 3.002 0 0 0 1.09-.5c.31-.237.556-.543.736-.918.183-.38.275-.849.275-1.405 0-.403-.052-.755-.156-1.056a2.542 2.542 0 0 0-.45-.813 3.295 3.295 0 0 0-.704-.633 6.754 6.754 0 0 0-.922-.535 12.4 12.4 0 0 1-.676-.348c-.2-.115-.37-.231-.51-.347a1.502 1.502 0 0 1-.322-.375.91.91 0 0 1-.115-.453c0-.153.033-.288.101-.408a.948.948 0 0 1 .29-.32c.123-.089.275-.156.454-.202a2.18 2.18 0 0 1 .598-.078c.16 0 .326.015.502.043.18.028.36.07.539.13.18.056.354.13.522.218.171.088.329.188.472.304V6.871a4.039 4.039 0 0 0-.957-.285 6.448 6.448 0 0 0-1.185-.096zm-8.774.165l-3.123 9.967h2.094l.605-2.217h3.053l.61 2.217h2.107L9.869 6.656H7.576zm1.072 1.78h.047c.028.347.077.646.145.896l.922 3.35H7.564l.934-3.377c.08-.288.13-.578.15-.87z" font-weight="400" font-size="51.019" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="android" xmlns="http://www.w3.org/2000/svg"><path d="M15 5h-1V4h1m-5 1H9V4h1m5.53-1.84L16.84.85c.19-.19.19-.51 0-.71a.513.513 0 0 0-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.14a.501.501 0 0 0-.7 0c-.2.2-.2.52 0 .71l1.31 1.31C6.97 3.26 6 5 6 7h12c0-2-1-3.75-2.47-4.84M20.5 8A1.5 1.5 0 0 0 19 9.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 20.5 8m-17 0A1.5 1.5 0 0 0 2 9.5v7A1.5 1.5 0 0 0 3.5 18 1.5 1.5 0 0 0 5 16.5v-7A1.5 1.5 0 0 0 3.5 8M6 18a1 1 0 0 0 1 1h1v3.5A1.5 1.5 0 0 0 9.5 24a1.5 1.5 0 0 0 1.5-1.5V19h2v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5V19h1a1 1 0 0 0 1-1V8H6v10z" fill="#c0ca33"/></symbol><symbol viewBox="0 0 24 24" id="angular" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="angular-component" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#0288d1"/></symbol><symbol viewBox="0 0 24 24" id="angular-directive" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ab47bc"/></symbol><symbol viewBox="0 0 24 24" id="angular-guard" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-pipe" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#00897b"/></symbol><symbol viewBox="0 0 24 24" id="angular-resolver" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-routing" xmlns="http://www.w3.org/2000/svg"><path d="M11 10H5L3 8l2-2h6V3l1-1 1 1v1h6l2 2-2 2h-6v2h6l2 2-2 2h-6v6a2 2 0 0 1 2 2H9a2 2 0 0 1 2-2V10z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-service" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ffca28"/></symbol><symbol viewBox="0 0 100 100" id="apiblueprint" xmlns="http://www.w3.org/2000/svg"><title>api-blueprint</title><path d="M50.133 7.521A16.998 16.998 0 0 0 33.135 24.52a16.998 16.998 0 0 0 4.945 11.974L24.861 57.398a16.998 16.998 0 0 0-3.175-.308A16.998 16.998 0 0 0 4.688 74.088a16.998 16.998 0 0 0 16.998 16.998 16.998 16.998 0 0 0 16.998-16.998 16.998 16.998 0 0 0-7.063-13.773l12.576-19.89a16.998 16.998 0 0 0 5.936 1.093 16.998 16.998 0 0 0 6.154-1.155l12.537 19.83a16.998 16.998 0 0 0-7.244 13.895 16.998 16.998 0 0 0 16.998 17 16.998 16.998 0 0 0 16.998-17A16.998 16.998 0 0 0 78.578 57.09a16.998 16.998 0 0 0-2.95.262L62.337 36.327A16.998 16.998 0 0 0 67.13 24.52 16.998 16.998 0 0 0 50.132 7.522z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="applescript" xmlns="http://www.w3.org/2000/svg"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" fill="#78909c"/></symbol><symbol viewBox="0 0 24 24" id="appveyor" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c-.084 0-.165.008-.248.01a10 10 0 0 0-.266.01 9.952 9.952 0 0 0-.754.066 10 10 0 0 0-.148.018 9.855 9.855 0 0 0-.93.177 10 10 0 0 0-.07.02c-.196.049-.392.1-.584.16v.012a10 10 0 0 0-2 .875V3.34c-.02.012-.038.027-.059.039a10 10 0 0 0-.953.635c-.09.067-.172.142-.26.213a10 10 0 0 0-.628.546c-.109.104-.211.211-.315.319a10 10 0 0 0-.476.539c-.1.12-.201.237-.295.361a10 10 0 0 0-.52.766c-.088.143-.17.288-.252.435a10 10 0 0 0-.363.723c-.072.161-.136.327-.2.492a10 10 0 0 0-.269.778c-.02.067-.044.131-.062.199a10 10 0 0 0-.008.027c-.098.364-.166.728-.22 1.09-.012.077-.024.153-.034.23a9.85 9.85 0 0 0-.08 1.182c0 .03-.006.057-.006.086a10 10 0 0 0 .008.148c.001.094-.002.188.002.282l.011.004a10 10 0 0 0 .333 2.158l-.012-.004c.012.047.033.091.047.139a10 10 0 0 0 .322.955c.02.052.037.106.059.158a10 10 0 0 0 .503 1.035c.065.116.14.226.21.34a10 10 0 0 0 .423.64c.092.128.187.252.285.375a10 10 0 0 0 .448.52c.112.123.222.248.341.365a10 10 0 0 0 .803.719 10 10 0 0 0 .01.006c.099.078.207.146.309.22a10 10 0 0 0 .648.442c.138.085.28.163.424.242a10 10 0 0 0 .715.358c.114.051.226.106.343.154a10 10 0 0 0 1.133.389c.016.004.031.01.047.015a10 10 0 0 0 .461.098 10 10 0 0 0 .482.103 10 10 0 0 0 .418.051 10 10 0 0 0 .575.065 10 10 0 0 0 .144.005A10 10 0 0 0 12 22a10 10 0 0 0 .197-.01 10 10 0 0 0 .496-.025 10 10 0 0 0 .49-.043 10 10 0 0 0 .489-.074 10 10 0 0 0 .51-.098 10 10 0 0 0 .47-.12 10 10 0 0 0 .477-.14 10 10 0 0 0 .47-.172 10 10 0 0 0 .481-.197 10 10 0 0 0 .414-.201 10 10 0 0 0 .475-.252 10 10 0 0 0 .39-.238 10 10 0 0 0 .452-.301 10 10 0 0 0 .38-.291 10 10 0 0 0 .385-.315 10 10 0 0 0 .375-.347 10 10 0 0 0 .36-.363 10 10 0 0 0 .293-.334 10 10 0 0 0 .353-.434 10 10 0 0 0 .28-.393 10 10 0 0 0 .263-.4 10 10 0 0 0 .264-.461 10 10 0 0 0 .228-.436 10 10 0 0 0 .195-.437 10 10 0 0 0 .196-.48 10 10 0 0 0 .228-.69 10 10 0 0 0 .028-.094 10 10 0 0 0 .021-.066 10 10 0 0 0 .098-.461 10 10 0 0 0 .103-.482 10 10 0 0 0 .051-.418 10 10 0 0 0 .065-.575 10 10 0 0 0 .005-.144A10 10 0 0 0 22 12a10 10 0 0 0-.01-.197 10 10 0 0 0-.025-.496 10 10 0 0 0-.043-.49 10 10 0 0 0-.074-.489 10 10 0 0 0-.098-.51 10 10 0 0 0-.12-.47 10 10 0 0 0-.14-.477 10 10 0 0 0-.172-.47 10 10 0 0 0-.197-.481 10 10 0 0 0-.201-.414 10 10 0 0 0-.252-.475 10 10 0 0 0-.238-.39 10 10 0 0 0-.301-.452 10 10 0 0 0-.291-.38 10 10 0 0 0-.315-.385 10 10 0 0 0-.347-.375 10 10 0 0 0-.363-.36 10 10 0 0 0-.334-.293 10 10 0 0 0-.434-.353 10 10 0 0 0-.393-.28 10 10 0 0 0-.4-.263 10 10 0 0 0-.461-.264 10 10 0 0 0-.436-.228 10 10 0 0 0-.437-.196 10 10 0 0 0-.48-.195 10 10 0 0 0-.69-.228 10 10 0 0 0-.094-.028 10 10 0 0 0-.066-.021 10 10 0 0 0-.461-.098 10 10 0 0 0-.482-.103 10 10 0 0 0-.418-.051 10 10 0 0 0-.575-.065 10 10 0 0 0-.144-.005A10 10 0 0 0 12 2zm-.016 5.002a5 5 0 0 1 .262.01 5 5 0 0 1 .227.011 5 5 0 0 1 .341.05 5 5 0 0 1 .135.019 5 5 0 0 1 .014.004 5 5 0 0 1 .115.025 5 5 0 0 1 .303.076 5 5 0 0 1 .265.086 5 5 0 0 1 .2.074 5 5 0 0 1 .242.106 5 5 0 0 1 .228.11 5 5 0 0 1 .196.109 5 5 0 0 1 .244.15 5 5 0 0 1 .17.12 5 5 0 0 1 .224.171 5 5 0 0 1 .186.16 5 5 0 0 1 .176.164 5 5 0 0 1 .172.18 5 5 0 0 1 .177.203 5 5 0 0 1 .133.172 5 5 0 0 1 .16.223 5 5 0 0 1 .133.214 5 5 0 0 1 .12.21 5 5 0 0 1 .107.216 5 5 0 0 1 .109.24 5 5 0 0 1 .084.223 5 5 0 0 1 .08.242 5 5 0 0 1 .07.264 5 5 0 0 1 .047.207 5 5 0 0 1 .045.277 5 5 0 0 1 .028.227 5 5 0 0 1 .02.351 5 5 0 0 1 .003.079 5 5 0 0 1-.012.271 5 5 0 0 1-.011.227 5 5 0 0 1-.05.341 5 5 0 0 1-.019.135 5 5 0 0 1-.004.014 5 5 0 0 1-.025.115 5 5 0 0 1-.076.303 5 5 0 0 1-.086.265 5 5 0 0 1-.074.2 5 5 0 0 1-.106.242 5 5 0 0 1-.11.228 5 5 0 0 1-.109.196 5 5 0 0 1-.15.244 5 5 0 0 1-.12.17 5 5 0 0 1-.171.224 5 5 0 0 1-.16.186 5 5 0 0 1-.164.176 5 5 0 0 1-.18.172 5 5 0 0 1-.203.177l-.002.002c-.018.019-.028.035-.047.053l-3.959 5.09-3.05-.979a141.684 141.684 0 0 0 3.177-3.084 5 5 0 0 1-.103-.015 5 5 0 0 1-.149-.024 5 5 0 0 1-.115-.025 5 5 0 0 1-3.57-3.04 5.072 5.072 0 0 1-.206-.661 5 5 0 0 1-.033-.147c-.025-.118-.036-.24-.054-.36-.987.993-1.964 1.993-2.954 3.05l-.98-3.053 5.092-3.957c.043-.044.082-.07.125-.11a5 5 0 0 1 .71-.634c.18-.13.367-.25.561-.356a5 5 0 0 1 .16-.08 4.94 4.94 0 0 1 .516-.222 5 5 0 0 1 .147-.057c.211-.07.43-.123.654-.164a5 5 0 0 1 .172-.027c.236-.035.476-.058.722-.059zM12 9a3 3 0 0 0-.053.002 3 3 0 0 0-.166.01 3 3 0 0 0-.133.011 3 3 0 0 0-.17.026 3 3 0 0 0-.113.021 3 3 0 0 0-.19.05 3 3 0 0 0-.103.03 3 3 0 0 0-.16.057 3 3 0 0 0-.129.053 3 3 0 0 0-.146.072 3 3 0 0 0-.12.063 3 3 0 0 0-.132.082 3 3 0 0 0-.123.08 3 3 0 0 0-.116.088 3 3 0 0 0-.126.105 3 3 0 0 0-.1.094 3 3 0 0 0-.111.111 3 3 0 0 0-.096.107 3 3 0 0 0-.094.116 3 3 0 0 0-.098.136 3 3 0 0 0-.072.11 3 3 0 0 0-.076.133 3 3 0 0 0-.07.132 3 3 0 0 0-.063.14 3 3 0 0 0-.054.14 3 3 0 0 0-.077.228 3 3 0 0 0-.007.026 3 3 0 0 0-.03.138 3 3 0 0 0-.031.149 3 3 0 0 0-.014.11 3 3 0 0 0-.02.183 3 3 0 0 0-.001.052A3 3 0 0 0 9 12a3 3 0 0 0 .002.053 3 3 0 0 0 .01.166 3 3 0 0 0 .011.133 3 3 0 0 0 .026.17 3 3 0 0 0 .021.113 3 3 0 0 0 .05.19 3 3 0 0 0 .03.103 3 3 0 0 0 .057.16 3 3 0 0 0 .053.129 3 3 0 0 0 .072.146 3 3 0 0 0 .063.12 3 3 0 0 0 .082.132 3 3 0 0 0 .08.123 3 3 0 0 0 .088.116 3 3 0 0 0 .105.126 3 3 0 0 0 .094.1 3 3 0 0 0 .111.111 3 3 0 0 0 .107.096 3 3 0 0 0 .116.094 3 3 0 0 0 .136.098 3 3 0 0 0 .11.072 3 3 0 0 0 .133.076 3 3 0 0 0 .132.07 3 3 0 0 0 .135.06 3 3 0 0 0 .153.061 3 3 0 0 0 .216.07 3 3 0 0 0 .004.003 3 3 0 0 0 .026.007 3 3 0 0 0 .138.03 3 3 0 0 0 .149.031 3 3 0 0 0 .11.014 3 3 0 0 0 .183.02 3 3 0 0 0 .011.001 3 3 0 0 0 .041 0A3 3 0 0 0 12 15a3 3 0 0 0 .053-.002 3 3 0 0 0 .166-.01 3 3 0 0 0 .133-.011 3 3 0 0 0 .17-.026 3 3 0 0 0 .113-.021 3 3 0 0 0 .19-.05 3 3 0 0 0 .103-.03 3 3 0 0 0 .16-.057 3 3 0 0 0 .129-.053 3 3 0 0 0 .146-.072 3 3 0 0 0 .12-.063 3 3 0 0 0 .132-.082 3 3 0 0 0 .123-.08 3 3 0 0 0 .116-.088 3 3 0 0 0 .126-.105 3 3 0 0 0 .1-.094 3 3 0 0 0 .111-.111 3 3 0 0 0 .096-.107 3 3 0 0 0 .094-.116 3 3 0 0 0 .098-.136 3 3 0 0 0 .072-.11 3 3 0 0 0 .076-.133 3 3 0 0 0 .07-.132 3 3 0 0 0 .06-.135 3 3 0 0 0 .061-.153 3 3 0 0 0 .07-.216 3 3 0 0 0 .003-.004 3 3 0 0 0 .007-.026 3 3 0 0 0 .03-.138 3 3 0 0 0 .031-.149 3 3 0 0 0 .002-.008 3 3 0 0 0 .012-.101 3 3 0 0 0 .02-.184 3 3 0 0 0 .001-.011 3 3 0 0 0 0-.041A3 3 0 0 0 15 12a3 3 0 0 0-.002-.053 3 3 0 0 0-.01-.166 3 3 0 0 0-.011-.133 3 3 0 0 0-.026-.17 3 3 0 0 0-.021-.113 3 3 0 0 0-.05-.19 3 3 0 0 0-.03-.103 3 3 0 0 0-.057-.16 3 3 0 0 0-.053-.129 3 3 0 0 0-.072-.146 3 3 0 0 0-.063-.12 3 3 0 0 0-.082-.132 3 3 0 0 0-.08-.123 3 3 0 0 0-.088-.116 3 3 0 0 0-.105-.126 3 3 0 0 0-.094-.1 3 3 0 0 0-.111-.111 3 3 0 0 0-.107-.096 3 3 0 0 0-.116-.094 3 3 0 0 0-.136-.098 3 3 0 0 0-.11-.072 3 3 0 0 0-.133-.076 3 3 0 0 0-.132-.07 3 3 0 0 0-.14-.063 3 3 0 0 0-.14-.054 3 3 0 0 0-.228-.077 3 3 0 0 0-.026-.007 3 3 0 0 0-.138-.03 3 3 0 0 0-.149-.031 3 3 0 0 0-.008-.002 3 3 0 0 0-.101-.012 3 3 0 0 0-.184-.02 3 3 0 0 0-.011-.001 3 3 0 0 0-.041 0A3 3 0 0 0 12 9z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 720 720" id="arduino" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="ana" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke-opacity="100%" stroke-width="60" stroke="#00979c" d="M174 30a10.5 10.1 0 0 0 0 280C364 320 344 30 544 30a10.5 10.1 0 0 1 0 280C354 320 374 30 174 30"/><path d="M528 205v-32.8h-32.5v-13.7H528V126h13.9v32.5h32.5v13.7h-32.5V205H528z" text-anchor="middle" fill="#00979c" stroke-width="20" stroke="#00979c" font-family="sans-serif" font-size="167"/><path fill="#00979c" stroke="#00979c" stroke-width="23.6" transform="matrix(1.56 0 0 .64 -366 .528)" d="M321 266v-17.4h53.3V266H321z"/></symbol></defs><title>Layer 1</title><use x="20.063" y="360.85" transform="matrix(.997 0 0 .997 -18.596 -159.19)" xlink:href="#ana"/></symbol><symbol viewBox="0 0 24 24" id="assembly" xmlns="http://www.w3.org/2000/svg"><path d="M1.746 1.566v20.905H5.13v-2.088H3.438V3.656h1.69v-2.09H1.747zm17.219 0v2.09h1.693v16.727h-1.693v2.09h3.383V1.566h-3.383zM15.196 3.988c-.5 0-.93.076-1.29.225-.359.15-.652.372-.877.671-.226.302-.39.673-.494 1.108a6.715 6.715 0 0 0-.155 1.54c0 .573.049 1.083.15 1.528.1.442.264.811.49 1.11.222.298.512.524.872.676.36.153.795.23 1.304.23.518 0 .954-.075 1.308-.224.353-.153.643-.376.869-.671.219-.29.38-.661.484-1.112.104-.454.156-.967.156-1.54 0-.573-.052-1.079-.152-1.515a2.92 2.92 0 0 0-.485-1.106 2.09 2.09 0 0 0-.868-.686c-.354-.155-.79-.234-1.312-.234zm-6.814.12a.941.941 0 0 1-.138.458.849.849 0 0 1-.356.296A1.71 1.71 0 0 1 7.385 5a5.244 5.244 0 0 1-.631.037v1.11H8.19v3.6H6.754v1.188h4.545V9.745H9.894V4.11H8.382zm6.814 1.138c.375 0 .643.176.805.527.161.348.241.933.241 1.756 0 .814-.082 1.399-.247 1.756-.164.356-.43.534-.799.534-.369 0-.636-.178-.8-.534-.165-.357-.248-.941-.248-1.749 0-.829.082-1.415.243-1.763.162-.35.43-.527.805-.527zm-6.33 7.64c-.5 0-.93.073-1.29.223-.359.15-.651.374-.877.673-.225.302-.39.67-.494 1.106a6.715 6.715 0 0 0-.155 1.54c0 .573.05 1.082.15 1.527.1.442.264.814.49 1.112.222.3.514.525.874.677.36.152.793.229 1.302.229.519 0 .954-.076 1.308-.225.354-.153.643-.376.869-.672.22-.29.38-.66.484-1.111.104-.455.156-.967.156-1.54 0-.573-.05-1.079-.15-1.515a2.923 2.923 0 0 0-.487-1.106 2.084 2.084 0 0 0-.867-.686c-.353-.156-.791-.232-1.313-.232zm5.846.119a.941.941 0 0 1-.138.457.85.85 0 0 1-.356.296 1.71 1.71 0 0 1-.503.137 5.245 5.245 0 0 1-.631.037v1.112h1.435v3.597h-1.435v1.189h4.545v-1.189h-1.405v-5.636h-1.512zm-5.846 1.137c.375 0 .643.176.805.527.162.347.241.933.241 1.756 0 .813-.08 1.399-.245 1.755-.164.357-.432.534-.8.534-.37 0-.637-.177-.802-.534-.164-.356-.245-.939-.245-1.746 0-.83.08-1.418.242-1.765.161-.35.43-.527.804-.527z" fill="#ff6e40"/></symbol><symbol viewBox="0 0 24 24" id="aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="api" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apa"/><linearGradient id="apa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#apb"/><linearGradient id="apb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#apc"/><linearGradient id="apc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#apd"/><linearGradient id="apd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#ape"/><linearGradient id="ape" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#apf"/><linearGradient id="apf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apg"/><linearGradient id="apg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="app" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#aph"/><linearGradient id="aph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><g transform="rotate(11.282 -1.694 21.569) scale(.47102)" clip-rule="evenodd" fill="none" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#api)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#apj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#apk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#apl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#apm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#apn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#apo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#app)"/></g></symbol><symbol viewBox="0 0 24 24" id="autohotkey" xmlns="http://www.w3.org/2000/svg"><path d="M5 3c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5zm3.668 3.447a.9.9 0 0 1 .652.256.84.84 0 0 1 .262.625c0 .34-.014.852-.041 1.537-.022.68-.033 1.19-.033 1.53 0 .111-.016.326-.047.644a6.149 6.149 0 0 0-.033.68l2.578-.485c1.007-.179 1.874-.281 2.603-.308.018-.3.048-1.105.088-2.416.01-.345.115-.742.317-1.19.25-.55.533-.826.851-.826.237 0 .448.08.631.236.197.17.295.382.295.637a.775.775 0 0 1-.025.201c-.09.327-.135.612-.135.854 0 .125-.014.32-.041.584-.023.26-.033.453-.033.578 0 .425-.022 1.056-.067 1.893a38.963 38.963 0 0 0-.068 1.892c0 .327.025.816.074 1.465.05.649.074 1.136.074 1.463a.84.84 0 0 1-.261.625.893.893 0 0 1-.65.254 1 1 0 0 1-.686-.254.777.777 0 0 1-.29-.611c0-.327-.015-.818-.046-1.471a39.552 39.552 0 0 1-.041-1.47c0-.256.004-.482.013-.679-.702.032-1.57.142-2.603.33-.86.157-1.719.316-2.578.477-.01.304-.042.812-.096 1.523a22.354 22.354 0 0 0-.066 1.538.84.84 0 0 1-.262.625.893.893 0 0 1-.65.253.898.898 0 0 1-.653-.253.84.84 0 0 1-.262-.625c0-.452.038-1.128.114-2.028.08-.9.12-1.575.12-2.027 0-.573.015-1.436.042-2.586.027-1.155.04-2.017.04-2.59a.84.84 0 0 1 .263-.625.895.895 0 0 1 .65-.256z" fill="#4caf50"/></symbol><symbol viewBox="0 0 24 24" id="autoit" xmlns="http://www.w3.org/2000/svg"><defs id="ardefs8"><style id="arstyle4482">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style><style id="arstyle4510">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style></defs><g id="arg4522" transform="translate(-59.538 -26.404) scale(.0555)"><path d="M12.8 2.133A10.666 10.666 0 0 0 2.136 12.799 10.666 10.666 0 0 0 12.8 23.465a10.666 10.666 0 0 0 10.668-10.666A10.666 10.666 0 0 0 12.8 2.133zm.15 4.713c.456 0 .836.105 1.142.314.306.21.565.469.78.78l6.089 8.812H9.627l1.82-2.506h3.36c.315 0 .589.01.822.03a11.93 11.93 0 0 1-.473-.663 39.13 39.13 0 0 0-.517-.75l-1.748-2.578-4.577 6.467H4.746l6.25-8.813c.204-.281.46-.534.772-.757.31-.224.705-.336 1.181-.336z" transform="matrix(16.89188 0 0 16.89188 1072.761 475.745)" id="arcircle4514" fill="#1976d2" stroke-width=".026"/></g></symbol><symbol viewBox="0 0 213.33333 213.33333" id="babel" xmlns="http://www.w3.org/2000/svg"><path d="M50.22 199.659c-.875-.406-1.261-1.6-.857-2.652.404-1.053.12-1.914-.63-1.914s-1.615.748-1.92 1.663c-.328.983-1.27.302-2.304-1.667-.962-1.831-3.718-5.533-6.126-8.226-9.418-10.535-7.71-27.444 5.432-53.77 12.459-24.96 23.117-39.033 45.966-60.696 30.229-28.66 52.679-46.223 70.587-55.22 10.98-5.518 13.025-5.059 2.778.624-11.004 6.102-11.378 6.359-10.512 7.226.33.33 7.306-2.67 15.504-6.667 15.87-7.737 16.34-7.912 16.34-6.082 0 .652-4.95 3.738-11 6.858-13.062 6.736-12.722 6.48-10.472 7.872 1.117.69 5.428-.582 11.54-3.406 5.367-2.48 10.397-4.508 11.179-4.508 2.755 0-3.928 5.302-11.541 9.157-20.437 10.35-68.937 46.043-68.07 50.097.166.777-5.792 7.639-13.241 15.248-15.257 15.587-26.14 30.002-33.748 44.706-6.379 12.326-7.457 17.734-5.385 26.996 3.482 15.56 11.592 18.366 31.482 10.895 28.228-10.603 45.758-28.704 47.022-48.556.602-9.442-1.317-13.479-8.52-17.93-4.01-2.48-5.268-2.621-12.065-1.365-4.173.771-10.153 2.906-13.289 4.744s-6.455 3.34-7.377 3.34c-.922 0-3.216 1.336-5.096 2.968-1.88 1.633.48-1.13 5.247-6.14 6.82-7.167 7.956-8.9 5.333-8.132-5.208 1.525-10.194 4.33-15.649 8.803-2.76 2.264-.923.175 4.08-4.641 11.565-11.131 21.183-15.97 33.088-16.641 17.097-.966 27.254 5.805 31.964 21.31 2.435 8.017 2.609 10.24 1.353 17.37-1.65 9.361-7.034 21.553-15.593 35.307-4.398 7.067-8.434 11.427-15.588 16.844-9.166 6.94-15.654 11.02-15.654 9.845 0-.295 2.455-2.161 5.455-4.147 8.818-5.835 5.075-5.377-8.326 1.02-6.854 3.27-15.199 6.593-18.542 7.38-7.106 1.675-30.527 3.164-32.846 2.089zm-8.408-19.899c0-1.1-.6-2-1.333-2-.734 0-1.334.9-1.334 2s.6 2 1.333 2c.734 0 1.334-.9 1.334-2zm89.255-8.204c1.53-1.945 2.473-3.845 2.097-4.222-.377-.377-.836-.435-1.02-.13-.182.306-1.787 2.206-3.565 4.223-1.778 2.016-2.571 3.666-1.763 3.666s2.72-1.591 4.25-3.536zm-77.644-1.745c-.82-2.172-1.74-3.7-2.045-3.396-.951.952 1.088 7.345 2.343 7.345.656 0 .522-1.777-.298-3.95zm82.303-27.915c-.837-.837-3.217 2.55-3.184 4.53.012.734.896.178 1.965-1.235 1.07-1.413 1.618-2.896 1.219-3.295zm-66.238-36.904c-1.312-1.312-3.676.702-3.676 3.133 0 2.035.175 2.031 2.254-.047 1.24-1.24 1.88-2.628 1.422-3.086zm39.657.768c4.403-2.196 6.8-3.986 5.333-3.982-2.838.01-16.667 6.028-16.667 7.254 0 1.6 3.717.527 11.333-3.272zm16.667-5.333c0-.733-.9-1.333-2-1.333s-2 .6-2 1.333.9 1.333 2 1.333 2-.6 2-1.333zm-3.334-3.923l5.334-1.104-7.334-.133c-4.033-.073-8.233.45-9.333 1.16-2.539 1.64 3.572 1.682 11.333.077zm35.738-63.976c2.788-1.69 4.765-3.376 4.393-3.748-.947-.947-11.942 5.654-14.237 8.548-1.792 2.258-1.714 2.276 1.44.329a1452.76 1452.76 0 0 1 8.403-5.13z" fill="#ffca28" stroke-width="1.333"/></symbol><symbol viewBox="0 0 400 400" fill-opacity=".05" id="bithound" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.88 0 0 .88 24.121 2.895)" fill="#e53935" fill-opacity="1"><path d="M370.5 207c-1.5-14.8-4.8-29.9-9.5-44-13.5-40.3-38.6-81.6-70.3-110.1-1.4-1.2-6.7-4.4-8.7-3.3-5.2 2.9 4.6 22.8 5.8 26.4 7.4 22 12.1 45.3 6.8 68.3-7.1 30.4-30.4 51.7-61.5 54.3-17.1 1.4-34.3-.5-51.4 1.5-25.6 3-51.7 11.8-68 32.8-1.9 2.4-3.6 5.1-5.2 7.9h-.4c-6.3.7-12.6-2-15.7-3.7-.8-.5-1.6-.9-2.2-1.2-19-10.5-33-34-41.6-53.4-3.9-9-7.2-18.4-9.3-27.9-1-4.3-1.1-8.8-1.3-13.2-.1-2.7.3-6.5-1.2-8.9-3.3-5.2-7.5-.2-8.2 4-1.1 6.9-2.1 13.7-1.8 20.7.5 11.8 3.8 23.5 8 34.5 6.2 16.2 14.9 31.1 26.2 44.4 4.7 5.5 9.7 10.6 15.1 15.3 4.8 4.3 10.9 7.7 14.5 13.2 4.2 6.3 4.9 14.1 4.5 21.4-1 19.3-1.6 37.4 3.9 56.2 4.8 16.7 10.8 33.8 20.8 48.1 5 7.1 11.2 14.6 18 19.9 4.6 3.6 13.3 4 8.3-9.2-11.1-29.3-12.1-59.7 5.2-87.1 14.5-22.8 40.1-43.1 69-39.5 42.5 5.3 72.1 44.3 70 86-.6 11.7-1 21.7-4.7 32.7-1.5 4.4-2.6 10-1.5 14.6 1.8 7.8 10.5 4.9 14.3-.2 10.3-14 21.1-27.6 30.8-42 31.6-47.2 47-101.8 41.3-158.5z"/><path d="M132.4 92.1c.7 2.3 1.4 4.8 1.9 7.5.1 1.1.4 2.3 1 3.4 2.6 6.8 8.9 10.5 14.8 14 3.6 2.2 10.1 4.3 14.1 5.9 5.2 2.1 16.4-.6 21.7-1 12.2-1 23.5-5.3 34.7 1.2-57.4 67.3-3.2 82.3 38.8 49.9 48-37 2.8-124.3 2.8-124.3s-1-6.8-19.2-10.8c-1.7-.9-3.4-1.7-5.1-2.4-18-8.3-34.2 5.3-47.2 16.4-3.8 3.2-7.5 6.4-11.5 9.4-5.4 4-11.2 7.3-17.3 10.2-6.4 3-14 6.4-21.1 6.7-1 0-2.9.2-4.9.6-3.1.3-4.7 1.1-5.4 2.5-1.2 1-2 2.4-1.8 4.2.2 2.5 1.4 4.6 2.7 6.2.4.1.7.3 1 .4z"/></g></symbol><symbol viewBox="0 0 400.00001 399.99999" id="bower" xmlns="http://www.w3.org/2000/svg"><g transform="translate(12.061 33.203) scale(.81733)"><path d="M447.61 200.08c-23.139-22.234-138.85-36.114-175.36-40.154a107.137 107.137 0 0 0 4.517-12.944 146.107 146.107 0 0 1 15.905-5.901c.677 1.997 3.865 9.648 5.682 13.279 73.415 2.025 77.184-54.557 80.17-70.058 2.92-15.157 2.771-29.802 27.953-56.575-37.516-10.933-91.467 16.945-109.54 58.437-6.79-2.545-13.597-4.424-20.328-5.586-4.824-19.46-29.944-73.672-95.863-73.672-83.46 0-174.43 68.853-174.43 185.41 0 97.976 66.891 183.84 104.68 183.84 16.505 0 30.703-12.36 34.036-23.44 2.795 7.597 11.368 31.213 14.184 37.225 4.162 8.89 23.41 16.583 31.833 7.357 10.83 6.017 30.703 9.641 41.534-6.405 20.86 4.412 39.3-8.026 39.702-22.868 10.235-.546 15.256-14.918 13.021-26.363-1.647-8.426-19.248-38.66-26.113-49.098 13.59 11.054 48.013 14.183 52.194.007 21.911 17.198 56.057 8.171 58.765-5.815 26.624 6.917 57.16-8.276 52.146-26.676 42.771-2.958 37.296-48.464 25.296-59.996z" fill="#543729" stroke-width=".973"/><path d="M328.514 103.025c9.212-18.277 20.788-38.234 35.409-50.58-16.093 6.485-31.981 25.873-41.375 46.595a144.914 144.914 0 0 0-14.552-8.132c13.105-27.972 43.555-51.332 77.112-53.157-22.477 20.385-14.498 62.754-32.979 85.183-5.288-5.311-17.43-15.562-23.615-19.909zm-14.53 29.762c.01-.7.272-6.094.763-8.557-1.288-.304-9.3-1.87-13.476-1.772-.304 5.245 2.204 14.17 4.684 19.541 17.075-.358 29.408-5.471 36.667-10.172-6.18-2.88-16.726-5.442-24.745-6.974-.894 1.851-3.097 6.568-3.892 7.934z" fill="#00acee"/><g stroke-width=".973"><path d="M250.54 277.39c.004.024.015.057.018.082-2.165-4.657-4.463-10.314-7.208-17.708 10.688 15.557 44.184 7.533 42.427-6.407 16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455 28 5.4 54.832 10.783 63.256 12.938-5.595 9.123-18.339 15.566-37.549 11.089 10.38 14.14-9.773 31.105-37.844 21.76 6.18 13.883-18.814 26.38-47.22 11.91.361 13.889-35.24 15.488-49.315.143zm55.543-70.194c32.497 2.495 86.238 7.34 119.51 11.997-2.102-10.828-7.844-13.921-25.905-18.772-19.425 2.072-68.706 6.913-93.604 6.776z" fill="#2baf2b"/><path d="M285.78 253.36c16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455-33.103-6.383-67.84-12.788-75.719-13.908 4.78.254 12.702.797 22.59 1.556 24.899.137 74.18-4.704 93.604-6.775-31.452-7.975-95.666-19.613-140.01-22.48-2.055 3.003-5.833 8.097-12.413 13.51-19.403 41.053-54.557 68.34-93.454 68.34-11.335 0-24.018-1.912-38.233-6.456-8.865 9.497-46.661 16.694-77.329 1.641 24.326 56.961 80.74 94.984 143.19 94.984 52.591 0 75.912-53.704 70.808-67.914-1.238-3.45-6.145-14.889-8.891-22.283 10.689 15.556 44.185 7.532 42.429-6.408z" fill="#ffcc2f"/><path d="M253.91 145.27c4.644-2.526 20.69-12.253 35.981-15.908a67.843 67.843 0 0 1-.536-5.12c-10.032 2.403-28.945 10.51-39.784-.661 22.866 6.9 34.283-6.149 51.09-6.149 10.014 0 24.305 2.798 35.57 7.22-9.061-8.37-38.772-33.63-75.558-33.717-8.213 9.957-17.09 31.526-6.764 54.334z" fill="#cecece"/><path d="M115.58 253.33c14.215 4.544 26.898 6.457 38.233 6.457 38.896 0 74.05-27.29 93.454-68.341-14.351 11.978-39.291 22.228-78.241 22.228 34.694-7.866 64.56-25.156 79.753-50.427-10.68-16.998-22.263-54.603 7.07-84.33-4.512-14.497-26.475-52.766-75.095-52.766-84.85 0-155.17 71.001-155.17 166.15 0 22.525 4.547 43.65 12.67 62.664 30.666 15.054 68.462 7.858 77.327-1.64z" fill="#ef5734"/><path d="M141.03 108.45c0 21.644 17.546 39.191 39.19 39.191s39.192-17.548 39.192-39.191c0-21.644-17.548-39.191-39.192-39.191-21.644 0-39.19 17.547-39.19 39.191z" fill="#ffcc2f"/><path d="M156.76 108.45c0 12.958 10.507 23.463 23.463 23.463 12.96 0 23.464-10.506 23.464-23.463 0-12.959-10.504-23.464-23.464-23.464-12.957 0-23.463 10.506-23.463 23.464z" fill="#543729"/><ellipse cx="180.22" cy="98.044" rx="13.673" ry="8.501" fill="#fff"/></g></g></symbol><symbol viewBox="0 0 140 140" id="browserlist" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><path d="M70.314 10.066a59.828 59.828 0 0 0-59.828 59.828 59.828 59.828 0 0 0 59.828 59.828 59.828 59.828 0 0 0 59.828-59.828 59.828 59.828 0 0 0-59.828-59.828zm-4.836 8.785c.496 4.043 1.352 7.322 2.572 10.223 4.779-4.287 10.265-7.546 16.041-9.02-.981 3.938-1.357 7.295-1.261 10.43 6.026-2.314 12.349-3.404 18.3-2.706-3.182 2.413-5.482 4.717-7.128 7.015-2.201 12.074 6.858 20.43 14.779 24.551a5.128 5.128 0 0 1 5.183-3.888 5.128 5.128 0 0 1 3.7 8.435v.002c-.487 1.055-2.002 2.343-3.497 3.219-4.075 2.39-11.172 5.736-20.914 7.39.045 1.214.077 2.453.077 3.747 0 4.817-.485 8.291-1.385 10.699-3.3 13.313-12.648 26.76-24.695 31.95.357-4.083.197-7.485-.402-10.591-5.582 3.218-11.646 5.278-17.623 5.52h-.002c1.785-3.662 2.855-6.878 3.412-9.976-6.347.996-12.727.742-18.377-1.17 2.93-2.732 5.054-5.314 6.673-7.96-6.292-1.344-12.169-3.87-16.766-7.686 3.822-1.544 6.795-3.239 9.3-5.197-5.426-3.517-10.034-7.998-12.972-13.23 4.012-.07 7.321-.568 10.3-1.453-3.786-5.215-6.468-11.032-7.333-16.951 3.861 1.405 7.196 2.133 10.36 2.355-1.662-6.22-2.081-12.605-.768-18.436 3.03 2.634 5.824 4.48 8.63 5.815.678-6.406 2.576-12.52 5.893-17.496 1.926 3.622 3.914 6.391 6.111 8.672 2.93-5.754 6.9-10.798 11.791-14.262zm26.465 19.557c-2.395 5.514-1.665 11.297-.555 18.732a2.138 2.138 0 0 0 .28-4.178 3.419 3.419 0 1 1 .092 6.704c.574 3.882 1.157 8.18 1.421 13.125a67.143 67.143 0 0 0 3.25-.649c6.616-1.487 12.258-3.801 16.871-6.506.45-.264.884-.563 1.276-.867.366-.557.333-.957.035-1.285-4.831-1.245-10.891-4.53-15.258-8.795-4.764-4.653-7.428-10.164-7.412-16.281z" fill="#ffca28" stroke-width=".855"/></symbol><symbol viewBox="0 0 140 140" id="browserlist_light" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><g transform="translate(10.823 10.1)" stroke-width=".855"><circle cx="59.492" cy="59.795" r="59.828" fill="#ffca28"/><path d="M54.656 8.752c-4.89 3.464-8.862 8.508-11.791 14.262-2.198-2.28-4.185-5.05-6.111-8.672-3.318 4.976-5.216 11.09-5.893 17.496-2.807-1.335-5.6-3.18-8.63-5.814-1.314 5.83-.895 12.216.767 18.436-3.164-.223-6.498-.95-10.36-2.356.865 5.92 3.548 11.737 7.333 16.951-2.978.885-6.287 1.383-10.3 1.453 2.939 5.233 7.547 9.714 12.972 13.23-2.505 1.959-5.478 3.654-9.299 5.198 4.596 3.815 10.474 6.341 16.766 7.685-1.62 2.647-3.743 5.228-6.674 7.96 5.65 1.912 12.03 2.166 18.377 1.17-.556 3.098-1.626 6.314-3.412 9.975h.002c5.977-.24 12.042-2.3 17.623-5.52.6 3.108.76 6.51.402 10.593 12.047-5.19 21.395-18.638 24.695-31.951.9-2.408 1.385-5.881 1.385-10.7 0-1.293-.031-2.531-.076-3.745 9.742-1.655 16.839-5.001 20.914-7.39 1.494-.877 3.01-2.165 3.496-3.22v-.002a5.128 5.128 0 0 0-3.7-8.435 5.128 5.128 0 0 0-5.183 3.889c-7.92-4.122-16.98-12.477-14.779-24.551 1.646-2.299 3.947-4.603 7.13-7.016-5.952-.698-12.276.392-18.302 2.707-.095-3.135.28-6.492 1.262-10.43-5.776 1.473-11.262 4.733-16.041 9.02-1.22-2.902-2.076-6.18-2.572-10.223zm26.465 19.557c-.015 6.117 2.648 11.628 7.412 16.281 4.366 4.265 10.426 7.55 15.258 8.795.298.328.331.728-.035 1.285-.392.304-.825.603-1.275.867-4.613 2.704-10.256 5.019-16.871 6.506-1.071.24-2.154.458-3.25.649-.265-4.945-.848-9.243-1.422-13.125a3.419 3.419 0 1 0-.092-6.703 2.138 2.138 0 0 1-.28 4.177c-1.11-7.435-1.84-13.218.555-18.732z" fill="#37474f"/></g></symbol><symbol viewBox="0 0 24 24" id="bucklescript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm14.1 8.858a5.5 5.5 0 0 1 1.26.145c.417.093.778.213 1.082.357v1.723h-.18a3.281 3.281 0 0 0-.959-.603 2.867 2.867 0 0 0-1.155-.247c-.14 0-.277.011-.416.035a1.4 1.4 0 0 0-.395.12.756.756 0 0 0-.291.231.54.54 0 0 0-.123.348c0 .198.065.35.196.456.13.104.376.2.738.288.237.057.466.11.683.164.22.054.455.128.706.222.496.188.86.444 1.095.77.238.32.357.738.357 1.253 0 .737-.271 1.336-.813 1.798-.538.46-1.27.689-2.197.689a5.447 5.447 0 0 1-1.402-.161 6.725 6.725 0 0 1-1.117-.416v-1.794h.183c.344.318.73.563 1.155.734.429.17.839.256 1.233.256.1 0 .235-.01.4-.03.166-.02.3-.055.403-.102a.97.97 0 0 0 .313-.225c.084-.09.127-.223.127-.4a.568.568 0 0 0-.183-.424c-.119-.12-.294-.213-.526-.276-.243-.067-.5-.128-.773-.185a5.523 5.523 0 0 1-.76-.227c-.544-.204-.936-.48-1.177-.828-.237-.351-.357-.786-.357-1.305 0-.697.27-1.265.81-1.703.54-.442 1.235-.663 2.083-.663zm-8.981.135h2.51c.521 0 .903.02 1.143.06.243.041.484.13.721.266.246.144.43.338.548.583.121.24.181.518.181.83 0 .36-.082.68-.247.959a1.697 1.697 0 0 1-.7.642v.04c.423.098.758.298 1.004.603.249.305.373.706.373 1.205 0 .361-.063.686-.19.97-.125.285-.296.52-.516.707a2.31 2.31 0 0 1-.845.472c-.304.094-.69.141-1.159.141H8.12v-7.478zm1.659 1.372v1.582h.262c.263 0 .486-.007.672-.017.185-.01.332-.043.44-.1.15-.077.248-.175.294-.295.046-.124.07-.266.07-.427a.91.91 0 0 0-.083-.371.518.518 0 0 0-.282-.277 1.187 1.187 0 0 0-.456-.086c-.18-.007-.433-.01-.76-.01h-.157zm0 2.873V18.1H9.9c.469 0 .804-.002 1.007-.006.202-.003.39-.046.56-.13a.712.712 0 0 0 .357-.33c.067-.142.099-.302.099-.483 0-.237-.04-.42-.121-.547-.078-.13-.214-.228-.405-.291a1.842 1.842 0 0 0-.538-.072 49.47 49.47 0 0 0-.716-.003h-.366z" fill="#26a69a" stroke-width="1.067"/></symbol><symbol viewBox="0 0 24 24" id="c" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 15.97l.42 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96-1.14-1.27-1.68-2.88-1.68-4.83C6 9.9 6.68 8.13 8 6.89 9.28 5.64 10.92 5 12.9 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.6 2.49-1.04-.34c-.4-.1-.87-.15-1.4-.15-1.15-.01-2.11.36-2.86 1.1-.76.73-1.14 1.85-1.18 3.34.01 1.36.37 2.42 1.08 3.2.71.77 1.7 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.09-.32z" fill="#0277bd"/></symbol><symbol viewBox="0 0 300 300" id="cabal" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -822.52)" fill-rule="evenodd" color="#000"><rect transform="matrix(-.98339 .18149 .60192 .79856 0 0)" x="405.55" y="967.22" width="107.25" height="156.59" rx="12.306" ry="12.31" fill="#2d9bbd"/><rect transform="matrix(-.98528 .17093 -.59175 .80612 0 0)" x="-1156.5" y="1461.9" width="108.34" height="123.15" rx="10.69" ry="12.31" fill="#4a4bcd"/><path d="M52.112 965.158c-1.343 3.515-26.292 23.248-25.744 27.277.548 4.03 29.812 16.023 32.04 19.027s10.545 41.668 13.603 42.5 18.828-31.274 21.548-32.932c2.72-1.658 32.808 2.503 34.15-1.01 1.343-3.515-18.174-35.352-18.721-39.381-.548-4.03 9.732-40.12 7.502-43.125-2.229-3.005-30.06 9.427-33.118 8.594-3.059-.833-26.793-27.3-29.514-25.643-2.72 1.657-.405 41.177-1.747 44.693z" fill="#2e5bc1"/></g></symbol><symbol viewBox="0 0 24 24" id="cake" xmlns="http://www.w3.org/2000/svg"><path d="M12.254 6.621a1.807 1.807 0 0 0 1.808-1.807c0-.344-.09-.66-.262-.932l-1.546-2.684-1.546 2.684a1.72 1.72 0 0 0-.262.932 1.808 1.808 0 0 0 1.808 1.807m4.158 9.04l-.967-.976-.976.976c-1.175 1.166-3.236 1.175-4.42 0l-.959-.976-.994.976a3.134 3.134 0 0 1-3.977.353v4.167a.904.904 0 0 0 .904.904h14.463a.904.904 0 0 0 .904-.904v-4.167a3.134 3.134 0 0 1-3.977-.353m1.265-6.328h-4.52V7.525H11.35v1.808H6.83a2.712 2.712 0 0 0-2.711 2.712v1.392c0 .977.795 1.772 1.771 1.772.489 0 .94-.18 1.248-.515l1.952-1.926 1.908 1.926c.669.669 1.835.669 2.504 0l1.916-1.926 1.944 1.926c.316.334.768.515 1.247.515.976 0 1.78-.795 1.78-1.772v-1.392a2.712 2.712 0 0 0-2.711-2.712z" fill="#ff7043" stroke-width=".904"/></symbol><symbol viewBox="0 0 24 24" id="certificate" xmlns="http://www.w3.org/2000/svg"><path d="M4 3c-1.11 0-2 .89-2 2v10a2 2 0 0 0 2 2h8v5l3-3 3 3v-5h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4m8 2l3 2 3-2v3.5l3 1.5-3 1.5V15l-3-2-3 2v-3.5L9 10l3-1.5V5M4 5h5v2H4V5m0 4h3v2H4V9m0 4h5v2H4v-2z" fill="#ff5722"/></symbol><symbol viewBox="0 0 24 24" id="changelog" xmlns="http://www.w3.org/2000/svg"><path d="M11 7v5.11l4.71 2.79.79-1.28-4-2.37V7m0-5C8.97 2 5.91 3.92 4.27 6.77L2 4.5V11h6.5L5.75 8.25C6.96 5.73 9.5 4 12.5 4a7.5 7.5 0 0 1 7.5 7.5 7.5 7.5 0 0 1-7.5 7.5c-3.27 0-6.03-2.09-7.06-5h-2.1c1.1 4.03 4.77 7 9.16 7 5.24 0 9.5-4.25 9.5-9.5A9.5 9.5 0 0 0 12.5 2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="clojure" xmlns="http://www.w3.org/2000/svg"><path d="M3.355 1.78c-.845 0-1.525.68-1.525 1.525v17.441c0 .845.68 1.525 1.525 1.525h17.442c.845 0 1.525-.68 1.525-1.525V3.305c0-.845-.68-1.526-1.525-1.526H3.355zm6.168 2.572h1.963l6.368 14.931H15.93l-3.38-8.086-3.349 8.086H7.21l4.346-10.38-2.032-4.551z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="cmake" xmlns="http://www.w3.org/2000/svg"><path d="M11.99 2.965L2.977 20.999l9.874-8.47-.863-9.564z" fill="#1e88e5"/><path d="M12.007 2.963l.002.29 1.312 14.498-.001.006.023.26 7.362 2.979h.416l-.158-.311-.114-.228h-.002l-8.84-17.494z" fill="#e53935"/><path d="M8.607 16.11L2.98 20.995h17.743v-.016L8.607 16.11z" fill="#7cb342"/></symbol><symbol class="bfmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate" xmlns="http://www.w3.org/2000/svg"><path class="bfsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#eee" stroke-width="2.849"/></symbol><symbol class="bgmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate_light" xmlns="http://www.w3.org/2000/svg"><path class="bgsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#455a64" stroke-width="2.849"/></symbol><symbol viewBox="0 0 24 24" id="coffee" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="coldfusion" xmlns="http://www.w3.org/2000/svg"><rect transform="rotate(90)" x="2.283" y="-21.86" width="19.487" height="19.487" ry="0" fill="#0d3858" stroke="#4dd0e1" stroke-width=".7"/><text x="6.653" y="16.426" fill="#4dd0e1" font-family="Calibri" font-size="29.001" font-weight="bold" letter-spacing="0" stroke-width=".725" word-spacing="0" style="line-height:1.25"><tspan x="6.653" y="16.426" font-family="'Segoe UI'" font-size="10.634" font-weight="normal">C<tspan font-size="11.844">f</tspan></tspan></text></symbol><symbol viewBox="0 0 24 24" id="conduct" xmlns="http://www.w3.org/2000/svg"><path d="M10 17l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9m-6-6a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#cddc39"/></symbol><symbol viewBox="0 0 24 24" id="console" xmlns="http://www.w3.org/2000/svg"><path d="M20 19V7H4v12h16m0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16m-7 14v-2h5v2h-5m-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z" fill="#ff7043"/></symbol><symbol viewBox="0 0 24 24" id="contributing" xmlns="http://www.w3.org/2000/svg"><path d="M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="cpp" xmlns="http://www.w3.org/2000/svg"><path d="M10.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C1.56 15.77 1 14.16 1 12.21c.05-2.31.72-4.08 2-5.32C4.32 5.64 5.96 5 7.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M11 11h2V9h2v2h2v2h-2v2h-2v-2h-2v-2m7 0h2V9h2v2h2v2h-2v2h-2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="credits" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v2H3V3m4 4h10v2H7V7m-4 4h18v2H3v-2m4 4h10v2H7v-2m-4 4h18v2H3v-2z" fill="#9ccc65"/></symbol><symbol viewBox="0 0 200 200" id="crystal" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" stroke-width="1.153" fill="#cfd8dc"/></symbol><symbol viewBox="0 0 200 200" id="crystal_light" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" fill="#37474f" stroke-width="1.153"/></symbol><symbol viewBox="0 0 24 24" id="csharp" xmlns="http://www.w3.org/2000/svg"><path d="M11.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C2.56 15.77 2 14.16 2 12.21c.05-2.31.72-4.08 2-5.32C5.32 5.64 6.96 5 8.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M13.89 19l.61-4H13l.34-2h1.5l.32-2h-1.5L14 9h1.5l.61-4h2l-.61 4h1l.61-4h2l-.61 4H22l-.34 2h-1.5l-.32 2h1.5L21 15h-1.5l-.61 4h-2l.61-4h-1l-.61 4h-2m2.95-6h1l.32-2h-1l-.32 2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="css" xmlns="http://www.w3.org/2000/svg"><path d="M5 3l-.65 3.34h13.59L17.5 8.5H3.92l-.66 3.33h13.59l-.76 3.81-5.48 1.81-4.75-1.81.33-1.64H2.85l-.79 4 7.85 3 9.05-3 1.2-6.03.24-1.21L21.94 3H5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="css-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#42a5f5"/><path d="M4.676 3l-.488 2.51h10.211l-.33 1.623H3.864l-.496 2.502H13.58l-.57 2.863-4.119 1.36-3.569-1.36.248-1.232H3.06l-.593 3.005 5.898 2.254 6.8-2.254.902-4.53.18-.91L17.406 3H4.675z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 33 33" id="cucumber" xmlns="http://www.w3.org/2000/svg"><title>cucumber-mark-transparent-pips</title><g transform="translate(0 -5)" fill="none" fill-rule="evenodd"><path d="M-4-1h40v40H-4z"/><path d="M16.641 7.092c-7.028 0-12.714 5.686-12.714 12.714 0 6.187 4.435 11.327 10.288 12.471v3.64C21.824 34.77 28.561 28.73 29.063 20.8c.303-4.772-2.076-9.644-6.09-12.01a10.575 10.575 0 0 0-1.455-.728l-.243-.097c-.223-.082-.448-.175-.68-.242a12.614 12.614 0 0 0-3.954-.632zm2.62 4.707a1.387 1.387 0 0 0-1.213.485c-.233.31-.379.611-.534.923-.466 1.087-.31 2.251.388 3.105 1.087-.233 2.01-.927 2.475-2.014a2.45 2.45 0 0 0 .243-1.02c.048-.824-.634-1.404-1.359-1.479zm-5.654.073c-.708.068-1.382.63-1.382 1.407 0 .31.087.709.243 1.02.466 1.086 1.46 1.78 2.546 2.013.621-.854.782-2.018.316-3.105-.155-.311-.3-.617-.534-.85a1.364 1.364 0 0 0-1.188-.485zm-3.809 3.735c-1.224.063-1.77 1.602-.752 2.402.31.233.612.403.922.559 1.087.466 2.344.306 3.275-.316-.233-1.009-1.023-1.936-2.11-2.402-.388-.155-.703-.243-1.092-.243-.087-.009-.161-.004-.243 0zm11.961 4.708a3.551 3.551 0 0 0-2.013.582c.233 1.01 1.023 1.936 2.11 2.401.389.156.705.244 1.093.244 1.397.077 2.08-1.65.994-2.427-.31-.233-.611-.379-.922-.534a3.354 3.354 0 0 0-1.262-.266zm-10.603.072a3.376 3.376 0 0 0-1.261.267c-.389.155-.69.325-.923.558-1.009.854-.33 2.48 1.068 2.402.388 0 .782-.087 1.092-.243 1.087-.465 1.859-1.392 2.014-2.401a3.474 3.474 0 0 0-1.99-.582zm3.931 2.378c-1.087.233-2.009.927-2.475 2.014-.155.31-.243.684-.243.995-.077 1.32 1.724 2.028 2.5 1.02.233-.312.378-.613.534-.923.466-1.01.306-2.174-.316-3.106zm2.887.073c-.621.854-.781 2.019-.315 3.106.155.31.3.615.534.848.854.932 2.65.243 2.572-.921 0-.31-.088-.71-.243-1.02-.466-1.087-1.46-1.78-2.547-2.013z" fill="#4caf50" stroke-width=".776"/></g></symbol><symbol id="cuda" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style>.bust0{fill:#76b900}</style><title>NVIDIA-Logo</title><path id="buEye_Mark" class="bust0" d="M76.362 75.199V64.116c1.095-.068 2.19-.137 3.284-.137 30.377-.958 50.286 26.135 50.286 26.135s-21.483 29.83-44.539 29.83c-3.079 0-6.089-.48-8.962-1.438v-33.66c11.836 1.436 14.23 6.636 21.277 18.471l15.804-13.273s-11.562-15.12-30.992-15.12c-2.053-.068-4.105.069-6.158.274m0-36.67v16.556l3.284-.205c42.213-1.437 69.784 34.618 69.784 34.618s-31.608 38.45-64.516 38.45c-2.873 0-5.678-.274-8.483-.753v10.262c2.326.274 4.72.48 7.046.48 30.65 0 52.817-15.668 74.3-34.14 3.558 2.874 18.13 9.784 21.14 12.794-20.388 17.104-67.937 30.856-94.893 30.856-2.6 0-5.062-.137-7.525-.41v14.436h116.44V38.532zm0 79.977v8.757C48.038 122.2 40.17 92.712 40.17 92.712s13.615-15.05 36.192-17.514v9.579h-.068c-11.836-1.437-21.14 9.646-21.14 9.646s5.268 18.678 21.209 24.082M26.077 91.481S42.839 66.714 76.43 64.115v-9.03C39.213 58.094 7.057 89.565 7.057 89.565s18.199 52.68 69.305 57.47v-9.579c-37.492-4.652-50.286-45.975-50.286-45.975z" fill="#8bc34a" stroke-width=".684"/></symbol><symbol viewBox="0 0 24 24" id="dart" xmlns="http://www.w3.org/2000/svg"><title>Dart</title><path d="M12.486 1.385a.978.978 0 0 0-.682.281l-.01.007-6.387 3.692 6.371 6.372v.004l7.659 7.659 1.46-2.63-5.265-12.64-2.456-2.457a.972.972 0 0 0-.69-.288z" fill="#00ca94"/><path d="M5.422 5.35L1.73 11.733l-.007.01a.967.967 0 0 0 .006 1.371l3.059 3.061 11.963 4.706 2.704-1.502-.073-.073-.018.002-7.5-7.512h-.01L5.423 5.35z" fill="#1565c0"/><path d="M5.405 5.353l6.518 6.525h.01l7.502 7.51 2.855-.544.005-8.449-3.016-2.955c-.66-.647-1.675-1.064-2.695-1.202l.002-.032-11.181-.853z" fill="#1565c0"/><path d="M5.414 5.361l6.521 6.522v.009l7.506 7.506-.546 2.855h-8.448l-2.954-3.017c-.647-.66-1.064-1.676-1.2-2.696l-.033.003L5.414 5.36z" fill="#00ee94"/></symbol><symbol viewBox="0 0 24 24" id="database" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.58 3 4 4.79 4 7s3.58 4 8 4 8-1.79 8-4-3.58-4-8-4M4 9v3c0 2.21 3.58 4 8 4s8-1.79 8-4V9c0 2.21-3.58 4-8 4s-8-1.79-8-4m0 5v3c0 2.21 3.58 4 8 4s8-1.79 8-4v-3c0 2.21-3.58 4-8 4s-8-1.79-8-4z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="diff" xmlns="http://www.w3.org/2000/svg"><path d="M3 1c-1.11 0-2 .89-2 2v11c0 1.11.89 2 2 2h2v-2H3V3h11v2h2V3c0-1.11-.89-2-2-2H3m6 6c-1.11 0-2 .89-2 2v2h2V9h2V7H9m4 0v2h1v1h2V7h-3m5 0v2h2v11H9v-2H7v2c0 1.11.89 2 2 2h11c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-2m-4 5v2h-2v2h2c1.11 0 2-.89 2-2v-2h-2m-7 1v3h3v-2H9v-1H7z" fill="#42a5f5"/></symbol><symbol id="docker" viewBox="0 0 41 34.5" xmlns="http://www.w3.org/2000/svg"><style id="bystyle2">.byst0{fill:#fff}.byst1{clip-path:url(#bySVGID_4_)}</style><g id="byg34" transform="translate(.292 1.9)" fill="#0087c9"><g id="byg32"><g id="byg30"><g id="byg28"><g id="byg26"><g id="byg9"><path id="bySVGID_1_" class="byst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g></g></g></g></g></g></symbol><symbol viewBox="0 0 24 24" id="document" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m9 16v-2H6v2h9m3-4v-2H6v2h12z" fill="#42a5f5"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone_light" xmlns="http://www.w3.org/2000/svg"><g fill="#424242" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol viewBox="0 0 3473 3473" id="editorconfig" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" xmlns="http://www.w3.org/2000/svg"><defs id="ccdefs4"><style id="ccstyle2">.ccfil2{fill:#020202}.ccfil0{fill:#e3e3f8}.ccfil5{fill:#efefef}.ccfil6{fill:#faf1f1}.ccfil3{fill:#fdf2f2}.ccfil1{fill:#fdfdfd}.ccfil4{fill:#fef3f3}</style></defs><g id="ccLayer_x0020_1" transform="matrix(.8945 0 0 .8945 138.649 275.985)"><g id="cc_631799120"><g id="ccg11"><path class="ccfil0" d="M967 1895c46-30 84-105 61-158-63 27-60 89-61 158z" id="ccpath7" fill="#e3e3f8"/><path class="ccfil0" d="M1679 2067c50-16 98-72 71-130-39 27-64 64-71 130z" id="ccpath9" fill="#e3e3f8"/></g><g id="ccg21"><path class="ccfil1" d="M280 2895c0 63 16 131 60 155 162 91 730 20 923-23 101-23 183-98 278-139 214-93 369-168 540-293 124-91 321-347 342-500l-169-38c-4 172-43 211-196 251-103 28-304 34-409 16-139-23-202-96-265-179-122-162 27-275-166-286-203 249-561 70-718 45-67 97-224 727-222 871 97-33 158 3 245 37 308 119 39 224-84 193-84-20-110-75-159-110z" id="ccpath13" fill="#fdfdfd"/><path class="ccfil1" d="M683 1458c125 24 236 76 342 129 173 86 204 74 220 194 2 22-2 34 61 54 106 33-61-26 223-25 169 1 556 69 681 148 52 33 42 75 218 70-2-207-57-516-138-706-99-230-230-265-497-351-156-50-614-105-756-17-133 83-158 182-282 356-36 51-49 90-72 148z" id="ccpath15" fill="#fdfdfd"/><path class="ccfil1" d="M1784 1883c100 41-5 306-144 242-45-127 62-199 91-256-60-9-231-36-282-17-66 25-81 166-47 232 160 314 867 247 792 3-30-99-58-115-159-149-81-27-162-55-251-55z" id="ccpath17" fill="#fdfdfd"/><path class="ccfil1" d="M527 1848c80 77 261 89 378 95 15-155 28-271 152-262 61 83 29 181-35 244 109-1 172-83 156-202-92-66-371-198-511-217-39 42-135 272-140 342z" id="ccpath19" fill="#fdfdfd"/></g><path class="ccfil2" d="M339 2838c66-6 238 44 252 100-107 13-243 3-252-100zm-59 57c49 35 75 90 159 110 123 31 392-74 84-193-87-34-148-70-245-37-2-144 155-774 222-871 157 25 515 204 718-45 193 11 44 124 166 286 63 83 126 156 265 179 105 18 306 12 409-16 153-40 192-79 196-251l169 38c-21 153-218 409-342 500-171 125-326 200-540 293-95 41-177 116-278 139-193 43-761 114-923 23-44-24-60-92-60-155zm1399-828c7-66 32-103 71-130 27 58-21 114-71 130zm105-184c89 0 170 28 251 55 101 34 129 50 159 149 75 244-632 311-792-3-34-66-19-207 47-232 51-19 222 8 282 17-29 57-136 129-91 256 139 64 244-201 144-242zm-817 12c1-69-2-131 61-158 23 53-15 128-61 158zm-440-47c5-70 101-300 140-342 140 19 419 151 511 217 16 119-47 201-156 202 64-63 96-161 35-244-124-9-137 107-152 262-117-6-298-18-378-95zm-100-80c-37-102-37-261 120-274l-80 223c-21 48-21 37-40 51zm256-310c23-58 36-97 72-148 124-174 149-273 282-356 142-88 600-33 756 17 267 86 398 121 497 351 81 190 136 499 138 706-176 5-166-37-218-70-125-79-512-147-681-148-284-1-117 58-223 25-63-20-59-32-61-54-16-120-47-108-220-194-106-53-217-105-342-129zm1770-49c-19-63 16-59 77-102 35-25 63-51 106-75 161-90 461-105 589 2 52 43 137 127 124 237-27 219-177 339-300 439-125 102-333 207-548 137-18-44-4-323-25-426-19-92-9-102 44-157 156-162 494-280 686-141 81 60 58 83 100 129 52-56-45-244-403-232-243 8-348 198-450 189zM997 840c5-139 133-427 261-527 155-120 317-233 555-98 59 33 56 50 62 132 5 79-2 108-22 172-158 510-290 217-796 338 19-166 163-314 243-391 137-133 236-219 442-191 57 95 63 155-6 266-92 148-115 139-101 240 72-18 94-88 127-158 201-420-91-471-270-394-120 51-334 287-404 429-14 28-29 64-42 95zm792 21c21-125 145-156 145-541 0-166-204-315-471-204-229 94-264 166-386 350-115 174-111 365-210 526-29 46-55 62-87 108-23 34-40 77-63 117-47 77-95 133-133 225-120 3-221 5-233 129-16 170 64 212 64 276-1 69-281 765-203 1180 22 114 97 115 217 129 289 35 664 23 923-81l470-225c119-67 319-194 408-287 63-65 96-120 150-197 74-108 76-106 92-253 98 18 281 61 342 114-7 69-41 36-41 98 39 1 104-48 120-102-41-60-84-50-143-98 47-37 132-54 197-81 140-58 379-234 438-394 47-129 12-344-64-428-80-88-266-133-418-133-181 0-368 130-514 186-56-49-60-105-101-159-47-64-353-224-499-255z" id="ccpath23" fill="#020202"/><path class="ccfil3" d="M2453 1409c102 9 207-181 450-189 358-12 455 176 403 232-42-46-19-69-100-129-192-139-530-21-686 141-53 55-63 65-44 157 21 103 7 382 25 426 215 70 423-35 548-137 123-100 273-220 300-439 13-110-72-194-124-237-128-107-428-92-589-2-43 24-71 50-106 75-61 43-96 39-77 102z" id="ccpath25" fill="#fdf2f2"/><path class="ccfil4" d="M997 840l49-87c13-31 28-67 42-95 70-142 284-378 404-429 179-77 471-26 270 394-33 70-55 140-127 158-14-101 9-92 101-240 69-111 63-171 6-266-206-28-305 58-442 191-80 77-224 225-243 391 506-121 638 172 796-338 20-64 27-93 22-172-6-82-3-99-62-132-238-135-400-22-555 98-128 100-256 388-261 527z" id="ccpath27" fill="#fef3f3"/><path class="ccfil5" d="M427 1768c19-14 19-3 40-51l80-223c-157 13-157 172-120 274z" id="ccpath29" fill="#efefef"/><path class="ccfil6" d="M591 2938c-14-56-186-106-252-100 9 103 145 113 252 100z" id="ccpath31" fill="#faf1f1"/></g></g></symbol><symbol viewBox="0 0 24 24" id="elixir" xmlns="http://www.w3.org/2000/svg"><path d="M12.431 22.383c-3.86 0-6.99-3.64-6.99-8.13 0-3.678 2.774-8.172 4.916-10.91 1.014-1.295 2.931-2.321 2.931-2.321s-.982 5.238 1.683 7.318c2.365 1.847 4.105 4.25 4.105 6.363 0 4.232-2.784 7.68-6.645 7.68z" fill="#9575cd" stroke-width="1.256"/></symbol><symbol viewBox="0 0 323.00001 322.99999" id="elm" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.8053 0 0 .8053 30.106 31.524)"><path fill="#f0ad00" d="M160.8 153.865l68.028-68.03H92.77z"/><path fill="#7fd13b" d="M160.983 5.098H12.033l68.524 68.525H229.51z"/><path fill="#7fd13b" stroke-width=".974" d="M243.906 88.021l74.136 74.137-74.474 74.475-74.137-74.137z"/><path fill="#60b5cc" d="M318.2 145.045V5.098H178.252z"/><path fill="#5a6378" d="M152.164 162.499L3.4 13.733v297.533z"/><path fill="#f0ad00" d="M252.205 245.27l65.995 65.996v-131.99z"/><path fill="#60b5cc" d="M160.8 171.134L12.034 319.899h297.53z"/></g></symbol><symbol viewBox="0 0 24 24" id="email" xmlns="http://www.w3.org/2000/svg"><path d="M20 8l-8 5-8-5V6l8 5 8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 30 30" id="erlang" xmlns="http://www.w3.org/2000/svg"><path style="line-height:1.25;-inkscape-font-specification:'Wide Latin'" d="M5.217 4.367c-.048.052-.097.1-.144.153C2.697 7.182 1.51 10.798 1.51 15.366c0 4.418 1.156 7.862 3.46 10.34h19.414c2.553-1.152 4.127-3.43 4.127-3.43l-3.147-2.52-1.454 1.381c-.866.773-.845.931-2.314 1.78-1.496.674-3.04.966-4.634.966-2.516 0-4.423-.909-5.723-2.059-1.286-1.15-1.985-4.511-2.097-6.68l17.458.067-.182-1.472s-.847-7.129-2.542-9.372zm8.76.846c1.565 0 3.22.535 3.96 1.471.742.937.932 1.667.974 3.524H9.12c.111-1.955.436-2.81 1.372-3.697.937-.888 2.03-1.298 3.484-1.298z" font-weight="400" font-size="48" font-family="Wide Latin" letter-spacing="0" word-spacing="0" fill="#f44336" stroke-width=".97"/></symbol><symbol viewBox="0 0 299.99999 300.00001" id="eslint" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-2.88 18.438) scale(1.0344)"><path d="M97.021 99.016l48.432-27.962c1.212-.7 2.706-.7 3.918 0l48.433 27.962a3.92 3.92 0 0 1 1.959 3.393v55.924a3.924 3.924 0 0 1-1.959 3.394l-48.433 27.962c-1.212.7-2.706.7-3.918 0l-48.432-27.962a3.92 3.92 0 0 1-1.959-3.394v-55.924a3.922 3.922 0 0 1 1.959-3.393" fill="#7986cb"/><path d="M273.34 124.49L215.473 23.82c-2.102-3.64-5.985-6.325-10.188-6.325H89.545c-4.204 0-8.088 2.685-10.19 6.325L21.488 124.27c-2.102 3.641-2.102 8.236 0 11.877l57.867 99.847c2.102 3.64 5.986 5.501 10.19 5.501h115.74c4.203 0 8.087-1.805 10.188-5.446l57.867-100.01c2.104-3.639 2.104-7.907.001-11.547m-47.917 48.41c0 1.48-.891 2.849-2.174 3.59l-73.71 42.527a4.194 4.194 0 0 1-4.17 0l-73.767-42.527c-1.282-.741-2.179-2.109-2.179-3.59V87.847c0-1.481.884-2.849 2.167-3.59l73.707-42.527a4.185 4.185 0 0 1 4.168 0l73.772 42.527c1.283.741 2.186 2.109 2.186 3.59z" fill="#3f51b5"/></g></symbol><symbol viewBox="0 0 24 24" id="exe" xmlns="http://www.w3.org/2000/svg"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14m0 14V8H5v10h14z" fill="#e64a19"/></symbol><symbol viewBox="0 0 24 24" id="favicon" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.45 4.73L5.82 21 12 17.27z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="file" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 400 400" id="firebase" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 103)"><path d="M72.55 208.77l44.456-292.29 56.209 90.445L195.49-37.57 330.6 209.28z" fill="#ffa712"/><path d="M195.7 276.73l134.9-67.45-46.5-224.83L72.55 208.77z" fill="#fcca3f"/><path d="M173.22 6.932L72.56 208.772l136.35-144.58z" fill="#f6820c"/></g></symbol><symbol viewBox="0 0 24 24" id="flash" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cma"><stop offset="0" stop-color="#d92f3c"/><stop offset="1" stop-color="#791223"/></linearGradient><linearGradient xlink:href="#cma" id="cmb" x1="2.373" y1="12.027" x2="21.86" y2="12.027" gradientUnits="userSpaceOnUse" gradientTransform="translate(-.09 -24.144)"/></defs><rect width="19.487" height="19.487" x="2.283" y="-21.86" transform="rotate(90)" ry="0" fill="url(#cmb)"/><path style="line-height:125%" d="M16.802 5.768l-.013.002a6.43 6.43 0 0 0-1.182.192 5.062 5.062 0 0 0-1.494.718c-.428.323-.817.72-1.17 1.191-.34.48-.682 1.032-1.022 1.66-.12.228-.233.424-.35.636v.002h-.004l-1.34 2.394-.005-.002c-.238.443-.461.847-.665 1.198a4.358 4.358 0 0 1-.716.94 2.79 2.79 0 0 1-.907.594c-.072.027-.161.042-.242.063h-.989v2.414h.989v-.002a6.427 6.427 0 0 0 1.185-.192 5.062 5.062 0 0 0 1.494-.718 5.94 5.94 0 0 0 1.171-1.191c.34-.48.681-1.033 1.021-1.66.12-.228.235-.425.353-.637l.006.002.003-.005.037-.066h2.53v.002h1.124v-2.408h-.33v-.001h-1.98c.22-.407.432-.789.621-1.115.214-.37.452-.682.717-.94a2.79 2.79 0 0 1 .906-.594c.07-.027.16-.041.239-.061h.992V8.18h-.002V5.77h-.977v-.002z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol class="cnflow-logo" viewBox="0 0 299.99999 300" id="flow" xmlns="http://www.w3.org/2000/svg"><title>Flow logo</title><path d="M38.75 33.427l77.461 77.47H54.436l61.145 61.16H38.437l93.462 93.478v-77.158l.01-.01v-77.47h-.01V66.982h46.691l20.394 20.393H153.57v76.531h22.05l24.474 24.473h-15.806l-.01-.01v.01h-31.665l-.01-.01v.01h-.313l.313.313v77.148h109.149l-39.2-39.2v-15.806l8.465 8.466v-77.37h-15.682l.017-38.191 30.09 30.086V56.362h-64.874l-22.94-22.934H113.71z" fill="#fbc02d" fill-opacity=".976" stroke-width=".955" class="cnflow-logo-mark"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="coa" x1="-388.15%" x2="237.68%" y1="-144.18%" y2="430.41%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cob" x1="72.945%" x2="-97.052%" y1="84.424%" y2="-147.7%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="coc" x1="-283.88%" x2="287.54%" y1="-693.6%" y2="101.71%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cod" x1="-821.19%" x2="101.99%" y1="-469.05%" y2="288.24%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coe" x1="-140.36%" x2="419.01%" y1="-230.93%" y2="261.98%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cof" x1="191.08%" x2="20.358%" y1="253.95%" y2="20.403%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cog" x1="-388.09%" x2="237.67%" y1="-173.85%" y2="518.99%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#coa"/><linearGradient id="coj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cob"/><linearGradient id="cok" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#coc"/><linearGradient id="col" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cod"/><linearGradient id="com" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#coe"/><linearGradient id="con" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cof"/><linearGradient id="coo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cog"/><linearGradient id="cop" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#coh"/><linearGradient id="coh" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#coi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#coj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cok)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#col)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#com)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#con)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#coo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cop)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia-open" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cpi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpa"/><linearGradient id="cpa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cpb"/><linearGradient id="cpb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#cpc"/><linearGradient id="cpc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cpd"/><linearGradient id="cpd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#cpe"/><linearGradient id="cpe" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cpf"/><linearGradient id="cpf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpg"/><linearGradient id="cpg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpp" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#cph"/><linearGradient id="cph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#cpi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#cpj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cpk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#cpl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#cpm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#cpn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#cpo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cpp)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#cddc39" fill-rule="nonzero"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#cddc39"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00acc1" fill-rule="nonzero"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00acc1"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#e57373" fill-rule="nonzero"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" stroke-width=".644"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#e57373"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" fill-rule="evenodd" stroke-width=".644"/></symbol><symbol id="folder-docker" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><defs id="cydefs10"><path id="cySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><path id="cypath2" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><style id="cystyle2">.cyst0{fill:#fff}.cyst1{clip-path:url(#cySVGID_4_)}</style><g id="cyg34" transform="translate(8.319 9.626) scale(.39491)" fill="#b3e5fc"><g id="cyg32"><g id="cyg30"><title id="cytitle4">Group 3</title><g id="cyg28"><g id="cyg26"><g id="cyg9"><path id="cySVGID_1_" class="cyst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="cyg24"><clipPath id="cySVGID_4_"><use id="cyuse14" width="100%" height="100%" xlink:href="#cySVGID_2_"/></clipPath><g id="cyg22" class="cyst1" clip-path="url(#cySVGID_4_)"><g id="cyg20"><g id="cyg18"><path id="cySVGID_3_" class="cyst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docker-open" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="cza"><use width="100%" height="100%" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#SVGID_2_"/></clipPath></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><g transform="matrix(.3949 0 0 .39489 8.319 9.626)" fill="#b3e5fc"><title>Group 3</title><path class="czst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/><g class="czst1" clip-path="url(#cza)"><path class="czst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#01579b" fill-rule="nonzero"/><style>.dcst0{fill:#1173b6}.st1{fill:#585d67}</style><path class="dcst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#01579b"/><path class="ddst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M21.132 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.217 1.217m.608-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M21.133 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.216 1.217m.609-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ffca28" fill-rule="nonzero"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.411 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill-rule="nonzero" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ffca28"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.412 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#c0ca33" fill-rule="nonzero"/><path d="M17.39 12.544a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#c0ca33"/><path d="M17.391 12.543a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.036 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.968 4.968 0 0 1-2.679 2.203m-.155-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.1-1.238h2.894c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399h-1.806a4.902 4.902 0 0 1 2.672-2.202c-.37.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.049.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.94 4.94 0 0 1 2.679 2.202m-4.281-3.712a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.037 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.967 4.967 0 0 1-2.68 2.203m-.154-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.099-1.238h2.895c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399H13.06a4.902 4.902 0 0 1 2.672-2.202c-.371.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.05.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.941 4.941 0 0 1 2.679 2.202M17.34 9.322a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M16.473 13.927c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a17.015 17.015 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359a5.558 5.558 0 0 0-.203.604c.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.569-1.053c-.21-.372-.435-.702-.639-1.032-.38-.022-.78-.022-1.2-.022-.422 0-.823 0-1.202.022-.204.33-.428.66-.639 1.032l-.569 1.053.57 1.054c.21.372.434.702.638 1.032.38.021.78.021 1.201.021.421 0 .822 0 1.201-.02.204-.331.428-.661.639-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.245-9.284c-.436-.267-1.405.14-2.522 1.194.366.414.724.864 1.06 1.334.577.057 1.146.14 1.686.253.359-1.503.225-2.535-.224-2.78m-.492 4.03l.204.358c.077-.203.154-.407.203-.604-.19-.042-.4-.077-.618-.112l.211.358m1.018-4.95c1.033.589 1.145 2.141.71 3.953 1.784.527 3.069 1.398 3.069 2.584 0 1.187-1.285 2.058-3.07 2.585.436 1.812.324 3.364-.709 3.954-1.025.59-2.423-.085-3.77-1.37-1.35 1.285-2.747 1.96-3.78 1.37-1.025-.59-1.137-2.142-.702-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.203 6.538c.239.527.45 1.054.625 1.588 1.475-.443 2.303-1.075 2.303-1.588 0-.512-.828-1.144-2.303-1.587a15.81 15.81 0 0 1-.625 1.587m-7.136 0a15.806 15.806 0 0 1-.625-1.587c-1.474.443-2.303 1.075-2.303 1.587 0 .513.829 1.145 2.303 1.588.176-.534.387-1.06.625-1.588m6.321 1.588l-.21.358c.217-.035.428-.07.617-.113-.049-.196-.126-.4-.203-.604l-.204.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.522 1.194.45-.246.583-1.278.224-2.781-.54.112-1.11.196-1.685.253-.337.47-.695.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.113.049.196.126.4.203.604l.204-.359m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M16.473 13.928c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a16.997 16.997 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359c-.077.204-.154.408-.203.604.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.57-1.053c-.21-.372-.434-.702-.638-1.032-.38-.022-.78-.022-1.201-.022-.421 0-.822 0-1.2.022-.205.33-.43.66-.64 1.032l-.569 1.053.569 1.054c.21.372.435.702.64 1.032.378.021.779.021 1.2.021.421 0 .822 0 1.2-.02.205-.33.43-.661.64-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.244-9.284c-.435-.267-1.404.14-2.52 1.194.364.414.723.864 1.06 1.334.575.057 1.144.14 1.685.253.358-1.503.225-2.535-.225-2.78m-.491 4.03l.203.358c.078-.203.155-.407.204-.604-.19-.042-.4-.077-.618-.112l.21.358m1.02-4.95c1.032.589 1.144 2.141.708 3.953 1.784.527 3.07 1.398 3.07 2.584 0 1.187-1.286 2.058-3.07 2.585.436 1.812.323 3.364-.709 3.954-1.025.59-2.423-.085-3.771-1.37-1.348 1.285-2.746 1.96-3.778 1.37-1.026-.59-1.138-2.142-.703-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.204 6.538c.24.527.45 1.054.625 1.588 1.475-.443 2.304-1.075 2.304-1.588 0-.512-.829-1.144-2.304-1.587a15.81 15.81 0 0 1-.625 1.587m-7.135 0a15.808 15.808 0 0 1-.625-1.587c-1.475.443-2.303 1.075-2.303 1.587 0 .513.828 1.145 2.303 1.588.176-.534.386-1.06.625-1.588m6.32 1.588l-.21.358c.218-.035.428-.07.618-.113a5.56 5.56 0 0 0-.204-.604l-.203.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.521 1.194.45-.246.583-1.278.225-2.781-.54.112-1.11.196-1.685.253-.338.47-.696.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.112.049.197.126.4.203.604l.204-.358m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#fbc02d" fill-rule="nonzero"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.434h-1.217v8.518a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#fbc02d"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.433h-1.217v8.519a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#4caf50" fill-rule="nonzero"/><g fill="#c8e6c9" transform="translate(2.065 -.225) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#4caf50"/><g fill="#c8e6c9" fill-rule="evenodd" transform="translate(2.064 -.224) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1e88e5" fill-rule="nonzero"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1e88e5"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.811 8.52l-5.988 5.506-3.346-2.522-1.383.805 3.298 3.03-3.298 3.032 1.383.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.622v6.396l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.81 8.52l-5.988 5.506-3.346-2.522-1.384.805 3.3 3.03-3.3 3.032 1.384.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.621v6.397l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><g transform="translate(8.459 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688"/><g transform="translate(8.458 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#03a9f4" fill-rule="nonzero"/><g transform="translate(9.192 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#03a9f4"/><g transform="translate(9.193 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol viewBox="0 0 24 24" id="font" xmlns="http://www.w3.org/2000/svg"><path d="M9.62 12L12 5.67 14.37 12M11 3L5.5 17h2.25l1.12-3h6.25l1.13 3h2.25L13 3h-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 500 500" id="fsharp" xmlns="http://www.w3.org/2000/svg"><path d="M235.906 36.66L21.963 250.601l213.943 213.943v-84.36L106.209 250.487l129.697-129.696z" fill="#378bba" stroke-width="14.706"/><path d="M235.906 156.614l-93.622 93.62 93.622 93.622z" fill="#378bba" stroke-width="15.006"/><path d="M263.417 36.64L477.36 250.583 263.417 464.526v-84.36l129.696-129.697-129.696-129.696z" fill="#30b9db" stroke-width="14.706"/></symbol><symbol viewBox="0 0 152.99 160.01" id="fusebox" xmlns="http://www.w3.org/2000/svg"><defs id="fkdefs4"><style id="fkstyle2">.fkcls-1{fill:#fff}.fkcls-2{fill:#515151}.fkcls-3{fill:#1d79bf}.fkcls-4{fill:#383838}</style></defs><title id="fktitle6">Asset 3</title><g id="fkLayer_2" data-name="Layer 2" transform="matrix(.87285 0 0 .87285 10.17 10.175)"><g id="fkFuse_Box" data-name="Fuse Box"><g id="fkLOGO"><path class="fkcls-1" id="fkpolygon8" fill="#fff" d="M76.56 2.19l74.22 24.93-7.7 87.77-65.41 42.66-69.79-43.93-5.7-86.13z"/><path class="fkcls-2" d="M77.69 160L5.87 114.81 0 26 76.55 0 153 25.67l-7.94 90.4zM9.88 112.43l67.77 42.66 63.45-41.39 7.47-85.13-72-24.18L4.36 28.95z" id="fkpath10" fill="#515151"/><path class="fkcls-3" id="fkpolygon12" fill="#1d79bf" d="M76.4 148.8V61.68l66.93-29.82-5.99 78.77z"/><path id="fkF" class="fkcls-4" fill="#383838" d="M76.4 148.8l-60.35-37.39L9.63 31.8 76.4 61.68z"/><path class="fkcls-1" d="M25.58 52.73l.54 15.93 37.35 18.18.12 14.69-37-18.21 1.64 37.1-14.56-9-5.05-80.55 67.79 30.82v15.46z" id="fkpath15" fill="#fff"/><path class="fkcls-1" d="M135.91 90.77c-.08 13.12-6.33 26.59-16.77 33.12l-42.8 27.93V61.71l42.27-18.84c5.16-2.41 9.51-1.43 12.4 3.11 1.9 3 2.89 7.23 2.86 12.21A35.69 35.69 0 0 1 129.34 76c4.29 2 6.66 6.55 6.57 14.77zM123 63.76c0-4.64-2-6.93-4.92-5.45l-29 14.48L89 90l29.44-15.59c2.5-1.32 4.56-5.91 4.56-10.65zM125.15 96c0-5.71-2.42-8.24-6.55-5.93L89 106.64v19.58l29.34-17.46c4.43-2.64 6.79-7.27 6.81-12.76z" id="fkpath17" fill="#fff"/><path id="fkTOP" class="fkcls-4" fill="#383838" d="M76.4 8.82L9.71 31.77l109.77 2.38-84.02 9.21L76.4 61.68l20.76-9.25-27.73-1.37 49.78-8.46 24.12-10.74z"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="git" xmlns="http://www.w3.org/2000/svg"><path d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2 2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2 2 2 0 0 0 2-2 2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82z" fill="#e64a19"/></symbol><symbol viewBox="0 0 494 455" id="gitlab" xmlns="http://www.w3.org/2000/svg"><title>logo</title><defs><path id="fma" d="M0 1173.3h2000V0H0v1173.3z"/></defs><g transform="matrix(.88256 0 0 -.88256 -286.767 742.766)" fill="none" fill-rule="evenodd"><mask id="fmb" fill="#fff"><use width="100%" height="100%" xlink:href="#fma"/></mask><g><g transform="translate(358.67 358.67)"><path d="M492.532 195.445l-27.559 84.815-54.617 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.619-168.1h-181.37l-54.62 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.617-168.1-27.557-84.815a18.775 18.775 0 0 1 6.82-20.992l238.51-173.29 238.51 173.29a18.777 18.777 0 0 1 6.82 20.992" fill="#fc6d26"/><path d="M247.2 1.16l90.684 279.1h-181.37z" fill="#e24329"/><path d="M247.201 1.16l-90.684 279.09H29.427z" fill="#fc6d26"/><path d="M29.422 280.256L1.862 195.44a18.774 18.774 0 0 1 6.822-20.991L247.194 1.16z" fill="#fca326"/><path d="M29.422 280.26h127.09l-54.619 168.1c-2.81 8.65-15.047 8.65-17.856 0z" fill="#e24329"/><path d="M247.2 1.16l90.684 279.09h127.09z" fill="#fc6d26"/><path d="M464.98 280.256l27.559-84.815a18.774 18.774 0 0 0-6.821-20.991L247.208 1.16z" fill="#fca326"/><path d="M464.97 280.26H337.88l54.619 168.1c2.81 8.65 15.047 8.65 17.856 0z" fill="#e24329"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="go" xmlns="http://www.w3.org/2000/svg"><path d="M10.575 1.695c-2.634 0-4.756 2.453-4.756 5.502v4.6l-.027-.003v4.71c0 3.05 2.123 5.502 4.757 5.502h2.286c2.634 0 4.757-2.453 4.757-5.502v-4.6a5.1 5.1 0 0 0 .026.003v-4.71c0-3.049-2.122-5.502-4.756-5.502h-2.287z" fill="#73cddc"/><rect width="2.289" height="3.335" x="-1.178" y="6.092" ry="1.125" transform="matrix(.4849 -.87457 .85979 .51065 0 0)" fill="#73cddc"/><rect width="2.297" height="3.39" x="10.261" y="-15.076" ry="1.143" transform="matrix(.44646 .8948 -.89204 .45195 0 0)" fill="#73cddc"/><circle cx="9.267" cy="5.13" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><circle cx="14.214" cy="5.116" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><ellipse cx="8.039" cy="5.051" rx=".792" ry=".901" fill="#030d18"/><path d="M11.792 9.556l.763.138a.403.689 0 0 1 .008.138.403.689 0 0 1-.402.69.403.689 0 0 1-.404-.69.403.689 0 0 1 .035-.276z" fill="#fff" stroke="#fff" stroke-width=".155"/><ellipse cx="8.51" cy="5.365" rx=".138" ry=".166" fill="#fff"/><ellipse cx="12.945" cy="5.189" rx=".792" ry=".901" fill="#030d18"/><ellipse cx="13.414" cy="5.446" rx=".138" ry=".166" fill="#fff"/><ellipse cx="-12.982" cy="-3.409" rx=".708" ry="1.026" transform="rotate(-129.403)" fill="#f6d2a1" stroke-width=".4"/><path d="M11.772 9.553l-.757.135a.4.672 0 0 0-.008.135.4.672 0 0 0 .4.672.4.672 0 0 0 .4-.672.4.672 0 0 0-.035-.27z" fill="#fff" stroke="#fff" stroke-width=".153"/><ellipse cx="1.841" cy="-21.563" rx=".707" ry="1.026" transform="scale(1 -1) rotate(50.597)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="-17.281" cy="-21.784" rx=".864" ry="1.27" transform="matrix(.3054 -.95222 -.97065 -.2405 0 0)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="22.885" cy="2.587" rx=".864" ry="1.27" transform="matrix(.22652 .974 .95652 -.29167 0 0)" fill="#f6d2a1" stroke-width=".4"/><path d="M10.708 8.392a.594.594 0 0 0-.594.597v.115c0 .331.264.598.594.598h.386a.973.772 0 0 1 .697-.235.973.772 0 0 1 .698.235h.334c.33 0 .594-.267.594-.598V8.99a.595.595 0 0 0-.594-.597h-2.115z" fill="#f6d2a1" stroke="#657075" stroke-width=".1"/><ellipse cx="11.734" cy="8.203" rx="1.208" ry=".68" fill="#030d18" stroke="#fff" stroke-width=".162"/></symbol><symbol viewBox="0 0 24 24" id="gradle" xmlns="http://www.w3.org/2000/svg"><path d="M21.718 5.503c-.731-1.315-2.04-1.708-2.963-1.727-1.133-.023-2.065.605-1.888 1.017.037.088.25.55.38.741.19.275.527.064.646 0 .353-.187.73-.248 1.16-.198.409.048.954.3 1.319 1.001.859 1.652-1.794 5.05-5.114 2.697-3.32-2.353-6.548-1.574-8.01-1.1-1.462.475-2.135.952-1.556 2.055.785 1.498.524 1.038 1.285 2.28 1.21 1.97 3.856-.908 3.856-.908-1.972 2.906-3.662 2.204-4.31 1.188a15.864 15.864 0 0 1-1.038-1.97c-4.993 1.76-3.642 9.534-3.642 9.534h2.48c.632-2.862 2.892-2.757 3.28 0h1.892c1.673-5.59 5.914 0 5.914 0h2.466c-.69-3.812 1.388-5.01 2.697-7.246 1.31-2.235 2.551-4.969 1.146-7.364zm-6.362 7.362c-1.304-.426-.837-1.723-.837-1.723s1.139.368 2.68.87c-.09.403-.856 1.175-1.843.853z" fill="#0097a7" stroke-width=".47"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 300 300" id="graphcool" xmlns="http://www.w3.org/2000/svg"><path d="M246.886 107.727c-12.237-6.892-27.616 2.1-30.081 3.646l-52.834 29.965c-7.8-6.196-18.914-5.933-26.412.625-7.499 6.558-9.24 17.537-4.14 26.094 5.102 8.556 15.588 12.246 24.923 8.768 9.335-3.478 14.852-13.129 13.111-22.937l52.688-29.9.321-.196c3.464-2.188 11.5-5.462 15.256-3.34 2.706 1.524 4.252 6.629 4.376 14.148h-.066v66.092a17.313 17.313 0 0 1-8.635 14.95l-75.739 43.755a17.312 17.312 0 0 1-17.261 0l-75.74-43.756a17.312 17.312 0 0 1-8.634-14.95V113.22c.01-6.165 3.3-11.86 8.634-14.95l68.549-39.562c6.522 7.482 17.451 9.25 26 4.206s12.283-15.468 8.886-24.794c-3.397-9.327-12.962-14.904-22.751-13.27-9.79 1.636-17.022 10.02-17.204 19.944L59.397 85.632a31.932 31.932 0 0 0-15.978 27.588v87.454a31.933 31.933 0 0 0 15.927 27.602l75.74 43.755a31.934 31.934 0 0 0 31.846 0l75.74-43.755a31.933 31.933 0 0 0 15.927-27.58V137.12h.05c.373-14.913-3.616-24.794-11.762-29.389z" fill="#27ae60" stroke="#27ae60" stroke-width="7.883622079999999"/></symbol><symbol viewBox="0 0 400 400" id="graphql" xmlns="http://www.w3.org/2000/svg"><path d="M67.008 293.022l-13.143-7.588L200.282 31.839l13.143 7.588z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M50.855 265.174H343.69v15.177H50.855z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M203.122 358.269L56.649 273.7l7.589-13.143 146.472 84.568zm127.24-220.407L183.889 53.293l7.589-13.143 146.472 84.568z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M64.278 137.803l-7.588-13.142 146.472-84.568 7.588 13.143z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M327.661 293.025L181.244 39.43l13.143-7.589 146.417 253.596zM62.466 114.597h15.176v169.136H62.466zm254.528 0h15.176v169.136h-15.176z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M200.538 351.845l-6.628-11.481L321.3 266.812l6.629 11.48z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M352.284 288.67c-8.777 15.268-28.342 20.48-43.61 11.703-15.268-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.36 8.869 20.57 28.342 11.703 43.61M97.574 141.567c-8.778 15.268-28.343 20.48-43.61 11.703-15.269-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.268 8.869 20.479 28.342 11.702 43.61M42.353 288.67c-8.777-15.268-3.566-34.741 11.702-43.61 15.268-8.776 34.741-3.565 43.61 11.703 8.776 15.268 3.565 34.741-11.703 43.61-15.36 8.776-34.833 3.565-43.61-11.703m254.71-147.103c-8.776-15.268-3.565-34.741 11.703-43.61 15.268-8.776 34.742-3.565 43.61 11.703 8.777 15.268 3.566 34.741-11.702 43.61-15.268 8.776-34.833 3.565-43.61-11.703m-99.745 236.608c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907c0 17.554-14.262 31.907-31.907 31.907m0-294.206c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907-14.262 31.907-31.907 31.907" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/></symbol><symbol viewBox="0 0 24 24" id="groovy" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.982a10.119 10.119 0 0 0-10.12 10.12A10.119 10.119 0 0 0 12 22.22 10.119 10.119 0 0 0 22.12 12.1 10.119 10.119 0 0 0 12 1.983zm1.254 2.422c.91 0 1.647.261 2.213.78.571.518.857 1.188.857 2.013 0 .889-.319 1.673-.959 2.35-.64.677-1.376 1.015-2.207 1.015-.486 0-.89-.119-1.213-.357-.317-.238-.476-.532-.476-.88 0-.212.06-.4.181-.563.127-.164.274-.246.438-.246.159 0 .238.092.238.277 0 .164.06.29.182.38.121.09.261.136.42.136.423 0 .828-.29 1.215-.866.391-.582.587-1.202.587-1.863 0-.465-.151-.844-.453-1.135-.301-.296-.69-.445-1.166-.445-.714 0-1.406.318-2.078.953-.666.635-1.211 1.47-1.635 2.506-.417 1.031-.627 2.014-.627 2.945 0 .857.185 1.54.555 2.047.37.503.863.754 1.477.754 1.037 0 2.027-.734 2.974-2.2l1.493-.212c.185-.026.277.018.277.135 0 .053-.072.28-.215.681-.143.402-.337 1.074-.586 2.016.82-.476 1.455-1.003 1.904-1.58v.914c-.36.418-1.046.888-2.062 1.412-.212 1.407-.682 2.493-1.406 3.26-.725.772-1.54 1.16-2.444 1.16-.433 0-.775-.102-1.023-.303-.243-.2-.365-.477-.365-.832 0-.984.955-1.94 2.865-2.865.2-.714.395-1.356.586-1.928-.333.482-.817.907-1.451 1.278-.635.37-1.225.554-1.77.554-.889 0-1.628-.383-2.22-1.15-.588-.772-.881-1.748-.881-2.928 0-1.243.333-2.42 1-3.531a7.747 7.747 0 0 1 2.625-2.674c1.084-.672 2.134-1.008 3.15-1.008zM12.03 16.592c-1.375.687-2.062 1.365-2.062 2.031 0 .354.169.533.508.533.666 0 1.184-.856 1.554-2.564z" fill="#26c6da"/></symbol><symbol viewBox="0 0 24 24" id="gulp" xmlns="http://www.w3.org/2000/svg"><path d="M8.37 15.94a596.238 596.238 0 0 1-.482-4.982c.002-.042-.225-.077-.505-.077h-.508V8.95h3.966V5.198l1.871-1.124c1.14-.685 1.978-1.125 2.144-1.125.4 0 .866.506.866.939 0 .19-.057.422-.127.517-.07.095-.722.53-1.45.966l-1.321.792-.029 1.393-.028 1.393h3.972v1.932h-.98l-.495 4.983-.495 4.983H8.854l-.485-4.906z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="h" xmlns="http://www.w3.org/2000/svg"><path d="M16.745 19.818h-3.007v-5.882q0-2.381-1.736-2.381-.869 0-1.438.663-.56.662-.56 1.718v5.882H6.988V4.533h3.016v6.508h.037q1.186-1.802 3.193-1.802 3.511 0 3.511 4.239z" stroke-width=".478" fill="#0277bd"/></symbol><symbol viewBox="0 0 253.6 253.6" id="hack" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-29.243 -29.515) scale(1.2301)"><path fill="#607d8b" d="M69.496 159.551v52.576l51.77-52.576zM123.507 41.523l-54.01 52.755v55.084l54.01-54.009z"/><path fill="#eceff1" d="M130.023 95.663v51.501l52.128-51.5z"/><path fill="#607d8b" d="M185.465 101.867l-55.442 55.174v55.083l55.442-55.262z"/><path fill="#ffa000" d="M73.068 154.283l50.427.09v-50.248z"/></g></symbol><symbol viewBox="0 0 300 300.00001" id="haml" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 165.6)"><path d="M78.42-132.307c-12.047-.302-26.924 5.998-26.924 5.998l49.195 99.791L74.605 85.005c23.81 20.134 50.07 10.504 50.07 10.504L136.76 9.212c1.526 1.446 3.146 2.77 4.777 3.995 5.244 3.714 10.925 6.553 16.606 8.738 5.68 2.185 11.583 3.933 17.482 5.244 3.933.874 7.645 1.53 11.578 1.967-1.748 3.933-2.84 8.083-2.621 12.672 0 .437.22.873.656 1.092h.217c4.152 2.185 8.521 3.934 13.328 5.027 4.589.874 9.615 1.312 14.422.656 5.026-.655 10.051-2.623 13.984-5.9 3.933-3.278 6.774-7.648 8.522-12.237l.219-.218v-.217l.656-5.899v-.22c2.185-1.311 4.37-2.621 6.555-4.37 2.622-2.184 5.025-4.589 6.773-7.648 1.748-3.059 2.84-6.774 2.621-10.488-.218-3.496-1.53-6.99-3.06-10.049-1.53-3.059-3.495-5.901-5.68-8.523-4.37-5.026-9.614-9.176-15.295-12.454-5.462-3.496-11.581-6.338-17.7-8.304l-2.404-.656-1.962-.655c-1.311-.437-2.406-1.092-3.498-1.53-2.185-1.31-3.717-2.622-4.809-4.37-2.185-3.278-2.403-8.301-1.31-13.545.218-1.311.656-2.623 1.093-3.934a96.064 96.064 0 0 0 1.31-4.152c.314-1.412.51-2.829.598-4.402l29.203-25.553c-2.275-8.404-27.488-17.158-27.488-17.158l-74.931 63.726-43.243-81.584c-1.553-.35-3.218-.527-4.94-.57zm107.682 73.14c-.449 2.336-.647 4.795-.647 7.258.219 3.715 1.311 7.87 3.715 11.366 2.403 3.496 5.68 6.117 8.957 7.646a29.663 29.663 0 0 0 5.027 1.967l2.623.654 2.184.438c5.68 1.53 11.142 3.714 16.168 6.554 5.025 2.84 9.833 6.337 13.766 10.27s6.992 8.959 7.43 13.984c.218 3.496-.22 6.118-1.313 8.303-1.093 2.404-2.84 4.588-4.807 6.555-.874.874-1.966 1.747-2.84 2.402a27.11 27.11 0 0 0-.654-5.898c-.219-1.093-.438-1.966-.875-3.059-.437-.874-.872-1.966-1.965-2.621-.218 0-.44-.001-.44.217-1.31 3.277-3.494 6.12-5.898 8.086-2.403 1.966-5.462 2.84-8.521 3.058-3.06.219-6.338-.436-9.616-1.31-3.277-.874-6.552-1.968-9.83-3.06l-.439-.22c-.656-.218-1.526.002-1.963.44-1.748 2.185-3.06 4.149-4.59 6.334a58.435 58.435 0 0 0-2.84 5.027c-3.933-1.53-7.649-2.841-11.582-4.37-5.462-2.186-10.925-4.37-15.95-6.991-5.245-2.404-10.268-5.246-14.638-8.524-3.15-2.363-6.062-4.845-8.185-7.681l2.404-17.172z" fill="#f4511e" stroke-width="0" stroke-linejoin="round"/></g></symbol><symbol viewBox="0 0 24 24" id="handlebars" xmlns="http://www.w3.org/2000/svg"><path d="M8.55 10.32c-2.753 0-4.202 3.48-5.793 3.48-.98 0-1.126-.677-1.126-.915 0-.332.236-.706.564-.706.59 0 .414.77.414.77s.798-.555.272-1.298c-.42-.595-1.31-.623-1.92-.17-.617.458-1.057 1.146-.853 2.287.1.551.468 1.35 1.233 1.805.764.455 1.925.566 2.335.566 2.194 0 4.342-1.633 6.639-2.322a5.513 5.513 0 0 1 1.497-.222 6.19 6.19 0 0 1 1.92.226c2.296.689 4.444 2.323 6.638 2.323.41 0 1.57-.11 2.335-.566.765-.455 1.132-1.256 1.231-1.807.204-1.14-.235-1.829-.853-2.287-.61-.453-1.497-.423-1.918.172-.526.743.27 1.297.27 1.297s-.176-.77.414-.77c.329 0 .565.373.565.705 0 .238-.147.914-1.126.914-1.592 0-3.04-3.478-5.794-3.478-2.565 0-3.076 1.177-3.462 1.718-.004.005-.005.011-.008.016-.005-.006-.007-.013-.012-.02-.386-.54-.896-1.717-3.461-1.717z" fill="#ff7043" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 300.00001 300" id="haskell" xmlns="http://www.w3.org/2000/svg"><g stroke-width="2.422"><path d="M23.928 240.5l59.94-89.852-59.94-89.855h44.955l59.94 89.855-59.94 89.852z" fill="#ef5350"/><path d="M83.869 240.5l59.94-89.852-59.94-89.855h44.955l119.88 179.71h-44.95l-37.46-56.156-37.468 56.156z" fill="#ffa726"/><path d="M228.72 188.08l-19.98-29.953h69.93v29.956h-49.95zm-29.97-44.924l-19.98-29.953h99.901v29.953z" fill="#ffee58"/></g></symbol><symbol viewBox="0 0 210 210" id="haxe" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -87)"><path fill="#f68712" stroke-width=".221" d="M42.78 191.545l63.431-63.43 63.431 63.43-63.431 63.431z"/><path d="M42.8 191.592L31.193 148.28 19.59 104.97 62.9 116.575l43.311 11.605-31.706 31.706z" fill="#fab20b" stroke-width=".266"/><path d="M105.956 128.111l-43.19-11.544-43.177-11.597 22.927.185 23.228.294 20.264 11.36z" fill="#fbc707" stroke-width=".265"/><path d="M19.59 104.97l11.596 43.176 11.545 43.19-11.303-19.948-11.36-20.263-.294-23.228z" fill="#fff200" stroke-width=".265"/><path d="M106.23 128.133l43.312-11.605 43.311-11.605-11.605 43.31-11.605 43.312-31.706-31.706z" fill="#f47216" stroke-width=".266"/><path d="M169.711 191.289l11.545-43.19 11.597-43.176-.185 22.927-.294 23.228-11.36 20.263z" fill="#f1471d" stroke-width=".265"/><path d="M192.853 104.923l-43.176 11.597-43.19 11.544 19.947-11.303 20.264-11.36 23.228-.293z" fill="#fbc707" stroke-width=".265"/><path d="M169.643 191.545l11.605 43.31 11.605 43.312-43.311-11.605-43.311-11.606 31.706-31.705z" fill="#f25c19" stroke-width=".266"/><path d="M106.487 255.025l43.19 11.544 43.176 11.598-22.927-.185-23.228-.294-20.264-11.36z" fill="#f68712" stroke-width=".265"/><path d="M192.853 278.167l-11.597-43.176-11.545-43.19 11.303 19.947 11.36 20.264.294 23.228z" fill="#f1471d" stroke-width=".265"/><path d="M106.211 254.976l-43.31 11.605-43.312 11.605 11.605-43.31L42.8 191.563l31.706 31.706z" fill="#f89c0e" stroke-width=".266"/><path d="M42.731 191.82l-11.545 43.19-11.597 43.176.185-22.927.294-23.228 11.36-20.263z" fill="#fff200" stroke-width=".265"/><path d="M19.59 278.186l43.175-11.597 43.19-11.544-19.947 11.303-20.264 11.36-23.228.293z" fill="#f25c19" stroke-width=".265"/></g></symbol><symbol viewBox="0 0 144 152" id="heroku" xmlns="http://www.w3.org/2000/svg"><path d="M118.68 13.279H26.865c-6.337 0-11.476 5.139-11.476 11.476V129.32c0 6.338 5.139 11.477 11.476 11.477h91.813c6.338 0 11.477-5.14 11.477-11.477V24.755c0-6.337-5.139-11.476-11.477-11.476zM44.08 121.669V96.165l14.346 12.752zm44.632 0v-38.08c-.063-2.976-1.496-6.551-7.97-6.551-12.966 0-27.51 6.52-27.654 6.586l-9.008 4.08V32.407h12.752v36.201c6.366-2.072 15.266-4.321 23.91-4.321 7.882 0 12.6 3.099 15.17 5.698 5.484 5.547 5.56 12.613 5.551 13.43v38.255zm3.188-68.54H79.149c5.011-6.576 8.158-13.496 9.564-20.723h12.751c-.86 7.243-3.796 14.187-9.563 20.722z" fill="#6963b9"/></symbol><symbol viewBox="0 0 24 24" id="hpp" xmlns="http://www.w3.org/2000/svg"><path d="M9.757 19.818H6.751v-5.882q0-2.381-1.737-2.381-.868 0-1.438.663-.56.662-.56 1.718v5.882H0V4.533h3.016v6.508h.037Q4.24 9.239 6.247 9.239q3.51 0 3.51 4.239z" stroke-width=".478" fill="#0277bd"/><path d="M13.073 11.448v2h-2v2h2v2h2v-2h2v-2h-2v-2zm7 0v2h-2v2h2v2h2v-2h2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="html" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.56l4.07-1.13.55-6.1H9.38L9.2 8.3h7.6l.2-1.99H7l.56 6.01h6.89l-.23 2.58-2.22.6-2.22-.6-.14-1.66h-2l.29 3.19L12 17.56M4.07 3h15.86L18.5 19.2 12 21l-6.5-1.8L4.07 3z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" id="http" xmlns="http://www.w3.org/2000/svg"><path d="M16.046 13.784c.074-.613.13-1.225.13-1.856s-.056-1.244-.13-1.856h3.137c.148.594.241 1.215.241 1.856a7.65 7.65 0 0 1-.241 1.856m-4.78 5.16c.557-1.03.984-2.144 1.281-3.304h2.738a7.452 7.452 0 0 1-4.019 3.304m-.232-5.16H9.828a12.314 12.314 0 0 1-.149-1.856c0-.631.056-1.253.149-1.856h4.343c.084.603.149 1.225.149 1.856 0 .63-.065 1.243-.149 1.856M12 19.315c-.77-1.113-1.393-2.348-1.773-3.675h3.545c-.38 1.327-1.002 2.562-1.773 3.675m-3.712-11.1h-2.71a7.353 7.353 0 0 1 4.01-3.304c-.557 1.03-.975 2.144-1.3 3.304m-2.71 7.425h2.71c.325 1.16.743 2.274 1.3 3.304a7.433 7.433 0 0 1-4.01-3.304m-.761-1.856a7.65 7.65 0 0 1-.241-1.856c0-.64.093-1.262.241-1.856h3.137c-.074.612-.13 1.225-.13 1.856 0 .63.056 1.243.13 1.856m4.046-9.253c.77 1.114 1.393 2.357 1.773 3.684h-3.545c.38-1.327 1.002-2.57 1.773-3.684m6.422 3.684h-2.738a14.523 14.523 0 0 0-1.28-3.304 7.412 7.412 0 0 1 4.018 3.304m-6.423-5.568c-5.132 0-9.28 4.176-9.28 9.28a9.28 9.28 0 0 0 9.28 9.282 9.28 9.28 0 0 0 9.281-9.281A9.28 9.28 0 0 0 12 2.647z" fill="#e53935" stroke-width=".928"/></symbol><symbol viewBox="0 0 24 24" id="image" xmlns="http://www.w3.org/2000/svg"><path d="M13.009 9.202h5.368l-5.368-5.368v5.368M6.177 2.37h7.808l5.856 5.856v11.711a1.952 1.952 0 0 1-1.952 1.952H6.178a1.951 1.951 0 0 1-1.952-1.952V4.322c0-1.083.868-1.952 1.952-1.952m0 17.567h11.71V12.13l-3.903 3.903-1.952-1.951-5.856 5.855M8.13 9.202a1.952 1.952 0 0 0-1.952 1.952 1.952 1.952 0 0 0 1.952 1.952 1.952 1.952 0 0 0 1.952-1.952A1.952 1.952 0 0 0 8.13 9.202z" fill="#26a69a" stroke-width=".976"/></symbol><symbol viewBox="0 0 512 512" id="ionic" xmlns="http://www.w3.org/2000/svg"><g fill="#4f8ff7"><path d="M423.592 132.804A31.855 31.855 0 0 0 429 115c0-17.675-14.33-32-32-32a31.853 31.853 0 0 0-17.805 5.409C344.709 63.015 302.11 48 256 48 141.125 48 48 141.125 48 256c0 114.877 93.125 208 208 208 114.873 0 208-93.123 208-208 0-46.111-15.016-88.71-40.408-123.196zM391.83 391.832c-17.646 17.646-38.191 31.499-61.064 41.174-23.672 10.012-48.826 15.089-74.766 15.089-25.94 0-51.095-5.077-74.767-15.089-22.873-9.675-43.417-23.527-61.064-41.174s-31.5-38.191-41.174-61.064C68.982 307.096 63.905 281.94 63.905 256c0-25.94 5.077-51.095 15.089-74.767 9.674-22.873 23.527-43.417 41.174-61.064s38.191-31.5 61.064-41.174c23.673-10.013 48.828-15.09 74.768-15.09 25.939 0 51.094 5.077 74.766 15.089a191.221 191.221 0 0 1 37.802 21.327A31.853 31.853 0 0 0 365 115c0 17.675 14.327 32 32 32 5.293 0 10.28-1.293 14.678-3.568a191.085 191.085 0 0 1 21.327 37.801c10.013 23.672 15.09 48.827 15.09 74.767 0 25.939-5.077 51.096-15.09 74.768-9.675 22.873-23.527 43.418-41.175 61.064z"/><circle cx="256.003" cy="256" r="96"/></g></symbol><symbol viewBox="0 0 24 24" id="java" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="javascript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v18H3V3m4.73 15.04c.4.85 1.19 1.55 2.54 1.55 1.5 0 2.53-.8 2.53-2.55v-5.78h-1.7V17c0 .86-.35 1.08-.9 1.08-.58 0-.82-.4-1.09-.87l-1.38.83m5.98-.18c.5.98 1.51 1.73 3.09 1.73 1.6 0 2.8-.83 2.8-2.36 0-1.41-.81-2.04-2.25-2.66l-.42-.18c-.73-.31-1.04-.52-1.04-1.02 0-.41.31-.73.81-.73.48 0 .8.21 1.09.73l1.31-.87c-.55-.96-1.33-1.33-2.4-1.33-1.51 0-2.48.96-2.48 2.23 0 1.38.81 2.03 2.03 2.55l.42.18c.78.34 1.24.55 1.24 1.13 0 .48-.45.83-1.15.83-.83 0-1.31-.43-1.67-1.03l-1.38.8z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="javascript-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#ffca28"/><path d="M2.444 2.506h14.135v14.136H2.444V2.506m3.714 11.811c.315.668.935 1.218 1.995 1.218 1.178 0 1.987-.629 1.987-2.003V8.993H8.805v4.508c0 .675-.275.848-.707.848-.455 0-.644-.314-.856-.683l-1.084.651m4.697-.14c.392.769 1.185 1.358 2.426 1.358 1.257 0 2.199-.652 2.199-1.854 0-1.107-.636-1.602-1.767-2.089l-.33-.141c-.573-.243-.816-.408-.816-.801 0-.322.243-.573.636-.573.377 0 .628.165.856.573l1.028-.683c-.432-.754-1.044-1.045-1.884-1.045-1.186 0-1.948.754-1.948 1.752 0 1.083.636 1.594 1.594 2.002l.33.141c.613.267.974.432.974.888 0 .377-.354.652-.903.652-.652 0-1.029-.338-1.312-.81l-1.083.63z" fill="#ffca28"/></symbol><symbol viewBox="0 0 180 180" id="jenkins" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="gia"><path transform="scale(1 -1)" fill="#37474f" d="M.899-144.42h144.42V0H.899z"/></clipPath></defs><g transform="matrix(1.0691 0 0 -1.0691 9.4 166.143)" clip-path="url(#gia)"><g fill-rule="evenodd"><path d="M107.96 30.661l-12.506-1.876-16.883-1.876-10.943-.312-10.629.312-8.13 2.502-7.19 7.815-5.628 15.945-1.25 3.44-7.504 2.5-4.377 7.191-3.126 10.317 3.44 9.067 8.128 2.814 6.565-3.127 3.127-6.878 3.752.626 1.25 1.563-1.25 7.19-.313 9.068 1.876 12.505-.074 7.143 5.701 9.114 10.005 7.19 17.508 7.504 19.383-2.814 16.883-12.193 7.817-12.505 5.002-9.067 1.25-22.51-3.752-19.384-6.877-17.195-6.566-9.066" fill="#f0d6b7"/><path d="M97.334-23.425l-44.709-1.876v-7.503l3.752-26.262-1.876-2.19-31.264 10.63-2.19 3.752-3.126 35.328-7.19 21.26-1.563 5.002 25.01 17.195 7.818 3.127 6.877-8.441 5.94-5.315 6.88-2.188 3.125-.938L68.57 1.899l2.814-3.44 7.19 2.502-5.002-9.693 27.2-12.818-3.439-1.876" fill="#335061"/><path d="M23.238 85.687l8.128 2.814 6.566-3.127 3.127-6.878 3.751.626.938 3.751-1.876 7.19 1.876 17.197-1.563 9.379 5.627 6.565 12.193 9.692-3.44 4.69-17.194-8.442-7.191-5.627-4.064-8.754-6.253-8.442-1.876-10.005 1.251-10.63" fill="#6d6b6d"/><path d="M36.055 115.07s4.69 11.567 23.448 17.195c18.759 5.628.938 4.065.938 4.065l-20.321-7.817-7.817-7.816-3.438-6.253 7.19.626M26.676 87.875s-6.566 21.886 18.446 25.012l-.938 3.752-17.195-4.065-5.003-16.257 1.251-10.63 3.439 2.188" fill="#dcd9d8"/></g><g fill="#f7e4cd"><path d="M36.681 58.799l4.094 3.966s1.847-.214 2.16-2.402c.312-2.19 1.25-21.886 14.693-32.516 1.227-.97-10.004 1.564-10.004 1.564L37.62 45.042M94.209 64.739s.729 9.477 3.28 8.748c2.553-.729 2.553-3.28 2.553-3.28s-6.198-4.01-5.833-5.468" fill-rule="evenodd"/><path d="M120.16 99.442s-5.153-1.088-5.628-5.628c-.474-4.54 5.628-.938 6.566-.625M82.327 99.129s-6.879-.938-6.879-5.314c0-4.378 7.817-4.065 10.005-2.19"/><g fill-rule="evenodd"><path d="M39.807 78.808s-11.881 7.191-13.131.312c-1.25-6.877-4.065-11.88 1.876-19.07l-4.064 1.25-3.752 9.691-1.25 9.38 7.19 7.504 8.129-.626 4.69-3.751.312-4.69M45.435 98.504s5.315 27.512 32.203 32.827c22.136 4.375 33.765-.938 38.142-5.94 0 0-19.696 23.447-38.455 16.257-18.759-7.191-32.514-20.322-32.202-28.762.532-14.377.313-14.382.313-14.382M117.97 122.27s-9.066.312-9.38-7.817c0 0 0-1.25.625-2.5 0 0 7.192 8.129 11.568 3.751"/><path d="M78.268 111.1s-1.56 12.477-12.199 5.223c-6.878-4.69-6.252-11.255-5.002-12.505s.91-3.77 1.862-2.04c.952 1.728.638 7.356 4.078 8.918 3.439 1.564 9.077 3.31 11.26.404"/></g></g><g fill="#49728b" fill-rule="evenodd"><path d="M48.874 26.597L19.486 13.466s12.193-48.46 5.94-63.467l-4.377 1.563-.313 18.446-8.128 35.015-3.44 9.692 30.639 20.633 9.067-8.753M51.896-.206l4.17-5.087v-18.76h-5.003s-.625 13.132-.625 14.696c0 1.563.624 7.19.624 7.19M52-26.866l-14.069-.625 4.065-2.813L52-31.868"/></g><g fill-rule="evenodd"><path d="M100.15-23.739l11.567.313 2.814-28.764-11.881-1.563-2.5 30.014" fill="#335061"/><path d="M103.27-23.739l17.508.938s7.19 18.133 7.19 19.07c0 .939 6.253 26.263 6.253 26.263l-14.069 14.694-2.813 2.501-7.504-7.503V3.148l-6.565-26.887" fill="#335061"/><path d="M111.09-21.55l-10.942-2.188 1.563-8.755c4.064-1.876 10.943 3.127 10.943 3.127M111.4 33.162l21.885-16.257.626 7.503-16.57 15.32-5.94-6.566" fill="#49728b"/><path d="M62.85-85.332l-6.473 26.266-3.22 19.38-.531 14.385 29.296 1.56 18.226.003-1.658-32.83 2.814-25.324-.312-4.69-23.76-1.876-14.382 3.126" fill="#fff"/><path d="M96.083-23.426s-1.563-32.515 3.127-55.65c0 0-9.38-5.94-23.136-7.503l26.262.938 3.126 1.875-3.752 51.273-.938 10.944" fill="#dcd9d8"/><path d="M115.06-49.691l12.193 3.44 23.135 1.25 3.44 10.629-6.254 18.446-7.19.938-10.005-3.127-9.599-4.686-5.095.935-3.972-1.56" fill="#fff"/><path d="M114.84-43.435s8.128 3.751 9.38 3.438L120.78-22.8l4.065 1.563s2.814-16.257 2.814-18.133c0 0 17.507-.938 19.07-.938 0 0 3.752 7.191 2.814 14.694l3.44-10.005.312-5.628-5.002-7.503-5.627-1.25-9.38.312-3.126 4.064-10.943-1.563-3.44-1.25" fill="#dcd9d8"/></g><path d="M102.56-21.241L95.682-3.733l-7.19 10.317s1.562 4.377 3.75 4.377h7.192l6.878-2.501-.625-11.568-3.127-18.134" fill="#fff"/><path d="M103.9-15.297S95.145 1.585 95.145 4.086c0 0 1.563 3.752 3.752 2.814 2.19-.938 6.879-3.439 6.879-3.439v5.94l-10.63 2.19-7.19-.939 12.193-28.763 2.5-.313" fill="#dcd9d8" fill-rule="evenodd"/><path d="M65.664 25.968l-8.661.942-8.13 2.501v-2.814l3.972-4.38 12.506-5.627" fill="#fff"/><path d="M51.689 25.031s9.693-4.065 12.819-3.127l.311-3.748-8.752 1.872-5.316 3.752.938 1.251" fill="#dcd9d8" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43" fill="#d33833" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669" fill="#d33833" fill-rule="evenodd"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695" fill="#d33833" fill-rule="evenodd"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M102.87 10.649s-2.19 3.127-.626 4.065c1.564.938 3.127 0 4.065 1.563s0 2.501.313 4.377 1.877 2.189 3.44 2.501c1.562.313 5.94.938 6.565-.625l-1.876 5.627-3.752 1.25-11.88-6.877-.626-3.44v-6.877M70.041.331c-.376 4.88-.773 9.752-1.215 14.626-.662 7.279 1.748 6.009 8.057 6.009.964 0 5.933-1.15 6.289-1.876 1.705-3.483-2.851-2.709 1.964-5.335 4.065-2.216 11.246 1.346 9.603 6.273-.919 1.095-4.789.341-6.176 1.06l-7.327 3.8c-3.108 1.612-10.29 3.962-13.603 1.709-8.395-5.71.53-19.974 3.524-25.93" fill="#ef3d3a" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M78.268 111.1c-8.521 1.985-12.755-3.566-15.338-9.323-2.306.559-1.389 3.695-.806 5.294 1.525 4.194 7.672 9.778 12.694 9.02 2.161-.325 5.086-2.301 3.45-4.99M119.79 101.4l.404-.016c1.926-4 3.593-8.238 6.022-11.769-1.628-3.79-12.322-7.144-12.157-.338 2.313 1.01 6.305.206 8.356 1.497-1.186 3.254-2.897 6.024-2.625 10.626M82.63 101.29c1.827-3.35 2.422-6.868 5.019-9.4 1.17-1.14 3.444-2.529 2.316-5.698-.263-.747-2.189-2.414-3.3-2.741-4.06-1.2-13.521-.248-10.317 4.814 3.358-.157 7.871-2.18 10.38.257-1.927 3.081-5.363 9.177-4.098 12.768M118.26 67.253c-6.113-3.927-12.93-8.197-22.947-7.207-2.14 1.86-2.956 6.002-.877 8.737 1.082-1.861.402-5.284 3.419-5.799 5.684-.972 12.299 3.477 16.387 5.032 2.535 4.275-.219 5.847-2.503 8.597-4.675 5.636-10.947 12.622-10.72 21.06 1.89 1.37 2.053-2.092 2.325-2.722 2.44-5.714 8.585-13.021 13.07-17.912 1.1-1.205 2.914-2.36 3.115-3.157.582-2.315-1.513-5.09-1.27-6.63M37.668 71.387c-1.916 1.094-2.372 5.91-4.622 6.048-3.215.195-2.629-6.25-2.616-10.018-2.213 2.009-2.602 8.194-.976 11.37-1.853.91-2.68-1.003-3.708-1.677 1.32 9.595 14.036 4.45 11.922-5.723M122.15 63.257c-2.846-5.417-6.871-11.382-15.222-11.555-.17 1.75-.3 4.411.009 5.464 6.384.614 10.325 3.863 15.212 6.091M82.149 59.745c5.326-2.8 15.114-3.102 22.353-2.89.388-1.586.379-3.545.394-5.48-9.305-.463-20.307 1.84-22.747 8.37M81.136 54.523c3.683-9.247 16.341-8.182 27.016-7.927-.47-1.2-1.489-2.62-2.755-3.132-3.42-1.392-12.855-2.448-17.604.074-3.011 1.601-4.946 5.219-6.596 7.34-.797 1.024-4.765 3.64-.06 3.645"/></g><path d="M117.82 3.516c-4.322-7.402-8.457-15.005-13.585-21.534 2.15 6.32 3.07 16.9 3.394 24.965 4.498 2.105 8.349-.474 10.191-3.43" fill="#81b0c4" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M141.07-23.089c-4.839-.969-8.239-5.671-12.959-5.37 2.594 3.658 7.14 5.2 12.959 5.37M143.21-30.661c-3.944-.417-8.576-1.055-12.577-.726 1.894 2.892 9.19 1.894 12.577.726M144.58-37.19c-4.433-.096-9.942-.008-14.155.346 2.492 2.677 11.28.993 14.155-.346"/></g><g fill-rule="evenodd"><path d="M109.48-55.057c.636-5.567 2.843-11.207 2.566-17.304-2.45-.827-3.858-1.55-7.142-1.545-.232 5.181-.925 13.102-.718 18.041 1.615-.107 3.997 1.154 5.294.808" fill="#dcd9d8"/><path d="M102.33 26.985c-2.226-1.453-4.121-3.267-6.259-4.818-4.74-.235-7.327.328-10.81 3.05.057.219.407.121.42.39 5.075-2.262 11.524.92 16.648 1.378" fill="#f0d6b7"/><path d="M75.694-7.603c1.394 6.04 6.857 9.17 11.817 12.497 5.12-6.498 8.234-14.855 11.663-22.92-8.102 2.443-16.38 6.406-23.481 10.423" fill="#81b0c4"/><path d="M104.18-55.865c-.207-4.94.486-12.86.718-18.041 3.283-.004 4.691.718 7.142 1.545.276 6.096-1.93 11.737-2.566 17.304-1.298.346-3.679-.914-5.294-.808zm-51.13 28.09c2.165-19.906 5.301-36.639 11.054-54.266 12.766-3.876 28.157-4.214 39.441-.716-2.072 9.948-1.167 22.06-2.378 32.677-.912 7.98-.447 16.009-1.698 24.15-13.673 2.844-33 .665-46.418-1.845zm49.651 1.72c-.115-8.549.383-16.982 1.036-25.542 3.282.493 5.51.822 8.56 1.49-.99 8.241-.869 17.514-2.886 24.804-2.332-.023-4.385.027-6.71-.752zm16.653 1.378c-1.558.357-3.372.014-4.86-.015.7-6.969 2.397-14.659 2.995-21.974 2.342-.073 3.593 1.032 5.52 1.403.102 6.421-.562 15.268-3.655 20.586zm25.215-23.038c4.882 1.186 7.952 7.165 6.586 13.305-.916 4.127-2.548 11.898-4.295 14.538-1.29 1.953-4.79 4.51-7.584 2.72-4.545-2.91-12.552-3.755-15.867-7.278 1.662-5.534 2.178-13.135 2.864-20.146 5.678-.354 12.665 1.562 17.387-.471-3.297-1.068-7.575-1.077-10.423-2.633 2.328-1.125 7.778-.897 11.332-.035zM99.17-18.025c-3.43 8.063-6.543 16.42-11.663 22.918-4.96-3.327-10.423-6.456-11.817-12.497 7.1-4.017 15.379-7.98 23.481-10.422zm8.453 24.971c-.325-8.065-1.245-18.644-3.395-24.965 5.128 6.53 9.263 14.132 13.585 21.534-1.842 2.957-5.693 5.536-10.19 3.431zm-9.582 3.405c-1.943.21-3.592-2.233-6.117-1.177-.58-.64-1.105-1.333-1.695-1.958 5.579-6.723 8.114-16.262 12.423-24.163 2.312 7.59 2.045 15.904 2.555 24.188-3.177-.201-4.94 2.873-7.166 3.11zm-6.161 8.132c-.208-2.303.328-3.056.791-5.695 7.57-2.367 6.248 10.388-.791 5.695zm-8.394 2.755c-3.261 1.782-8.161 3.723-12.374 4.527-5.222.999-4.732-7.123-4.51-11.968.173-3.836 2.168-7.893 3.035-10.441.406-1.19.498-2.453 1.515-2.69 1.798-.418 7.73 1.954 9.42 2.875 3.575 1.95 6.348 5.045 9.384 7.123.04 1.011.078 2.021.119 3.032-1.826.91-3.935 1.555-6.615 1.673 1.818.914 4.492.901 6.148 1.989.016.405.033.81.047 1.21-3.024.234-4.176 1.58-6.17 2.67zm-31.152 5.659c-2.707-2.748 7.592-6.494 10.871-6.696-.018 1.739.991 3.378.788 4.626-3.895.684-9.013.232-11.66 2.07zm33.345-1.29c-.013-.27-.363-.172-.42-.39 3.482-2.722 6.07-3.285 10.81-3.05 2.137 1.551 4.033 3.365 6.259 4.818-5.124-.458-11.574-3.64-16.648-1.379zm30.606-9.282c-.146 3.053-.948 9.332-2.835 10.431-3.961 2.312-11.002-4.668-13.984-5.732.324-.934.86-1.674.901-2.868 1.764.434 3.912.137 5.44-.615-1.767-.198-3.727-.185-4.897-1.027-.429-1.239.105-2.927-.18-4.647 4.196-1.184 8.989-1.814 14.294-1.97 1.032 1.341 1.383 3.896 1.261 6.429zM47.777 24.24c-.85.606-6.6 8.087-7.388 7.777-10.405-4.103-20.134-11.199-28.828-17.91 8.29-17.787 11.635-39.579 12.227-60.582 9.496-4.441 17.836-10.844 30.722-11.512-1.491 10.55-2.852 19.962-3.699 29.895-3.237 1.365-7.882-.062-10.913.423-.025 3.651 4.628 1.6 5.015 4.054.292 1.858-2.56 1.998-1.631 4.923 2.368-.861 3.612-2.763 6.138-3.477 2.309 5.05-.032 13.985.3 18.205.064.792.397 4.39 2.172 3.759 1.57-.559-.09-9.569.082-13.563.157-3.68-.444-7.242 1.046-9.552a355.817 355.817 0 0 0 38.576 3.16c-2.964 1.272-6.485 2.475-10.345 4.651-2.093 1.18-8.69 3.635-9.293 5.622-.964 3.167 2.528 4.855 3.125 7.57-6.285-3.428-7.511 3.286-8.998 8.042-1.347 4.308-2.114 7.526-2.445 10.01-5.414 2.581-11.203 5.195-15.863 8.505zm63.009 6.872c8.67 4.204 10.232-15.711 6.834-22.127.525-1.914 2.331-2.646 3.069-4.366-4.838-8.667-10.211-16.756-15.148-25.32 3.672 2.286 8.917.409 13.238 2.12 1.58.624 2.722 4.24 3.918 7.133 3.29 7.958 6.743 17.99 8.28 25.586.346 1.73 1.292 5.5 1.08 7.04-.378 2.758-4.12 4.803-6.022 6.508-3.506 3.15-5.714 5.921-9.371 8.866-1.483-2.189-4.666-3.66-5.878-5.44zM27.95 107.99c-4.13-4.545-3.266-13.062-2.766-19.121 7.467 4.697 17.377-.372 17.284-8.36 3.565.094 1.332 4.452.687 7.259-2.107 9.169 3.55 19.13.256 27.516-6.395-.485-11.649-3.097-15.46-7.294zm29.558 26.38c-9.352-2.65-21.337-9.446-25.18-17.847 2.976.432 5.041 1.933 7.977 2.119 1.11.072 2.563-.466 3.838-.148 2.54.63 4.685 6.327 6.602 8.447 1.868 2.07 4.114 2.954 5.651 4.841.988.477 2.448.444 2.504 1.927-.428.457-.879.806-1.392.66zm48.681-2.493c-9.707 5.477-26.136 9.596-36.462 4.449-8.331-4.155-19.593-11.027-23.433-19.737 3.587-8.405-1.062-16.106-1.36-24.64-.157-4.54 2.139-8.504 2.315-13.446-1.228-2.025-4.978-2.275-7.574-2.136-.873 4.372-2.403 9.287-6.906 9.78-6.371.697-11.03-4.576-11.319-10.085-.342-6.48 4.978-17.22 12.517-16.475 2.913.287 3.629 3.207 6.802 3.177 1.72-3.432-2.653-4.51-3.103-6.964-.117-.634.363-3.112.642-4.274 1.37-5.658 4.422-12.982 7.427-17.29 3.814-5.464 11.307-6.288 19.37-6.823 1.44 3.101 6.743 2.846 10.2 2.035-4.143 1.64-7.993 5.617-11.185 9.137-3.665 4.039-7.378 8.371-7.566 13.65 6.927-9.61 12.65-18.003 25.246-22.23 9.53-3.196 20.662 1.465 27.986 6.608 3.039 2.137 4.853 5.529 7.013 8.634 8.082 11.626 11.854 28.219 11.024 44.303-.342 6.633-.327 13.244-2.552 17.706-2.326 4.666-10.193 8.84-14.8 4.62-.853 4.537 3.83 7.344 9.331 5.71-3.922 5.063-8.039 11.145-13.614 14.29zm18.084-149.66c7.585 3.77 21.757 10.149 26.512-.014 1.755-3.746 3.814-10.079 4.723-13.946 1.284-5.456-1.392-16.923-7-18.754-4.953-1.617-10.733-1.518-16.7-.32-.702.585-1.484 1.603-2.03 2.665-4.261.165-8.25-.229-11.615-1.98.319-3.15-1.812-3.656-3.81-4.305-1.48-5.872 2.963-13.541 1.9-18.896-.76-3.815-5.453-4.405-8.902-5.118-.113-2.12.15-3.89.386-5.683-.789-2.907-4.327-4.561-7.679-4.967-11.029-1.326-27.775-1.922-38.384 1.893-2.96 7.261-5.292 16.093-7.758 24.384-10.346-1.105-18.715 4.464-26.603 8.113-2.731 1.266-6.51 1.964-7.53 4.138-.99 2.105-.584 6.14-.83 9.95-.625 9.733-1.16 19.12-3.73 29.086-1.154 4.472-3.165 8.418-4.568 12.727C9.358 5.184 7.092 10.12 6.5 14.1c-.877 5.903 4.681 6.232 8.235 8.79 5.494 3.954 9.806 6.142 15.756 9.711 1.762 1.057 7.077 3.733 7.681 4.966 1.202 2.443-2.062 5.888-2.935 7.803-1.38 3.03-2.1 5.602-2.298 8.59-4.992.789-8.775 3.76-11.06 7.109-3.781 5.543-6.403 15.798-3.132 23.599.257.614 1.536 1.822 1.725 2.765.372 1.858-.7 4.329-.768 6.305-.343 10.14 1.716 18.875 8.541 21.932 2.771 11.038 12.688 14.71 22.032 20.195 3.493 2.05 7.343 3.36 11.32 4.824 14.263 5.25 36.15 4.261 47.987-4.692 5.02-3.797 13.044-11.813 15.914-17.617 7.58-15.323 7.042-40.931 1.74-59.571-.712-2.503-1.746-6.181-3.19-9.187-1.006-2.1-4.134-6.3-3.754-8.153.391-1.916 7.132-7.034 8.577-8.428 2.603-2.51 7.548-5.843 7.948-9.012.43-3.372-1.485-7.984-2.456-11.238-3.245-10.858-6.412-20.895-10.091-30.576" fill="#231f20"/><path d="M73.674 57.38c.411.548 2.674 1.38 5.84-.144 0 0-3.752-.626-3.44-6.881l-1.564.313s-1.615 5.672-.836 6.712" fill="#f7e4cd"/><path d="M101.09 3.617a1.72 1.72 0 1 0-3.44.001 1.72 1.72 0 0 0 3.44-.001M102.81-4.355a1.72 1.72 0 1 0-3.44 0 1.72 1.72 0 0 0 3.44 0" fill="#1d1919"/></g><g><rect transform="matrix(.8 0 0 -.8 0 144)" x="16.854" y="177.38" width="70.412" height="4.12" rx=".983" ry=".983"/><rect transform="scale(1 -1)" x="78.502" y="-2.097" width="50.037" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="13.483" y="-3.697" width="54.831" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="83.296" y="-3.697" width="45.243" height="3.296" rx=".786" ry=".786"/></g></g></symbol><symbol viewBox="0 0 24 24" id="json" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#fbc02d"/></symbol><symbol viewBox="0 0 50 50" id="julia" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" stroke-width="5.673"><circle cx="13.497" cy="281.632" r="9.555" fill="#bc342d"/><circle cx="36.081" cy="281.632" r="9.555" fill="#864e9f"/><circle cx="24.722" cy="262.389" r="9.555" fill="#328a22"/></g></symbol><symbol viewBox="0 0 64 64" id="karma" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -233)"><path d="M38.556 288.413l-20.29-26.687 9.532-7.246 20.29 26.686h-.001.002l5.527 7.247z" fill="#359b8b" stroke-width=".173"/><path d="M35.681 241.172L24.92 255.327v-14.13H12.947v13.817l7.84 33.235h4.132v-13.147l.003.003 20.29-26.686-.008-.006 5.504-7.24H35.84v.12z" fill="#3cbeae" stroke-width=".206"/></g></symbol><symbol viewBox="0 0 24 24" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7 14a2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2 2 2 0 0 1-2 2m5.65-4A5.99 5.99 0 0 0 7 6a6 6 0 0 0-6 6 6 6 0 0 0 6 6 5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4H12.65z" fill="#26a69a"/></symbol><symbol viewBox="0 0 24 24" id="kivy" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.89 0 0 1.89 -12.157 -11.429)" fill="#90a4ae"><path d="M7.026 8.63v4.474l1.928-1.928a.437.437 0 0 0 0-.619zM9.38 16.072v-4.474l-1.927 1.927a.437.437 0 0 0 0 .62zM18.576 10.412l-5.346.564-.017.018 2.39 2.39zM9.922 8.502s.023 3.304-.003 4.452c-.02.856.371 1.114.746 1.507.538.564 1.599 1.57 1.599 1.57a.53.53 0 0 0 .75 0l1.843-1.844a.53.53 0 0 0 0-.75z"/></g></symbol><symbol viewBox="0 0 24 24" id="kl" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:#3aaae1}.b{fill:#fdfeff}</style></defs><title>kl</title><path d="M12.033 1.737c-.25-.003-.5.11-.729.337C8.225 5.15 5.15 8.227 2.078 11.31c-.144.144-.229.346-.341.521v.41c.16.223.294.474.485.666a3259.51 3259.51 0 0 0 8.936 8.937c.193.192.443.325.666.486h.41c.205-.142.436-.256.609-.428 3.046-3.041 6.09-6.083 9.133-9.127.47-.47.472-1.005.006-1.472l-9.218-9.217c-.23-.23-.48-.347-.731-.35zm-1.062 4.545l1.386.832c.702.422 1.403.846 2.109 1.262a.544.544 0 0 1 .04.026l.016.013.017.013c.061.056.089.123.088.224a510.281 510.281 0 0 0 0 3.794.463.463 0 0 1-.007.094c-.015.069-.054.103-.142.109a.464.464 0 0 1-.044.002c-.045-.002-.09-.002-.136-.003-.323-.006-.648-.001-.998-.001v-.527-1.34-.671-.003l.004-.668c0-.147-.039-.231-.17-.308-.893-.528-1.78-1.066-2.67-1.6-.051-.03-.101-.065-.173-.111l.001-.003h-.001zm.362 3.39c.068-.003.119.043.173.138.085.148.174.293.264.44l.015.025c.096.154.194.31.292.47l-1.915 1.176c-.337.207-.673.417-1.014.617-.113.067-.154.143-.154.277.01.977.01 1.955.014 2.932V16H7.7V16h-.002c-.004-.053-.014-.112-.014-.17-.005-1.25-.006-2.501-.015-3.751 0-.142.045-.222.164-.294a467.13 467.13 0 0 0 3.353-2.054l.016-.01a.606.606 0 0 1 .032-.017l.016-.008a.308.308 0 0 1 .033-.013l.012-.004a.157.157 0 0 1 .028-.005l.01-.001zm5.677 3.126l.314.54.346.594v.001c-.158.094-.298.178-.438.259l-3.097 1.798c-.106.062-.189.071-.3.01l-.893-.496-1.524-.843-.895-.493c-.035-.02-.068-.044-.129-.085h.001l.137-.25.495-.902 1.446.795c.442.243.886.483 1.323.734.121.07.212.072.334 0 .894-.525 1.792-1.043 2.689-1.563.057-.034.118-.061.191-.1z" fill="#29b6f6" stroke-width=".041"/></symbol><symbol viewBox="0 0 24 24" id="kotlin" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="gpb"><stop offset="0" stop-color="#cb55c0"/><stop offset="1" stop-color="#f28e0e"/></linearGradient><linearGradient id="gpa"><stop offset="0" stop-color="#0296d8"/><stop offset="1" stop-color="#8371d9"/></linearGradient><linearGradient xlink:href="#gpa" id="gpc" x1="1.725" y1="22.67" x2="22.185" y2="1.982" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/><linearGradient xlink:href="#gpb" id="gpd" x1="1.869" y1="22.382" x2="22.798" y2="3.377" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/></defs><path d="M3.307 3.003v18.048h18.05v-.03L16.88 16.51l-4.48-4.515 4.48-4.515 4.443-4.477H3.307z" fill="url(#gpc)"/><path d="M12.538 3.003l-9.23 9.23v8.818h.083l9.032-9.032-.025-.024 4.48-4.515 4.444-4.477h-8.784z" fill="url(#gpd)"/></symbol><symbol viewBox="0 0 240 240" id="laravel" xmlns="http://www.w3.org/2000/svg"><path d="M216.05 119.036c-1.433.343-24.945 6.673-24.945 6.673l-19.227-28.622c-.537-.828-.99-1.656.359-1.849 1.345-.196 23.195-4.477 24.182-4.723.99-.245 1.837-.536 3.053 1.267 1.21 1.8 17.836 24.626 18.464 25.506.627.877-.447 1.41-1.883 1.748m-4.101 49.326c.588 1.003 1.176 1.64-.67 2.367-1.843.73-62.243 22.847-63.418 23.39-1.173.546-2.092.73-3.607-1.637-1.51-2.362-21.16-39.264-21.16-39.264l64.03-18.075c1.876-.644 2.317-.405 3.103.822 1.074 1.68 21.143 31.403 21.726 32.4m-103.7-21.087c-.78.202-37.566 9.733-39.525 10.22-1.965.485-1.965.246-2.188-.49-.226-.727-43.728-98.053-44.333-99.271-.605-1.214-.574-2.177 0-2.177.571 0 34.734-3.313 35.944-3.383 1.207-.07 1.08.205 1.526 1.033l49.025 91.818c.84 1.58 1.239 1.81-.452 2.248m94.588-59.77c-3.5-4.58-5.2-3.751-7.357-3.41-2.154.336-27.277 4.915-30.194 5.449-2.918.536-4.758 1.803-2.963 4.53 1.597 2.422 18.113 27.824 21.751 33.42l-65.663 17.066L66.18 49.832c-2.075-3.342-2.507-4.514-7.236-4.28-4.735.23-40.969 3.495-43.55 3.731-2.58.233-5.416 1.479-2.835 8.09 2.583 6.612 43.734 102.82 44.88 105.62 1.149 2.803 4.128 7.345 11.11 5.527 7.157-1.871 31.969-8.894 45.52-12.742 7.163 14.07 21.77 42.619 24.473 46.707 3.607 5.459 6.089 4.56 11.626 2.738 4.325-1.42 67.65-26.129 70.502-27.4 2.855-1.273 4.613-2.184 2.685-5.275-1.419-2.28-18.124-26.558-26.876-39.26 5.993-1.733 27.305-7.888 29.575-8.557 2.646-.779 3.008-2.19 1.572-3.94-1.436-1.755-21.293-28.72-24.79-33.296z" fill="#ff5722" stroke="#ff5722" stroke-width="8.852" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="less" xmlns="http://www.w3.org/2000/svg"><path d="M13.696 2.999V5h2.002v5a2 2 0 0 0 1.999 2 2 2 0 0 0-2 2v5h-2v2h2a2 2 0 0 0 2-2v-4a2 2 0 0 1 2-2h1V11h-1a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2.001zm-.03 12.766v.47a1 1 0 0 0 .03-.236 1 1 0 0 0-.03-.234zM10.566 21v-2.001H8.565v-5a2 2 0 0 0-2-2 2 2 0 0 0 2-2V5h2.001v-2H8.565a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-.999V13h1a2 2 0 0 1 2 2v3.999A2 2 0 0 0 8.564 21zm.03-12.766v-.47a1 1 0 0 0-.03.236 1 1 0 0 0 .03.234z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="lib" xmlns="http://www.w3.org/2000/svg"><path d="M19 7H9V5h10m-4 10H9v-2h6m4-2H9V9h10m1-7H8a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2M4 6H2v14a2 2 0 0 0 2 2h14v-2H4V6z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 40 40" id="livescript" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -257)" fill="#317eac"><path stroke-width="3.299" d="M5.419 260.18h3.685v34.207H5.419z"/><path stroke-width="3.299" d="M37.074 288.197v3.685H2.867v-3.685z"/><path stroke-width="2.894" d="M29.612 265.658l2.004 2.005L7.428 291.85l-2.004-2.005z"/><path stroke-width="2.325" d="M10.73 262.471h2.835v22.08H10.73z"/><path stroke-width="2.063" d="M15.36 262.519h2.835v17.382H15.36z"/><path stroke-width="1.77" d="M19.99 262.471h2.835v12.802H19.99z"/><path stroke-width="1.422" d="M24.526 262.491h2.835v8.254h-2.835z"/><path stroke-width="1.128" d="M28.783 262.463h2.835v5.197h-2.835z"/><path stroke-width="2.325" d="M34.801 286.545v-2.835h-22.08v2.835z"/><path stroke-width="2.063" d="M34.753 281.914v-2.835H17.371v2.835z"/><path stroke-width="1.77" d="M34.801 277.284v-2.835H21.999v2.835z"/><path stroke-width="1.422" d="M34.781 272.749v-2.835h-8.254v2.835z"/><path stroke-width="1.128" d="M34.809 268.492v-2.835h-5.197v2.835z"/></g></symbol><symbol viewBox="0 0 24 24" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="lua" xmlns="http://www.w3.org/2000/svg"><circle cx="12.203" cy="12.102" r="10.322" fill="none" stroke="#42a5f5"/><path d="M12.33 5.746a6.483 6.381 0 0 0-6.482 6.381 6.483 6.381 0 0 0 6.482 6.38 6.483 6.381 0 0 0 6.484-6.38 6.483 6.381 0 0 0-6.484-6.38zm1.86 1.916a2.329 2.292 0 0 1 2.33 2.293 2.329 2.292 0 0 1-2.33 2.291 2.329 2.292 0 0 1-2.329-2.29 2.329 2.292 0 0 1 2.328-2.294z" fill="#42a5f5" fill-rule="evenodd"/><ellipse cy="4.615" cx="19.631" rx="2.329" ry="2.292" fill="#42a5f5" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="markdown" xmlns="http://www.w3.org/2000/svg"><path d="M2 16V8h2l3 3 3-3h2v8h-2v-5.17l-3 3-3-3V16H2m14-8h3v4h2.5l-4 4.5-4-4.5H16V8z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" preserveAspectRatio="xMidYMid" id="markojs" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -120.96)" stroke-width=".984"><path d="M4.002 126.482c-.655 1.07-1.32 2.14-1.976 3.21-.655 1.06-1.308 2.142-1.963 3.212l.002.002-.002.002c.655 1.07 1.308 2.15 1.963 3.211.655 1.07 1.32 2.141 1.976 3.211h3.33c-.664-1.07-1.318-2.14-1.974-3.21-.653-1.069-1.307-2.145-1.961-3.214.654-1.068 1.308-2.146 1.961-3.215a601.93 601.93 0 0 1 1.974-3.209z" fill="#2196f3"/><path d="M3.999 126.482l-.002.002c.655 1.07 1.31 2.15 1.964 3.212.655 1.07 1.32 2.14 1.974 3.21h3.331c-.664-1.07-1.319-2.14-1.974-3.21-.653-1.068-1.306-2.146-1.96-3.214z" fill="#26a69a"/><path d="M15.203 126.482l.002.002c-.655 1.07-1.31 2.15-1.965 3.212-.655 1.07-1.319 2.14-1.974 3.21h-3.33c.664-1.07 1.318-2.14 1.973-3.21.654-1.069 1.307-2.146 1.961-3.214z" fill="#8bc34a"/><path d="M11.874 126.484c.664 1.07 1.318 2.14 1.974 3.21.653 1.068 1.307 2.146 1.961 3.214-.654 1.069-1.308 2.145-1.961 3.213-.656 1.07-1.31 2.14-1.974 3.21h3.33c.655-1.07 1.319-2.14 1.974-3.21.655-1.06 1.31-2.14 1.966-3.21l-.002-.003.002-.002c-.656-1.07-1.311-2.152-1.966-3.213-.655-1.07-1.319-2.138-1.974-3.209z" fill="#ffc107"/><path d="M16.74 126.482c.665 1.07 1.319 2.14 1.974 3.21.654 1.068 1.306 2.146 1.96 3.214-.654 1.069-1.306 2.145-1.96 3.213-.655 1.07-1.31 2.141-1.974 3.211h3.33c.656-1.07 1.32-2.14 1.974-3.21.655-1.062 1.31-2.141 1.966-3.212l-.002-.002.002-.002c-.655-1.07-1.31-2.152-1.966-3.213-.655-1.07-1.318-2.138-1.973-3.209z" fill="#f44336"/></g></symbol><symbol viewBox="0 0 23 24" id="mathematica" xmlns="http://www.w3.org/2000/svg"><path d="M11.512 1.523l-.073.025-.46.794-.454.763-1.217 2.09H9.29L5.435 3.5l-.1-.047h-.018v.092l.025.163v.086l.132 1.226v.082l.032.252v.082l.22 2.137v.075l.018.082v.06l-2.348.507-.04.015-.457.1-.025.01h-.042l-1.096.244-.04.007-.17.036v.082l.018.01 1.859 2.086.053.052.114.132.804.909v.005l-.053.05-.22.257-2.564 2.875-.01.007v.082l.071.006.295.075 1.697.366v.006l2.139.472h.015v.047l-.036.252v.08l-.046.412v.082l-.036.244v.082l-.045.412v.08l-.05.41v.08l-.036.244v.082l-.046.412v.082l-.05.407v.082l-.032.248V20l-.05.407v.104h.037l3.642-1.6.294-.134h.018l.177.312.539.911.015.032.854 1.465.16.262.404.695.007.022h.092l.005-.022.017-.025.56-.947.014-.042.6-1.033.316-.539.644-1.091.05.013 3.906 1.721h.035v-.085l-.138-1.32v-.082l-.032-.244v-.082l-.035-.245v-.085l-.033-.244v-.081l-.032-.245v-.082l-.032-.244v-.085l-.035-.245v-.082l-.032-.245v-.082l-.033-.244v-.085l-.025-.17v-.053l1.632-.354.043-.008.458-.107h.028v-.01l.23-.05.03-.01h.042l.382-.09.025-.01h.043l.194-.05h.033l1.015-.23.07-.007v-.064l-.015-.013-1.19-1.342-.028-.028-.197-.22-1.428-1.604v-.006l.295-.323.4-.457 2.148-2.408.015-.01v-.065l-.035-.008-1.288-.28-.372-.084-.047-.01-2.481-.544v-.045l.432-4.265v-.02h-.042l-.302.135-.01.014h-.025l-3.307 1.45-.297.135h-.015l-2.028-3.483-.099-.145-.014-.045zm-.001 1.114l1.365 2.323.34.592-.008.025-1.18 1.511-.517.66-.012-.01-.258-.335-.04-.05-1.397-1.787.03-.063 1.378-2.365.287-.491zm4.908 2.039l-.007.025-.168.225-.538.066zm-9.817.004l.053.02.677.3h-.499l-.224-.3zM16.947 5l-.123 1.248-.113-.928.226-.307zm-9.26.156l.053.024.705.309-.757-.175zm7.388.116l.02.168-1.318.403.003-.003.16-.071 1.015-.444zM9.669 6.388l.944 1.204v.01L9.483 7.2zm3.55.172l.21.682-.234.084-.089.022-.702.255.008-.022.776-.982zm-5 .836l.986.356.898.312.048.02 1.054.373.011 3.086-.362-.117-.67-.224-.081-.038-.735-.245-.77-.256-.29-.1-.011-.255-.032-1.195-.01-.287-.015-.894-.013-.297zm6.583 0l-.011.227-.028.9-.008.303-.032 1.475-.01.262-.337.117-.734.245-.77.256-.712.245-.355.117.01-3.086 1.632-.578zm.585.437l.09.735.79-.097-.915 1.302-.018.006.01-.183.018-.877zm-9.451.536l.152.22 1.447 2.049-2.607.968-.05.015-1.972-2.214-.28-.312.003-.01.115-.018.424-.1.14-.021.337-.078.042-.01zm11.146.003l3.284.713.029.01-.022.025-1.954 2.192-.277.312-.092-.036-2.564-.95.475-.681.152-.216zM6.787 8.52h.86l.036 1.258-.013-.006-.763-1.078zm1.358 2.625l.152.06.77.252.712.245.746.247.49.167-.065.092-1.723 2.334-1.015-.302-.082-.017-.035-.015-1.902-.56.938-1.22.981-1.277zm6.73 0l.033.006 1.787 2.327.132.17-.128.036-.032.014-2.196.642-.105.032-.564.17-.018-.003-1.053-1.44-.174-.239-.547-.726-.007-.018.469-.16.769-.254.713-.245.77-.252zm-7.766.305l-.007.02-.405.523-.291-.291.657-.245zm8.802 0l.043.007.578.212.714.27-.661.394-.375-.479-.03-.042-.262-.342zm-10.843.75l-.67.668.355-.397.207-.23zm12.911.016l.068.025.045.042.554.627.042.043.204.228-.255.135zm-6.473.265l.022.015 1.38 1.872.032.05.343.465.008.031-.088.117-.422.629-.047.074-.245.343-.97 1.43-.013.007-1.18-1.72-.096-.16-.493-.708-.008-.037 1.618-2.191.007-.01zm7.827 1.194l.565.633.063.082-.272-.093-.037-.013zm-15.785.148l.297.299-.637.218-.152.05.038-.058zm13.224.47l-.855.448.346.66-.185-.058-.27-.088-1.092-.348.012-.01zm-9.687.255l1.222.356-.006.007-.458.145-.443.135-.032.01-.49.157zm-2.765.048l.318.32 2.007.517-.567.18-.055.004-2.103-.469-.744-.156.007-.006zm14.966.205l.548.188v.003l-.457.1-.043.014-1.069.23zm-10.23.507l.007.227.01.347.025 1.363.025.691-.007.255-.24.107-2.863 1.255.032-.372.033-.255.017-.227.031-.256.037-.407.045-.42.018-.23.032-.251.032-.412.05-.414.013-.14 1.455-.457.003-.014.301-.098zm4.908 0l1.245.39v.014l.312.1 1.146.362.022.23.03.255.043.408.04.42.017.23.033.251.032.412.042.325.078.848-.078-.04-3.025-1.322-.004-.305.06-2.368zm-4.295.617l.015.007.067.107.6.875-.64.531-.034-1.438zm3.671 0h.008l-.005.06-.02.678-.005.214-.479-.223zm-2.888 3.605l.763.915.001.37-.017-.006-.025-.05-.464-.791-.012-.018zm1.53.61l.184.083-.343.586-.018.007.002-.532z" fill="#f44336" fill-rule="evenodd" stroke="#f44336" stroke-width=".7747499999999999" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 720 720" id="matlab" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><path d="M209.247 329.98L52.368 387.638l121.325 85.822 96.752-95.804-61.198-47.674z" fill="#4db6ac" fill-rule="evenodd" stroke-width=".3"/><path d="M480.193 71.446c-13.123 1.784-9.565 1.013-28.4 16.09-18.008 14.418-69.925 100.347-97.673 129.256-24.688 25.722-34.46 12.199-60.102 33.661-25.68 21.494-65.273 64.464-65.273 64.464l63.978 47.32L394.15 222.754c23.948-32.932 23.694-37.266 36.744-71.82 6.384-16.907 17.76-29.9 27.756-45.809 12.488-19.874 30.186-34.855 21.543-33.68z" fill="#00897b" fill-rule="evenodd" stroke-width=".3"/><path d="M478.206 69.796c-31.268-.189-62.068 137.245-115.56 242.691-54.543 107.519-162.235 176.82-162.235 176.82 18.156 8.243 34.681 4.91 54.236 23.394 13.375 16.164 52.09 95.976 75.174 146.117 0 0 18.964-10.297 42.994-27.695 24.03-17.397 53.124-41.896 73.384-70.3 26.883-37.692 47.897-61.043 65.703-75.271 17.806-14.23 32.404-19.336 46.458-20.54 50.238-4.305 124.582 85.792 124.582 85.792S527.267 70.09 478.206 69.796z" fill="#ffb74d" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 24 24" id="merlin" xmlns="http://www.w3.org/2000/svg"><text style="line-height:1.25;-inkscape-font-specification:'Century Gothic Bold'" x="1.953" y="21.178" transform="scale(.99582 1.0042)" font-weight="700" font-size="30.255" font-family="Century Gothic" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-width=".756"><tspan x="1.953" y="21.178" style="-inkscape-font-specification:'Century Gothic Bold'" font-size="22.745">M</tspan></text></symbol><symbol viewBox="0 0 192 191.99999" id="mocha" xmlns="http://www.w3.org/2000/svg"><title>Mocha Logo</title><g transform="translate(-354.75 -262.42) scale(4.835)" fill="#a1887f"><path d="M103.6 69.6c0-.5-.4-1-1-1H83.8c-.5 0-1 .4-1 1 0 3.4.5 15.1 5.5 20.8.2.2.4.3.7.3h8.4c.3 0 .5-.1.7-.3 5-5.6 5.5-17.3 5.5-20.8zm-7.4 18.2h-5.9c-.3 0-.5-.1-.7-.3-3.4-4-3.8-12-3.9-14.8 0-.5.4-1 1-1h13.2c.5 0 1 .4 1 1 0 2.8-.5 10.7-3.9 14.8-.3.2-.5.3-.8.3zM95.1 66.6s3.6-2.1 1.4-5.9c-1.3-2-1.9-3.7-1.4-4.4-1.3 1.6-3.5 3.3-1.1 6.9.8.9 1.2 2.8 1.1 3.4zM91.1 66.9s2.4-1.4.9-4c-.9-1.3-1.3-2.5-.9-2.9-.9 1.1-2.3 2.2-.7 4.7.5.5.7 1.8.7 2.2z"/><path d="M99.3 78.5c-.4 2.7-1.2 5.8-2.9 7.8-.2.2-.4.3-.6.3h-5c-.2 0-.5-.1-.6-.3-1.2-1.5-2-3.5-2.5-5.6 0 0 5.8.8 9.1-.4 2.4-.9 2.5-1.8 2.5-1.8z"/></g></symbol><symbol viewBox="0 0 24 24" id="movie" xmlns="http://www.w3.org/2000/svg"><path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V4h-4z" fill="#ff9800"/></symbol><symbol viewBox="0 0 24 24" id="music" xmlns="http://www.w3.org/2000/svg"><path d="M16 9V7h-4v5.5c-.42-.31-.93-.5-1.5-.5A2.5 2.5 0 0 0 8 14.5a2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 2.5-2.5V9h3m-4-7a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z" fill="#ef5350"/></symbol><symbol viewBox="0 0 24 24" id="mxml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#ffa726"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#ab47bc" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#26c6da" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#e53935" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#9ccc65" stroke-width="12.914"/></symbol><symbol viewBox="0 0 24 24" id="nim" xmlns="http://www.w3.org/2000/svg"><path d="M4.464 15.75L2.288 3.78l5.985 7.617L12.08 3.78l3.809 7.617 5.985-7.617-2.177 11.97H4.464m15.234 3.264a1.088 1.088 0 0 1-1.088 1.088H5.553a1.088 1.088 0 0 1-1.089-1.088v-1.089h15.234z" stroke-width="1.088" fill="#ffca28"/></symbol><symbol viewBox="0 0 500 500" id="nix" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-1.965 36.302)" stroke-width=".395"><path d="M135.59 415.7c0-.295-2.752-5.283-6.116-11.084-3.364-5.801-6.116-10.776-6.116-11.055s9.514-16.889 21.143-36.912c11.629-20.022 21.323-36.798 21.542-37.279.346-.76-1.608-4.363-14.896-27.466-8.412-14.625-15.294-26.785-15.294-27.023 0-.5 24.46-43.501 25.206-44.31.414-.45.592-.384 1.078.395.32.513 16.876 29.256 36.791 63.87 62.62 108.85 74.852 130.01 75.41 130.46.3.242.544.554.544.694 0 .14-11.836.21-26.302.154-23.023-.09-26.313-.175-26.393-.694-.11-.714-27.662-48.825-28.86-50.392-.746-.978-.906-1.035-1.426-.51-.688.696-28.954 49.323-29.49 50.733l-.365.96h-13.229c-10.896 0-13.229-.095-13.229-.538zm167.58-125.61c-.134-.216 1.188-2.863 2.938-5.882 6.924-11.944 84.291-145.75 96.491-166.88 7.143-12.371 13.142-22.465 13.333-22.433.363.062 25.861 43.105 25.861 43.655 0 .174-6.761 11.952-15.026 26.173-8.46 14.557-14.932 26.104-14.81 26.421.185.483 4.564.564 30.213.564h29.996l.958 1.48c.526.814 3.296 5.547 6.155 10.518 2.859 4.971 5.45 9.29 5.756 9.597.706.705.704.724-.16 1.572-.395.388-3.36 5.323-6.587 10.965-3.228 5.643-6.056 10.387-6.285 10.543-.23.156-19.695.171-43.256.034l-42.84-.249-.804 1.15c-.441.632-7.504 12.736-15.696 26.897l-14.892 25.747H339.03c-8.517 0-20.015.116-25.55.259-6.55.168-10.15.121-10.309-.135zM169.42 132.23c-56.373-.055-102.5-.182-102.5-.282 0-.1 5.617-10.132 12.481-22.294l12.481-22.112h30.332c27.113 0 30.332-.065 30.332-.611 0-.336-6.659-12.228-14.797-26.427-8.139-14.199-14.797-25.917-14.797-26.04 0-.123 2.682-4.853 5.96-10.51s6.003-10.578 6.055-10.934c.086-.586 1.376-.648 13.572-.648 7.413 0 13.463.143 13.446.317-.017.174.222.707.531 1.184.31.476 9.763 16.937 21.007 36.578 11.244 19.64 20.71 36.022 21.036 36.4.554.647 2.549.691 31.428.691h30.837l12.896 22.145c7.093 12.18 12.8 22.301 12.682 22.492-.118.19-4.776.303-10.352.249-5.575-.054-56.26-.143-112.63-.198z" fill="#5075c1"/><path d="M25.289 203.14c-6.098 10.563-6.69 11.711-6.225 12.078.283.224 3.18 5.044 6.44 10.712 3.261 5.668 6.017 10.355 6.124 10.417.106.061 13.585.153 29.95.204 16.367.052 29.994.23 30.285.399.472.273-1.08 3.094-14.637 26.574L62.06 289.793l12.907 21.865c7.1 12.026 12.982 21.906 13.068 21.956.086.05 23.257-39.831 51.492-88.624 11.352-19.617 21.214-36.64 30.37-52.442 23.308-40.452 30.68-53.468 30.73-54.132-1.097-.11-6.141-.187-13.006-.216-3.945-.01-7.82-.02-12.75-.002l-25.341.092-15.42 26.706c-14.256 24.693-15.445 26.663-16.278 26.86l-.024.037c-.011.003-1.62-.001-1.825 0-4.29.062-20.453.063-40.226-.01-22.632-.082-41.615-.125-42.183-.096-.568.03-1.147-.03-1.29-.132-.142-.102-3.29 5.066-6.996 11.485zm205.16-190.3c-.123.149 5.62 10.392 12.761 22.763 12.199 21.131 89.393 155.03 96.276 167 1.502 2.613 2.92 4.803 3.443 5.348.9-1.249 3.531-5.63 7.954-13.219a1342.88 1342.88 0 0 1 10.049-17.76l6.606-11.443c.692-1.403.754-1.818.653-2.117-.162-.48-6.904-12.332-14.982-26.337-8.078-14.005-14.824-25.849-14.991-26.32a.73.73 0 0 1-.009-.366l-.426-.913L359.42 72.5c3.69-6.307 6.425-11.042 9.47-16.29 9.159-15.948 12.037-21.189 11.896-21.55-.126-.324-2.7-4.83-5.72-10.017-3.021-5.185-5.845-10.148-6.275-11.026-.483-.987-.734-1.364-1.1-1.456-.054.014-.083.018-.145.035-.42.112-5.454.195-11.189.185-5.734-.01-11.22.024-12.188.073l-1.76.089-14.997 25.978c-12.824 22.212-15.084 25.964-15.595 25.883-.024-.004-.15-.189-.235-.301-.109.066-.2.09-.272.05-.255-.148-7.143-11.902-15.306-26.119l-14.36-25.016c-.115-.186-.444-.744-.457-.752-.477-.275-50.502.287-50.737.57zm-18.646 283.09c-.047.109-.026.262.042.48.329 1.05 25.338 43.735 25.772 43.985.207.119 14.178.239 31.05.266 26.651.044 30.75.152 31.234.832.308.43 9.988 17.214 21.513 37.296s21.152 36.627 21.394 36.767c.242.14 5.927.243 12.633.23 6.706-.013 12.401.099 12.657.246.132.076.382-.141.852-.795l6.008-10.406c5.234-9.065 6.62-11.684 6.294-11.888-.575-.36-15.597-26.643-23.859-41.482-3.09-5.45-5.37-9.516-5.441-9.774-.195-.712-.065-.822 1.156-.98 1.956-.252 57.397-.057 58.07.205.238.092.79-.569 2.594-3.497 1.866-3.067 5.03-8.524 11-18.866 7.22-12.505 13.044-22.784 12.942-22.843-.102-.059-.771-.051-1.489.016l-.046.001c-4.452.204-33.918.203-149.74.025-38.96-.06-69.786-.09-71.912-.072-1.121.01-2.095.076-2.66.172a.25.25 0 0 0-.062.083z" fill="#7db7e1"/></g></symbol><symbol viewBox="0 0 24 24" id="nodejs" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.85c-.27 0-.55.07-.78.2l-7.44 4.3c-.48.28-.78.8-.78 1.36v8.58c0 .56.3 1.08.78 1.36l1.95 1.12c.95.46 1.27.47 1.71.47 1.4 0 2.21-.85 2.21-2.33V8.44c0-.12-.1-.22-.22-.22H8.5c-.13 0-.23.1-.23.22v8.47c0 .66-.68 1.31-1.77.76L4.45 16.5a.26.26 0 0 1-.11-.21V7.71c0-.09.04-.17.11-.21l7.44-4.29c.06-.04.16-.04.22 0l7.44 4.29c.07.04.11.12.11.21v8.58c0 .08-.04.16-.11.21l-7.44 4.29c-.06.04-.16.04-.23 0L10 19.65c-.08-.03-.16-.04-.21-.01-.53.3-.63.36-1.12.51-.12.04-.31.11.07.32l2.48 1.47c.24.14.5.21.78.21s.54-.07.78-.21l7.44-4.29c.48-.28.78-.8.78-1.36V7.71c0-.56-.3-1.08-.78-1.36l-7.44-4.3c-.23-.13-.5-.2-.78-.2M14 8c-2.12 0-3.39.89-3.39 2.39 0 1.61 1.26 2.08 3.3 2.28 2.43.24 2.62.6 2.62 1.08 0 .83-.67 1.18-2.23 1.18-1.98 0-2.4-.49-2.55-1.47a.226.226 0 0 0-.22-.18h-.96c-.12 0-.21.09-.21.22 0 1.24.68 2.74 3.94 2.74 2.35 0 3.7-.93 3.7-2.55 0-1.61-1.08-2.03-3.37-2.34-2.31-.3-2.54-.46-2.54-1 0-.45.2-1.05 1.91-1.05 1.5 0 2.09.33 2.32 1.36.02.1.11.17.21.17h.97c.05 0 .11-.02.15-.07.04-.04.07-.1.05-.16C17.56 8.82 16.38 8 14 8z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 300 300" id="nodemon" xmlns="http://www.w3.org/2000/svg"><title>nodemon</title><path d="M149.868 20.62c-2.124 0-4.25.55-6.154 1.648L41.899 81.083a12.306 12.306 0 0 0-6.15 10.652v117.633a12.29 12.29 0 0 0 6.152 10.646l101.815 58.766h.001a12.282 12.282 0 0 0 12.291 0l101.84-58.766a12.29 12.29 0 0 0 6.153-10.652V91.738a12.31 12.31 0 0 0-6.146-10.652L156.015 22.27a12.302 12.302 0 0 0-6.153-1.648zM83.303 70.93s11.789 33.031 35.477 31.934l27.74-15.961a7.348 7.348 0 0 1 3.414-.99h.641a7.233 7.233 0 0 1 3.404.99l27.738 15.961c23.69 1.094 35.475-31.934 35.475-31.934 5.233 23.154 1.06 38.641-5.924 48.942l4.541 2.614h.002c2.321 1.327 3.734 3.795 3.737 6.49l-.12 95.811a3.724 3.724 0 0 1-1.855 3.227 3.624 3.624 0 0 1-3.735 0L177.1 206.971c-2.311-1.363-3.742-3.818-3.742-6.48v-44.763a7.44 7.44 0 0 0-3.737-6.465l-15.642-9.01a7.28 7.28 0 0 0-3.715-1.01 7.378 7.378 0 0 0-3.742 1.01l-15.648 9.01c-2.316 1.323-3.729 3.798-3.729 6.467v44.762c0 2.663-1.413 5.1-3.738 6.48l-36.748 21.041a3.571 3.571 0 0 1-3.71 0c-1.173-.65-1.864-1.887-1.864-3.224l-.137-95.812a7.483 7.483 0 0 1 3.74-6.49l4.541-2.615c-6.982-10.302-11.16-25.79-5.925-48.942z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 990 990" id="npm" xmlns="http://www.w3.org/2000/svg"><defs><style>.hncls-1{fill:#cb3837}.cls-2{fill:#fff}</style></defs><title>n</title><path class="hncls-1" d="M113.26 876.74V113.27h763.47v763.47zm143.59-620.4v476.18h240.61V355.63h140.21v376.96h95.457V256.34z" fill="#e53935" stroke-width=".771"/></symbol><symbol id="nunjucks" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.host0{fill:#388e3c}</style><path class="host0" d="M11.2 21.1H8.1l-2.3-7.9v7.9H2.7V2.9h3.1l2.3 7.4V2.9h3.1zM21.3 19.2c0 1-.8 1.9-1.9 1.9h-4.8c-1 0-1.9-.8-1.9-1.9v-3.8l3.2-.7V18h2.3V7.2h3.1v12z"/></symbol><symbol viewBox="0 0 150 150.00001" id="ocaml" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.76136 0 0 .76136 11.616 19.98)"><path d="M83.02 101.645l.023-.062c-.035-.159-.047-.195-.024.062z" fill="none" stroke-width="1.028"/><linearGradient id="hpa" gradientUnits="userSpaceOnUse" x1="-696.735" y1="97.7" x2="-696.735" y2="142.997" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M82.313 138.79c-.471-1.004-1.904-3.621-2.624-4.46-1.562-1.828-1.927-1.966-2.386-4.275-.799-4.02-2.913-11.31-5.405-16.341-1.286-2.596-3.426-4.777-5.385-6.66-1.71-1.652-5.565-4.431-6.237-4.294-6.296 1.257-8.249 7.432-11.21 12.323-1.638 2.705-3.374 5.007-4.665 7.885-1.192 2.646-1.087 5.577-3.128 7.849-2.093 2.333-3.454 4.814-4.48 7.829-.194.574-.747 6.596-1.348 8.015l9.357-.659c8.719.594 6.2 3.936 19.81 3.208l21.487-.665c-.666-1.97-1.584-4.25-1.938-4.991-.599-1.248-1.352-3.69-1.848-4.763z" fill="url(#hpa)" stroke-width="1.028"/><linearGradient id="hpb" gradientUnits="userSpaceOnUse" x1="-666.972" y1="142.12" x2="-666.972" y2="142.12" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><linearGradient id="hpc" gradientUnits="userSpaceOnUse" x1="-675.228" y1="-1.28" x2="-675.228" y2="142.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M109.553 94.296c-1.652 1.193-4.88 4.06-11.902 5.145-3.152.487-6.1.527-9.335.365-1.584-.076-3.077-.157-4.665-.177-.936-.008-4.074-.107-3.919.193l-.349.871c.054.287.169 1.004.2 1.177.129.704.165 1.265.192 1.912.048 1.331-.11 2.719-.043 4.062.141 2.787 1.175 5.326 1.306 8.137.143 3.13 1.69 6.442 3.188 8.998.569.973 1.434 1.084 1.811 2.283.442 1.373.024 2.83.239 4.293.842 5.675 2.477 11.606 5.032 16.728.018.043.038.09.06.128 3.156-.53 6.318-1.665 10.418-2.271 7.517-1.115 17.972-.54 24.688-1.17 16.993-1.597 26.216 6.97 41.478 3.459V22.459c0-11.84-9.594-21.438-21.435-21.438H19.239C7.4 1.021-2.197 10.62-2.197 22.458v46.774c3.067-1.11 7.479-7.635 8.861-9.222 2.419-2.775 2.858-6.315 4.062-8.544 2.743-5.078 3.215-8.57 9.451-8.57 2.907 0 4.061.67 6.027 3.31 1.368 1.834 3.731 5.224 4.837 7.49 1.277 2.615 3.357 6.153 4.272 6.867.677.53 1.35.928 1.976 1.163 1.012.38 1.848-.316 2.525-.855.863-.687 1.235-2.088 2.035-3.957 1.152-2.696 2.408-5.926 3.122-7.054 1.237-1.949 1.658-4.261 2.993-5.381 1.97-1.652 4.54-1.768 5.246-1.908 3.957-.781 5.755 1.906 7.704 3.645 1.276 1.138 3.019 3.432 4.256 6.507.967 2.4 2.199 4.622 2.714 6.008.497 1.339 1.725 3.484 2.453 6.055.661 2.336 2.43 4.125 3.102 5.235 0 0 1.029 2.882 7.285 5.516 1.357.572 4.1 1.501 5.736 2.096 2.718.988 5.351.86 8.704.458 2.391 0 3.686-3.462 4.772-6.234.643-1.639 1.259-6.334 1.678-7.667.406-1.297-.544-2.3.265-3.437.946-1.327 1.508-1.399 2.054-3.129 1.172-3.704 7.95-3.89 11.761-3.89 3.176 0 2.772 3.083 8.16 2.028 3.086-.605 6.059.398 9.335 1.265 2.758.732 5.352 1.566 6.906 3.385 1.005 1.178 3.5 7.08.958 7.331.244.3.423.84.88 1.135-.566 2.226-3.03.64-4.4.355-1.845-.383-3.147.057-4.952.856-3.085 1.374-7.598 1.214-10.286 3.452-2.281 1.898-2.277 6.133-3.34 8.507-.002-.001-2.955 7.6-9.402 12.248z" fill="url(#hpc)" stroke-width="1.028"/><linearGradient id="hpd" gradientUnits="userSpaceOnUse" x1="-735.137" y1="90.833" x2="-735.137" y2="141.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M38.247 105.09c-1.467-.15-2.83-.317-4.256-.605-2.662-.536-5.57-1.06-8.193-1.688-1.592-.385-6.895-2.263-8.048-2.792-2.702-1.246-4.496-4.63-6.609-4.282-1.348.22-2.662.682-3.5 2.042-.685 1.11-.917 3.016-1.391 4.294-.55 1.485-1.5 2.87-2.331 4.284-1.53 2.595-4.282 4.941-5.468 7.469-.239.52-.45 1.101-.649 1.708V144.415a48.57 48.57 0 0 1 4.45.96c11.955 3.19 14.872 3.46 26.598 2.119l1.1-.146c.897-1.867 1.59-8.227 2.171-10.195.454-1.51 1.077-2.712 1.313-4.253.223-1.463-.02-2.858-.146-4.188-.329-3.332 2.427-4.522 3.742-7.384 1.186-2.589 1.871-5.535 2.853-8.181.941-2.54 2.41-6.13 4.918-7.408-.305-.355-5.237-.518-6.554-.65z" fill="url(#hpd)" stroke-width="1.028"/></g></symbol><symbol viewBox="0 0 24 24" id="pdf" xmlns="http://www.w3.org/2000/svg"><path d="M14 9h5.5L14 3.5V9M7 2h8l6 6v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m4.93 10.44c.41.9.93 1.64 1.53 2.15l.41.32c-.87.16-2.07.44-3.34.93l-.11.04.5-1.04c.45-.87.78-1.66 1.01-2.4m6.48 3.81c.18-.18.27-.41.28-.66.03-.2-.02-.39-.12-.55-.29-.47-1.04-.69-2.28-.69l-1.29.07-.87-.58c-.63-.52-1.2-1.43-1.6-2.56l.04-.14c.33-1.33.64-2.94-.02-3.6a.853.853 0 0 0-.61-.24h-.24c-.37 0-.7.39-.79.77-.37 1.33-.15 2.06.22 3.27v.01c-.25.88-.57 1.9-1.08 2.93l-.96 1.8-.89.49c-1.2.75-1.77 1.59-1.88 2.12-.04.19-.02.36.05.54l.03.05.48.31.44.11c.81 0 1.73-.95 2.97-3.07l.18-.07c1.03-.33 2.31-.56 4.03-.75 1.03.51 2.24.74 3 .74.44 0 .74-.11.91-.3m-.41-.71l.09.11c-.01.1-.04.11-.09.13h-.04l-.19.02c-.46 0-1.17-.19-1.9-.51.09-.1.13-.1.23-.1 1.4 0 1.8.25 1.9.35M8.83 17c-.65 1.19-1.24 1.85-1.69 2 .05-.38.5-1.04 1.21-1.69l.48-.31m3.02-6.91c-.23-.9-.24-1.63-.07-2.05l.07-.12.15.05c.17.24.19.56.09 1.1l-.03.16-.16.82-.05.04z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="perl" xmlns="http://www.w3.org/2000/svg"><path d="M12 14c-1 0-3 1-3 2 0 2 3 2 3 2v-1a1 1 0 0 1-1-1 1 1 0 0 1 1-1v-1m0 5s-4-.5-4-2.5c0-3 3-3.75 4-3.75V11.5c-1 0-5 1.5-5 4.5 0 4 5 4 5 4v-1M10.07 7.03l1.19.53c.43-2.44 1.58-4.06 1.58-4.06-.43 1.03-.71 1.88-.89 2.55C13.16 3.55 15.61 2 15.61 2a15.916 15.916 0 0 0-2.64 3.53c1.58-1.68 3.77-2.78 3.77-2.78-2.69 1.72-3.9 4.45-4.2 5.21l.55.08c0 .52 0 1 .25 1.38C14.1 11.31 18 11.47 18 16s-4.03 6-6.17 6C9.69 22 5 21.03 5 16s4.95-5.07 5.83-7.08c.12-.38-.76-1.89-.76-1.89z" fill="#9575cd"/></symbol><symbol viewBox="0 0 24 24" id="php" xmlns="http://www.w3.org/2000/svg"><path d="M12 18.08c-6.63 0-12-2.72-12-6.08s5.37-6.08 12-6.08S24 8.64 24 12s-5.37 6.08-12 6.08m-5.19-7.95c.54 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.58 1.09-.28.22-.71.33-1.29.33h-.87l.53-2.76h.99m-3.5 5.55h1.44l.34-1.75h1.23c.54 0 .98-.06 1.33-.17.35-.12.67-.31.96-.58.24-.22.43-.46.58-.73.15-.26.26-.56.31-.88.16-.78.05-1.39-.33-1.82-.39-.44-.99-.65-1.82-.65H4.59l-1.28 6.58m7.25-8.33l-1.28 6.58h1.42l.74-3.77h1.14c.36 0 .6.06.71.18.11.12.13.34.07.66l-.57 2.93h1.45l.59-3.07c.13-.62.03-1.07-.27-1.36-.3-.27-.85-.4-1.65-.4h-1.27L12 7.35h-1.44M18 10.13c.55 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.57 1.09-.29.22-.72.33-1.3.33h-.85l.5-2.76h1m-3.5 5.55h1.44l.34-1.75h1.22c.55 0 1-.06 1.35-.17.35-.12.65-.31.95-.58.24-.22.44-.46.58-.73.15-.26.26-.56.32-.88.15-.78.04-1.39-.34-1.82-.36-.44-.99-.65-1.82-.65h-2.75l-1.29 6.58z" fill="#1E88E5"/></symbol><symbol viewBox="0 0 79 78" id="postcss" xmlns="http://www.w3.org/2000/svg"><title>postcss-logo-symbol</title><g transform="translate(5.48 5.52) scale(.85425)" fill="#e53935" fill-rule="evenodd" stroke="#e53935" stroke-width="1.519"><path d="M15.447 32.623c.106.08.29.132.106.29-.132.184-.29.342-.395.553-.105.185-.184.237-.342.106.21-.343.42-.66.63-.95zM68.342 60.24c0 .078.026.13.026.21.053-.105.053-.158.08-.21zm0 .236v-.026zm-5.368 10.277l-4.58-25.402c-.078-.025-.183-.077-.368-.13.053.105.08.184.106.263.13-.026.184-.026.236-.052 0-.026 0-.052.027-.08l4.58 25.404zm-4.737-31.12c-.026.078-.026.158-.026.237 0-.08 0-.16.028-.238zm.026.526c-.026 0-.026 0-.052-.028v.026c.028.026.028.026.054 0zm-.052.21v-.185c-.077.026-.156.026-.262.053.132.05.264.078.264.13z"/><path d="M78.71 33.967c-.052-1.028-.078-2.056-.184-3.083-.184-1.397-.368-2.82-.684-4.19-.237-1.133-.63-2.214-1.026-3.294-.5-1.265-1-2.556-1.632-3.768-1.026-1.95-2.368-3.69-3.605-5.508-.818-1.16-1.87-2.108-2.66-3.294-.447-.685-1.105-1.264-1.763-1.79-1.053-.845-2.158-1.61-3.263-2.347a32.525 32.525 0 0 0-2.58-1.634c-.71-.397-1.473-.713-2.21-1.056-.842-.395-1.658-.87-2.605-1.054-.238-.05-.448-.13-.685-.21-.605-.21-1.184-.447-1.79-.632-.92-.29-1.815-.632-2.763-.87C50.342 1 49.394.843 48.446.71 47.394.555 46.316.5 45.262.397a26.83 26.83 0 0 0-2.026-.184C42.236.16 41.21.16 40.21.134c-.5-.027-1.026-.08-1.526-.053-.763.026-1.526.105-2.29.21-.736.08-1.473.21-2.183.317-.867.105-1.735.158-2.604.264-.816.106-1.658.264-2.473.396-.29.053-.58.158-.87.21-.63.132-1.288.185-1.92.396-1.13.344-2.263.74-3.368 1.16-1.027.422-2.027.87-3 1.397-1 .552-1.948 1.21-2.895 1.844a45.325 45.325 0 0 0-2.66 1.923c-.84.66-1.63 1.397-2.394 2.135-.42.42-.763.922-1.158 1.396-.657.765-1.315 1.502-1.947 2.293-.524.66-1 1.344-1.5 2.03-.893 1.21-1.656 2.502-2.366 3.794-.29.527-.553 1.054-.816 1.58-.395.79-.816 1.555-1.184 2.372-.264.554-.474 1.16-.632 1.766-.367 1.292-.736 2.61-1.078 3.9-.316 1.16-.395 2.372-.42 3.558-.027 1.054.078 2.082.183 3.136.027.264-.13.58.184.79-.105.29-.026.45.13.5-.182.29.08.476-.024.74-.027.052.08.157.13.236 0 .08-.025.185 0 .264.028.237.133.474.133.738 0 .184.157.395.21.58.026.078 0 .21-.053.263-.158.184-.132.342.105.448.133.342.08.5.054.66.052.236-.027.315 0 .368.21.422.29.896.315 1.37 0 .106.053.212.106.343.026 0 0 .5 0 .5.13-.078.237-.104.368-.157.08.342.158.66.263.95.132.21.132.314.08.34.105.474.157.922.34 1.37 0-.5-.05-1-.13-1.475.368.132.684.263.895.263.027-.08.053-.184.08-.237-.158-.157-.29-.394-.448-.552.053.21 0 .29 0 .37-.105-.054-.237-.107-.368-.16.105-.13.21-.263.368-.42 0-.238-.13-.45-.5-.423.158-.052.316-.13.5-.184.29-.157-.026-.447-.026-.816.026-.447-.237-.895-.316-1.37-.132-.737-.105-1.844-.184-2.582-.158-.132-.29.21-.316.237.08.632.158 1.264.21 1.897-.157-.527-.263-1.107-.394-1.74-.027.185-.053.264-.053.37-.13.13-.026.29.053.474-.184-.08-.395-.052-.395-.052v.738c-.262-.264-.34-.474-.473-.66-.052-.21-.08-.42-.13-.63.05-.133 0-.212 0-.29a15.968 15.968 0 0 1-.08-.634c.026-.026-.026-.42-.026-.42.21.025.343.05.474.05-.263-.34-.08-.552.027-.763.053-.106.237-.13.29-.238.21-.395.553-.71.553-1.212 0-.237.08-.5.105-.738.053-.448.105-.896.13-1.344.054-.58 0-1.16.133-1.713.212-.92.475-1.843.764-2.766.21-.66.448-1.29.71-1.95.395-1.028.764-2.056 1.264-3.03.71-1.424 1.526-2.794 2.316-4.19.5-.87 1.026-1.687 1.58-2.53.525-.817 1.05-1.66 1.657-2.425a21.452 21.452 0 0 1 2.79-2.978c1.053-.948 2.053-1.923 3.184-2.793a32.218 32.218 0 0 1 4.685-3.005c1.343-.71 2.737-1.266 4.132-1.793.895-.342 1.868-.5 2.79-.79 1.052-.343 2.105-.5 3.21-.527.71-.027 1.395-.106 2.105-.185.632-.05 1.263-.104 1.948-.183-.08.105-.106.158-.132.21-.288.422-.604.844-.894 1.265-.237.343-.5.712-.737 1.054-.422.555-.87 1.108-1.264 1.688-.605.87-1.158 1.766-1.79 2.635-.63.843-1.315 1.634-1.973 2.45-.868 1.134-1.684 2.293-2.552 3.426-.79 1.08-1.63 2.11-2.394 3.19-.684.947-1.29 1.95-1.948 2.923-.973 1.45-1.947 2.872-2.92 4.322a271.93 271.93 0 0 1-2.316 3.294c-.053.08-.132.104-.21.157-.21.342-.21.527-.29.685-.21.395-.42.79-.658 1.16-.132.21-.316.394-.474.605-.026-.316.42-.474.21-.87-.13.212-.263.396-.394.607l-.316.63c.105.08.29.133.105.29-.08.133-.158.29-.237.423a.954.954 0 0 0 .29-.264c0 .29-.158.526-.29.763-.105.21-.368.37-.552.527.026.027.21.106.237.132.237-.08.316-.21.343-.132.08-.105.158-.184.184-.263.104-.264.262-.474.525-.58.106-.053.184-.132.263-.21.79-.818 1.606-1.608 2.316-2.478 1.106-1.345 2.106-2.74 3.16-4.11.446-.58.973-1.16 1.446-1.714.078.606.026 1.185 0 1.74-.08.974-.132 1.95-.21 2.95-.027.395 0 .79-.027 1.186 0 .105-.08.184-.08.29 0 .263.08.553.08.817-.08.975-.186 1.923-.265 2.898-.027.21.078.422.13.607-.13 1.422.16 2.925-.078 4.427.184-.29.237-.474.237-.658.025-.158 0-.316 0-.5v-.264c.025-.475.13-.975.078-1.45-.053-.527-.053-1.027.053-1.528.053-.21-.026-.474.106-.738v.395c-.026 1.5.027 3.003-.183 4.505-.027.132.08.37-.21.343-.238.474.052.817-.21 1.08-.054.053.05.29.077.448-.106.317-.106.317.052.343.026.58.08 1.106.105 1.66.42-1 .21-2.03.396-3.058.026.422.053.844.026 1.29 0 .687-.026 1.345-.052 2.03 0 .132-.027.264-.053.396-.08.37-.105.738-.237 1.08-.105.264-.052.66-.052.975v1.003c.105.448-.027.685.052.948-.08.265-.105.344-.08.423l.08.395c.527-.053.29.343.5.553-.158.212-.105.29-.105.397 0 .237-.025.448-.052.685 0 .606-.026 1.212-.026 1.792 0 .08.026.157.026.236 0 .054-.026.74-.026.74.053.078 0 .157-.08.236-.025 0-.104-3.347-.104-3.347h-.395c-.052 1.58.08 3.003-.21 4.48-.316.025-.42.078-.764.078-.816 0-1.632 0-2.448.026-.974 0-1.92.026-2.895.026-.472 0-.972.054-1.446.054-.632 0-1.29-.08-1.92-.08-.975 0-1.922.08-2.896.106-.71.026-1.42.026-2.13.053-.475.025-.95.05-1.422.104-.21.026-.395.105-.658.184-.08 0-.263-.026-.42 0-.265.053-.5.21-.765.264-.395.08-.5.184-.448.58v.263c-.026.052.58-.08.58-.08-.054 0-.08.158-.16.29.212-.08.343-.132.475-.184.395.185.737.08 1.052.16 1.026.262 2.078.37 3.13.473.685.053 1.343.08 2.027.105.973.053 1.947.106 2.92.106.816 0 1.606-.08 2.42-.08 1.13 0 2.264.052 3.395.08.237 0 .5-.028.763-.028h1.92c1.712-.052 3.422-.08 5.133-.13.975-.028 1.975-.08 2.948-.107l3-.08c1.158-.026 2.316-.026 3.448-.05.868 0 1.71-.03 2.58-.055.972-.026 1.972-.105 2.946-.157.527-.027 1.054-.08 1.58-.132.632-.052 1.29-.13 1.92-.157.948-.054 1.922-.08 2.87-.133 1.184-.078 2.368-.183 3.578-.21 1.106-.052 2.237-.026 3.343-.052.974-.027 1.948-.08 2.948-.106l1.66-.08s1.104-.026 1.657-.08c.947-.052 1.894-.157 2.842-.183.604-.027 1.21 0 1.815-.027.973-.026 1.973-.08 2.947-.08.367 0 .762.054 1.236.08-.21.185-.342.29-.5.422.105.026.21.08.316.132a.71.71 0 0 1-.42.13c-.054.133-.107.186-.16.45h.474c-.184 0-.342.237-.526.395-.21-.054-.395 0-.5.29.184.104.158.183.132.29-.316.104-.553.21-.42.552-.107.052-.238.105-.37.184-.13.21-.368.263-.316.553.106.025.21.08.29.104-.132.053-.263.132-.395.184-.473.29-.262.422-.157.554-.08.053-.158.105-.237.132.052.237.13.29.157.29a9.3 9.3 0 0 0-.395.316c-.08.237-.185.342-.29.5s-.158.37-.29.527c-.552.607-.947 1.32-1.657 1.793-.264.185-.5.422-.737.66-.474.447-.895.948-1.395 1.37a29.595 29.595 0 0 1-2.052 1.554 151.56 151.56 0 0 1-2.604 1.792c-.474.315-1 .552-1.5.842s-.974.554-1.474.843c-.316.21-.606.5-.948.66-.868.37-1.79.685-2.684 1.028-.87.37-1.5.685-2.158.922-.605.21-1.237.37-1.868.5-.21.054-.448 0-.685.027-.448.08-.895.186-1.343.238-1.158.158-2.316.264-3.473.422-.685.08-1.343.21-2.027.29-.473.026-.973-.026-1.447-.026-.342 0-.71.08-1.053.027-.552-.08-1.105-.21-1.658-.316-.13-.026-.316-.08-.42-.026-.21.106-.396-.052-.607 0-.13.027-.262-.08-.394-.08-.106-.025-.238.028-.37 0-.29-.078-.552-.183-.87-.157-.313.026-.63-.132-.97-.21-.475-.106-.92-.21-1.396-.317a2.38 2.38 0 0 1-.525-.237c-.685 0-1.133-.026-1.554-.185-.368-.13-.71-.315-1.105-.262-.104.026-.183-.026-.29-.026-.08-.106-.157-.317-.235-.317-.526.027-.842-.42-1.29-.553-.236-.08-.42-.343-.657-.422-.58-.237-1.052-.737-1.71-.816-.21-.027-.42-.132-.658-.21.08.104.13.183.21.262-.763-.37-1.473-.79-2.184-1.186-.104-.026-.183-.13-.262-.184l-.71-.474c-.395.08-.553-.08-.66-.132-.71-.5-1.525-.817-2.21-1.37-.29-.238-.63-.396-.84-.686-.37-.448-.817-.764-1.317-1.027-.394-.21-.762-.448-1.13-.685-.185-.132-.37-.29-.37-.58 0-.185-.078-.37-.315-.264-.105-.158-.21-.342-.342-.395-.316-.13-.526-.37-.763-.58s-.42-.5-.71-.605c-.527-.21-.843-.658-1.158-1.027-.738-.87-1.396-1.82-2.08-2.74-.053-.08-.158-.133-.237-.212.105.29.237.527.368.79-.262-.105-.446-.29-.604-.474-.027.027 1.815 3.057 1.815 3.057.16.237.29.475.448.712a.813.813 0 0 1-.79-.422c-.236-.42-.5-.684-1.026-.63a4.588 4.588 0 0 1-.13-.58c-.107 0-.185 0-.37-.027.37.58.685 1.08 1.027 1.66-.133-.08-.21-.132-.265-.158.473.5.815 1.133 1.42 1.45.132.605.816.895.974 1.475-.13-.027-.238-.053-.37-.08-.21-.263-.447-.526-.683-.816.052.184.13.342.236.474.316.395.606.79.974 1.133.132.134.316.187.316.424.21.105.29.13.368.13.054.16-.025.397.29.344.21.395.42.395.71.264.343.343.528.37.764.16 0 .13.026.262.026.368.105-.053.08-.132.08-.264.13.105.21.158.262.21.263.37.5.712.868 1.002.5.422.948.87 1.42 1.265.922.765 1.95 1.398 2.975 1.977 1.264.712 2.475 1.476 3.764 2.16 1.552.818 3.21 1.372 4.92 1.767.632.132 1.237.263 1.87.42.55.16 1.104.397 1.657.528.842.185 1.71.343 2.552.5.183.027.37.054.58.08.235.053.524-.053.577.027.132.21.237.104.395.078.184-.053.395-.053.605-.053.737.026 1.447.184 2.184.132.16 0 .396-.133.528.13.236-.105.368-.105.473-.13.028.236 0 .236-.05.262-.054.026-.133.053-.238.132.947.184 1.842.21 2.63 0 1.37.105 2.554-.053 3.686-.448.105.132.184.316.342.053.052-.08.184-.107.29-.133.236-.053.526-.158.736-.08.238.08.317-.13.5-.13.317 0 .606-.027.896-.08.158-.026.316-.105.5-.158a1.285 1.285 0 0 0-.58-.133c.317-.158.606-.29.896-.42-.053.078-.106.183-.21.183h.367c-.08 0-.185.237-.316.395.946-.237 1.814-.448 2.657-.66-.29-.552.315-.367.526-.684-.263.08-.526.158-.79.21.895-.447 1.816-.842 2.71-1.237-.13.158-.29.237-.525.37.158.025.263.025.342.05.42.133.316-.262.447-.5.5 0 .71-.078.947-.158.263-.08.526-.158.79-.263.42-.184.815-.42 1.236-.63.08-.028.21 0 .316 0 .29-.186.394-.344.473-.318.37.053.63-.08.736-.42.184-.133.316-.238.447-.318.578-.316 1.13-.632 1.71-.948.21 0 .316 0 .368-.027.344-.16.66-.342.975-.527a2.258 2.258 0 0 1-.263-.13c.262-.054.34-.08.5-.133.63-.74 1.5-1.24 2.157-1.82.29-.026.29-.105.29-.157.104-.132.21-.29.34-.396.58-.527 1.21-.975 1.737-1.528a37.16 37.16 0 0 0 2.184-2.374c.63-.738 1.264-1.475 1.79-2.292.737-1.133 1.368-2.293 2.026-3.48.474-.842.895-1.685 1.37-2.528.05-.08.157-.185.236-.185.71-.08 1.422-.13 2.106-.21.158-.026.342-.13.5-.21-.08-.132-.132-.29-.21-.422-.106-.16-.264-.29-.37-.45-.104-.13-.183-.29-.262-.447-.08-.13-.158-.236-.237-.37a9.7 9.7 0 0 1-.45-.894c-.026-.08-.08-.21-.052-.29.474-1.027.658-2.134 1.105-3.162.447-1.054.58-2.24.79-3.373.184-1.08.29-2.16.42-3.24.08-.764.185-1.502.21-2.266.16-1.212.106-2.346.08-3.48-.026-1-.08-2.028-.13-3.03zM12.685 66.405c-.184-.21-.342-.448-.526-.658l.08-.08c.287.317.577.633.866.976-.158-.08-.342-.132-.42-.238zm.42.238c.08-.027.16-.027.238-.053.08.132.132.29.21.448-.368-.027-.552-.185-.447-.395zm27.37 10.883v-.08c.5-.052.973-.105 1.473-.157v.077c-.5.08-.973.13-1.473.158zm6.63-.685c-.367.08-.762.133-1.13.186-.132.026-.29.158-.342-.08-.053.027-.106.027-.158.054.13.394.447.078.71.236-.58.08-1.13.132-1.684.21v-.052c.16-.026.343-.053.5-.08v-.078a7.743 7.743 0 0 0-.79-.053c-.077 0-.183.106-.262.132-.105.026-.21.053-.342.053-.447.026-.894.026-1.316.052-.027 0-.08-.026-.106-.026v-.08c1.763-.236 3.5-.473 5.263-.71.027.052.027.105.053.157-.158 0-.263.055-.395.08zm.396-.262c.606-.08 1.16-.132 1.738-.21-1.21.342-1.605.394-1.737.21zM24.58 23.374c.84-1.16 1.71-2.32 2.552-3.505.263-.345.473-.714.736-1.056.08-.106.185-.158.316-.264l-.026-.05c.105-.133.21-.24.263-.344.134-.21.213-.448.318-.685a.385.385 0 0 1 .105-.103c.37.184.37-.21.5-.343.237-.264.474-.553.684-.817.158-.21.316-.395.448-.632.026-.08-.053-.21-.08-.317h-.078c.08-.052.158-.13.237-.184.026 0 .026 0 .052-.026.158-.238.316-.475.474-.686.315-.42.657-.842 1.025-1.21-.052.13-.105.263-.158.368.027 0 .027.027.053.027.316-.422.658-.817.974-1.24-.027-.025-.053-.052-.08-.052-.13.132-.236.264-.368.396-.026-.027-.052-.053-.08-.053.265-.343.528-.685.79-1.08.053.08.106.184.21.395.107-.263.212-.447.29-.632-.078.08-.183.158-.262.238l-.08-.08.474-.71c.5-.712 1-1.45 1.5-2.162.185-.263.42-.474.58-.738.5-1 1.29-1.792 1.894-2.714.132-.184.316-.342.474-.5.13-.16.237-.106.342.026.71.896 1.42 1.818 2.13 2.714.528.66 1.054 1.29 1.554 1.976.605.844 1.184 1.687 1.79 2.53.684.975 1.368 1.95 2.026 2.95 1 1.477 1.947 2.953 2.947 4.428.737 1.08 1.474 2.135 2.184 3.215h-1.344c-1.236-.025-2.5-.13-3.736-.078-1.684.08-3.394.264-5.078.396-2.132.185-4.29.21-6.42.21-.765 0-1.528.107-2.29.16-.922.052-1.817.105-2.738.13-1.08.054-2.13.08-3.21.107-.606.026-1.237 0-1.895 0zm30.183 12.12v.238c-.026 0-.052.027-.105.027-.105-.37-.21-.766-.342-1.135-.263-.765-.553-1.53-1.027-2.214-.528-.737-1-1.5-1.528-2.265-.13-.185-.316-.343-.474-.5-.553-.607-1.106-1.24-1.816-1.687a21.485 21.485 0 0 0-3.29-1.688 7.374 7.374 0 0 1-.92-.474h.63l4.5-.08c.974-.025 1.922-.025 2.895-.078.236 0 .368.08.5.29.236.395.473.79.736 1.186.027.052.08.13.08.21 0 .58 0 1.186.026 1.766.025.606.08 1.186.104 1.792 0 .606-.053 1.238-.026 1.87.027.897.053 1.82.053 2.74zM26.447 26.67c1.237-.053 2.42-.132 3.632-.185.945-.053 1.92-.08 2.866-.132.395-.025.764-.05 1.158 0-.42.212-.842.423-1.21.686-.474.316-.92.737-1.395 1.08-.475.342-.896.764-1.29 1.212-.5.605-1.053 1.132-1.58 1.712-.37.422-.79.817-1.105 1.265-.447.58-.842 1.21-1.263 1.87.132-2.504.29-4.98.184-7.51zm17.185 25.35c-.843.21-1.71.448-2.58.553-.736.106-1.5.08-2.263.08a25.42 25.42 0 0 1-2.028-.08c-.763-.078-1.526-.157-2.263-.5-.633-.29-1.29-.553-1.92-.87-.634-.316-1.265-.684-1.74-1.264-.34-.423-.815-.765-1.236-1.134.08.316.263.58.553.764-.132.158-.316.08-.58-.343-.078.053-.157.08-.21.106.08-.185.158-.37.237-.527-.105-.21-.237-.448-.342-.66-.21-.342-.42-.71-.605-1.053-.053-.08-.053-.158-.105-.237a5.893 5.893 0 0 1-.37-.475c-.21-.315-.394-.657-.657-.974 0 .08.027.158.027.264-.027 0-.053.026-.053.026l-.554-1.344c-.026 0-.026 0-.052.026l.473 1.74c-.026 0-.052.025-.08.025-.077-.104-.156-.21-.21-.34-.052-.212-.21-.212-.34-.133-.08.053-.133.237-.106.316.185.448.395.896.606 1.344.052.158.105.29.184.448.027.053.106.105.106.184.106.21.185.42.316.606.237.316.5.632.737.948.235.316.445.66.656.975.026.053.105.053.13.08.133.395.58.684.896.526.08.606.737.817 1 1.397a11.957 11.957 0 0 1-.763-.343c-.027.026-.027.052-.054.105.316.158.632.316.92.5.265.16.528.317.765.5.316.29.685.45 1.13.554a.282.282 0 0 0-.05-.107c.736.343 1.5.712 2.078 1-2.737.054-5.658.107-8.685.16 0-.5-.026-.975-.026-1.476 0-.21.052-.395.025-.606-.08-1.21-.08-2.424-.237-3.61-.157-1.264-.157-2.503-.13-3.77.025-.683-.027-1.394-.054-2.08 0-.922 0-1.82.028-2.74 0-.132.053-.237.106-.37h.08c.025.054 0 .133.05.16.08.08.212.21.265.184.157-.106.394-.21.447-.37.13-.315.184-.658.184-.974 0-.236.106-.394.21-.553.054-.08.08-.158.133-.263-.105-.08-.21-.132-.342-.237.106-.29.08-.633.475-.79.052-.027.052-.16.08-.238.025-.213.05-.45.078-.66.052.08.08.105.13.157a.42.42 0 0 1 .054-.08c0-.104-.026-.315 0-.315.316-.053.184-.395.342-.553.025-.028-.027-.107-.027-.16 0-.052 0-.13.026-.13.367-.08.315-.475.552-.66.08-.053.105-.13.21-.263.21.368-.158.553-.184.816.446-.263.578-.895.315-1.08.105-.08.21-.184.29-.29.29-.316.604-.606.868-.922.185-.236.29-.526.474-.763.106-.132.316-.237.474-.317.474-.262.92-.552 1.21-1 .053-.053.132-.105.21-.158.08-.053.238-.053.264-.132.027-.052-.052-.184-.105-.263.104-.053.21-.158.42-.264-.08.158-.105.264-.158.37l.13.13c.238-.184.606-.394.843-.552 0-.025-.132-.13-.132-.13-.157.08-.394.21-.63.316.05-.08.05-.132.08-.158.367-.237.735-.474 1.13-.66.92-.42 1.842-.842 2.763-1.237.158-.08.37-.026.553-.026.078 0 .13 0 .21-.026.42-.132.842-.264 1.263-.37.183-.052.393-.078.58-.078.787.025 1.577.025 2.366.078.342.026.658.105.974.21a9.88 9.88 0 0 1 1.184.5c.447.24.868.502 1.29.792.763.5 1.473 1.054 2.236 1.502.737.448 1.316 1.054 1.79 1.74.58.816 1.237 1.554 1.5 2.555l.394 1.74c.08.316.264.632.185 1-.133.66-.238 1.345-.343 2.004-.052.265-.105.53-.078.79.05.82-.265 1.53-.58 2.268-.106.237-.264.475-.395.738a.798.798 0 0 0 .21.106l.237-.474c.027 0 .027 0 .053.027-.132.368-.237.764-.37 1.133-.314.817-.63 1.66-1.025 2.45-.21.448-.58.817-.842 1.24-.262.368-.473.763-.736 1.106-.237.29-.473.58-.79.79-.71.527-1.447 1.054-2.21 1.476-.473.29-1.026.448-1.552.58zm-14.027-1.4l-.026.027c-.055-.026-.134-.052-.186-.105l-.632-.95c-.052-.078-.08-.157-.052-.262.29.448.58.87.895 1.29zm16.37 3.61c1.183-.5 2.157-1.21 3.05-2.028.133-.132.264-.263.422-.37 1.106-.684 1.92-1.633 2.658-2.687.842-1.212 1.395-2.582 2.08-3.873a2.73 2.73 0 0 1 .157-.29c-.053 3.004.29 5.955.684 8.933-2.973.105-6 .21-9.052.316zm26.683-.79c-.026.053-.08.106-.105.16-.027-.054-.027-.133-.053-.24-.158.423-.5.212-.737.212-1.42.027-2.868.027-4.29.027-1.368 0-2.762 0-4.13.024-.448 0-.922.105-1.37.132-1.078.052-2.157.08-3.236.105-.08 0-.158-.13-.29-.236a1.81 1.81 0 0 1-.158.237c-.028-.052-.08-.104-.133-.183-.026.08-.053.158-.08.21H58c-.053-.368-.158-.71-.158-1.08 0-.79.08-1.58.105-2.372.027-.368 0-.71 0-1.054.106.08.185.133.29.21.052-.103.105-.182.158-.26 0 0-.053-.028-.106-.08.05-.027.104-.08.104-.106.026-.08.08-.158.08-.21 0-.185-.054-.343-.08-.5.026 0 .052 0 .08-.028l.157.79h.08c-.106-.183.236-.342-.053-.552-.026-.027.026-.185.026-.264-.08-.157-.13-.315-.21-.526.026-.026.105-.053.184-.08-.105-.052-.184-.104-.263-.13.263-.238.263-.37.026-.633.054-.025.106-.025.106-.05 0-.238 0-.475-.052-.71-.053-.266.08-.58-.316-.74a.79.79 0 0 0 .105.21s-.08.027-.158.08c-.342-.317-.13-.74-.21-1.213.184.053.316.106.447.16-.053-.186-.184-.397-.263-.634h-.107v-1.74c0 .027.184.027.29.054 0-.027.025-.053.025-.08-.08-.105-.185-.21-.29-.342l.053-.053c-.21-.262-.105-.63-.105-.71V39.4c.264.264-.13.606.264.764v-.263h-.027c-.026-.395-.026-.79-.052-1.186h-.052c-.027.054-.027.08-.054.133h-.052l.158-6.298c.263.342.552.66.736 1 .606 1.108 1.395 2.057 2.132 3.058.632.87 1.21 1.818 1.79 2.714.71 1.08 1.394 2.16 2.105 3.24a81.41 81.41 0 0 0 1.63 2.426c.5.71 1.028 1.396 1.554 2.082.446.606.92 1.212 1.367 1.818.527.738 1.053 1.475 1.58 2.187.262.368.552.737.84 1.106.16.21.396.37.554.5-.025 0-.052 0-.104-.026.08.105.13.184.184.237.29.158.316.316.158.554zM74 46.854v-.185c0 .052.026.13 0 .184zm.895-11.62c-.027 0-.184-.16-.21-.186-.027.08 0 .158-.053.264-.027-.078-.21-.052-.21-.13-.027.368.157.737.13 1.106.08-.053.395-.08.474-.158.027.026.08.052.106.052-.527.396-.395.79-.158 1.24.052.104.21.315.052.526-.052.053.027.21.053.343h.077v.05l-.237.08c-.052-.08-.367-.236-.367-.37v1.346c.263.08.263.448.368.633a.768.768 0 0 0 .107-.21l.027.024c-.027.158-.053.316-.106.475-.052.236-.105.447-.13.684 0 .026.05.08.05.105-.288.66-.13 1.396-.235 2.08-.08.5 0 1.03-.053 1.556-.054.448-.16.922-.264 1.37-.027.08-.08.105-.21.158.052-.316.026-.527-.027-.817-.028 0-.37-.184-.397-.184 0 .37.21.87.29 1.29-.08-.026-.395-.21-.42-.21-.054.316-.054.738-.08 1.08-.027.264-.263.5-.29.79 0 .16.184.264.158.528h.21c0-.526.238-1 .238-1.554h.078c.027.053.106.106.08.132-.053.29-.16.606-.132.896 0 .158.13.316.08.5-.054.16-.08.317-.107.554-.027-.132-.053-.184-.053-.263-.026 0-.263-.027-.29-.027-.026.158.185.316.158.448-.026.026-.052.026-.105.053l-.868-1.266c-.686-1-1.37-2.003-2.054-3.03a6.312 6.312 0 0 1-.475-.79 37.09 37.09 0 0 0-2.71-4.033c-.762-.974-1.37-2.03-2.08-3.055-.656-.975-1.314-1.924-1.972-2.9-.237-.315-.526-.605-.737-.948-.683-1.08-1.29-2.187-1.972-3.267-.58-.897-1.21-1.767-1.816-2.636-.21-.29-.42-.607-.632-.923a.37.37 0 0 1-.052-.182c-.053-.58-.106-1.16-.132-1.713 0-.527.053-1.054.053-1.608v-.474c0-.132.025-.237.025-.37.025-.025.052-.078.078-.104-.763 0-1.553-.028-2.316 0-.5.025-.763-.186-1.105-.555-1-1.133-1.737-2.424-2.605-3.636a162.42 162.42 0 0 0-2.5-3.427c-.685-.922-1.37-1.818-2.053-2.74-.764-1.054-1.5-2.108-2.29-3.162a381.983 381.983 0 0 0-2.895-3.794c-.45-.58-.95-1.133-1.45-1.74.343.054.66.106.975.133l1.264.08c.947.077 1.894.13 2.84.26.79.107 1.58.265 2.396.396 1.738.29 3.448.765 5.106 1.318.974.316 1.92.738 2.87 1.133 2.13.87 4.157 1.924 6.157 3.03.63.343 1 .896 1.472 1.397.685.712 1.37 1.423 2.027 2.16.762.87 1.472 1.766 2.21 2.662.657.79 1.34 1.58 2 2.372.21.237.37.527.552.79.42.633.895 1.24 1.263 1.924.262.502.42 1.082.604 1.635.262.817.526 1.607.79 2.424.183.606.34 1.24.472 1.87.106.423.08.87.21 1.29.16.556 0 1.16.16 1.715.025.053.05.132.078.185.105.104.184.21.026.368-.025.026-.025.13 0 .21.054-.052.08-.105.133-.184 0 .053.025.08.025.105 0 .104-.027.21 0 .315 0 .052.052.13.078.184.053-.054.105-.08.21-.16.237.897.264 1.793.264 2.715 0 .87.157 1.74-.21 2.583.078-.29-.106-.555-.027-.818z"/><path d="M58.08 45.482c.025 0 .052.027.052.027l-.027-.03c0-.025 0-.025-.026 0zm4.157 26.036c-.29.21-.58.395-.948.474-.028-.026-.028-.053-.054-.08.29-.184.605-.368.895-.553.027.05.08.104.106.157zM12.895 35.81c.29-.367.58-.736.894-1.105.025.026.235.08.262.105-.29.37-.685.87-.974 1.265-.054-.053-.133-.237-.185-.264zM5.42 48.725c-.21-.448-.42-.923-.63-1.37a.91.91 0 0 1 .236-.106c.29.42.42.92.632 1.37 0 0-.21.105-.237.105zm6.712-12.65c-.158.238-.316.502-.474.74-.026-.028-.316.104-.342.078.158-.237.552-.66.71-.896.027.026.053.053.106.08zM59.422 72.6c.025 0 .025-.026.052-.026.184.026.394.052.605.052-.344.237-.555.21-.66-.026zm-47.24-35.418c.028-.08.08-.158.133-.237.052 0 .13-.027.13-.027.107-.184.107-.316.212-.474-.026-.026-.053-.026-.08-.053-.157.108-.315.24-.473.345.053.052.053.08.053.132-.21-.027-.29.08-.395.368-.026.08-.158.106-.29.21-.026.054-.052.186-.105.317l.027.028c-.053.053-.132.08-.132.08-.158.157-.342.29-.5.447-.026.08-.052.158-.052.237.185-.184.5-.527.737-.738l.027.027c.105-.158.184-.316.29-.474.025.026.025.052.052.08-.08.21-.158.446-.237.657-.055.026-.134.08-.134.053-.105.08-.184.184-.29.263l-.473.316c-.263.237-.526.447-.816.685-.184.29-.368.553-.58.896.317-.08.396.053.37.317.368.052.395-.237.5-.448.026-.054.053-.16.105-.186.237-.21.5-.394.763-.605.053-.053.053-.16.053-.238 0-.026-.133-.026-.212-.053.237-.264.58-.71.816-1 .132-.08.263-.186.263-.265-.026-.29.158-.368.37-.474-.106-.08-.133-.157-.133-.183z"/><path d="M12.71 36.892c-.105.184-.21.342-.315.527l-.158-.08c-.105.605-.474 1.132-.842 1.237.105.053.21.106.29.08.078-.027.13-.16.183-.238l.71-1.028.238-.396-.105-.105zM3.948 48.46c.132 0 .264.026.42.026 0-.105.133-.08.133-.184h.08c0 .132.026.237.026.37h-.552c-.027-.027-.132-.186-.106-.212zm-.21-1.212c-.08-.08-.21-.158-.21-.237-.027-.104.052-.235.13-.367.054.184.08.342.132.527-.027.025-.053.052-.053.078zm.658-1.687c.105.266.21.556.316.82a.798.798 0 0 0-.21.105c-.105-.264-.237-.554-.342-.817a.652.652 0 0 1 .237-.106zm58.58 25.194c.13-.052.288-.08.5-.13-.238.183-.422.315-.58.473-.027-.026-.053-.053-.08-.053.053-.105.106-.184.16-.29zM30.63 15.074c.157-.106.29-.185.447-.29l.052.052c-.16.21-.29.42-.475.685-.026-.183-.026-.29-.053-.42-.026 0 0 0 .027-.026zm7.71 13.333c.237-.106.474-.21.763-.343-.026.158-.026.264-.026.37a.927.927 0 0 0-.264-.054c-.158.027-.448.238-.58.264-.025 0 .106-.21.106-.237zm19.74 22.346c.052.263.552.395.052.658.08.055.157.08.236.134a.2.2 0 0 1-.052.106c-.053.025-.158.078-.21.05-.027 0-.08-.104-.08-.157 0-.237.027-.474.053-.79z"/></g></symbol><symbol viewBox="0 0 24 24" id="powerpoint" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M8 11v2h1v6H8v1h4v-1h-1v-2h2a3 3 0 0 0 3-3 3 3 0 0 0-3-3H8m5 2a1 1 0 0 1 1 1 1 1 0 0 1-1 1h-2v-2h2z" fill="#d14524"/></symbol><symbol viewBox="0 0 67.47 70" id="powershell" xmlns="http://www.w3.org/2000/svg"><path d="M18.545 12.4c-3.014 0-6.08 2.34-6.873 5.248L1.91 53.438c-.793 2.908.996 5.248 4.01 5.248h42.887c3.014 0 6.08-2.34 6.873-5.248l9.761-35.79c.794-2.908-.993-5.248-4.007-5.248h-42.89zm4.848 6.243c.652.04 1.29.33 1.76.86l7.96 9.013-3.957 3.246 3.957-3.244 4.832 5.47c.037.042.06.088.094.131.026.034.057.06.082.096.02.028.032.057.05.086.057.087.105.176.15.267.028.06.055.117.08.178a2.546 2.546 0 0 1 .171.764c.005.073.01.146.008.219-.002.09-.01.178-.021.267a2.53 2.53 0 0 1-.036.217 2.56 2.56 0 0 1-.07.252c-.024.076-.048.15-.08.224a2.547 2.547 0 0 1-.111.22 2.503 2.503 0 0 1-.133.218 2.546 2.546 0 0 1-.147.187c-.058.07-.118.137-.185.202-.027.026-.048.057-.076.082-.037.032-.077.054-.116.084-.038.03-.07.065-.11.093L16.8 52.271a2.552 2.552 0 0 1-3.563-.626 2.553 2.553 0 0 1 .63-3.563l18.349-12.853-3.06-3.467-7.839-8.873a2.549 2.549 0 0 1 .225-3.608 2.546 2.546 0 0 1 1.85-.638zm22.441 28.214c1.377 0 2.255 1.083 1.969 2.43-.287 1.347-1.627 2.433-3.004 2.434l-9.957.006c-1.378 0-2.256-1.083-1.969-2.43.287-1.347 1.626-2.433 3.004-2.434l9.957-.006z" fill="#03a9f4" stroke-width="5.342" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 210 210" id="prettier" xmlns="http://www.w3.org/2000/svg"><title>prettier-icon-dark</title><g transform="matrix(.9 0 0 .9 10.5 10.5)" fill="none" fill-rule="evenodd"><rect fill="#56B3B4" x="165" y="40" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="200" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="135" y="120" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="75" y="120" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="120" width="50" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="160" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="80" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="65" y="20" width="110" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="20" width="40" height="10" rx="5"/><rect fill="#F7BA3E" x="55" y="180" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="55" y="60" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="180" width="30" height="10" rx="5"/><rect fill="#F7BA3E" x="15" y="60" width="30" height="10" rx="5"/><rect fill="#56B3B4" x="95" y="100" width="90" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="100" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="100" width="20" height="10" rx="5"/><rect fill="#BF85BF" x="105" y="40" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="40" width="80" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="140" width="100" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="140" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="135" y="60" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="135" y="80" width="60" height="10" rx="5"/><rect fill="#56B3B4" x="15" width="130" height="10" rx="5"/></g></symbol><symbol viewBox="0 0 80 80" id="protractor" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="hxa"><path transform="scale(1 -1)" fill="#564b55" stroke-width="27.224" d="M-2.983-69.251h69.412v67.108H-2.983z"/></clipPath></defs><g transform="matrix(1.13039 0 0 -1.13039 5.714 82.137)" clip-path="url(#hxa)"><g transform="scale(.1)"><path d="M1180.54 92.324c-5.53 0-9.93-1.797-13.23-5.39-3.29-3.614-5.22-8.594-5.81-14.97h36.02c0 6.583-1.47 11.622-4.4 15.126-2.93 3.496-7.12 5.234-12.58 5.234zm2.84-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.88 6.21-8.83 14.824-8.83 25.84 0 11.101 2.73 19.922 8.21 26.464 5.45 6.524 12.81 9.805 22.02 9.805 8.63 0 15.46-2.851 20.48-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.45c.21-8.086 2.26-14.22 6.12-18.418 3.89-4.18 9.34-6.29 16.38-6.29 7.42 0 14.76 1.563 22 4.669V34.14c-3.68-1.602-7.18-2.746-10.48-3.438-3.28-.684-7.24-1.035-11.89-1.035M1272.34 30.918v44.57c0 5.606-1.28 9.805-3.82 12.559-2.56 2.773-6.56 4.16-12.02 4.16-7.2 0-12.49-1.953-15.84-5.851-3.34-3.895-5.03-10.32-5.03-19.286V30.918h-10.42v68.887h8.47l1.71-9.422h.5c2.14 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.15 2.804 12.88 2.804 8.29 0 14.54-2.011 18.73-6.015 4.19-3.985 6.28-10.391 6.28-19.192V30.918h-10.43M1328.96 38.406c7.1 0 12.27 1.938 15.48 5.813 3.22 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.44 6.25-15.56 6.25-6.11 0-10.79-2.383-14.04-7.129-3.26-4.746-4.88-11.472-4.88-20.136 0-8.797 1.61-15.45 4.84-19.93 3.23-4.484 7.97-6.723 14.22-6.723zm20.85 1.762h-.56c-4.83-7.004-12.02-10.5-21.62-10.5-9.01 0-16.03 3.066-21.04 9.238-5 6.153-7.5 14.922-7.5 26.27 0 11.355 2.51 20.176 7.54 26.465 5.03 6.289 12.03 9.433 21 9.433 9.34 0 16.5-3.398 21.49-10.195h.81l-.43 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.38 9.25M1434.91 38.27c1.85 0 3.63.136 5.34.421 1.72.274 3.09.547 4.1.84v-7.976c-1.15-.559-2.81-.996-5.01-1.36-2.18-.351-4.17-.527-5.94-.527-13.32 0-19.97 7.012-19.97 21.055V91.71h-9.88v5.027l9.88 4.336 4.38 14.707h6.04V99.805h20V91.71h-20V51.16c0-4.15.98-7.333 2.96-9.56 1.97-2.206 4.67-3.331 8.1-3.331M1463.81 65.43c0-8.809 1.76-15.508 5.27-20.118 3.53-4.609 8.69-6.906 15.53-6.906s12.01 2.297 15.56 6.875c3.53 4.602 5.3 11.301 5.3 20.149 0 8.75-1.77 15.41-5.3 19.953-3.55 4.539-8.77 6.824-15.69 6.824-6.82 0-11.99-2.246-15.47-6.73-3.46-4.48-5.2-11.16-5.2-20.047zm52.47 0c0-11.23-2.83-20-8.48-26.309-5.66-6.309-13.47-9.453-23.44-9.453-6.17 0-11.64 1.445-16.42 4.336-4.78 2.89-8.46 7.031-11.06 12.45-2.59 5.401-3.88 11.73-3.88 18.976 0 11.23 2.8 19.968 8.41 26.242 5.61 6.258 13.4 9.402 23.38 9.402 9.64 0 17.3-3.222 22.97-9.62 5.69-6.415 8.52-15.087 8.52-26.024M1591.71 92.324c-5.54 0-9.94-1.797-13.23-5.39-3.3-3.614-5.24-8.594-5.81-14.97h36c0 6.583-1.46 11.622-4.39 15.126-2.93 3.496-7.13 5.234-12.57 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.89 6.21-8.83 14.824-8.83 25.84 0 11.101 2.74 19.922 8.2 26.464 5.46 6.524 12.81 9.805 22.04 9.805 8.62 0 15.45-2.851 20.48-8.523 5.03-5.676 7.54-13.157 7.54-22.461v-6.613h-47.45c.21-8.086 2.25-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.36-6.29 7.43 0 14.77 1.563 22.01 4.669V34.14c-3.69-1.602-7.17-2.746-10.46-3.438-3.3-.684-7.27-1.035-11.91-1.035M1683.5 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12.01 4.16-7.2 0-12.48-1.953-15.83-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M1740.11 38.406c7.12 0 12.28 1.938 15.49 5.813 3.21 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.43 6.25-15.56 6.25-6.12 0-10.8-2.383-14.05-7.129-3.24-4.746-4.88-11.472-4.88-20.136 0-8.797 1.64-15.45 4.85-19.93 3.22-4.484 7.96-6.723 14.21-6.723zm20.87 1.762h-.57c-4.82-7.004-12.03-10.5-21.62-10.5-9.01 0-16.02 3.066-21.03 9.238-5 6.153-7.52 14.922-7.52 26.27 0 11.355 2.52 20.176 7.55 26.465 5.02 6.289 12.02 9.433 21 9.433 9.34 0 16.5-3.398 21.48-10.195h.83l-.44 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.37 9.25M1846.07 38.27c1.85 0 3.64.136 5.36.421 1.7.274 3.07.547 4.08.84v-7.976c-1.13-.559-2.8-.996-5-1.36-2.2-.351-4.18-.527-5.94-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.86v5.027l9.86 4.336 4.4 14.707h6.04V99.805H1855V91.71h-19.98V51.16c0-4.15.98-7.333 2.95-9.56 1.97-2.206 4.68-3.331 8.1-3.331M1894.26 92.324c-5.53 0-9.94-1.797-13.22-5.39-3.31-3.614-5.25-8.594-5.83-14.97h36.01c0 6.583-1.45 11.622-4.38 15.126-2.95 3.496-7.13 5.234-12.58 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.1 9.297-5.9 6.21-8.84 14.824-8.84 25.84 0 11.101 2.73 19.922 8.2 26.464 5.47 6.524 12.81 9.805 22.03 9.805 8.63 0 15.46-2.851 20.49-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.46c.22-8.086 2.26-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.37-6.29 7.42 0 14.75 1.563 22 4.669V34.14c-3.7-1.602-7.17-2.746-10.47-3.438-3.28-.684-7.25-1.035-11.9-1.035M1983.36 49.727c0-6.426-2.4-11.368-7.18-14.844-4.77-3.477-11.47-5.215-20.11-5.215-9.13 0-16.26 1.445-21.37 4.336v9.687a51.32 51.32 0 0 1 10.65-3.964c3.79-.977 7.45-1.457 10.97-1.457 5.46 0 9.64.87 12.57 2.609 2.95 1.738 4.41 4.394 4.41 7.95 0 2.694-1.17 4.98-3.5 6.894-2.32 1.914-6.85 4.152-13.6 6.757-6.41 2.383-10.97 4.473-13.67 6.25-2.71 1.778-4.72 3.81-6.04 6.067-1.31 2.254-1.98 4.96-1.98 8.113 0 5.606 2.29 10.04 6.86 13.281 4.57 3.25 10.84 4.883 18.79 4.883 7.42 0 14.66-1.515 21.74-4.531l-3.71-8.496c-6.9 2.851-13.17 4.277-18.79 4.277-4.94 0-8.67-.77-11.18-2.324-2.52-1.543-3.78-3.691-3.78-6.406 0-1.844.48-3.418 1.42-4.707.95-1.309 2.46-2.54 4.56-3.711 2.09-1.184 6.11-2.871 12.07-5.086 8.16-2.98 13.69-5.98 16.55-8.996 2.87-3.02 4.32-6.809 4.32-11.367M2021.28 38.27c1.85 0 3.64.136 5.35.421 1.71.274 3.09.547 4.09.84v-7.976c-1.14-.559-2.81-.996-5.01-1.36-2.18-.351-4.18-.527-5.93-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.87v5.027l9.87 4.336 4.4 14.707h6.02V99.805h20V91.71h-20V51.16c0-4.15 1-7.333 2.97-9.56 1.98-2.206 4.67-3.331 8.1-3.331M2053.61 30.918h-10.42v68.887h10.42zm-11.31 87.559c0 2.39.59 4.14 1.76 5.253 1.18 1.106 2.65 1.661 4.42 1.661 1.67 0 3.1-.567 4.32-1.7 1.22-1.132 1.82-2.871 1.82-5.214 0-2.344-.6-4.09-1.82-5.247-1.22-1.16-2.65-1.726-4.32-1.726-1.77 0-3.24.566-4.42 1.726-1.17 1.157-1.76 2.903-1.76 5.247M2121.59 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.49l1.69-9.422h.5c2.15 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.16 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2159.29 77.742c0-4.812 1.35-8.465 4.08-10.926 2.72-2.48 6.51-3.71 11.37-3.71 10.19 0 15.28 4.953 15.28 14.831 0 10.344-5.16 15.532-15.47 15.532-4.9 0-8.67-1.32-11.31-3.965-2.63-2.649-3.95-6.555-3.95-11.762zm-5.67-58.387c0-3.73 1.58-6.55 4.72-8.488 3.14-1.922 7.65-2.879 13.52-2.879 8.75 0 15.24 1.309 19.45 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.15 6.32-3.45 7.754-2.31 1.457-6.65 2.168-13.01 2.168h-12.51c-4.74 0-8.43-1.12-11.06-3.386-2.65-2.266-3.97-5.508-3.97-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.23-3.39 3.15-5.754.91-2.371 1.37-5.039 1.37-8.02 0-6.746-2.29-12.128-6.91-16.152-4.61-4.012-10.93-6.023-18.98-6.023-2.05 0-3.98.156-5.78.5-4.45-2.356-6.67-5.305-6.67-8.871 0-1.883.77-3.282 2.34-4.176 1.54-.902 4.21-1.36 7.97-1.36h12.2c7.46 0 13.19-1.574 17.19-4.707 4-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2192.38 2.004 2183.46 0 2171.72 0c-9 0-15.95 1.68-20.82 5.027-4.88 3.352-7.34 8.079-7.34 14.211 0 4.18 1.35 7.813 4.03 10.88 2.68 3.046 6.45 5.116 11.32 6.21-1.77.8-3.24 2.031-4.44 3.711-1.19 1.68-1.78 3.633-1.78 5.84 0 2.52.66 4.707 2.01 6.602 1.34 1.882 3.44 3.71 6.34 5.468-3.56 1.465-6.46 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.26 13.37 6.79 17.452 4.52 4.082 10.93 6.133 19.22 6.133 3.6 0 6.86-.429 9.75-1.27h23.82M2284.61 91.71h-17.54V30.919h-10.43v60.793h-12.31v4.707l12.31 3.766v3.839c0 16.922 7.4 25.391 22.19 25.391 3.65 0 7.93-.73 12.82-2.195l-2.7-8.364c-4.03 1.301-7.46 1.946-10.31 1.946-3.93 0-6.85-1.309-8.73-3.926-1.89-2.617-2.84-6.816-2.84-12.598v-4.472h17.54V91.71M2302.87 65.43c0-8.809 1.76-15.508 5.28-20.118 3.52-4.609 8.7-6.906 15.52-6.906 6.84 0 12.02 2.297 15.57 6.875 3.54 4.602 5.3 11.301 5.3 20.149 0 8.75-1.76 15.41-5.3 19.953-3.55 4.539-8.78 6.824-15.69 6.824-6.83 0-11.99-2.246-15.46-6.73-3.48-4.48-5.22-11.16-5.22-20.047zm52.48 0c0-11.23-2.82-20-8.47-26.309-5.67-6.309-13.48-9.453-23.46-9.453-6.15 0-11.62 1.445-16.4 4.336-4.77 2.89-8.47 7.031-11.06 12.45-2.59 5.401-3.9 11.73-3.9 18.976 0 11.23 2.81 19.968 8.43 26.242 5.6 6.258 13.4 9.402 23.38 9.402 9.63 0 17.28-3.222 22.97-9.62 5.68-6.415 8.51-15.087 8.51-26.024M2403.79 101.074c3.07 0 5.8-.254 8.22-.761l-1.43-9.676c-2.86.633-5.37.933-7.55.933-5.58 0-10.33-2.261-14.3-6.785-3.95-4.531-5.94-10.156-5.94-16.902V30.918h-10.43v68.887h8.62l1.19-12.754h.5c2.56 4.48 5.63 7.949 9.23 10.37 3.61 2.423 7.56 3.653 11.89 3.653M2500.33 69.766l-10.68 28.476c-1.39 3.594-2.81 8.028-4.28 13.262-.93-4.024-2.24-8.438-3.96-13.262l-10.81-28.476zm14.77-38.848l-11.44 29.227h-36.83l-11.32-29.227h-10.81l36.34 92.273h8.98l36.13-92.273h-11.05M2583.07 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2620.76 77.742c0-4.812 1.36-8.465 4.08-10.926 2.73-2.48 6.53-3.71 11.37-3.71 10.2 0 15.28 4.953 15.28 14.831 0 10.344-5.15 15.532-15.45 15.532-4.91 0-8.68-1.32-11.32-3.965-2.64-2.649-3.96-6.555-3.96-11.762zm-5.66-58.387c0-3.73 1.57-6.55 4.71-8.488 3.15-1.922 7.65-2.879 13.53-2.879 8.75 0 15.23 1.309 19.44 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.14 6.32-3.45 7.754-2.31 1.457-6.64 2.168-13 2.168h-12.51c-4.74 0-8.43-1.12-11.07-3.386-2.63-2.266-3.96-5.508-3.96-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.22-3.39 3.14-5.754.92-2.371 1.38-5.039 1.38-8.02 0-6.746-2.3-12.128-6.92-16.152-4.61-4.012-10.92-6.023-18.97-6.023-2.05 0-3.99.156-5.78.5-4.46-2.356-6.67-5.305-6.67-8.871 0-1.883.78-3.282 2.33-4.176 1.55-.902 4.21-1.36 7.98-1.36h12.2c7.46 0 13.18-1.574 17.18-4.707 4.01-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2653.87 2.004 2644.94 0 2633.2 0c-9 0-15.95 1.68-20.83 5.027-4.88 3.352-7.33 8.079-7.33 14.211 0 4.18 1.35 7.813 4.02 10.88 2.69 3.046 6.47 5.116 11.32 6.21-1.77.8-3.23 2.031-4.43 3.711-1.19 1.68-1.79 3.633-1.79 5.84 0 2.52.66 4.707 2.01 6.602 1.35 1.882 3.45 3.71 6.35 5.468-3.56 1.465-6.47 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.25 13.37 6.79 17.452 4.52 4.082 10.92 6.133 19.21 6.133 3.62 0 6.86-.429 9.75-1.27h23.83M2692.7 99.805V55.117c0-5.605 1.27-9.805 3.83-12.566 2.56-2.766 6.57-4.145 12.01-4.145 7.2 0 12.47 1.965 15.81 5.903 3.33 3.945 4.99 10.379 4.99 19.304v36.192h10.44V30.918h-8.62l-1.5 9.25h-.58c-2.13-3.41-5.1-5.988-8.88-7.793-3.8-1.809-8.13-2.707-12.99-2.707-8.37 0-14.65 1.992-18.81 5.977-4.18 3.964-6.26 10.351-6.26 19.101v45.059h10.56M2760.61 30.918h10.43v97.805h-10.43zM2810.67 38.27c6.5 0 11.6 1.789 15.31 5.343 3.71 3.575 5.56 8.555 5.56 14.961v6.23l-10.44-.448c-8.3-.286-14.27-1.583-17.94-3.868-3.66-2.273-5.5-5.82-5.5-10.644 0-3.781 1.14-6.64 3.42-8.613 2.29-1.973 5.48-2.961 9.59-2.961zm23.57-7.352l-2.07 9.805h-.51c-3.44-4.305-6.86-7.227-10.27-8.77-3.42-1.523-7.68-2.285-12.8-2.285-6.83 0-12.17 1.758-16.05 5.273-3.87 3.528-5.81 8.536-5.81 15.032 0 13.906 11.12 21.199 33.37 21.875l11.7.359v4.277c0 5.418-1.17 9.395-3.5 11.985-2.32 2.566-6.03 3.855-11.15 3.855-5.74 0-12.24-1.758-19.49-5.273l-3.21 7.988c3.4 1.836 7.11 3.281 11.16 4.324a47.81 47.81 0 0 0 12.16 1.575c8.23 0 14.3-1.817 18.27-5.461 3.96-3.66 5.93-9.5 5.93-17.54V30.919h-7.73M2893.6 101.074c3.07 0 5.8-.254 8.25-.761l-1.46-9.676c-2.84.633-5.35.933-7.54.933-5.56 0-10.33-2.261-14.3-6.785-3.96-4.531-5.93-10.156-5.93-16.902V30.918h-10.44v68.887h8.61l1.19-12.754h.5c2.57 4.48 5.65 7.949 9.25 10.37 3.6 2.423 7.56 3.653 11.87 3.653M2901.63 6.727c-3.94 0-7.04.558-9.31 1.691v9.121c2.97-.84 6.08-1.25 9.31-1.25 4.14 0 7.3 1.25 9.45 3.77 2.16 2.507 3.24 6.132 3.24 10.859v91.895h10.69V31.797c0-7.95-2.01-14.121-6.04-18.496-4.02-4.383-9.8-6.574-17.34-6.574M2999.96 55.371c0-8.086-2.93-14.394-8.8-18.918-5.87-4.52-13.83-6.785-23.88-6.785-10.9 0-19.27 1.406-25.14 4.219v10.3c3.77-1.59 7.88-2.847 12.31-3.765 4.45-.93 8.85-1.399 13.21-1.399 7.12 0 12.49 1.36 16.09 4.063 3.59 2.695 5.4 6.465 5.4 11.277 0 3.196-.63 5.805-1.91 7.832-1.29 2.024-3.42 3.907-6.42 5.625-2.99 1.711-7.56 3.664-13.67 5.84-8.55 3.059-14.66 6.692-18.32 10.871-3.66 4.2-5.51 9.668-5.51 16.407 0 7.089 2.68 12.714 7.99 16.914 5.32 4.191 12.36 6.289 21.12 6.289 9.13 0 17.54-1.68 25.2-5.032l-3.32-9.304c-7.59 3.183-14.96 4.785-22.13 4.785-5.66 0-10.07-1.223-13.26-3.652-3.19-2.43-4.78-5.809-4.78-10.118 0-3.191.59-5.8 1.76-7.832 1.17-2.031 3.14-3.886 5.95-5.597 2.78-1.688 7.04-3.563 12.79-5.625 9.63-3.426 16.26-7.118 19.89-11.063 3.62-3.937 5.43-9.043 5.43-15.332M741.648 375.406h30c28.965 0 50.227 5.039 63.774 15.117 13.531 10.079 20.32 25.821 20.32 47.247 0 19.832-6.074 34.628-18.191 44.402-12.141 9.758-31.028 14.641-56.692 14.641h-39.211zm172.192 64.246c0-36.062-11.809-63.691-35.434-82.898-23.621-19.219-57.234-28.82-100.847-28.82h-35.911V198.73h-56.445v345.329h99.438c43.14 0 75.457-8.829 96.961-26.465 21.496-17.637 32.238-43.614 32.238-77.942M1099.26 464.691c11.17 0 20.39-.789 27.63-2.371l-5.43-51.718c-7.88 1.894-16.07 2.832-24.57 2.832-22.2 0-40.19-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.514v261.227h43.464l7.32-46.055h2.83c8.66 15.594 19.96 27.95 33.9 37.09 13.93 9.141 28.93 13.699 45 13.699M1206.88 329.82c0-60.308 22.28-90.465 66.85-90.465 44.08 0 66.13 30.157 66.13 90.465 0 59.688-22.21 89.512-66.61 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.95-75.972-32.83-99.898-21.89-23.945-52.35-35.918-91.41-35.918-24.41 0-45.97 5.508-64.7 16.543-18.75 11.016-33.16 26.836-43.23 47.48-10.08 20.625-15.11 44.551-15.11 71.793 0 42.364 10.86 75.43 32.58 99.2 21.73 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.05-24.328 33.06-57.121 33.06-98.379M1558.11 238.887c13.54 0 27.07 2.129 40.62 6.386v-41.816c-6.13-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.59 0-78.88 27.715-78.88 83.144v140.778h-35.68v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.48-9.57 26.34-9.57M1783.44 464.691c11.17 0 20.38-.789 27.62-2.371l-5.43-51.718c-7.88 1.894-16.06 2.832-24.56 2.832-22.2 0-40.2-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.52v261.227h43.46l7.34-46.055h2.82c8.66 15.594 19.95 27.95 33.9 37.09 13.92 9.141 28.93 13.699 45 13.699M1925.05 236.523c20.15 0 36.32 5.625 48.52 16.895 12.21 11.25 18.31 27.051 18.31 47.344v22.676l-33.54-1.407c-26.13-.937-45.16-5.312-57.04-13.105-11.89-7.793-17.82-19.727-17.82-35.781 0-11.661 3.45-20.665 10.39-27.051 6.91-6.387 17.32-9.571 31.18-9.571zm82.66-37.793l-11.11 36.387h-1.87c-12.62-15.918-25.29-26.738-38.04-32.48-12.74-5.742-29.13-8.633-49.13-8.633-25.67 0-45.7 6.934-60.1 20.801-14.41 13.847-21.62 33.457-21.62 58.808 0 26.934 10 47.246 30 60.934 19.99 13.691 50.45 21.172 91.41 22.441l45.09 1.414v13.938c0 16.699-3.88 29.16-11.68 37.441-7.79 8.262-19.88 12.383-36.25 12.383-13.39 0-26.23-1.953-38.5-5.891a294.638 294.638 0 0 1-35.44-13.933l-17.94 39.668c14.17 7.41 29.68 13.035 46.52 16.894 16.85 3.868 32.77 5.789 47.72 5.789 33.22 0 58.31-7.246 75.22-21.726 16.94-14.492 25.4-37.246 25.4-68.262V198.73h-39.68M2220.04 194.004c-39.52 0-69.55 11.543-90.1 34.609-20.55 23.067-30.82 56.172-30.82 99.321 0 43.925 10.74 77.707 32.23 101.339 21.5 23.614 52.56 35.418 93.18 35.418 27.56 0 52.35-5.117 74.41-15.359l-16.78-44.641c-23.46 9.133-42.82 13.704-58.1 13.704-45.19 0-67.79-29.993-67.79-89.981 0-29.293 5.63-51.305 16.89-66.031 11.26-14.707 27.76-22.09 49.48-22.09 24.72 0 48.11 6.152 70.15 18.437v-48.417c-9.92-5.84-20.5-10-31.76-12.52-11.26-2.52-24.93-3.789-40.99-3.789M2451.52 238.887c13.54 0 27.08 2.129 40.63 6.386v-41.816c-6.15-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.6 0-78.9 27.715-78.9 83.144v140.778h-35.66v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.47-9.57 26.33-9.57M2585.92 329.82c0-60.308 22.28-90.465 66.84-90.465 44.09 0 66.15 30.157 66.15 90.465 0 59.688-22.22 89.512-66.62 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.94-75.972-32.83-99.898-21.89-23.945-52.36-35.918-91.4-35.918-24.42 0-45.98 5.508-64.72 16.543-18.74 11.016-33.14 26.836-43.22 47.48-10.07 20.625-15.12 44.551-15.12 71.793 0 42.364 10.87 75.43 32.59 99.2 21.74 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.04-24.328 33.06-57.121 33.06-98.379M2972.33 464.691c11.18 0 20.38-.789 27.63-2.371l-5.43-51.718c-7.87 1.894-16.05 2.832-24.57 2.832-22.2 0-40.19-7.246-53.96-21.731-13.78-14.48-20.67-33.301-20.67-56.453V198.73h-55.51v261.227h43.46l7.33-46.055h2.83c8.66 15.594 19.96 27.95 33.89 37.09 13.94 9.141 28.94 13.699 45 13.699" fill="#100f0d"/><path d="M610.11 372.83c0-170.584-138.257-308.862-308.846-308.862-170.602 0-308.846 138.278-308.846 308.863 0 170.576 138.244 308.846 308.846 308.846 170.59 0 308.846-138.27 308.846-308.846" fill="#e53935" stroke-width="1.029"/><path d="M460.694 521.792l-105.04.958-61.415 61.415-72.096-47.883 12.445-12.438-29.207.26-99.129-166.817H67.357l24.39-24.402-24.57-41.363L294.66 64.049c2.192-.04 4.399-.08 6.603-.08 170.416 0 308.585 138.055 308.846 308.408L460.694 521.792" fill="#d51c2f" stroke-width="1.029"/><path d="M149.093 350.258c0 84.048 68.13 152.151 152.171 152.151 84.028 0 152.139-68.103 152.139-152.151zm342.063-7.017v14.046h44.015c-1.75 59.337-25.556 113.104-63.54 153.419L438.75 477.81l-9.925 9.94 32.875 32.887c-40.314 37.983-94.081 61.79-153.41 63.527l-.015-44.003h-14.035v44.003c-59.34-1.737-113.096-25.556-153.41-63.527l32.887-32.887-9.945-9.92-32.883 32.875c-37.975-40.315-61.781-94.082-63.53-153.419h44.002l-.008-14.034H67.176v-51.511h468.176v51.5h-44.196" fill="#f5f5f5" stroke-width="1.029"/></g></g></symbol><symbol id="pug" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#c1272d}.hyst1{fill:#efcca3}.st2{fill:#ed1c24}.hyst3{fill:#ccac8d}.hyst4{fill:#fff}.st5{fill:#ff931e}.st6{fill:#ffb81e}.hyst7{fill:#56332b}.hyst8{fill:#442823}.hyst9{fill:#7f4a41}.hyst10{fill:#331712}.st11{fill:#fc6}.st12{fill:#ccc}.st13{fill:#b3b3b3}.st14{fill:#989898}.st15{fill:#323232}.st16{fill:#1e1e1e}.st17{fill:#4c4c4c}.st18{fill:#e6e6e6}.st19{fill:#606060}</style><path class="hyst1" d="M107.4 50.9c-.2-4.4.4-8.3-1.6-11.6-4.8-8.2-16.8-13-40.8-13v.7h-.5.5v-.7c-24 0-36.6 4.8-41.4 13.1-1.9 3.4-1.7 7.2-2 11.6-.2 3.5-1.8 7.2-1.1 11.2.8 5.2 1.1 10.4 1.9 15.2.6 3.9 6 7.2 6.5 10.9 1.4 10.2 12 14.9 36 14.9v.8h-.6.7v-.8c24 0 34.2-4.7 35.5-14.9.5-3.8 5.5-7 6.1-10.9.8-4.8 1.1-10 1.9-15.2.7-4-.9-7.8-1.1-11.3z"/><path class="hyst3" d="M64.6 54.5c4.3.1 7.3 2.8 10.1 5.3 3.3 2.9 8.9 4.9 11.2 7.4 2.3 2.5 5.3 5 6.4 8.9 1.1 3.9 1.4 8.9 1.4 10.2 0 1.3.7 1 2.7 0 4.7-2.3 9.9-8.5 9.9-8.5-.6 3.9-5.7 7.4-6.2 11.1C98.9 99.1 89 104 64.5 104h-.1.6"/><path class="hyst3" d="M80.4 46.7c.9 3.1 4.1 13.6-2.1 10.1 0 0 2.6 1.5 4.2 7.2 1.7 5.7 5.8 6.4 5.8 6.4s6.7 1.3 11.7-3c4.2-3.6 4.9-10 3.1-14.9-1.8-4.8-5-6.3-9.7-7.3-4.7-1.1-14.1-2-13 1.5z"/><circle cx="92.3" cy="58.1" r="8.8"/><circle class="hyst4" cx="90" cy="54.2" r="2.3"/><path class="hyst1" d="M78.9 57.7s7.9 5.4 12.2 10.7c4.3 5.3 4.2 6.3 4.2 6.3l-3.1 1.4s-4.4-8.3-9.8-11.4c-5.5-3.1-6.1-5.7-6.1-5.7l2.6-1.3z"/><path class="hyst3" d="M64.9 54.5c-4.3.1-7.5 2.8-10.4 5.3-3.3 2.9-9.1 4.9-11.4 7.4-2.3 2.5-5.4 5-6.5 8.9-1.1 3.9-1.5 8.9-1.5 10.2 0 1.3.2 1.4-2.7 0-4.7-2.2-9.9-8.5-9.9-8.5.6 3.9 5.7 7.4 6.2 11.1C30.1 99.1 40 104 64.5 104h.5"/><path class="hyst7" d="M88.1 71.4C83.3 65.5 75.6 60 64.9 60h-.1c-10.7 0-18.4 5.5-23.2 11.4-5 6.1-4.6 8.5-4.6 14.3 0 21 7.4 15 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.7 12.3-17.3.1-5.8.4-8.4-4.6-14.5z"/><path class="hyst8" d="M64.4 65.2s-.7 9.7-2.1 11.6l2.6-.6-.5-11z"/><path class="hyst8" d="M65.1 65.2s.7 9.7 2.1 11.6l-2.6-.6.5-11z"/><path class="hyst7" d="M56.7 62.9c-1-2.3 2.6-6 8.3-6.1 5.7 0 9.3 3.7 8.3 6.1-1 2.4-4.6 3.1-8.3 3.2-3.6-.1-7.3-.8-8.3-3.2z"/><path d="M65 65.2c0-.4 3.4-.5 5.2-1.7 0 0-3.7 1.2-4.5.7-.8-.4-1-1.6-1-1.6s-.3 1.2-.9 1.6c-.7.4-4.9-.7-4.9-.7s5.6 1.4 5.6 1.7c0 .3-.1 1.3-.1 2 0 2.5 0 8.7.4 9.2.6.9.4-6.7.4-9.2-.1-.8-.1-1.6-.2-2z"/><path class="hyst9" d="M65.2 78.6c1.7 0 4.7 1.2 7.4 3.1-2.6-2.9-5.7-4.9-7.4-4.9-1.8 0-5.6 2.2-8.3 5.4 2.8-2.2 6.4-3.6 8.3-3.6z"/><path class="hyst8" d="M64.5 96.3c-3.8 0-7.5-1.2-10.9-2.1-.7-.2-1.4.3-2.1.1-6.3-2-11.4-5.4-14.5-9.7v1c0 21 7.4 15.1 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.6 12.3-17.4 0-.8 0-1.6.1-2.3-2.9 4.7-8.2 8.4-14.8 10.6-.6.2-2-.3-2.6-.2-3.6 1.2-6.8 2.5-10.9 2.5z"/><path class="hyst8" d="M55 85s-2.5 7.5-.8 10.8l-2.3-1s1.7-7.6 3.1-9.8zM74.8 85s2.5 7.5.8 10.8l2.3-1s-1.8-7.6-3.1-9.8z"/><path class="hyst3" d="M48.6 46.7c-.9 3.1-4.1 13.6 2.1 10.1 0 0-2.6 1.5-4.2 7.2s-5.8 6.4-5.8 6.4-6.7 1.3-11.7-3c-4.2-3.6-4.9-10-3.1-14.9s5-6.3 9.7-7.3c4.7-1.1 14-2 13 1.5z"/><path d="M64.9 76.8c2.7 0 11.1 5.8 11.2 12.9v-.4c0-7.4-6.8-13.3-11.2-13.3-4.4 0-11.2 6-11.2 13.3v.4c.1-7.1 8.5-12.9 11.2-12.9z"/><ellipse transform="rotate(-14.465 66.712 61.468)" class="hyst10" cx="66.7" cy="61.5" rx=".8" ry="1.5"/><ellipse transform="rotate(17.235 62.371 61.462)" class="hyst10" cx="62.4" cy="61.5" rx=".8" ry="1.5"/><circle cx="37.2" cy="58.1" r="8.8"/><circle class="hyst4" cx="39.5" cy="54.2" r="2.3"/><path class="hyst9" d="M67.5 58.2c0-.1-2.3 1-2.9 1.1-.6-.1-2.9-1.2-2.9-1.1h5.8z"/><path class="hyst1" d="M50 57.7s-7.9 5.4-12.2 10.7c-4.3 5.3-4.2 6.3-4.2 6.3l3.1 1.4s4.4-8.3 9.8-11.4 6.1-5.7 6.1-5.7L50 57.7z"/><path class="hyst3" d="M32.7 41.7S30 49.1 24 52.2c0 0 9.4-1.1 8.7-10.5zM95.8 41.7s2.7 7.4 8.7 10.5c0 0-9.4-1.1-8.7-10.5zM78.7 55.5s-5.9-6.2-13.8-6.4h.1.1c-8 .2-13.8 6.4-13.8 6.4 6.9-4.8 12.8-4.7 13.8-4.7-.1 0 6.7-.1 13.6 4.7zM71.8 42.5s-3-4.2-7-4.3h.2c-3 .1-6.9 4.3-6.9 4.3 3.4-3.3 6.9-3.2 6.9-3.2s3.3-.1 6.8 3.2zM37.2 73.2s-4.7 2.3-8.1.9H29c-3-1.7-4.5-6.8-4.5-6.8s3 9 12.7 5.9zM92 73.2s4.7 2.3 8.1.9c4-1.7 4.6-6.8 4.6-6.8s-3 9-12.7 5.9z"/><path class="hyst3" d="M42.6 41.2c2.6-.5 6.9-.6 10.3.5 4.3 1.5.8 7 1.7 7.3.9.3 2.1-3.8 10.1-3.4 8.1.4 9 4 10.1 3.4s-1.1-10 11-7.8c0 0-12.7-3.4-12.1 5.8 0 0-7.3-5.6-17.5-.6.1 0 2.7-8.6-13.6-5.2zM86.9 41.2c.2 0 .3.1.4.1.1 0-.1-.1-.4-.1zM86.9 41.2zM39.1 28.9S28.3 42.5 26.7 47.7c-1.6 5.3-2.8 27-4.2 30.1l-5-21.4 9.2-22.3 12.4-5.2zM89.9 28.9s10.8 13.6 12.4 18.8c1.6 5.3 2.8 27 4.2 30.1l5-21.4-9.2-22.3-12.4-5.2z"/><path class="hyst7" d="M89.4 28.9s11.6 9.7 15 20.9c3.4 11.2 2 24.8 4.6 26.5 3.7 2.4 7.9-11.9 9.3-13.4 2.2-2.4 9.5-8.5 10-9.6.5-1.1-14.8-17.8-21.5-21.1-8.1-3.8-18.1-4.1-17.4-3.3z"/><path class="hyst8" d="M99.3 34.9s13.7 17.5 13.5 39.3l5.5-11.2c-.1 0-4.9-14.3-19-28.1z"/><path class="hyst7" d="M39.1 28.9s-11.6 9.7-15 20.9-2 24.8-4.6 26.5c-3.7 2.4-7.9-11.9-9.3-13.4C8 60.5.7 54.4.2 53.3-.3 52.2 15 35.5 21.7 32.2c8.1-3.8 18.1-4.1 17.4-3.3z"/><path class="hyst8" d="M29.2 34.9S15.5 52.4 15.7 74.2L10.3 63s4.8-14.3 18.9-28.1z"/><path class="hyst3" d="M21.8 74.6s1 5.4 2.6 7.1.5-1.3.5-1.3-1.7-.9-1.4-7.8-1.7 2-1.7 2zM107.1 74.6s-1 5.4-2.6 7.1-.5-1.3-.5-1.3 1.7-.9 1.4-7.8 1.7 2 1.7 2z"/><g><circle class="hyst8" cx="54.5" cy="70.5" r=".8"/><circle class="hyst8" cx="49.9" cy="75.3" r=".8"/><circle class="hyst8" cx="48.4" cy="70.5" r=".8"/></g><g><circle class="hyst8" cx="74" cy="70.5" r=".8"/><circle class="hyst8" cx="78.6" cy="75.3" r=".8"/><circle class="hyst8" cx="80.1" cy="70.5" r=".8"/></g></symbol><symbol viewBox="0 0 50 50" id="puppet" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" fill="#fbc02d"><path stroke-width=".283" d="M11.559 249.467h13.587v13.587H11.559zM27.435 265.056h13.587v13.587H27.435zM11.559 281.074h13.587v13.587H11.559z"/><path stroke-width=".256" d="M16.62 251.615l18.305 18.305-3.236 3.236-18.305-18.305z"/><path stroke-width=".256" d="M37.834 271.331L19.53 289.636l-3.237-3.237 18.305-18.304z"/></g></symbol><symbol viewBox="0 0 100 99.999997" id="purescript" xmlns="http://www.w3.org/2000/svg"><path clip-path="url(#SVGID_2_)" d="M98.079 38.548L79.22 19.68l-5.087 5.088L90.447 41.09 74.134 57.41l5.087 5.087 18.858-18.86a3.59 3.59 0 0 0 1.055-2.55 3.578 3.578 0 0 0-1.055-2.54M25.483 42.794l-5.09-5.089L1.53 56.568a3.566 3.566 0 0 0-1.05 2.545c0 .961.373 1.863 1.05 2.542L20.394 80.52l5.089-5.086L9.162 59.113z" fill="#42a5f5" stroke-width="1.192"/><path clip-path="url(#SVGID_2_)" transform="matrix(1.19175 0 0 1.19175 -306.84 -629.047)" fill="#42a5f5" d="M281.841 551.736l6.461 6.037h28.379l-6.461-6.037zM288.302 566.861l-6.463 6.035h28.381l6.463-6.035zM281.838 581.982l6.464 6.035h28.381l-6.463-6.035z"/></symbol><symbol viewBox="0 0 24 24" id="python" xmlns="http://www.w3.org/2000/svg"><path d="M19.14 7.5A2.86 2.86 0 0 1 22 10.36v3.78A2.86 2.86 0 0 1 19.14 17H12c0 .39.32.96.71.96H17v1.68a2.86 2.86 0 0 1-2.86 2.86H9.86A2.86 2.86 0 0 1 7 19.64v-3.75a2.85 2.85 0 0 1 2.86-2.85h5.25a2.85 2.85 0 0 0 2.85-2.86V7.5h1.18m-4.28 11.79c-.4 0-.72.3-.72.89 0 .59.32.71.72.71a.71.71 0 0 0 .71-.71c0-.59-.32-.89-.71-.89m-10-1.79A2.86 2.86 0 0 1 2 14.64v-3.78A2.86 2.86 0 0 1 4.86 8H12c0-.39-.32-.96-.71-.96H7V5.36A2.86 2.86 0 0 1 9.86 2.5h4.28A2.86 2.86 0 0 1 17 5.36v3.75a2.85 2.85 0 0 1-2.86 2.85H8.89a2.85 2.85 0 0 0-2.85 2.86v2.68H4.86M9.14 5.71c.4 0 .72-.3.72-.89 0-.59-.32-.71-.72-.71-.39 0-.71.12-.71.71s.32.89.71.89z"/><path d="M9.264 22.379c-.895-.24-1.581-.799-1.947-1.582-.228-.489-.237-.606-.238-2.957-.001-2.745.057-3.074.666-3.785.193-.226.568-.517.833-.648.47-.23.579-.239 3.839-.288 3.131-.048 3.386-.065 3.814-.264.626-.291 1.07-.687 1.4-1.247.27-.46.278-.522.311-2.29l.034-1.82.932.051c1.075.058 1.504.211 2.098.748.853.77.869.841.869 3.957 0 2.434-.02 2.783-.18 3.075a3.365 3.365 0 0 1-1.337 1.33l-.517.273-3.95.031-3.951.031.068.274c.037.151.164.377.282.503.209.224.262.229 2.433.229h2.22v1.05c0 1.653-.394 2.437-1.54 3.072l-.545.302-2.644.018c-1.455.01-2.782-.018-2.95-.063zm6.12-1.692c.22-.222.253-.325.206-.675-.07-.523-.278-.73-.732-.73-.467 0-.672.217-.735.78-.042.372-.012.496.163.672.3.3.77.28 1.097-.047z" fill="#fc0" stroke="#fc0" stroke-width=".102"/><path d="M9.349 22.38c-.911-.15-1.936-1.074-2.176-1.963-.073-.273-.101-1.279-.079-2.868.033-2.317.047-2.473.27-2.926.13-.263.401-.623.603-.8.674-.592.87-.63 3.484-.675 4.399-.076 4.927-.166 5.705-.967.642-.662.706-.9.774-2.883l.061-1.784.951.055c.523.031 1.11.122 1.304.204.54.225 1.358 1.042 1.472 1.47.153.572.243 3.18.16 4.617-.071 1.23-.093 1.327-.395 1.78-.193.288-.577.647-.966.903l-.647.425-3.922.008c-2.157.004-3.942.028-3.966.052-.115.115.354.82.587.883.14.038 1.181.073 2.314.079l2.06.01v.91c0 1.739-.326 2.446-1.454 3.162l-.631.4-2.543-.011c-1.398-.007-2.733-.043-2.966-.081zm5.98-1.718c.285-.256.313-.328.251-.658-.09-.483-.301-.682-.722-.682-.436 0-.625.193-.715.73-.065.384-.044.453.2.663.358.308.595.295.985-.053z" fill="#fdd835" stroke-width=".102"/><path d="M4.281 17.396c-.88-.215-1.714-.935-2.024-1.747-.149-.389-.168-.804-.142-3.041.027-2.26.054-2.638.215-2.962.259-.519.851-1.092 1.392-1.346.437-.206.632-.217 4.408-.245l3.95-.03-.067-.275a1.367 1.367 0 0 0-.282-.504c-.21-.224-.263-.23-2.433-.23h-2.22l.002-1.143c.003-1.338.157-1.795.84-2.493.746-.763 1.103-.838 4.025-.838 2.961 0 3.28.06 4.067.768.37.333.572.621.728 1.037.201.539.213.735.183 3.072-.035 2.777-.045 2.824-.78 3.598-.787.829-.76.824-4.59.883-3.812.06-3.797.057-4.61.806-.765.706-.917 1.2-.964 3.133l-.04 1.653-.677-.01c-.371-.007-.813-.045-.98-.086zM9.59 5.551c.237-.204.286-.326.286-.72 0-.547-.201-.763-.71-.763-.502 0-.765.248-.765.724 0 .492.141.782.439.902.345.14.444.12.75-.143z" fill="#3c78aa"/></symbol><symbol viewBox="0 0 24 24" id="r" xmlns="http://www.w3.org/2000/svg"><path d="M11.956 4.05c-5.694 0-10.354 3.106-10.354 6.947 0 3.396 3.686 6.212 8.531 6.813v2.205h3.53V17.82c.88-.093 1.699-.259 2.475-.497l1.43 2.692h3.996l-2.402-4.048c1.936-1.263 3.147-3.034 3.147-4.97 0-3.841-4.659-6.947-10.354-6.947m1.584 2.712c4.349 0 7.558 1.45 7.558 4.753 0 1.77-.952 3.013-2.505 3.779a1.081 1.081 0 0 1-.228-.156c-.373-.165-.994-.352-.994-.352s3.085-.227 3.085-3.302-3.23-3.127-3.23-3.127h-7.092v7.413c-2.64-.766-4.462-2.392-4.462-4.255 0-2.63 3.52-4.753 7.868-4.753m.156 4.12h2.143s.983-.05.983.974c0 1.004-.983 1.004-.983 1.004h-2.143v-1.977m-.031 4.566h.952c.186 0 .28.052.445.207.135.103.28.3.404.476-.57.073-1.17.104-1.801.104z" fill="#1976d2" stroke-width="1.035"/></symbol><symbol viewBox="0 0 24 24" id="raml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="razor" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 11.91c-.11-2.21-1.75-3.54-3.73-3.54h-.08c-2.29 0-3.55 1.8-3.55 3.84 0 2.29 1.53 3.74 3.54 3.74 2.25 0 3.72-1.65 3.83-3.59m-3.81-5.97c1.53 0 2.97.68 4.02 1.74 0-.51.33-.89.83-.89h.11c.74 0 .89.7.89.92v7.9c-.04.52.54.78.87.44 1.27-1.29 2.78-6.69-.79-9.81-3.33-2.92-7.8-2.44-10.18-.8-2.52 1.74-4.14 5.61-2.57 9.22 1.71 3.95 6.61 5.13 9.52 3.95 1.48-.59 2.15 1.4.65 2.05-2.34.99-8.77.89-11.78-4.32-2.03-3.52-1.93-9.71 3.46-12.92C10.81 1.42 16.24 2.1 19.5 5.5c3.45 3.6 3.25 10.3-.1 12.91-1.51 1.18-3.76.03-3.74-1.7l-.02-.56a5.611 5.611 0 0 1-3.99 1.66C8.63 17.81 6 15.15 6 12.13c0-3.05 2.63-5.74 5.65-5.74z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="react" xmlns="http://www.w3.org/2000/svg"><path d="M12 10.11c1.03 0 1.87.84 1.87 1.89 0 1-.84 1.85-1.87 1.85-1.03 0-1.87-.85-1.87-1.85 0-1.05.84-1.89 1.87-1.89M7.37 20c.63.38 2.01-.2 3.6-1.7-.52-.59-1.03-1.23-1.51-1.9a22.7 22.7 0 0 1-2.4-.36c-.51 2.14-.32 3.61.31 3.96m.71-5.74l-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16l-.3-.51m6.54-.76l.81-1.5-.81-1.5c-.3-.53-.62-1-.91-1.47C13.17 9 12.6 9 12 9c-.6 0-1.17 0-1.71.03-.29.47-.61.94-.91 1.47L8.57 12l.81 1.5c.3.53.62 1 .91 1.47.54.03 1.11.03 1.71.03.6 0 1.17 0 1.71-.03.29-.47.61-.94.91-1.47M12 6.78c-.19.22-.39.45-.59.72h1.18c-.2-.27-.4-.5-.59-.72m0 10.44c.19-.22.39-.45.59-.72h-1.18c.2.27.4.5.59.72M16.62 4c-.62-.38-2 .2-3.59 1.7.52.59 1.03 1.23 1.51 1.9.82.08 1.63.2 2.4.36.51-2.14.32-3.61-.32-3.96m-.7 5.74l.29.51c.11-.29.22-.58.29-.86-.27-.06-.57-.11-.88-.16l.3.51m1.45-7.05c1.47.84 1.63 3.05 1.01 5.63 2.54.75 4.37 1.99 4.37 3.68 0 1.69-1.83 2.93-4.37 3.68.62 2.58.46 4.79-1.01 5.63-1.46.84-3.45-.12-5.37-1.95-1.92 1.83-3.91 2.79-5.38 1.95-1.46-.84-1.62-3.05-1-5.63-2.54-.75-4.37-1.99-4.37-3.68 0-1.69 1.83-2.93 4.37-3.68-.62-2.58-.46-4.79 1-5.63 1.47-.84 3.46.12 5.38 1.95 1.92-1.83 3.91-2.79 5.37-1.95M17.08 12c.34.75.64 1.5.89 2.26 2.1-.63 3.28-1.53 3.28-2.26 0-.73-1.18-1.63-3.28-2.26-.25.76-.55 1.51-.89 2.26M6.92 12c-.34-.75-.64-1.5-.89-2.26-2.1.63-3.28 1.53-3.28 2.26 0 .73 1.18 1.63 3.28 2.26.25-.76.55-1.51.89-2.26m9 2.26l-.3.51c.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.29.51m-2.89 4.04c1.59 1.5 2.97 2.08 3.59 1.7.64-.35.83-1.82.32-3.96-.77.16-1.58.28-2.4.36-.48.67-.99 1.31-1.51 1.9M8.08 9.74l.3-.51c-.31.05-.61.1-.88.16.07.28.18.57.29.86l.29-.51m2.89-4.04C9.38 4.2 8 3.62 7.37 4c-.63.35-.82 1.82-.31 3.96a22.7 22.7 0 0 1 2.4-.36c.48-.67.99-1.31 1.51-1.9z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="readme" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="reason" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm5.119 8.993h2.798c.382 0 .71.025.985.075.275.05.534.159.774.326.244.168.435.386.577.654.145.265.218.598.218 1 0 .552-.112 1.001-.335 1.35-.22.348-.536.638-.947.87l2.16 3.203H12.31l-1.763-2.742h-.77v2.742H8.12v-7.478zm6.594 0h4.676v1.447h-3.018v1.29h2.802v1.447h-2.802v1.848h3.018v1.446h-4.676v-7.478zM9.778 13.37v2.014h.513c.266 0 .49-.014.67-.044.18-.03.329-.1.45-.207a.96.96 0 0 0 .253-.34c.055-.128.082-.297.082-.508 0-.187-.034-.35-.1-.483a.698.698 0 0 0-.343-.317 1.086 1.086 0 0 0-.395-.095 6.012 6.012 0 0 0-.526-.02h-.604z" fill="#f44336" stroke-width="1.067"/></symbol><symbol viewBox="0 0 172 193" id="restql" xmlns="http://www.w3.org/2000/svg"><title>Group</title><g transform="translate(14.767 16.713) scale(.82795)" fill="none"><path d="M171.39 55.799c-.975-6.147-4.673-11.642-10.15-14.805L96.381 3.546C93.217 1.72 89.615.756 85.964.756s-7.253.964-10.415 2.788L10.69 40.992A20.896 20.896 0 0 0 .272 59.035v74.89a20.894 20.894 0 0 0 10.416 18.042l64.859 37.446c3.165 1.827 6.767 2.791 10.417 2.791s7.252-.964 10.415-2.79l64.859-37.445c5.479-3.166 9.178-8.66 10.152-14.808zm-16.516 85.147L90.017 178.39a8.104 8.104 0 0 1-8.108 0l-64.857-37.444a8.109 8.109 0 0 1-4.053-7.021v-74.89a8.109 8.109 0 0 1 4.053-7.021l64.857-37.446c1.254-.725 2.654-1.086 4.054-1.086s2.8.361 4.054 1.086l64.857 37.446a8.106 8.106 0 0 1 4.053 7.021v74.89a8.109 8.109 0 0 1-4.053 7.021z" fill="#83e8c2"/><path d="M158.93 59.035a8.109 8.109 0 0 0-4.053-7.021L90.02 14.568c-1.254-.725-2.654-1.086-4.054-1.086s-2.8.361-4.054 1.086L17.055 52.014a8.106 8.106 0 0 0-4.053 7.021v74.89a8.109 8.109 0 0 0 4.053 7.021l64.857 37.444a8.104 8.104 0 0 0 8.108 0l64.857-37.444a8.109 8.109 0 0 0 4.053-7.021zm-46.766 31.681c.119-.069.242-.118.365-.149.044-.012.088-.01.131-.018.076-.012.152-.029.228-.029l.015.001c.02.001.038.005.059.006.093.005.184.019.273.04l.1.03c.077.025.15.057.223.095.028.014.057.027.084.043.094.057.184.122.263.199.007.008.013.017.021.024.07.071.133.15.188.235.018.029.033.059.05.09.04.072.072.148.099.229a1.512 1.512 0 0 1 .081.46v16.209l-3.278 1.893a1.548 1.548 0 0 0-.678.83 1.533 1.533 0 0 0-.098.514v3.785l-14.038 8.104-.01.004a1.55 1.55 0 0 1-.354.146c-.045.012-.09.011-.135.018-.074.012-.15.029-.225.029l-.014-.001c-.02-.001-.039-.005-.059-.006a1.463 1.463 0 0 1-.273-.041c-.034-.008-.066-.019-.1-.03a1.318 1.318 0 0 1-.223-.094c-.029-.015-.057-.027-.084-.044a1.45 1.45 0 0 1-.263-.198c-.009-.008-.015-.019-.023-.027a1.495 1.495 0 0 1-.185-.232c-.019-.029-.034-.06-.051-.09a1.422 1.422 0 0 1-.098-.229 1.702 1.702 0 0 1-.033-.101 1.487 1.487 0 0 1-.048-.358l-.001-.002v-20.053a1.446 1.446 0 0 1 .727-1.255zM85.24 31.369a1.449 1.449 0 0 1 1.452 0l45.741 26.41a1.45 1.45 0 0 1 0 2.512l-17.366 10.027a1.457 1.457 0 0 1-1.452 0l-15.49-8.943 1.727-.996a1.552 1.552 0 0 0 0-2.688l-13.111-7.57c-.239-.139-.508-.207-.775-.207s-.535.068-.775.207l-3.278 1.893-14.038-8.104a1.451 1.451 0 0 1 0-2.513zM57.59 47.558c.251 0 .501.065.726.194l15.489 8.942-1.727.997a1.552 1.552 0 0 0 0 2.688l1.727.996-15.488 8.943a1.457 1.457 0 0 1-1.452 0L39.499 60.291a1.45 1.45 0 0 1 0-2.512l17.366-10.027c.225-.129.475-.194.725-.194zm-9.56 92.328c-.241 0-.489-.062-.724-.196l-17.365-10.026a1.45 1.45 0 0 1-.726-1.256V75.59c0-.847.694-1.453 1.452-1.453.242 0 .49.062.724.197l17.366 10.025c.449.26.726.738.726 1.257v17.886l-1.727-.997a1.552 1.552 0 0 0-2.327 1.344v15.139c0 .555.295 1.067.775 1.344l3.278 1.894v16.209a1.45 1.45 0 0 1-1.452 1.451zm29.828 14.929a1.452 1.452 0 0 1-2.177 1.257l-17.365-10.026a1.452 1.452 0 0 1-.726-1.257v-17.885l1.726.996c.25.145.515.211.773.211.811 0 1.554-.648 1.554-1.555v-1.993l15.489 8.942c.449.26.726.738.726 1.257zm0-32.768c0 .127-.02.246-.049.36-.009.035-.021.067-.032.101-.026.08-.059.157-.099.229-.017.03-.032.061-.05.09a1.48 1.48 0 0 1-.188.235l-.021.025a1.51 1.51 0 0 1-.264.199c-.026.016-.055.028-.082.043a1.597 1.597 0 0 1-.324.124 1.362 1.362 0 0 1-.278.041c-.018.001-.036.006-.055.006l-.015.001c-.077 0-.155-.018-.233-.03-.043-.007-.084-.005-.125-.017a1.484 1.484 0 0 1-.366-.149l-14.035-8.104v-3.784a1.545 1.545 0 0 0-.776-1.343l-3.276-1.892V91.976c0-.127.02-.246.049-.361.009-.034.021-.066.032-.1a1.33 1.33 0 0 1 .099-.229c.017-.03.032-.062.051-.091.054-.084.116-.163.187-.234l.021-.025c.079-.076.168-.142.263-.199.027-.016.056-.029.084-.043a1.476 1.476 0 0 1 .601-.166c.019 0 .036-.005.055-.005l.015-.001c.078 0 .157.018.236.03.04.007.081.005.122.017.124.031.246.08.366.149l17.361 10.023a1.456 1.456 0 0 1 .726 1.259zm-9.984-45.373a1.448 1.448 0 0 1-.544-.55 1.466 1.466 0 0 1 0-1.413c.121-.219.303-.41.544-.55l14.038-8.104 3.277 1.892c.48.276 1.071.276 1.551 0l3.278-1.893 14.038 8.105a1.45 1.45 0 0 1 0 2.513L86.691 86.7a1.447 1.447 0 0 1-1.452 0zm74.842 51.733c0 .518-.276.997-.726 1.256l-45.741 26.409a1.452 1.452 0 0 1-2.177-1.257v-20.053c0-.519.277-.997.727-1.257l15.488-8.941v1.992c0 .906.743 1.555 1.553 1.555.26 0 .523-.066.774-.21l13.11-7.57a1.55 1.55 0 0 0 .776-1.344v-3.784l14.038-8.105a1.452 1.452 0 0 1 2.177 1.257v20.052zm0-32.764c0 .519-.276.997-.726 1.256l-15.489 8.943v-1.993c0-.906-.744-1.554-1.554-1.554a1.519 1.519 0 0 0-.773.21l-1.727.996V85.616c0-.519.277-.997.727-1.257l17.365-10.025c.234-.135.482-.197.724-.197.758 0 1.453.606 1.453 1.453z" fill="#111d5a"/><g fill="#83e8c2"><path d="M59.402 90.568zM94.485 123.06zM94.771 123.29zM77.775 122.51zM77.072 123.33zM77.418 123.09zM77.856 122.05zM76.749 123.45zM94.119 122.41zM77.131 133.51l-15.489-8.942v1.993c0 .906-.743 1.555-1.554 1.555a1.53 1.53 0 0 1-.773-.211l-1.726-.996v17.885c0 .519.276.997.726 1.257l17.365 10.026a1.452 1.452 0 0 0 2.177-1.257v-20.053a1.454 1.454 0 0 0-.726-1.257zM94.25 122.74zM110.28 111.42zM94.494 100.98c.088-.089.189-.168.303-.232l17.365-10.026-17.365 10.026a1.392 1.392 0 0 0-.303.232zM77.627 122.83zM58.027 90.936zM58.374 90.693zM59.044 90.521l-.015.001c.083-.001.167.015.251.029-.079-.012-.158-.03-.236-.03zM57.819 91.195zM58.696 90.568zM57.589 91.977zM76.043 123.46zM57.67 91.516zM75.677 123.31l-14.035-8.11zM76.401 123.5l.015-.001c-.082.001-.166-.016-.248-.029.078.012.156.03.233.03zM112.16 90.716zM77.662 101.27zM113.64 90.734zM96.237 123.31zM113.33 90.597zM112.89 90.52c-.075 0-.151.018-.228.029.081-.014.162-.029.242-.028l-.014-.001zM141.26 74.137c-.241 0-.489.062-.724.197l-17.365 10.025c-.449.26-.727.738-.727 1.257v17.885l1.727-.996c.25-.145.515-.211.773-.21.81 0 1.554.647 1.554 1.554v1.993l15.489-8.943a1.45 1.45 0 0 0 .726-1.256V75.59c0-.847-.695-1.453-1.453-1.453zM112.96 90.526zM95.523 123.5c.074 0 .15-.018.225-.029-.08.013-.159.028-.238.028l.013.001zM95.451 123.5zM85.238 86.7zM95.078 123.43zM141.26 106.9c-.241 0-.489.062-.724.196l-14.038 8.105v3.784c0 .555-.296 1.067-.776 1.344l-13.11 7.57c-.251.144-.515.21-.774.21-.81 0-1.553-.648-1.553-1.555v-1.992l-15.488 8.941c-.449.26-.727.738-.727 1.257v20.053a1.452 1.452 0 0 0 2.177 1.257l45.741-26.409a1.45 1.45 0 0 0 .726-1.256v-20.053a1.454 1.454 0 0 0-1.454-1.452zM67.871 41.396a1.451 1.451 0 0 0 0 2.513l14.038 8.104 3.278-1.893c.24-.139.508-.207.775-.207s.536.068.775.207l13.111 7.57a1.552 1.552 0 0 1 0 2.688l-1.727.996 15.49 8.943a1.457 1.457 0 0 0 1.452 0l17.366-10.027a1.45 1.45 0 0 0 0-2.512l-45.741-26.41a1.449 1.449 0 0 0-1.452 0zM39.497 57.779a1.45 1.45 0 0 0 0 2.512l17.366 10.027a1.457 1.457 0 0 0 1.452 0l15.488-8.943-1.727-.996a1.552 1.552 0 0 1 0-2.688l1.727-.997-15.489-8.942a1.458 1.458 0 0 0-1.451 0zM49.481 138.43v-16.209l-3.278-1.894a1.55 1.55 0 0 1-.775-1.344v-15.139c0-.906.743-1.555 1.554-1.554.259 0 .523.065.773.21l1.727.997V85.611a1.45 1.45 0 0 0-.726-1.257L31.39 74.33a1.436 1.436 0 0 0-.724-.197c-.758 0-1.452.606-1.452 1.453v52.817c0 .518.276.997.726 1.256l17.365 10.026a1.45 1.45 0 0 0 2.176-1.255zM114.34 108.18l-3.278 1.893 3.278-1.893V91.971zM114.11 91.193zM114.16 91.283z"/></g><g fill="#de5941"><path d="M94.494 100.98a1.45 1.45 0 0 0-.424 1.023v20.053l.001.002c0 .126.02.244.048.358.01.034.021.066.033.101.026.08.059.156.098.229.017.03.032.061.051.09.055.084.115.162.185.232.009.009.015.02.023.027.079.077.169.142.263.198.027.017.055.029.084.044a1.46 1.46 0 0 0 .596.165c.02.001.039.005.059.006.079 0 .158-.016.238-.028.045-.007.09-.006.135-.018.119-.031.238-.08.354-.146l.01-.004 14.038-8.104v-3.785c0-.18.04-.35.098-.514.122-.343.353-.643.678-.83l3.278-1.893V91.977c0-.127-.021-.246-.049-.361-.009-.033-.021-.065-.032-.099a1.266 1.266 0 0 0-.099-.229c-.017-.031-.032-.061-.05-.09a1.425 1.425 0 0 0-.188-.235l-.021-.024a1.41 1.41 0 0 0-.263-.199c-.027-.016-.056-.029-.084-.043a1.509 1.509 0 0 0-.323-.125 1.591 1.591 0 0 0-.273-.04c-.021-.001-.039-.005-.059-.006-.08-.001-.161.015-.242.028-.043.008-.087.006-.131.018-.123.031-.246.08-.365.149l-17.365 10.026a1.447 1.447 0 0 0-.302.233zM77.13 100.74L59.769 90.717a1.424 1.424 0 0 0-.366-.149c-.041-.012-.082-.01-.122-.017-.084-.015-.168-.03-.251-.029-.019 0-.036.005-.055.005-.095.005-.188.02-.278.041-.034.009-.065.02-.099.03a1.406 1.406 0 0 0-.224.095c-.028.014-.057.027-.084.043a1.515 1.515 0 0 0-.263.199l-.021.025c-.07.071-.133.15-.187.234-.019.029-.034.061-.051.091-.04.073-.072.149-.099.229a1.463 1.463 0 0 0-.081.461v16.206l3.276 1.892a1.547 1.547 0 0 1 .776 1.343v3.784l14.035 8.104c.119.068.242.117.366.149.041.012.082.01.125.017.082.014.166.03.248.029.019 0 .037-.005.055-.006.095-.004.188-.019.278-.041.034-.008.065-.019.099-.029.077-.025.152-.058.225-.095.027-.015.056-.027.082-.043.095-.058.185-.123.264-.199l.021-.025c.07-.071.133-.15.188-.235.018-.029.033-.06.05-.09.04-.072.072-.149.099-.229a1.448 1.448 0 0 0 .081-.461v-20.047a1.456 1.456 0 0 0-.726-1.259zM86.689 86.7l17.365-10.026a1.45 1.45 0 0 0 0-2.513l-14.038-8.105-3.278 1.893a1.556 1.556 0 0 1-1.551 0l-3.277-1.892-14.038 8.104c-.241.14-.423.331-.544.55a1.466 1.466 0 0 0 0 1.413c.121.218.303.41.544.55L85.238 86.7a1.447 1.447 0 0 0 1.451 0z"/></g></g></symbol><symbol viewBox="0 0 24 24" id="riot" xmlns="http://www.w3.org/2000/svg"><defs><path d="M13.26 3.04l.58.05.54.07.52.09.49.11.46.13.44.14.41.16.39.17.36.19.33.21.32.22.29.23.26.25.22.22.2.22.19.24.17.24.15.25.15.26.12.27.12.28.1.29.08.31.07.31.05.32.04.34.02.35.01.37v.05l-.02.51-.05.49-.09.48-.13.45-.15.43-.19.4-.22.39-.26.37-.28.34-.31.33-.33.3-.37.28-.39.27-.41.24-.44.22L21 21h-7.04l-3.48-5.14H9.17V21H3V3h9.01l.64.01.61.03zm-4.09 8.52h2.66l.99-.11.75-.35.47-.55.16-.74v-.05l-.17-.75-.47-.54-.74-.32-.96-.11H9.17v3.52z" id="ija"/></defs><use xlink:href="#ija" fill="#ff1744"/><use xlink:href="#ija" fill-opacity="0" stroke="#000" stroke-opacity="0"/></symbol><symbol viewBox="0 0 24 24" id="robot" xmlns="http://www.w3.org/2000/svg"><path d="M12.05 2.804a1.787 1.787 0 0 1 1.788 1.788c0 .661-.357 1.242-.893 1.546v1.135h.893a6.256 6.256 0 0 1 6.256 6.256h.894a.894.894 0 0 1 .893.893v2.681a.894.894 0 0 1-.893.894h-.894v.894a1.787 1.787 0 0 1-1.787 1.787H5.795a1.787 1.787 0 0 1-1.787-1.787v-.894h-.894a.894.894 0 0 1-.894-.894v-2.68a.894.894 0 0 1 .894-.894h.894a6.256 6.256 0 0 1 6.255-6.256h.894V6.138a1.773 1.773 0 0 1-.894-1.546 1.787 1.787 0 0 1 1.788-1.788m-4.022 9.83a2.234 2.234 0 0 0-2.234 2.235 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.234-2.234 2.234 2.234 0 0 0-2.234-2.234m8.043 0a2.234 2.234 0 0 0-2.234 2.234 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.235-2.234 2.234 2.234 0 0 0-2.235-2.234z" fill="#ff5722" stroke-width=".894"/></symbol><symbol viewBox="100 100 800 800" id="rollup" xmlns="http://www.w3.org/2000/svg"><style>.ilst0{fill:url(#ilXMLID_4_)}.ilst1{fill:url(#ilXMLID_5_)}.ilst2{fill:url(#ilXMLID_8_)}.ilst3{fill:url(#ilXMLID_9_)}.ilst4{fill:url(#ilXMLID_11_)}.ilst5{opacity:.3;fill:url(#ilXMLID_16_)}</style><g id="ilXMLID_14_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_4_" x1="444.47" x2="598.47" y1="526.05" y2="562.05" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_15_" class="ilst0" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_4_)"/></g><g id="ilXMLID_2_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_5_" x1="420.38" x2="696.38" y1="475" y2="689" gradientUnits="userSpaceOnUse"><stop stop-color="#BF3338" offset="0"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_10_" class="ilst1" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_5_)"/></g><linearGradient id="ilXMLID_8_" x1="429.39" x2="469.39" y1="517.16" y2="559.16" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_3_" class="ilst2" d="M329.82 813.46c15.58-8.903 122.41-220.34 227.02-320.5s117.96-66.771 60.094-175.83c0 0-221.46 310.49-301.58 464.06" fill="url(#ilXMLID_8_)" stroke-width="1.113"/><g id="ilXMLID_7_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_9_" x1="502.11" x2="490.11" y1="589.46" y2="417.46" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_12_" class="ilst3" d="M373 537c134.4-247.1 152-272 222-272 36.8 0 73.9 16.6 97.9 46.1-32.7-52.7-90.6-88-156.9-89H307.7c-4.8 0-8.7 3.9-8.7 8.7V691c13.6-35.1 36.7-85.3 74-154z" fill="url(#ilXMLID_9_)"/></g><linearGradient id="ilXMLID_11_" x1="450.12" x2="506.94" y1="514.21" y2="552.85" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FBB040" offset="0"/><stop stop-color="#FB8840" offset="1"/></linearGradient><path id="ilXMLID_6_" class="ilst4" d="M556.84 492.96c-104.61 100.16-211.44 311.6-227.02 320.5s-41.732 10.016-55.643-5.564c-14.801-16.582-37.837-43.401 86.802-272.65 149.57-274.99 169.15-302.7 247.05-302.7 40.953 0 82.24 18.473 108.95 51.302 1.447 2.337 2.893 4.785 4.34 7.233-45.738-47.074-145.23-57.98-169.93-.222-25.373 59.204 42.622 125.08 72.335 119.85 37.837-6.677-6.677-93.48-6.677-93.48 57.757 108.95 44.403 75.563-60.205 175.72z" fill="url(#ilXMLID_11_)" stroke-width="1.113"/><linearGradient id="ilXMLID_16_" x1="508.33" x2="450.33" y1="295.76" y2="933.76" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFF" offset="0"/><stop stop-color="#FFF" stop-opacity="0" offset="1"/></linearGradient><path id="ilXMLID_13_" class="ilst5" d="M373.22 547.49c149.57-274.99 169.15-302.7 247.05-302.7 33.719 0 67.661 12.575 93.48 35.277-26.708-30.492-66.326-47.519-105.72-47.519-77.9 0-97.486 27.71-247.05 302.7-124.64 229.25-101.6 256.07-86.802 272.65 2.114 2.337 4.563 4.34 7.122 6.01-13.02-18.919-18.807-62.877 91.922-266.42z" fill="url(#ilXMLID_16_)" opacity=".3" stroke-width="1.113"/></symbol><symbol viewBox="0 0 24 24" id="ruby" xmlns="http://www.w3.org/2000/svg"><path d="M16 9h3l-5 7m-4-7h4l-2 8M5 9h3l2 7m5-12h2l2 3h-3m-5-3h2l1 3h-4M7 4h2L8 7H5m1-5L2 8l10 14L22 8l-4-6H6z" fill="#f44336"/></symbol><symbol viewBox="0 0 144 144" id="rust" xmlns="http://www.w3.org/2000/svg"><path d="M68.252 26.206a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0M25.766 58.451a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m84.97.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m-74.661 4.88a3.252 3.252 0 0 0 1.651-4.29l-1.58-3.574h6.214v28.01H29.823a43.847 43.847 0 0 1-1.42-16.738zm25.994.688v-8.256h14.798c.764 0 5.397.883 5.397 4.347 0 2.877-3.553 3.908-6.475 3.908zm-20.203 44.452a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m52.769.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m1.101-8.076a3.246 3.246 0 0 0-3.856 2.498l-1.787 8.342a43.847 43.847 0 0 1-36.566-.175l-1.787-8.342a3.246 3.246 0 0 0-3.854-2.497l-7.365 1.581a43.847 43.847 0 0 1-3.808-4.488h35.834c.406 0 .676-.074.676-.443V84.527c0-.369-.27-.442-.676-.442h-10.48V76.05h11.335c1.035 0 5.532.296 6.97 6.045.45 1.768 1.44 7.519 2.116 9.36.674 2.065 3.417 6.19 6.34 6.19h18.501a43.847 43.847 0 0 1-4.06 4.7zm19.898-33.468a43.847 43.847 0 0 1 .093 7.612h-4.499c-.45 0-.631.296-.631.737v2.066c0 4.863-2.742 5.92-5.145 6.19-2.288.258-4.825-.958-5.138-2.358-1.35-7.593-3.6-9.214-7.152-12.016 4.409-2.8 8.996-6.93 8.996-12.457 0-5.97-4.092-9.729-6.881-11.572-3.914-2.58-8.246-3.096-9.415-3.096H39.336A43.847 43.847 0 0 1 63.867 28.52l5.484 5.753a3.243 3.243 0 0 0 4.59.105l6.137-5.869a43.847 43.847 0 0 1 30.017 21.38l-4.201 9.487a3.256 3.256 0 0 0 1.652 4.29zm10.477.154l-.143-1.467 4.327-4.036c.88-.82.55-2.472-.574-2.891l-5.532-2.068-.433-1.428 3.45-4.792c.704-.974.058-2.53-1.127-2.724l-5.833-.949-.7-1.31 2.45-5.38c.502-1.095-.43-2.496-1.636-2.45l-5.92.206-.935-1.135 1.36-5.766c.275-1.17-.913-2.36-2.084-2.085l-5.765 1.359-1.136-.935.207-5.92c.046-1.198-1.357-2.135-2.45-1.637l-5.379 2.452-1.31-.703-.95-5.833c-.193-1.183-1.75-1.83-2.723-1.128l-4.796 3.45-1.425-.432-2.068-5.532c-.42-1.127-2.072-1.452-2.89-.576l-4.036 4.33-1.467-.143-3.117-5.036c-.63-1.02-2.318-1.02-2.946 0l-3.117 5.036-1.467.143-4.037-4.33c-.819-.876-2.47-.551-2.89.576l-2.069 5.532-1.426.432-4.795-3.45c-.974-.703-2.53-.055-2.723 1.128l-.951 5.833-1.31.703-5.379-2.452c-1.093-.5-2.496.439-2.45 1.637l.206 5.92-1.136.935-5.765-1.36c-1.171-.272-2.36.915-2.086 2.086l1.358 5.766-.933 1.135-5.92-.206c-1.193-.035-2.134 1.355-1.637 2.45l2.453 5.38-.703 1.31-5.832.949c-1.185.192-1.827 1.75-1.128 2.724l3.45 4.792-.433 1.428-5.532 2.068c-1.123.42-1.452 2.07-.574 2.891l4.328 4.036-.143 1.467-5.035 3.116c-1.02.63-1.02 2.318 0 2.946l5.035 3.117.143 1.467-4.328 4.037c-.878.818-.549 2.468.574 2.89l5.532 2.068.433 1.428-3.45 4.793c-.701.976-.056 2.532 1.129 2.723l5.831.948.703 1.312-2.453 5.378c-.5 1.093.444 2.5 1.638 2.451l5.917-.207.935 1.136-1.358 5.768c-.275 1.168.915 2.355 2.086 2.08l5.765-1.357 1.137.932-.207 5.921c-.046 1.199 1.357 2.136 2.45 1.636l5.379-2.45 1.31.702.95 5.83c.193 1.187 1.75 1.829 2.725 1.13l4.792-3.453 1.427.435 2.069 5.53c.42 1.123 2.072 1.454 2.89.574l4.037-4.328 1.467.146 3.117 5.035c.628 1.016 2.316 1.018 2.946 0l3.117-5.035 1.467-.146 4.036 4.328c.818.88 2.47.549 2.89-.574l2.068-5.53 1.428-.435 4.793 3.453c.974.699 2.53.055 2.722-1.13l.952-5.83 1.31-.703 5.378 2.451c1.093.5 2.493-.435 2.45-1.636l-.206-5.92 1.135-.933 5.765 1.357c1.171.275 2.36-.912 2.085-2.08l-1.358-5.768.932-1.136 5.92.207c1.194.048 2.138-1.358 1.636-2.451l-2.45-5.378.7-1.312 5.833-.948c1.187-.19 1.831-1.747 1.127-2.723l-3.45-4.793.433-1.428 5.532-2.068c1.125-.422 1.454-2.072.574-2.89l-4.327-4.037.143-1.467 5.035-3.117c1.02-.628 1.021-2.315.001-2.946z" fill="#ff7043" stroke-width="1.146"/></symbol><symbol viewBox="0 0 500 500" id="sass" xmlns="http://www.w3.org/2000/svg"><path d="M422.676 96.573c-12.192-47.839-91.508-63.557-166.575-36.892-44.68 15.877-93.029 40.786-127.81 73.311-41.349 38.675-47.943 72.328-45.216 86.395 9.583 49.622 77.585 82.069 105.535 106.126v.144c-8.246 4.05-68.565 34.584-82.684 65.799-14.893 32.932 2.372 56.556 13.804 59.742 35.424 9.859 71.764-7.866 91.311-37.01 18.853-28.12 17.28-64.422 9.086-82.487 11.3-2.976 24.476-4.314 41.218-2.36 47.248 5.52 56.517 35.017 54.747 47.366-1.77 12.35-11.681 19.14-14.998 21.186-3.317 2.045-4.326 2.766-4.05 4.287.405 2.215 1.94 2.137 4.758 1.652 3.894-.656 24.804-10.042 25.709-32.828 1.14-28.933-26.587-61.302-75.684-60.45-20.216.354-32.933 2.268-42.123 5.69-.681-.774-1.363-1.547-2.084-2.307-30.35-32.382-86.46-55.285-84.088-98.824.866-15.823 6.372-57.5 107.817-108.052 83.104-41.415 149.637-30.009 161.135-4.76 16.427 36.08-35.554 103.137-121.858 112.812-32.88 3.684-50.198-9.059-54.498-13.804-4.536-4.995-5.204-5.218-6.909-4.287-2.753 1.533-1.01 5.938 0 8.574 2.583 6.712 13.15 18.603 31.176 24.515 15.863 5.205 54.459 8.063 101.156-9.99 52.283-20.255 93.12-76.523 81.125-123.548zM200.213 340.34c3.92 14.5 3.487 28.016-.564 40.248a65.289 65.289 0 0 1-3.225 7.97c-3.12 6.477-7.316 12.534-12.442 18.132-15.653 17.069-37.507 23.532-46.88 18.092-10.122-5.874-5.048-29.944 13.083-49.11 19.52-20.636 47.602-33.903 47.602-33.903l-.039-.079 2.465-1.35z" fill="#ec407a" stroke="#ec407a" stroke-width="16.286552999999998"/></symbol><symbol viewBox="0 0 300 300" id="sbt" xmlns="http://www.w3.org/2000/svg"><path d="M105.46 209.517c-7.875 0-13.452-7.521-13.452-15.37v-.327c0-7.848 5.578-13.735 13.452-13.735h164.05c1.476-4.905 2.625-11.446 3.281-17.986h-137.81c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h137.31c-.82-6.54-1.969-13.081-3.773-17.986h-104.01c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h91.87c-21.327-37.607-60.864-61.315-106.14-61.315-67.918 0-123.04 54.448-123.04 122.3 0 67.856 55.122 123.28 123.04 123.28 46.59 0 87.112-25.507 107.95-63.114h-152.73z" fill="#0277bd" stroke-width="1.638"/></symbol><symbol viewBox="0 0 256 256" id="scala" xmlns="http://www.w3.org/2000/svg"><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M59.607 50.647l149.097-21.982v49.488L59.607 100.135zM59.593 114.08L208.69 92.098v49.488L59.593 163.568zM59.587 177.358l149.097-21.982v49.488L59.587 226.846z"/><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M62.425 91.414l95.605 30.923-2.832 8.757-95.605-30.922zM113.084 61.13l95.604 30.922-2.832 8.757-95.605-30.922zM62.425 154.79l95.605 30.922-2.833 8.758-95.604-30.923zM113.097 124.408l95.604 30.923-2.832 8.757-95.605-30.922z"/></symbol><symbol viewBox="0 0 24 24" id="settings" xmlns="http://www.w3.org/2000/svg"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="shaderlab" xmlns="http://www.w3.org/2000/svg"><path d="M9.11 17H6.5l-4.91-5L6.5 7h2.61l1.31-2.26L17.21 3l1.87 6.74L17.77 12l1.31 2.26L17.21 21l-6.79-1.74L9.11 17m.14-.25l5.13 1.38L11.42 13H5.5l3.75 3.75m6.87.38L17.5 12l-1.38-5.13L13.15 12l2.97 5.13M9.25 7.25L5.5 11h5.92l2.96-5.13-5.13 1.38z" fill="#1976d2"/></symbol><symbol viewBox="0 0 24 24" id="slim" xmlns="http://www.w3.org/2000/svg"><path d="M6.959 2.5a4.605 4.605 0 0 0-4.615 4.615v9.957a4.605 4.605 0 0 0 4.615 4.615h9.957a4.605 4.605 0 0 0 4.615-4.615V7.115A4.605 4.605 0 0 0 16.916 2.5zm4.938 2.691a6.811 6.811 0 0 1 6.81 6.813H13.43L9.938 7.287l.699 4.717H5.086a6.811 6.811 0 0 1 6.81-6.813z" fill="#f57f17"/></symbol><symbol id="smarty" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.iust0{fill:#ffce00}</style><path class="iust0" d="M9.14 20.606c0 .556.398.953.954.953h3.812c.556 0 .953-.397.953-.953v-.953H9.141zM12 2.5c-3.653 0-6.671 3.018-6.671 6.671 0 2.303 1.112 4.289 2.859 5.48v2.144c0 .556.397.953.953.953h5.718c.556 0 .953-.397.953-.953V14.65c1.747-1.191 2.86-3.177 2.86-5.48 0-3.653-3.019-6.671-6.672-6.671zm2.7 10.563l-.794.555v2.224h-3.812v-2.224l-.794-.555A4.712 4.712 0 0 1 7.235 9.17 4.78 4.78 0 0 1 12 4.405a4.78 4.78 0 0 1 4.765 4.765 4.712 4.712 0 0 1-2.065 3.892z"/></symbol><symbol viewBox="0 0 200 200" id="snyk" xmlns="http://www.w3.org/2000/svg"><title>Group 2</title><g transform="translate(15.255 18.22) scale(1.8477)" fill="none" fill-rule="evenodd"><path d="M65.161 24.997c-1.656 5.974-5.255 23.587-5.255 23.587s-6.618-2.464-14.148-2.476h-.055c-.413.002-.822.012-1.23.026v41.649h6.677v.003h5.815v-.003h20.858c.111-8.177-2.036-27.066-2.036-27.066-1.088-2.279.46-7.668.46-7.668-8.869-9.092-11.086-28.051-11.086-28.051zm-3.357 43.958c5.476 0 1.381 4.64.9 5.168H52.35c.944-1.18 4.504-5.168 9.453-5.168z" fill="#607d8b" stroke-width="1.6"/><path d="M26.366 24.995s-2.217 18.961-11.087 28.053c0 0 1.548 5.391.46 7.669 0 0-2.15 18.895-2.038 27.066h19.273v.003h7.079v-.003h5.744V46.107h-.025c-7.532.013-14.151 2.478-14.151 2.478s-3.6-17.615-5.255-23.59zm3.264 43.96c4.95 0 8.51 3.987 9.452 5.168H28.73c-.479-.528-4.573-5.168.9-5.168z" fill="#90a4ae" stroke-width="1.6"/><g transform="translate(23.76 77.45) scale(1.5998)"><g transform="translate(17.526)"><path d="M7.357.06H.177v.075C.177 2.64 2.345 4.67 4.89 4.67 7.431 4.67 9.6 2.64 9.6.135V.059z" fill="#455a64"/><path d="M1.972.06v.075a2.692 2.692 0 1 0 5.386 0V.059z" fill="#fff"/><path d="M5.496.06H4.234c-.012 0-.023.005-.034.007.157.033.243.388.21.624a.721.721 0 0 1-.71.617c.102.471.487.85.997.922a1.188 1.188 0 0 0 1.35-1.007C6.112.743 5.881.06 5.495.06z" fill="#37474f"/></g><path d="M7.552.06H.372v.075c0 2.505 2.17 4.535 4.712 4.535 2.544 0 4.712-2.03 4.712-4.535V.059z" fill="#455a64"/><path d="M2.168.06v.075a2.692 2.692 0 1 0 5.385 0V.059z" fill="#fff"/><path d="M5.692.06H4.428c-.01 0-.022.005-.032.007.156.033.242.388.21.624a.72.72 0 0 1-.712.617c.104.471.488.85.999.922A1.187 1.187 0 0 0 6.24 1.223C6.308.743 6.078.06 5.69.06z" fill="#37474f"/></g><path d="M25.514-.27l-4.202 7.697C19.838 10.17 6.858 34.465 6.858 43.243v.516L12.8 59.573c-.8 7.258-2.203 21.643-1.78 28.21h5.73c-.354-3.787.648-17.008 1.903-28.25l.076-.677-1.075-2.892c3.694-3.868 6.285-9.193 8.073-14.261l.174 1.235 5.869 9.629 2.291-.983c.058-.024 5.935-2.523 11.643-2.523 5.672 0 11.646 2.5 11.702 2.525l2.29.976 5.86-9.626.23-1.608c1.769 5.117 4.358 10.536 8.07 14.49l-1.127 3.035.076.678c1.259 11.286 2.266 24.564 1.916 28.252h5.677c.406-6.567-1.05-20.952-1.848-28.208l5.838-15.817v-.514c0-8.779-12.876-33.074-14.347-35.816L65.923-.27l-5.897 41.229-2.723 4.478c-2.628-.882-7.1-2.11-11.603-2.11-4.498 0-8.94 1.225-11.557 2.108l-2.722-4.476-2.07-14.452a.832.832 0 0 0 .006-.071l-.016-.004zm-3.166 18.39l1.206 8.407c-.46 3.143-2.561 15.47-8.198 23.24l-2.598-6.99c.325-4.554 5.067-15.462 9.59-24.656zm46.763 0c4.523 9.194 9.267 20.104 9.592 24.657L76.166 49.6c-6.09-8.553-8-22.459-8.166-23.73z" fill="#607d8b" stroke-width="1.6"/></g></symbol><symbol viewBox="0 0 24 24" id="solidity" xmlns="http://www.w3.org/2000/svg"><path d="M5.8 14.05l6.253 8.61 6.252-8.61-6.254 3.807z" fill="#0288d1" stroke-width="4.553" stroke-linejoin="round"/><path d="M12.051 1.347L5.8 11.833l6.252 3.807 6.254-3.807z" fill="#0288d1" stroke-width="5.025" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 120 120" id="sonar" xmlns="http://www.w3.org/2000/svg"><style>.a,.b{fill:#fff}.b{stroke:#fff;stroke-miterlimit:10}</style><path d="M115.45 23.033S97.961 33.27 97.534 33.412c-.427.284-.852.57-1.137.854-1.422 1.421-1.848 3.41-1.422 5.26.285.852.711 1.849 1.422 2.56.711.71 1.564 1.137 2.559 1.422 1.848.426 3.84 0 5.262-1.422.426-.427.709-.853.851-1.28l.143-.427 2.56-4.692zm-39.102 9.242c-27.441 0-31.99 13.08-31.99 29.29 0 3.838.569 7.962-1.99 11.942-3.84 5.972-8.957 5.828-10.236 5.828-1.706 0-7.962-.993-8.246-2.841h.994c6.682 0 11.658-5.404 11.658-12.655v-2.56h-5.686c-4.123 0-7.82 1.849-10.238 5.12-2.417-3.271-6.113-5.12-10.236-5.12h-5.83v2.56c0 7.11 5.688 12.795 12.797 12.795h1.848c0 4.124 5.687 20.332 47.63 20.332 16.352 0 40.665-2.843 40.665-33.697 0-5.829-1.848-11.23-4.691-15.78-.996.284-1.992.568-3.13.568a8.92 8.92 0 0 1-8.956-8.957c0-.995.141-1.991.425-2.986-4.265-2.702-8.53-3.838-14.787-3.838z" fill="#1e88e5" stroke-width="1.422"/></symbol><symbol viewBox="0 0 412 395" id="stylelint" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-white</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#cfd8dc" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 412 395" id="stylelint_light" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-black</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#546e7a" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 200.00001 200.00001" id="stylus" xmlns="http://www.w3.org/2000/svg"><path d="M126.814 155.9c14.64-17.51 16.362-35.595 5.024-69.18-7.177-21.24-19.09-37.602-10.334-50.807 9.329-14.065 29.135-.43 12.63 18.371l3.301 2.297c19.806 2.296 29.566-24.83 14.783-32.58C113.179 3.621 79.02 42.803 94.09 88.156c6.458 19.232 15.5 39.613 8.18 55.83-6.314 13.923-18.514 22.103-26.695 22.39-17.079.862-5.74-38.32 13.922-48.08 1.722-.861 4.162-2.01 1.866-4.88-24.256-2.727-38.464 8.468-46.645 24.112-23.825 45.497 45.21 62.29 82.095 18.371z" fill="#c0ca33" stroke-width="1.435"/></symbol><symbol viewBox="0 0 24 24" id="swc" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="jba"><stop offset="0" stop-color="#791223"/><stop offset="1" stop-color="#d92f3c"/></linearGradient><linearGradient xlink:href="#jba" id="jbb" x1="12.356" y1="21.559" x2="12.356" y2="2.949" gradientUnits="userSpaceOnUse"/></defs><path d="M6 3c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6 3 6.5V19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.5c0-.5-.17-.93-.46-1.27l-1.39-1.68C18.88 3.21 18.47 3 18 3H6zm-.07 1h12l.94 1H5.12l.81-1z" fill="url(#jbb)"/><path style="line-height:125%" d="M11.053 11.918h-.008c-.244.022-.475.054-.676.11a2.9 2.9 0 0 0-.856.412 3.399 3.399 0 0 0-.67.683 9.36 9.36 0 0 0-.586.95c-.07.131-.134.244-.201.365v.001h-.002l-.768 1.372-.003-.001c-.136.253-.264.485-.38.686-.123.212-.26.39-.411.539a1.599 1.599 0 0 1-.52.34c-.04.016-.092.024-.138.036h-.567v1.383H5.834v-.001c.245-.02.477-.053.679-.11a2.9 2.9 0 0 0 .856-.411c.245-.185.469-.413.67-.683.195-.275.39-.591.585-.95.07-.131.135-.244.202-.366l.004.001.002-.002.02-.038H10.948v-1.378h-.19v-.001H9.624c.125-.234.246-.452.355-.64.123-.21.259-.39.41-.538.152-.148.325-.26.52-.34.04-.015.091-.024.136-.035h.57V13.3h-.002v-1.381h-.56v-.001z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol viewBox="0 0 24 24" id="swift" xmlns="http://www.w3.org/2000/svg"><path d="M17.09 19.72c-2.36 1.36-5.59 1.5-8.86.1A13.807 13.807 0 0 1 2 14.5c.67.55 1.46 1 2.3 1.4 3.37 1.57 6.73 1.46 9.1 0-3.37-2.59-6.24-5.96-8.37-8.71-.45-.45-.78-1.01-1.12-1.51 8.28 6.05 7.92 7.59 2.41-1.01 4.89 4.94 9.43 7.74 9.43 7.74.16.09.25.16.36.22.1-.25.19-.51.26-.78.79-2.85-.11-6.12-2.08-8.81 4.55 2.75 7.25 7.91 6.12 12.24-.03.11-.06.22-.05.39 2.24 2.83 1.64 5.78 1.35 5.22-1.21-2.39-3.48-1.65-4.62-1.17z" fill="#fe5e2f"/></symbol><symbol viewBox="0 0 24 24" id="table" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5m4 7.5h-4v2h1l-2 1.67L10 13h1v-2H7v2h1l3 2.5L8 18H7v2h4v-2h-1l2-1.67L14 18h-1v2h4v-2h-1l-3-2.5 3-2.5h1v-2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 200 200" id="terraform" xmlns="http://www.w3.org/2000/svg"><g transform="translate(177.03 -58.705) scale(.92881)" fill="#5c6bc0" stroke="#b0aff5" stroke-linejoin="round"><g stroke-width=".288"><path transform="skewY(26.439) scale(.89541 1)" d="M-203.8 170.95h64.714v51.88H-203.8zM-124.37 171.04h64.714v51.88h-64.714zM-124.37 236.09h64.714v51.88h-64.714z"/></g><path transform="skewY(-22.59) scale(-.92328 1)" stroke-width=".284" d="M-19.172 128.27h62.76v51.88h-62.76z"/></g></symbol><symbol viewBox="0 0 24 24" id="test-js" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="test-jsx" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="test-ts" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tex" xmlns="http://www.w3.org/2000/svg"><g font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-linejoin="miter"><text style="line-height:125%" x="9.914" y="364.919"><tspan x="9.914" y="364.919" font-size="287.5">T</tspan></text><text style="line-height:125%" x="136.374" y="435.558"><tspan x="136.374" y="435.558" font-size="287.5">E</tspan></text><text style="line-height:125%" x="307.819" y="361.201"><tspan x="307.819" y="361.201" font-size="287.5">X</tspan></text></g></symbol><symbol viewBox="0 0 24 24" id="todo" xmlns="http://www.w3.org/2000/svg"><path d="M3 5h6v6H3V5m2 2v2h2V7H5m6 0h10v2H11V7m0 8h10v2H11v-2m-6 5l-3.5-3.5 1.41-1.41L5 17.17l4.59-4.58L11 14l-6 6z" fill="#42a5f5"/></symbol><symbol id="travis" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style id="jkstyle2">.jkst0{fill:#cb3349}.jkst1{fill:#f4edae}.jkst2{fill:#e6ccad}.jkst3{fill:#656c67}.jkst4{fill:#e5caa3}.jkst5{fill:#c7b39a}.jkst6{fill:#ebd599}.jkst7{fill:#2d3136}.jkst8{fill:#edf6fa}.jkst9{opacity:.8}.jkst10{opacity:.75;fill:#ebd599}</style><g id="jkg99" transform="translate(11.017 12.484) scale(.8858)"><g id="jkg10"><path class="jkst0" d="M47.781 86.572s-31.118 21.903-32.335 30.247l2.335-.48S55.045 91.64 84.584 88.628l.669-3.749z" id="jkpath4" fill="#cb3349"/><path class="jkst0" d="M96.629 83.442l-24.511 17.385 1.325 1.063c.999-.806 43.539-13.798 43.539-13.798l8.969-5.623c-6.018.749-29.322.973-29.322.973z" id="jkpath6" fill="#cb3349"/><path class="jkst0" d="M117.932 104.469c17.405 0 43.495-17.046 43.495-17.046l-8.434-1.605c-.417.417-13.6-.462-13.6-.462l-6.258-1.738-14.951 17.036-1.217 2.956c1.075-.437.965.859.965.859z" id="jkpath8" fill="#cb3349"/></g><path class="jkst0" d="M174.728 158.832l-5.377 1.514-24.843-.537-15.541-12.085-18.784 4.7-21.726-1.88-12.166 13.294-22.828 6.819-11.398-3.534-.574-.494 5.116 12.527s11.588 12.424 18.061 13.885c6.472 1.461 18.165-.105 26.935-1.463 8.769-1.357 15.764-4.489 18.582-9.603 2.818-5.117 3.236-6.578 3.236-6.578s8.353 11.797 15.556 13.155c7.203 1.357 28.605-5.952 28.605-5.952s13.051-3.549 15.346-8.038c2.297-4.489 8.353-19.209 8.353-19.209zM44.456 169.038l-.361-.166-2.013-1.736z" id="jkpath12" fill="#cb3349"/><g id="jkg97"><path class="jkst1" d="M195.832 70.085a48.125 48.125 0 0 0-.21-2.009 26.472 26.472 0 0 0-.215-1.424c-1.793-1.509-3.831-2.851-5.952-4.071-2.299-1.343-4.704-2.546-7.159-3.663-2.438-1.15-4.942-2.191-7.461-3.207a134.313 134.313 0 0 0-3.798-1.477c-1.269-.495-2.55-.956-3.835-1.424 2.697.447 5.366 1.059 8.015 1.741 1.723.446 3.437.945 5.14 1.477-12.112-31.655-41.07-52.27-72.687-52.27-31.622 0-60.577 20.615-72.686 52.27a109.044 109.044 0 0 1 5.137-1.477c2.653-.682 5.323-1.294 8.018-1.741-1.289.468-2.567.929-3.84 1.424-1.267.472-2.536.967-3.798 1.477-2.519 1.016-5.016 2.057-7.46 3.207-2.45 1.117-4.857 2.32-7.156 3.663-2.121 1.219-4.157 2.562-5.957 4.071-.075.457-.151.951-.21 1.424a51.768 51.768 0 0 0-.21 2.009 51.354 51.354 0 0 0-.177 4.061 59.216 59.216 0 0 0 .5 8.11c.37 2.692.864 5.366 1.595 7.951.36 1.295.768 2.572 1.24 3.808.237.617.495 1.225.764 1.816.134.294.274.585.413.864l.172.328c.199.101.408.204.607.3l1.204.575c.671.305 1.6.746 2.368 1.09.043-.037.086-.075.123-.114l-2.235-8.513c.474-.13 4.718-1.225 12.032-2.617a38.816 38.816 0 0 1-1.772-.381c-1.665-.414-3.309-.919-4.899-1.564a22.415 22.415 0 0 1-2.309-1.115c-.742-.426-1.472-.908-2.037-1.548 8.036 2.622 24.64 1.434 39.399-.091 13.499-1.391 27.029-2.293 40.63-2.32 13.602.027 27.137.929 40.63 2.32 14.766 1.525 31.37 2.713 39.405.091-.564.64-1.293 1.123-2.035 1.548a22.5 22.5 0 0 1-2.308 1.115c-1.592.645-3.234 1.15-4.899 1.564-.247.059-.496.113-.743.166 8.02 1.488 12.689 2.697 13.188 2.831l-2.138 8.11c.43-.194.864-.381 1.29-.574l1.202-.575c.2-.097.403-.199.607-.3l.166-.328c.146-.279.286-.57.419-.864.27-.591.528-1.199.764-1.816a42.235 42.235 0 0 0 1.241-3.808c.731-2.585 1.225-5.259 1.595-7.951.345-2.685.526-5.398.501-8.11a50.874 50.874 0 0 0-.179-4.059z" id="jkpath14" fill="#f4edae"/><path class="jkst2" d="M116.787 182.661c-1.064.16-2.128.295-3.186.375-.682.033-1.404.102-2.059.102l-.242.005c.822-1.837 1.446-3.26 1.919-4.339.963 1.08 2.188 2.417 3.568 3.857z" id="jkpath16" fill="#e6ccad"/><path class="jkst2" d="M119.101 185.018c3.304 3.272 7.398 5.146 11.904 5.479-7.569 3.074-14.702 4.26-20.197 4.63-5.478.367-11.032-.279-16.474-1.771.456-.082.79-.14 1.193-.189.447-.054 10.206-1.327 14.605-7.868l.413.009 1.08-.009c.731 0 1.395-.06 2.094-.087a43.69 43.69 0 0 0 4.878-.703c.167.171.333.338.504.509z" id="jkpath18" fill="#e6ccad"/><path class="jkst3" d="M128.464 87.071a98.82 98.82 0 0 1-1.048 1.343c-1.933 2.444-4.614 5.57-7.794 8.627a369.585 369.585 0 0 0-11.404-.177c-6.46 0-12.655.171-18.537.457 8.311-3.449 18.296-6.818 29.109-8.842a113.323 113.323 0 0 1 9.674-1.408z" id="jkpath20" fill="#656c67"/><path class="jkst3" d="M79.821 90.792c-2.966 2.084-6.317 4.744-9.566 7.971a360.155 360.155 0 0 0-21.567 2.81c9.207-4.232 19.713-8.127 31.133-10.781z" id="jkpath22" fill="#656c67"/><path class="jkst3" d="M181.48 107.969l-3.384 23.679-16.212 11.355-42.283-4.807-6.365-20.961a1.383 1.383 0 0 0-1.108-.971c-1.567-.253-2.953-.382-4.108-.382-1.16 0-2.541.129-4.115.382-.522.086-.95.461-1.106.971l-6.209 20.45-42.047 9.357-16.662-11.672-3.283-26.572c.715-.404 1.441-.806 2.176-1.209 1.031-.222 2.191-.457 3.475-.704l3.094 25.073c.048.392.264.741.586.967l11.462 8.032a1.425 1.425 0 0 0 1.101.213l34.57-7.692c.119-.027.237-.069.344-.124a1.39 1.39 0 0 0 .682-.827l6.225-20.498c1.67-.43 5.947-1.429 9.706-1.429 3.749 0 8.03.999 9.701 1.429l6.225 20.498c.161.532.624.912 1.176.977l34.57 3.927c.335.037.677-.05.952-.242l11.469-8.025c.31-.22.52-.566.573-.946l3.062-21.421c2.301.444 4.224.846 5.733 1.172z" id="jkpath24" fill="#656c67"/><path class="jkst3" d="M185.751 93.119l-2.976 11.29c-6.086-1.342-19.456-3.975-37.654-5.747 5.946-2.535 12-5.715 17.531-9.69 10.829 1.53 18.78 3.169 23.099 4.147z" id="jkpath26" fill="#656c67"/><g id="jkg32"><path class="jkst4" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath28" fill="#e5caa3"/><path class="jkst4" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath30" fill="#e5caa3"/></g><g id="jkg38"><path class="jkst5" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath34" fill="#c7b39a"/><path class="jkst5" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath36" fill="#c7b39a"/></g><path class="jkst2" d="M187.481 115.502c.508.419.911 1.504.456 6.558-.559 6.188-3.16 17.049-4.771 18.8-1.778.344-5.505-.064-7.778-.595.393-1.559.505-2.306.822-3.9l3.975-2.781c.317-.22.526-.566.58-.941l2.778-19.466c1.686.912 3.421 1.899 3.938 2.325z" id="jkpath40" fill="#e6ccad"/><path class="jkst2" d="M40.937 140.908c.199.704.408 1.407.624 2.1-2.139.628-6.495 1.23-8.465.886-1.633-1.645-4.679-12.966-5.345-18.978-.543-4.871-.162-5.924.333-6.334.575-.483 2.728-1.708 4.593-2.707l2.519 20.449c.048.393.257.741.586.967z" id="jkpath42" fill="#e6ccad"/><path class="jkst2" d="M121.347 141.194l-.151 1.305s-4.581 4.248-11.956 5.199c-7.375.95-13.171-3.582-13.171-3.582.242.788.586 2.567 2.256 4.086a53.184 53.184 0 0 0-6.313-.393c-.804 0-1.616.023-2.401.061-4.539.237-10.924 7.1-15.414 14.014-2.203.697-9.089 2.883-17.06 5.237-7.44-10.309-11.098-20.842-11.469-21.932l.005-.006c-.15-.419-.301-.839-.441-1.268l1.913 1.338v.005l4.726 3.309 1.58 1.101c.236.167.515.253.794.253.102 0 .204-.011.305-.031l43.435-9.67a1.385 1.385 0 0 0 1.025-.95l6.194-20.39c1.069-.145 2.008-.22 2.814-.22.801 0 1.746.075 2.815.22l6.374 20.997c.162.532.624.919 1.171.977z" id="jkpath44" fill="#e6ccad"/><path class="jkst2" d="M170.926 140.066l1.402-.984c-.232.973-.484 1.94-.747 2.896-1.949 6.248-4.25 11.774-6.805 16.656-.565.039-1.161.061-1.8.061-1.972 0-3.986-.167-6.215-.371-3.868-.355-10.007-1.058-11.946-1.283-1.67-1.332-7.385-5.873-12.14-9.615-.187-.151-.348-.291-.505-.42-.837-.708-1.789-1.513-3.717-1.513-1.751 0-4.308.638-10.489 2.508 3.212-2.401 3.233-5.5 3.233-5.5l.151-1.305 40.748 4.629a1.41 1.41 0 0 0 .955-.241l4.094-2.868z" id="jkpath46" fill="#e6ccad"/><path class="jkst6" d="M140.937 54.337c.124 3.625.033 10.194-1.655 16.345a1.335 1.335 0 0 0 0 .704 259.298 259.298 0 0 0-6.446-.591c2.412-5.054 2.938-10.436 3.052-12.332 1.852-1.317 3.696-2.896 5.049-4.126z" id="jkpath48" fill="#ebd599"/><path class="jkst6" d="M79.456 58.462c.112 1.896.638 7.267 3.046 12.317-2.149.171-4.297.37-6.441.596a1.328 1.328 0 0 0 0-.694c-1.686-6.139-1.772-12.714-1.654-16.345 1.353 1.231 3.19 2.81 5.049 4.126z" id="jkpath50" fill="#ebd599"/><path class="jkst7" d="M151.835 125.675c-2.89-1.396-6.059.377-11.828.484-4.292.151-7.896.198-8.132-6.543-.237-6.747 2.513-12.326 6.805-12.478 4.292-.15 8.207 5.2 8.735 11.931.145 1.854-.06 3.207-.521 4.21 3.996-.477 4.899 2.235 4.941 2.396zm-13.488-9.878a2.203 2.203 0 0 0 2.154-2.235 2.186 2.186 0 0 0-2.235-2.153 2.194 2.194 0 0 0 .081 4.388z" id="jkpath52" fill="#2d3136"/><circle transform="rotate(-1.049 138.093 113.428)" class="jkst8" cx="138.307" cy="113.602" id="jkellipse54" r="2.194" fill="#edf6fa"/><path class="jkst7" d="M83.484 120.953c.063 6.747-3.509 6.339-7.806 6.381-.435.011-.848.016-1.258.022-.482.011-.944.016-1.39.005-4.168-.005-6.833-.194-9.19 1.079.058-.145 1.09-2.461 4.835-3.4-.414-.914-.673-2.181-.742-3.937-.257-6.741 3.9-12.269 8.197-12.306 4.292-.042 7.289 5.411 7.354 12.156zm-6.634-3.529a2.195 2.195 0 1 0-.122-4.388 2.195 2.195 0 0 0 .122 4.388z" id="jkpath56" fill="#2d3136"/><circle transform="rotate(-1.473 76.78 115.216)" class="jkst8" cx="76.79" cy="115.23" id="jkellipse58" r="2.195" fill="#edf6fa"/><g class="jkst9" id="jkg64" opacity=".8"><path class="jkst6" d="M50.691 75.155s.667-8.692 2.03-12.023c.702-1.717 4.996-2.81 8.276-3.591 3.278-.78 8.508-2.342 9.524 2.264 1.015 4.606 2.653 7.963 3.746 9.446l-1.404-18.97-22.562 5.464-1.484 16.786.703 1.327 1.171-.703" id="jkpath60" fill="#ebd599"/><path class="jkst6" d="M164.855 75.155s-.666-8.692-2.029-12.023c-.703-1.717-4.997-2.81-8.275-3.591-3.28-.78-8.51-2.342-9.526 2.264-1.013 4.606-2.654 7.963-3.748 9.446l1.407-18.97 22.562 5.464 1.483 16.786-.703 1.327-1.171-.703" id="jkpath62" fill="#ebd599"/></g><path class="jkst10" d="M132.965 18.378s-.598 45.49-11.224 45.49h-14.875-12.752c-10.626 0-11.484-45.47-11.484-45.47l-5.22 15.438.085 21.183 3.707 2.947 1.685 9.096 2.357 5.307 45.482.084 2.105-3.791 1.769-6.4.254-4.043 5.023-14.341z" id="jkpath66" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M166.429 60.794s2.187 15.692 7.974 18.522c5.788 2.829 0 0 0 0l-8.103-2.444z" id="jkpath68" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M48.908 60.794s-2.187 15.692-7.975 18.522c-5.788 2.829 0 0 0 0l8.104-2.444z" id="jkpath70" opacity=".75" fill="#ebd599"/><path class="jkst7" d="M167.987 76.8c2.755.902 5.526 1.858 8.036 3.325-1.343-.532-2.729-.913-4.126-1.257a70.385 70.385 0 0 0-4.201-.924c-2.82-.531-5.65-.982-8.498-1.327-2.841-.37-5.687-.682-8.546-.924-2.858-.241-5.709-.483-8.573-.65-11.446-.704-22.924-.88-34.41-.892-11.483.006-22.962.221-34.409.897-2.862.166-5.715.409-8.572.651-2.857.241-5.71.548-8.546.923-2.847.345-5.678.796-8.498 1.327-1.407.264-2.81.57-4.206.919-1.391.344-2.783.725-4.126 1.257 2.509-1.466 5.28-2.427 8.041-3.331.232-.075.467-.139.703-.214-.015-.059-.032-.113-.043-.177-.048-.317-1.069-7.859.709-18.645.086-.516.456-.935.962-1.075l2.917-.831c.634-22.625 9.952-33.266 10.243-33.594-8.326 13.397-8.25 29.286-8.106 32.986l18.128-5.152c.016-.005.026-.005.042-.01.076-.016.151-.027.226-.032.021 0 .049-.006.075-.006a1.19 1.19 0 0 1 .297.027c.015 0 .031.011.053.016.075.016.145.042.224.075.033.016.054.033.086.049.058.033.119.07.177.112.016.011.034.016.049.033l.032.032c.016.016.037.027.054.044.012.016.494.493 1.262 1.209-.182-5.973.102-23.108 8.262-37.31-.172.498-6.646 19.428-4.415 40.645.724.58 1.486 1.149 2.229 1.649.359.247.58.655.585 1.09.006.07.161 6.833 3.148 12.586.042.086.074.177.102.268 7.429-.505 14.878-.709 22.312-.714 7.436.005 14.88.22 22.307.731.027-.097.06-.193.109-.285 2.986-5.753 3.142-12.516 3.142-12.586.01-.436.231-.843.591-1.09.741-.5 1.493-1.069 2.224-1.649 2.234-21.217-4.24-40.147-4.411-40.645 8.153 14.201 8.444 31.336 8.262 37.31a62.536 62.536 0 0 0 1.261-1.209c.016-.016.039-.027.053-.044.012-.01.018-.021.033-.032.016-.016.033-.022.049-.033.06-.042.119-.079.177-.118.028-.01.054-.027.081-.043.081-.033.155-.059.236-.08.016 0 .033-.011.049-.011.096-.021.2-.032.296-.027.027 0 .049.006.07.006.075.005.156.016.231.032.012.006.028.006.042.01l18.129 5.152c.146-3.7.221-19.59-8.104-32.986.289.328 9.609 10.969 10.237 33.594l2.922.831c.499.14.875.559.962 1.075 1.777 10.786.752 18.328.708 18.645-.01.065-.026.124-.042.182.239.07.47.139.707.215zm-3.297-.968c.14-1.207.789-7.809-.591-16.801l-20.52-5.833c.184 3.475.265 11.012-1.707 18.199a1.619 1.619 0 0 1-.101.258c.203.021.408.037.606.064 5.769.661 11.511 1.584 17.189 2.83 1.712.398 3.426.823 5.124 1.283zm-25.409-5.151c1.688-6.15 1.779-12.72 1.655-16.345-1.353 1.23-3.197 2.809-5.049 4.125-.114 1.896-.64 7.278-3.052 12.332 2.149.173 4.298.366 6.446.591a1.33 1.33 0 0 1 0-.703zm-56.78.098c-2.408-5.05-2.934-10.422-3.046-12.317-1.858-1.316-3.696-2.895-5.049-4.125-.119 3.631-.032 10.206 1.654 16.345.065.237.058.473 0 .694 2.145-.227 4.292-.425 6.441-.597zm-8.933.864a1.65 1.65 0 0 1-.098-.247c-1.975-7.187-1.889-14.723-1.712-18.199L51.244 59.03c-1.38 8.982-.736 15.583-.597 16.797 1.703-.462 3.411-.887 5.131-1.284 2.835-.628 5.693-1.154 8.556-1.638 2.869-.478 5.747-.843 8.626-1.192.205-.027.404-.042.608-.07z" id="jkpath72" fill="#2d3136"/><g id="jkXMLID_1_"><g id="jkg78"><path class="jkst7" d="M129.293 18.973v17.025h-12.068v-4.974h-2.72v22.981h4.109v12.85H97.505v-12.85h4.092v-22.98h-2.711v4.974h-12.06V18.973zm-3.626 13.408v-9.789H90.443v9.789h4.816v-4.974h9.964v30.225h-4.1v5.606h13.865v-5.606h-4.1V27.407h9.964v4.974z" id="jkpath74" fill="#2d3136"/><path class="jkst0" id="jkpolygon76" fill="#cb3349" d="M101.123 57.632h4.1V27.407h-9.964v4.974h-4.816v-9.79h35.224v9.79h-4.816v-4.974h-9.964v30.225h4.1v5.606h-13.864z"/></g></g><path class="jkst3" d="M30.694 93.119c1.759-.399 4.136-.907 7.051-1.47a104.37 104.37 0 0 0-6.222 4.597z" id="jkpath83" fill="#656c67"/><path class="jkst5" d="M95.111 139.78s.492 3.165-3.938 4.519c-4.428 1.355-32.482 9.716-35.682 9.263-3.199-.451-11.319-5.874-11.319-5.874l-1.969-7.004 12.016 7.492z" id="jkpath85" fill="#c7b39a"/><path class="jkst5" d="M120.242 139.167s-.354 3.182 4.131 4.345c4.484 1.161 32.875 8.295 36.05 7.704 3.176-.591 11.053-6.361 11.053-6.361l1.663-7.084-11.045 6.588z" id="jkpath87" fill="#c7b39a"/><path class="jkst5" d="M28.412 133.956s3.887 7.775 10.166 5.083l4.485 1.645-.448 3.29-9.419 1.195-2.541-1.494z" id="jkpath89" fill="#c7b39a"/><path class="jkst5" d="M187.551 131.822s-6.353 8.115-12.632 5.424l-2.019 1.302.448 3.289 9.419 1.196 2.54-1.495z" id="jkpath91" fill="#c7b39a"/><path class="jkst5" d="M89.279 192.904s23.03 11.611 49.106-4.188l-8.374-.571s-18.272 7.232-32.738 3.235z" id="jkpath93" fill="#c7b39a"/><path class="jkst7" d="M112.626 171.509l1.594 1.899c.036.046 3.577 4.26 7.906 8.552 2.879 2.853 6.357 4.297 10.343 4.297 1.361 0 2.791-.175 4.235-.523 1.34-.326 2.796-.673 4.287-1.03 5.384-1.287 11.482-2.749 14.438-3.577.585-.166 1.238-.315 1.925-.472 3.935-.909 9.329-2.163 12.187-7.889 2.149-4.297 5.047-9.874 7.197-13.961-1.863.859-3.816 1.79-5.203 2.52-2.138 1.123-4.938 1.667-8.558 1.667-2.152 0-4.266-.181-6.605-.389-4.675-.43-12.586-1.361-12.667-1.372l-.606-.067-.478-.383c-.071-.052-7.003-5.575-12.606-9.981-.227-.186-.434-.358-.621-.513-.59-.503-.59-.503-.942-.503-1.797 0-7.02 1.62-18.462 5.167l-.703.223-.689-.26c-.078-.026-7.585-2.81-16.581-2.81-.736 0-1.47.019-2.185.056-.901.046-5.958 2.448-12.425 12.68l-.419.657-.741.238c-.107.037-11.238 3.63-23.042 7.005l-.766.218-.725-.337c-.077-.031-4.696-2.174-9.091-4.194 2.397 3.541 5.462 7.958 8.159 11.422 4.711 6.067 10.649 11.674 22.034 11.674 1.428 0 2.945-.088 4.503-.265 11.581-1.309 14.563-1.837 16.168-2.117.543-.092.973-.171 1.522-.238.088-.011 9.571-1.237 12.232-7.206 2.744-6.134 3.298-7.595 3.319-7.651l.968-2.583s.12-.669.317-.877c0 .005 0 .005.005.005l.019.016c.305.219.757.902.757.902zM40.499 55.71c-2.516 1.014-5.016 2.06-7.46 3.209-2.449 1.119-4.856 2.32-7.155 3.66-2.121 1.222-4.157 2.563-5.954 4.076-.077.455-.149.952-.211 1.423a51.357 51.357 0 0 0-.388 6.068c-.026 2.713.16 5.426.502 8.112.372 2.692.864 5.369 1.594 7.952a41.963 41.963 0 0 0 1.243 3.804c.233.623.492 1.228.762 1.818.134.294.274.585.413.864l.172.326c.201.104.409.207.605.3l1.206.574c.673.311 1.6.751 2.366 1.093.046-.037.088-.078.124-.114l-2.231-8.511c.471-.129 4.717-1.227 12.032-2.619a33.744 33.744 0 0 1-1.775-.379 36.704 36.704 0 0 1-4.898-1.563 22.857 22.857 0 0 1-2.309-1.119c-.741-.425-1.471-.905-2.035-1.547 8.035 2.624 24.637 1.433 39.398-.088 13.501-1.393 27.028-2.293 40.628-2.325 13.6.031 27.138.931 40.63 2.325 14.77 1.522 31.374 2.713 39.406.088-.564.642-1.293 1.122-2.034 1.547-.739.42-1.522.782-2.309 1.119a36.965 36.965 0 0 1-4.903 1.563c-.244.056-.492.114-.741.166 8.02 1.486 12.689 2.697 13.186 2.832l-2.138 8.107c.43-.192.864-.377 1.288-.574l1.207-.574c.196-.094.404-.196.606-.3l.166-.326c.144-.279.284-.57.419-.864.27-.591.528-1.196.767-1.818.471-1.231.879-2.51 1.236-3.804.731-2.583 1.228-5.26 1.595-7.952.346-2.686.528-5.4.502-8.112a52.755 52.755 0 0 0-.176-4.059 51.573 51.573 0 0 0-.213-2.009 29.83 29.83 0 0 0-.213-1.423c-1.797-1.513-3.831-2.853-5.954-4.076-2.299-1.34-4.704-2.541-7.159-3.66-2.438-1.149-4.943-2.195-7.46-3.209a140.105 140.105 0 0 0-3.801-1.476c-1.267-.491-2.552-.956-3.835-1.423 2.696.445 5.369 1.06 8.013 1.739 1.724.446 3.444.948 5.141 1.481-12.11-31.658-41.07-52.272-72.685-52.272-31.622 0-60.576 20.614-72.684 52.272a107.832 107.832 0 0 1 5.135-1.481c2.651-.678 5.322-1.294 8.02-1.739-1.29.466-2.568.931-3.842 1.423-1.268.47-2.535.967-3.799 1.475zm159.43 18.316a53.972 53.972 0 0 1-.258 8.733 55.462 55.462 0 0 1-1.619 8.605c-.4 1.414-.86 2.811-1.404 4.198a38.295 38.295 0 0 1-.89 2.071c-.161.341-.331.678-.523 1.025l-.284.512a8.975 8.975 0 0 1-.348.574l-.294.457-.461.237c-.492.254-.895.445-1.342.653l-1.298.585a88.22 88.22 0 0 1-2.62 1.065c-.611.239-1.15.457-1.662.674l-1.444 5.487c-.036-.009-.471-.12-1.283-.315l-.078.574c1.594.833 4.726 2.522 5.793 3.403 2.148 1.775 2.299 4.587 1.823 9.841-.244 2.697-1.139 7.946-2.381 12.767-2.144 8.298-3.283 9.273-4.753 9.649-.746.192-1.894.383-3.008.383-2.266 0-5.353.063-7.429-.439-.533 1.888-2.055 6.812-5.068 12.962.151-.073.3-.135.435-.207 3.717-1.952 10.861-5.064 11.162-5.199l5.643-2.452-2.89 5.435c-.067.118-6.264 11.773-10.059 19.383-3.769 7.538-10.835 9.179-15.065 10.151-.637.151-1.241.291-1.733.425-3.035.854-9.18 2.319-14.599 3.623-.064.016-.13.033-.197.042a64.057 64.057 0 0 1-10.955 5.411c-14.568 5.518-29.923 5.208-43.844.092a647.05 647.05 0 0 1-9.193 1.097 45.12 45.12 0 0 1-4.985.291c-13.264 0-20.294-6.736-25.425-13.331-5.493-7.062-12.212-17.546-12.497-17.985L31 158.426l6.585 2.961c3.152 1.419 12.524 5.757 15.205 7 .217-.061.43-.124.642-.186-4.457-6.357-8.112-13.605-10.695-21.634-2.195.662-5.576 1.175-8.206 1.175-.961 0-1.822-.072-2.484-.228-1.471-.336-3.148-1.754-5.431-9.795-1.325-4.668-2.314-9.764-2.603-12.387-.57-5.121-.466-7.864 1.662-9.636 1.283-1.071 5.611-3.344 6.507-3.809l-.192-1.58c-13.75 8.08-21.991 15.22-22.157 15.366L0 134.302l7.005-11.047c5.544-8.755 11.948-15.832 17.84-21.284-.244-.098-.471-.196-.71-.294l-1.299-.585a34.907 34.907 0 0 1-1.34-.653l-.461-.237-.295-.457c-.166-.249-.238-.388-.347-.574l-.29-.512c-.181-.347-.358-.684-.518-1.025a30.878 30.878 0 0 1-.89-2.071 44.74 44.74 0 0 1-1.404-4.198 54.745 54.745 0 0 1-1.62-8.605 54.664 54.664 0 0 1-.259-8.733c.078-1.455.218-2.909.419-4.354.104-.725.213-1.45.358-2.17.15-.734.296-1.418.518-2.221l.155-.564.404-.317c2.294-1.802 4.768-3.163 7.284-4.369a78.87 78.87 0 0 1 6.311-2.616c5.943-16.493 16.162-31.118 29.591-41.311C74.337 5.57 90.664 0 107.671 0s33.334 5.57 47.218 16.106c13.43 10.193 23.649 24.819 29.588 41.307a78.282 78.282 0 0 1 6.316 2.62c2.515 1.206 4.99 2.567 7.283 4.369l.404.317.156.564c.227.803.372 1.487.517 2.221.146.72.26 1.445.357 2.17.203 1.443.348 2.897.419 4.352zm-11.995 48.031c.456-5.052.058-6.139-.455-6.554-.513-.43-2.247-1.412-3.935-2.329l-2.779 19.464a1.39 1.39 0 0 1-.58.942l-3.977 2.781c-.315 1.593-.429 2.345-.817 3.903 2.273.528 5.999.938 7.775.595 1.612-1.748 4.214-12.61 4.768-18.802zm-5.161-17.648l2.977-11.29c-4.318-.978-12.27-2.615-23.1-4.148-5.53 3.976-11.582 7.155-17.53 9.691 18.199 1.771 31.57 4.406 37.653 5.747zm-4.68 27.237l3.385-23.676a240.127 240.127 0 0 0-5.731-1.169l-3.059 21.422a1.415 1.415 0 0 1-.575.943l-11.472 8.023c-.27.192-.616.28-.947.243l-34.572-3.929a1.391 1.391 0 0 1-1.176-.973l-6.227-20.5c-1.668-.431-5.949-1.43-9.696-1.43-3.764 0-8.041.999-9.708 1.43l-6.228 20.5a1.388 1.388 0 0 1-1.025.947l-34.572 7.692a1.483 1.483 0 0 1-.306.033 1.36 1.36 0 0 1-.792-.25l-11.467-8.029a1.396 1.396 0 0 1-.585-.968l-3.091-25.072c-1.284.249-2.443.487-3.479.703-.734.405-1.46.809-2.174 1.213l3.281 26.568 16.666 11.675 42.047-9.354 6.207-20.449a1.389 1.389 0 0 1 1.108-.975c1.574-.253 2.95-.382 4.116-.382 1.153 0 2.536.129 4.105.382.528.083.957.461 1.108.975l6.366 20.956 42.282 4.808zm-8.07-4.411l2.992-20.948c-8.439-1.536-20.78-3.394-35.897-4.554-13.647 4.707-25.077 6.108-25.766 6.155l-.797.057c4.353.374 8.454 1.544 8.66 1.605.452.135.804.481.944.933l6.186 20.366 33.138 3.764zm2.303 11.845l-1.404.983-3.779 2.651-4.095 2.868c-.279.192-.621.28-.954.243l-40.746-4.633-2.966-.337a1.39 1.39 0 0 1-1.171-.977l-6.377-20.998c-1.066-.145-2.014-.219-2.81-.219-.809 0-1.751.073-2.817.219l-6.192 20.392a1.383 1.383 0 0 1-1.025.946l-43.435 9.672c-.103.02-.206.03-.305.03-.279 0-.559-.083-.798-.253l-1.578-1.098-4.726-3.307v-.011l-1.91-1.335c.135.43.289.85.441 1.268l-.006.006c.368 1.092 4.028 11.622 11.467 21.929a873.96 873.96 0 0 0 17.057-5.234c4.488-6.917 10.877-13.777 15.418-14.014a51.12 51.12 0 0 1 2.402-.061c2.221 0 4.344.16 6.31.393-1.671-1.517-2.013-3.298-2.256-4.085 0 0 5.793 4.53 13.17 3.584 7.378-.953 11.959-5.204 11.959-5.204s-.021 3.102-3.236 5.503c6.182-1.869 8.739-2.511 10.489-2.511 1.931 0 2.883.808 3.717 1.519.161.129.322.268.507.419a3519.302 3519.302 0 0 1 12.141 9.614c1.936.227 8.075.926 11.943 1.283 2.23.201 4.245.372 6.217.372.637 0 1.233-.026 1.797-.063 2.558-4.88 4.857-10.411 6.808-16.653.261-.96.516-1.928.743-2.901zm-15.034-51.593c-.01-.006-.02-.012-.031-.012a551.624 551.624 0 0 0-9.826-.651 905.6 905.6 0 0 0-13.667-.668 72.95 72.95 0 0 1-1.574 2.225c-2.479 3.355-7.398 9.51-13.704 14.729 8.926-1.6 24.409-5.56 37.803-14.905.336-.238.668-.486.999-.718zm-29.876.926c.377-.471.729-.926 1.044-1.34-3.281.331-6.512.808-9.67 1.408-10.814 2.024-20.801 5.389-29.11 8.837a383.259 383.259 0 0 1 18.54-.455c3.908 0 7.708.067 11.404.176 3.179-3.056 5.861-6.182 7.792-8.626zm3.587 102.085c-4.503-.332-8.598-2.205-11.903-5.477a271.86 271.86 0 0 0-.502-.512 44.25 44.25 0 0 1-4.881.704c-.698.026-1.361.087-2.091.087l-1.083.011-.413-.011c-4.396 6.539-14.159 7.813-14.605 7.87-.403.046-.734.103-1.191.186 5.442 1.491 10.996 2.138 16.474 1.77 5.492-.367 12.627-1.558 20.195-4.628zm-17.4-7.461a45.604 45.604 0 0 0 3.184-.378 138.958 138.958 0 0 1-3.568-3.857 398.441 398.441 0 0 1-1.92 4.339h.243c.658.001 1.378-.071 2.061-.104zm-3.354-78.632c1.827-1.103 3.582-2.366 5.249-3.712a422.33 422.33 0 0 0-7.278-.072c-10.137 0-19.606.415-28.189 1.061-8.61 4.209-13.875 7.672-13.998 7.76l-8.268 5.514 5.679-8.149a52.452 52.452 0 0 1 2.956-3.857c-9.536 1.066-17.477 2.329-23.41 3.422l3.038 24.632 10.453 7.321 33.184-7.378 6.212-20.464c.104-.337.331-.621.627-.793.098-.063.202-.109.315-.14.192-.052 3.51-.999 7.336-1.465zm3.816-18.788c-2.31-.036-4.623-.057-6.933-.062h-.005c-3.39.005-6.787.041-10.189.109l-6.269 2.971c-.005.005-.041.021-.088.048-.942.46-9.174 4.613-16.919 12.021 6.943-3.65 17.146-8.418 29.153-12.115a144.186 144.186 0 0 1 11.25-2.972zM70.251 98.761c3.251-3.225 6.605-5.886 9.567-7.967-11.415 2.651-21.923 6.543-31.128 10.778a360.846 360.846 0 0 1 21.561-2.811zm2.159-9.949a150.122 150.122 0 0 1 11.813-2.796c-5.798.212-11.6.481-17.393.808-3.366.186-6.715.414-10.065.667-1.678.129-3.345.263-5.007.445-.476.046-.942.098-1.418.16-4.369 2.614-21.127 13.134-32.631 26.889 11.179-7.769 30.654-19.443 54.701-26.173zm-30.85 54.197a68.861 68.861 0 0 1-.621-2.102l-5.162-3.612a1.391 1.391 0 0 1-.586-.969l-2.516-20.449c-1.864.999-4.017 2.225-4.592 2.707-.497.409-.875 1.46-.336 6.332.668 6.01 3.712 17.333 5.348 18.979 1.968.347 6.327-.258 8.465-.886zm-3.815-51.36a229.005 229.005 0 0 0-7.051 1.47l.829 3.127a103.93 103.93 0 0 1 6.222-4.597z" id="jkpath95" fill="#2d3136"/></g></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tune" xmlns="http://www.w3.org/2000/svg"><path d="M6.85 2.852h-2v6h2v-6m12 0h-2v10h2v-10m-16 10h2v8h2v-8h2v-2h-6v2m12-6h-2v-4h-2v4h-2v2h6v-2m-4 14h2v-10h-2v10m4-6v2h2v4h2v-4h2v-2h-6z" fill="#fbc02d" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 50 50" id="twig" xmlns="http://www.w3.org/2000/svg"><path d="M9.727 47.556c-.125-.223-.297-2.168-.183-2.087.034.025.171.267.304.537.132.27.282.487.332.482.123-.011.075-1.196-.1-2.454-.331-2.398-1.176-4.435-2.358-5.69-.2-.212-.344-.4-.319-.419.093-.067 1.327.843 1.842 1.359.293.293.735.825.981 1.181.328.474.465.618.51.534.078-.147-.21-9.903-.376-12.701-.074-1.255.063-1.023.61 1.035 1.064 4.006 1.858 7.922 2.342 11.55.086.637.173 1.172.195 1.19.022.016.092.001.157-.034.888-.483 1.524-.667 2.55-.736.727-.048.945.062.35.178-1.15.222-1.99 1.013-2.344 2.201-.315 1.061-.327 2.707-.024 3.434.152.366.037.426-1.067.56-.716.088-.977.096-1.202.037-.356-.092-1.118-.098-1.195-.008-.031.036-.243.066-.47.066-.38 0-.423-.017-.535-.215zm1.974-3.233c.152-.205.072-.41-.204-.522-.225-.09-.263-.088-.437.025-.21.137-.252.43-.08.554.18.13.607.096.72-.057zm1.248.086a.763.763 0 0 0 .214-.203c.241-.33-.352-.622-.745-.366-.406.265.08.785.531.569zm2.288 3.094c-.033-.039.117-.387.334-.775.216-.387.411-.665.433-.618.07.152-.201 1.28-.33 1.372-.15.108-.354.117-.437.02zM8.2 47.092c-.29-.343-.221-.434.14-.182.176.123.321.263.321.31 0 .165-.279.087-.46-.128zm8.649-.145c0-.053.102-.18.227-.282.25-.204.312-.113.143.207-.095.18-.37.236-.37.075zm8.065-.827c-.243-.025-.48-.088-.527-.141-.11-.125-.114-3.043-.004-3.043.045 0 .132.149.193.331.127.38.228.42.31.124.094-.337.065-3.472-.039-4.297-.449-3.55-1.865-6.124-4.342-7.89-1.086-.774-2.653-1.436-4.047-1.711-.764-.15-.522-.224.598-.182 2.364.089 4.167.706 5.847 2.001a11.046 11.046 0 0 1 2.32 2.502c.453.682.64.854.64.584 0-.07.063-.882.139-1.805.679-8.26 2.396-15.1 4.984-19.86 1.86-3.422 5.108-6.817 7.885-8.244 1.397-.718 2.539-.988 4.02-.952.933.023 1.01.036 1.77.307a6.822 6.822 0 0 1 1.363.662c.612.407 1.309 1.004 1.235 1.058-.026.018-.343-.165-.705-.407-2.657-1.771-5.062-1.52-7.12.742-1.108 1.22-2.651 3.53-3.634 5.443-2.828 5.503-4.541 11.464-5.291 18.413-.163 1.509-.282 3.76-.195 3.703.032-.022.266-.52.518-1.108 1.597-3.723 3.578-6.428 5.79-7.908.672-.449 1.612-.904 1.715-.83.022.016-.172.22-.432.454-1.957 1.754-3.248 3.76-4.232 6.572-.938 2.68-1.366 5.588-1.368 9.3-.002 1.741.188 4.385.366 5.101.125.505.08.546-.585.546-.55 0-2.306.138-3.416.27-.414.05-.817.04-1.609-.036-.58-.056-1.129-.119-1.218-.14-.165-.037-.18-.014-.2.302-.01.186-.098.203-.728.139zm2.507-6.725c.294-.11.375-.22.375-.517 0-.63-1.309-.706-1.524-.088-.074.211.13.51.42.616.297.108.413.106.73-.011zm2.369-.052c.277-.222.318-.364.174-.611-.4-.691-1.755-.307-1.428.404.121.266.299.35.738.354.227 0 .387-.045.516-.147zm3.011 6.681c-.027-.05.088-.268.256-.484.879-1.135 1.22-1.544 1.284-1.544.04 0 .056.037.036.082l-.423.964c-.212.485-.445.924-.519.977-.169.122-.57.125-.634.005zm2.446-.596c0-.121.853-.683.896-.59.018.04-.056.209-.166.376-.168.259-.238.305-.464.305-.164 0-.266-.035-.266-.091zm-13.04-.124c-.177-.159-.493-.656-.462-.725.018-.038.248.1.512.309.264.207.457.405.428.438-.075.088-.371.074-.478-.022z" fill="#9bb92f" stroke-width=".078"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript" xmlns="http://www.w3.org/2000/svg"><path d="M49 51h408v408H49V51zm246.669 314.879l19.463-1.702c.922 7.8 3.067 14.199 6.435 19.198 3.368 4.998 8.597 9.04 15.688 12.124 7.09 3.085 15.067 4.627 23.93 4.627 7.87 0 14.819-1.17 20.845-3.51 6.027-2.34 10.512-5.548 13.455-9.625 2.942-4.077 4.413-8.526 4.413-13.348 0-4.892-1.418-9.164-4.254-12.816-2.836-3.651-7.516-6.718-14.039-9.2-4.183-1.63-13.436-4.165-27.759-7.604s-24.355-6.683-30.099-9.732c-7.445-3.899-12.993-8.739-16.644-14.517-3.652-5.779-5.478-12.249-5.478-19.41 0-7.871 2.234-15.227 6.701-22.069 4.467-6.842 10.99-12.036 19.569-15.581 8.58-3.546 18.116-5.318 28.61-5.318 11.557 0 21.75 1.861 30.577 5.584 8.828 3.722 15.617 9.199 20.368 16.432 4.75 7.232 7.303 15.421 7.657 24.568l-19.782 1.489c-1.064-9.856-4.662-17.301-10.795-22.335-6.133-5.034-15.191-7.551-27.174-7.551-12.479 0-21.573 2.286-27.281 6.86-5.707 4.573-8.561 10.086-8.561 16.538 0 5.602 2.021 10.21 6.062 13.826 3.971 3.617 14.34 7.321 31.109 11.115 16.769 3.793 28.273 7.108 34.513 9.944 9.076 4.183 15.776 9.483 20.101 15.9 4.325 6.417 6.488 13.809 6.488 22.175 0 8.296-2.375 16.113-7.126 23.452-4.751 7.338-11.575 13.046-20.474 17.123-8.898 4.077-18.913 6.116-30.045 6.116-14.11 0-25.933-2.056-35.47-6.169-9.537-4.112-17.017-10.299-22.441-18.559-5.424-8.26-8.278-17.602-8.562-28.025zm-65.728 50.094V278.454h51.583v-18.399H157.938v18.399h51.37v137.519h20.633z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript-def" xmlns="http://www.w3.org/2000/svg"><path d="M457 459H49V51h408v408zM69 71v368h368V71H69z" fill="#0288d1"/><text x="342.219" y="344.544" font-family="ArialMT" font-size="12" fill="#0288d1" transform="translate(-6058.94 -5838) scale(18.1514)"><tspan style="-inkscape-font-specification:sans-serif" font-family="sans-serif" font-weight="400">TS</tspan></text></symbol><symbol viewBox="0 0 24 24" id="url" xmlns="http://www.w3.org/2000/svg"><path d="M16 6h-3v1.9h3a4.1 4.1 0 0 1 4.1 4.1 4.1 4.1 0 0 1-4.1 4.1h-3V18h3a6 6 0 0 0 6-6c0-3.32-2.69-6-6-6M3.9 12A4.1 4.1 0 0 1 8 7.9h3V6H8a6 6 0 0 0-6 6 6 6 0 0 0 6 6h3v-1.9H8c-2.26 0-4.1-1.84-4.1-4.1M8 13h8v-2H8v2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="verilog" xmlns="http://www.w3.org/2000/svg"><path d="M17.282 17.08H6.718V6.513h10.564m4.226 4.226V8.627h-2.113V6.514c0-1.173-.95-2.113-2.113-2.113H15.17V2.288h-2.113v2.113h-2.112V2.288H8.83v2.113H6.718c-1.173 0-2.113.94-2.113 2.113v2.113H2.492v2.113h2.113v2.113H2.492v2.113h2.113v2.113a2.113 2.113 0 0 0 2.113 2.113H8.83v2.113h2.113v-2.113h2.112v2.113h2.113v-2.113h2.113a2.113 2.113 0 0 0 2.113-2.113v-2.113h2.113v-2.113h-2.113V10.74m-6.339 2.113h-2.112V10.74h2.112m2.113-2.113H8.831v6.34h6.338z" fill="#ff7043" stroke-width="1.056"/></symbol><symbol viewBox="0 0 24 23.999999" id="vfl" xmlns="http://www.w3.org/2000/svg"><defs><style>.jra{fill:#f05223}.jrb{fill:url(#jra)}</style><radialGradient id="jra" cx="205.45" cy="208.29" r="225.35" gradientTransform="matrix(.04556 0 0 .0456 2.888 2.88)" gradientUnits="userSpaceOnUse"><stop stop-color="#ffd104" offset="0"/><stop stop-color="#faa60e" offset=".35"/><stop stop-color="#f05023" offset="1"/></radialGradient></defs><title>houdinibadge</title><g stroke-width=".046"><path class="jra" d="M19.97 3H4.03A1.03 1.031 0 0 0 3 4.031v4.135C4.548 6.977 6.563 6.21 8.948 6.21c5.107.003 8.35 3.574 8.348 8.081 0 3.13-1.46 5.485-3.746 6.71h6.42A1.03 1.031 0 0 0 21 19.968V4.031a1.03 1.031 0 0 0-1.03-1.03z" fill="#f4511e"/><path class="jrb" d="M3 17.722v2.247A1.03 1.031 0 0 0 4.03 21h1.837C4.474 20.21 3.49 19 3 17.722z" fill="url(#jra)"/><path class="jra" d="M8.948 8.231c-2.586-.09-4.598.86-5.948 2.264v3.163c.918-2.654 3.447-3.87 5.565-3.85 2.647.027 4.689 2.025 4.7 4.284.012 2.159-.892 3.748-3.33 4.14-1.33.213-3.411-.567-3.318-2.578.046-1.037.854-1.622 1.777-1.58-.905 1.213.293 2.102 1.139 1.921 1.048-.224 1.475-1.156 1.475-1.878 0-.762-.718-1.994-2.498-1.951-2.204.052-3.591 1.639-3.638 3.602-.056 2.468 2.253 4.091 4.622 4.121 3.48.046 5.543-2.24 5.539-5.586-.005-3.029-2.434-5.946-6.085-6.072z" fill="#f05223"/></g></symbol><symbol viewBox="0 0 24 24" id="virtual" xmlns="http://www.w3.org/2000/svg"><path d="M21 14H3V4h18m0-2H3c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h7l-2 3v1h8v-1l-2-3h7a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 281.25 281.25" id="visualstudio" xmlns="http://www.w3.org/2000/svg"><path d="M196.18 101.74l-52.778 42.444 52.778 40.889V101.74m-136.67 110l-30-18.889v-100L62.843 81.74l47.778 37 96.666-89.222 44.444 27.778v172.22l-55.555 22.222-85.111-81.555-51.555 41.555m3.333-48.889l20.667-19.111-20.667-19.778z" fill="#ab47bc" stroke-width="11.111"/></symbol><symbol viewBox="0 0 300 300" id="vscode" xmlns="http://www.w3.org/2000/svg"><defs><style>.icon-canvas-transparent{fill:#f6f6f6;opacity:0}.icon-white{fill:#fff}</style></defs><title>BrandVisualStudioCode</title><path d="M218.62 29.953l-105.41 96.92L54.301 82.47 29.955 96.64l58.068 53.359-58.068 53.359 24.346 14.212 58.909-44.402 105.41 96.878 51.424-24.976V54.93zm0 63.744v112.6l-74.719-56.302z" fill="#2196f3" stroke-width="17.15"/></symbol><symbol viewBox="0 0 24 24" id="vue" xmlns="http://www.w3.org/2000/svg"><path d="M1.821 4.15l10.21 17.618L22.24 4.235V4.15h-7.692L12.113 8.33 9.691 4.15H1.82z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179H5.937z" fill="#35495e"/></symbol><symbol viewBox="0 0 420 419" id="watchman" xmlns="http://www.w3.org/2000/svg"><g stroke="#fff" stroke-linecap="round" stroke-linejoin="bevel"><path d="M166.95 145.32a93.935 123.23 0 0 1 92.934 3.263" fill="none" stroke-width="18.467"/><path d="M162.92 137.96L44.63 256.25a174.07 173.93 0 0 0 5.705 16.486l123.68-123.68-11.096-11.096zM266.54 144.04l-11.096 11.096 117.16 117.16a174.07 173.93 0 0 0 5.691-16.5l-111.76-111.76zm170.65 170.65v22.193l17.1 17.1 11.096-11.098-28.195-28.195z" fill="#fff" stroke-width="1.963"/><path d="M167.52 273.36a93.935 123.23 0 0 1 92.934-3.263" fill="none" stroke-width="18.467"/><path d="M49.516 144.56a174.07 173.93 0 0 0-.809 2.213 174.07 173.93 0 0 0-4.757 14.344 174.07 173.93 0 0 0-.016.055l119.56 119.56 11.098-11.096-125.07-125.07zM454.87 64.703l-17.668 17.668v22.191l28.764-28.764-11.096-11.096zm-80.984 80.984l-117.86 117.86 11.098 11.096 112.18-112.18a174.07 173.93 0 0 0-5.416-16.777z" fill="#fff" stroke-width="1.963"/></g><image x="21.229" y="20.262" width="378" height="377.1" preserveAspectRatio="none" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaQAAAGjCAYAAABjSWGNAAAgAElEQVR4AeydB3hUVdrH/+fOpNMF JAFUmivFXtZuIBRRQUUTil1RV3et6Lr6rSu6rg3B1dXVtXeBCCioCASIimLDhkFsgAIJSAkhPZmZ 8z3vjReGMJlMueeWmfc88MzMLaf8zp3855zznvcV4MQEkoxA7sVr01PrUrv4pdZNBNA1AHQRkJ0g ZAcJrYOA7Aipv8/Q6JgUKdBkG0iZISDSDVwSaAfAY3z+/bVSAD56L+mfwA5INAqgSkrUCKA2IOQO IcQOIUU5pNwhNZQLia2Q2OyH3OJBxqaiwk4VzfLlj0wg4QmIhG8hNzDJCEgxJH9DjgfevgGBAyCx vxSyh5DoAWA/CHQH0MEFUOoBbJbAeiHEBiED6wMSv0DgF02In1J2Vq+ZP78fXcOJCSQMARakhOnK 5GrICaO3tE3P8B0kERgkgP6Qop8E+gqgL4Bdo5gEphKAwHpIrIHAjxL4DpCrAo1yVfHsHhsSuN3c tAQmwIKUwJ2bGE2TYsjY33qLQOBwCHk4JA6HhgE08kmM9ilpRQUgvwPE1xD4MiC0r2R67dfFz/eq U1IaZ8oETCLAgmQSSM7GHAIj8td38sN7HAROkpDHATgMTWs15hSQvLn4JLBaQHwqEPgQQiwvmpHz XfLi4JY7kQALkhN7JYnqlDd2w4GQnuMk5AkCOBHAQQD4ubTgGZDAFg1ieQCBj6Dhg7SKmhW8LmUB eC6iRQL8xW8RDZ8wn4AUQ8eVHib9YjCELj4nAOhqfjmcY4wE6iGxAkIsCwDvSel5v7iwa1WMefFt TCBqAixIUSPjG6IhkDtmQw/N4xkKTQ4TEkNZgKKhZ/u1DZD4CAKLIMSik/p3WzF5sgjYXiuuQMIS YEFK2K61p2GTJ0tt2Xebj5EyMArAab+vAdlTGS7VVAI0xSeAd6QQb2uBtIW8V8pUvJwZz9XzM2AG AdpoKqpTh3uEdlYA8gwBdDEjX87D0QQaALlMCLzha5Rz2NTc0X3lmsrxCMk1XeWsig6/YFOWr0Ge JoBzICWNhNo6q4ZcGwsJkFOKT4TUZiOgzSqate8aC8vmohKIAAtSAnWm6qaMHPljWl2bzBGapk2A lKMBZKguk/N3JYHPpMCrHuGbuWj6fqWubAFX2hYCLEi2YHdToVIMKSg7SQAXABgDoJObas91tZWA H5DvCeCV2rrUwg/ndqm0tTZcuOMJsCA5vovsqeDwszd19af4LwbEZQAOtKcWXGoCEaiCwAwB7emi Gd0+TqB2cVNMJMCCZCJMt2dFFnIflJQNh8BEgKbkRIrb28T1dySBlULIpzyBwCsLCntud2QNuVK2 EGBBsgW7swqlvUIer+cSQE7UPWI7q3pcm8QlUAtgtibFU4sKu70PCJm4TeWWRUKABSkSSgl5DTkt 3XSqJuVfJDAiRFyfhGw1N8qxBH4AxJP+hrpnit/otcOxteSKKSXAgqQUr/My18216wMXCOB6AH9w Xg25RklOoEoI8bzw+R5ZNKvnj0nOIumaz4KUJF0+YsyWbL/Xdx0gr5BAxyRpNjfTvQQCkPIdDdqD iwqz33NvM7jm0RBgQYqGlguvbfKmrd2MJrPtNBc2gauc5ASElJ8EIO4/eWD2m+xLL7EfBhakBO3f wfmlR2oCtwI4G4CWoM3kZiURASmwGpAPVLTPeXnFk6IxiZqeNE1lQUqwrs4bW3Y0AoF/QIjT2Vdh gnUuN8cgsFYK3LNPoPzFwsKBDcZBfnU/ARYk9/eh3oIh+WXHaELeIZs8bCdIq7gZTCAsgXUQuK9T oPw5FqawnFxzkgXJNV0VuqJD8zcdDCHvlvpG1tDX8FEmkOAE1gohJp/Yv9vLvMbk7p5mQXJp/w0Z u7mPkP7JACbwGpFLO5GrbS4BiRII/H3xzOw3eZOtuWityo0FySrSJpWTO760s8cv7wQEeVVINSlb zoYJJBAB+bGQ4uaiwpxlCdSopGgKC5JLuvm4/PUZmcJ7NSD/DqCDS6rN1WQCdhKY45f+vxYX9vzJ zkpw2ZETYEGKnJVNV1L4h9JxAuI+9jNnUxdwsW4mQFZ4//E31N/NLomc340sSA7uo2H5Gw7zC+3f AjjFwdXkqjEBNxDYKgVuPbl/9rNs+ODc7mJBcmDf5J61toMnNfVfgLgCgNeBVeQqMQF3EpD4XGja NRyTyZndx4LkqH6RYkj+pouEkFMAdHZU1bgyTCBxCEgIPFvvabx52av7lydOs9zfEhYkh/Th0HM2 95Ye//8ADHVIlbgaTCDRCWwWENcWzcyemegNdUv7WJBs7qncXOnVum66XoBMuZFpc3W4eCaQdAQE xFyfz//n4tk9NiRd4x3WYBYkGzuEjBak0J6WwJE2VoOLZgJMANgJIW49qX+3J9jowb7HgQXJBva0 pyhLeO+QkJPYaMGGDuAimUDLBJYJgSuKZuR81/IlfEYVARYkVWRbyHfouLJcBPCUhOzbwiV8mAkw AXsJ1EOKe3Z07HYvh7mwtiNYkCzinXvx2nRvTdq9EriOw0JYBJ2LYQLxEfgCUlywuDB7VXzZ8N2R EmBBipRUHNcNGVt6hJB4CcCAOLLhW5kAE7CeQB1tqF0yI/thdtiqHj4LkkLG+fnSUy7KbpWQ/wBE isKiOGsmwASUEpBLPBouXji9+3qlxSR55ixIih6A3DEbemhe7WV2+6MIMGfLBKwnsA3AxMUzc96w vujkKJEFSUE/D87fOFoT4lkA+yjInrNkAkzAVgLyv/7MhknFz/eqs7UaCVg4C5KJnTpy5I9p9W3b TBGQf2HDBRPBclZMwHkEVnr8KFg4K2e186rm3hqxIJnUd0PGlO4vPHgdAkeZlCVnwwSYgLMJVEHI yxfP6D7d2dV0T+1YkEzoq6H5ZadLIV/gKToTYHIWTMBlBIQQj6bsrLpp/vx+9S6ruuOqy4IUR5fo VnRa2Z1S4jaeoosDJN/KBNxP4DMhcW5RYc6v7m+KfS1gQYqRvR6zKCXtVQiMjDELvo0JMIEEIiCB LR4p8hcVZr+XQM2ytCmapaUlSGF5+WUDvKnpn7EYJUiHcjOYgAkEBNAlIAKLho7deI0J2SVlFjxC irLbh+SXni0EXgTQJspb+XImwASShICAeC6lsuoqXleKrsNZkKLglVdQeiuAf/F6URTQ+FImkLwE lnkatXMWzun2W/IiiK7lLEgR8KL9RQ1tsyia60URXM6XMAEmwAQMAmsDHjFq6WvZJcYBfm2ZAAtS y2z0M7njSzt7/JgD4MRWLuXTTIAJMIFQBCqkkAVLZnRfGOokH9tNgI0adrPY611u/vq+Xr9YzmK0 Fxo+wASYQOQE2gsp3h4ytnRi5Lck55UsSC30+5D8smM8wvMhB9JrARAfZgJMIBoCXiHxVN7YjXcC kmemWiDHYEKAyRu76QzIALkDyQpxmg8xASbABGImQBZ4vt+6XVFcLHwxZ5KgN7IgNevYvPyNl0EI MmDwNDvFH5kAE2AC5hCQmJ9Zh3PnzcupMSfDxMiFp+yC+nFIQdlNEOIpFqMgKPyWCTAB8wkIjKxJ xyLy+GJ+5u7NkUdIet9JkVdQRvuLaJ8RJybABJiAVQS+9kvvqcWFXTdZVaCTy0l6QZo8WWrLVpU9 JoE/ObmjuG5MgAkkJgEB8ZNPw7Di6dnrErOFkbcqqQWJvHVvF2UU2fXCyJHxlUyACTAB0wn86pf+ vOLCnj+ZnrOLMkzaNaTcXOndLja9zGLkoqeVq8oEEpfAfh7hfW/4OaUHJW4TW29ZUo6Q8vNLUreh 42tCYEzriPgKJsAEmIBlBDZDiiGLC7NXWVaigwpKOkEiMSoXnQol5GgH9QNXhQkwASagE6C4SprU 8ooKu61MNiRJNWVH03Q0MmIxSrbHnNvLBNxDgOIqSREoGjq2tL97am1OTZNmhEQGDNtE6asCosAc dJwLE2ACTEAlAVkGIXMXz+jxg8pSnJR3UoyQfreme57FyEmPHteFCTCB8ARENqS2mJw8h78ucc4m gSBJUS7KHgVwfuJ0G7eECTCBJCHQwyM8i3LHbOiRDO1NeEEaMrbsHt70mgyPMreRCSQsgQM8Hu3d kfllXRK2hb83LKEFaUjBxluExN8SvRO5fUyACSQ4AYGBjQjMG5q/vX0itzRhBSlvbOmVAuLeRO48 bhsTYALJQ0AK8UcpamfT1pVEbXVCCtLg/I2jIfEYgKSxIkzUB5TbxQSYQDABMWS76Phsogb5SzhB Gjxu0x81IV7jEBLBDzG/ZwJMIIEInJc3tuyhBGrPrqYklCCReaQIBOYByNzVQn7DBJgAE0g0AhLX Dc0vTbj18YSZ0iILlEaBjyRk0tjsJ9p3jNvDBJhAVAQkpBy/uLD7jKjucvDFCTFCokW+Bsg3WIwc /KRx1ZgAEzCbgIAQz9IyhdkZ25VfAgiSFPoin8DxdkHkcpkAE2ACNhHI1AKBuYmycdb1gjS0oOz/ AJxn08PAxTIBJsAE7CbQ1ePV3kmEPUquFqS8/LJzJXCX3U8Dl88EmAATsJnAwVLUv+R2c3CPzRBj Ln5o/qaDIeRbABJ2k1jMcPhGJsAEkpHAH3oPrNTWlkxb6tbGu9LKLu/sDfsgRfsEQB+3gud6MwEm wAQUEJBC4NyiGTmzFeStPEvXTdlRKAmkaLTxlcVI+ePBBTABJuAyAkJKvKDPILms4lRd1wlSudj0 LwDDXMiaq8wEmAATsIJAG6kFZrnRyMFVgkQ+6iTkX63oUS6DCTABJuBaAhL9Aqhznc871wjS0HM2 99aEeIEdprr2K8IVZwJMwEICQmBMXsGmGy0sMu6iXGHUkHvx2nRPddpHEDg87hZzBkyACTCBpCEg G4UUQ4oKc5a5ocmuGCF5a9IeYjFyw+PEdWQCTMBZBESKFGKGbpnsrIqFrI3jBWno2NIxHII8ZN/x QSbABJhABARkDlI8z7lh06yjBWnImNL9IfF0BMT5EibABJgAE2iRgByVl192bYunHXLCsWtItN9o O8reBztNdcijwtVgAkzA5QTqhSaPK5re/UuntsOxI6RyUXo7i5FTHxuuFxNgAi4kkBaQ4tVRo0od G8DUkYI0JL/sGAlBXrw5MQEmwASYgEkEhMRBtZnifpOyMz0bx03ZDb9gU5a/PvAFgANNby1nyASY ABNgAlIKeeqSGd0XOg2F40ZI/nr/AyxGTntMuD5MgAkkEAEhpHh2RP76Tk5rk6MEacjYjcMBcZXT IHF9mAATYAIJRqC7X3gedVqbHDNld8LoLW3T0xu/BbCf0yBxfZgAE2ACiUhASoxZUpgzxyltc8wI KT29cQqLkVMeC64HE2ACyUBACPnYiRN+6eiUtjpCkIbll50C4AqnQOF6MAEmwASSg4DITvN7pzml rbZP2ZFNfE0GvuGAe055JLgeTIAJJB0BiZGLC3Petbvdto+QajPEP1iM7H4MuHwmwASSmoDA407Y MGurIOWNX3+IRMBV8TqS+qHlxjMBJpCoBA6oyRB32N042wRp8mSpwe99AhApdkPg8pkAE2ACTEDe OHjshkPt5GCbIC0r2XQlII+zs/FcNhNgAkyACewi4NWkeEIfLOw6ZO0bWwRpZH5ZFynkPdY2lUtj AkyACTCB8ATEse9/V3Zp+GvUnbVFkBqaxKiDumZxzkyACTABJhATAYl7cs9aa8vfZ8sFadi40qMA 2KbAMXUQ38QEmAATSBICAuiipabfZUdzLRYkKQIB+R8AFpdrB1oukwkwASbgTgIC8uph+RsOs7r2 lgrDkPxNFwHiWKsbyeUxASbABJhAVAQ8AWgPRXWHCRdb5qnhd48MPwDobkK9OQsmwASYABNQTEAI eVbRjO5vKi5mV/aWjZBq0sVNLEa7uPMbJsAEmIDjCUgpHjjyCmnZXlFLBGnEmC3ZEPJmx9PnCjIB JsAEmEAwgQM7lJddHXxA5XtLBMmX0vhPAG1UNoTzZgJMgAkwAQUEBG63KkSFckEafk7pQZC4SAEm zpIJMAEmwATUE9gn1Z/yV/XFWGB+7feARkdeKxrDZTABJsAEmID5BITENXnjN+9rfs575qh0hPT7 Jthz9iySPzEBJsAEmIDLCGSJQODvquusVJACAZC/OstMy1XD4vyZABNgAslKQEp5xfD8Tb1Utl+Z IA0pKD0ZwDCVlee8mQATYAJMwDICqX7NTwFVlSVlgiQgbQ/2pIwaZ8wEmAATSEYCUpw/ZOzmPqqa rkSQ8saVngSIIaoqzfkyASbABJiALQS8Aj5la0lKBAkBOdkWVFwoE2ACTIAJqCUgxflDz9ncW0Uh pgvS0PzSE3l0pKKrOE8mwASYgCMIeOFRY3FnuiBB4FZHIONKMAEmwASYgBICEvK84eM29jQ7c1MF KW/8+kMkMNLsSnJ+TIAJMAEm4CgCqb6AuMHsGpkqSPB7yL0E7zsyu5c4PybABJiAwwgI4PIR+es7 mVkt0wQpd1zZAQDGmlk5zosJMAEmwAQcS6CNT3j/bGbtTBMkLYAb2WedmV3DeTEBJsAEnE5AXntc /voMs2ppitPT3LPWdhCQl5hVKc6HCdhNwOsVaN9WQ1amQFaGQEaGhox0gdQUAaEBaali19x0Q6PU j1OdG30S/gDQ0CDh9wPVtQFUVUlUVgdQXRNATa20u2lcPhMwk0DnLOE5H8BTZmRqiiBpqekTAcnx jszoEc7DUgIkLL3396JvrxT03T8FPXK8yNnXgy77eJTUo7pG4tdSH9Zv9OGXDT78vK4Rq39uRFV1 QEl5nCkTUE1AAtcD8mlAxP1rK24DhNxc6fV0LfsZwH6qG875M4F4CXTqoOHQAWk4dGAqBv0hFQf0 NOU3WbzV0gXqu58asXJ1A75YWY/NW/xx58kZMAGrCAjg1KKZOQviLS/ub6O3a9mZksUo3n7g+xUR SEkROKR/Kv54eJr+v3t23I+8kpr27O4F/R9+StN0/MZNPnzxTQM++7oeK76pR31D3D8+ldSbM2UC REBKXAUgbkGKe4SUl1+6FAK53C1MwCkEaK3n2CPSMfiEdBx/VLpTqhVXPZZ9WocPP6vDxyvq9fWo uDLjm5mA+QQCwu/pVzRr3zXxZB2XIOXllw2AkCXxVIDvZQJmEKCR0PFHpWHw8Rk48ZjEEKGWuJAw LXq/Fp98WY/GRh45tcSJj1tLQEDcXzQz+2/xlBqfIBVsfBQQptqhx9MYvjf5CPQ5IAUjB2fgrFOz kq/xAN4qqsE7i2vww5rGpGw/N9pRBLZ2kuXdCwsHNsRaq5gFadSo0syaDGwE0CHWwvk+JhALAbKM yzsxA6OGZ6Jfr5RYski4e0iQ5i2qwdIPa1FXz6OmhOtglzRISHleUWH3V2OtbsyCNLRg46US4plY C+b7mEC0BPbt4sGoYZkYdybvMAjHbtbb1Zi7qAYby3zhLuNzTMB8AhIfLC7MoWjhMaWYBElKeeCM udULXp1TeQDtq+DEBFQSGHRQKkYPy8SQE03bEK6yuo7J+73ldZi7sBpfr4p5BsUxbeGKuIOApkE+ PLnTjQMOSv93LDWOWpBIjAB8T4XNW1iDp1/bCRalWNDzPa0ROO7IdH1a7pjD0lq7lM+HIfDNqgZ9 xFT8UW2Yq/gUE4iPAE2fP35fZyOTvwsh/mV8iPQ1FkG6F8AuSwoWpUhR83WREsg9PkMfER0yIDXS W/i6CAjQxtt5C6t1Cz3JExsREONLIiXQe78UPDlllxgZt3mFEFHt8I5KkKSU5E9lr4lpFiWDP7/G Q+CUY2lElIXDBrIQxcMxknsfeqoCbxfVRHIpX8MEwhJoQYzontOEEPPD3tzsZLSCdCqAkAWQKP3v 5Z1s4dMMMH9snQBNyZHFHE3RcbKOwKofGnTLPNrTxIkJxEIgjBhRdjOFEFGFJIpWkF4EcEFLFSdR eviZipZO83EmsAeBA3unYPTwTJw6OHOP4/zBWgLkmohMxskbBCcmECkB8gP59INdWru8vRBiZ2sX GecjFiQpJf3VqDZubOmVRaklMnzcINCxvaabbp9zuns3s1IYCWMdJjNDoLau6XNKCnaFojDa65ZX GimRVd53P/ImW7f0mV31pC0Y40a30Wc2WqnDJUKI51u5ZtfpaASJhl7Td90Z5g2LUhg4SXxKCGD8 WW1w6bi2jqZAISHWl/qwcZMf5OR0yzY/tu8IoKIygIqdgV1CFK4RXg/QsYMHnTtq6NTRg25dPdi/ hxf75XhBZuxOTrSPqfCtamzdHtV6tJObxHUzkUAUYkSlLhJCDI+0+GgE6U0AoyPNmEUpUlLJcR15 Vrj1Guc59aA/urRP59vVDbr7nTW/+pT7hyNh7pHtxYADU3BQ31TdiKNnjvO8kD8/sxKvzK6KSICT 4ynmVkYpRgawbCHEJuNDuNeIBElK2RHA9nAZhTrHohSKSnIdoz+8Z52a6Shfcx9/UY9PvqjDFysb 9BGQE3qka2cPjjg4DUcflgayNnRSuu/RHSj6gA0fnNQndtQlRjGiql4jhHg0kjpHKkgUnvzZSDJs fg2LUnMiyfP5vDFtcMlYZ0zP0R/UpR/V4qtvGxwfW4g8lx91SBpOOCYNp+Y6w+CDnLjSd/nnX3h9 KXm+wbtb2r6thosL2kayZrT7pt3vioUQg3d/bPldpIL0NtmUt5xN+DMsSuH5JNpZp3hYIKuxhe/V 6kHu3BqmgcSJggtS4D4nxHZ66fUqvPh6JU/jJdqXNkx7sjIFJo5vF6sYGTnvK4T4zfjQ0murgiSl bA9gR0sZRHqcRSlSUu69zuMBLhvXDgWj7bOeW7fBh/lLavQpJjJASKTUrq2GYSdl4KqL2tnerH8+ VI73PmYzcds7QnEFTBIjquWVQognW6tuJII0HkDM7sSDK8CiFEwjsd4POzkDt/zZPqMFGg3NXViD L1bWJxbYEK0ho4hDB6TqXi3sXG+a/U41XplTpVsehqgmH3I5ARPFiEgsFEKMaA1JJII0E0B+axlF ep5FKVJS7riOwoVfPLYtzjnNnlHRzHnVmLeoGmWbk9NEOWdfjy5M+WfYw5+e0keeqdB/DLjjieVa RkKA9tZdPiHuabrmRXUQQoT1nNCqIAUCcqYQ5gkS1ZBFqXk/ufOznaOiFwsrMXt+DaqqE2taLtYn gf6AnD0yyzYjkjcX1GD2/GqOwRRrBzrsvusuax/vmlGoFh0phPgi1AnjWKuCNOutquvHnJ71kHGD Wa8sSmaRtD4fWiuiX0/n2vCr/IXCSsycW+14Sznre6WpRK9XYMxpmbjiPHvWmZ54aSdef6tVhy52 4eFyIyCgSIyoZE0IEdbPPHnvDpu2+f5y7uqfGo/PO8nc4Gh/6JOCju09+OTLxJ/zDwvYZSfJ0uvZ aV0w4EBrvQ2QEP3ffeX63iF/cs7ORfSkBAJAyfeN+tpOba3EkYdYG0vqqEPT0KG9B6Wb/dhZyaPX iDrNQRepEqM/3bK14sWnMqesWnVnfILUa9Ckh0s3+Tuv/qkRLEoOenJsqArtKbpuIhldWpdmzK3C P6aU47Ov6sFCFDl38rNX8kMjXn2jGo0+icMHWSdM9GPzrFOzUF0r2S9e5F1m+5WqxOiav28lLyjp dahatGbV1F/DNTTsCGnI2M19hJR3UQbk14tFKRzKxD3X94AUfVOclc5Q58yvxgOPV2Dph3U8PRfH o0UjppXfNWDGvGpoAjjYQj96Rx+ahvbtPPh5nU93PhtHM/hWxQRUidH1d2zDqh9+30wtsGltydQl 4ZoSVpB6DbxxrADOMDIgUfplgw+nHMfTdwaTRH+lX7p33dwR/XqnWNLU5Svq8PgLO/HGuzU85WMi cRpdfvltAxYva9o71L+fNVOuB/VNAVkA/rYtgJ/WsZcHE7vUtKyuubQ9Ro8w3yMIiRH5iAxKaWtL pj4d9Hmvt2GNGobkl84SAmOa30V7H26/gdzbmZvY0MFcnvHmRhswrTTnfvQ5EiJnLoiTFdthA9NA 01H7dfcip5sHbTI1tMnSdMwNjVIfBZBn8A2lPtAG3a9K6rH2170CLMfbLabcT/uYRg/PwinHWec3 j4wdyOiBk3MIkBidaY0YUaP9XunvuqCwZ4t+UVsUpPx86dkuyrYCCLnbUZUokfnof54Na6runN5M 0JrQegMFzjvpj9b8saJ1ohdmVoH+qDsppaUKDD4hA8NPzsAhA2IbUWwvD+CDT+vwzmJn+oEbNSzT 0nXBjz6v04MB0pogJ3sJqBKjG+7YhpV7jox2N1SK/MWF2a/vPrDnuxYFKS9/w3EQ2kd7Xr7nJxal PXkkwieaXrnyAmtMht9bTt4VqvXwD05iRzvUzz29DS44t42p1fr863p9Ayn9UXZS0jToa4QTzja3 veHa+NyMptAW4a7hc+oIqBKj2+7djk/D/dgQeHLxjJwrW2pZi2tIvQbedJEQGNLSjXSc1pNUrCnR vDMthn7KJuHh8Jt+jqboLjjXGu/cT76yU18r2rzFOTbcNCKiP8r33baP7prHbMA53bz6iOsPfVJR XRvAxjJntJ0s8mh9icJy0B6zvr3UrxfSKJy+4zSlSdF3OVlH4M8Xt1MSDqZVMWpqYoe1JVMfaam1 LY6QhuaXLpACEUX645FSS3jdcZz2FNHUDXleUJ30Hf3vVDsmDhG1lzaTjh1tvZcDGimR/z0aOTkp 0ZoC/YK2Kt37nx1YvIzjLVnBm8SIPHqYnSIUI71YKf09lhT23BiqDiFHSLm50ivaVP0XQEQT5zxS CoXWHcdGDc/EnZM6os/+6n8VP/x0BV6aVYXKKmdsmKTRwLgz22DaHftYuk/HeDIoSuzQkzJ0I4kd lQE4ZbT4/c+NmPVONVJThCUboGmt0uMR+KpkD4ssAxO/mkTACWJETdEgVqxZNW1lqGaFFKRex151 tJDy6lA3tHSMRaklMs49ftn4tpg4Qf16EVnO0eZWChXuhETesmmt7N93ddajtNpdpwN6pmBEbia6 dfVie7kfW7fbL9iNjdBHbr+W+nGyBRFsDx63vWsAACAASURBVOmfqntuIevE6hqewjP7mVQlRpOn lmP5iihH+EJsXVsy9a1QbQwpSH37T5oAgWGhbgh3jEUpHB3nnOvYXsOV57eDFRtdyWKSgrrV1tn/ R4aEiPZVPfqvziAXN05LfQ5IwWl5mejU0YMt2wIor7BfmNat9+mjWlpfG/SHiCZMYsZKJvW0zWB9 qR9ULidzCKgSI4qJ9cEnMRnoZK0tmfpYqNaFXEPKKyibC8hRoW6I5BivKUVCyZ5rjjksDffc2kl5 4bQ2MnNuFTY5wGiBhIj+0N9wuXXrImYApvW2eQur9T1NZuQXbx4nHJ2OO28yf/9hqHqRN/cXX68K dYqPRUHgTxeocYIcb4DGVCm6zi/M3tK8KSFHSL0GTnpYADGvfPFIqTlmZ3ymxcxbrwm5rczUCj79 WiWeea0SVQ6YeqHQ3/+7vwsorLrbElmbjh6RpW++JWG321np+tKm0VJ6qoaBikdLhw5MA0XI/ea7 BvicYYzotscHThUjAtkoRPG6kqk/Noe6lyANz9/USwp5W/MLo/3MohQtMbXXTxzfFpeOU2vSvWRZ LR5+Zifo1e405MQMkH8uFRZFVreN3PzQVGN6uoYNZfavsaxYWY9fNvpwyrFqrTIP6puqm+GvXN2I Tb+xKkXz3CkTo3+bE7peCPnz2pJpxc3btJcgHXDwDacC4tzmF8bymUUpFmrm3pOWJnD1Re2Vxy4i bwsPP70Tv2219w8HucK59rL2utFC1857Pd7mwrU4NxqV0BoLWb+tXe9DXb1963L03SaHrRlpAqr9 4tEot6IyALL+49Q6AVViNOVxMs+Pac0oRKVFw9qSqS81P7HXN7b3oJsmAji2+YWxfmZRipVc/PfR lM+F57bF6UPNd5wYXLsHn6jQg+YFH7P6PcVposXb8We1Qbcuez3WVldHaXmDDkpFwag2ujB9v6YR ZBFnRyKHrZ99XY/ynQEce4TaKdE/Hp6u7xejDbycWiYwcUJbFIw23+MGidGCYlNnPjpflP/gA8XF e8ZH2suoIa9g43JAmCZIBjo2dDBIWPOad2KG8vUicoY7Y16VrdMpZC1HfvdIkJI1vTK7Cq+9UWXr iClnXw/yz2ijIuz1Ht1Khh5Pv7rTEVabe1TMAR9IjGhfndlJgRjpVfT40X/hrJzVwfXd46ckOVSt FVX/BmD6LkkeKQVjV/ueHkrVgfTojyB5bq6qtmfaiJydXj6haR8VbTBN5kR7eMjlEfXEdz82gmIg WZ0qq6Ue/ZnqQF7RVSUa9Xfr6sGOioDt08Oq2hhLvm4TI72NQi5vvkF2D0HKOfjKgwBcHwuQSO5h UYqEUnzXkPHChflqjRfuf2yHvpM/vprGdnf/vim4dFw7XH1RO9CGUjsTTR9R8Luff/FhQ5kfASn1 zZ121YmE4PwxbdDogx5M0w5h+mZVA35c68OQE9QZPPTaLwWnDs7E9h0BikRqF27HlKtKjMizyjtL TJ2m24OZEFi7pmTaouCDe0zZ5Y0tPQ8SLwdfoOL9iNwM3HyV+ebHyR66QtUGOOMZoCm619+2xw9d v14p+nTQaUPUrocZbW3p1fDYTYEEySlp89Q2S9MNSM4bY/7USfOyWvtMDmxnvV1tS+j3lBShj2DH nBbz7pHWmqeff3lWFZ6fWRnRtYl4kSoxeuSZCt3PokpmUorFSwqzhwaXsccIqffASRcBOD74AhXv KaTx5q1+0EY7M1Oyegnvso8Hl09ohzNPVfflnzm3Gv99YaflfugO6OnVR3yTrmwPEiW7Erk9euqV nfr+KtqP01KimE7kk418wdEIhabT7EpHHpKGC85pq6+30FSelYnaTgYPZAlI9VCVaOqW9iuFDXmg qnCb8724oK0+VWt2NawQI6qzEGi/tmTqA8H133OEVFBKw6c9FCv4YrPf80gpfqL0B2/a5H3izyhM DnZEcqV1IfJArvoXdphm66dKvm/QfynG6o2a3DSRqfa4s+wfMVE/vrmgOuTIrjUO8Zy3IuAjjd4L 36pC6WZ7tx3Ewymae0mMzj/H/GfKKjEy2iok9i8qzPl112fjDb3mFWwsBUR28DHV71WJ0pz51Xjs +cQOl2yFJd3N/9ymx8pR/RwY+dOUF5luF4xWN9ozymrt9YH/7sDC98yZQ9+3iwdnjshCwSj72/XQ UxV4u6imteabep6CAF51oZrQB8EVjSYMQvB9bnqfKGKkM5cYubgw512D/64puxMn/NLRG/DcbZyw 6lXV9B1t1mvbRkOihkomx6g3XKHONxv5orvlX9vx68aWp6fMfkbox8kjd3dW7pamtXrTH2zyTk7P plmJPFiv+KYeRR/U6lN5FIPKrkRulAb8IVWfygs3/Whm/Wi9jb6LZHBxxMHqpvDyTsrAth0B/Jig xg6qxOh/L+3EnPnW/kjRny8hvlpbMnW58aztEqQDD/rrURC41Dhh5SuLUnS06aGk0BGqEnldePyF naD1EKsSmS3/5RJ1AhtJO2hK6//u367UcovMo8kwYulHTUYRqr0ctNTunH29GHx8Bsj4wMrNpt+u brJKpLJVJRJcenLJ4i+REhnKXKTAgpbEqPCtaptQiQ1rS6bONQrfJUi9Dp40EsAZxgmrX1mUIiNO bkFUrkfQw0lB9KxMNNq74jz1cZlaahO1+dZ7t+um0i1dY/ZxcpRKI4YPP6unxV0c2Nseg42DD0rV nbeSAYJViUZlbxXVIC1NA4WcUJHIBD4zQ8Pn31jXLhXtMPIkMbpkrPk/Qu0VI2qdrF9bMu0Zo527 BKn3oEkXmOkyyCggmlcWpfC0/nJJO6WL/HdOK8f8peasmYRvye6zfQ9IsSykwe5Sm96RR3ISom+/ t9YCLbgeFPPo4y/qdXGiUOoUE8nqRKO01FSBL1ZaN6Kg+FiffFkP8rWoKs4STYt2bO9ByQ+Nlo72 ze6/xBUjIiXarS2Zep/BbJcg9Rk46RoA/YwTdr2yKO1Nnhb6aUGYFsVVJLJQuu/RHVi52ro/SEY7 aPqxn8Wjg+dmkBCV61M6ofYSGXWz8pWixH74WR2+WtUACoZn9aZf8o9nbFy3st0kghSm5OjD1Kwr 0QisXRtND3hIG2ndllSJ0bPTKzFjrl3TdHv0QlrvQ/76xNpvH9Qrs0uQeg2c9A8Anfe41KYPLEq7 we/X3YsJZ6nzETb7nWrQ2okdsXbItFvFBund9PZ890JhJf7vvnJ9zcQOLwZ71ib0p81b/Hj/kzpQ yIWMdIH9e1jnFql9O49pVoWhWxf6KO2RIiexZDWqItEPnjOGZeplbCxzj1m4KjGiH2SvzrF2Wj5c v2oy8Maakmnr6RpdkJp82FVOBcQugQqXgRXnWJSAwwel4rF7OiubZ6fQ4k+/at8ud/rCDein3tqM /O7d/sB2fP51gy1eC2L5vlD8n/eW12HVj43IzBCwwl8f+Yhb9UOjLXt5SCjeXlyL9FSh7Hknwftt WwA/rbNvijbSZyH/jCxMnGD+uiqJEX0fHJWE9t6akqlfU510Aeox8KoDpMCNjqokoJvdqvDo4AaT 8NzjM3D3X9WFGievC9PftPfBJFdH7dtqyh676W9UYfK0cny8ot62EA3xNo42epJF3k/rfPo2hpxu akdM6ekCxcvNinkTXeuNdSUKRKgqIi15hSfvEbSu5NREYnTlBUkiRmTWAJSsLZm6lPpDf7r98PcF 9nDa4Ji+MmJwmD21Y0QSdeLmWfJQoNJb9z8p6qNNf3SMB8vrgbJf/TPnVYOmIrdud8/0jMGlpdeP Pq8D/acfKqOHZYJc5qhIJx6Trlv92bm29uTLO1G+w6/kjzIxu+L8droFnhN94CWbGFF/aEAf41nW BUlqYn9h3ZYTo+yIX5NJlFQ9kAbsa2/fhlU/WG+8YJRvvHbsYP7sMDkSJQ8dm7YkjhAZvIzX4o9q Qf8piuqo4Vkg7+dmp306emwXc9oXQ/14x40dzW6enh+53UlPE3oIFSUFxJCpqu8+OaB13DRdEB8p sb/xUf+r0HvQjWcC4hTjoBNfk2FNidZUVMwbU3+SJR25VdlQZp73gXieE/pjkD/KHF9cNBp64L8V IH9zZLGVDIlCXsxfUqOviZgdnHDRB7Uod4BFGnkJ+fDzemiK9mmRWTgZcnz6pf17lc4ckYmrLzZ/ Y7grvKELYG3J1Ifoe6uPkITUekp9b7Ozv8qJPFJS5RKEetSJfv0aTJjCp82VJLQ//2JCZs5+9Fus 3btLa7CguAan5WXihsvN/4PWYsEWnfh5XaPuZd7nB+iPttmJ8iTBe+H1Sj3on9n5R5IfRTy+5lLz +84VYtQEKCc3V3qLi4VPHyEdMOCma4RA70jg2X1NIo6UKKYJuc5RkWio/uQr9lnStdQm2ogZT7hl cm1EFkO0sZQTdN9t5GFjZ6XEMYfHt6eHhN4JIySjX/1+6KMYVcYOtFcpI03Tpwgrdlr7PJEYXXtZ UosRdbOW0rby6Z+/nbZTN3ESQvY0Ot8NrzRSomiGZicydCDLLyuT7groTDVi9NQrlfofbSvbw2XZ R4AMEd54txpTHt9hXyUUlkzGDuRdQ0UaNTwTz0ztAnKlZFVSJUZkPetEg41wXGUA+9F5w+a2R7iL nXhu3qIaUOwOs5OVokTid+4Zarwv/PupCpCTVE5MIJEIvPZGFR58wvzvvcHooTv3gdlrckbewa8q xcjOvYXBbYzmfUBqetgjbdSoUpqYNX9yNpraxHgthUhwqyjRnLFheh5j81u87Z8PlevOK1u8gE8w ARcToHUzitOlKt11c0cMO1mN1wiqM4VZUTFNRyMjN4oRMZEIdKNXrTrTY2lAPrMfIpWiRNNpZidy B3PdZe2VLNBSXW+6axve+9iejY1ms+L8mEBLBChkxgXX/qYbtbR0TTzHb/lzB9AoxuykKiCpm8WI GAsNXejVK4Xs7OQ9SJE8ECRKlMz+1WFMpz3xkjmRZ7MyBSaObwearzY7kbXZ7Hersd7CgHpmt4Hz YwLRECjb7Mdjz1cgINVY4NHfE/JcMdMkJ6QsRmF6V4ocOuvVAgFXj5CMJjpdlLp29mD8mWqcpJIY Pf3aTlBUUk5MIF4CNbXWWprFU18yB//PsxXw+SQorpbZieJ0kfd18vsYT1IlRq+/Ve3aabpmPHXH 3l4BdEmUP2NOFaVuXTwYO1qNGJFVFXnr5sQEzCIg3aNHu5r8+Is7dR91tLnc7ERRWj2aiNlyjdw9 me36jNpIYmTW7I3ZzKLPT+xL93ghRQe4fc4uqPVOE6Xe+6XgySlqonok1gMZ1In8lgnEQID2pdXU Slx+nvmRVcnVkNeLqEcjpxybjr9f1yGG1oS/JfG++1KHpEnR9CZ88911lkRJhfUdrSlFY+jQr5c6 MaJFzMT5deSu54tr61wCtNVh2pNqzMJpI3c0338So9tvMN8XX+KJkf486aA0IPEEiZpntyhRBM7H 71MzMiKXIG4173TunzKuWaIQeGdxDf4xpVxJcyL9UapKjMgNWIL+EG0aIQkI8yVcyaMQfaZ2iRIF 1vv3nftEX+EI7qBpCbftwo6gWXwJEzCVAIXquO4favYqkSiF8+hysqKRkRN9UprYaem5F69NJ08N 5jtSMrGW8WZltSgdc1gaptyuRoz+99JOR7uRj7ev+H4mYCaBku8bcOmNW5TsVaJN7aEcotL3/x8K pukSXIyaur06q4NXAubv/jTzqTIhLxIlSqr3KdHDeM+taqK8PvxMhZIvlgl4OQsm4FgCFMLi+cIm /3dm7/8zPIXTd5OSqu9/UogRgFQEMryQSHdosFhTH3LVovTFynplYnT/Yzuw6P1aU3lwZkwgWQiQ B+9HnlWzgdYQueUr6pR8/5NFjOhZDABtvBCIz1e9i55qlaJkeHUwG8fkqeVY9im7AjKbK+eXXATI EzptoA0EpOk+JEmUDGEyk+qbC2rw2PPJs8fQ70EmBehL6DWk5g+IKlFqXo4ZnynC66df2R/N0oy2 cB5MwAkE6A88xVdS9QPSrDaSGJGAJlMSgUAaCVLSjJCMznWDKN04eRu++a7BqDK/MgEmYBIBMpuu b5BQ4dXBjComoxgRN02KtmRll24GRLflQaLk1OHw9XewGLnteeL6uosAbZ+g/XxOS8kqRkY/0Agp aRMtGHo9wJUKwkzECvWKm7diza+Nsd7O9zEBJhAhAdrP1+iTuGSs+a6GIqzCHpeRk+Rkm6YLBiAR aJPUgkQwCt+q1pnYLUr0ML65sBrr1vuC+4jfMwEmoJDAK7Or0NAgbf9RSt9/w3xcYXMdnbUAPCRI zvh5YCMqu0WJHsbpc6uweYvfRgpcNBNITgL0/acwFuG8L6gkw2K0my4JEq0jJX2yS5ToYXx5dhW2 lbMYJf1DyABsI0DT936/NH3zfGsNYjHak1DST9kF47BalOhh5MB6wT3A75mAfQTI0Ims71TELgrV Khajvanw6KgZExIl8hmnOrEYqSbM+TOB6AksKK7FPx9S4yk8uDb0/f/fy+r/zgSX6Yb3JEgujA+p Fq1qUWIxUtt/nDsTiIfAex/XKRUl4/tfV58osbrjob3nvSRITZ4H9zye1J+yMgVy9lU3m0luRkYO yUxqxtx4JuBkAocMUOcvgL7/+WeYH2rdyTwjrZu6v7qR1sBh15EYTRzfTolvquCmUuRJEWR2Hnwu Gd5npFPrOTEB5xG47rL2yr//FBKdEsc2293/EvCzIO3mAavEyCiS9j4JAcyc17QXyjieDK8eXr1M hm52VRszMwQun6D+x6gBhUXJINH0KqBV0Z8FdiUNWC5GRldccX47x/rUMurIr0wg0Ql0bK9ZKkYG TxKliwuSfiuogQM0Qkp6d9JWj4x20f/9DbkuSfEKHr43BxPmcy0vCIehw6eiIdCtiwdjR7dRPk3X Up14pNREJiBkJQlScvk4b/ZU0DDdijWjZsXu9ZEeSq8XePpVtjHZC06IA7SJkRMTiJdA7/1S8OSU zvFmE/f99P0nv3rkyihZk9S0eg0yuUdIVs4Zt/agjTuzDSZO4OF7a5z4PBMwg0D/fs4QI6MtNFPi 1JAYRh1Vvnr8qNEgkncNyQprmmg7kESJLPA4MQG7CGRkJL4F5CEDUvGfu+0fGTXv42QWJQ2o0gSQ lNuFnShGxsNJ0SztcvRo1IFf3UugPs64jhr9VUjgdPxR6Zh2xz6ObWGyilIDtNqkXENyshgZ35Kz R2aB/jAkc3wUgwW/RkegsZHX11oiNuzkDNzy5w4tnXbMcRIlSkm1ppRVvUOTkOodNzmmmwFVYvT6 73GVzGzqmSMy9fqmpyX2L1YzmXFeTKAlAqOHZyoTI4pAa3YiUco/I8vsbJ2aX13x873qvIDY4dQa ml0vVWJ0273b8elXTdbzNN1mZiI3I5QK36pC6WYOUWEmW84reQgUjMoC7flTkcgZK/m/o2SMbMwq xwgcakQiMCtfB+aj65AmZHIIkhVi9MRLO6FipESi9OIjXXFQ3xQHPkdcJSbgbAIXntvGEjGi6bWX Z5lvtk2ilAQjJX2mToOQCT9CskKMjK8kiZIqV0CP/qszjj5MndNHow38ygQShQBto7gwX81Witsf 2D0yMniRbzoWJYNGNK9NAyNNAluiuc1t11opRgabJ1/eielvmv9LifK/99ZOGHJChlEUvzIBJtAC Ado+QdsoVKS/3bMdy1eE9rqmUpTMXhJQwSa2POVmuk8LaFpZbBk4/y47xMigQh4XVPxSovxvu7YD Rg3j8BUGa35lAs0JXHNpe6j64339Hdvw+dfhPa6pEiUSWTLOSMC0ldrkFVLobxKtgXaKkcGSHkpy B2L2Qiflf93E9qANjDPnJp+ncIMvvzKB5gQ6ddBwwTltlfmlu/KWrfh5XWPzYkN+pu8/JcNXXciL Yjh47WXt9bso5HrCJCFLqS3erBp/WU2CzQA5QYyMB4UWOn0+4PLzzJ/HvuK8dsjK0KDC5NSoP78y AbcQ+EOfFDx2jxrvCxTldfa71Vi/0RcVDhIl8lFp9tRhoomSDDQtHWnz5uWQzCaM1DpJjIwnd8bc KvzvJTUOMcj31VUXqjFnNerPr0zA6QTI+4JKMZo+typqMTKY0fS9ijVlEqVEmb4T0DYRLyNM2gYD nptfad7Y2LdjZjuC9xnFmi/tI3jkGTWO1c85PQvU9pQU3kAba//wfe4lcFpeJu66uaOSBtDI6OnX dmLzlvj2AKoUpTOGun9NSRMB3ZZBjxgrpVgvhDxQSY9alCn9QSbPBmanux4q37XpNd68ac63vkHi 5qvMd11CbScXZLPnV2N9aXTTCvG2i+9nAnYRoDhGKqbDqT1vLqjBo89VQJrkickILWP29N31l7fX 16oXFNfa1Q1xlys0/EqZ6CMkoQXWx52jjRmoEiPagf3+7zuwzWoePTQkcioSjQ6fe6gLDh/Ee5VU 8OU8nUWAjIVUidGc+dW6H0mzxMggp2qkRD9yR+S61hjA37ApRx8hNU3ZSbhWkFSKkeEOxHiYzHol kbv5n9vNym6vfKbc3glDT3Ltw7lXe/gAE2hO4OqL2imLHUTeVh57Xs2aL7WDREmFRxcXi1JpcbHQ p3V0QZJC6MOl5p3u9M9uFCOD6Zff1uNPt2zFO4vV2JP87S8dcM5p5vrVM+ruhFdaxD7qEB4JOqEv rKxD504e3eHwGEXP9qtzqkDeVlQnVW7GXClKQQMifQ0JkL8C7loQd7MYGQ/7T+sa8eLrVfD7ocQY 46qL2qFDew3PvGa+J2KjDXa9nnB0Ouh/8Ue1mLuoBt+sijMIkF0NMbFcjwf6jxBVTkRNrGpMWR06 IBVTFcYxou0TVoZ7MITP7A28JEqNPmDJMpesKQUNiHRB8gY8P/pFIKaHxI6bEkGMDG5bt/vx3xeb fpGpsBAcf1YbZKQLPPqc+l99RpusfM09PgP0f+F7tZi3sBrf/RTZpkUr66i6LCEAGjEksvk/rY/Q H1pVibZl2OFRW5Uo3XZNB/h9cpcXclXczMlXrjHy8dCbIwZOqawVlbcCQv9snHTiayKJkcE3EAA+ +bIebdto6N8v1Ths2utBfVPRsb0Hm7b4UbHTGT88AhIgsTQr9TkgBWT+26mjB1u2BVBe4Yx2mtW+ UPmQEJ11ahZ0p7uHmjd9+eaCauxwyHNC7R53Vhtcc0mTd4JQHOI99u+nKvDGu2qmziOp2+ff1CMz Q8OAA8397p9yXAZ+2eDT/0dSD7uuEZDPrymZ9iWVrwvQqlV3yt4DbzofgHPj+gL6XhsVpt3B8Uzs 6hQq97Ov1DyYlDftYj9zRBbW/OrDr1HuNlfBhKYUzj2jjel7pw7snaL7+WvXVmsS4MrEFKbTh2bi v/d2xjGHmydERj/TNHJdvUm2zkamMb6SN5ILzjXvh0vzakyeWo6iD+yf2iJRUvGD1BWipImpa7+d qtsx7BoR9Rk4aSSAfs07zCmfE3FkFIotPZgej8Ah/c39tWSURdNbldUSqx0wtdW/Xwp65vy+jGlU 0KRXGhWSAGdladj0mx87qxJDmIafkoHrr1DnXLeqOoDnZ6rxVB9N12ZmCPzpwvagTd+q0rW3b8OK b8I7SVVVdqh86QdpMoqSP+C9bd2qKfpDt0uQeg2adAyAY0OBsvtYsoiRwfmrkgb9F+qRiqzIjjks TRc9KsfOJCFw8rHpSqswoF+qPq2Vnq5hQ5kP1TXO+OUfbaPph8S1l7bX14q6dNr1tY02m1avX/5F vel771ottNkFBx+UiosK2mLkYPM3ulNR5H2BRkY0neW0pFKUflzr078DDmvzzqWF3f5u1GnXk917 0KT9AZxhnHDKa7KJkcG95IdG/LY1gOOPVvMHm0ZgNK21YmW9aTvRjbpH+kpThxeca77T2VDlD/xD qm6BlpoisHa9zzFTUqHqGnzsxGPS8eeLm+L67Ntl19c1+BJT39Pifumm+NzkxFMhcoMzeVJH9NpP TXRk8r7wyLMVqKl17g8TVaJEcdRoZmSjjf0b4tn4Zm3J1KeM47ue8D4DJmVA4FLjhBNek1WMDPZk Fk4PUJ6iTa40rUW/trduD2BbufVTWrQLnkYsVkbBHXRQKgpGNa1d/bCmEY0ONcr74xFpuPrC9nro gpx91UxrGs+Z8frdj436pk3js9Wv5HlBpck6bUb97wvusDZVJUr0t8RhorR4bcnUN4xnbZcg9Rt4 fVVAaLcYJ+x+/csl7fSpFrPr4RQDhkjbRb9mPvi0DgJCN0yI9L5Ir+vbKwW0QL55qx8/r7N+CoP+ CLZv58FBfdX8Im6JA00LkZUfrddRHWgvmBPSUYem4U8XtsPFBW3RI9saITLa/ez0Sqz5xfpngJwC k+cF+qGgKlGwzKddth+PREnFd8NJoiQhX1tbMu1Do993CdLPqx6q7T3wxisBYc0cilGDEK80RXH2 SPMXM+9/bAeWfBg67HCIajjm0I6KAFaubkCbTE2JKFFDaZMppa9t2GBK0Tc7tPMoa1u4jqSpSwrh QRM4q35oBJng25EOGZCKyye0xcQJ7ZQZeoRr18x51SicZ32wx4EHpuLisW11k/1w9Yvn3JMv78Qr c+w31IilDZ9+mdiipEnt32tWTf3RYLNLkOhArwE3jxQCvY2TdryqEqMpj+/QN0/a0SYzyiQzaZV7 laiOhw5M09eVSJSsHDHQ1N2n9GuwrfUjJaNvDhuYpk+PEWea0rBKmPr3TcGl49rpI4QDelo7SjTa To5En/h9c7ZxzIrXkUOawkb03l9du+/9zw68VWTfHiMzOKoUpa9WNcQdWiOeNgrg1jWrpu6Ky7OH IPUeNOkQAMfHU0A896oUIze7Zg9mSsN4r1fgYEVm4bSuRCOGb75r0PfxBJet+j198eoaJFRZF0ZS /yMObhKm2jqJ739uVGbw0Wf/FN2SjEIH0KZeuxK5ynnyFetdS106ri2uPF9tYMnrbt+m/4izi62Z 5aoSpRG5mfji2wb8ttWWOettiwtzCjVkfQAAIABJREFU/i+Y056CNHBSVwBjgi+w6j2LUeSkv/y2 AVWKjQGGn5Jpy36lku8b8frb1boQqBLdSEjTWs4F57RFTZ3U15giuSeSa2jf1QXntMHNV3cAbeK1 Ky1fUYfHX9iJtxU5922pXZ06aLj8vHYw239bcHlk1v23e7Y70cQ5uJpRv1clSqcOtkeUpBTL166a +mIwiD0Eqc+gSWRz9OfgC6x4z2IUPWVaiCevC7Q/RVWi/UoZGZrlmwdp2oxEd96iGgT8AFnG2ZVI mC48ty12VjWNmGKtR86+Hpx/Tlvcek0HJe6hIq0X+fp7bnqlbk1ntfkvmbBTmHHyGqIqGaEjGhqc a9YdT9tJlMgNmNkM7RAlTcjZa0qmLQrmsYcgHTngwfJaUXUTAHVPTHDppH6KDBhozShRpumaIdv1 kfbxqLTAo4Jo0blrZ3tMw8l9DU0nLHivVl/Tor1EdiVy0UPCtG1HAD+uidxWvMs+TUL09+s7mu6r LFoW0/5XgUeeqcDPNljSnX9OG9D0pMr0YmElnplu/fSjyjaFypvWkhNBlITEY2tWTVsZ3Ma9Yk7k FWxcDghLPDawGAV3RezvyckmsSRHmyoT/TGjMOx2pe7ZXowelqnUnUwkbaNRBnkWX/R+bYtrTB3b a/pGXHIManci56E0NWd29NNI2kWbry/KbwsVPiiDy3/46Qp9RB18LNHfX3dZeyVha668ZSt+Xhf5 j65YOQuBAUUzcr4Lvn9vQRpb+m9IXBd8kYr3LEbmU504vq3uGdn8nHfnSFMihsv83Uetfdeze5Mw qdgaEG1LSJRopEpGEB4N6NhBQ98DUkBTfXYnCjlCnrvtECJqO7mF+scNHZVj+L/7tieM8UK0sFSJ 0hU3b8WaX5WKUsVJA7I7TZ68Z9yjEIK0cRykeC1aMNFcz2IUDa3orqXF4j9doNZ6iUK70wjBbl94 ZKlGMaTI3Qyn3QTI/Y9hGLL7qLXvaGMvTdOpTGS88PLsKmwrt8VCTGXTosrbpaK0aPHMnOHNG7rH GhKd7HPQLTXQpLIREotR8y4w9zNt7ly3wQdyO68qHdDDC7LCa2gEvv3ePgetFPPo4y/q9bAdZApv p/m0KtbR5EuRgW+9dzu+/V7pL9uwVdqvu1f3MpE/Su30MW3kJTdANDJN9kTfQRWb5unH3rJP69XE FhPylbUl04qb991eIyS6YEhB6W8C6NL84ng/sxjFSzDy+7t38+Lc07OUzDEH14KcVU5/swpbttn/ K5W8HdAak0rLw+C2O+X9C4WVeO2Navh89v5xpj9g9GtddbIruqvqdsWTf1amwMTx7ZR835VM3wlt 1OIZ3d5q3ua9Rkh0Qe+BN51EMd2aXxzPZxajeOhFf29lVUCfV8/KND8SZXBtyAcdCd9v2wIgZ7B2 ps1b/Hj/kzqQp3QK206/1hM5kX82GhF9sbLBMs8SoXimpQndKSo5R1Wd/jGlPOGtZ2NhSE6CVY6U Fr5Xq+99jKVuoe5Jlbj+p1VT97KQakGQbuwJiGGhMorlGDlOHHOa+UP4ZDDtjoV38D3kJ67BB5AH ApXp+KPS9TDMX5XUg8KT25nKNvtRvLwOZA1Hgd5UBQG0q43kXeH2B7brU5VWungK1V4ajf7v/s6g uFMqE60X3fVQue7WSWU5bs5bpSileIW+FGBSPLHvFhbmTAnFOqQg9Rn0Vz8gJ4a6IdpjtMCuIuoj mbLOX2J/6OFoedhx/berG/S9J4MVbqKldg04MFXf/PnLRp8jgp9RXJ+lH9Xhp3U+PRJnTjd3j5im v1GFO6eVY/mKekeEzZg4oa3ug0/1M134VjUee36na4MrquYTnD+JEq0jZ2WY64iZNuKmmidKs9eW TN1ruo7aEVKQ9usyZZOWVTUJQFw/e0iMVLgIof0wtIufU+QE1pf68M6SWqSlqgljEVyTU47NAE3j kLcFJyRqe9EHtfi11I8O7TRYEejOzHbTAv49/9mBDz6pc0RgwWOPSMNVF7XHqbnqrRtpi8FLr7vT U7eZz0A0eTU0Sn00QwJipkcHs0RJSPlQ8w2xRvtCCtK6dXcG+gyYdAoE+hgXRvuqUozs3JwZLQcn XU9RMmmXN8WfoXhAKtOgP6Tqng0oTLRTQkWvW+/TvT5s2uJHp44ePTihSgbx5j3r7Wrc/98KFH9U 65jRAe11u3Zie0tiNd3yr+1Y/AHPgsTyHNHUGlnbOlGU/H55/brvpoWMlBhSkAjAAQMn7S8EhsQC g8UoFmrW3UMjF4oSe9xRasKjB7eEzM8zMzR8/Z29C+/BdSLXOfOX1FjGILjsSN6/8W41pjxeoY/q yDjFCYnWCGktmLxDq07U/kl3bQeNbDnFTkC1KJERRdQRlwXWLH29+90ttapFQeo98AYfhLispRtb Os5i1BIZZx3/cW2jbpGmKhJtcGv1taUxbWyLShtcl+D3xOClWbSxMoDjjlQvzsFlh3pPcXumPlGh W5FV7HSGENH+LgoaSBGcaSuB6vTcjErQfiq7jTVUt9Oq/FWKEu19ilqUBApbWj8iJi0K0lEDp5bV iqprAUT8TWUxsuoxM6ecHTubTMPT0zRY4biUotKSb7MNZT49tIU5rYg/F3KWSsK0s1KCnKhancik lox0SJBos69T0ojcDDx+b2dLng1qM5l0v2NxOAynsFZZDxKlX0t9GGNyFG5aU4pWlATEA2tKppa0 1N6QG2ONi/MKSmcDONv4HO6VxSgcHeefOy0vEzdeoX5To0Hi6dcqQVZjTkvkqJb2VV2p2P0StZvW huYuqsE3NoSND8edfAWeOTxTubNeow40RUcjI/rDyUkdgd77peDJKZ1NL4BM8p9+LSIryIBX+rss KOy5vaVKtDhCohv6DLipEwROb+lm4ziLkUHCva80fbXkwzp4NHMtc1oiQvuiaCqPDC2ctlZAZrOv zKlCQ4Oa/VsffV6nmzFPf7Pa1vDRofrmgnPb4I4bO4IiB1uR6IfJs9Mro1+LsKJyCVYGjb7JFRB5 1DAzGSOllasbQLHMwqTPFxX2eCTMeYQdIeXmr+/rEZ4fw2XAYhSOjjvP0Y57CmNuVZozv1p3Bkqe FpyWPB6A9m+dPjQzbstEmpKjX5M//2KvR4tQjIeckKH/oVJtfWmU7RQHvUZ9kulV5Ujp4WcqwqG8 Z/HMPUOWN784rCDRxXkFpd8DOLD5jfSZxSgUlcQ4RtE9J09SHzogmBYFWCPvzQHnLKMEVw8d2ms4 8uA0HH5wKg7okYIe2R60ydL2uMb4QKbltFb2/U+NesTdVT80wOc8vdVDZYwengmasrUq0b6qJ18O afVrVRWSvhw7RElInFRUmLMsHPxIBOlBALRJdo/EYrQHjoT8QPuVaDf+OQrcPoUDZncgwHB1a36O XBNlpGtITxfw+yRq6qTugbqx0dnrISSk487MwrgzrRsJE7up/yMPK7ypvflzZMfnfr1S8Ph9ataU QoyUtneS2V0LC0XYn2Vh15AIUu+BN9H8wkXBwFiMgmkk7nsaqZAvvPKdARx7RMTGlnED+eMR6Ti4 f6rulYCC3zk50Zw5hUCg/UJVNRL1DdKxIzziqGnAhLPb4P7/64RBijdHB/cbGS7c8eAOfPOdM7x3 BNctWd9v3xHAF9824NTB5o6OaU2JQqzTJvygNOutwnazgj6HfNuqIHU64cGN6XW7zb9ZjEJyTOiD P/zciIXv18LjESDv3lak7K5e5B6Xgb69UnQT8dLNYX9YWVElV5dB1oMU4v4/d3fG4YOsNW0n9z/P z6zi2EUOfIJ+2+q3RJSkEHevLZm6qjUErU7ZUQZDC0pnSiCfpm9UDPEp0Nbsd6pbqyufdwABKyLS hmqmU02kQ9XVScdIiGh96IbLrTPpN9pPBhxvLqwGuWzi5GwCNFr+9537mF5Jipf2n2d3NAqZ0aWo sFNYiwcqPCJByhu7cdzE8e1eUyFGHGzL9GdAeYb79/DirBHqg/+FasiyT+t0x7orvtljOiDUpUl9 jIRoxCmZuOkq64WIwD/1SiVmzHXePrOkfihaabwqUVr6Ue36ISdk7tdK8frpiASp9LfGU7O7eOdH kmE017AYRUPLeddSWJGrLmxnS8U+/6ZeN6H+8LM6W8p3aqFkpj5qWJbu6seOOvKoyA7q5pV5+KBU TLnd/JESgH5CiJ9aq2lEgiSlfA3AuNYyi+Y8i1E0tJx7bbcuHj3ECK1P2JHW/NqoC9O7xbVwumWb Sj5tszR9H9Gl49RHbW2pHbRW9PpbPPXeEh+3HD/msDTcc2sns6vbTQixubVMIxWkCwG80FpmkZ5n MYqUlHuus9r1UCgy09+swluLakB7gJIl0X4S2nk/api5llLR8CMLOgqi58SNzdG0g6/dTcBkUSoW QgzenXvL7yIVpA4AylvOJvIzLEaRs3LblakpAhcXtEXBaHtGSwavT76ox4L3an6PrOrs/UBGnaN5 peCHucel47QhmZY5Pm2pfryvqCUy7j9uoihdI4R4NBIiEQkSZSSlfBPA6EgybekaFqOWyCTW8aMO SdN/tZN3b7sTWfkUfVCD1T81QrpYm8hI4ZD+qcg7McNSrwot9R+NRskHnVO9arRUbz4eHQGTRClb CLEpkpKjEaSxAKZHkmmoa1iMQlFJ7GMFo7Jwxfn2GD2EIkvrGxQGfNWPDa4QJxKhAf1SccIx6SCW TkhLljV5KP92NW9wdUJ/WFEH+mF5500xuxFbJIQYHmk9oxEkmqSOacWSxSjS7ki869q20XDe2W10 wwcnta7og1p8+mU9Vqysh1OC4RGfrEyBwwam4Y9HpOlTck5i9uhzO0HrRZySj8Apx6bj9htiEqVL hBDPR0osYkGiDKWULwK4INLM6ToWo2hoJe61hw5IxejhWTjlOPun8ZpTXrfBh5XfNej/v1/TiNJN PstGUB3bayBXKxQgkTwoWOUJozmDcJ9fnVOFFwo5ims4RslwLkZRai+EiNiTbrSCdCqAiPcjsRgl w2MaXRuHnZyBW/5MNjLOTuRzjTwMbNzkQ+kmP0o3+0ARdmMZTZGT2k4dNHTu6EGPHA96ZnuxXw+v 7mm7a+dWvXfZBmreoqZwGWRaz4kJEIEoRalQCFEQDbloBYm+PRH5AWExiqYbkutaWhspGNUGl59n 356ZWIn7/UBFZUDf80SOVBt9TZYSXo8Atcv4nJmu6Y5MMzMFaI+QmxJZKc5dVA165cQEmhOIQpRO F0K80/z+cJ+jEiTKSEp5L4C/hcuUxSgcHT5nEKCRw4Sz2oCilHJyBoEHn6jAu0s5PIQzesO5tYhw psMrRPhwE81bGIsg/QHA6uYZGZ/Zh5VBgl8jJdC+rYb8M7Iw7iwWpkiZmX0dGSy8uaDasrUzs+vP +VlPYERuBm6+qsXp9/uEELdGW6uoBYkKkFJ+AODE5oU9N6MSr8xmh4rNufDnyAjQesroYZksTJHh MuUqms14/W0WIlNgJmEmYUTpICEERRuPKsUqSJcCeCa4JBajYBr8Ph4CZHlGUWp5xBQPxfD3khDN nl8NWhPjxATiIRBClJYJIU6KJc9YBYl26e0aCrEYxYKe72mNAIXZPuvUTN0dUWvX8vnICDz8dAXe XlzDHhYiw8VXRUigmSidJ4R4NcJb97gsJkGiHKSU1wJ4+LHnd2LOfN4stwdV/mAqAa9X4PS8DFxz qT2xfUxtjA2Zfb2qAXMXVuP9j+t4jcgG/slSZJssbdurj+57aVaWmBtrm2MWJCrw7oe3n7j0wzpa T+LEBJQTILPqIw9Jw+jhmTj+KOdtsFUOIMoCFhTXYt6iat2PX5S38uVMIGoCAuL+opnZYS2wW8s0 LkGizPPyS5dCILe1gvg8EzCTABlAnDE0ExPOZsu85lzJ6ek7S2qwoyLQ/BR/ZgKqCASE39OvaNa+ a+IpwARBKjsXQhbGUwm+lwnESsDrAY45PB3DT8nAicck76jpvY/r9FhQX5XU87RcrA8T3xczAQEx t2hm9pkxZ/D7jXELUm6u9Hq6lv0MIKKY6fFWmO9nAi0RoP1Mg0/I0GMFDTootaXLEub4N6sasPjD Wn1tqLKKR0MJ07EubIgATi2ambMg3qrHLUhUgSEFZTcJyCnxVobvZwJmEdinowcn/TEdJx+brscR Mitfu/MhH3sffV6H95bXYcs2ttm2uz+4fJ3AqsUzswcBIu6IY6YIUu5Zazt4UtPWA+AJfX5CHUeA zMePPCQVfzw8HUcflgba5+SmRNNxH6+ow8df1INHQm7queSoqwCuKJqZ85QZrTVFkKgiQwrKHhGQ 15hRKc6DCagk0LO7F4f2T8XB/VP10VOXfZzjcZsct67+sRG0FvTFtw1Y84u7I92q7EfO2xEEttZI /37LC3vWmlEb0wRpeP6mXn4R+AGA14yKcR5MwCoC7dtpOLB3CvockIJ+vVLQM8eD7vt6kZZm2tcj ZFPKNvvx60Yfftnow/c/NYBiMW36jafhQsLig84kIMUdiwuz7zKrcqZ+4/IKSl8GcJ5ZleN8mICd BMhIonMnD9q1FaBpP4p+2yZT6EKV4hWgDbspv//8CgRoszjg+X2wVVsndW8IdQ0S9fUSOysD+nTb zqqAbo69dbsfPtYeO7uXy46fQJVX+vdfUNhze/xZNeVg7mjG438Afs8EAKYKnVmN5XyYQDQEaPqM /nNiAkxgbwISeMpMMaISTF3dXfxaz29EFBFl924iH2ECTIAJMAEXEGjwavIhs+tpqiBR5aSG+8yu JOfHBJgAE2ACziEgIF5ZOL07WVabmkwXpMXTcz4A5BJTa8mZMQEmwASYgFMI+ODX7lZRGdMFiSop Ie5UUVnOkwkwASbABGwmIOTL8fqsa6kFSgRpycyc9wEsbalQPs4EmAATYAKuJODzBwL/UlVzJYJE lZXAZFWV5nyZABNgAkzAFgKvFBf2/ElVycoE6fdR0iJVFed8mQATYAJMwFICDR6pKV2OUSZIhEnT cFvTYMlSaFwYE2ACTIAJmE5APrWwsNta07MNylCpIC2anvM5gFlB5fFbJsAEmAATcB+BGq8vVdna kYFDqSBRIR4/bgfgMwrkVybABJgAE3AXASnwyILZXcpU11q5IC2clbMaAi+obgjnzwSYABNgAkoI bGvwND6gJOdmmSoXJCrP25hCo6TqZmXzRybABJgAE3A4ASlw97JX9y+3opqWCBIN9SQER5S1oke5 DCbABJiAWQQEfqxon/2YWdm1lo8lgkSVyKqlEOeitLUK8XkmwASYABNwBgEZwC0rnhSNVtXGMkGa Ny+nRorA361qGJfDBJgAE2ACsROQwHtLCnPmxJ5D9HdaJkhUtZP757wACTIF58QEmAATYALOJeD3 yMD1VlfP8kB6eWPLjoaUH5sdi8lqcFweE2ACTCBRCUghH18yo/vVVrfP0hESNW7xjOzPADxrdUO5 PCbABJgAE2idgAS2BOobyMuO5clyQaIWpkpBjd1heWu5QCbABJgAEwhPQOC24jd62fL32RZBml+Y vQVC/F94KnyWCTABJsAErCQgpPzk5P7Zts1g2SJIBPik/t2eAPS1JCt5c1lMgAkwASYQmoDPr8kr J08WgdCn1R+13KghuEmDx244VJMaWd15g4/zeybABJgAE7CYgJBTFs/o/leLS92jONtGSFSLpTN6 fA2IaXvUiD8wASbABJiA1QTWeVI9SmMdRdIgWwWJKphZKwmC0hgbkYDga5gAE2ACyUpACnH1wpe6 2e5v1HZBIg8Omha4nAP5JetXgdvNBJiAzQReXjIje77NddCLt12QqBaLpvdYDOBJJwDhOjABJsAE kojAJq/0X+eU9jpCkAhGXV3KzQB+dQoYrgcTYAJMINEJSImrFxT23O6UdjpGkD6c26VSCslTd055 MrgeTIAJJDgBMd1q56mtAXWMIFFFl8zovhCQj7dWaT7PBJgAE2AC8RAQpV7p+3M8Oai411GCRA30 pHnIDv4HFY3lPJkAE2ACTABSQF7qpKk6o08cJ0hkehjQtAsB+IxK8isTYAJMgAmYRUD+t2hmzgKz cjMzH8cJEjVu6fRun0DIe8xsKOfFBJgAE0h2AlJgdWatsNUbQ7g+cKQgUYX9m3P+CYmPwlWezzEB JsAEmEDEBBoAnEd7PyO+w+ILHStIxcXCJ/2YIIByi5lwcUyACTCBxCMg8dclM3K+cHLDHCtIBG3J 7JxfpBRXOBkg140JMAEm4AICby8uzH7E6fV0tCARvMWF2a8L4Amng+T6MQEmwAScSUCU+j24GBDS mfXbXSvHCxJVtVr6b4TEl7urze+YABNgAkwgAgI+aHJc8Ws5WyO41vZLXCFIywt71krNkw+gwnZi XAEmwASYgEsISIhbF0/P+cAl1YUrBIlgLpmx789CyIvYK7hbHi2uJxNgAjYTmLNkZrepNtchquJd I0jUqqIZ3d+EkA9G1UK+mAkwASaQbAQEfhQy/RI3rBsFd42rBIkq3imQc6uUgsJVcGICTIAJMIG9 CVSLgHZOUWEn1y1xuE6QCguFPw0YD4Ff9u4HPsIEmAATSGoCUgAXFRV2W+lGCq4TJII8vzB7SwCB MwE4dsexGx8GrjMTYALuJiCBfxXNzJnl1la4UpAI9tIZPb6WQlzMRg5uffS43kyACZhKQMq3Th6Q fYepeVqcmbC4PNOLyysoux2Qd5meMWfIBJgAE3APgZVCpp/kxnWjYMSuFyRAiryCspfIaWBww/g9 E2ACTCAZCEhgS8AXOKJ4do8Nbm+va6fsdoMXspMsvxSQH+8+xu+YABNgAklBoEZq2qhEECPqrQQY ITU9dCPzy7o0CnwkIfsmxWPIjWQCTCDZCUgJed6Smd1fSxQQCTBCauoKsrzzSd9IGr4mSudwO5gA E2ACLREQErclkhhROxNmhGR02uBxm/6oBQJLAGQax/iVCTABJpBIBKSQjy+Z0f3qRGoTtSVhRkhG x1D484CU4ynorHGMX5kAE2ACCUTglX0COdckUHt2NSXhBIlatrSw+1wJ/IX3KO3qZ37DBJhAQhCQ S8iIizzWJERzmjUi4absgts3NL/0b1Lg3uBj/J4JMAEm4FICnwqZPtzte43CsU/IEZLR4KLCnPsE xP3GZ35lAkyACbiUwKpUKc5IZDGifknoEVLTg6dvnH0cwJUufRC52kyACSQzAYFf/I2BExNlr1G4 rkzoEVJTw2njbPafAbwSDgSfYwJMgAk4kMAGf8A/NBnEiNgnwQip6RHLz5eebaL0VQFR4MCHjqvE BJgAE2hGQJZByNzFM3r80OxEwn5MGkGiHjzyCpnSvrxsuhAYk7A9yg1jAkzA9QRog78mcErRjJzv XN+YKBqQBFN2u2mseFI07oPy8QJi7u6j/I4JMAEm4BwCuhhJLS/ZxIh6IKkEiRpcWDiwoaPcni8l ZjvnEeSaMAEmwAR0ApulRwx2a8TXePswqabsgmHl5kqvZ99NL0PKscHH+T0TYAJMwB4CotTjl3kL Z+Wstqd8+0tNuhGSgby4WPg6BbpRDKWXjWP8ygSYABOwicB6v/SdksxiRNyTVpCo8eR+o5PMpjDo /7PpIeRimQATSHICAuInvyZOLi7s+VOSo0ges+/wHS3FkLFl9wiJv4W/js8yASbABEwkIMU3fnhG FBd23WRirq7NKqlHSLt7TcglM3JuFVL8lR2y7qbC75gAE1BIQOKj+pSGXBaj3YyT1qhhN4I93w0Z WzpRSDwBwLPnGf7EBJgAEzCNwLuZtThn3rycGtNyTICMWJBCdOKQgo2jBASFBc4KcZoPMQEmwARi JiAgnivv0O1K2hcZcyYJeiMLUgsdS5FnRSAwTwBdWriEDzMBJsAEoiQg/7l4Zs4dgJBR3pgUl7Mg henm3Pz1fb3CO19C9g1zGZ9iAkyACbRGwC+Aq4pm5jzV2oXJfJ4FqZXezx1f2tnjxxwAJ7ZyKZ9m AkyACYQiUCGAsUUzcxaEOsnHdhNgK7vdLEK+K34tZ2tqZfVQ3kAbEg8fZAJMIDyBtQGPOIHFKDwk 4yyPkAwSEbzmFZTeCuBfyRS2IwIsfAkTYAIhCYjlnkZx1sI53X4LeZoP7kWABWkvJOEPDMkvPVsI vAigTfgr+SwTYAJJS0DI51N31vxp/vx+9UnLIIaGsyDFAG342E2DAlLOYWOHGODxLUwgsQn4hJA3 Fs3o/p/Ebqaa1rEgxcj1xAm/dEzzpbwK4NQYs+DbmAATSCACehwjTRQUTc8uTqBmWdoUNmqIEfey V/cv7ySzzwBwL7sbihEi38YEEoWAxOfw4WgWo/g6lEdI8fHT7x6aX3a6FPIFAPuYkB1nwQSYgIsI CCEeTdlZdROvF8XfaSxI8TPUcxiev6mXH4GZEDjKpCw5GybABJxNoAoCf1o8I+cVZ1fTPbXjKTuT +mphYbe1qVXVJ9KvJZ7CMwkqZ8MEnEtgpcePo1mMzO0gHiGZy1PPbejYjWdKKZ7hKTwFcDlLJmAz ASnk44GMhhuLn+9VZ3NVEq54FiRFXZo7ZkMPzau9LIBTFBXB2TIBJmAtge1SYuKSwhxyJcZJAQGe slMAlbIsnt1jwz4yOw8Q/wAku5lXxJmzZQIWEVgqJA5nMVJLm0dIavnquQ8bV3pUIACywhtgQXFc BBNgAuYRqBPAbScOyH548mQRMC9bzikUARakUFQUHDsuf31Gpua5FxLXsi88BYA5SyZgNgGJLwFx /uLC7FVmZ835hSbAghSai7KjeQVlgwXw5P+3d26xVVRRGP7/PbW2oqYgSIGC8a6QoPLgJYKppYL1 Fi+p0AcUrw/6oDHxHhU1XuOLiYmJqVGjRvAIGjERQ9GCGNEgmGgUlYhSORRRqQhKy5lZZho1EUs9 9zMz5386ObPXWnutb03yZ86Zvbe2HSoZYgUWgUIJDBj48K8NjQ/rVNdCUebmL0HKjVdRrAeflliz ALBbANQUJaiCiIAIFEzAgA/DPS8CAAAGbElEQVRovF5PRQWjzCuABCkvbMVxap275RQL2AlgWnEi KooIiECeBHYCvGvG5Man9V9RngSL4CZBKgLEQkI0N1uNO7z3ZsIeAFBfSCz5ioAI5EOAS/2Mf0P4 Zmw+3vIpHgEJUvFYFhSp9bJtRwUueIa0mQUFkrMIiEC2BLbB7KYVqQmLsnWQXWkJSJBKyzfH6MaW Oen5ND4OYHSOzjIXARHIjoABfK7GMre+k5r4S3YusioHAQlSOSjnOMfs9p5RGboHAV6vlx5yhCdz ERiewDrS3di1qHHN8GYarQQBCVIlqGc55+BLDz6fBDEjSxeZiYAIDE3gJxrunj5lXKdeWhgaUBSu SpCi0IVhczC2XJ6eS/BRAJOGNdWgCIjAvgQGQDzl9/c/2P3GkX37Dup7tAhIkKLVj/1mE65dGgHv JiNuB9CwX0MNiIAI/E3gdd/827pTEzf+fUGf0SYgQYp2f/6TXXNHerTn2/0ArgN4wH8MdEEEqpwA zT6C5+7QceLxuxEkSPHr2WDGze09x3j0FgDoAKBd22PaR6VdVAJfkLina9G41wFaUSMrWFkISJDK grl0k8zs6JmKjHsI5AWlm0WRRSDCBIjvYbxvlDW+lErRj3CmSu1/CEiQ/gdQXIZb5/SebkFwL4i2 uOSsPEWgQAKbaXx0JH55NpWaMlBgLLlHgIAEKQJNKGYKLe1bTyXtXgDnFzOuYolAhAh8R8MjI7Hj eQlRhLpShFQkSEWAGMUQ4aGAvo87SVys/5ii2CHllAeBrwh7fEfD+Bd1LEQe9GLgIkGKQZMKSfHs 9i3HO8dbYZgHoLaQWPIVgQoR+JjEY9NPHPeGFrVWqANlmlaCVCbQlZ7mnLmbxwfm3QzjdVrHVOlu aP4sCAQElhn4xIpXx72Xhb1MEkBAgpSAJuZSwqx5vSP8Absy3OUYwHG5+MpWBMpAYDdgL4D25IpF TV+XYT5NESECEqQINaOcqSxYYG7lF73nerAbDZgNwCvn/JpLBP5FgPjGjM8EA3s6tcXPv8hU1RcJ UlW1e+hiW9vTkwLyKsKu1n55QzPS1ZIQ6AfwGh07uxY2rtRi1pIwjlVQCVKs2lXaZMOnplVf9s5G YNeSdqG2Jiot7yqO/hkMz9bAf1HnEVXxXTBE6RKkIaDoEjCzY9tY8zPzSV4Dw7FiIgIFEtgF8FWY 37ki1fRhgbHknlACEqSENrZ4ZYXHX2ydQeIKGi41YGTxYitSwgkEAFbC7OXa2rrU2y8ftjPh9aq8 AglIkAoEWE3ubW3fHDhwyIg2g3UQvBBAfTXVr1qzJGBYC+AV52UWLl84KZ2ll8xEABIk3QR5ETjz ou2H1B04cB7Jyww4D8CIvALJKQkEDLCPaG4JAre4a/HYb5NQlGooPwEJUvmZJ27G8PDAg+iFr45f YkAbgTGJK1IF7UsgA9gqI5cGe4PXupc0/bCvgb6LQK4EJEi5EpP9sATa28372UufxoAXwHg+aFOH ddBgnAj8BGAZzN4i6pd1pUb9GqfklWv0CUiQot+jWGfYfOkPTTU1nGVw5xhspp6eYtXO8EiHNQSW +84tH+2PXavzhmLVv9glK0GKXcvim3C4zmn1hvRJ8F2rITgL5HTtqxepfvowrIfDKgZ8NwNvZXfq 8F2RylDJJJqABCnR7Y12ceHPezu89FTzeRaIGQBOBzAh2lknKrvdMKwDsdrI9/v/qFn9wZtjfktU hSomVgQkSLFqV/KTbWnvmeCcdxrMzjDwVAAnAzg0+ZWXvMKMARsIfGLAGs+CNXu3T/i8u5uZks+s CUQgSwISpCxByaxSBIwtc348CgimATaNNihQk7Xn3rD92EmzL438FMT6gO7Tg3cHny1dOv73Yb00 KAIVJiBBqnADNH1+BMJ1ULUH+ZO9wJ9ixAk0d7yZHQ3iaAB1+UWNlZcBCF+13mjARho2mLPPXcAN Xanxm2NViZIVgb8ISJB0KySMgHHW3HSTH7hjBp+iDEcYrYmGpsHvxMSY/AQY7oS9zYAewLYQDD83 G7DJkRsz9f2bup8/ck/CmqdyqpyABKnKb4BqLL95/qa62j21Y3xzjQDHGvwxNDfKYA2ObDCzBpg1 GFy9ozWY8QA4Oxhm9QT/efraz75+vxMIxQTh9gUg+mDYS2CXGcKxPwJaH8k+GnfArC8g+hyw3Rx+ 9PZie+DqtmqNTzXemar5T7boKrYfCqI6AAAAAElFTkSuQmCC"/></symbol><symbol viewBox="0 0 24 24" id="webpack" xmlns="http://www.w3.org/2000/svg"><path d="M19.376 15.988l-7.709 4.45-7.708-4.45V7.087l7.708-4.45 7.709 4.45z" fill="#fff" fill-opacity=".785" stroke-width="0"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18.21 0 .41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.939v2.104h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07l7-3.94zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#1c78c0"/></symbol><symbol viewBox="0 0 24 24" id="wolframlanguage" xmlns="http://www.w3.org/2000/svg"><title>wolframLanguage</title><g transform="scale(.12121)" fill="none" fill-rule="evenodd"><circle cx="99.197" cy="98.946" r="83.28" fill="#212121" stroke-width=".841"/><path d="M182.529 98.828a83.406 83.406 0 0 1-39.14 70.721.064.064 0 0 1-.038.019l-28.62-35.665 23.71 2.612s11.385 1.177 13.978 0c2.373-.938 15.175-18.963 15.175-18.963s-36.75-23.23-49.312-36.032c1.434-21.575-1.656-50.269-1.656-50.03-9.251 9.234-10.429 10.669-19.68 19.203-4.028-13.04-5.923-17.547-9.95-30.588-12.104 9.95-21.337 26.799-27.977 46.48a78.68 78.68 0 0 0-4.23 5.094 109.774 109.774 0 0 0-2.667 3.66 114.558 114.558 0 0 0-5.132 8.002 172.555 172.555 0 0 0-3.403 6.051c-7.706 14.475-14.034 31.066-19.515 46.001a.858.858 0 0 1-.092-.184c-14.988-30.912-9.502-67.85 13.822-93.072 23.325-25.223 59.723-33.575 91.71-21.045 31.988 12.53 53.029 43.382 53.017 77.736z" fill="#e53935"/><path d="M101.452 69.178s-1.416-8.295-2.373-11.367c6.401-6.18 7.357-7.118 13.52-13.04.477 11.845.238 18.006-.479 32.481-3.55-3.568-10.668-8.074-10.668-8.074zm-27.737 40.778s-6.64-4.029-11.624-4.728c1.435-3.329 5.223-7.596 6.18-8.773-1.913.699-15.653 6.86-17.087 12.084a74.804 74.804 0 0 1 11.385 3.79 35.993 35.993 0 0 0-8.774 20.158s21.815-3.33 38.185-1.196c.283.168.609.251.938.24l8.534.239 27.111 45.136.221.35c-.037.018-.055.037-.073.037-51.133 18.485-88.085-15.543-95.976-27.443.034-.102.058-.206.074-.313 7.1-30.017 15.855-65.939 30-76.552 7.356-12.82 9.49-31.783 22.751-41.734 3.33 9.951 8.553 30.588 12.103 40.539 15.653 15.652 39.361 35.094 55.234 43.15 1.656.956 3.79 7.596 3.79 7.596l-6.401 8.056-68.276-6.879a54.462 54.462 0 0 0-4.58-.183 86.848 86.848 0 0 0-14.144 1.36c3.311-8.295 10.43-14.935 10.43-14.935zm22.054-8.774c3.789-.46 7.817.956 12.323 3.568 4.267-1.195 4.745-1.434 9.013-2.612-5.463-4.028-11.386-8.295-19.442-7.118a47.249 47.249 0 0 0-1.894 6.162z" fill="#fff" stroke-width=".936"/></g></symbol><symbol viewBox="0 0 24 24" id="word" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M7 13l1.5 7h2l1.5-3 1.5 3h2l1.5-7h1v-2h-4v2h1l-.9 4.2L13 15h-2l-1.1 2.2L9 13h1v-2H6v2h1z" fill="#01579b"/></symbol><symbol viewBox="0 0 24 24" id="xaml" xmlns="http://www.w3.org/2000/svg"><path d="M18.93 12l-3.47 6H8.54l-3.47-6 3.47-6h6.92l3.47 6m4.84 0l-4.04 7L18 18l3.46-6L18 6l1.73-1 4.04 7M.23 12l4.04-7L6 6l-3.46 6L6 18l-1.73 1-4.04-7z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="xml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="yaml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="yang" xmlns="http://www.w3.org/2000/svg"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8 8 8 0 0 0 8 8 4 4 0 0 1-4-4 4 4 0 0 1 4-4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 2.5A1.5 1.5 0 0 1 13.5 8 1.5 1.5 0 0 1 12 9.5 1.5 1.5 0 0 1 10.5 8 1.5 1.5 0 0 1 12 6.5m0 8a1.5 1.5 0 0 0-1.5 1.5 1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5 1.5 1.5 0 0 0-1.5-1.5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 289.99999 290.00001" id="yarn" xmlns="http://www.w3.org/2000/svg"><path d="M250.733 218.418c-12.39 2.943-18.661 5.653-33.993 15.641-24.004 15.487-50.176 22.688-50.176 22.688s-2.168 3.252-8.44 4.723c-10.84 2.633-51.647 4.878-55.364 4.956-9.988.077-16.105-2.555-17.809-6.66-5.188-12.388 7.434-17.809 7.434-17.809s-2.788-1.703-4.414-3.252c-1.471-1.47-3.02-4.413-3.484-3.33-1.936 4.724-2.943 16.261-8.13 21.45-7.125 7.2-20.598 4.8-28.573.619-8.75-4.646.62-15.564.62-15.564s-4.724 2.788-8.518-2.942c-3.407-5.266-6.582-14.248-5.73-25.32 1.084-12.777 15.176-25.011 15.176-25.011s-2.477-18.661 5.653-37.787c7.356-17.422 27.179-31.437 27.179-31.437s-16.648-18.352-10.454-35c4.027-10.84 5.653-10.763 6.97-11.227 4.645-1.781 9.136-3.717 12.466-7.356 16.648-17.964 37.864-14.557 37.864-14.557s9.911-30.431 19.203-24.469c2.865 1.859 13.163 24.778 13.163 24.778s10.996-6.426 12.235-4.026c6.659 12.931 7.433 37.632 4.49 52.654-4.955 24.778-17.344 38.096-22.3 46.459-1.161 1.936 13.319 8.053 22.456 33.373 8.44 23.152.929 42.587 2.245 44.756.232.387.31.542.31.542s9.679.774 29.114-11.228c10.376-6.427 22.688-13.628 36.703-13.783 13.55-.232 14.247 15.719 4.104 18.12z" fill="#2c8ebb" stroke-width=".774"/></symbol><symbol viewBox="0 0 24 24" id="zip" xmlns="http://www.w3.org/2000/svg"><path d="M14 17h-2v-2h-2v-2h2v2h2m0-6h-2v2h2v2h-2v-2h-2V9h2V7h-2V5h2v2h2m5-4H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#afb42b"/></symbol></svg>
diff --git a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js
index b4803be4d52..f89533aeb1d 100644
--- a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js
+++ b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js
@@ -1,8 +1,7 @@
import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer';
export default () => {
- new PayloadPreviewer(
- document.querySelector('.js-usage-ping-payload-trigger'),
- document.querySelector('.js-usage-ping-payload'),
- ).init();
+ Array.from(document.querySelectorAll('.js-payload-preview-trigger')).forEach(trigger => {
+ new PayloadPreviewer(trigger).init();
+ });
};
diff --git a/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue
new file mode 100644
index 00000000000..2ea55d44420
--- /dev/null
+++ b/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlEmptyState,
+ GlSprintf,
+ GlLink,
+ },
+ inject: {
+ svgPath: {
+ type: String,
+ },
+ docsLink: {
+ type: String,
+ },
+ primaryButtonPath: {
+ type: String,
+ },
+ },
+};
+</script>
+<template>
+ <gl-empty-state
+ class="js-empty-state"
+ :title="__('Activate user activity analysis')"
+ :svg-path="svgPath"
+ :primary-button-text="__('Turn on usage ping')"
+ :primary-button-link="primaryButtonPath"
+ >
+ <template #description>
+ <gl-sprintf
+ :message="
+ __(
+ 'Turn on %{strongStart}usage ping%{strongEnd} to activate analysis of user activity, known as %{docLinkStart}Cohorts%{docLinkEnd}.',
+ )
+ "
+ >
+ <template #docLink="{content}">
+ <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ <template #strong="{ content }"
+ ><strong>{{ content }}</strong></template
+ >
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue
new file mode 100644
index 00000000000..5429ec403d3
--- /dev/null
+++ b/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlEmptyState,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ inject: {
+ isAdmin: {
+ type: Boolean,
+ },
+ svgPath: {
+ type: String,
+ },
+ docsLink: {
+ type: String,
+ },
+ primaryButtonPath: {
+ type: String,
+ },
+ },
+};
+</script>
+<template>
+ <gl-empty-state class="js-empty-state" :title="__('Usage ping is off')" :svg-path="svgPath">
+ <template #description>
+ <gl-sprintf
+ v-if="!isAdmin"
+ :message="
+ __(
+ 'To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}.',
+ )
+ "
+ >
+ <template #docLink="{content}">
+ <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ <template v-else
+ ><p>
+ {{ __('Turn on usage ping to review instance-level analytics.') }}
+ </p>
+
+ <gl-button category="primary" variant="success" :href="primaryButtonPath">
+ {{ __('Turn on usage ping') }}</gl-button
+ >
+ </template>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/admin/statistics_panel/store/getters.js b/app/assets/javascripts/admin/statistics_panel/store/getters.js
index 2aa34b8f38e..1c1868b5bca 100644
--- a/app/assets/javascripts/admin/statistics_panel/store/getters.js
+++ b/app/assets/javascripts/admin/statistics_panel/store/getters.js
@@ -3,7 +3,6 @@
* and returns an array of the following form:
* [{ key: "forks", label: "Forks", value: 50 }]
*/
-// eslint-disable-next-line import/prefer-default-export
export const getStatistics = state => labels =>
Object.keys(labels).map(key => {
const result = {
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
deleted file mode 100644
index 54e86f329e4..00000000000
--- a/app/assets/javascripts/ajax_loading_spinner.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'jquery';
-
-export default class AjaxLoadingSpinner {
- static init() {
- const $elements = $('.js-ajax-loading-spinner');
-
- $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
- $elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
- }
-
- static ajaxBeforeSend(e) {
- e.target.setAttribute('disabled', '');
- const iconElement = e.target.querySelector('i');
- // get first fa- icon
- const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g)[0];
- iconElement.dataset.icon = originalIcon;
- AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
- $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
- }
-
- static ajaxComplete(e) {
- e.target.removeAttribute('disabled');
- const iconElement = e.target.querySelector('i');
- AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
- $(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
- }
-
- static toggleLoadingIcon(iconElement) {
- const { classList } = iconElement;
- classList.toggle(iconElement.dataset.icon);
- classList.toggle('fa-spinner');
- classList.toggle('fa-spin');
- }
-}
diff --git a/app/assets/javascripts/alert_handler.js b/app/assets/javascripts/alert_handler.js
new file mode 100644
index 00000000000..8fffb61d1dd
--- /dev/null
+++ b/app/assets/javascripts/alert_handler.js
@@ -0,0 +1,13 @@
+// This allows us to dismiss alerts that we've migrated from bootstrap
+// Note: This ONLY works on alerts that are created on page load
+// You can follow this effort in the following epic
+// https://gitlab.com/groups/gitlab-org/-/epics/4070
+
+export default function initAlertHandler() {
+ const ALERT_SELECTOR = '.gl-alert';
+ const CLOSE_SELECTOR = '.gl-alert-dismiss';
+
+ const dismissAlert = ({ target }) => target.closest(ALERT_SELECTOR).remove();
+ const closeButtons = document.querySelectorAll(`${ALERT_SELECTOR} ${CLOSE_SELECTOR}`);
+ closeButtons.forEach(alert => alert.addEventListener('click', dismissAlert));
+}
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index 5d260fcc200..c6605452616 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import * as Sentry from '@sentry/browser';
import {
GlAlert,
@@ -9,7 +10,6 @@ import {
GlTabs,
GlTab,
GlButton,
- GlTable,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import alertQuery from '../graphql/queries/details.query.graphql';
@@ -27,6 +27,7 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue';
import AlertMetrics from './alert_metrics.vue';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar');
@@ -42,18 +43,19 @@ export default {
tabsConfig: [
{
id: 'overview',
- title: s__('AlertManagement|Overview'),
- },
- {
- id: 'fullDetails',
title: s__('AlertManagement|Alert details'),
},
{
id: 'metrics',
title: s__('AlertManagement|Metrics'),
},
+ {
+ id: 'activity',
+ title: s__('AlertManagement|Activity feed'),
+ },
],
components: {
+ AlertDetailsTable,
GlBadge,
GlAlert,
GlIcon,
@@ -62,7 +64,6 @@ export default {
GlTab,
GlTabs,
GlButton,
- GlTable,
TimeAgoTooltip,
AlertSidebar,
SystemNote,
@@ -330,32 +331,17 @@ export default {
</div>
<div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div>
</div>
- <template>
- <div v-if="alert.notes.nodes" class="issuable-discussion py-5">
- <ul class="notes main-notes-list timeline">
- <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
- </ul>
- </div>
- </template>
+ <alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
- <gl-table
- class="alert-management-details-table"
- :items="[{ key: 'Value', ...alert }]"
- :show-empty="true"
- :busy="loading"
- stacked
- >
- <template #empty>
- {{ s__('AlertManagement|No alert data to display.') }}
- </template>
- <template #table-busy>
- <gl-loading-icon size="lg" color="dark" class="mt-3" />
- </template>
- </gl-table>
+ <alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
- <alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
+ <div v-if="alert.notes.nodes.length > 0" class="issuable-discussion">
+ <ul class="notes main-notes-list timeline">
+ <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
+ </ul>
+ </div>
</gl-tab>
</gl-tabs>
<alert-sidebar
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 92fd85c6217..0fd00fe90eb 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -1,8 +1,12 @@
<script>
+/* eslint-disable vue/no-v-html */
import {
GlLoadingIcon,
GlTable,
GlAlert,
+ GlAvatarsInline,
+ GlAvatarLink,
+ GlAvatar,
GlIcon,
GlLink,
GlTabs,
@@ -11,6 +15,7 @@ import {
GlPagination,
GlSearchBoxByType,
GlSprintf,
+ GlTooltipDirective,
} from '@gitlab/ui';
import { debounce, trim } from 'lodash';
import { __, s__ } from '~/locale';
@@ -35,6 +40,7 @@ const tdClass =
const thClass = 'gl-hover-bg-blue-50';
const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-bg-blue-50 gl-hover-cursor-pointer gl-hover-border-b-solid gl-hover-border-blue-200';
+const TH_TEST_ID = { 'data-testid': 'alert-management-severity-sort' };
const initialPaginationState = {
currentPage: 1,
@@ -55,12 +61,14 @@ export default {
"AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear.",
),
searchPlaceholder: __('Search or filter results...'),
+ unassigned: __('Unassigned'),
},
fields: [
{
key: 'severity',
label: s__('AlertManagement|Severity'),
thClass: `${thClass} gl-w-eighth`,
+ thAttr: TH_TEST_ID,
tdClass: `${tdClass} rounded-top text-capitalize sortable-cell`,
sortable: true,
},
@@ -72,7 +80,7 @@ export default {
sortable: true,
},
{
- key: 'title',
+ key: 'alertLabel',
label: s__('AlertManagement|Alert'),
thClass: `gl-pointer-events-none`,
tdClass,
@@ -110,6 +118,9 @@ export default {
GlLoadingIcon,
GlTable,
GlAlert,
+ GlAvatarsInline,
+ GlAvatarLink,
+ GlAvatar,
TimeAgo,
GlIcon,
GlLink,
@@ -121,6 +132,9 @@ export default {
GlSprintf,
AlertStatus,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
projectPath: {
type: String,
@@ -264,11 +278,8 @@ export default {
const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
- getAssignees(assignees) {
- // TODO: Update to show list of assignee(s) after https://gitlab.com/gitlab-org/gitlab/-/issues/218405
- return assignees.nodes?.length > 0
- ? assignees.nodes[0]?.username
- : s__('AlertManagement|Unassigned');
+ hasAssignees(assignees) {
+ return Boolean(assignees.nodes?.length);
},
getIssueLink(item) {
return joinPaths('/', this.projectPath, '-', 'issues', item.issueIid);
@@ -397,8 +408,14 @@ export default {
{{ item.eventCount }}
</template>
- <template #cell(title)="{ item }">
- <div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
+ <template #cell(alertLabel)="{ item }">
+ <div
+ class="gl-max-w-full text-truncate"
+ :title="`${item.iid} - ${item.title}`"
+ data-testid="idField"
+ >
+ #{{ item.iid }} {{ item.title }}
+ </div>
</template>
<template #cell(issue)="{ item }">
@@ -409,8 +426,32 @@ export default {
</template>
<template #cell(assignees)="{ item }">
- <div class="gl-max-w-full text-truncate" data-testid="assigneesField">
- {{ getAssignees(item.assignees) }}
+ <div data-testid="assigneesField">
+ <template v-if="hasAssignees(item.assignees)">
+ <gl-avatars-inline
+ :avatars="item.assignees.nodes"
+ :collapsed="true"
+ :max-visible="4"
+ :avatar-size="24"
+ badge-tooltip-prop="name"
+ :badge-tooltip-max-chars="100"
+ >
+ <template #avatar="{ avatar }">
+ <gl-avatar-link
+ :key="avatar.username"
+ v-gl-tooltip
+ target="_blank"
+ :href="avatar.webUrl"
+ :title="avatar.name"
+ >
+ <gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="24" />
+ </gl-avatar-link>
+ </template>
+ </gl-avatars-inline>
+ </template>
+ <template v-else>
+ {{ $options.i18n.unassigned }}
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/components/alert_status.vue b/app/assets/javascripts/alert_management/components/alert_status.vue
index 8531ca1374e..ff71b348cc9 100644
--- a/app/assets/javascripts/alert_management/components/alert_status.vue
+++ b/app/assets/javascripts/alert_management/components/alert_status.vue
@@ -101,12 +101,12 @@ export default {
@keydown.esc.native="$emit('hide-dropdown')"
@hide="$emit('hide-dropdown')"
>
- <div v-if="isSidebar" class="dropdown-title text-center">
- <span class="alert-title">{{ s__('AlertManagement|Assign status') }}</span>
+ <div v-if="isSidebar" class="dropdown-title gl-display-flex">
+ <span class="alert-title gl-ml-auto">{{ s__('AlertManagement|Assign status') }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
- class="dropdown-title-button dropdown-menu-close"
+ class="dropdown-title-button dropdown-menu-close gl-ml-auto gl-text-black-normal!"
icon="close"
@click="$emit('hide-dropdown')"
/>
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
index 4af5c83b30c..0f354e85e96 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
@@ -12,7 +12,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
-import { s__, __ } from '~/locale';
+import { s__ } from '~/locale';
import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.mutation.graphql';
import SidebarAssignee from './sidebar_assignee.vue';
@@ -82,8 +82,11 @@ export default {
userName() {
return this.alert?.assignees?.nodes[0]?.username;
},
- assignedUser() {
- return this.userName || __('None');
+ userFullName() {
+ return this.alert?.assignees?.nodes[0]?.name;
+ },
+ userImg() {
+ return this.alert?.assignees?.nodes[0]?.avatarUrl;
},
sortedUsers() {
return this.users
@@ -184,15 +187,15 @@ export default {
</script>
<template>
- <div class="block alert-status">
- <div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
+ <div class="block alert-assignees ">
+ <div ref="assignees" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
<gl-icon name="user" :size="14" />
<gl-loading-icon v-if="isUpdating" />
</div>
- <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
+ <gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left">
<gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK">
<template #assignees>
- {{ assignedUser }}
+ {{ userName }}
</template>
</gl-sprintf>
</gl-tooltip>
@@ -215,19 +218,19 @@ export default {
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
<gl-deprecated-dropdown
ref="dropdown"
- :text="assignedUser"
+ :text="userName"
class="w-100"
toggle-class="dropdown-menu-toggle"
variant="outline-default"
@keydown.esc.native="hideDropdown"
@hide="hideDropdown"
>
- <div class="dropdown-title">
- <span class="alert-title">{{ __('Assign To') }}</span>
+ <div class="dropdown-title gl-display-flex">
+ <span class="alert-title gl-ml-auto">{{ __('Assign To') }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
- class="dropdown-title-button dropdown-menu-close"
+ class="dropdown-title-button dropdown-menu-close gl-ml-auto gl-text-black-normal!"
icon="close"
@click="hideDropdown"
/>
@@ -272,14 +275,28 @@ export default {
</div>
<gl-loading-icon v-if="isUpdating" :inline="true" />
- <p v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
- <span v-if="userName" class="gl-text-gray-500" data-testid="assigned-users">{{
- assignedUser
- }}</span>
- <span v-else class="gl-display-flex gl-align-items-center">
+ <div v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
+ <div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
+ <span class="gl-relative mr-2">
+ <img
+ :alt="userName"
+ :src="userImg"
+ :width="32"
+ class="avatar avatar-inline gl-m-0 s32"
+ data-qa-selector="avatar_image"
+ />
+ </span>
+ <span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
+ <strong class="dropdown-menu-user-full-name">
+ {{ userFullName }}
+ </strong>
+ <span class="dropdown-menu-user-username">{{ userName }}</span>
+ </span>
+ </div>
+ <span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
{{ __('None') }} -
<gl-button
- class="gl-pl-2"
+ class="gl-ml-2"
href="#"
variant="link"
data-testid="unassigned-users"
@@ -288,7 +305,7 @@ export default {
{{ __('assign yourself') }}
</gl-button>
</span>
- </p>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
index 5bd69a1f0ec..84d54466a10 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
@@ -1,8 +1,9 @@
<script>
+import produce from 'immer';
import { s__ } from '~/locale';
import Todo from '~/sidebar/components/todo_toggle/todo.vue';
-import createAlertTodo from '../../graphql/mutations/alert_todo_create.mutation.graphql';
-import todoMarkDone from '../../graphql/mutations/alert_todo_mark_done.mutation.graphql';
+import createAlertTodoMutation from '../../graphql/mutations/alert_todo_create.mutation.graphql';
+import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
import alertQuery from '../../graphql/queries/details.query.graphql';
export default {
@@ -52,7 +53,7 @@ export default {
},
methods: {
updateToDoCount(add) {
- const oldCount = parseInt(document.querySelector('.todos-count').innerText, 10);
+ const oldCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
const count = add ? oldCount + 1 : oldCount - 1;
const headerTodoEvent = new CustomEvent('todo:toggle', {
detail: {
@@ -66,7 +67,7 @@ export default {
this.isUpdating = true;
return this.$apollo
.mutate({
- mutation: createAlertTodo,
+ mutation: createAlertTodoMutation,
variables: {
iid: this.alert.iid,
projectPath: this.projectPath,
@@ -89,7 +90,7 @@ export default {
this.isUpdating = true;
return this.$apollo
.mutate({
- mutation: todoMarkDone,
+ mutation: todoMarkDoneMutation,
variables: {
id: this.firstToDoId,
},
@@ -109,12 +110,15 @@ export default {
});
},
updateCache(store) {
- const data = store.readQuery({
+ const sourceData = store.readQuery({
query: alertQuery,
variables: this.getAlertQueryVariables,
});
- data.project.alertManagementAlerts.nodes[0].todos.nodes.shift();
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.alertManagementAlerts.nodes[0].todos.nodes = [];
+ });
store.writeQuery({
query: alertQuery,
diff --git a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
index 39717ab609f..0b206ce42f4 100644
--- a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import NoteHeader from '~/notes/components/note_header.vue';
import { spriteIcon } from '~/lib/utils/common_utils';
@@ -31,7 +32,7 @@ export default {
</script>
<template>
- <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper">
+ <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper gl-px-0!">
<div class="timeline-entry-inner">
<div class="timeline-icon" v-html="iconHtml"></div>
<div class="timeline-content">
diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js
index b9670466c0f..73cb5ecdf98 100644
--- a/app/assets/javascripts/alert_management/constants.js
+++ b/app/assets/javascripts/alert_management/constants.js
@@ -64,4 +64,4 @@ export const trackAlertStatusUpdateOptions = {
label: 'Status',
};
-export const DEFAULT_PAGE_SIZE = 10;
+export const DEFAULT_PAGE_SIZE = 20;
diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/alert_management/details.js
index dccf990f0b4..c2020dfcbe3 100644
--- a/app/assets/javascripts/alert_management/details.js
+++ b/app/assets/javascripts/alert_management/details.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import produce from 'immer';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import createRouter from './router';
@@ -16,8 +17,11 @@ export default selector => {
const resolvers = {
Mutation: {
toggleSidebarStatus: (_, __, { cache }) => {
- const data = cache.readQuery({ query: sidebarStatusQuery });
- data.sidebarStatus = !data.sidebarStatus;
+ const sourceData = cache.readQuery({ query: sidebarStatusQuery });
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.sidebarStatus = !draftData.sidebarStatus;
+ });
cache.writeQuery({ query: sidebarStatusQuery, data });
},
},
@@ -34,6 +38,7 @@ export default selector => {
return defaultDataIdFromObject(object);
},
},
+ assumeImmutableResults: true,
}),
});
diff --git a/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql
index c37f29c74fc..62119177887 100644
--- a/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql
+++ b/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql
@@ -8,7 +8,10 @@ fragment AlertListItem on AlertManagementAlert {
issueIid
assignees {
nodes {
+ name
username
+ avatarUrl
+ webUrl
}
}
}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql
index 40b4b6ae854..5008bfa5e1b 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql
+++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql
@@ -10,6 +10,9 @@ mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $i
assignees {
nodes {
username
+ name
+ avatarUrl
+ webUrl
}
}
notes {
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1d8fb1fc5a6..dbc7ff67d9d 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -20,6 +20,7 @@ const Api = {
projectPath: '/api/:version/projects/:id',
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
+ projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename',
projectUsersPath: '/api/:version/projects/:id/users',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
@@ -42,7 +43,6 @@ const Api = {
userPostStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits/:sha',
commitsPath: '/api/:version/projects/:id/repository/commits',
-
applySuggestionPath: '/api/:version/suggestions/:id/apply',
applySuggestionBatchPath: '/api/:version/suggestions/batch_apply',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
@@ -309,10 +309,12 @@ const Api = {
});
},
- projectMilestones(id) {
+ projectMilestones(id, params = {}) {
const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id));
- return axios.get(url);
+ return axios.get(url, {
+ params,
+ });
},
mergeRequests(params = {}) {
diff --git a/app/assets/javascripts/authentication/mount_2fa.js b/app/assets/javascripts/authentication/mount_2fa.js
index 9917151ac81..dd5a42fa5fc 100644
--- a/app/assets/javascripts/authentication/mount_2fa.js
+++ b/app/assets/javascripts/authentication/mount_2fa.js
@@ -1,14 +1,23 @@
import $ from 'jquery';
import initU2F from './u2f';
+import initWebauthn from './webauthn';
import U2FRegister from './u2f/register';
+import WebAuthnRegister from './webauthn/register';
export const mount2faAuthentication = () => {
- // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
- initU2F();
+ if (gon.webauthn) {
+ initWebauthn();
+ } else {
+ initU2F();
+ }
};
export const mount2faRegistration = () => {
- // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
- const u2fRegister = new U2FRegister($('#js-register-u2f'), gon.u2f);
- u2fRegister.start();
+ if (gon.webauthn) {
+ const webauthnRegister = new WebAuthnRegister($('#js-register-token-2fa'), gon.webauthn);
+ webauthnRegister.start();
+ } else {
+ const u2fRegister = new U2FRegister($('#js-register-token-2fa'), gon.u2f);
+ u2fRegister.start();
+ }
};
diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js
index 201cd5c2e61..f9b5ca3e5b4 100644
--- a/app/assets/javascripts/authentication/u2f/authenticate.js
+++ b/app/assets/javascripts/authentication/u2f/authenticate.js
@@ -40,7 +40,6 @@ export default class U2FAuthenticate {
this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge'));
this.templates = {
- setup: '#js-authenticate-token-2fa-setup',
inProgress: '#js-authenticate-token-2fa-in-progress',
error: '#js-authenticate-token-2fa-error',
authenticated: '#js-authenticate-token-2fa-authenticated',
@@ -86,7 +85,7 @@ export default class U2FAuthenticate {
renderError(error) {
this.renderTemplate('error', {
error_message: error.message(),
- error_code: error.errorCode,
+ error_name: error.errorCode,
});
return this.container.find('#js-token-2fa-try-again').on('click', this.renderInProgress);
}
diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js
index 52c0ce1fc04..9773a9185f8 100644
--- a/app/assets/javascripts/authentication/u2f/register.js
+++ b/app/assets/javascripts/authentication/u2f/register.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { template as lodashTemplate } from 'lodash';
+import { __ } from '~/locale';
import importU2FLibrary from './util';
import U2FError from './error';
@@ -24,11 +25,10 @@ export default class U2FRegister {
this.signRequests = u2fParams.sign_requests;
this.templates = {
- notSupported: '#js-register-u2f-not-supported',
- setup: '#js-register-u2f-setup',
- inProgress: '#js-register-u2f-in-progress',
- error: '#js-register-u2f-error',
- registered: '#js-register-u2f-registered',
+ message: '#js-register-2fa-message',
+ setup: '#js-register-token-2fa-setup',
+ error: '#js-register-token-2fa-error',
+ registered: '#js-register-token-2fa-registered',
};
}
@@ -65,18 +65,22 @@ export default class U2FRegister {
renderSetup() {
this.renderTemplate('setup');
- return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
+ return this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
}
renderInProgress() {
- this.renderTemplate('inProgress');
+ this.renderTemplate('message', {
+ message: __(
+ 'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
+ ),
+ });
return this.register();
}
renderError(error) {
this.renderTemplate('error', {
error_message: error.message(),
- error_code: error.errorCode,
+ error_name: error.errorCode,
});
return this.container.find('#js-token-2fa-try-again').on('click', this.renderSetup);
}
@@ -89,6 +93,10 @@ export default class U2FRegister {
}
renderNotSupported() {
- return this.renderTemplate('notSupported');
+ return this.renderTemplate('message', {
+ message: __(
+ "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).",
+ ),
+ });
}
}
diff --git a/app/assets/javascripts/authentication/webauthn/authenticate.js b/app/assets/javascripts/authentication/webauthn/authenticate.js
new file mode 100644
index 00000000000..42c4c2b63bd
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/authenticate.js
@@ -0,0 +1,69 @@
+import WebAuthnError from './error';
+import WebAuthnFlow from './flow';
+import { supported, convertGetParams, convertGetResponse } from './util';
+
+// Authenticate WebAuthn devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> authenticated -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
+export default class WebAuthnAuthenticate {
+ constructor(container, form, webauthnParams, fallbackButton, fallbackUI) {
+ this.container = container;
+ this.webauthnParams = convertGetParams(JSON.parse(webauthnParams.options));
+ this.renderInProgress = this.renderInProgress.bind(this);
+
+ this.form = form;
+ this.fallbackButton = fallbackButton;
+ this.fallbackUI = fallbackUI;
+ if (this.fallbackButton) {
+ this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
+ }
+
+ this.flow = new WebAuthnFlow(container, {
+ inProgress: '#js-authenticate-token-2fa-in-progress',
+ error: '#js-authenticate-token-2fa-error',
+ authenticated: '#js-authenticate-token-2fa-authenticated',
+ });
+
+ this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress);
+ }
+
+ start() {
+ if (!supported()) {
+ this.switchToFallbackUI();
+ } else {
+ this.renderInProgress();
+ }
+ }
+
+ authenticate() {
+ navigator.credentials
+ .get({ publicKey: this.webauthnParams })
+ .then(resp => {
+ const convertedResponse = convertGetResponse(resp);
+ this.renderAuthenticated(JSON.stringify(convertedResponse));
+ })
+ .catch(err => {
+ this.flow.renderError(new WebAuthnError(err, 'authenticate'));
+ });
+ }
+
+ renderInProgress() {
+ this.flow.renderTemplate('inProgress');
+ this.authenticate();
+ }
+
+ renderAuthenticated(deviceResponse) {
+ this.flow.renderTemplate('authenticated');
+ const container = this.container[0];
+ container.querySelector('#js-device-response').value = deviceResponse;
+ container.querySelector(this.form).submit();
+ this.fallbackButton.classList.add('hidden');
+ }
+
+ switchToFallbackUI() {
+ this.fallbackButton.classList.add('hidden');
+ this.container[0].classList.add('hidden');
+ this.fallbackUI.classList.remove('hidden');
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/error.js b/app/assets/javascripts/authentication/webauthn/error.js
new file mode 100644
index 00000000000..a1a3f861c25
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/error.js
@@ -0,0 +1,28 @@
+import { __ } from '~/locale';
+import { isHTTPS, FLOW_AUTHENTICATE, FLOW_REGISTER } from './util';
+
+export default class WebAuthnError {
+ constructor(error, flowType) {
+ this.error = error;
+ this.errorName = error.name || 'UnknownError';
+ this.message = this.message.bind(this);
+ this.httpsDisabled = !isHTTPS();
+ this.flowType = flowType;
+ }
+
+ message() {
+ if (this.errorName === 'NotSupportedError') {
+ return __('Your device is not compatible with GitLab. Please try another device');
+ } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_AUTHENTICATE) {
+ return __('This device has not been registered with us.');
+ } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_REGISTER) {
+ return __('This device has already been registered with us.');
+ } else if (this.errorName === 'SecurityError' && this.httpsDisabled) {
+ return __(
+ 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
+ );
+ }
+
+ return __('There was a problem communicating with your device.');
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/flow.js b/app/assets/javascripts/authentication/webauthn/flow.js
new file mode 100644
index 00000000000..10a1debc876
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/flow.js
@@ -0,0 +1,24 @@
+import { template } from 'lodash';
+
+/**
+ * Generic abstraction for WebAuthnFlows, especially for register / authenticate
+ */
+export default class WebAuthnFlow {
+ constructor(container, templates) {
+ this.container = container;
+ this.templates = templates;
+ }
+
+ renderTemplate(name, params) {
+ const templateString = document.querySelector(this.templates[name]).innerHTML;
+ const compiledTemplate = template(templateString);
+ this.container.html(compiledTemplate(params));
+ }
+
+ renderError(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_name: error.errorName,
+ });
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/index.js b/app/assets/javascripts/authentication/webauthn/index.js
new file mode 100644
index 00000000000..bbf694c7698
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/index.js
@@ -0,0 +1,13 @@
+import $ from 'jquery';
+import WebAuthnAuthenticate from './authenticate';
+
+export default () => {
+ const webauthnAuthenticate = new WebAuthnAuthenticate(
+ $('#js-authenticate-token-2fa'),
+ '#js-login-token-2fa-form',
+ gon.webauthn,
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form'),
+ );
+ webauthnAuthenticate.start();
+};
diff --git a/app/assets/javascripts/authentication/webauthn/register.js b/app/assets/javascripts/authentication/webauthn/register.js
new file mode 100644
index 00000000000..06e4ffd6f3e
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/register.js
@@ -0,0 +1,78 @@
+import { __ } from '~/locale';
+import WebAuthnError from './error';
+import WebAuthnFlow from './flow';
+import { supported, isHTTPS, convertCreateParams, convertCreateResponse } from './util';
+
+// Register WebAuthn devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> registered -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
+export default class WebAuthnRegister {
+ constructor(container, webauthnParams) {
+ this.container = container;
+ this.renderInProgress = this.renderInProgress.bind(this);
+ this.webauthnOptions = convertCreateParams(webauthnParams.options);
+
+ this.flow = new WebAuthnFlow(container, {
+ message: '#js-register-2fa-message',
+ setup: '#js-register-token-2fa-setup',
+ error: '#js-register-token-2fa-error',
+ registered: '#js-register-token-2fa-registered',
+ });
+
+ this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress);
+ }
+
+ start() {
+ if (!supported()) {
+ // we show a special error message when the user visits the site
+ // using a non-ssl connection as this makes WebAuthn unavailable in
+ // any case, regardless of the used browser
+ this.renderNotSupported(!isHTTPS());
+ } else {
+ this.renderSetup();
+ }
+ }
+
+ register() {
+ navigator.credentials
+ .create({
+ publicKey: this.webauthnOptions,
+ })
+ .then(cred => this.renderRegistered(JSON.stringify(convertCreateResponse(cred))))
+ .catch(err => this.flow.renderError(new WebAuthnError(err, 'register')));
+ }
+
+ renderSetup() {
+ this.flow.renderTemplate('setup');
+ this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
+ }
+
+ renderInProgress() {
+ this.flow.renderTemplate('message', {
+ message: __(
+ 'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
+ ),
+ });
+ return this.register();
+ }
+
+ renderRegistered(deviceResponse) {
+ this.flow.renderTemplate('registered');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
+ this.container.find('#js-device-response').val(deviceResponse);
+ }
+
+ renderNotSupported(noHttps) {
+ const message = noHttps
+ ? __(
+ 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
+ )
+ : __(
+ "Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+).",
+ );
+
+ this.flow.renderTemplate('message', { message });
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/util.js b/app/assets/javascripts/authentication/webauthn/util.js
new file mode 100644
index 00000000000..5f06c000afe
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/util.js
@@ -0,0 +1,120 @@
+export function supported() {
+ return Boolean(
+ navigator.credentials &&
+ navigator.credentials.create &&
+ navigator.credentials.get &&
+ window.PublicKeyCredential,
+ );
+}
+
+export function isHTTPS() {
+ return window.location.protocol.startsWith('https');
+}
+
+export const FLOW_AUTHENTICATE = 'authenticate';
+export const FLOW_REGISTER = 'register';
+
+// adapted from https://stackoverflow.com/a/21797381/8204697
+function base64ToBuffer(base64) {
+ const binaryString = window.atob(base64);
+ const len = binaryString.length;
+ const bytes = new Uint8Array(len);
+ for (let i = 0; i < len; i += 1) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes.buffer;
+}
+
+// adapted from https://stackoverflow.com/a/9458996/8204697
+function bufferToBase64(buffer) {
+ if (typeof buffer === 'string') {
+ return buffer;
+ }
+
+ let binary = '';
+ const bytes = new Uint8Array(buffer);
+ const len = bytes.byteLength;
+ for (let i = 0; i < len; i += 1) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return window.btoa(binary);
+}
+
+/**
+ * Returns a copy of the given object with the id property converted to buffer
+ *
+ * @param {Object} param
+ */
+function convertIdToBuffer({ id, ...rest }) {
+ return {
+ ...rest,
+ id: base64ToBuffer(id),
+ };
+}
+
+/**
+ * Returns a copy of the given array with all `id`s of the items converted to buffer
+ *
+ * @param {Array} items
+ */
+function convertIdsToBuffer(items) {
+ return items.map(convertIdToBuffer);
+}
+
+/**
+ * Returns an object with keys of the given props, and values from the given object converted to base64
+ *
+ * @param {String} obj
+ * @param {Array} props
+ */
+function convertPropertiesToBase64(obj, props) {
+ return props.reduce(
+ (acc, property) => Object.assign(acc, { [property]: bufferToBase64(obj[property]) }),
+ {},
+ );
+}
+
+export function convertGetParams({ allowCredentials, challenge, ...rest }) {
+ return {
+ ...rest,
+ ...(allowCredentials ? { allowCredentials: convertIdsToBuffer(allowCredentials) } : {}),
+ challenge: base64ToBuffer(challenge),
+ };
+}
+
+export function convertGetResponse(webauthnResponse) {
+ return {
+ type: webauthnResponse.type,
+ id: webauthnResponse.id,
+ rawId: bufferToBase64(webauthnResponse.rawId),
+ response: convertPropertiesToBase64(webauthnResponse.response, [
+ 'clientDataJSON',
+ 'authenticatorData',
+ 'signature',
+ 'userHandle',
+ ]),
+ clientExtensionResults: webauthnResponse.getClientExtensionResults(),
+ };
+}
+
+export function convertCreateParams({ challenge, user, excludeCredentials, ...rest }) {
+ return {
+ ...rest,
+ challenge: base64ToBuffer(challenge),
+ user: convertIdToBuffer(user),
+ ...(excludeCredentials ? { excludeCredentials: convertIdsToBuffer(excludeCredentials) } : {}),
+ };
+}
+
+export function convertCreateResponse(webauthnResponse) {
+ return {
+ type: webauthnResponse.type,
+ id: webauthnResponse.id,
+ rawId: bufferToBase64(webauthnResponse.rawId),
+ clientExtensionResults: webauthnResponse.getClientExtensionResults(),
+ response: convertPropertiesToBase64(webauthnResponse.response, [
+ 'clientDataJSON',
+ 'attestationObject',
+ ]),
+ };
+}
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 3242993b06a..3dd05f73841 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -1,13 +1,12 @@
<script>
-import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
// name: 'Badge' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Badge',
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
directives: {
@@ -84,7 +83,7 @@ export default {
<div v-show="hasError" class="btn-group">
<div class="btn btn-default btn-sm disabled">
- <icon :size="16" class="gl-ml-3 gl-mr-3" name="doc-image" aria-hidden="true" />
+ <gl-icon :size="16" class="gl-ml-3 gl-mr-3" name="doc-image" aria-hidden="true" />
</div>
<div class="btn btn-default btn-sm disabled">
<span class="gl-ml-3 gl-mr-3">{{ s__('Badges|No badge image') }}</span>
@@ -99,7 +98,7 @@ export default {
type="button"
@click="reloadImage"
>
- <icon :size="16" name="retry" />
+ <gl-icon :size="16" name="retry" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 08834df0a9b..6afb10dd2ad 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -1,10 +1,10 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape, debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import { GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
@@ -14,7 +14,7 @@ export default {
name: 'BadgeForm',
components: {
Badge,
- LoadingButton,
+ GlButton,
GlLoadingIcon,
GlFormInput,
GlFormGroup,
@@ -219,23 +219,23 @@ export default {
</div>
<div v-if="isEditing" class="row-content-block gl-display-flex gl-justify-content-end">
- <button class="btn btn-cancel gl-mr-4" type="button" @click="onCancel">
+ <gl-button class="btn-cancel gl-mr-4" data-testid="cancelEditing" @click="onCancel">
{{ __('Cancel') }}
- </button>
- <loading-button
+ </gl-button>
+ <gl-button
:loading="isSaving"
- :label="s__('Badges|Save changes')"
type="submit"
- container-class="btn btn-success"
- />
+ variant="success"
+ category="primary"
+ data-testid="saveEditing"
+ >
+ {{ s__('Badges|Save changes') }}
+ </gl-button>
</div>
<div v-else class="gl-display-flex gl-justify-content-end form-group">
- <loading-button
- :loading="isSaving"
- :label="s__('Badges|Add badge')"
- type="submit"
- container-class="btn btn-success"
- />
+ <gl-button :loading="isSaving" type="submit" variant="success" category="primary">
+ {{ s__('Badges|Add badge') }}
+ </gl-button>
</div>
</form>
</template>
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index bad14666bb2..3343634ecad 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -1,8 +1,7 @@
<script>
import { mapActions, mapState } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
@@ -10,7 +9,7 @@ export default {
name: 'BadgeListRow',
components: {
Badge,
- Icon,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -58,7 +57,7 @@ export default {
type="button"
@click="editBadge(badge)"
>
- <icon :size="16" :aria-label="__('Edit')" name="pencil" />
+ <gl-icon :size="16" :aria-label="__('Edit')" name="pencil" />
</button>
<button
:disabled="badge.isDeleting"
@@ -68,7 +67,7 @@ export default {
data-target="#delete-badge-modal"
@click="updateBadgeInModal(badge)"
>
- <icon :size="16" :aria-label="__('Delete')" name="remove" />
+ <gl-icon :size="16" :aria-label="__('Delete')" name="remove" />
</button>
<gl-loading-icon v-show="badge.isDeleting" :inline="true" />
</div>
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index 39c1b8decee..a6cd36caede 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
import NoteableNote from '~/notes/components/noteable_note.vue';
diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
index 7520cc2401b..2b37ed19176 100644
--- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
@@ -1,16 +1,16 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { sprintf, n__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import DraftsCount from './drafts_count.vue';
import PublishButton from './publish_button.vue';
import PreviewItem from './preview_item.vue';
export default {
components: {
+ GlButton,
GlLoadingIcon,
- Icon,
+ GlIcon,
DraftsCount,
PublishButton,
PreviewItem,
@@ -29,7 +29,7 @@ export default {
watch: {
showPreviewDropdown() {
if (this.showPreviewDropdown && this.$refs.dropdown) {
- this.$nextTick(() => this.$refs.dropdown.focus());
+ this.$nextTick(() => this.$refs.dropdown.$el.focus());
}
},
},
@@ -63,32 +63,35 @@ export default {
show: showPreviewDropdown,
}"
>
- <button
+ <gl-button
ref="dropdown"
type="button"
- class="btn btn-success review-preview-dropdown-toggle qa-review-preview-toggle"
+ category="primary"
+ variant="success"
+ class="review-preview-dropdown-toggle qa-review-preview-toggle"
@click="toggleReviewDropdown"
>
{{ __('Finish review') }}
<drafts-count />
- <icon name="angle-up" />
- </button>
+ <gl-icon name="angle-up" />
+ </gl-button>
<div
class="dropdown-menu dropdown-menu-large dropdown-menu-right dropdown-open-top"
:class="{
show: showPreviewDropdown,
}"
>
- <div class="dropdown-title">
- {{ dropdownTitle }}
- <button
+ <div class="dropdown-title gl-display-flex gl-align-items-center">
+ <span class="gl-ml-auto">{{ dropdownTitle }}</span>
+ <gl-button
:aria-label="__('Close')"
type="button"
- class="dropdown-title-button dropdown-menu-close"
+ category="tertiary"
+ size="small"
+ class="dropdown-title-button gl-ml-auto gl-p-0!"
+ icon="close"
@click="toggleReviewDropdown"
- >
- <icon name="close" />
- </button>
+ />
</div>
<div class="dropdown-content">
<ul v-if="isNotesFetched">
diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue
index 982fb01f49a..c89a6b537ef 100644
--- a/app/assets/javascripts/batch_comments/components/preview_item.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_item.vue
@@ -1,9 +1,8 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlIcon } from '@gitlab/ui';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { sprintf, __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import resolvedStatusMixin from '../mixins/resolved_status';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
@@ -14,7 +13,7 @@ import {
export default {
components: {
- Icon,
+ GlIcon,
GlSprintf,
},
mixins: [resolvedStatusMixin, glFeatureFlagsMixin()],
@@ -101,7 +100,7 @@ export default {
@click="scrollToDraft(draft)"
>
<span class="review-preview-item-header">
- <icon class="flex-shrink-0" :name="iconName" />
+ <gl-icon class="flex-shrink-0" :name="iconName" />
<span
class="bold text-nowrap"
:class="{ 'gl-align-items-center': glFeatures.multilineComments }"
@@ -138,7 +137,7 @@ export default {
v-if="draft.discussion_id && resolvedStatusMessage"
class="review-preview-item-footer draft-note-resolution p-0"
>
- <icon class="gl-mr-3" name="status_success" /> {{ resolvedStatusMessage }}
+ <gl-icon class="gl-mr-3" name="status_success" /> {{ resolvedStatusMessage }}
</span>
</button>
</template>
diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue
index 2d7b86d2431..e51888eabc1 100644
--- a/app/assets/javascripts/batch_comments/components/review_bar.vue
+++ b/app/assets/javascripts/batch_comments/components/review_bar.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
diff --git a/app/assets/javascripts/batch_comments/index.js b/app/assets/javascripts/batch_comments/index.js
index e06285c0b37..9c763e70d63 100644
--- a/app/assets/javascripts/batch_comments/index.js
+++ b/app/assets/javascripts/batch_comments/index.js
@@ -3,7 +3,6 @@ import { mapActions } from 'vuex';
import store from '~/mr_notes/stores';
import ReviewBar from './components/review_bar.vue';
-// eslint-disable-next-line import/prefer-default-export
export const initReviewBar = () => {
const el = document.getElementById('js-review-bar');
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index add43b81f6d..d61797b7ae4 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,8 +1,13 @@
import Autosize from 'autosize';
+import { waitForCSSLoaded } from '../helpers/startup_css_helper';
document.addEventListener('DOMContentLoaded', () => {
- const autosizeEls = document.querySelectorAll('.js-autosize');
+ waitForCSSLoaded(() => {
+ const autosizeEls = document.querySelectorAll('.js-autosize');
- Autosize(autosizeEls);
- Autosize.update(autosizeEls);
+ Autosize(autosizeEls);
+ Autosize.update(autosizeEls);
+
+ autosizeEls.forEach(el => el.classList.add('js-autosize-initialized'));
+ });
});
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 8060938c72a..fd12c282b62 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,7 +1,7 @@
+import $ from 'jquery';
import './autosize';
import './bind_in_out';
import './markdown/render_gfm';
-import initGFMInput from './markdown/gfm_auto_complete';
import initCopyAsGFM from './markdown/copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
@@ -15,9 +15,27 @@ import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resi
import initSelect2Dropdowns from './select2';
installGlEmojiElement();
-initGFMInput();
+
initCopyAsGFM();
initCopyToClipboard();
+
initPageShortcuts();
initCollapseSidebarOnWindowResize();
initSelect2Dropdowns();
+
+document.addEventListener('DOMContentLoaded', () => {
+ window.requestIdleCallback(
+ () => {
+ // Check if we have to Load GFM Input
+ const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)');
+ if ($gfmInputs.length) {
+ import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete')
+ .then(({ default: initGFMInput }) => {
+ initGFMInput($gfmInputs);
+ })
+ .catch(() => {});
+ }
+ },
+ { timeout: 500 },
+ );
+});
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
index 6bbd2133344..d712c90242c 100644
--- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -2,8 +2,8 @@ import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { parseBoolean } from '~/lib/utils/common_utils';
-export default function initGFMInput() {
- $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
+export default function initGFMInput($els) {
+ $els.each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = parseBoolean(el.dataset.supportsAutocomplete);
@@ -14,6 +14,7 @@ export default function initGFMInput() {
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
+ vulnerabilities: enableGFM,
});
});
}
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 01627b7206d..5e9d80e1529 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -5,7 +5,6 @@ import renderMermaid from './render_mermaid';
import renderMetrics from './render_metrics';
import highlightCurrentUser from './highlight_current_user';
import initUserPopovers from '../../user_popovers';
-import initMRPopovers from '../../mr_popover';
// Render GitLab flavoured Markdown
//
@@ -17,9 +16,25 @@ $.fn.renderGFM = function renderGFM() {
renderMermaid(this.find('.js-render-mermaid'));
highlightCurrentUser(this.find('.gfm-project_member').get());
initUserPopovers(this.find('.js-user-link').get());
- initMRPopovers(this.find('.gfm-merge_request').get());
+
+ const mrPopoverElements = this.find('.gfm-merge_request').get();
+ if (mrPopoverElements.length) {
+ import(/* webpackChunkName: 'MrPopoverBundle' */ '../../mr_popover')
+ .then(({ default: initMRPopovers }) => {
+ initMRPopovers(mrPopoverElements);
+ })
+ .catch(() => {});
+ }
+
renderMetrics(this.find('.js-render-metrics').get());
return this;
};
-$(() => $('body').renderGFM());
+$(() => {
+ window.requestIdleCallback(
+ () => {
+ $('body').renderGFM();
+ },
+ { timeout: 500 },
+ );
+});
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 03d9955f8fc..30783562da9 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -1,5 +1,6 @@
import { deprecatedCreateFlash as flash } from '~/flash';
import { s__, sprintf } from '~/locale';
+import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
// Renders math using KaTeX in any element with the
// `js-render-math` class
@@ -111,7 +112,7 @@ class SafeMathRenderer {
// Give the browser time to reflow the svg
waitForReflow(() => {
- const deltaTime = Date.now() - this.startTime;
+ const deltaTime = differenceInMilliseconds(this.startTime);
this.totalMS += deltaTime;
this.renderElement();
diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js
index 7987a533ae5..7352be0dbd5 100644
--- a/app/assets/javascripts/behaviors/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts.js
@@ -1,5 +1,3 @@
-import Shortcuts from './shortcuts/shortcuts';
-
export default function initPageShortcuts() {
const { page } = document.body.dataset;
const pagesWithCustomShortcuts = [
@@ -29,7 +27,9 @@ export default function initPageShortcuts() {
// the pages above have their own shortcuts sub-classes instantiated elsewhere
// TODO: replace this whitelist with something more automated/maintainable
if (page && !pagesWithCustomShortcuts.includes(page)) {
- return new Shortcuts();
+ import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts')
+ .then(({ default: Shortcuts }) => new Shortcuts())
+ .catch(() => {});
}
return false;
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 85636f3e5d2..8a8b61a57cd 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import Vue from 'vue';
+import { flatten } from 'lodash';
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
import ShortcutsToggle from './shortcuts_toggle.vue';
import axios from '../../lib/utils/axios_utils';
@@ -9,13 +10,13 @@ import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility';
import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
-const defaultStopCallback = Mousetrap.stopCallback;
-Mousetrap.stopCallback = (e, element, combo) => {
+const defaultStopCallback = Mousetrap.prototype.stopCallback;
+Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
}
- return defaultStopCallback(e, element, combo);
+ return defaultStopCallback.call(this, e, element, combo);
};
function initToggleButton() {
@@ -27,6 +28,39 @@ function initToggleButton() {
});
}
+/**
+ * The key used to save and fetch the local Mousetrap instance
+ * attached to a `<textarea>` element using `jQuery.data`
+ */
+const LOCAL_MOUSETRAP_DATA_KEY = 'local-mousetrap-instance';
+
+/**
+ * Gets a mapping of toolbar button => keyboard shortcuts
+ * associated to the given markdown editor `<textarea>` element
+ *
+ * @param {HTMLTextAreaElement} $textarea The jQuery-wrapped `<textarea>`
+ * element to extract keyboard shortcuts from
+ *
+ * @returns A Map with keys that are jQuery-wrapped toolbar buttons
+ * (i.e. `$toolbarBtn`) and values that are arrays of string
+ * keyboard shortcuts (e.g. `['command+k', 'ctrl+k]`).
+ */
+function getToolbarBtnToShortcutsMap($textarea) {
+ const $allToolbarBtns = $textarea.closest('.md-area').find('.js-md');
+ const map = new Map();
+
+ $allToolbarBtns.each(function attachToolbarBtnHandler() {
+ const $toolbarBtn = $(this);
+ const keyboardShortcuts = $toolbarBtn.data('md-shortcuts');
+
+ if (keyboardShortcuts?.length) {
+ map.set($toolbarBtn, keyboardShortcuts);
+ }
+ });
+
+ return map;
+}
+
export default class Shortcuts {
constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this);
@@ -34,6 +68,7 @@ export default class Shortcuts {
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
+ Mousetrap.bind('/', Shortcuts.focusSearch);
Mousetrap.bind('f', this.focusFilter.bind(this));
Mousetrap.bind('p b', Shortcuts.onTogglePerfBar);
@@ -143,4 +178,62 @@ export default class Shortcuts {
e.preventDefault();
}
}
+
+ /**
+ * Initializes markdown editor shortcuts on the provided `<textarea>` element
+ *
+ * @param {JQuery} $textarea The jQuery-wrapped `<textarea>` element
+ * where markdown shortcuts should be enabled
+ * @param {Function} handler The handler to call when a
+ * keyboard shortcut is pressed inside the markdown `<textarea>`
+ */
+ static initMarkdownEditorShortcuts($textarea, handler) {
+ const toolbarBtnToShortcutsMap = getToolbarBtnToShortcutsMap($textarea);
+
+ const localMousetrap = new Mousetrap($textarea[0]);
+
+ // Save a reference to the local mousetrap instance on the <textarea>
+ // so that it can be retrieved when unbinding shortcut handlers
+ $textarea.data(LOCAL_MOUSETRAP_DATA_KEY, localMousetrap);
+
+ toolbarBtnToShortcutsMap.forEach((keyboardShortcuts, $toolbarBtn) => {
+ localMousetrap.bind(keyboardShortcuts, e => {
+ e.preventDefault();
+
+ handler($toolbarBtn);
+ });
+ });
+
+ // Get an array of all shortcut strings that have been added above
+ const allShortcuts = flatten([...toolbarBtnToShortcutsMap.values()]);
+
+ const originalStopCallback = Mousetrap.prototype.stopCallback;
+ localMousetrap.stopCallback = function newStopCallback(e, element, combo) {
+ if (allShortcuts.includes(combo)) {
+ return false;
+ }
+
+ return originalStopCallback.call(this, e, element, combo);
+ };
+ }
+
+ /**
+ * Removes markdown editor shortcut handlers originally attached
+ * with `initMarkdownEditorShortcuts`.
+ *
+ * Note: it is safe to call this function even if `initMarkdownEditorShortcuts`
+ * has _not_ yet been called on the given `<textarea>`.
+ *
+ * @param {JQuery} $textarea The jQuery-wrapped `<textarea>`
+ * to remove shortcut handlers from
+ */
+ static removeMarkdownEditorShortcuts($textarea) {
+ const localMousetrap = $textarea.data(LOCAL_MOUSETRAP_DATA_KEY);
+
+ if (localMousetrap) {
+ getToolbarBtnToShortcutsMap($textarea).forEach(keyboardShortcuts => {
+ localMousetrap.unbind(keyboardShortcuts);
+ });
+ }
+ }
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
index 8658081c6c2..f0d2ecfd210 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
@@ -5,12 +5,11 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
constructor(projectFindFile) {
super();
- const oldStopCallback = Mousetrap.stopCallback;
- this.projectFindFile = projectFindFile;
+ const oldStopCallback = Mousetrap.prototype.stopCallback;
- Mousetrap.stopCallback = (e, element, combo) => {
+ Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
if (
- element === this.projectFindFile.inputElement[0] &&
+ element === projectFindFile.inputElement[0] &&
(combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
) {
// when press up/down key in textbox, cursor prevent to move to home/end
@@ -18,12 +17,12 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
return false;
}
- return oldStopCallback(e, element, combo);
+ return oldStopCallback.call(this, e, element, combo);
};
- Mousetrap.bind('up', this.projectFindFile.selectRowUp);
- Mousetrap.bind('down', this.projectFindFile.selectRowDown);
- Mousetrap.bind('esc', this.projectFindFile.goToTree);
- Mousetrap.bind('enter', this.projectFindFile.goToBlob);
+ Mousetrap.bind('up', projectFindFile.selectRowUp);
+ Mousetrap.bind('down', projectFindFile.selectRowDown);
+ Mousetrap.bind('esc', projectFindFile.goToTree);
+ Mousetrap.bind('enter', projectFindFile.goToBlob);
}
}
diff --git a/app/assets/javascripts/blob/components/blob_edit_content.vue b/app/assets/javascripts/blob/components/blob_edit_content.vue
index 26ba7b98a39..6293f3bed1c 100644
--- a/app/assets/javascripts/blob/components/blob_edit_content.vue
+++ b/app/assets/javascripts/blob/components/blob_edit_content.vue
@@ -46,7 +46,7 @@ export default {
blobGlobalId: this.fileGlobalId,
});
- this.editor.onChangeContent(debounce(this.onFileChange.bind(this), 250));
+ this.editor.onDidChangeModelContent(debounce(this.onFileChange.bind(this), 250));
window.requestAnimationFrame(() => {
if (!performance.getEntriesByName(SNIPPET_MARK_BLOBS_CONTENT).length) {
diff --git a/app/assets/javascripts/blob/components/blob_embeddable.vue b/app/assets/javascripts/blob/components/blob_embeddable.vue
deleted file mode 100644
index 00b915ec8bd..00000000000
--- a/app/assets/javascripts/blob/components/blob_embeddable.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import { GlFormInputGroup, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlFormInputGroup,
- GlDeprecatedButton,
- GlIcon,
- },
- props: {
- url: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- optionValues: [
- // eslint-disable-next-line no-useless-escape
- { name: __('Embed'), value: `<script src='${this.url}.js'><\/script>` },
- { name: __('Share'), value: this.url },
- ],
- };
- },
-};
-</script>
-<template>
- <gl-form-input-group
- id="embeddable-text"
- :predefined-options="optionValues"
- readonly
- select-on-click
- >
- <template #append>
- <gl-deprecated-button new-style data-clipboard-target="#embeddable-text">
- <gl-icon name="copy-to-clipboard" :title="__('Copy')" />
- </gl-deprecated-button>
- </template>
- </gl-form-input-group>
-</template>
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 4409d7a33cc..5058ca7122d 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -1,16 +1,18 @@
import $ from 'jquery';
+
import Api from '~/api';
+import toast from '~/vue_shared/plugins/global_toast';
+import { __ } from '~/locale';
+import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { deprecatedCreateFlash as Flash } from '../flash';
+
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
import MetricsDashboardSelector from './template_selectors/metrics_dashboard_selector';
-import toast from '~/vue_shared/plugins/global_toast';
-import { __ } from '~/locale';
-import initPopover from '~/blob/suggest_gitlab_ci_yml';
export default class FileTemplateMediator {
constructor({ editor, currentAction, projectId }) {
diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js
index 476901aae75..bd39aa2e16f 100644
--- a/app/assets/javascripts/blob/file_template_selector.js
+++ b/app/assets/javascripts/blob/file_template_selector.js
@@ -45,11 +45,15 @@ export default class FileTemplateSelector {
}
renderLoading() {
- this.$loadingIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down');
+ this.$loadingIcon
+ .addClass('gl-spinner gl-spinner-orange gl-spinner-sm')
+ .removeClass('fa-chevron-down');
}
renderLoaded() {
- this.$loadingIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin');
+ this.$loadingIcon
+ .addClass('fa-chevron-down')
+ .removeClass('gl-spinner gl-spinner-orange gl-spinner-sm');
}
reportSelection(options) {
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index 90eafb75758..411241b72d5 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -1,5 +1,5 @@
<script>
-import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { sprintf, s__, __ } from '~/locale';
import { glEmojiTag } from '~/emoji';
@@ -18,6 +18,8 @@ export default {
helpMessage: s__(
`MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Continuous Integration%{beginnerLinkEnd} and our %{exampleLinkStart}examples of GitLab CI/CD%{exampleLinkEnd} to learn more.`,
),
+ pipelinesButton: s__('MR widget|See your pipeline in action'),
+ mergeRequestButton: s__('MR widget|Back to the Merge request'),
modalTitle: sprintf(
__("That's it, well done!%{celebrate}"),
{
@@ -25,11 +27,13 @@ export default {
},
false,
),
- goToTrackValue: 10,
+ goToTrackValuePipelines: 10,
+ goToTrackValueMergeRequest: 20,
trackEvent: 'click_button',
components: {
GlModal,
GlSprintf,
+ GlButton,
GlLink,
},
mixins: [trackingMixin],
@@ -38,6 +42,11 @@ export default {
type: String,
required: true,
},
+ projectMergeRequestsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
commitCookie: {
type: String,
required: true,
@@ -59,6 +68,15 @@ export default {
property: this.humanAccess,
};
},
+ goToMergeRequestPath() {
+ return this.commitCookiePath || this.projectMergeRequestsPath;
+ },
+ commitCookiePath() {
+ const cookieVal = Cookies.get(this.commitCookie);
+
+ if (cookieVal !== 'true') return cookieVal;
+ return '';
+ },
},
mounted() {
this.track();
@@ -100,17 +118,28 @@ export default {
</template>
</gl-sprintf>
<template #modal-footer>
- <a
- ref="goto"
+ <gl-button
+ v-if="projectMergeRequestsPath"
+ ref="goToMergeRequest"
+ :href="goToMergeRequestPath"
+ :data-track-property="humanAccess"
+ :data-track-value="$options.goToTrackValueMergeRequest"
+ :data-track-event="$options.trackEvent"
+ :data-track-label="trackLabel"
+ >
+ {{ $options.mergeRequestButton }}
+ </gl-button>
+ <gl-button
+ ref="goToPipelines"
:href="goToPipelinesPath"
- class="btn btn-success"
+ variant="success"
:data-track-property="humanAccess"
- :data-track-value="$options.goToTrackValue"
+ :data-track-value="$options.goToTrackValuePipelines"
:data-track-event="$options.trackEvent"
:data-track-label="trackLabel"
>
- {{ __('See your pipeline in action') }}
- </a>
+ {{ $options.pipelinesButton }}
+ </gl-button>
</template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index aff6a56cb0b..06f436adb8e 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -49,6 +49,10 @@ export default {
type: String,
required: true,
},
+ mergeRequestPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -109,7 +113,7 @@ export default {
:css-classes="['suggest-gitlab-ci-yml', 'ml-4']"
>
<template #title>
- <span v-html="suggestTitle"></span>
+ <span>{{ suggestTitle }}</span>
<span class="ml-auto">
<gl-button
:aria-label="__('Close')"
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js
index 3b67b3dd259..55edb852ee6 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js
@@ -10,6 +10,7 @@ export default el =>
target: el.dataset.target,
trackLabel: el.dataset.trackLabel,
dismissKey: el.dataset.dismissKey,
+ mergeRequestPath: el.dataset.mergeRequestPath,
humanAccess: el.dataset.humanAccess,
},
});
diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue
new file mode 100644
index 00000000000..1308ca53e74
--- /dev/null
+++ b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlAlert, GlButton } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ },
+ props: {
+ dismissEndpoint: {
+ type: String,
+ required: true,
+ },
+ featureId: {
+ type: String,
+ required: true,
+ },
+ editPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showAlert: true,
+ };
+ },
+ methods: {
+ dismissAlert() {
+ this.showAlert = false;
+
+ return axios.post(this.dismissEndpoint, {
+ feature_name: this.featureId,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissAlert">
+ {{ __('The Web IDE offers advanced syntax highlighting capabilities and more.') }}
+ <div class="gl-mt-5">
+ <gl-button :href="editPath" category="primary" variant="info">{{
+ __('Open Web IDE')
+ }}</gl-button>
+ </div>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/index.js b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js
new file mode 100644
index 00000000000..eadf3cd6216
--- /dev/null
+++ b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import WebIdeAlert from './components/web_ide_alert.vue';
+
+export default el => {
+ const { dismissEndpoint, featureId, editPath } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ render(createElement) {
+ return createElement(WebIdeAlert, {
+ props: {
+ dismissEndpoint,
+ featureId,
+ editPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index 2427e25a17d..257458138dc 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
import $ from 'jquery';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class TemplateSelector {
constructor({ dropdown, data, pattern, wrapper, editor, $input } = {}) {
@@ -19,7 +19,7 @@ export default class TemplateSelector {
}
initDropdown(dropdown, data) {
- return $(dropdown).glDropdown({
+ return initDeprecatedJQueryDropdown($(dropdown), {
data,
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
index d819452df68..3a4e86fe572 100644
--- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class BlobCiYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -15,7 +16,7 @@ export default class BlobCiYamlSelector extends FileTemplateSelector {
initDropdown() {
// maybe move to super class as well
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
index 7d5e98889d3..3cb4bb83930 100644
--- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
@@ -1,5 +1,6 @@
import FileTemplateSelector from '../file_template_selector';
import { __ } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class DockerfileSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -16,7 +17,7 @@ export default class DockerfileSelector extends FileTemplateSelector {
initDropdown() {
// maybe move to super class as well
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
index 39a8937641d..1721230dcb7 100644
--- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class BlobGitignoreSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -14,7 +15,7 @@ export default class BlobGitignoreSelector extends FileTemplateSelector {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js
index f4041835a7d..dafde82b1e0 100644
--- a/app/assets/javascripts/blob/template_selectors/license_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/license_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class BlobLicenseSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -14,7 +15,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
index b4accaadfa3..9e698bfea5d 100644
--- a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class MetricsDashboardSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -14,7 +15,7 @@ export default class MetricsDashboardSelector extends FileTemplateSelector {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js
index cb4e1aaa9ac..01625911815 100644
--- a/app/assets/javascripts/blob/template_selectors/type_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/type_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class FileTemplateTypeSelector extends FileTemplateSelector {
constructor({ mediator, dropdownData }) {
@@ -12,7 +13,7 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.config.dropdownData,
filterable: false,
selectable: true,
diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js
index a0211c8bb8e..8043c0bbc07 100644
--- a/app/assets/javascripts/blob/utils.js
+++ b/app/assets/javascripts/blob/utils.js
@@ -1,20 +1,16 @@
import Editor from '~/editor/editor_lite';
export function initEditorLite({ el, ...args }) {
- if (!el) {
- throw new Error(`"el" parameter is required to initialize Editor`);
- }
const editor = new Editor({
scrollbar: {
alwaysConsumeMouseWheel: false,
},
});
- editor.createInstance({
+
+ return editor.createInstance({
el,
...args,
});
-
- return editor;
}
export default () => ({});
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 9b9ade28623..c9972f0b43c 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -7,12 +7,14 @@ import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
+import initWebIdeAlert from '~/blob/suggest_web_ide_ci';
export default () => {
const editBlobForm = $('.js-edit-blob-form');
const uploadBlobForm = $('.js-upload-blob-form');
const deleteBlobForm = $('.js-delete-blob-form');
const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
+ const alertEl = document.getElementById('js-suggest-web-ide-ci');
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
@@ -65,12 +67,15 @@ export default () => {
if (commitButton) {
const { dismissKey, humanAccess } = suggestEl.dataset;
+ const urlParams = new URLSearchParams(window.location.search);
+ const mergeRequestPath = urlParams.get('mr_path') || true;
+
const commitCookieName = `suggest_gitlab_ci_yml_commit_${dismissKey}`;
const commitTrackLabel = 'suggest_gitlab_ci_yml_commit_changes';
const commitTrackValue = '20';
commitButton.addEventListener('click', () => {
- setCookie(commitCookieName, true);
+ setCookie(commitCookieName, mergeRequestPath);
Tracking.event(undefined, 'click_button', {
label: commitTrackLabel,
@@ -80,4 +85,8 @@ export default () => {
});
}
}
+
+ if (alertEl) {
+ initWebIdeAlert(alertEl);
+ }
};
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index e22c9b0d4c4..2a4ab4b8827 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -40,9 +40,10 @@ export default class EditBlob {
const MarkdownExtensionPromise = this.options.isMarkdown
? import('~/editor/editor_markdown_ext')
: Promise.resolve(false);
+ const FileTemplateExtensionPromise = import('~/editor/editor_file_template_ext');
- return Promise.all([EditorPromise, MarkdownExtensionPromise])
- .then(([EditorModule, MarkdownExtension]) => {
+ return Promise.all([EditorPromise, MarkdownExtensionPromise, FileTemplateExtensionPromise])
+ .then(([EditorModule, MarkdownExtension, FileTemplateExtension]) => {
const EditorLite = EditorModule.default;
const editorEl = document.getElementById('editor');
const fileNameEl =
@@ -50,18 +51,16 @@ export default class EditBlob {
const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form');
- this.editor = new EditorLite();
+ const rootEditor = new EditorLite();
- if (MarkdownExtension) {
- this.editor.use(MarkdownExtension.default);
- }
-
- this.editor.createInstance({
+ this.editor = rootEditor.createInstance({
el: editorEl,
blobPath: fileNameEl.value,
blobContent: editorEl.innerText,
});
+ rootEditor.use([MarkdownExtension.default, FileTemplateExtension.default], this.editor);
+
fileNameEl.addEventListener('change', () => {
this.editor.updateModelLanguage(fileNameEl.value);
});
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 384a386d69c..5c8df94ca90 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,28 +1,72 @@
+import { sortBy } from 'lodash';
import ListIssue from 'ee_else_ce/boards/models/issue';
+import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export function getMilestone() {
return null;
}
+export function formatIssue(issue) {
+ return new ListIssue({
+ ...issue,
+ labels: issue.labels?.nodes || [],
+ assignees: issue.assignees?.nodes || [],
+ });
+}
+
export function formatListIssues(listIssues) {
- return listIssues.nodes.reduce((map, list) => {
+ const issues = {};
+
+ const listData = listIssues.nodes.reduce((map, list) => {
+ const sortedIssues = sortBy(list.issues.nodes, 'relativePosition');
return {
...map,
- [list.id]: list.issues.nodes.map(
- i =>
- new ListIssue({
- ...i,
- id: getIdFromGraphQLId(i.id),
- labels: i.labels?.nodes || [],
- assignees: i.assignees?.nodes || [],
- }),
- ),
+ [list.id]: sortedIssues.map(i => {
+ const id = getIdFromGraphQLId(i.id);
+
+ const listIssue = new ListIssue({
+ ...i,
+ id,
+ labels: i.labels?.nodes || [],
+ assignees: i.assignees?.nodes || [],
+ });
+
+ issues[id] = listIssue;
+
+ return id;
+ }),
};
}, {});
+
+ return { listData, issues };
+}
+
+export function fullBoardId(boardId) {
+ return `gid://gitlab/Board/${boardId}`;
+}
+
+export function moveIssueListHelper(issue, fromList, toList) {
+ if (toList.type === ListType.label) {
+ issue.addLabel(toList.label);
+ }
+ if (fromList && fromList.type === ListType.label) {
+ issue.removeLabel(fromList.label);
+ }
+
+ if (toList.type === ListType.assignee) {
+ issue.addAssignee(toList.assignee);
+ }
+ if (fromList && fromList.type === ListType.assignee) {
+ issue.removeAssignee(fromList.assignee);
+ }
+
+ return issue;
}
export default {
getMilestone,
+ formatIssue,
formatListIssues,
+ fullBoardId,
};
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index afdf0290e8e..55e3e4a6329 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -1,10 +1,14 @@
<script>
+import { GlButton } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { __ } from '~/locale';
import ListLabel from '~/boards/models/label';
import boardsStore from '../stores/boards_store';
export default {
+ components: {
+ GlButton,
+ },
data() {
return {
predefinedLabels: [
@@ -84,15 +88,17 @@ export default {
)
}}
</p>
- <button
- class="btn btn-success btn-inverted btn-block"
- type="button"
+ <gl-button
+ category="secondary"
+ variant="success"
+ block="block"
+ class="gl-mb-0"
@click.stop="addDefaultLists"
>
{{ s__('BoardBlankState|Add default lists') }}
- </button>
- <button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState">
+ </gl-button>
+ <gl-button category="secondary" variant="default" block="block" @click.stop="clearBlankState">
{{ s__("BoardBlankState|Nevermind, I'll use my own") }}
- </button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 246d3b9dcd1..31050eef83d 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable vue/require-default-prop */
-import IssueCardInner from './issue_card_inner.vue';
+import BoardCardLayout from './board_card_layout.vue';
import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import boardsStore from '../stores/boards_store';
@@ -8,7 +7,7 @@ import boardsStore from '../stores/boards_store';
export default {
name: 'BoardsIssueCard',
components: {
- IssueCardInner,
+ BoardCardLayout,
},
props: {
list: {
@@ -21,80 +20,29 @@ export default {
default: () => ({}),
required: false,
},
- issueLinkBase: {
- type: String,
- default: '',
- required: false,
- },
- disabled: {
- type: Boolean,
- default: false,
- required: false,
- },
- index: {
- type: Number,
- default: 0,
- required: false,
- },
- rootPath: {
- type: String,
- default: '',
- required: false,
- },
- groupId: {
- type: Number,
- required: false,
- },
- },
- data() {
- return {
- showDetail: false,
- detailIssue: boardsStore.detail,
- multiSelect: boardsStore.multiSelect,
- };
- },
- computed: {
- issueDetailVisible() {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- },
- multiSelectVisible() {
- return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1;
- },
- canMultiSelect() {
- return gon.features && gon.features.multiSelectBoard;
- },
},
methods: {
- mouseDown() {
- this.showDetail = true;
+ // These are methods instead of computed's, because boardsStore is not reactive.
+ isActive() {
+ return this.getActiveId() === this.issue.id;
},
- mouseMove() {
- this.showDetail = false;
+ getActiveId() {
+ return boardsStore.detail?.issue?.id;
},
- showIssue(e) {
- if (e.target.classList.contains('js-no-trigger')) return;
-
+ showIssue({ isMultiSelect }) {
// If no issues are opened, close all sidebars first
- if (!boardsStore.detail?.issue?.id) {
+ if (!this.getActiveId()) {
sidebarEventHub.$emit('sidebar.closeAll');
}
+ if (this.isActive()) {
+ eventHub.$emit('clearDetailIssue', isMultiSelect);
- // If CMD or CTRL is clicked
- const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
-
- if (this.showDetail || isMultiSelect) {
- this.showDetail = false;
-
- if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
- eventHub.$emit('clearDetailIssue', isMultiSelect);
-
- if (isMultiSelect) {
- eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
- }
- } else {
+ if (isMultiSelect) {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
- boardsStore.setListDetail(this.list);
}
+ } else {
+ eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
+ boardsStore.setListDetail(this.list);
}
},
},
@@ -102,28 +50,12 @@ export default {
</script>
<template>
- <li
- :class="{
- 'multi-select': multiSelectVisible,
- 'user-can-drag': !disabled && issue.id,
- 'is-disabled': disabled || !issue.id,
- 'is-active': issueDetailVisible,
- }"
- :index="index"
- :data-issue-id="issue.id"
+ <board-card-layout
data-qa-selector="board_card"
- class="board-card p-3 rounded"
- @mousedown="mouseDown"
- @mousemove="mouseMove"
- @mouseup="showIssue($event)"
- >
- <issue-card-inner
- :list="list"
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :group-id="groupId"
- :root-path="rootPath"
- :update-filters="true"
- />
- </li>
+ :issue="issue"
+ :list="list"
+ :is-active="isActive()"
+ v-bind="$attrs"
+ @show="showIssue"
+ />
</template>
diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue
new file mode 100644
index 00000000000..072dd87861a
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card_layout.vue
@@ -0,0 +1,93 @@
+<script>
+import IssueCardInner from './issue_card_inner.vue';
+import boardsStore from '../stores/boards_store';
+
+export default {
+ name: 'BoardsIssueCard',
+ components: {
+ IssueCardInner,
+ },
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ issue: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ required: false,
+ },
+ isActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ showDetail: false,
+ multiSelect: boardsStore.multiSelect,
+ };
+ },
+ computed: {
+ multiSelectVisible() {
+ return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1;
+ },
+ canMultiSelect() {
+ return gon.features && gon.features.multiSelectBoard;
+ },
+ },
+ methods: {
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ // Don't do anything if this happened on a no trigger element
+ if (e.target.classList.contains('js-no-trigger')) return;
+
+ const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
+
+ if (this.showDetail || isMultiSelect) {
+ this.showDetail = false;
+ this.$emit('show', { event: e, isMultiSelect });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ :class="{
+ 'multi-select': multiSelectVisible,
+ 'user-can-drag': !disabled && issue.id,
+ 'is-disabled': disabled || !issue.id,
+ 'is-active': isActive,
+ }"
+ :index="index"
+ :data-issue-id="issue.id"
+ :data-issue-iid="issue.iid"
+ :data-issue-path="issue.referencePath"
+ data-testid="board_card"
+ class="board-card p-3 rounded"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)"
+ >
+ <issue-card-inner :list="list" :issue="issue" :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index dae24338e45..6d216911798 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -1,9 +1,11 @@
<script>
+import { mapGetters, mapActions } from 'vuex';
import Sortable from 'sortablejs';
import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import EmptyComponent from '~/vue_shared/components/empty_component';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoardBlankState from './board_blank_state.vue';
import BoardList from './board_list.vue';
import boardsStore from '../stores/boards_store';
@@ -21,7 +23,7 @@ export default {
directives: {
Tooltip,
},
- mixins: [isWipLimitsOn],
+ mixins: [isWipLimitsOn, glFeatureFlagMixin()],
props: {
list: {
type: Object,
@@ -32,27 +34,15 @@ export default {
type: Boolean,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- boardId: {
- type: String,
- required: true,
- },
canAdminList: {
type: Boolean,
required: false,
default: false,
},
- groupId: {
- type: Number,
- required: false,
- default: null,
+ },
+ inject: {
+ boardId: {
+ type: String,
},
},
data() {
@@ -62,6 +52,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['getIssues']),
showBoardListAndBoardInfo() {
return this.list.type !== ListType.blank && this.list.type !== ListType.promotion;
},
@@ -69,19 +60,36 @@ export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
},
+ listIssues() {
+ if (!this.glFeatures.graphqlBoardLists) {
+ return this.list.issues;
+ }
+ return this.getIssues(this.list.id);
+ },
+ shouldFetchIssues() {
+ return this.glFeatures.graphqlBoardLists && this.list.type !== ListType.blank;
+ },
},
watch: {
filter: {
handler() {
- this.list.page = 1;
- this.list.getIssues(true).catch(() => {
- // TODO: handle request error
- });
+ if (this.shouldFetchIssues) {
+ this.fetchIssuesForList(this.list.id);
+ } else {
+ this.list.page = 1;
+ this.list.getIssues(true).catch(() => {
+ // TODO: handle request error
+ });
+ }
},
deep: true,
},
},
mounted() {
+ if (this.shouldFetchIssues) {
+ this.fetchIssuesForList(this.list.id);
+ }
+
const instance = this;
const sortableOptions = getBoardSortableDefaultOptions({
@@ -108,6 +116,7 @@ export default {
Sortable.create(this.$el.parentNode, sortableOptions);
},
methods: {
+ ...mapActions(['fetchIssuesForList']),
showListNewIssueForm(listId) {
eventHub.$emit('showForm', listId);
},
@@ -130,22 +139,14 @@ export default {
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
>
- <board-list-header
- :can-admin-list="canAdminList"
- :list="list"
- :disabled="disabled"
- :board-id="boardId"
- />
+ <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list
v-if="showBoardListAndBoardInfo"
ref="board-list"
:disabled="disabled"
- :group-id="groupId || null"
- :issue-link-base="issueLinkBase"
- :issues="list.issues"
+ :issues="listIssues"
:list="list"
:loading="list.loading"
- :root-path="rootPath"
/>
<board-blank-state v-if="canAdminList && list.id === 'blank'" />
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index c42295792f1..c7b3da0e672 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,13 +1,15 @@
<script>
-import { mapState } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
-import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
+import { GlAlert } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
BoardColumn,
- EpicsSwimlanes,
+ BoardContentSidebar: () => import('ee_component/boards/components/board_content_sidebar.vue'),
+ EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
+ GlAlert,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -19,66 +21,58 @@ export default {
type: Boolean,
required: true,
},
- groupId: {
- type: Number,
- required: false,
- default: null,
- },
disabled: {
type: Boolean,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- boardId: {
- type: String,
- required: true,
- },
},
computed: {
- ...mapState(['isShowingEpicsSwimlanes', 'boardLists']),
- isSwimlanesOn() {
- return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes;
+ ...mapState(['boardLists', 'error']),
+ ...mapGetters(['isSwimlanesOn']),
+ boardListsToUse() {
+ return this.glFeatures.graphqlBoardLists ? this.boardLists : this.lists;
},
},
+ mounted() {
+ if (this.glFeatures.graphqlBoardLists) {
+ this.fetchLists();
+ this.showPromotionList();
+ }
+ },
+ methods: {
+ ...mapActions(['fetchLists', 'showPromotionList']),
+ },
};
</script>
<template>
<div>
+ <gl-alert v-if="error" variant="danger" :dismissible="false">
+ {{ error }}
+ </gl-alert>
<div
v-if="!isSwimlanesOn"
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
data-qa-selector="boards_list"
>
<board-column
- v-for="list in lists"
+ v-for="list in boardListsToUse"
:key="list.id"
ref="board"
:can-admin-list="canAdminList"
- :group-id="groupId"
:list="list"
:disabled="disabled"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :board-id="boardId"
/>
</div>
- <epics-swimlanes
- v-else
- ref="swimlanes"
- :lists="boardLists"
- :can-admin-list="canAdminList"
- :disabled="disabled"
- :board-id="boardId"
- :group-id="groupId"
- :root-path="rootPath"
- />
+
+ <template v-else>
+ <epics-swimlanes
+ ref="swimlanes"
+ :lists="boardLists"
+ :can-admin-list="canAdminList"
+ :disabled="disabled"
+ />
+ <board-content-sidebar />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 231059b895e..385dd5fdc71 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -25,11 +25,11 @@ export default {
type: Boolean,
required: true,
},
- milestonePath: {
+ labelsPath: {
type: String,
required: true,
},
- labelsPath: {
+ labelsWebUrl: {
type: String,
required: true,
},
@@ -201,8 +201,8 @@ export default {
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
- :milestone-path="milestonePath"
:labels-path="labelsPath"
+ :labels-web-url="labelsWebUrl"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 1a26782f6f0..25f8ffca633 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,6 +6,7 @@ import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
getBoardSortableDefaultOptions,
@@ -24,12 +25,8 @@ export default {
boardNewIssue,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagMixin()],
props: {
- groupId: {
- type: Number,
- required: false,
- default: 0,
- },
disabled: {
type: Boolean,
required: true,
@@ -46,14 +43,6 @@ export default {
type: Boolean,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -83,6 +72,7 @@ export default {
deep: true,
},
issues() {
+ if (this.glFeatures.graphqlBoardLists) return;
this.$nextTick(() => {
if (
this.scrollHeight() <= this.listHeight() &&
@@ -413,6 +403,8 @@ export default {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
+ if (this.glFeatures.graphqlBoardLists) return;
+
if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) {
this.loadNextPage();
}
@@ -430,11 +422,7 @@ export default {
<div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
<gl-loading-icon />
</div>
- <board-new-issue
- v-if="list.type !== 'closed' && showIssueForm"
- :group-id="groupId"
- :list="list"
- />
+ <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" />
<ul
v-show="!loading"
ref="list"
@@ -450,9 +438,6 @@ export default {
:index="index"
:list="list"
:issue="issue"
- :issue-link-base="issueLinkBase"
- :group-id="groupId"
- :root-path="rootPath"
:disabled="disabled"
/>
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index bafe07afb48..361fe252afb 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -1,4 +1,5 @@
<script>
+import { mapActions } from 'vuex';
import {
GlButton,
GlButtonGroup,
@@ -17,6 +18,7 @@ import boardsStore from '../stores/boards_store';
import eventHub from '../eventhub';
import { ListType } from '../constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -32,7 +34,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [isWipLimitsOn],
+ mixins: [isWipLimitsOn, glFeatureFlagMixin()],
props: {
list: {
type: Object,
@@ -43,10 +45,6 @@ export default {
type: Boolean,
required: true,
},
- boardId: {
- type: String,
- required: true,
- },
canAdminList: {
type: Boolean,
required: false,
@@ -58,6 +56,11 @@ export default {
default: false,
},
},
+ inject: {
+ boardId: {
+ type: String,
+ },
+ },
data() {
return {
weightFeatureAvailable: false,
@@ -94,10 +97,11 @@ export default {
showAssigneeListDetails() {
return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader);
},
+ issuesCount() {
+ return this.list.issuesSize;
+ },
issuesTooltipLabel() {
- const { issuesSize } = this.list;
-
- return n__(`%d issue`, `%d issues`, issuesSize);
+ return n__(`%d issue`, `%d issues`, this.issuesCount);
},
chevronTooltip() {
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
@@ -126,8 +130,12 @@ export default {
collapsedTooltipTitle() {
return this.listTitle || this.listAssignee;
},
+ shouldDisplaySwimlanes() {
+ return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn;
+ },
},
methods: {
+ ...mapActions(['updateList']),
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
@@ -136,20 +144,28 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
- if (this.list.isExpandable) {
- this.list.isExpanded = !this.list.isExpanded;
-
- if (AccessorUtilities.isLocalStorageAccessSafe() && !this.isLoggedIn) {
- localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
- }
+ this.list.isExpanded = !this.list.isExpanded;
- if (this.isLoggedIn) {
- this.list.update();
- }
+ if (!this.isLoggedIn) {
+ this.addToLocalStorage();
+ } else {
+ this.updateListFunction();
+ }
- // When expanding/collapsing, the tooltip on the caret button sometimes stays open.
- // Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ // When expanding/collapsing, the tooltip on the caret button sometimes stays open.
+ // Close all tooltips manually to prevent dangling tooltips.
+ this.$root.$emit('bv::hide::tooltip');
+ },
+ addToLocalStorage() {
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
+ }
+ },
+ updateListFunction() {
+ if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
+ this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded });
+ } else {
+ this.list.update();
}
},
},
@@ -172,7 +188,7 @@ export default {
<h3
:class="{
'user-can-drag': !disabled && !list.preset,
- 'gl-py-3': !list.isExpanded && !isSwimlanesHeader,
+ 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader,
'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
'gl-py-2': !list.isExpanded && isSwimlanesHeader,
}"
@@ -288,7 +304,7 @@ export default {
<gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />
<span ref="issueCount" class="issue-count-badge-count">
<gl-icon class="gl-mr-2" name="issues" />
- <issue-count :issues-size="list.issuesSize" :max-issue-count="list.maxIssueCount" />
+ <issue-count :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" />
</span>
<!-- The following is only true in EE. -->
<template v-if="weightFeatureAvailable">
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 34e8438ba4c..348d485ff37 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,11 +1,13 @@
<script>
import $ from 'jquery';
+import { mapActions, mapGetters } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import boardsStore from '../stores/boards_store';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'BoardNewIssue',
@@ -13,17 +15,18 @@ export default {
ProjectSelect,
GlButton,
},
+ mixins: [glFeatureFlagMixin()],
props: {
- groupId: {
- type: Number,
- required: false,
- default: 0,
- },
list: {
type: Object,
required: true,
},
},
+ inject: {
+ groupId: {
+ type: Number,
+ },
+ },
data() {
return {
title: '',
@@ -32,18 +35,23 @@ export default {
};
},
computed: {
+ ...mapGetters(['isSwimlanesOn']),
disabled() {
if (this.groupId) {
return this.title === '' || !this.selectedProject.name;
}
return this.title === '';
},
+ shouldDisplaySwimlanes() {
+ return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn;
+ },
},
mounted() {
this.$refs.input.focus();
eventHub.$on('setSelectedProject', this.setSelectedProject);
},
methods: {
+ ...mapActions(['addListIssue', 'addListIssueFailure']),
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return Promise.resolve();
@@ -70,21 +78,31 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
+ if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
+ this.addListIssue({ list: this.list, issue, position: 0 });
+ }
+
return this.list
.newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
- boardsStore.setIssueDetail(issue);
- boardsStore.setListDetail(this.list);
+ if (!this.shouldDisplaySwimlanes && !this.glFeatures.graphqlBoardLists) {
+ boardsStore.setIssueDetail(issue);
+ boardsStore.setListDetail(this.list);
+ }
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
// Remove the issue
- this.list.removeIssue(issue);
+ if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
+ this.addListIssueFailure({ list: this.list, issue });
+ } else {
+ this.list.removeIssue(issue);
+ }
// Show error message
this.error = true;
@@ -121,7 +139,7 @@ export default {
<project-select v-if="groupId" :group-id="groupId" :list="list" />
<div class="clearfix gl-mt-3">
<gl-button
- ref="submit-button"
+ ref="submitButton"
:disabled="disabled"
class="float-left"
variant="success"
@@ -129,9 +147,14 @@ export default {
type="submit"
>{{ __('Submit issue') }}</gl-button
>
- <gl-button class="float-right" type="button" variant="default" @click="cancel">{{
- __('Cancel')
- }}</gl-button>
+ <gl-button
+ ref="cancelButton"
+ class="float-right"
+ type="button"
+ variant="default"
+ @click="cancel"
+ >{{ __('Cancel') }}</gl-button
+ >
</div>
</form>
</div>
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 3149762ecdf..e2600883e89 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -1,11 +1,12 @@
<script>
import { GlDrawer, GlLabel } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import { inactiveId } from '~/boards/constants';
+import { LIST } from '~/boards/constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
@@ -23,18 +24,20 @@ export default {
BoardSettingsListTypes: () =>
import('ee_component/boards/components/board_settings_list_types.vue'),
},
+ mixins: [glFeatureFlagMixin()],
computed: {
- ...mapState(['activeId']),
+ ...mapGetters(['isSidebarOpen']),
+ ...mapState(['activeId', 'sidebarType', 'boardLists']),
activeList() {
/*
Warning: Though a computed property it is not reactive because we are
referencing a List Model class. Reactivity only applies to plain JS objects
*/
+ if (this.glFeatures.graphqlBoardLists) {
+ return this.boardLists.find(({ id }) => id === this.activeId);
+ }
return boardsStore.state.lists.find(({ id }) => id === this.activeId);
},
- isSidebarOpen() {
- return this.activeId !== inactiveId;
- },
activeListLabel() {
return this.activeList.label;
},
@@ -44,18 +47,18 @@ export default {
listTypeTitle() {
return this.$options.labelListText;
},
+ showSidebar() {
+ return this.sidebarType === LIST;
+ },
},
created() {
- eventHub.$on('sidebar.closeAll', this.closeSidebar);
+ eventHub.$on('sidebar.closeAll', this.unsetActiveId);
},
beforeDestroy() {
- eventHub.$off('sidebar.closeAll', this.closeSidebar);
+ eventHub.$off('sidebar.closeAll', this.unsetActiveId);
},
methods: {
- ...mapActions(['setActiveId']),
- closeSidebar() {
- this.setActiveId(inactiveId);
- },
+ ...mapActions(['unsetActiveId']),
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
@@ -65,10 +68,11 @@ export default {
<template>
<gl-drawer
+ v-if="showSidebar"
class="js-board-settings-sidebar"
:open="isSidebarOpen"
:header-height="$options.headerHeight"
- @close="closeSidebar"
+ @close="unsetActiveId"
>
<template #header>{{ $options.listSettingsText }}</template>
<template v-if="isSidebarOpen">
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 3790c494085..d26f15c1723 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -83,7 +83,7 @@ export default Vue.extend({
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el)
- .data('glDropdown')
+ .data('deprecatedJQueryDropdown')
.clearMenu();
});
}
@@ -95,7 +95,7 @@ export default Vue.extend({
},
},
created() {
- // Get events from glDropdown
+ // Get events from deprecatedJQueryDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 48f6ba6cfc7..271e1fc4b5f 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -36,10 +36,6 @@ export default {
type: Object,
required: true,
},
- milestonePath: {
- type: String,
- required: true,
- },
throttleDuration: {
type: Number,
default: 200,
@@ -65,6 +61,10 @@ export default {
type: String,
required: true,
},
+ labelsWebUrl: {
+ type: String,
+ required: true,
+ },
projectId: {
type: Number,
required: true,
@@ -335,8 +335,8 @@ export default {
<board-form
v-if="currentPage"
- :milestone-path="milestonePath"
:labels-path="labelsPath"
+ :labels-web-url="labelsWebUrl"
:project-id="projectId"
:group-id="groupId"
:can-admin-board="canAdminBoard"
diff --git a/app/assets/javascripts/boards/components/issuable_title.vue b/app/assets/javascripts/boards/components/issuable_title.vue
new file mode 100644
index 00000000000..40627a9fab8
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issuable_title.vue
@@ -0,0 +1,21 @@
+<script>
+export default {
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ refPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="issue-title">
+ <p class="gl-font-weight-bold">{{ title }}</p>
+ <p class="gl-mb-0">{{ refPath }}</p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index d90928f35b6..8658f51e5cf 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,10 +1,9 @@
<script>
import { sortBy } from 'lodash';
import { mapState } from 'vuex';
-import { GlLabel, GlTooltipDirective } from '@gitlab/ui';
+import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import { sprintf, __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueDueDate from './issue_due_date.vue';
@@ -15,7 +14,7 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
components: {
GlLabel,
- Icon,
+ GlIcon,
UserAvatarLink,
TooltipOnTruncate,
IssueDueDate,
@@ -31,28 +30,23 @@ export default {
type: Object,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
list: {
type: Object,
required: false,
default: () => ({}),
},
- rootPath: {
- type: String,
- required: true,
- },
updateFilters: {
type: Boolean,
required: false,
default: false,
},
+ },
+ inject: {
groupId: {
type: Number,
- required: false,
- default: null,
+ },
+ rootPath: {
+ type: String,
},
},
data() {
@@ -148,7 +142,7 @@ export default {
<div>
<div class="d-flex board-card-header" dir="auto">
<h4 class="board-card-title gl-mb-0 gl-mt-0">
- <icon
+ <gl-icon
v-if="issue.blocked"
v-gl-tooltip
name="issue-block"
@@ -156,7 +150,7 @@ export default {
class="issue-blocked-icon gl-mr-2"
:aria-label="__('Blocked issue')"
/>
- <icon
+ <gl-icon
v-if="issue.confidential"
v-gl-tooltip
name="eye-slash"
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index 4add5ee646a..fb45de6e14d 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -1,7 +1,6 @@
<script>
import dateFormat from 'dateformat';
-import { GlTooltip } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltip, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import {
getDayDifference,
@@ -12,7 +11,7 @@ import {
export default {
components: {
- Icon,
+ GlIcon,
GlTooltip,
},
props: {
@@ -87,7 +86,7 @@ export default {
<template>
<span>
<span ref="issueDueDate" :class="cssClass" class="board-card-info card-number">
- <icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" />
+ <gl-icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" />
<time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{
body
}}</time>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
index e8b7689da13..fe56833016e 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -1,12 +1,11 @@
<script>
-import { GlTooltip } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltip, GlIcon } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import boardsStore from '../stores/boards_store';
export default {
components: {
- Icon,
+ GlIcon,
GlTooltip,
},
props: {
@@ -34,7 +33,7 @@ export default {
<template>
<span>
<span ref="issueTimeEstimate" class="board-card-info card-number">
- <icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{
+ <gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{
timeEstimate
}}</time>
</span>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index 66f59009714..cd4512f320f 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -1,9 +1,14 @@
<script>
+/* eslint-disable vue/no-v-html */
+import { GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
export default {
+ components: {
+ GlButton,
+ },
mixins: [modalMixin],
props: {
newIssuePath: {
@@ -53,17 +58,22 @@ export default {
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
- <a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted">{{
- __('New issue')
- }}</a>
- <button
+ <gl-button
+ v-if="activeTab === 'all'"
+ :href="newIssuePath"
+ category="secondary"
+ variant="success"
+ >
+ {{ __('New issue') }}
+ </gl-button>
+ <gl-button
v-if="activeTab === 'selected'"
- class="btn btn-default"
- type="button"
+ category="primary"
+ variant="default"
@click="changeTab('all')"
>
{{ __('Open issues') }}
- </button>
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index c4953dda793..d28a03da97f 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __, n__ } from '../../../locale';
@@ -10,6 +11,7 @@ import boardsStore from '../../stores/boards_store';
export default {
components: {
ListsDropdown,
+ GlButton,
},
mixins: [modalMixin, footerEEMixin],
data() {
@@ -65,14 +67,14 @@ export default {
<template>
<footer class="form-actions add-issues-footer">
<div class="float-left">
- <button :disabled="submitDisabled" class="btn btn-success" type="button" @click="addIssues">
+ <gl-button :disabled="submitDisabled" category="primary" variant="success" @click="addIssues">
{{ submitText }}
- </button>
+ </gl-button>
<span class="inline add-issues-footer-to-list">{{ __('to list') }}</span>
<lists-dropdown />
</div>
- <button class="btn btn-default float-right" type="button" @click="toggleModal(false)">
+ <gl-button class="float-right" @click="toggleModal(false)">
{{ __('Cancel') }}
- </button>
+ </gl-button>
</footer>
</template>
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 573284d2b44..3e96ecca24c 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -1,5 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import ModalFilters from './filters';
import ModalTabs from './tabs.vue';
@@ -10,6 +11,7 @@ export default {
components: {
ModalTabs,
ModalFilters,
+ GlButton,
},
mixins: [modalMixin],
props: {
@@ -17,10 +19,6 @@ export default {
type: Number,
required: true,
},
- milestonePath: {
- type: String,
- required: true,
- },
labelPath: {
type: String,
required: true,
@@ -43,7 +41,7 @@ export default {
},
methods: {
toggleAll() {
- this.$refs.selectAllBtn.blur();
+ this.$refs.selectAllBtn.$el.blur();
ModalStore.toggleAll();
},
@@ -55,28 +53,28 @@ export default {
<header class="add-issues-header border-top-0 form-actions">
<h2 class="m-0">
Add issues
- <button
- type="button"
+ <gl-button
+ category="tertiary"
+ icon="close"
class="close"
data-dismiss="modal"
:aria-label="__('Close')"
@click="toggleModal(false)"
- >
- <span aria-hidden="true">×</span>
- </button>
+ />
</h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0" />
<div v-if="showSearch" class="d-flex gl-mb-3">
<modal-filters :store="filter" />
- <button
+ <gl-button
ref="selectAllBtn"
- type="button"
- class="btn btn-success btn-inverted gl-ml-3"
+ category="secondary"
+ variant="success"
+ class="gl-ml-3"
@click="toggleAll"
>
{{ selectAllText }}
- </button>
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 20344b66140..817b3bdddb0 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -26,22 +26,10 @@ export default {
type: String,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
projectId: {
type: Number,
required: true,
},
- milestonePath: {
- type: String,
- required: true,
- },
labelPath: {
type: String,
required: true,
@@ -149,17 +137,8 @@ export default {
class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100"
>
<div class="add-issues-container d-flex flex-column m-auto rounded">
- <modal-header
- :project-id="projectId"
- :milestone-path="milestonePath"
- :label-path="labelPath"
- />
- <modal-list
- v-if="!loading && showList && !filterLoading"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :empty-state-svg="emptyStateSvg"
- />
+ <modal-header :project-id="projectId" :label-path="labelPath" />
+ <modal-list v-if="!loading && showList && !filterLoading" :empty-state-svg="emptyStateSvg" />
<empty-state
v-if="showEmptyState"
:new-issue-path="newIssuePath"
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index 78e3351a79e..219263bd9b9 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -1,23 +1,15 @@
<script>
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue';
export default {
components: {
IssueCardInner,
- Icon,
+ GlIcon,
},
props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
emptyStateSvg: {
type: String,
required: true,
@@ -134,8 +126,8 @@ export default {
class="board-card position-relative p-3 rounded"
@click="toggleIssue($event, issue)"
>
- <issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" />
- <icon
+ <issue-card-inner :issue="issue" />
+ <gl-icon
v-if="issue.selected"
:aria-label="'Issue #' + issue.id + ' selected'"
name="mobile-issue-close"
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
index 3fbe8fe1be7..fe10e7fb856 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -1,13 +1,12 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLink, GlIcon } from '@gitlab/ui';
import ModalStore from '../../stores/modal_store';
import boardsStore from '../../stores/boards_store';
export default {
components: {
GlLink,
- Icon,
+ GlIcon,
},
data() {
return {
@@ -29,7 +28,7 @@ export default {
<div class="dropdown inline">
<button class="dropdown-menu-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
<span :style="{ backgroundColor: selected.label.color }" class="dropdown-label-box"> </span>
- {{ selected.title }} <icon name="chevron-down" />
+ {{ selected.title }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2b9fdf11b37..2e356f1353a 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -6,6 +6,7 @@ import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
$(document)
.off('created.label')
@@ -36,7 +37,7 @@ export default function initNewListDropdown() {
$dropdownToggle.data('projectPath'),
);
- $dropdownToggle.glDropdown({
+ initDeprecatedJQueryDropdown($dropdownToggle, {
data(term, callback) {
axios
.get($dropdownToggle.attr('data-list-labels-path'))
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 598e92726c1..59e7620962a 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -1,30 +1,30 @@
<script>
import $ from 'jquery';
import { escape } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import eventHub from '../eventhub';
import Api from '../../api';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default {
name: 'BoardProjectSelect',
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
props: {
- groupId: {
- type: Number,
- required: true,
- default: 0,
- },
list: {
type: Object,
required: true,
},
},
+ inject: {
+ groupId: {
+ type: Number,
+ },
+ },
data() {
return {
loading: true,
@@ -37,7 +37,7 @@ export default {
},
},
mounted() {
- $(this.$refs.projectsDropdown).glDropdown({
+ initDeprecatedJQueryDropdown($(this.$refs.projectsDropdown), {
filterable: true,
filterRemote: true,
search: {
@@ -105,13 +105,13 @@ export default {
data-toggle="dropdown"
aria-expanded="false"
>
- {{ selectedProjectName }} <icon name="chevron-down" />
+ {{ selectedProjectName }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">{{ __('Projects') }}</div>
<div class="dropdown-input">
<input class="dropdown-input-field" type="search" :placeholder="__('Search projects')" />
- <icon name="search" class="dropdown-input-search" data-hidden="true" />
+ <gl-icon name="search" class="dropdown-input-search" data-hidden="true" />
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><gl-loading-icon /></div>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
new file mode 100644
index 00000000000..8df03ea581f
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -0,0 +1,79 @@
+<script>
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+
+export default {
+ components: { GlButton, GlLoadingIcon },
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ inject: ['canUpdate'],
+ data() {
+ return {
+ edit: false,
+ };
+ },
+ destroyed() {
+ window.removeEventListener('click', this.collapseWhenOffClick);
+ },
+ methods: {
+ collapseWhenOffClick({ target }) {
+ if (!this.$el.contains(target)) {
+ this.collapse();
+ }
+ },
+ expand() {
+ if (this.edit) {
+ return;
+ }
+
+ this.edit = true;
+ this.$emit('changed', this.edit);
+ window.addEventListener('click', this.collapseWhenOffClick);
+ },
+ collapse() {
+ if (!this.edit) {
+ return;
+ }
+
+ this.edit = false;
+ this.$emit('changed', this.edit);
+ window.removeEventListener('click', this.collapseWhenOffClick);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
+ <span class="gl-vertical-align-middle">
+ <span data-testid="title">{{ title }}</span>
+ <gl-loading-icon v-if="loading" inline class="gl-ml-2" />
+ </span>
+ <gl-button
+ v-if="canUpdate"
+ variant="link"
+ class="gl-text-gray-900!"
+ data-testid="edit-button"
+ @click="expand()"
+ >
+ {{ __('Edit') }}
+ </gl-button>
+ </div>
+ <div v-show="!edit" class="gl-text-gray-400" data-testid="collapsed-content">
+ <slot name="collapsed">{{ __('None') }}</slot>
+ </div>
+ <div v-show="edit" data-testid="expanded-content">
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 35c52558cac..2f64014a949 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -15,6 +15,9 @@ export const ListType = {
export const inactiveId = 0;
+export const ISSUABLE = 'issuable';
+export const LIST = 'list';
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index b7966dd869d..fff89832bf0 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -27,6 +27,11 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
updateObject(path) {
this.store.path = path.substr(1);
+ if (gon.features.boardsWithSwimlanes || gon.features.graphqlBoardLists) {
+ boardsStore.updateFiltersUrl();
+ boardsStore.performSearch();
+ }
+
if (this.updateUrl) {
boardsStore.updateFiltersUrl();
}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 971edd71eec..1173c6d0578 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import { mapActions } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
@@ -24,7 +24,6 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
import './models/label';
import './models/assignee';
-import { BoardType } from './constants';
import toggleFocusMode from '~/boards/toggle_focus';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
@@ -42,11 +41,9 @@ import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
+ urlParamsToObject,
} from '~/lib/utils/common_utils';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
-import projectBoardQuery from './queries/project_board.query.graphql';
-import groupQuery from './queries/group_board.query.graphql';
Vue.use(VueApollo);
@@ -85,6 +82,11 @@ export default () => {
BoardAddIssuesModal,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
},
+ provide: {
+ boardId: $boardApp.dataset.boardId,
+ groupId: Number($boardApp.dataset.groupId) || null,
+ rootPath: $boardApp.dataset.rootPath,
+ },
store,
apolloProvider,
data() {
@@ -94,16 +96,14 @@ export default () => {
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
- boardId: $boardApp.dataset.boardId,
disabled: parseBoolean($boardApp.dataset.disabled),
- issueLinkBase: $boardApp.dataset.issueLinkBase,
- rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: boardsStore.detail,
parent: $boardApp.dataset.parent,
};
},
computed: {
+ ...mapState(['isShowingEpicsSwimlanes']),
detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
},
@@ -114,10 +114,15 @@ export default () => {
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
- boardId: this.boardId,
+ boardId: $boardApp.dataset.boardId,
fullPath: $boardApp.dataset.fullPath,
};
- this.setInitialBoardData({ ...endpoints, boardType: this.parent });
+ this.setInitialBoardData({
+ ...endpoints,
+ boardType: this.parent,
+ disabled: this.disabled,
+ showPromotion: parseBoolean($boardApp.getAttribute('data-show-promotion')),
+ });
boardsStore.setEndpoints(endpoints);
boardsStore.rootPath = this.boardsEndpoint;
@@ -125,55 +130,24 @@ export default () => {
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
+ eventHub.$on('performSearch', this.performSearch);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
+ eventHub.$off('performSearch', this.performSearch);
},
mounted() {
this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
this.filterManager.setup();
- boardsStore.disabled = this.disabled;
-
- if (gon.features.graphqlBoardLists) {
- this.$apollo.addSmartQuery('lists', {
- query() {
- return this.parent === BoardType.group ? groupQuery : projectBoardQuery;
- },
- variables() {
- return {
- fullPath: this.state.endpoints.fullPath,
- boardId: `gid://gitlab/Board/${this.boardId}`,
- };
- },
- update(data) {
- return this.getNodes(data);
- },
- result({ data, error }) {
- if (error) {
- throw error;
- }
-
- const lists = this.getNodes(data);
+ this.performSearch();
- lists.forEach(list =>
- boardsStore.addList({
- ...list,
- id: getIdFromGraphQLId(list.id),
- }),
- );
+ boardsStore.disabled = this.disabled;
- boardsStore.addBlankState();
- setPromotionState(boardsStore);
- },
- error() {
- Flash(__('An error occurred while fetching the board lists. Please try again.'));
- },
- });
- } else {
+ if (!gon.features.graphqlBoardLists) {
boardsStore
.all()
.then(res => res.data)
@@ -189,10 +163,22 @@ export default () => {
}
},
methods: {
- ...mapActions(['setInitialBoardData']),
+ ...mapActions([
+ 'setInitialBoardData',
+ 'setFilters',
+ 'fetchEpicsSwimlanes',
+ 'fetchIssuesForAllLists',
+ ]),
updateTokens() {
this.filterManager.updateTokens();
},
+ performSearch() {
+ this.setFilters(convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)));
+ if (gon.features.boardsWithSwimlanes && this.isShowingEpicsSwimlanes) {
+ this.fetchEpicsSwimlanes(false);
+ this.fetchIssuesForAllLists();
+ }
+ },
updateDetailIssue(newIssue, multiSelect = false) {
const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
@@ -354,6 +340,8 @@ export default () => {
class="btn btn-success gl-ml-3"
type="button"
data-placement="bottom"
+ data-track-event="click_button"
+ data-track-label="board_add_issues"
ref="addIssuesButton"
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 98eac35b2ed..822e6d62ab3 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -15,7 +15,7 @@ class ListIssue {
this.labels = [];
this.assignees = [];
this.selected = false;
- this.position = obj.position || obj.relative_position || Infinity;
+ this.position = obj.position || obj.relative_position || obj.relativePosition || Infinity;
this.isFetching = {
subscriptions: true,
};
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index b8b30c958a9..2f6caffbf84 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -47,7 +47,7 @@ class List {
this.loading = true;
this.loadingMore = false;
this.issues = obj.issues || [];
- this.issuesSize = obj.issuesSize ? obj.issuesSize : 0;
+ this.issuesSize = obj.issuesSize || obj.issuesCount || 0;
this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0;
if (obj.label) {
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 73d37459bfe..51bb72b7657 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -27,7 +27,7 @@ export default () => {
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
- projectId: Number(dataset.projectId),
+ projectId: dataset.projectId ? Number(dataset.projectId) : 0,
groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights),
diff --git a/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql
new file mode 100644
index 00000000000..dcfe69222a0
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./board_list.fragment.graphql"
+
+mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean) {
+ boardListCreate(input: { boardId: $boardId, backlog: $backlog }) {
+ list {
+ ...BoardListFragment
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
index 8abd79332fb..d85b736720b 100644
--- a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
+++ b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
@@ -4,7 +4,7 @@ fragment BoardListShared on BoardList {
position
listType
collapsed
- maxIssueCount
+ issuesCount
label {
id
title
diff --git a/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql
new file mode 100644
index 00000000000..b474c9acb93
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./board_list.fragment.graphql"
+
+mutation UpdateBoardList($listId: ID!, $position: Int, $collapsed: Boolean) {
+ updateBoardList(input: { listId: $listId, position: $position, collapsed: $collapsed }) {
+ list {
+ ...BoardListFragment
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql b/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql
deleted file mode 100644
index 724c7884c58..00000000000
--- a/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql
+++ /dev/null
@@ -1,18 +0,0 @@
-#import "./issue.fragment.graphql"
-
-query GroupListIssues($fullPath: ID!, $boardId: ID!) {
- group(fullPath: $fullPath) {
- board(id: $boardId) {
- lists {
- nodes {
- id
- issues {
- nodes {
- ...IssueNode
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/queries/issue.fragment.graphql b/app/assets/javascripts/boards/queries/issue.fragment.graphql
index 89d56b895a4..4b429f875a6 100644
--- a/app/assets/javascripts/boards/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/queries/issue.fragment.graphql
@@ -7,14 +7,10 @@ fragment IssueNode on Issue {
referencePath: reference(full: true)
dueDate
timeEstimate
- weight
confidential
webUrl
subscribed
- blocked
- epic {
- id
- }
+ relativePosition
assignees {
nodes {
...User
diff --git a/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql
new file mode 100644
index 00000000000..ff6aa597f48
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql
@@ -0,0 +1,28 @@
+#import "ee_else_ce/boards/queries/issue.fragment.graphql"
+
+mutation IssueMoveList(
+ $projectPath: ID!
+ $iid: String!
+ $boardId: ID!
+ $fromListId: ID
+ $toListId: ID
+ $moveBeforeId: ID
+ $moveAfterId: ID
+) {
+ issueMoveList(
+ input: {
+ projectPath: $projectPath
+ iid: $iid
+ boardId: $boardId
+ fromListId: $fromListId
+ toListId: $toListId
+ moveBeforeId: $moveBeforeId
+ moveAfterId: $moveAfterId
+ }
+ ) {
+ issue {
+ ...IssueNode
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/lists_issues.query.graphql b/app/assets/javascripts/boards/queries/lists_issues.query.graphql
new file mode 100644
index 00000000000..c66cdf68cf4
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/lists_issues.query.graphql
@@ -0,0 +1,39 @@
+#import "ee_else_ce/boards/queries/issue.fragment.graphql"
+
+query ListIssues(
+ $fullPath: ID!
+ $boardId: ID!
+ $id: ID
+ $filters: BoardIssueInput
+ $isGroup: Boolean = false
+ $isProject: Boolean = false
+) {
+ group(fullPath: $fullPath) @include(if: $isGroup) {
+ board(id: $boardId) {
+ lists(id: $id) {
+ nodes {
+ id
+ issues(filters: $filters) {
+ nodes {
+ ...IssueNode
+ }
+ }
+ }
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ board(id: $boardId) {
+ lists(id: $id) {
+ nodes {
+ id
+ issues(filters: $filters) {
+ nodes {
+ ...IssueNode
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql b/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql
deleted file mode 100644
index 149b76848ef..00000000000
--- a/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql
+++ /dev/null
@@ -1,18 +0,0 @@
-#import "./issue.fragment.graphql"
-
-query ProjectListIssues($fullPath: ID!, $boardId: ID!) {
- project(fullPath: $fullPath) {
- board(id: $boardId) {
- lists {
- nodes {
- id
- issues {
- nodes {
- ...IssueNode
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index b4be7546252..4b81d9c73ef 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,66 +1,248 @@
-import * as types from './mutation_types';
+import Cookies from 'js-cookie';
+import { sortBy, pick } from 'lodash';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import { parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
-import { BoardType } from '~/boards/constants';
-import { formatListIssues } from '../boards_util';
-import groupListsIssuesQuery from '../queries/group_lists_issues.query.graphql';
-import projectListsIssuesQuery from '../queries/project_lists_issues.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { BoardType, ListType, inactiveId } from '~/boards/constants';
+import * as types from './mutation_types';
+import { formatListIssues, fullBoardId } from '../boards_util';
+import boardStore from '~/boards/stores/boards_store';
-const gqlClient = createDefaultClient();
+import listsIssuesQuery from '../queries/lists_issues.query.graphql';
+import projectBoardQuery from '../queries/project_board.query.graphql';
+import groupBoardQuery from '../queries/group_board.query.graphql';
+import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
+import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
+import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
throw new Error('Not implemented!');
};
+export const gqlClient = createDefaultClient();
+
export default {
setInitialBoardData: ({ commit }, data) => {
commit(types.SET_INITIAL_BOARD_DATA, data);
},
- setActiveId({ commit }, id) {
- commit(types.SET_ACTIVE_ID, id);
+ setActiveId({ commit }, { id, sidebarType }) {
+ commit(types.SET_ACTIVE_ID, { id, sidebarType });
},
- fetchLists: () => {
- notImplemented();
+ unsetActiveId({ dispatch }) {
+ dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
},
+ setFilters: ({ commit }, filters) => {
+ const filterParams = pick(filters, [
+ 'assigneeUsername',
+ 'authorUsername',
+ 'labelName',
+ 'milestoneTitle',
+ 'releaseTag',
+ 'search',
+ ]);
+ commit(types.SET_FILTERS, filterParams);
+ },
+
+ fetchLists: ({ commit, state, dispatch }) => {
+ const { endpoints, boardType } = state;
+ const { fullPath, boardId } = endpoints;
+
+ let query;
+ if (boardType === BoardType.group) {
+ query = groupBoardQuery;
+ } else if (boardType === BoardType.project) {
+ query = projectBoardQuery;
+ } else {
+ createFlash(__('Invalid board'));
+ return Promise.reject();
+ }
+
+ const variables = {
+ fullPath,
+ boardId: fullBoardId(boardId),
+ };
+
+ return gqlClient
+ .query({
+ query,
+ variables,
+ })
+ .then(({ data }) => {
+ let { lists } = data[boardType]?.board;
+ // Temporarily using positioning logic from boardStore
+ lists = lists.nodes.map(list =>
+ boardStore.updateListPosition({
+ ...list,
+ doNotFetchIssues: true,
+ }),
+ );
+ commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position'));
+ // Backlog list needs to be created if it doesn't exist
+ if (!lists.find(l => l.type === ListType.backlog)) {
+ dispatch('createList', { backlog: true });
+ }
+ dispatch('showWelcomeList');
+ })
+ .catch(() => {
+ createFlash(
+ __('An error occurred while fetching the board lists. Please reload the page.'),
+ );
+ });
+ },
+
+ // This action only supports backlog list creation at this stage
+ // Future iterations will add the ability to create other list types
+ createList: ({ state, commit, dispatch }, { backlog = false }) => {
+ const { boardId } = state.endpoints;
+ gqlClient
+ .mutate({
+ mutation: createBoardListMutation,
+ variables: {
+ boardId: fullBoardId(boardId),
+ backlog,
+ },
+ })
+ .then(({ data }) => {
+ if (data?.boardListCreate?.errors.length) {
+ commit(types.CREATE_LIST_FAILURE);
+ } else {
+ const list = data.boardListCreate?.list;
+ dispatch('addList', list);
+ }
+ })
+ .catch(() => {
+ commit(types.CREATE_LIST_FAILURE);
+ });
+ },
+
+ addList: ({ state, commit }, list) => {
+ const lists = state.boardLists;
+ // Temporarily using positioning logic from boardStore
+ lists.push(boardStore.updateListPosition({ ...list, doNotFetchIssues: true }));
+ commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position'));
+ },
+
+ showWelcomeList: ({ state, dispatch }) => {
+ if (state.disabled) {
+ return;
+ }
+ if (
+ state.boardLists.find(list => list.type !== ListType.backlog && list.type !== ListType.closed)
+ ) {
+ return;
+ }
+ if (parseBoolean(Cookies.get('issue_board_welcome_hidden'))) {
+ return;
+ }
+
+ dispatch('addList', {
+ id: 'blank',
+ listType: ListType.blank,
+ title: __('Welcome to your issue board!'),
+ position: 0,
+ });
+ },
+
+ showPromotionList: () => {},
+
generateDefaultLists: () => {
notImplemented();
},
- createList: () => {
- notImplemented();
+ moveList: ({ state, commit, dispatch }, { listId, newIndex, adjustmentValue }) => {
+ const { boardLists } = state;
+ const backupList = [...boardLists];
+ const movedList = boardLists.find(({ id }) => id === listId);
+
+ const newPosition = newIndex - 1;
+ const listAtNewIndex = boardLists[newIndex];
+
+ movedList.position = newPosition;
+ listAtNewIndex.position += adjustmentValue;
+ commit(types.MOVE_LIST, {
+ movedList,
+ listAtNewIndex,
+ });
+
+ dispatch('updateList', { listId, position: newPosition, backupList });
},
- updateList: () => {
- notImplemented();
+ updateList: ({ commit }, { listId, position, collapsed, backupList }) => {
+ gqlClient
+ .mutate({
+ mutation: updateBoardListMutation,
+ variables: {
+ listId,
+ position,
+ collapsed,
+ },
+ })
+ .then(({ data }) => {
+ if (data?.updateBoardList?.errors.length) {
+ commit(types.UPDATE_LIST_FAILURE, backupList);
+ }
+ })
+ .catch(() => {
+ commit(types.UPDATE_LIST_FAILURE, backupList);
+ });
},
deleteList: () => {
notImplemented();
},
- fetchIssuesForList: () => {
- notImplemented();
+ fetchIssuesForList: ({ state, commit }, listId) => {
+ const { endpoints, boardType, filterParams } = state;
+ const { fullPath, boardId } = endpoints;
+
+ const variables = {
+ fullPath,
+ boardId: fullBoardId(boardId),
+ id: listId,
+ filters: filterParams,
+ isGroup: boardType === BoardType.group,
+ isProject: boardType === BoardType.project,
+ };
+
+ return gqlClient
+ .query({
+ query: listsIssuesQuery,
+ context: {
+ isSingleRequest: true,
+ },
+ variables,
+ })
+ .then(({ data }) => {
+ const { lists } = data[boardType]?.board;
+ const listIssues = formatListIssues(lists);
+ commit(types.RECEIVE_ISSUES_FOR_LIST_SUCCESS, { listIssues, listId });
+ })
+ .catch(() => commit(types.RECEIVE_ISSUES_FOR_LIST_FAILURE, listId));
},
fetchIssuesForAllLists: ({ state, commit }) => {
commit(types.REQUEST_ISSUES_FOR_ALL_LISTS);
- const { endpoints, boardType } = state;
+ const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
- const query = boardType === BoardType.group ? groupListsIssuesQuery : projectListsIssuesQuery;
-
const variables = {
fullPath,
- boardId: `gid://gitlab/Board/${boardId}`,
+ boardId: fullBoardId(boardId),
+ filters: filterParams,
+ isGroup: boardType === BoardType.group,
+ isProject: boardType === BoardType.project,
};
return gqlClient
.query({
- query,
+ query: listsIssuesQuery,
variables,
})
.then(({ data }) => {
@@ -71,14 +253,56 @@ export default {
.catch(() => commit(types.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE));
},
- moveIssue: () => {
- notImplemented();
+ moveIssue: (
+ { state, commit },
+ { issueId, issueIid, issuePath, fromListId, toListId, moveBeforeId, moveAfterId },
+ ) => {
+ const originalIssue = state.issues[issueId];
+ const fromList = state.issuesByListId[fromListId];
+ const originalIndex = fromList.indexOf(Number(issueId));
+ commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId });
+
+ const { boardId } = state.endpoints;
+ const [fullProjectPath] = issuePath.split(/[#]/);
+
+ gqlClient
+ .mutate({
+ mutation: issueMoveListMutation,
+ variables: {
+ projectPath: fullProjectPath,
+ boardId: fullBoardId(boardId),
+ iid: issueIid,
+ fromListId: getIdFromGraphQLId(fromListId),
+ toListId: getIdFromGraphQLId(toListId),
+ moveBeforeId,
+ moveAfterId,
+ },
+ })
+ .then(({ data }) => {
+ if (data?.issueMoveList?.errors.length) {
+ commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex });
+ } else {
+ const issue = data.issueMoveList?.issue;
+ commit(types.MOVE_ISSUE_SUCCESS, { issue });
+ }
+ })
+ .catch(() =>
+ commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex }),
+ );
},
createNewIssue: () => {
notImplemented();
},
+ addListIssue: ({ commit }, { list, issue, position }) => {
+ commit(types.ADD_ISSUE_TO_LIST, { list, issue, position });
+ },
+
+ addListIssueFailure: ({ commit }, { list, issue }) => {
+ commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issue });
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 30c71d64085..faf4f9ebfd3 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -15,6 +15,7 @@ import {
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '../eventhub';
import { ListType } from '../constants';
import IssueProject from '../models/project';
@@ -303,7 +304,11 @@ const boardsStore = {
onNewListIssueResponse(list, issue, data) {
issue.refreshData(data);
- if (list.issuesSize > 1) {
+ if (
+ !gon.features.boardsWithSwimlanes &&
+ !gon.features.graphqlBoardLists &&
+ list.issues.length > 1
+ ) {
const moveBeforeId = list.issues[1].id;
this.moveIssue(issue.id, null, null, null, moveBeforeId);
}
@@ -513,6 +518,10 @@ const boardsStore = {
eventHub.$emit('updateTokens');
},
+ performSearch() {
+ eventHub.$emit('performSearch');
+ },
+
setListDetail(newList) {
this.detail.list = newList;
},
@@ -706,6 +715,10 @@ const boardsStore = {
},
newIssue(id, issue) {
+ if (typeof id === 'string') {
+ id = getIdFromGraphQLId(id);
+ }
+
return axios.post(this.generateIssuesPath(id), {
issue,
});
@@ -714,6 +727,10 @@ const boardsStore = {
newListIssue(list, issue) {
list.addIssue(issue, null, 0);
list.issuesSize += 1;
+ let listId = list.id;
+ if (typeof listId === 'string') {
+ listId = getIdFromGraphQLId(listId);
+ }
return this.newIssue(list.id, issue)
.then(res => res.data)
@@ -854,21 +871,6 @@ const boardsStore = {
},
refreshIssueData(issue, obj) {
- // issue.id = obj.id;
- // issue.iid = obj.iid;
- // issue.title = obj.title;
- // issue.confidential = obj.confidential;
- // issue.dueDate = obj.due_date || obj.dueDate;
- // issue.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
- // issue.referencePath = obj.reference_path || obj.referencePath;
- // issue.path = obj.real_path || obj.webUrl;
- // issue.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
- // issue.project_id = obj.project_id;
- // issue.timeEstimate = obj.time_estimate || obj.timeEstimate;
- // issue.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
- // issue.blocked = obj.blocked;
- // issue.epic = obj.epic;
-
const convertedObj = convertObjectPropsToCamelCase(obj, {
dropKeys: ['issue_sidebar_endpoint', 'real_path', 'webUrl'],
});
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index 4de1576099d..3688476dc5f 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -1,3 +1,25 @@
+import { inactiveId } from '../constants';
+
export default {
getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
+ isSidebarOpen: state => state.activeId !== inactiveId,
+ isSwimlanesOn: state => {
+ if (!gon?.features?.boardsWithSwimlanes) {
+ return false;
+ }
+
+ return state.isShowingEpicsSwimlanes;
+ },
+ getIssueById: state => id => {
+ return state.issues[id] || {};
+ },
+
+ getIssues: (state, getters) => listId => {
+ const listIssueIds = state.issuesByListId[listId] || [];
+ return listIssueIds.map(id => getters.getIssueById(id));
+ },
+
+ getActiveIssue: state => {
+ return state.issues[state.activeId] || {};
+ },
};
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 0f96dc2e287..f0a283f6161 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -1,25 +1,34 @@
export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
+export const SET_FILTERS = 'SET_FILTERS';
+export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
+export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
+export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
+export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
-export const REQUEST_UPDATE_LIST = 'REQUEST_UPDATE_LIST';
-export const RECEIVE_UPDATE_LIST_SUCCESS = 'RECEIVE_UPDATE_LIST_SUCCESS';
-export const RECEIVE_UPDATE_LIST_ERROR = 'RECEIVE_UPDATE_LIST_ERROR';
+export const MOVE_LIST = 'MOVE_LIST';
+export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST';
export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS';
export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR';
export const REQUEST_ISSUES_FOR_ALL_LISTS = 'REQUEST_ISSUES_FOR_ALL_LISTS';
+export const RECEIVE_ISSUES_FOR_LIST_FAILURE = 'RECEIVE_ISSUES_FOR_LIST_FAILURE';
+export const RECEIVE_ISSUES_FOR_LIST_SUCCESS = 'RECEIVE_ISSUES_FOR_LIST_SUCCESS';
export const RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS = 'RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS';
export const RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE = 'RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE';
export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS';
export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR';
-export const REQUEST_MOVE_ISSUE = 'REQUEST_MOVE_ISSUE';
-export const RECEIVE_MOVE_ISSUE_SUCCESS = 'RECEIVE_MOVE_ISSUE_SUCCESS';
-export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR';
+export const MOVE_ISSUE = 'MOVE_ISSUE';
+export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS';
+export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE';
export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE';
export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
+export const ADD_ISSUE_TO_LIST = 'ADD_ISSUE_TO_LIST';
+export const ADD_ISSUE_TO_LIST_FAILURE = 'ADD_ISSUE_TO_LIST_FAILURE';
export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
+export const UPDATE_ISSUE_BY_ID = 'UPDATE_ISSUE_BY_ID';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index ca9b911ce5b..faeb3e25a71 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,19 +1,55 @@
+import Vue from 'vue';
+import { sortBy, pull } from 'lodash';
+import { formatIssue, moveIssueListHelper } from '../boards_util';
import * as mutationTypes from './mutation_types';
+import { __ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
throw new Error('Not implemented!');
};
+const removeIssueFromList = (state, listId, issueId) => {
+ Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
+};
+
+const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
+ const listIssues = state.issuesByListId[listId];
+ let newIndex = atIndex || 0;
+ if (moveBeforeId) {
+ newIndex = listIssues.indexOf(moveBeforeId) + 1;
+ } else if (moveAfterId) {
+ newIndex = listIssues.indexOf(moveAfterId);
+ }
+ listIssues.splice(newIndex, 0, issueId);
+ Vue.set(state.issuesByListId, listId, listIssues);
+};
+
export default {
- [mutationTypes.SET_INITIAL_BOARD_DATA]: (state, data) => {
- const { boardType, ...endpoints } = data;
+ [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
+ const { boardType, disabled, showPromotion, ...endpoints } = data;
state.endpoints = endpoints;
state.boardType = boardType;
+ state.disabled = disabled;
+ state.showPromotion = showPromotion;
},
- [mutationTypes.SET_ACTIVE_ID](state, id) {
+ [mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
+ state.boardLists = lists;
+ },
+
+ [mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id;
+ state.sidebarType = sidebarType;
+ },
+
+ [mutationTypes.SET_FILTERS](state, filterParams) {
+ state.filterParams = filterParams;
+ },
+
+ [mutationTypes.CREATE_LIST_FAILURE]: state => {
+ state.error = __('An error occurred while creating the list. Please try again.');
},
[mutationTypes.REQUEST_ADD_LIST]: () => {
@@ -28,16 +64,17 @@ export default {
notImplemented();
},
- [mutationTypes.REQUEST_UPDATE_LIST]: () => {
- notImplemented();
- },
-
- [mutationTypes.RECEIVE_UPDATE_LIST_SUCCESS]: () => {
- notImplemented();
+ [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => {
+ const { boardLists } = state;
+ const movedListIndex = state.boardLists.findIndex(l => l.id === movedList.id);
+ Vue.set(boardLists, movedListIndex, movedList);
+ Vue.set(boardLists, movedListIndex.position + 1, listAtNewIndex);
+ Vue.set(state, 'boardLists', sortBy(boardLists, 'position'));
},
- [mutationTypes.RECEIVE_UPDATE_LIST_ERROR]: () => {
- notImplemented();
+ [mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => {
+ state.error = __('An error occurred while updating the list. Please try again.');
+ Vue.set(state, 'boardLists', backupList);
},
[mutationTypes.REQUEST_REMOVE_LIST]: () => {
@@ -52,17 +89,41 @@ export default {
notImplemented();
},
+ [mutationTypes.RECEIVE_ISSUES_FOR_LIST_SUCCESS]: (state, { listIssues, listId }) => {
+ const { listData, issues } = listIssues;
+ Vue.set(state, 'issues', { ...state.issues, ...issues });
+ Vue.set(state.issuesByListId, listId, listData[listId]);
+ const listIndex = state.boardLists.findIndex(l => l.id === listId);
+ Vue.set(state.boardLists[listIndex], 'loading', false);
+ },
+
+ [mutationTypes.RECEIVE_ISSUES_FOR_LIST_FAILURE]: (state, listId) => {
+ state.error = __('An error occurred while fetching the board issues. Please reload the page.');
+ const listIndex = state.boardLists.findIndex(l => l.id === listId);
+ Vue.set(state.boardLists[listIndex], 'loading', false);
+ },
+
[mutationTypes.REQUEST_ISSUES_FOR_ALL_LISTS]: state => {
state.isLoadingIssues = true;
},
- [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS]: (state, listIssues) => {
- state.issuesByListId = listIssues;
+ [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS]: (state, { listData, issues }) => {
+ state.issuesByListId = listData;
+ state.issues = issues;
state.isLoadingIssues = false;
},
+ [mutationTypes.UPDATE_ISSUE_BY_ID]: (state, { issueId, prop, value }) => {
+ if (!state.issues[issueId]) {
+ /* eslint-disable-next-line @gitlab/require-i18n-strings */
+ throw new Error('No issue found.');
+ }
+
+ Vue.set(state.issues[issueId], prop, value);
+ },
+
[mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE]: state => {
- state.listIssueFetchFailure = true;
+ state.error = __('An error occurred while fetching the board issues. Please reload the page.');
state.isLoadingIssues = false;
},
@@ -78,16 +139,38 @@ export default {
notImplemented();
},
- [mutationTypes.REQUEST_MOVE_ISSUE]: () => {
- notImplemented();
+ [mutationTypes.MOVE_ISSUE]: (
+ state,
+ { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId },
+ ) => {
+ const fromList = state.boardLists.find(l => l.id === fromListId);
+ const toList = state.boardLists.find(l => l.id === toListId);
+
+ const issue = moveIssueListHelper(originalIssue, fromList, toList);
+ Vue.set(state.issues, issue.id, issue);
+
+ removeIssueFromList(state, fromListId, issue.id);
+ addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId });
},
- [mutationTypes.RECEIVE_MOVE_ISSUE_SUCCESS]: () => {
- notImplemented();
+ [mutationTypes.MOVE_ISSUE_SUCCESS]: (state, { issue }) => {
+ const issueId = getIdFromGraphQLId(issue.id);
+ Vue.set(state.issues, issueId, formatIssue({ ...issue, id: issueId }));
},
- [mutationTypes.RECEIVE_MOVE_ISSUE_ERROR]: () => {
- notImplemented();
+ [mutationTypes.MOVE_ISSUE_FAILURE]: (
+ state,
+ { originalIssue, fromListId, toListId, originalIndex },
+ ) => {
+ state.error = __('An error occurred while moving the issue. Please try again.');
+ Vue.set(state.issues, originalIssue.id, originalIssue);
+ removeIssueFromList(state, toListId, originalIssue.id);
+ addIssueToList({
+ state,
+ listId: fromListId,
+ issueId: originalIssue.id,
+ atIndex: originalIndex,
+ });
},
[mutationTypes.REQUEST_UPDATE_ISSUE]: () => {
@@ -102,6 +185,18 @@ export default {
notImplemented();
},
+ [mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => {
+ const listIssues = state.issuesByListId[list.id];
+ listIssues.splice(position, 0, issue.id);
+ Vue.set(state.issuesByListId, list.id, listIssues);
+ Vue.set(state.issues, issue.id, issue);
+ },
+
+ [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => {
+ state.error = __('An error occurred while creating the issue. Please try again.');
+ removeIssueFromList(state, list.id, issue.id);
+ },
+
[mutationTypes.SET_CURRENT_PAGE]: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index cb6930774ed..be937d68c6c 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -3,9 +3,17 @@ import { inactiveId } from '~/boards/constants';
export default () => ({
endpoints: {},
boardType: null,
+ disabled: false,
+ showPromotion: false,
isShowingLabels: true,
activeId: inactiveId,
+ sidebarType: '',
+ boardLists: [],
issuesByListId: {},
+ issues: {},
isLoadingIssues: false,
- listIssueFetchFailure: false,
+ filterParams: {},
+ error: undefined,
+ // TODO: remove after ce/ee split of board_content.vue
+ isShowingEpicsSwimlanes: false,
});
diff --git a/app/assets/javascripts/branches/ajax_loading_spinner.js b/app/assets/javascripts/branches/ajax_loading_spinner.js
new file mode 100644
index 00000000000..79f4f919f3d
--- /dev/null
+++ b/app/assets/javascripts/branches/ajax_loading_spinner.js
@@ -0,0 +1,31 @@
+import $ from 'jquery';
+
+export default class AjaxLoadingSpinner {
+ static init() {
+ const $elements = $('.js-ajax-loading-spinner');
+ $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+ }
+
+ static ajaxBeforeSend(e) {
+ const button = e.target;
+ const newButton = document.createElement('button');
+ newButton.classList.add('btn', 'btn-default', 'disabled', 'gl-button');
+ newButton.setAttribute('disabled', 'disabled');
+
+ const spinner = document.createElement('span');
+ spinner.classList.add('align-text-bottom', 'gl-spinner', 'gl-spinner-sm', 'gl-spinner-orange');
+ newButton.appendChild(spinner);
+
+ button.classList.add('hidden');
+ button.parentNode.insertBefore(newButton, button.nextSibling);
+
+ $(button).one('ajax:error', () => {
+ newButton.remove();
+ button.classList.remove('hidden');
+ });
+
+ $(button).one('ajax:success', () => {
+ $(button).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+ });
+ }
+}
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci_lint/components/ci_lint.vue
new file mode 100644
index 00000000000..135d02e4f76
--- /dev/null
+++ b/app/assets/javascripts/ci_lint/components/ci_lint.vue
@@ -0,0 +1,14 @@
+<script>
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template
+ ><div></div
+></template>
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint_results.vue b/app/assets/javascripts/ci_lint/components/ci_lint_results.vue
new file mode 100644
index 00000000000..9fd1bd30c49
--- /dev/null
+++ b/app/assets/javascripts/ci_lint/components/ci_lint_results.vue
@@ -0,0 +1,9 @@
+<script>
+export default {
+ props: {},
+};
+</script>
+
+<template
+ ><div></div
+></template>
diff --git a/app/assets/javascripts/ci_lint/index.js b/app/assets/javascripts/ci_lint/index.js
new file mode 100644
index 00000000000..ed2cf1fe714
--- /dev/null
+++ b/app/assets/javascripts/ci_lint/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import CILint from './components/ci_lint.vue';
+
+export default (containerId = '#js-ci-lint') => {
+ const containerEl = document.querySelector(containerId);
+ const { endpoint } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ render(createElement) {
+ return createElement(CILint, {
+ props: {
+ endpoint,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index 5c79f245f6d..cb1935c863d 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -53,7 +53,7 @@ export default class VariableList {
},
environment_scope: {
// We can't use a `.js-` class here because
- // gl_dropdown replaces the <input> and doesn't copy over the class
+ // deprecated_jquery_dropdown replaces the <input> and doesn't copy over the class
// See https://gitlab.com/gitlab-org/gitlab-foss/issues/42458
selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
default: '*',
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
index d22fef27964..0e09ae108ea 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
@@ -67,7 +67,7 @@ export default {
</script>
<template>
<gl-deprecated-dropdown :text="value">
- <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" />
+ <gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" />
<gl-deprecated-dropdown-item
v-for="environment in filteredResults"
:key="environment"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index 018704bff74..501c82b419e 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -46,6 +46,7 @@ export default {
{
key: 'actions',
label: '',
+ tdClass: 'text-right',
customStyle: { width: '35px' },
},
],
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index ef304c7ccee..564e1d01242 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -1,6 +1,5 @@
import { __ } from '~/locale';
-// eslint-disable import/prefer-default-export
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const displayText = {
diff --git a/app/assets/javascripts/ci_variable_list/store/getters.js b/app/assets/javascripts/ci_variable_list/store/getters.js
index 14b728302f9..619ad54cad6 100644
--- a/app/assets/javascripts/ci_variable_list/store/getters.js
+++ b/app/assets/javascripts/ci_variable_list/store/getters.js
@@ -1,6 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-// Disabling import/prefer-default-export can be
-// removed once a second getter is added to this file
import { uniq } from 'lodash';
export const joinedEnvironments = state => {
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 92517203972..a75646db162 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -237,7 +237,7 @@ export default class Clusters {
}
addBannerCloseHandler(el, status) {
- el.querySelector('.js-close-banner').addEventListener('click', () => {
+ el.querySelector('.js-close').addEventListener('click', () => {
el.classList.add('hidden');
this.setBannerDismissedState(status, true);
});
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index c86db28515f..412260da958 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -1,9 +1,8 @@
<script>
-import { GlLink, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlModalDirective, GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
-import loadingButton from '../../vue_shared/components/loading_button.vue';
import UninstallApplicationButton from './uninstall_application_button.vue';
import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue';
@@ -12,9 +11,10 @@ import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
export default {
components: {
- loadingButton,
+ GlButton,
identicon,
GlLink,
+ GlAlert,
GlSprintf,
UninstallApplicationButton,
UninstallApplicationConfirmationModal,
@@ -382,24 +382,28 @@ export default {
</template>
</div>
- <div
+ <gl-alert
v-if="updateFailed && !isUpdating"
- class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-update-details"
+ variant="danger"
+ :dismissible="false"
+ class="gl-mt-3 gl-mb-0 js-cluster-application-update-details"
>
{{ updateFailureDescription }}
- </div>
+ </gl-alert>
<template v-if="updateAvailable || updateFailed || isUpdating">
<template v-if="updatingNeedsConfirmation">
- <loading-button
+ <gl-button
v-gl-modal-directive="updateModalId"
- class="btn btn-primary js-cluster-application-update-button mt-2"
+ class="js-cluster-application-update-button mt-2"
+ variant="info"
+ category="primary"
:loading="isUpdating"
:disabled="isUpdating"
- :label="updateButtonLabel"
data-qa-selector="update_button_with_confirmation"
:data-qa-application="id"
- />
-
+ >
+ {{ updateButtonLabel }}
+ </gl-button>
<update-application-confirmation-modal
:application="id"
:application-title="title"
@@ -407,16 +411,19 @@ export default {
/>
</template>
- <loading-button
+ <gl-button
v-else
- class="btn btn-primary js-cluster-application-update-button mt-2"
+ class="js-cluster-application-update-button mt-2"
+ variant="info"
+ category="primary"
:loading="isUpdating"
:disabled="isUpdating"
- :label="updateButtonLabel"
data-qa-selector="update_button"
:data-qa-application="id"
@click="updateConfirmed"
- />
+ >
+ {{ updateButtonLabel }}
+ </gl-button>
</template>
</div>
</div>
@@ -431,16 +438,18 @@ export default {
}}</a>
</div>
<div class="btn-group table-action-buttons">
- <loading-button
+ <gl-button
v-if="displayInstallButton"
:loading="installButtonLoading"
:disabled="disabled || installButtonDisabled"
- :label="installButtonLabel"
class="js-cluster-application-install-button"
+ variant="default"
data-qa-selector="install_button"
:data-qa-application="id"
@click="installClicked"
- />
+ >
+ {{ installButtonLabel }}
+ </gl-button>
<uninstall-application-button
v-if="displayUninstallButton"
v-gl-modal-directive="uninstallModalId"
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
index 1236d2a46c9..2617ea0bdea 100644
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -6,8 +6,8 @@ import {
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
+ GlButton,
} from '@gitlab/ui';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { __, s__ } from '~/locale';
@@ -17,7 +17,7 @@ const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
- LoadingButton,
+ GlButton,
ClipboardButton,
GlLoadingIcon,
GlDeprecatedDropdown,
@@ -130,7 +130,7 @@ export default {
<gl-search-box-by-type
v-model.trim="searchQuery"
:placeholder="s__('ClusterIntegration|Search domains')"
- class="m-2"
+ class="gl-m-3"
/>
<gl-deprecated-dropdown-item
v-for="domain in filteredDomains"
@@ -215,13 +215,16 @@ export default {
}}
</p>
- <loading-button
- class="btn-success js-knative-save-domain-button mt-3 ml-3"
+ <gl-button
+ class="js-knative-save-domain-button gl-mt-5 gl-ml-5"
+ variant="success"
+ category="primary"
:loading="saving"
:disabled="saveButtonDisabled"
- :label="saveButtonLabel"
@click="$emit('save')"
- />
+ >
+ {{ saveButtonLabel }}
+ </gl-button>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/clusters/components/new_cluster.vue b/app/assets/javascripts/clusters/components/new_cluster.vue
new file mode 100644
index 00000000000..2e74ad073c5
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/new_cluster.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('ClusterIntegration|Enter the details for your Kubernetes cluster'),
+ information: s__(
+ 'ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{linkStart}documentation%{linkEnd} on Kubernetes',
+ ),
+ },
+ components: {
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ ...mapState(['clusterConnectHelpPath']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h4>{{ $options.i18n.title }}</h4>
+ <p>
+ <gl-sprintf :message="$options.i18n.information">
+ <template #link="{ content }">
+ <gl-link :href="clusterConnectHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index 3e3b102f0aa..c157b04b4f5 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
-import { GlModal, GlButton, GlDeprecatedButton, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import SplitButton from '~/vue_shared/components/split_button.vue';
import { s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
@@ -28,7 +29,6 @@ export default {
SplitButton,
GlModal,
GlButton,
- GlDeprecatedButton,
GlFormInput,
GlSprintf,
},
@@ -174,24 +174,31 @@ export default {
}}</span>
</template>
<template #modal-footer>
- <gl-deprecated-button variant="secondary" @click="handleCancel">{{
- s__('Cancel')
- }}</gl-deprecated-button>
+ <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
- <gl-deprecated-button :disabled="!canSubmit" variant="warning" @click="handleSubmit">{{
- s__('ClusterIntegration|Remove integration')
- }}</gl-deprecated-button>
- <gl-deprecated-button
+ <gl-button
+ :disabled="!canSubmit"
+ variant="warning"
+ category="primary"
+ @click="handleSubmit"
+ >{{ s__('ClusterIntegration|Remove integration') }}</gl-button
+ >
+ <gl-button
:disabled="!canSubmit"
variant="danger"
+ category="primary"
@click="handleSubmit(true)"
- >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-deprecated-button
+ >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-button
>
</template>
<template v-else>
- <gl-deprecated-button :disabled="!canSubmit" variant="danger" @click="handleSubmit">{{
- s__('ClusterIntegration|Remove integration')
- }}</gl-deprecated-button>
+ <gl-button
+ :disabled="!canSubmit"
+ variant="danger"
+ category="primary"
+ @click="handleSubmit"
+ >{{ s__('ClusterIntegration|Remove integration') }}</gl-button
+ >
</template>
</template>
</gl-modal>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
index 8465312d84d..73191d6d84d 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_button.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
@@ -1,5 +1,5 @@
<script>
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { GlButton } from '@gitlab/ui';
import { APPLICATION_STATUS } from '~/clusters/constants';
import { __ } from '~/locale';
@@ -7,7 +7,7 @@ const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
- LoadingButton,
+ GlButton,
},
props: {
status: {
@@ -30,5 +30,7 @@ export default {
</script>
<template>
- <loading-button :label="label" :disabled="disabled" :loading="loading" />
+ <gl-button :disabled="disabled" variant="default" :loading="loading">
+ {{ label }}
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
index e33431d2ea1..f82f4dd5012 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
import { sprintf, s__ } from '~/locale';
diff --git a/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue
index 04aa28e9b74..0aedc6e84fa 100644
--- a/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { ELASTIC_STACK } from '../constants';
diff --git a/app/assets/javascripts/clusters/new_cluster.js b/app/assets/javascripts/clusters/new_cluster.js
new file mode 100644
index 00000000000..71f585fd307
--- /dev/null
+++ b/app/assets/javascripts/clusters/new_cluster.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import NewCluster from './components/new_cluster.vue';
+import { createStore } from './stores/new_cluster';
+
+export default () => {
+ const entryPoint = document.querySelector('#js-cluster-new');
+
+ if (!entryPoint) {
+ return null;
+ }
+
+ return new Vue({
+ el: '#js-cluster-new',
+ store: createStore(entryPoint.dataset),
+ render(createElement) {
+ return createElement(NewCluster);
+ },
+ });
+};
diff --git a/app/assets/javascripts/clusters/stores/new_cluster/index.js b/app/assets/javascripts/clusters/stores/new_cluster/index.js
new file mode 100644
index 00000000000..ae082c07f26
--- /dev/null
+++ b/app/assets/javascripts/clusters/stores/new_cluster/index.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+
+Vue.use(Vuex);
+
+export const createStore = initialState =>
+ new Vuex.Store({
+ state: state(initialState),
+ });
+
+export default createStore;
diff --git a/app/assets/javascripts/clusters/stores/new_cluster/state.js b/app/assets/javascripts/clusters/stores/new_cluster/state.js
new file mode 100644
index 00000000000..1ca1ac8de18
--- /dev/null
+++ b/app/assets/javascripts/clusters/stores/new_cluster/state.js
@@ -0,0 +1,3 @@
+export default (initialState = {}) => ({
+ clusterConnectHelpPath: initialState.clusterConnectHelpPath,
+});
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 09d7c0329a9..7b53020fc49 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -5,7 +5,7 @@ import {
GlLink,
GlLoadingIcon,
GlPagination,
- GlSkeletonLoading,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlSprintf,
GlTable,
} from '@gitlab/ui';
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 1526d994770..2f4118c1717 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -247,7 +247,7 @@ export default {
}}
</p>
<gl-link
- href="/help/ci/merge_request_pipelines/index.html#create-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
+ href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
target="_blank"
>
{{ s__('Pipelines|More Information') }}
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index c4d663dfc8d..334f95bb27f 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -2,6 +2,3 @@ import 'jquery';
// common jQuery plugins
import 'jquery-ujs';
-import 'jquery.caret'; // must be imported before at.js
-import '@gitlab/at.js';
-import 'vendor/jquery.scrollTo';
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 34322755fe9..45ac1bafd61 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -5,6 +5,7 @@ import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from './flash';
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() {
@@ -13,7 +14,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
const $dropdownContainer = $dropdown.closest('.dropdown');
const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
- $dropdown.glDropdown({
+ initDeprecatedJQueryDropdown($dropdown, {
data(term, callback) {
const params = {
ref: $dropdown.data('ref'),
diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
index b8163ecfab2..5b4bdca46e4 100644
--- a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
@@ -1,13 +1,12 @@
<script>
-import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
- Icon,
+ GlIcon,
},
props: {
projects: {
@@ -41,17 +40,17 @@ export default {
<gl-deprecated-dropdown toggle-class="d-flex align-items-center w-100" class="w-100">
<template #button-content>
<span class="str-truncated-100 mr-2">
- <icon name="lock" />
+ <gl-icon name="lock" />
{{ dropdownText }}
</span>
- <icon name="chevron-down" class="ml-auto" />
+ <gl-icon name="chevron-down" class="ml-auto" />
</template>
<gl-deprecated-dropdown-item
v-for="project in projects"
:key="project.id"
@click="selectProject(project)"
>
- <icon
+ <gl-icon
name="mobile-issue-close"
:class="{ icon: project.id !== selectedProject.id }"
class="js-active-project-check"
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 88459d5962e..3a6707bc573 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '../../locale';
import { deprecatedCreateFlash as createFlash } from '../../flash';
import Api from '../../api';
@@ -8,6 +8,7 @@ import Dropdown from './dropdown.vue';
export default {
components: {
+ GlIcon,
GlLink,
GlSprintf,
Dropdown,
@@ -136,7 +137,7 @@ export default {
target="_blank"
>
<span class="sr-only">{{ __('Read more') }}</span>
- <i class="fa fa-question-circle" aria-hidden="true"></i>
+ <gl-icon name="question-o" />
</gl-link>
</p>
</div>
diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js
index 393af932fb0..f941c5aa944 100644
--- a/app/assets/javascripts/contributors/stores/actions.js
+++ b/app/assets/javascripts/contributors/stores/actions.js
@@ -3,7 +3,6 @@ import { __ } from '~/locale';
import service from '../services/contributors_service';
import * as types from './mutation_types';
-// eslint-disable-next-line import/prefer-default-export
export const fetchChartData = ({ commit }, endpoint) => {
commit(types.SET_LOADING_STATE, true);
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index d6c402fcb5d..a653e228e3f 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
import { escape } from 'lodash';
import { GlFormInput, GlFormCheckbox } from '@gitlab/ui';
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
index e063f9edfd9..5c13cbb2775 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
@@ -1,15 +1,15 @@
<script>
-import { GlFormInput } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlFormInput, GlButton } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapState, mapActions } from 'vuex';
import { sprintf, s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
export default {
components: {
GlFormInput,
- LoadingButton,
+ GlButton,
ClipboardButton,
},
props: {
@@ -130,13 +130,15 @@ export default {
<gl-form-input id="eks-provision-role-arn" v-model="roleArn" />
<p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p>
</div>
- <loading-button
- class="js-submit-service-credentials btn-success"
+ <gl-button
+ variant="success"
+ category="primary"
type="submit"
:disabled="submitButtonDisabled"
:loading="isCreatingRole"
- :label="submitButtonLabel"
@click.prevent="createRole({ roleArn, externalId })"
- />
+ >
+ {{ submitButtonLabel }}
+ </gl-button>
</form>
</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/constants.js b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
index a850ba89818..471d6e1f0aa 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/constants.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
@@ -1,2 +1,6 @@
-// eslint-disable-next-line import/prefer-default-export
-export const KUBERNETES_VERSIONS = [{ name: '1.14', value: '1.14' }];
+export const KUBERNETES_VERSIONS = [
+ { name: '1.14', value: '1.14' },
+ { name: '1.15', value: '1.15' },
+ { name: '1.16', value: '1.16', default: true },
+ { name: '1.17', value: '1.17' },
+];
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
index caf2729a4c7..5abff3c7831 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
@@ -56,6 +56,7 @@ export const createCluster = ({ dispatch, state }) => {
environment_scope: state.environmentScope,
managed: state.gitlabManagedCluster,
provider_aws_attributes: {
+ kubernetes_version: state.kubernetesVersion,
region: state.selectedRegion,
vpc_id: state.selectedVpc,
subnet_ids: state.selectedSubnet,
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js b/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js
index bbe4930c191..d8489ca31cf 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js
@@ -1,3 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const subnetValid = ({ selectedSubnet }) =>
Array.isArray(selectedSubnet) && selectedSubnet.length >= 2;
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
index d1337e7ea4a..ed51e95e434 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
@@ -1,6 +1,6 @@
import { KUBERNETES_VERSIONS } from '../constants';
-const [{ value: kubernetesVersion }] = KUBERNETES_VERSIONS;
+const kubernetesVersion = KUBERNETES_VERSIONS.find(version => version.default).value;
export default () => ({
createRolePath: null,
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index ec09dafebcb..75e8523adfa 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -1,5 +1,5 @@
import { escape } from 'lodash';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class CreateItemDropdown {
/**
@@ -28,7 +28,7 @@ export default class CreateItemDropdown {
}
buildDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.getData.bind(this),
filterable: true,
filterRemote: this.getDataRemote,
@@ -67,12 +67,12 @@ export default class CreateItemDropdown {
e.preventDefault();
this.refreshData();
- this.$dropdown.data('glDropdown').selectRowAtIndex();
+ this.$dropdown.data('deprecatedJQueryDropdown').selectRowAtIndex();
}
refreshData() {
// Refresh the dropdown's data, which ends up calling `getData`
- this.$dropdown.data('glDropdown').remote.execute();
+ this.$dropdown.data('deprecatedJQueryDropdown').remote.execute();
}
getData(term, callback) {
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
index f5207b47f69..6f8455e4bcf 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
@@ -1,8 +1,14 @@
<script>
-import { GlFormInput, GlLink, GlFormGroup, GlFormRadioGroup, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlFormInput,
+ GlLink,
+ GlFormGroup,
+ GlFormRadioGroup,
+ GlLoadingIcon,
+ GlIcon,
+} from '@gitlab/ui';
import { debounce } from 'lodash';
import { __, s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import csrf from '~/lib/utils/csrf';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
@@ -37,7 +43,7 @@ export default {
GlFormGroup,
GlFormRadioGroup,
GlLoadingIcon,
- Icon,
+ GlIcon,
},
props: {
formOperation: {
@@ -229,7 +235,7 @@ export default {
{{ s__('Metrics|Must be a valid PromQL query.') }}
<gl-link href="https://prometheus.io/docs/prometheus/latest/querying/basics/" tabindex="-1">
{{ s__('Metrics|Prometheus Query Documentation') }}
- <icon name="external-link" :size="12" />
+ <gl-icon name="external-link" :size="12" />
</gl-link>
</span>
</gl-form-group>
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
index 0db9d2dbcf9..4448d909c9b 100644
--- a/app/assets/javascripts/cycle_analytics/components/banner.vue
+++ b/app/assets/javascripts/cycle_analytics/components/banner.vue
@@ -1,10 +1,11 @@
<script>
+/* eslint-disable vue/no-v-html */
import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
documentationLink: {
@@ -32,7 +33,7 @@ export default {
type="button"
@click="dismissOverviewDialog"
>
- <icon name="close" />
+ <gl-icon name="close" />
</button>
<div class="svg-container" v-html="iconCycleAnalyticsSplash"></div>
<div class="inner-content">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
index dc13f409462..33b4e649ab0 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
@@ -3,14 +3,12 @@ import { GlIcon } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
-import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
userAvatarImage,
totalTime,
limitWarning,
- icon,
GlIcon,
},
props: {
@@ -60,7 +58,7 @@ export default {
</template>
<template v-else>
<span v-if="mergeRequest.branch" class="merge-request-branch">
- <icon :size="16" name="fork" />
+ <gl-icon :size="16" name="fork" />
<a :href="mergeRequest.branch.url"> {{ mergeRequest.branch.name }} </a>
</span>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
index 2a507b7e601..ba2be2e5167 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
@@ -1,16 +1,17 @@
<script>
+/* eslint-disable vue/no-v-html */
+import { GlIcon } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
-import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
userAvatarImage,
totalTime,
limitWarning,
- icon,
+ GlIcon,
},
props: {
items: {
@@ -44,7 +45,7 @@ export default {
<user-avatar-image :img-src="build.author.avatarUrl" />
<h5 class="item-title">
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
- <icon :size="16" name="fork" />
+ <gl-icon :size="16" name="fork" />
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
<span class="icon-branch" v-html="iconBranch"> </span>
<a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
index caff6f9c349..cd49b3c5222 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
@@ -1,15 +1,16 @@
<script>
+/* eslint-disable vue/no-v-html */
+import { GlIcon } from '@gitlab/ui';
import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
-import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
totalTime,
limitWarning,
- icon,
+ GlIcon,
},
props: {
items: {
@@ -46,7 +47,7 @@ export default {
<span class="icon-build-status" v-html="iconBuildStatus"> </span>
<a :href="build.url" class="item-build-name"> {{ build.name }} </a> &middot;
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
- <icon :size="16" name="fork" />
+ <gl-icon :size="16" name="fork" />
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
<span class="icon-branch" v-html="iconBranch"> </span>
<a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 4f9069f61a5..3a160d0532c 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -23,9 +23,6 @@ const EMPTY_STAGE_TEXTS = {
staging: __(
'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
),
- production: __(
- 'The total stage shows the time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
- ),
};
export default {
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index a03a7114b40..0ac16e6b6a0 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
@@ -7,14 +7,13 @@ import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
KeysPanel,
NavigationTabs,
GlLoadingIcon,
- Icon,
+ GlIcon,
},
props: {
endpoint: {
@@ -125,8 +124,8 @@ export default {
/>
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
- <div class="fade-left"><icon name="chevron-lg-left" :size="12" /></div>
- <div class="fade-right"><icon name="chevron-lg-right" :size="12" /></div>
+ <div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div>
+ <div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div>
<navigation-tabs :tabs="tabs" scope="deployKeys" @onChangeTab="onChangeTab" />
</div>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 585b091bc51..5b41d23bd27 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,7 +1,7 @@
<script>
import { head, tail } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
-import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -10,7 +10,7 @@ import actionBtn from './action_btn.vue';
export default {
components: {
actionBtn,
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -130,7 +130,7 @@ export default {
class="label deploy-project-label"
>
<span> {{ firstProject.project.full_name }} </span>
- <icon :name="firstProject.can_push ? 'lock-open' : 'lock'" />
+ <gl-icon :name="firstProject.can_push ? 'lock-open' : 'lock'" />
</a>
<a
v-if="isExpandable"
@@ -151,7 +151,7 @@ export default {
class="label deploy-project-label"
>
<span> {{ deployKeysProject.project.full_name }} </span>
- <icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'" />
+ <gl-icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'" />
</a>
</template>
<span v-else class="text-secondary">{{ __('None') }}</span>
@@ -161,7 +161,7 @@ export default {
<div role="rowheader" class="table-mobile-header">{{ __('Created') }}</div>
<div class="table-mobile-content text-secondary key-created-at">
<span v-tooltip :title="tooltipTitle(deployKey.created_at)">
- <icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span>
+ <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span>
</span>
</div>
</div>
@@ -178,7 +178,7 @@ export default {
class="btn btn-default text-secondary"
data-container="body"
>
- <icon name="pencil" />
+ <gl-icon name="pencil" />
</a>
<action-btn
v-if="isRemovable"
@@ -189,7 +189,7 @@ export default {
type="remove"
data-container="body"
>
- <icon name="remove" />
+ <gl-icon name="remove" />
</action-btn>
<action-btn
v-else-if="isEnabled"
@@ -200,7 +200,7 @@ export default {
type="disable"
data-container="body"
>
- <icon name="cancel" />
+ <gl-icon name="cancel" />
</action-btn>
</div>
</div>
diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js
index 268a37008c5..10333752936 100644
--- a/app/assets/javascripts/deploy_keys/service/index.js
+++ b/app/assets/javascripts/deploy_keys/service/index.js
@@ -2,20 +2,18 @@ import axios from '~/lib/utils/axios_utils';
export default class DeployKeysService {
constructor(endpoint) {
- this.axios = axios.create({
- baseURL: endpoint,
- });
+ this.endpoint = endpoint;
}
getKeys() {
- return this.axios.get().then(response => response.data);
+ return axios.get(this.endpoint).then(response => response.data);
}
enableKey(id) {
- return this.axios.put(`${id}/enable`).then(response => response.data);
+ return axios.put(`${this.endpoint}/${id}/enable`).then(response => response.data);
}
disableKey(id) {
- return this.axios.put(`${id}/disable`).then(response => response.data);
+ return axios.put(`${this.endpoint}/${id}/disable`).then(response => response.data);
}
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
index ec0d0cf6aef..c17f2d2efe4 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
@@ -1,16 +1,13 @@
-/* eslint-disable max-classes-per-file, one-var, consistent-return */
-
+/* eslint-disable consistent-return */
import $ from 'jquery';
import { escape } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import axios from './lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
-import { isObject } from './lib/utils/type_utility';
-import renderItem from './gl_dropdown/render';
-
-const BLUR_KEYCODES = [27, 40];
-
-const HAS_VALUE_CLASS = 'has-value';
+import { isObject } from '~/lib/utils/type_utility';
+import renderItem from './render';
+import { GitLabDropdownRemote } from './gl_dropdown_remote';
+import { GitLabDropdownInput } from './gl_dropdown_input';
+import { GitLabDropdownFilter } from './gl_dropdown_filter';
const LOADING_CLASS = 'is-loading';
@@ -32,216 +29,10 @@ const FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-fil
const NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
-class GitLabDropdownInput {
- constructor(input, options) {
- this.input = input;
- this.options = options;
- this.fieldName = this.options.fieldName || 'field-name';
- const $inputContainer = this.input.parent();
- const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', e => {
- // Clear click
- e.preventDefault();
- e.stopPropagation();
- return this.input
- .val('')
- .trigger('input')
- .focus();
- });
-
- this.input
- .on('keydown', e => {
- const keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', e => {
- let val = e.currentTarget.value || this.options.inputFieldName;
- val = val
- .split(' ')
- .join('-') // replaces space with dash
- .replace(/[^a-zA-Z0-9 -]/g, '')
- .toLowerCase() // replace non alphanumeric
- .replace(/(-)\1+/g, '-'); // replace repeated dashes
- this.cb(this.options.fieldName, val, {}, true);
- this.input
- .closest('.dropdown')
- .find('.dropdown-toggle-text')
- .text(val);
- });
- }
-
- onInput(cb) {
- this.cb = cb;
- }
-}
-
-class GitLabDropdownFilter {
- constructor(input, options) {
- let ref, timeout;
- this.input = input;
- this.options = options;
- // eslint-disable-next-line no-cond-assign
- this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
- const $inputContainer = this.input.parent();
- const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', e => {
- // Clear click
- e.preventDefault();
- e.stopPropagation();
- return this.input
- .val('')
- .trigger('input')
- .focus();
- });
- // Key events
- timeout = '';
- this.input
- .on('keydown', e => {
- const keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', () => {
- if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.addClass(HAS_VALUE_CLASS);
- } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.removeClass(HAS_VALUE_CLASS);
- }
- // Only filter asynchronously only if option remote is set
- if (this.options.remote) {
- clearTimeout(timeout);
- // eslint-disable-next-line no-return-assign
- return (timeout = setTimeout(() => {
- $inputContainer.parent().addClass('is-loading');
-
- return this.options.query(this.input.val(), data => {
- $inputContainer.parent().removeClass('is-loading');
- return this.options.callback(data);
- });
- }, 250));
- }
- return this.filter(this.input.val());
- });
- }
-
- static shouldBlur(keyCode) {
- return BLUR_KEYCODES.indexOf(keyCode) !== -1;
- }
-
- filter(searchText) {
- let group, results, tmp;
- if (this.options.onFilter) {
- this.options.onFilter(searchText);
- }
- const data = this.options.data();
- if (data != null && !this.options.filterByText) {
- results = data;
- if (searchText !== '') {
- // When data is an array of objects therefore [object Array] e.g.
- // [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ]
- if (Array.isArray(data)) {
- results = fuzzaldrinPlus.filter(data, searchText, {
- key: this.options.keys,
- });
- }
- // If data is grouped therefore an [object Object]. e.g.
- // {
- // groupName1: [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ],
- // groupName2: [
- // { prop: 'abc' },
- // { prop: 'def' }
- // ]
- // }
- else if (isObject(data)) {
- results = {};
- Object.keys(data).forEach(key => {
- group = data[key];
- tmp = fuzzaldrinPlus.filter(group, searchText, {
- key: this.options.keys,
- });
- if (tmp.length) {
- results[key] = tmp.map(item => item);
- }
- });
- }
- }
- return this.options.callback(results);
- }
- const elements = this.options.elements();
- if (searchText) {
- // eslint-disable-next-line func-names
- elements.each(function() {
- const $el = $(this);
- const matches = fuzzaldrinPlus.match($el.text().trim(), searchText);
- if (!$el.is('.dropdown-header')) {
- if (matches.length) {
- return $el.show().removeClass('option-hidden');
- }
- return $el.hide().addClass('option-hidden');
- }
- });
- } else {
- elements.show().removeClass('option-hidden');
- }
-
- elements
- .parent()
- .find('.dropdown-menu-empty-item')
- .toggleClass('hidden', elements.is(':visible'));
- }
-}
-
-class GitLabDropdownRemote {
- constructor(dataEndpoint, options) {
- this.dataEndpoint = dataEndpoint;
- this.options = options;
- }
-
- execute() {
- if (typeof this.dataEndpoint === 'string') {
- return this.fetchData();
- } else if (typeof this.dataEndpoint === 'function') {
- if (this.options.beforeSend) {
- this.options.beforeSend();
- }
- return this.dataEndpoint('', data => {
- // Fetch the data by calling the data function
- if (this.options.success) {
- this.options.success(data);
- }
- if (this.options.beforeSend) {
- return this.options.beforeSend();
- }
- });
- }
- }
-
- fetchData() {
- if (this.options.beforeSend) {
- this.options.beforeSend();
- }
-
- // Fetch the data through ajax if the data is a string
- return axios.get(this.dataEndpoint).then(({ data }) => {
- if (this.options.success) {
- return this.options.success(data);
- }
- });
- }
-}
-
-class GitLabDropdown {
+export class GitLabDropdown {
constructor(el1, options) {
- let selector, self;
+ let selector;
+ let self;
this.el = el1;
this.options = options;
this.updateLabel = this.updateLabel.bind(this);
@@ -354,7 +145,8 @@ class GitLabDropdown {
}
});
this.dropdown.on('blur', 'a', e => {
- let $dropdownMenu, $relatedTarget;
+ let $dropdownMenu;
+ let $relatedTarget;
if (e.relatedTarget != null) {
$relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
@@ -421,7 +213,8 @@ class GitLabDropdown {
}
parseData(data) {
- let groupData, html;
+ let groupData;
+ let html;
this.renderedData = data;
if (this.options.filterable && data.length === 0) {
// render no matching results
@@ -636,7 +429,11 @@ class GitLabDropdown {
}
rowClicked(el) {
- let field, groupName, selectedIndex, selectedObject, isMarking;
+ let field;
+ let groupName;
+ let selectedIndex;
+ let selectedObject;
+ let isMarking;
const { fieldName } = this.options;
const isInput = $(this.el).is('input');
if (this.renderedData) {
@@ -790,7 +587,8 @@ class GitLabDropdown {
selector = `.dropdown-page-one ${selector}`;
}
return $('body').on('keydown', e => {
- let $listItems, PREV_INDEX;
+ let $listItems;
+ let PREV_INDEX;
const currentKeyCode = e.which;
if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
e.preventDefault();
@@ -889,13 +687,3 @@ class GitLabDropdown {
return isInput ? field.val('') : field.remove();
}
}
-
-// eslint-disable-next-line func-names
-$.fn.glDropdown = function(opts) {
- // eslint-disable-next-line func-names
- return this.each(function() {
- if (!$.data(this, 'glDropdown')) {
- return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
- }
- });
-};
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
new file mode 100644
index 00000000000..89ffb5f5f79
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
@@ -0,0 +1,135 @@
+/* eslint-disable consistent-return */
+
+import $ from 'jquery';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { isObject } from '~/lib/utils/type_utility';
+
+const BLUR_KEYCODES = [27, 40];
+
+const HAS_VALUE_CLASS = 'has-value';
+
+export class GitLabDropdownFilter {
+ constructor(input, options) {
+ let ref;
+ let timeout;
+ this.input = input;
+ this.options = options;
+ // eslint-disable-next-line no-cond-assign
+ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
+ const $inputContainer = this.input.parent();
+ const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', e => {
+ // Clear click
+ e.preventDefault();
+ e.stopPropagation();
+ return this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ });
+ // Key events
+ timeout = '';
+ this.input
+ .on('keydown', e => {
+ const keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', () => {
+ if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.addClass(HAS_VALUE_CLASS);
+ } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.removeClass(HAS_VALUE_CLASS);
+ }
+ // Only filter asynchronously only if option remote is set
+ if (this.options.remote) {
+ clearTimeout(timeout);
+ // eslint-disable-next-line no-return-assign
+ return (timeout = setTimeout(() => {
+ $inputContainer.parent().addClass('is-loading');
+
+ return this.options.query(this.input.val(), data => {
+ $inputContainer.parent().removeClass('is-loading');
+ return this.options.callback(data);
+ });
+ }, 250));
+ }
+ return this.filter(this.input.val());
+ });
+ }
+
+ static shouldBlur(keyCode) {
+ return BLUR_KEYCODES.indexOf(keyCode) !== -1;
+ }
+
+ filter(searchText) {
+ let group;
+ let results;
+ let tmp;
+ if (this.options.onFilter) {
+ this.options.onFilter(searchText);
+ }
+ const data = this.options.data();
+ if (data != null && !this.options.filterByText) {
+ results = data;
+ if (searchText !== '') {
+ // When data is an array of objects therefore [object Array] e.g.
+ // [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ]
+ if (Array.isArray(data)) {
+ results = fuzzaldrinPlus.filter(data, searchText, {
+ key: this.options.keys,
+ });
+ }
+ // If data is grouped therefore an [object Object]. e.g.
+ // {
+ // groupName1: [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ],
+ // groupName2: [
+ // { prop: 'abc' },
+ // { prop: 'def' }
+ // ]
+ // }
+ else if (isObject(data)) {
+ results = {};
+ Object.keys(data).forEach(key => {
+ group = data[key];
+ tmp = fuzzaldrinPlus.filter(group, searchText, {
+ key: this.options.keys,
+ });
+ if (tmp.length) {
+ results[key] = tmp.map(item => item);
+ }
+ });
+ }
+ }
+ return this.options.callback(results);
+ }
+ const elements = this.options.elements();
+ if (searchText) {
+ // eslint-disable-next-line func-names
+ elements.each(function() {
+ const $el = $(this);
+ const matches = fuzzaldrinPlus.match($el.text().trim(), searchText);
+ if (!$el.is('.dropdown-header')) {
+ if (matches.length) {
+ return $el.show().removeClass('option-hidden');
+ }
+ return $el.hide().addClass('option-hidden');
+ }
+ });
+ } else {
+ elements.show().removeClass('option-hidden');
+ }
+
+ elements
+ .parent()
+ .find('.dropdown-menu-empty-item')
+ .toggleClass('hidden', elements.is(':visible'));
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js
new file mode 100644
index 00000000000..d857071d05f
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js
@@ -0,0 +1,44 @@
+export class GitLabDropdownInput {
+ constructor(input, options) {
+ this.input = input;
+ this.options = options;
+ this.fieldName = this.options.fieldName || 'field-name';
+ const $inputContainer = this.input.parent();
+ const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', e => {
+ // Clear click
+ e.preventDefault();
+ e.stopPropagation();
+ return this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ });
+
+ this.input
+ .on('keydown', e => {
+ const keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', e => {
+ let val = e.currentTarget.value || this.options.inputFieldName;
+ val = val
+ .split(' ')
+ .join('-') // replaces space with dash
+ .replace(/[^a-zA-Z0-9 -]/g, '')
+ .toLowerCase() // replace non alphanumeric
+ .replace(/(-)\1+/g, '-'); // replace repeated dashes
+ this.cb(this.options.fieldName, val, {}, true);
+ this.input
+ .closest('.dropdown')
+ .find('.dropdown-toggle-text')
+ .text(val);
+ });
+ }
+
+ onInput(cb) {
+ this.cb = cb;
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
new file mode 100644
index 00000000000..1f6a2e1f646
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
@@ -0,0 +1,42 @@
+/* eslint-disable consistent-return */
+
+import axios from '../lib/utils/axios_utils';
+
+export class GitLabDropdownRemote {
+ constructor(dataEndpoint, options) {
+ this.dataEndpoint = dataEndpoint;
+ this.options = options;
+ }
+
+ execute() {
+ if (typeof this.dataEndpoint === 'string') {
+ return this.fetchData();
+ } else if (typeof this.dataEndpoint === 'function') {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+ return this.dataEndpoint('', data => {
+ // Fetch the data by calling the data function
+ if (this.options.success) {
+ this.options.success(data);
+ }
+ if (this.options.beforeSend) {
+ return this.options.beforeSend();
+ }
+ });
+ }
+ }
+
+ fetchData() {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+
+ // Fetch the data through ajax if the data is a string
+ return axios.get(this.dataEndpoint).then(({ data }) => {
+ if (this.options.success) {
+ return this.options.success(data);
+ }
+ });
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/index.js b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
new file mode 100644
index 00000000000..90e7f15b5b7
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
@@ -0,0 +1,11 @@
+import $ from 'jquery';
+import { GitLabDropdown } from './gl_dropdown';
+
+export default function initDeprecatedJQueryDropdown($el, opts) {
+ // eslint-disable-next-line func-names
+ return $el.each(function() {
+ if (!$.data(this, 'deprecatedJQueryDropdown')) {
+ $.data(this, 'deprecatedJQueryDropdown', new GitLabDropdown(this, opts));
+ }
+ });
+}
diff --git a/app/assets/javascripts/gl_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 66546aa834f..167bc4c286e 100644
--- a/app/assets/javascripts/gl_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -1,3 +1,5 @@
+import { slugify } from '~/lib/utils/text_utility';
+
const renderersByType = {
divider(element) {
element.classList.add('divider');
@@ -95,15 +97,22 @@ function checkSelected(data, options) {
return options.parent.querySelector(`input[name='${options.fieldName}']`) == null;
}
-function createLink(url, selected, options) {
+function createLink(data, selected, options, index) {
const link = document.createElement('a');
- link.href = url;
+ link.href = getPropertyWithDefault(data, options, 'url', '#');
if (options.icon) {
link.classList.add('d-flex', 'align-items-center');
}
+ if (options.trackSuggestionClickedLabel) {
+ link.setAttribute('data-track-event', 'click_text');
+ link.setAttribute('data-track-label', options.trackSuggestionClickedLabel);
+ link.setAttribute('data-track-value', index);
+ link.setAttribute('data-track-property', slugify(data.category || 'no-category'));
+ }
+
link.classList.toggle('is-active', selected);
return link;
@@ -123,8 +132,7 @@ function assignTextToLink(el, data, options) {
function renderLink(row, data, { options, group, index }) {
const selected = checkSelected(data, options);
- const url = getPropertyWithDefault(data, options, 'url', '#');
- const link = createLink(url, selected, options);
+ const link = createLink(data, selected, options, index);
assignTextToLink(link, data, options);
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue
index 37686dd5a46..970197ef41b 100644
--- a/app/assets/javascripts/design_management/components/delete_button.vue
+++ b/app/assets/javascripts/design_management/components/delete_button.vue
@@ -98,6 +98,7 @@ export default {
:loading="loading"
:icon="buttonIcon"
:disabled="isDeleting || !hasSelectedDesigns"
- />
+ ><slot></slot
+ ></gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 6a20517eed7..845f1aec8cf 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,19 +1,20 @@
<script>
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
+import createFlash from '~/flash';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import allVersionsMixin from '../../mixins/all_versions';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
-import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
import DesignNote from './design_note.vue';
import DesignReplyForm from './design_reply_form.vue';
-import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
+import { hasErrors } from '../../utils/cache_update';
+import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
export default {
components: {
@@ -26,6 +27,7 @@ export default {
GlLink,
ToggleRepliesWidget,
TimeAgoTooltip,
+ GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -62,22 +64,20 @@ export default {
activeDiscussion: {
query: activeDiscussionQuery,
result({ data }) {
- const discussionId = data.activeDiscussion.id;
if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) {
return;
}
- // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists
- // We don't want scrollIntoView to be triggered from the discussion click itself
- if (
- discussionId &&
- data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin &&
- discussionId === this.discussion.notes[0].id
- ) {
- this.$el.scrollIntoView({
- behavior: 'smooth',
- inline: 'start',
- });
- }
+
+ this.$nextTick(() => {
+ // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists.
+ // We don't want scrollIntoView to be triggered from the discussion click itself.
+ if (this.$el && this.shouldScrollToDiscussion(data.activeDiscussion)) {
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ inline: 'start',
+ });
+ }
+ });
},
},
},
@@ -107,8 +107,8 @@ export default {
atVersion: this.designsVersion,
};
},
- isDiscussionHighlighted() {
- return this.discussion.notes[0].id === this.activeDiscussion.id;
+ isDiscussionActive() {
+ return this.discussion.notes.some(({ id }) => id === this.activeDiscussion.id);
},
resolveCheckboxText() {
return this.discussion.resolved
@@ -138,21 +138,10 @@ export default {
},
},
methods: {
- addDiscussionComment(
- store,
- {
- data: { createNote },
- },
- ) {
- updateStoreAfterAddDiscussionComment(
- store,
- createNote,
- getDesignQuery,
- this.designVariables,
- this.discussion.id,
- );
- },
- onDone() {
+ onDone({ data: { createNote } }) {
+ if (hasErrors(createNote)) {
+ createFlash({ message: ADD_DISCUSSION_COMMENT_ERROR });
+ }
this.discussionComment = '';
this.hideForm();
if (this.shouldChangeResolvedStatus) {
@@ -160,14 +149,14 @@ export default {
}
},
onCreateNoteError(err) {
- this.$emit('createNoteError', err);
+ this.$emit('create-note-error', err);
},
hideForm() {
this.isFormRendered = false;
this.discussionComment = '';
},
showForm() {
- this.$emit('openForm', this.discussion.id);
+ this.$emit('open-form', this.discussion.id);
this.isFormRendered = true;
},
toggleResolvedStatus() {
@@ -179,16 +168,24 @@ export default {
})
.then(({ data }) => {
if (data.errors?.length > 0) {
- this.$emit('resolveDiscussionError', data.errors[0]);
+ this.$emit('resolve-discussion-error', data.errors[0]);
}
})
.catch(err => {
- this.$emit('resolveDiscussionError', err);
+ this.$emit('resolve-discussion-error', err);
})
.finally(() => {
this.isResolving = false;
});
},
+ shouldScrollToDiscussion(activeDiscussion) {
+ const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
+ ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
+ ACTIVE_DISCUSSION_SOURCE_TYPES.url,
+ ];
+ const { source } = activeDiscussion;
+ return ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(source) && this.isDiscussionActive;
+ },
},
createNoteMutation,
};
@@ -196,13 +193,12 @@ export default {
<template>
<div class="design-discussion-wrapper">
- <div
- class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center"
+ <gl-badge
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-cursor-pointer"
:class="{ resolved: discussion.resolved }"
- type="button"
>
{{ discussion.index }}
- </div>
+ </gl-badge>
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
data-qa-selector="design_discussion_content"
@@ -211,8 +207,8 @@ export default {
:note="firstNote"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
+ :class="{ 'gl-bg-blue-50': isDiscussionActive }"
+ @error="$emit('update-note-error', $event)"
>
<template v-if="discussion.resolvable" #resolveDiscussion>
<button
@@ -220,7 +216,6 @@ export default {
:class="{ 'is-active': discussion.resolved }"
:title="resolveCheckboxText"
:aria-label="resolveCheckboxText"
- type="button"
class="line-resolve-btn note-action-button gl-mr-3"
data-testid="resolve-button"
@click.stop="toggleResolvedStatus"
@@ -255,8 +250,8 @@ export default {
:note="note"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
+ :class="{ 'gl-bg-blue-50': isDiscussionActive }"
+ @error="$emit('update-note-error', $event)"
/>
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
<reply-placeholder
@@ -272,7 +267,6 @@ export default {
:variables="{
input: mutationPayload,
}"
- :update="addDiscussionComment"
@done="onDone"
@error="onCreateNoteError"
>
@@ -280,8 +274,8 @@ export default {
v-model="discussionComment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
>
<template v-if="discussion.resolvable" #resolveCheckbox>
<label data-testid="resolve-checkbox">
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index 172e61920ef..7f4b3b31024 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -1,12 +1,12 @@
<script>
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignReplyForm from './design_reply_form.vue';
-import { findNoteId } from '../../utils/design_management_utils';
+import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
import { hasErrors } from '../../utils/cache_update';
export default {
@@ -17,9 +17,11 @@ export default {
DesignReplyForm,
ApolloMutation,
GlIcon,
+ GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
props: {
note: {
@@ -46,7 +48,7 @@ export default {
return findNoteId(this.note.id);
},
isNoteLinked() {
- return this.$route.hash === `#note_${this.noteAnchorId}`;
+ return extractDesignNoteId(this.$route.hash) === this.noteAnchorId;
},
mutationPayload() {
return {
@@ -58,11 +60,6 @@ export default {
return !this.isEditing && this.note.userPermissions.adminNote;
},
},
- mounted() {
- if (this.isNoteLinked) {
- this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
- }
- },
methods: {
hideForm() {
this.isEditing = false;
@@ -87,30 +84,30 @@ export default {
:img-alt="author.username"
:img-size="40"
/>
- <div class="d-flex justify-content-between">
+ <div class="gl-display-flex gl-justify-content-space-between">
<div>
- <a
+ <gl-link
v-once
:href="author.webUrl"
class="js-user-link"
:data-user-id="author.id"
:data-username="author.username"
>
- <span class="note-header-author-name bold">{{ author.name }}</span>
- <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
+ <span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span>
+ <span v-if="author.status_tooltip_html" v-safe-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
- </a>
+ </gl-link>
<span class="note-headline-light note-headline-meta">
<span class="system-note-message"> <slot></slot> </span>
- <template v-if="note.createdAt">
- <span class="system-note-separator"></span>
- <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`">
- <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
- </a>
- </template>
+ <gl-link
+ class="note-timestamp system-note-separator gl-display-block gl-mb-2"
+ :href="`#note_${noteAnchorId}`"
+ >
+ <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
+ </gl-link>
</span>
</div>
- <div class="gl-display-flex">
+ <div class="gl-display-flex gl-align-items-baseline">
<slot name="resolveDiscussion"></slot>
<button
v-if="isEditButtonVisible"
@@ -126,9 +123,9 @@ export default {
</div>
<template v-if="!isEditing">
<div
+ v-safe-html="note.bodyHtml"
class="note-text js-note-text md"
data-qa-selector="note_content"
- v-html="note.bodyHtml"
></div>
<slot name="resolvedStatus"></slot>
</template>
@@ -147,9 +144,9 @@ export default {
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
:is-new-comment="false"
- class="mt-5"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ class="gl-mt-5"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
/>
</apollo-mutation>
</timeline-entry-item>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 969034909f2..3754e1dbbc1 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlModal } from '@gitlab/ui';
+import { GlButton, GlModal } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { s__ } from '~/locale';
@@ -7,7 +7,7 @@ export default {
name: 'DesignReplyForm',
components: {
MarkdownField,
- GlDeprecatedButton,
+ GlButton,
GlModal,
},
props: {
@@ -66,13 +66,13 @@ export default {
},
methods: {
submitForm() {
- if (this.hasValue) this.$emit('submitForm');
+ if (this.hasValue) this.$emit('submit-form');
},
cancelComment() {
if (this.hasValue && this.formText !== this.value) {
this.$refs.cancelCommentModal.show();
} else {
- this.$emit('cancelForm');
+ this.$emit('cancel-form');
}
},
focusInput() {
@@ -112,20 +112,21 @@ export default {
</markdown-field>
<slot name="resolveCheckbox"></slot>
<div class="note-form-actions gl-display-flex gl-justify-content-space-between">
- <gl-deprecated-button
+ <gl-button
ref="submitButton"
:disabled="!hasValue || isSaving"
+ category="primary"
variant="success"
type="submit"
data-track-event="click_button"
data-qa-selector="save_comment_button"
- @click="$emit('submitForm')"
+ @click="$emit('submit-form')"
>
{{ buttonText }}
- </gl-deprecated-button>
- <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{
+ </gl-button>
+ <gl-button ref="cancelButton" variant="default" category="primary" @click="cancelComment">{{
__('Cancel')
- }}</gl-deprecated-button>
+ }}</gl-button>
</div>
<gl-modal
ref="cancelCommentModal"
@@ -134,7 +135,7 @@ export default {
:ok-title="modalSettings.okTitle"
:cancel-title="modalSettings.cancelTitle"
modal-id="cancel-comment-modal"
- @ok="$emit('cancelForm')"
+ @ok="$emit('cancel-form')"
>{{ modalSettings.content }}
</gl-modal>
</form>
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 926e7c74802..5c4a3ab5f94 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import DesignNotePin from './design_note_pin.vue';
@@ -236,18 +237,26 @@ export default {
});
},
isNoteInactive(note) {
- return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
+ const discussionNotes = note.discussion.notes.nodes || [];
+
+ return (
+ this.activeDiscussion.id &&
+ !discussionNotes.some(({ id }) => id === this.activeDiscussion.id)
+ );
},
designPinClass(note) {
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
},
},
+ i18n: {
+ newCommentButtonLabel: __('Add comment to design'),
+ },
};
</script>
<template>
<div
- class="position-absolute image-diff-overlay frame"
+ class="gl-absolute gl-top-0 gl-left-0 frame"
:style="overlayStyle"
@mousemove="onOverlayMousemove"
@mouseleave="onNoteMouseup"
@@ -255,26 +264,28 @@ export default {
<button
v-show="!disableCommenting"
type="button"
- class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
+ role="button"
+ :aria-label="$options.i18n.newCommentButtonLabel"
+ class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent design-detail-overlay-add-comment"
data-qa-selector="design_image_button"
@mouseup="onAddCommentMouseup"
></button>
- <template v-for="note in notes">
- <design-note-pin
- v-if="resolvedDiscussionsExpanded || !note.resolved"
- :key="note.id"
- :label="note.index"
- :repositioning="isMovingNote(note.id)"
- :position="
- isMovingNote(note.id) && movingNoteNewPosition
- ? getNotePositionStyle(movingNoteNewPosition)
- : getNotePositionStyle(note.position)
- "
- :class="designPinClass(note)"
- @mousedown.stop="onNoteMousedown($event, note)"
- @mouseup.stop="onNoteMouseup(note)"
- />
- </template>
+
+ <design-note-pin
+ v-for="note in notes"
+ v-if="resolvedDiscussionsExpanded || !note.resolved"
+ :key="note.id"
+ :label="note.index"
+ :repositioning="isMovingNote(note.id)"
+ :position="
+ isMovingNote(note.id) && movingNoteNewPosition
+ ? getNotePositionStyle(movingNoteNewPosition)
+ : getNotePositionStyle(note.position)
+ "
+ :class="designPinClass(note)"
+ @mousedown.stop="onNoteMousedown($event, note)"
+ @mouseup.stop="onNoteMouseup(note)"
+ />
<design-note-pin
v-if="currentCommentForm"
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index e5a3590877e..df425e3b96d 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -8,6 +8,8 @@ import { extractDiscussions, extractParticipants } from '../utils/design_managem
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import DesignDiscussion from './design_notes/design_discussion.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
+import DesignTodoButton from './design_todo_button.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -16,7 +18,9 @@ export default {
GlCollapse,
GlButton,
GlPopover,
+ DesignTodoButton,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
design: {
type: Object,
@@ -37,6 +41,14 @@ export default {
discussionWithOpenForm: '',
};
},
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ issueIid: {
+ default: '',
+ },
+ },
computed: {
discussions() {
return extractDiscussions(this.design.discussions);
@@ -59,6 +71,26 @@ export default {
resolvedCommentsToggleIcon() {
return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right';
},
+ showTodoButton() {
+ return this.glFeatures.designManagementTodoButton;
+ },
+ sidebarWrapperClass() {
+ return {
+ 'gl-pt-0': this.showTodoButton,
+ };
+ },
+ },
+ watch: {
+ isResolvedCommentsPopoverHidden(newVal) {
+ if (!newVal) {
+ this.$refs.resolvedComments.scrollIntoView();
+ }
+ },
+ },
+ mounted() {
+ if (!this.isResolvedCommentsPopoverHidden && this.$refs.resolvedComments) {
+ this.$refs.resolvedComments.$el.scrollIntoView();
+ }
},
methods: {
handleSidebarClick() {
@@ -89,7 +121,14 @@ export default {
</script>
<template>
- <div class="image-notes" @click="handleSidebarClick">
+ <div class="image-notes" :class="sidebarWrapperClass" @click="handleSidebarClick">
+ <div
+ v-if="showTodoButton"
+ class="gl-py-4 gl-mb-4 gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ >
+ <span>{{ __('To-Do') }}</span>
+ <design-todo-button :design="design" @error="$emit('todoError', $event)" />
+ </div>
<h2 class="gl-font-weight-bold gl-mt-0">
{{ issue.title }}
</h2>
@@ -120,15 +159,16 @@ export default {
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:discussion-with-open-form="discussionWithOpenForm"
data-testid="unresolved-discussion"
- @createNoteError="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
- @resolveDiscussionError="$emit('resolveDiscussionError', $event)"
+ @create-note-error="$emit('onDesignDiscussionError', $event)"
+ @update-note-error="$emit('updateNoteError', $event)"
+ @resolve-discussion-error="$emit('resolveDiscussionError', $event)"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- @openForm="updateDiscussionWithOpenForm"
+ @open-form="updateDiscussionWithOpenForm"
/>
<template v-if="resolvedDiscussions.length > 0">
<gl-button
id="resolved-comments"
+ ref="resolvedComments"
data-testid="resolved-comments"
:icon="resolvedCommentsToggleIcon"
variant="link"
@@ -151,9 +191,12 @@ export default {
)
}}
</p>
- <a href="#" rel="noopener noreferrer" target="_blank">{{
- s__('DesignManagement|Learn more about resolving comments')
- }}</a>
+ <a
+ href="https://docs.gitlab.com/ee/user/project/issues/design_management.html#resolve-design-threads"
+ rel="noopener noreferrer"
+ target="_blank"
+ >{{ s__('DesignManagement|Learn more about resolving comments') }}</a
+ >
</gl-popover>
<gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3">
<design-discussion
diff --git a/app/assets/javascripts/design_management/components/design_todo_button.vue b/app/assets/javascripts/design_management/components/design_todo_button.vue
new file mode 100644
index 00000000000..aff4f348d15
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_todo_button.vue
@@ -0,0 +1,168 @@
+<script>
+import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
+import getDesignQuery from '../graphql/queries/get_design.query.graphql';
+import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
+import TodoButton from '~/vue_shared/components/todo_button.vue';
+import allVersionsMixin from '../mixins/all_versions';
+import { updateStoreAfterDeleteDesignTodo } from '../utils/cache_update';
+import { findIssueId, findDesignId } from '../utils/design_management_utils';
+import { CREATE_DESIGN_TODO_ERROR, DELETE_DESIGN_TODO_ERROR } from '../utils/error_messages';
+
+export default {
+ components: {
+ TodoButton,
+ },
+ mixins: [allVersionsMixin],
+ props: {
+ design: {
+ type: Object,
+ required: true,
+ },
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ issueIid: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ todoLoading: false,
+ };
+ },
+ computed: {
+ designVariables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.issueIid,
+ filenames: [this.$route.params.id],
+ atVersion: this.designsVersion,
+ };
+ },
+ designTodoVariables() {
+ return {
+ projectPath: this.projectPath,
+ issueId: findIssueId(this.design.issue.id),
+ designId: findDesignId(this.design.id),
+ issueIid: this.issueIid,
+ filenames: [this.$route.params.id],
+ atVersion: this.designsVersion,
+ };
+ },
+ pendingTodo() {
+ // TODO data structure pending BE MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40555#note_405732940
+ return this.design.currentUserTodos?.nodes[0];
+ },
+ hasPendingTodo() {
+ return Boolean(this.pendingTodo);
+ },
+ },
+ methods: {
+ updateGlobalTodoCount(additionalTodoCount) {
+ const currentCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
+ const todoToggleEvent = new CustomEvent('todo:toggle', {
+ detail: {
+ count: Math.max(currentCount + additionalTodoCount, 0),
+ },
+ });
+
+ document.dispatchEvent(todoToggleEvent);
+ },
+ incrementGlobalTodoCount() {
+ this.updateGlobalTodoCount(1);
+ },
+ decrementGlobalTodoCount() {
+ this.updateGlobalTodoCount(-1);
+ },
+ createTodo() {
+ this.todoLoading = true;
+ return this.$apollo
+ .mutate({
+ mutation: createDesignTodoMutation,
+ variables: this.designTodoVariables,
+ update: (store, { data: { createDesignTodo } }) => {
+ // because this is a @client mutation,
+ // we control what is in errors, and therefore
+ // we are certain that there is at most 1 item in the array
+ const createDesignTodoError = (createDesignTodo.errors || [])[0];
+ if (createDesignTodoError) {
+ this.$emit('error', Error(createDesignTodoError.message));
+ }
+ },
+ })
+ .then(() => {
+ this.incrementGlobalTodoCount();
+ })
+ .catch(err => {
+ this.$emit('error', Error(CREATE_DESIGN_TODO_ERROR));
+ throw err;
+ })
+ .finally(() => {
+ this.todoLoading = false;
+ });
+ },
+ deleteTodo() {
+ if (!this.hasPendingTodo) return Promise.reject();
+
+ const { id } = this.pendingTodo;
+ const { designVariables } = this;
+
+ this.todoLoading = true;
+ return this.$apollo
+ .mutate({
+ mutation: todoMarkDoneMutation,
+ variables: {
+ id,
+ },
+ update(
+ store,
+ {
+ data: { todoMarkDone },
+ },
+ ) {
+ const todoMarkDoneFirstError = (todoMarkDone.errors || [])[0];
+ if (todoMarkDoneFirstError) {
+ this.$emit('error', Error(todoMarkDoneFirstError));
+ } else {
+ updateStoreAfterDeleteDesignTodo(
+ store,
+ todoMarkDone,
+ getDesignQuery,
+ designVariables,
+ );
+ }
+ },
+ })
+ .then(() => {
+ this.decrementGlobalTodoCount();
+ })
+ .catch(err => {
+ this.$emit('error', Error(DELETE_DESIGN_TODO_ERROR));
+ throw err;
+ })
+ .finally(() => {
+ this.todoLoading = false;
+ });
+ },
+ toggleTodo() {
+ if (this.hasPendingTodo) {
+ return this.deleteTodo();
+ }
+
+ return this.createTodo();
+ },
+ },
+};
+</script>
+
+<template>
+ <todo-button
+ issuable-type="design"
+ :issuable-id="design.iid"
+ :is-todo="hasPendingTodo"
+ :loading="todoLoading"
+ @click.stop.prevent="toggleTodo"
+ />
+</template>
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index 292b6e09055..36ea812d92e 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -1,6 +1,5 @@
<script>
import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { n__, __ } from '~/locale';
import { DESIGN_ROUTE_NAME } from '../../router/constants';
@@ -10,7 +9,6 @@ export default {
GlLoadingIcon,
GlIntersectionObserver,
GlIcon,
- Icon,
Timeago,
},
props: {
@@ -127,12 +125,14 @@ export default {
params: { id: filename },
query: $route.query,
}"
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
+ class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
>
- <div class="card-body p-0 d-flex-center overflow-hidden position-relative">
- <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute">
+ <div
+ class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
+ >
+ <div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute">
<span :title="icon.tooltip" :aria-label="icon.tooltip">
- <icon :name="icon.name" :size="18" :class="icon.classes" />
+ <gl-icon :name="icon.name" :size="18" :class="icon.classes" />
</span>
</div>
<gl-intersection-observer @appear="onAppear">
@@ -147,25 +147,28 @@ export default {
v-show="showImage"
:src="imageLink"
:alt="filename"
- class="block mx-auto mw-100 mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
data-qa-selector="design_image"
@load="onImageLoad"
@error="onImageError"
/>
</gl-intersection-observer>
</div>
- <div class="card-footer d-flex w-100">
- <div class="d-flex flex-column str-truncated-100">
- <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{
+ <div class="card-footer gl-display-flex gl-w-full">
+ <div class="gl-display-flex gl-flex-direction-column str-truncated-100">
+ <span class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name">{{
filename
}}</span>
<span v-if="updatedAt" class="str-truncated-100">
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span>
</div>
- <div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary">
- <icon name="comments" class="ml-1" />
- <span :aria-label="notesLabel" class="ml-1">
+ <div
+ v-if="notesCount"
+ class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
+ >
+ <gl-icon name="comments" class="gl-ml-2" />
+ <span :aria-label="notesLabel" class="gl-ml-2">
{{ notesCount }}
</span>
</div>
diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
index a03982cb91b..4a1be7b720a 100644
--- a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
+++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
@@ -1,13 +1,13 @@
<script>
-import { GlNewDropdown, GlNewDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import allVersionsMixin from '../../mixins/all_versions';
import { findVersionId } from '../../utils/design_management_utils';
export default {
components: {
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlSprintf,
},
mixins: [allVersionsMixin],
@@ -63,8 +63,8 @@ export default {
</script>
<template>
- <gl-new-dropdown :text="dropdownText" size="small">
- <gl-new-dropdown-item
+ <gl-dropdown :text="dropdownText" size="small">
+ <gl-dropdown-item
v-for="(version, index) in allVersions"
:key="version.id"
:is-check-item="true"
@@ -76,6 +76,6 @@ export default {
{{ allVersions.length - index }}
</template>
</gl-sprintf>
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/design_management/constants.js b/app/assets/javascripts/design_management/constants.js
index 21ff361a277..63a92ef5ec0 100644
--- a/app/assets/javascripts/design_management/constants.js
+++ b/app/assets/javascripts/design_management/constants.js
@@ -11,6 +11,7 @@ export const VALID_DATA_TRANSFER_TYPE = 'Files';
export const ACTIVE_DISCUSSION_SOURCE_TYPES = {
pin: 'pin',
discussion: 'discussion',
+ url: 'url',
};
export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0'];
diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js
index fae337aa75b..d1fe977b969 100644
--- a/app/assets/javascripts/design_management/graphql.js
+++ b/app/assets/javascripts/design_management/graphql.js
@@ -1,24 +1,70 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { uniqueId } from 'lodash';
+import produce from 'immer';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import axios from '~/lib/utils/axios_utils';
import createDefaultClient from '~/lib/graphql';
import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
+import getDesignQuery from './graphql/queries/get_design.query.graphql';
import typeDefs from './graphql/typedefs.graphql';
+import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils';
+import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages';
+import { addPendingTodoToStore } from './utils/cache_update';
Vue.use(VueApollo);
const resolvers = {
Mutation: {
updateActiveDiscussion: (_, { id = null, source }, { cache }) => {
- const data = cache.readQuery({ query: activeDiscussionQuery });
- data.activeDiscussion = {
- __typename: 'ActiveDiscussion',
- id,
- source,
- };
+ const sourceData = cache.readQuery({ query: activeDiscussionQuery });
+
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.activeDiscussion = {
+ __typename: 'ActiveDiscussion',
+ id,
+ source,
+ };
+ });
+
cache.writeQuery({ query: activeDiscussionQuery, data });
},
+ createDesignTodo: (
+ _,
+ { projectPath, issueId, designId, issueIid, filenames, atVersion },
+ { cache },
+ ) => {
+ return axios
+ .post(`/${projectPath}/todos`, {
+ issue_id: issueId,
+ issuable_id: designId,
+ issuable_type: 'design',
+ })
+ .then(({ data }) => {
+ const { delete_path } = data;
+ const todoId = extractTodoIdFromDeletePath(delete_path);
+ if (!todoId) {
+ return {
+ errors: [
+ {
+ message: CREATE_DESIGN_TODO_EXISTS_ERROR,
+ },
+ ],
+ };
+ }
+
+ const pendingTodo = createPendingTodo(todoId);
+ addPendingTodoToStore(cache, pendingTodo, getDesignQuery, {
+ fullPath: projectPath,
+ iid: issueIid,
+ filenames,
+ atVersion,
+ });
+
+ return pendingTodo;
+ });
+ },
},
};
@@ -37,6 +83,7 @@ const defaultClient = createDefaultClient(
},
},
typeDefs,
+ assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql
index bc3132f9b42..9bd70e7e886 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql
@@ -5,4 +5,9 @@ fragment DesignListItem on Design {
notesCount
image
imageV432x230
+ currentUserTodos(state: pending) {
+ nodes {
+ id
+ }
+ }
}
diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
index 26edd2c0be1..28224671326 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
@@ -25,5 +25,10 @@ fragment DesignNote on Note {
}
discussion {
id
+ notes {
+ nodes {
+ id
+ }
+ }
}
}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql
new file mode 100644
index 00000000000..0c989b2fdde
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql
@@ -0,0 +1,17 @@
+mutation createDesignTodo(
+ $projectPath: String!
+ $issueId: String!
+ $designId: String!
+ $issueIid: String!
+ $filenames: [String]!
+ $atVersion: String
+) {
+ createDesignTodo(
+ projectPath: $projectPath
+ issueId: $issueId
+ designId: $designId
+ issueIid: $issueIid
+ filenames: $filenames
+ atVersion: $atVersion
+ ) @client
+}
diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
index ab987dda525..96869a404b1 100644
--- a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
+++ b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
@@ -10,6 +10,7 @@ query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [Stri
nodes {
...DesignItem
issue {
+ id
title
webPath
webUrl
diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js
index 20c9cacf83f..1a87dd38137 100644
--- a/app/assets/javascripts/design_management/index.js
+++ b/app/assets/javascripts/design_management/index.js
@@ -4,7 +4,7 @@ import App from './components/app.vue';
import apolloProvider from './graphql';
export default () => {
- const el = document.querySelector('.js-design-management-new');
+ const el = document.querySelector('.js-design-management');
const { issueIid, projectPath, issuePath } = el.dataset;
const router = createRouter(issuePath);
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 17b72e73127..c6225c516e2 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -19,6 +19,8 @@ import {
extractDiscussions,
extractDesign,
updateImageDiffNoteOptimisticResponse,
+ toDiffNoteGid,
+ extractDesignNoteId,
} from '../../utils/design_management_utils';
import {
updateStoreAfterAddImageDiffNote,
@@ -31,6 +33,7 @@ import {
DESIGN_NOT_FOUND_ERROR,
DESIGN_VERSION_NOT_EXIST_ERROR,
UPDATE_NOTE_ERROR,
+ TOGGLE_TODO_ERROR,
designDeletionError,
} from '../../utils/error_messages';
import { trackDesignDetailView } from '../../utils/tracking';
@@ -145,8 +148,11 @@ export default {
mounted() {
Mousetrap.bind('esc', this.closeDesign);
this.trackEvent();
- // We need to reset the active discussion when opening a new design
- this.updateActiveDiscussion();
+
+ // Set active discussion immediately.
+ // This will ensure that, if a note is specified in the URL hash,
+ // the browser will scroll to, and highlight, the note in the UI
+ this.updateActiveDiscussionFromUrl();
},
beforeDestroy() {
Mousetrap.unbind('esc', this.closeDesign);
@@ -221,7 +227,7 @@ export default {
},
onError(message, e) {
this.errorMessage = message;
- throw e;
+ if (e) throw e;
},
onCreateImageDiffNoteError(e) {
this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e);
@@ -241,6 +247,9 @@ export default {
onResolveDiscussionError(e) {
this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
},
+ onTodoError(e) {
+ this.onError(e?.message || TOGGLE_TODO_ERROR, e);
+ },
openCommentForm(annotationCoordinates) {
this.annotationCoordinates = annotationCoordinates;
if (this.$refs.newDiscussionForm) {
@@ -266,15 +275,20 @@ export default {
this.isLatestVersion,
);
},
- updateActiveDiscussion(id) {
+ updateActiveDiscussion(id, source = ACTIVE_DISCUSSION_SOURCE_TYPES.discussion) {
this.$apollo.mutate({
mutation: updateActiveDiscussionMutation,
variables: {
id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
+ source,
},
});
},
+ updateActiveDiscussionFromUrl() {
+ const noteId = extractDesignNoteId(this.$route.hash);
+ const diffNoteGid = noteId ? toDiffNoteGid(noteId) : undefined;
+ return this.updateActiveDiscussion(diffNoteGid, ACTIVE_DISCUSSION_SOURCE_TYPES.url);
+ },
toggleResolvedComments() {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
},
@@ -339,6 +353,7 @@ export default {
@updateNoteError="onUpdateNoteError"
@resolveDiscussionError="onResolveDiscussionError"
@toggleResolvedComments="toggleResolvedComments"
+ @todoError="onTodoError"
>
<template #replyForm>
<apollo-mutation
@@ -357,8 +372,8 @@ export default {
v-model="comment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="closeCommentForm"
+ @submit-form="mutate"
+ @cancel-form="closeCommentForm"
/> </apollo-mutation
></template>
</design-sidebar>
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index cd68e9d6c5b..6c4c8c75054 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -281,13 +281,8 @@ export default {
.mutate({
mutation: moveDesignMutation,
variables: this.designMoveVariables(newIndex, element),
- update: (store, { data: { designManagementMove } }) => {
- return updateDesignsOnStoreAfterReorder(
- store,
- designManagementMove,
- this.projectQueryBody,
- );
- },
+ update: (store, { data: { designManagementMove } }) =>
+ updateDesignsOnStoreAfterReorder(store, designManagementMove, this.projectQueryBody),
optimisticResponse: moveDesignOptimisticResponse(this.reorderedDesigns),
})
.catch(() => {
@@ -327,7 +322,7 @@ export default {
v-if="isLatestVersion"
variant="link"
size="small"
- class="gl-mr-3 js-select-all"
+ class="gl-mr-4 js-select-all"
@click="toggleDesignsSelection"
>{{ selectAllButtonText }}
</gl-button>
diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js
index b79df9d01d5..ff41136fd54 100644
--- a/app/assets/javascripts/design_management/utils/cache_update.js
+++ b/app/assets/javascripts/design_management/utils/cache_update.js
@@ -1,22 +1,27 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { groupBy } from 'lodash';
+import produce from 'immer';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { extractCurrentDiscussion, extractDesign } from './design_management_utils';
+import { extractCurrentDiscussion, extractDesign, extractDesigns } from './design_management_utils';
import {
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
- ADD_DISCUSSION_COMMENT_ERROR,
+ DELETE_DESIGN_TODO_ERROR,
designDeletionError,
} from './error_messages';
+const designsOf = data => data.project.issue.designCollection.designs;
+
const deleteDesignsFromStore = (store, query, selectedDesigns) => {
- const data = store.readQuery(query);
+ const sourceData = store.readQuery(query);
- const changedDesigns = data.project.issue.designCollection.designs.nodes.filter(
- node => !selectedDesigns.includes(node.filename),
- );
- data.project.issue.designCollection.designs.nodes = [...changedDesigns];
+ const data = produce(sourceData, draftData => {
+ const changedDesigns = designsOf(sourceData).nodes.filter(
+ design => !selectedDesigns.includes(design.filename),
+ );
+ designsOf(draftData).nodes = [...changedDesigns];
+ });
store.writeQuery({
...query,
@@ -33,13 +38,15 @@ const deleteDesignsFromStore = (store, query, selectedDesigns) => {
*/
const addNewVersionToStore = (store, query, version) => {
if (!version) return;
+ const sourceData = store.readQuery(query);
- const data = store.readQuery(query);
-
- data.project.issue.designCollection.versions.nodes = [
- version,
- ...data.project.issue.designCollection.versions.nodes,
- ];
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.issue.designCollection.versions.nodes = [
+ version,
+ ...draftData.project.issue.designCollection.versions.nodes,
+ ];
+ });
store.writeQuery({
...query,
@@ -47,47 +54,12 @@ const addNewVersionToStore = (store, query, version) => {
});
};
-const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => {
- const data = store.readQuery({
- query,
- variables: queryVariables,
- });
-
- const design = extractDesign(data);
- const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId);
- currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note];
-
- design.notesCount += 1;
- if (
- !design.issue.participants.nodes.some(
- participant => participant.username === createNote.note.author.username,
- )
- ) {
- design.issue.participants.nodes = [
- ...design.issue.participants.nodes,
- {
- __typename: 'User',
- ...createNote.note.author,
- },
- ];
- }
- store.writeQuery({
- query,
- variables: queryVariables,
- data: {
- ...data,
- design: {
- ...design,
- },
- },
- });
-};
-
const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => {
- const data = store.readQuery({
+ const sourceData = store.readQuery({
query,
variables,
});
+
const newDiscussion = {
__typename: 'Discussion',
id: createImageDiffNote.note.discussion.id,
@@ -101,100 +73,100 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) =
nodes: [createImageDiffNote.note],
},
};
- const design = extractDesign(data);
- const notesCount = design.notesCount + 1;
- design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
- if (
- !design.issue.participants.nodes.some(
- participant => participant.username === createImageDiffNote.note.author.username,
- )
- ) {
- design.issue.participants.nodes = [
- ...design.issue.participants.nodes,
- {
- __typename: 'User',
- ...createImageDiffNote.note.author,
- },
- ];
- }
+
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ design.notesCount += 1;
+ design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
+
+ if (
+ !design.issue.participants.nodes.some(
+ participant => participant.username === createImageDiffNote.note.author.username,
+ )
+ ) {
+ design.issue.participants.nodes = [
+ ...design.issue.participants.nodes,
+ {
+ __typename: 'User',
+ ...createImageDiffNote.note.author,
+ },
+ ];
+ }
+ });
+
store.writeQuery({
query,
variables,
- data: {
- ...data,
- design: {
- ...design,
- notesCount,
- },
- },
+ data,
});
};
const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => {
- const data = store.readQuery({
+ const sourceData = store.readQuery({
query,
variables,
});
- const design = extractDesign(data);
- const discussion = extractCurrentDiscussion(
- design.discussions,
- updateImageDiffNote.note.discussion.id,
- );
-
- discussion.notes = {
- ...discussion.notes,
- nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
- };
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ const discussion = extractCurrentDiscussion(
+ design.discussions,
+ updateImageDiffNote.note.discussion.id,
+ );
+
+ discussion.notes = {
+ ...discussion.notes,
+ nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
+ };
+ });
store.writeQuery({
query,
variables,
- data: {
- ...data,
- design,
- },
+ data,
});
};
const addNewDesignToStore = (store, designManagementUpload, query) => {
- const data = store.readQuery(query);
+ const sourceData = store.readQuery(query);
- const currentDesigns = data.project.issue.designCollection.designs.nodes;
- const existingDesigns = groupBy(currentDesigns, 'filename');
- const newDesigns = currentDesigns.concat(
- designManagementUpload.designs.filter(d => !existingDesigns[d.filename]),
- );
+ const data = produce(sourceData, draftData => {
+ const currentDesigns = extractDesigns(draftData);
+ const existingDesigns = groupBy(currentDesigns, 'filename');
+ const newDesigns = currentDesigns.concat(
+ designManagementUpload.designs.filter(d => !existingDesigns[d.filename]),
+ );
- let newVersionNode;
- const findNewVersions = designManagementUpload.designs.find(design => design.versions);
+ let newVersionNode;
+ const findNewVersions = designManagementUpload.designs.find(design => design.versions);
- if (findNewVersions) {
- const findNewVersionsNodes = findNewVersions.versions.nodes;
+ if (findNewVersions) {
+ const findNewVersionsNodes = findNewVersions.versions.nodes;
- if (findNewVersionsNodes && findNewVersionsNodes.length) {
- newVersionNode = [findNewVersionsNodes[0]];
+ if (findNewVersionsNodes && findNewVersionsNodes.length) {
+ newVersionNode = [findNewVersionsNodes[0]];
+ }
}
- }
-
- const newVersions = [
- ...(newVersionNode || []),
- ...data.project.issue.designCollection.versions.nodes,
- ];
- const updatedDesigns = {
- __typename: 'DesignCollection',
- designs: {
- __typename: 'DesignConnection',
- nodes: newDesigns,
- },
- versions: {
- __typename: 'DesignVersionConnection',
- nodes: newVersions,
- },
- };
+ const newVersions = [
+ ...(newVersionNode || []),
+ ...draftData.project.issue.designCollection.versions.nodes,
+ ];
- data.project.issue.designCollection = updatedDesigns;
+ const updatedDesigns = {
+ __typename: 'DesignCollection',
+ designs: {
+ __typename: 'DesignConnection',
+ nodes: newDesigns,
+ },
+ versions: {
+ __typename: 'DesignVersionConnection',
+ nodes: newVersions,
+ },
+ };
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.issue.designCollection = updatedDesigns;
+ });
store.writeQuery({
...query,
@@ -203,14 +175,63 @@ const addNewDesignToStore = (store, designManagementUpload, query) => {
};
const moveDesignInStore = (store, designManagementMove, query) => {
- const data = store.readQuery(query);
- data.project.issue.designCollection.designs = designManagementMove.designCollection.designs;
+ const sourceData = store.readQuery(query);
+
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.issue.designCollection.designs =
+ designManagementMove.designCollection.designs;
+ });
+
store.writeQuery({
...query,
data,
});
};
+export const addPendingTodoToStore = (store, pendingTodo, query, queryVariables) => {
+ const sourceData = store.readQuery({
+ query,
+ variables: queryVariables,
+ });
+
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ const existingTodos = design.currentUserTodos?.nodes || [];
+ const newTodoNodes = [...existingTodos, { ...pendingTodo, __typename: 'Todo' }];
+
+ if (!design.currentUserTodos) {
+ design.currentUserTodos = {
+ __typename: 'TodoConnection',
+ nodes: newTodoNodes,
+ };
+ } else {
+ design.currentUserTodos.nodes = newTodoNodes;
+ }
+ });
+
+ store.writeQuery({ query, variables: queryVariables, data });
+};
+
+export const deletePendingTodoFromStore = (store, todoMarkDone, query, queryVariables) => {
+ const sourceData = store.readQuery({
+ query,
+ variables: queryVariables,
+ });
+
+ const {
+ todo: { id: todoId },
+ } = todoMarkDone;
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ const existingTodos = design.currentUserTodos?.nodes || [];
+
+ design.currentUserTodos.nodes = existingTodos.filter(({ id }) => id !== todoId);
+ });
+
+ store.writeQuery({ query, variables: queryVariables, data });
+};
+
const onError = (data, message) => {
createFlash(message);
throw new Error(data.errors);
@@ -235,20 +256,6 @@ export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
}
};
-export const updateStoreAfterAddDiscussionComment = (
- store,
- data,
- query,
- queryVariables,
- discussionId,
-) => {
- if (hasErrors(data)) {
- onError(data, ADD_DISCUSSION_COMMENT_ERROR);
- } else {
- addDiscussionCommentToStore(store, data, query, queryVariables, discussionId);
- }
-};
-
export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => {
if (hasErrors(data)) {
onError(data, ADD_IMAGE_DIFF_NOTE_ERROR);
@@ -280,3 +287,11 @@ export const updateDesignsOnStoreAfterReorder = (store, data, query) => {
moveDesignInStore(store, data, query);
}
};
+
+export const updateStoreAfterDeleteDesignTodo = (store, data, query, queryVariables) => {
+ if (hasErrors(data)) {
+ onError(data, DELETE_DESIGN_TODO_ERROR);
+ } else {
+ deletePendingTodoFromStore(store, data, query, queryVariables);
+ }
+};
diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js
index da8f89ff960..93e4d6060c3 100644
--- a/app/assets/javascripts/design_management/utils/design_management_utils.js
+++ b/app/assets/javascripts/design_management/utils/design_management_utils.js
@@ -30,10 +30,25 @@ export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1];
export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1];
+export const findIssueId = id => (id.match('Issue/(.+$)') || [])[1];
+
+export const findDesignId = id => (id.match('Design/(.+$)') || [])[1];
+
export const extractDesigns = data => data.project.issue.designCollection.designs.nodes;
export const extractDesign = data => (extractDesigns(data) || [])[0];
+export const toDiffNoteGid = noteId => `gid://gitlab/DiffNote/${noteId}`;
+
+/**
+ * Return the note ID from a URL hash parameter
+ * @param {String} urlHash URL hash, including `#` prefix
+ */
+export const extractDesignNoteId = urlHash => {
+ const [, noteId] = urlHash.match('#note_([0-9]+$)') || [];
+ return noteId || null;
+};
+
/**
* Generates optimistic response for a design upload mutation
* @param {Array<File>} files
@@ -135,3 +150,22 @@ const normalizeAuthor = author => ({
export const extractParticipants = users => users.map(node => normalizeAuthor(node));
export const getPageLayoutElement = () => document.querySelector('.layout-page');
+
+/**
+ * Extract the ID of the To-Do for a given 'delete' path
+ * Example of todoDeletePath: /delete/1234
+ * @param {String} todoDeletePath delete_path from REST API response
+ */
+export const extractTodoIdFromDeletePath = todoDeletePath =>
+ (todoDeletePath.match('todos/([0-9]+$)') || [])[1];
+
+const createTodoGid = todoId => {
+ return `gid://gitlab/Todo/${todoId}`;
+};
+
+export const createPendingTodo = todoId => {
+ return {
+ __typename: 'Todo', // eslint-disable-line @gitlab/require-i18n-strings
+ id: createTodoGid(todoId),
+ };
+};
diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js
index c815b11737d..bd21d711462 100644
--- a/app/assets/javascripts/design_management/utils/error_messages.js
+++ b/app/assets/javascripts/design_management/utils/error_messages.js
@@ -44,6 +44,14 @@ export const MOVE_DESIGN_ERROR = __(
'Something went wrong when reordering designs. Please try again',
);
+export const CREATE_DESIGN_TODO_ERROR = __('Failed to create To-Do for the design.');
+
+export const CREATE_DESIGN_TODO_EXISTS_ERROR = __('There is already a To-Do for this design.');
+
+export const DELETE_DESIGN_TODO_ERROR = __('Failed to remove To-Do for the design.');
+
+export const TOGGLE_TODO_ERROR = __('Failed to toggle To-Do for the design.');
+
const MAX_SKIPPED_FILES_LISTINGS = 5;
const oneDesignSkippedMessage = filename =>
diff --git a/app/assets/javascripts/design_management/utils/tracking.js b/app/assets/javascripts/design_management/utils/tracking.js
index b3ecc1453a6..49fa306914c 100644
--- a/app/assets/javascripts/design_management/utils/tracking.js
+++ b/app/assets/javascripts/design_management/utils/tracking.js
@@ -5,7 +5,6 @@ const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_contex
const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
const DESIGN_TRACKING_EVENT_NAME = 'view_design';
-// eslint-disable-next-line import/prefer-default-export
export function trackDesignDetailView(
referer = '',
owner = '',
diff --git a/app/assets/javascripts/design_management_legacy/components/app.vue b/app/assets/javascripts/design_management_legacy/components/app.vue
deleted file mode 100644
index 98240aef810..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/app.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-<template>
- <router-view />
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/delete_button.vue b/app/assets/javascripts/design_management_legacy/components/delete_button.vue
deleted file mode 100644
index 1fd902c9ed7..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/delete_button.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<script>
-import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-
-export default {
- name: 'DeleteButton',
- components: {
- GlDeprecatedButton,
- GlModal,
- },
- directives: {
- GlModalDirective,
- },
- props: {
- isDeleting: {
- type: Boolean,
- required: false,
- default: false,
- },
- buttonClass: {
- type: String,
- required: false,
- default: '',
- },
- buttonVariant: {
- type: String,
- required: false,
- default: '',
- },
- hasSelectedDesigns: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- data() {
- return {
- modalId: uniqueId('design-deletion-confirmation-'),
- };
- },
-};
-</script>
-
-<template>
- <div>
- <gl-modal
- :modal-id="modalId"
- :title="s__('DesignManagement|Delete designs confirmation')"
- :ok-title="s__('DesignManagement|Delete')"
- ok-variant="danger"
- @ok="$emit('deleteSelectedDesigns')"
- >
- <p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p>
- </gl-modal>
- <gl-deprecated-button
- v-gl-modal-directive="modalId"
- :variant="buttonVariant"
- :disabled="isDeleting || !hasSelectedDesigns"
- :class="buttonClass"
- >
- <slot></slot>
- </gl-deprecated-button>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue b/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue
deleted file mode 100644
index 62460ca551c..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
-import { ApolloMutation } from 'vue-apollo';
-import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import destroyDesignMutation from '../graphql/mutations/destroy_design.mutation.graphql';
-import { updateStoreAfterDesignsDelete } from '../utils/cache_update';
-
-export default {
- components: {
- ApolloMutation,
- },
- props: {
- filenames: {
- type: Array,
- required: true,
- },
- projectPath: {
- type: String,
- required: true,
- },
- iid: {
- type: String,
- required: true,
- },
- },
- computed: {
- projectQueryBody() {
- return {
- query: getDesignListQuery,
- variables: { fullPath: this.projectPath, iid: this.iid, atVersion: null },
- };
- },
- },
- methods: {
- updateStoreAfterDelete(
- store,
- {
- data: { designManagementDelete },
- },
- ) {
- updateStoreAfterDesignsDelete(
- store,
- designManagementDelete,
- this.projectQueryBody,
- this.filenames,
- );
- },
- },
- destroyDesignMutation,
-};
-</script>
-
-<template>
- <apollo-mutation
- #default="{ mutate, loading, error }"
- :mutation="$options.destroyDesignMutation"
- :variables="{
- filenames,
- projectPath,
- iid,
- }"
- :update="updateStoreAfterDelete"
- v-on="$listeners"
- >
- <slot v-bind="{ mutate, loading, error }"></slot>
- </apollo-mutation>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue b/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue
deleted file mode 100644
index 2b5e62c2870..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-
-export default {
- name: 'DesignNotePin',
- components: {
- GlIcon,
- },
- props: {
- position: {
- type: Object,
- required: true,
- },
- label: {
- type: Number,
- required: false,
- default: null,
- },
- repositioning: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- isNewNote() {
- return this.label === null;
- },
- pinStyle() {
- return this.repositioning ? { ...this.position, cursor: 'move' } : this.position;
- },
- pinLabel() {
- return this.isNewNote
- ? __('Comment form position')
- : sprintf(__("Comment '%{label}' position"), { label: this.label });
- },
- },
-};
-</script>
-
-<template>
- <button
- :style="pinStyle"
- :aria-label="pinLabel"
- :class="{
- 'btn-transparent comment-indicator': isNewNote,
- 'js-image-badge badge badge-pill': !isNewNote,
- }"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0"
- type="button"
- @mousedown="$emit('mousedown', $event)"
- @mouseup="$emit('mouseup', $event)"
- @click="$emit('click', $event)"
- >
- <gl-icon v-if="isNewNote" name="image-comment-dark" :size="24" />
- <template v-else>
- {{ label }}
- </template>
- </button>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue
deleted file mode 100644
index 6a20517eed7..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue
+++ /dev/null
@@ -1,297 +0,0 @@
-<script>
-import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import allVersionsMixin from '../../mixins/all_versions';
-import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
-import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
-import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
-import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
-import DesignNote from './design_note.vue';
-import DesignReplyForm from './design_reply_form.vue';
-import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
-import ToggleRepliesWidget from './toggle_replies_widget.vue';
-
-export default {
- components: {
- ApolloMutation,
- DesignNote,
- ReplyPlaceholder,
- DesignReplyForm,
- GlIcon,
- GlLoadingIcon,
- GlLink,
- ToggleRepliesWidget,
- TimeAgoTooltip,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [allVersionsMixin],
- props: {
- discussion: {
- type: Object,
- required: true,
- },
- noteableId: {
- type: String,
- required: true,
- },
- designId: {
- type: String,
- required: true,
- },
- markdownPreviewPath: {
- type: String,
- required: false,
- default: '',
- },
- resolvedDiscussionsExpanded: {
- type: Boolean,
- required: true,
- },
- discussionWithOpenForm: {
- type: String,
- required: true,
- },
- },
- apollo: {
- activeDiscussion: {
- query: activeDiscussionQuery,
- result({ data }) {
- const discussionId = data.activeDiscussion.id;
- if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) {
- return;
- }
- // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists
- // We don't want scrollIntoView to be triggered from the discussion click itself
- if (
- discussionId &&
- data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin &&
- discussionId === this.discussion.notes[0].id
- ) {
- this.$el.scrollIntoView({
- behavior: 'smooth',
- inline: 'start',
- });
- }
- },
- },
- },
- data() {
- return {
- discussionComment: '',
- isFormRendered: false,
- activeDiscussion: {},
- isResolving: false,
- shouldChangeResolvedStatus: false,
- areRepliesCollapsed: this.discussion.resolved,
- };
- },
- computed: {
- mutationPayload() {
- return {
- noteableId: this.noteableId,
- body: this.discussionComment,
- discussionId: this.discussion.id,
- };
- },
- designVariables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- filenames: [this.$route.params.id],
- atVersion: this.designsVersion,
- };
- },
- isDiscussionHighlighted() {
- return this.discussion.notes[0].id === this.activeDiscussion.id;
- },
- resolveCheckboxText() {
- return this.discussion.resolved
- ? s__('DesignManagement|Unresolve thread')
- : s__('DesignManagement|Resolve thread');
- },
- firstNote() {
- return this.discussion.notes[0];
- },
- discussionReplies() {
- return this.discussion.notes.slice(1);
- },
- areRepliesShown() {
- return !this.discussion.resolved || !this.areRepliesCollapsed;
- },
- resolveIconName() {
- return this.discussion.resolved ? 'check-circle-filled' : 'check-circle';
- },
- isRepliesWidgetVisible() {
- return this.discussion.resolved && this.discussionReplies.length > 0;
- },
- isReplyPlaceholderVisible() {
- return this.areRepliesShown || !this.discussionReplies.length;
- },
- isFormVisible() {
- return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id;
- },
- },
- methods: {
- addDiscussionComment(
- store,
- {
- data: { createNote },
- },
- ) {
- updateStoreAfterAddDiscussionComment(
- store,
- createNote,
- getDesignQuery,
- this.designVariables,
- this.discussion.id,
- );
- },
- onDone() {
- this.discussionComment = '';
- this.hideForm();
- if (this.shouldChangeResolvedStatus) {
- this.toggleResolvedStatus();
- }
- },
- onCreateNoteError(err) {
- this.$emit('createNoteError', err);
- },
- hideForm() {
- this.isFormRendered = false;
- this.discussionComment = '';
- },
- showForm() {
- this.$emit('openForm', this.discussion.id);
- this.isFormRendered = true;
- },
- toggleResolvedStatus() {
- this.isResolving = true;
- this.$apollo
- .mutate({
- mutation: toggleResolveDiscussionMutation,
- variables: { id: this.discussion.id, resolve: !this.discussion.resolved },
- })
- .then(({ data }) => {
- if (data.errors?.length > 0) {
- this.$emit('resolveDiscussionError', data.errors[0]);
- }
- })
- .catch(err => {
- this.$emit('resolveDiscussionError', err);
- })
- .finally(() => {
- this.isResolving = false;
- });
- },
- },
- createNoteMutation,
-};
-</script>
-
-<template>
- <div class="design-discussion-wrapper">
- <div
- class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center"
- :class="{ resolved: discussion.resolved }"
- type="button"
- >
- {{ discussion.index }}
- </div>
- <ul
- class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
- data-qa-selector="design_discussion_content"
- >
- <design-note
- :note="firstNote"
- :markdown-preview-path="markdownPreviewPath"
- :is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
- >
- <template v-if="discussion.resolvable" #resolveDiscussion>
- <button
- v-gl-tooltip
- :class="{ 'is-active': discussion.resolved }"
- :title="resolveCheckboxText"
- :aria-label="resolveCheckboxText"
- type="button"
- class="line-resolve-btn note-action-button gl-mr-3"
- data-testid="resolve-button"
- @click.stop="toggleResolvedStatus"
- >
- <gl-icon v-if="!isResolving" :name="resolveIconName" data-testid="resolve-icon" />
- <gl-loading-icon v-else inline />
- </button>
- </template>
- <template v-if="discussion.resolved" #resolvedStatus>
- <p class="gl-text-gray-500 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message">
- {{ __('Resolved by') }}
- <gl-link
- class="gl-text-gray-500 gl-text-decoration-none gl-font-sm link-inherit-color"
- :href="discussion.resolvedBy.webUrl"
- target="_blank"
- >{{ discussion.resolvedBy.name }}</gl-link
- >
- <time-ago-tooltip :time="discussion.resolvedAt" tooltip-placement="bottom" />
- </p>
- </template>
- </design-note>
- <toggle-replies-widget
- v-if="isRepliesWidgetVisible"
- :collapsed="areRepliesCollapsed"
- :replies="discussionReplies"
- @toggle="areRepliesCollapsed = !areRepliesCollapsed"
- />
- <design-note
- v-for="note in discussionReplies"
- v-show="areRepliesShown"
- :key="note.id"
- :note="note"
- :markdown-preview-path="markdownPreviewPath"
- :is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
- />
- <li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
- <reply-placeholder
- v-if="!isFormVisible"
- class="qa-discussion-reply"
- :button-text="__('Reply...')"
- @onClick="showForm"
- />
- <apollo-mutation
- v-else
- #default="{ mutate, loading }"
- :mutation="$options.createNoteMutation"
- :variables="{
- input: mutationPayload,
- }"
- :update="addDiscussionComment"
- @done="onDone"
- @error="onCreateNoteError"
- >
- <design-reply-form
- v-model="discussionComment"
- :is-saving="loading"
- :markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="hideForm"
- >
- <template v-if="discussion.resolvable" #resolveCheckbox>
- <label data-testid="resolve-checkbox">
- <input v-model="shouldChangeResolvedStatus" type="checkbox" />
- {{ resolveCheckboxText }}
- </label>
- </template>
- </design-reply-form>
- </apollo-mutation>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue
deleted file mode 100644
index b1f3a43a66d..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<script>
-import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import DesignReplyForm from './design_reply_form.vue';
-import { findNoteId } from '../../utils/design_management_utils';
-import { hasErrors } from '../../utils/cache_update';
-
-export default {
- components: {
- UserAvatarLink,
- TimelineEntryItem,
- TimeAgoTooltip,
- DesignReplyForm,
- ApolloMutation,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- note: {
- type: Object,
- required: true,
- },
- markdownPreviewPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- noteText: this.note.body,
- isEditing: false,
- };
- },
- computed: {
- author() {
- return this.note.author;
- },
- noteAnchorId() {
- return findNoteId(this.note.id);
- },
- isNoteLinked() {
- return this.$route.hash === `#note_${this.noteAnchorId}`;
- },
- mutationPayload() {
- return {
- id: this.note.id,
- body: this.noteText,
- };
- },
- isEditButtonVisible() {
- return !this.isEditing && this.note.userPermissions.adminNote;
- },
- },
- mounted() {
- if (this.isNoteLinked) {
- this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
- }
- },
- methods: {
- hideForm() {
- this.isEditing = false;
- this.noteText = this.note.body;
- },
- onDone({ data }) {
- this.hideForm();
- if (hasErrors(data.updateNote)) {
- this.$emit('error', data.errors[0]);
- }
- },
- },
- updateNoteMutation,
-};
-</script>
-
-<template>
- <timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
- <user-avatar-link
- :link-href="author.webUrl"
- :img-src="author.avatarUrl"
- :img-alt="author.username"
- :img-size="40"
- />
- <div class="d-flex justify-content-between">
- <div>
- <a
- v-once
- :href="author.webUrl"
- class="js-user-link"
- :data-user-id="author.id"
- :data-username="author.username"
- >
- <span class="note-header-author-name bold">{{ author.name }}</span>
- <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
- <span class="note-headline-light">@{{ author.username }}</span>
- </a>
- <span class="note-headline-light note-headline-meta">
- <span class="system-note-message"> <slot></slot> </span>
- <template v-if="note.createdAt">
- <span class="system-note-separator"></span>
- <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`">
- <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
- </a>
- </template>
- </span>
- </div>
- <div class="gl-display-flex">
- <slot name="resolveDiscussion"></slot>
- <button
- v-if="isEditButtonVisible"
- v-gl-tooltip
- type="button"
- :title="__('Edit comment')"
- class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button"
- @click="isEditing = true"
- >
- <gl-icon name="pencil" class="link-highlight" />
- </button>
- </div>
- </div>
- <template v-if="!isEditing">
- <div
- class="note-text js-note-text md"
- data-qa-selector="note_content"
- v-html="note.bodyHtml"
- ></div>
- <slot name="resolvedStatus"></slot>
- </template>
- <apollo-mutation
- v-else
- #default="{ mutate, loading }"
- :mutation="$options.updateNoteMutation"
- :variables="{
- input: mutationPayload,
- }"
- @error="$emit('error', $event)"
- @done="onDone"
- >
- <design-reply-form
- v-model="noteText"
- :is-saving="loading"
- :markdown-preview-path="markdownPreviewPath"
- :is-new-comment="false"
- class="mt-5"
- @submitForm="mutate"
- @cancelForm="hideForm"
- />
- </apollo-mutation>
- </timeline-entry-item>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue
deleted file mode 100644
index 969034909f2..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue
+++ /dev/null
@@ -1,141 +0,0 @@
-<script>
-import { GlDeprecatedButton, GlModal } from '@gitlab/ui';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { s__ } from '~/locale';
-
-export default {
- name: 'DesignReplyForm',
- components: {
- MarkdownField,
- GlDeprecatedButton,
- GlModal,
- },
- props: {
- markdownPreviewPath: {
- type: String,
- required: false,
- default: '',
- },
- value: {
- type: String,
- required: true,
- },
- isSaving: {
- type: Boolean,
- required: true,
- },
- isNewComment: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- data() {
- return {
- formText: this.value,
- };
- },
- computed: {
- hasValue() {
- return this.value.trim().length > 0;
- },
- modalSettings() {
- if (this.isNewComment) {
- return {
- title: s__('DesignManagement|Cancel comment confirmation'),
- okTitle: s__('DesignManagement|Discard comment'),
- cancelTitle: s__('DesignManagement|Keep comment'),
- content: s__('DesignManagement|Are you sure you want to cancel creating this comment?'),
- };
- }
- return {
- title: s__('DesignManagement|Cancel comment update confirmation'),
- okTitle: s__('DesignManagement|Cancel changes'),
- cancelTitle: s__('DesignManagement|Keep changes'),
- content: s__('DesignManagement|Are you sure you want to cancel changes to this comment?'),
- };
- },
- buttonText() {
- return this.isNewComment
- ? s__('DesignManagement|Comment')
- : s__('DesignManagement|Save comment');
- },
- },
- mounted() {
- this.focusInput();
- },
- methods: {
- submitForm() {
- if (this.hasValue) this.$emit('submitForm');
- },
- cancelComment() {
- if (this.hasValue && this.formText !== this.value) {
- this.$refs.cancelCommentModal.show();
- } else {
- this.$emit('cancelForm');
- }
- },
- focusInput() {
- this.$refs.textarea.focus();
- },
- },
-};
-</script>
-
-<template>
- <form class="new-note common-note-form" @submit.prevent>
- <markdown-field
- :markdown-preview-path="markdownPreviewPath"
- :can-attach-file="false"
- :enable-autocomplete="true"
- :textarea-value="value"
- markdown-docs-path="/help/user/markdown"
- class="bordered-box"
- >
- <template #textarea>
- <textarea
- ref="textarea"
- :value="value"
- class="note-textarea js-gfm-input js-autosize markdown-area"
- dir="auto"
- data-supports-quick-actions="false"
- data-qa-selector="note_textarea"
- :aria-label="__('Description')"
- :placeholder="__('Write a comment…')"
- @input="$emit('input', $event.target.value)"
- @keydown.meta.enter="submitForm"
- @keydown.ctrl.enter="submitForm"
- @keyup.esc.stop="cancelComment"
- >
- </textarea>
- </template>
- </markdown-field>
- <slot name="resolveCheckbox"></slot>
- <div class="note-form-actions gl-display-flex gl-justify-content-space-between">
- <gl-deprecated-button
- ref="submitButton"
- :disabled="!hasValue || isSaving"
- variant="success"
- type="submit"
- data-track-event="click_button"
- data-qa-selector="save_comment_button"
- @click="$emit('submitForm')"
- >
- {{ buttonText }}
- </gl-deprecated-button>
- <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{
- __('Cancel')
- }}</gl-deprecated-button>
- </div>
- <gl-modal
- ref="cancelCommentModal"
- ok-variant="danger"
- :title="modalSettings.title"
- :ok-title="modalSettings.okTitle"
- :cancel-title="modalSettings.cancelTitle"
- modal-id="cancel-comment-modal"
- @ok="$emit('cancelForm')"
- >{{ modalSettings.content }}
- </gl-modal>
- </form>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue
deleted file mode 100644
index 2e366282de3..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<script>
-import { GlIcon, GlButton, GlLink } from '@gitlab/ui';
-import { __, n__ } from '~/locale';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-
-export default {
- name: 'ToggleNotesWidget',
- components: {
- GlIcon,
- GlButton,
- GlLink,
- TimeAgoTooltip,
- },
- props: {
- collapsed: {
- type: Boolean,
- required: true,
- },
- replies: {
- type: Array,
- required: true,
- },
- },
- computed: {
- lastReply() {
- return this.replies[this.replies.length - 1];
- },
- iconName() {
- return this.collapsed ? 'chevron-right' : 'chevron-down';
- },
- toggleText() {
- return this.collapsed
- ? `${this.replies.length} ${n__('reply', 'replies', this.replies.length)}`
- : __('Collapse replies');
- },
- },
-};
-</script>
-
-<template>
- <li
- class="toggle-comments gl-bg-gray-50 gl-display-flex gl-align-items-center gl-py-3"
- :class="{ expanded: !collapsed }"
- data-testid="toggle-comments-wrapper"
- >
- <gl-icon :name="iconName" class="gl-ml-3" @click.stop="$emit('toggle')" />
- <gl-button
- variant="link"
- class="toggle-comments-button gl-ml-2 gl-mr-2"
- @click.stop="$emit('toggle')"
- >
- {{ toggleText }}
- </gl-button>
- <template v-if="collapsed">
- <span class="gl-text-gray-500">{{ __('Last reply by') }}</span>
- <gl-link
- :href="lastReply.author.webUrl"
- target="_blank"
- class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2"
- >
- {{ lastReply.author.name }}
- </gl-link>
- <time-ago-tooltip
- :time="lastReply.createdAt"
- tooltip-placement="bottom"
- class="gl-text-gray-500"
- />
- </template>
- </li>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_overlay.vue b/app/assets/javascripts/design_management_legacy/components/design_overlay.vue
deleted file mode 100644
index 926e7c74802..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_overlay.vue
+++ /dev/null
@@ -1,287 +0,0 @@
-<script>
-import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
-import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
-import DesignNotePin from './design_note_pin.vue';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
-
-export default {
- name: 'DesignOverlay',
- components: {
- DesignNotePin,
- },
- props: {
- dimensions: {
- type: Object,
- required: true,
- },
- position: {
- type: Object,
- required: true,
- },
- notes: {
- type: Array,
- required: false,
- default: () => [],
- },
- currentCommentForm: {
- type: Object,
- required: false,
- default: null,
- },
- disableCommenting: {
- type: Boolean,
- required: false,
- default: false,
- },
- resolvedDiscussionsExpanded: {
- type: Boolean,
- required: true,
- },
- },
- apollo: {
- activeDiscussion: {
- query: activeDiscussionQuery,
- },
- },
- data() {
- return {
- movingNoteNewPosition: null,
- movingNoteStartPosition: null,
- activeDiscussion: {},
- };
- },
- computed: {
- overlayStyle() {
- const cursor = this.disableCommenting ? 'unset' : undefined;
-
- return {
- cursor,
- width: `${this.dimensions.width}px`,
- height: `${this.dimensions.height}px`,
- ...this.position,
- };
- },
- isMovingCurrentComment() {
- return Boolean(this.movingNoteStartPosition && !this.movingNoteStartPosition.noteId);
- },
- currentCommentPositionStyle() {
- return this.isMovingCurrentComment && this.movingNoteNewPosition
- ? this.getNotePositionStyle(this.movingNoteNewPosition)
- : this.getNotePositionStyle(this.currentCommentForm);
- },
- },
- methods: {
- setNewNoteCoordinates({ x, y }) {
- this.$emit('openCommentForm', { x, y });
- },
- getNoteRelativePosition(position) {
- const { x, y, width, height } = position;
- const widthRatio = this.dimensions.width / width;
- const heightRatio = this.dimensions.height / height;
- return {
- left: Math.round(x * widthRatio),
- top: Math.round(y * heightRatio),
- };
- },
- getNotePositionStyle(position) {
- const { left, top } = this.getNoteRelativePosition(position);
- return {
- left: `${left}px`,
- top: `${top}px`,
- };
- },
- getMovingNotePositionDelta(e) {
- let deltaX = 0;
- let deltaY = 0;
-
- if (this.movingNoteStartPosition) {
- const { clientX, clientY } = this.movingNoteStartPosition;
- deltaX = e.clientX - clientX;
- deltaY = e.clientY - clientY;
- }
-
- return {
- deltaX,
- deltaY,
- };
- },
- isMovingNote(noteId) {
- const movingNoteId = this.movingNoteStartPosition?.noteId;
- return Boolean(movingNoteId && movingNoteId === noteId);
- },
- canMoveNote(note) {
- const { userPermissions } = note;
- const { adminNote } = userPermissions || {};
-
- return Boolean(adminNote);
- },
- isPositionInOverlay(position) {
- const { top, left } = this.getNoteRelativePosition(position);
- const { height, width } = this.dimensions;
-
- return top >= 0 && top <= height && left >= 0 && left <= width;
- },
- onNewNoteMove(e) {
- if (!this.isMovingCurrentComment) return;
-
- const { deltaX, deltaY } = this.getMovingNotePositionDelta(e);
- const x = this.currentCommentForm.x + deltaX;
- const y = this.currentCommentForm.y + deltaY;
-
- const movingNoteNewPosition = {
- x,
- y,
- width: this.dimensions.width,
- height: this.dimensions.height,
- };
-
- if (!this.isPositionInOverlay(movingNoteNewPosition)) {
- this.onNewNoteMouseup();
- return;
- }
-
- this.movingNoteNewPosition = movingNoteNewPosition;
- },
- onExistingNoteMove(e) {
- const note = this.notes.find(({ id }) => id === this.movingNoteStartPosition.noteId);
- if (!note || !this.canMoveNote(note)) return;
-
- const { position } = note;
- const { width, height } = position;
- const widthRatio = this.dimensions.width / width;
- const heightRatio = this.dimensions.height / height;
-
- const { deltaX, deltaY } = this.getMovingNotePositionDelta(e);
- const x = position.x * widthRatio + deltaX;
- const y = position.y * heightRatio + deltaY;
-
- const movingNoteNewPosition = {
- x,
- y,
- width: this.dimensions.width,
- height: this.dimensions.height,
- };
-
- if (!this.isPositionInOverlay(movingNoteNewPosition)) {
- this.onExistingNoteMouseup();
- return;
- }
-
- this.movingNoteNewPosition = movingNoteNewPosition;
- },
- onNewNoteMouseup() {
- if (!this.movingNoteNewPosition) return;
-
- const { x, y } = this.movingNoteNewPosition;
- this.setNewNoteCoordinates({ x, y });
- },
- onExistingNoteMouseup(note) {
- if (!this.movingNoteStartPosition || !this.movingNoteNewPosition) {
- this.updateActiveDiscussion(note.id);
- this.$emit('closeCommentForm');
- return;
- }
-
- const { x, y } = this.movingNoteNewPosition;
- this.$emit('moveNote', {
- noteId: this.movingNoteStartPosition.noteId,
- discussionId: this.movingNoteStartPosition.discussionId,
- coordinates: { x, y },
- });
- },
- onNoteMousedown({ clientX, clientY }, note) {
- this.movingNoteStartPosition = {
- noteId: note?.id,
- discussionId: note?.discussion.id,
- clientX,
- clientY,
- };
- },
- onOverlayMousemove(e) {
- if (!this.movingNoteStartPosition) return;
-
- if (this.isMovingCurrentComment) {
- this.onNewNoteMove(e);
- } else {
- this.onExistingNoteMove(e);
- }
- },
- onNoteMouseup(note) {
- if (!this.movingNoteStartPosition) return;
-
- if (this.isMovingCurrentComment) {
- this.onNewNoteMouseup();
- } else {
- this.onExistingNoteMouseup(note);
- }
-
- this.movingNoteStartPosition = null;
- this.movingNoteNewPosition = null;
- },
- onAddCommentMouseup({ offsetX, offsetY }) {
- if (this.disableCommenting) return;
- if (this.activeDiscussion.id) {
- this.updateActiveDiscussion();
- }
-
- this.setNewNoteCoordinates({ x: offsetX, y: offsetY });
- },
- updateActiveDiscussion(id) {
- this.$apollo.mutate({
- mutation: updateActiveDiscussionMutation,
- variables: {
- id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
- },
- });
- },
- isNoteInactive(note) {
- return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
- },
- designPinClass(note) {
- return { inactive: this.isNoteInactive(note), resolved: note.resolved };
- },
- },
-};
-</script>
-
-<template>
- <div
- class="position-absolute image-diff-overlay frame"
- :style="overlayStyle"
- @mousemove="onOverlayMousemove"
- @mouseleave="onNoteMouseup"
- >
- <button
- v-show="!disableCommenting"
- type="button"
- class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
- data-qa-selector="design_image_button"
- @mouseup="onAddCommentMouseup"
- ></button>
- <template v-for="note in notes">
- <design-note-pin
- v-if="resolvedDiscussionsExpanded || !note.resolved"
- :key="note.id"
- :label="note.index"
- :repositioning="isMovingNote(note.id)"
- :position="
- isMovingNote(note.id) && movingNoteNewPosition
- ? getNotePositionStyle(movingNoteNewPosition)
- : getNotePositionStyle(note.position)
- "
- :class="designPinClass(note)"
- @mousedown.stop="onNoteMousedown($event, note)"
- @mouseup.stop="onNoteMouseup(note)"
- />
- </template>
-
- <design-note-pin
- v-if="currentCommentForm"
- :position="currentCommentPositionStyle"
- :repositioning="isMovingCurrentComment"
- @mousedown.stop="onNoteMousedown"
- @mouseup.stop="onNoteMouseup"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_presentation.vue b/app/assets/javascripts/design_management_legacy/components/design_presentation.vue
deleted file mode 100644
index 84dbb2809d9..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_presentation.vue
+++ /dev/null
@@ -1,322 +0,0 @@
-<script>
-import { throttle } from 'lodash';
-import DesignImage from './image.vue';
-import DesignOverlay from './design_overlay.vue';
-
-const CLICK_DRAG_BUFFER_PX = 2;
-
-export default {
- components: {
- DesignImage,
- DesignOverlay,
- },
- props: {
- image: {
- type: String,
- required: false,
- default: '',
- },
- imageName: {
- type: String,
- required: false,
- default: '',
- },
- discussions: {
- type: Array,
- required: true,
- },
- isAnnotating: {
- type: Boolean,
- required: false,
- default: false,
- },
- scale: {
- type: Number,
- required: false,
- default: 1,
- },
- resolvedDiscussionsExpanded: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- overlayDimensions: null,
- overlayPosition: null,
- currentAnnotationPosition: null,
- zoomFocalPoint: {
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- },
- initialLoad: true,
- lastDragPosition: null,
- isDraggingDesign: false,
- };
- },
- computed: {
- discussionStartingNotes() {
- return this.discussions.map(discussion => ({
- ...discussion.notes[0],
- index: discussion.index,
- }));
- },
- currentCommentForm() {
- return (this.isAnnotating && this.currentAnnotationPosition) || null;
- },
- presentationStyle() {
- return {
- cursor: this.isDraggingDesign ? 'grabbing' : undefined,
- };
- },
- },
- beforeDestroy() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return;
-
- presentationViewport.removeEventListener('scroll', this.scrollThrottled, false);
- },
- mounted() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return;
-
- this.scrollThrottled = throttle(() => {
- this.shiftZoomFocalPoint();
- }, 400);
-
- presentationViewport.addEventListener('scroll', this.scrollThrottled, false);
- },
- methods: {
- syncCurrentAnnotationPosition() {
- if (!this.currentAnnotationPosition) return;
-
- const widthRatio = this.overlayDimensions.width / this.currentAnnotationPosition.width;
- const heightRatio = this.overlayDimensions.height / this.currentAnnotationPosition.height;
- const x = this.currentAnnotationPosition.x * widthRatio;
- const y = this.currentAnnotationPosition.y * heightRatio;
-
- this.currentAnnotationPosition = this.getAnnotationPositon({ x, y });
- },
- setOverlayDimensions(overlayDimensions) {
- this.overlayDimensions = overlayDimensions;
-
- // every time we set overlay dimensions, we need to
- // update the current annotation as well
- this.syncCurrentAnnotationPosition();
- },
- setOverlayPosition() {
- if (!this.overlayDimensions) {
- this.overlayPosition = {};
- }
-
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return;
-
- // default to center
- this.overlayPosition = {
- left: `calc(50% - ${this.overlayDimensions.width / 2}px)`,
- top: `calc(50% - ${this.overlayDimensions.height / 2}px)`,
- };
-
- // if the overlay overflows, then don't center
- if (this.overlayDimensions.width > presentationViewport.offsetWidth) {
- this.overlayPosition.left = '0';
- }
- if (this.overlayDimensions.height > presentationViewport.offsetHeight) {
- this.overlayPosition.top = '0';
- }
- },
- /**
- * Return a point that represents the center of an
- * overflowing child element w.r.t it's parent
- */
- getViewportCenter() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return {};
-
- // get height of scroll bars (i.e. the max values for scrollTop, scrollLeft)
- const scrollBarWidth = presentationViewport.scrollWidth - presentationViewport.offsetWidth;
- const scrollBarHeight = presentationViewport.scrollHeight - presentationViewport.offsetHeight;
-
- // determine how many child pixels have been scrolled
- const xScrollRatio =
- presentationViewport.scrollLeft > 0 ? presentationViewport.scrollLeft / scrollBarWidth : 0;
- const yScrollRatio =
- presentationViewport.scrollTop > 0 ? presentationViewport.scrollTop / scrollBarHeight : 0;
- const xScrollOffset =
- (presentationViewport.scrollWidth - presentationViewport.offsetWidth - 0) * xScrollRatio;
- const yScrollOffset =
- (presentationViewport.scrollHeight - presentationViewport.offsetHeight - 0) * yScrollRatio;
-
- const viewportCenterX = presentationViewport.offsetWidth / 2;
- const viewportCenterY = presentationViewport.offsetHeight / 2;
- const focalPointX = viewportCenterX + xScrollOffset;
- const focalPointY = viewportCenterY + yScrollOffset;
-
- return {
- x: focalPointX,
- y: focalPointY,
- };
- },
- /**
- * Scroll the viewport such that the focal point is positioned centrally
- */
- scrollToFocalPoint() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return;
-
- const scrollX = this.zoomFocalPoint.x - presentationViewport.offsetWidth / 2;
- const scrollY = this.zoomFocalPoint.y - presentationViewport.offsetHeight / 2;
-
- presentationViewport.scrollTo(scrollX, scrollY);
- },
- scaleZoomFocalPoint() {
- const { x, y, width, height } = this.zoomFocalPoint;
- const widthRatio = this.overlayDimensions.width / width;
- const heightRatio = this.overlayDimensions.height / height;
-
- this.zoomFocalPoint = {
- x: Math.round(x * widthRatio * 100) / 100,
- y: Math.round(y * heightRatio * 100) / 100,
- ...this.overlayDimensions,
- };
- },
- shiftZoomFocalPoint() {
- this.zoomFocalPoint = {
- ...this.getViewportCenter(),
- ...this.overlayDimensions,
- };
- },
- onImageResize(imageDimensions) {
- this.setOverlayDimensions(imageDimensions);
- this.setOverlayPosition();
-
- this.$nextTick(() => {
- if (this.initialLoad) {
- // set focal point on initial load
- this.shiftZoomFocalPoint();
- this.initialLoad = false;
- } else {
- this.scaleZoomFocalPoint();
- this.scrollToFocalPoint();
- }
- });
- },
- getAnnotationPositon(coordinates) {
- const { x, y } = coordinates;
- const { width, height } = this.overlayDimensions;
- return {
- x: Math.round(x),
- y: Math.round(y),
- width: Math.round(width),
- height: Math.round(height),
- };
- },
- openCommentForm(coordinates) {
- this.currentAnnotationPosition = this.getAnnotationPositon(coordinates);
- this.$emit('openCommentForm', this.currentAnnotationPosition);
- },
- closeCommentForm() {
- this.currentAnnotationPosition = null;
- this.$emit('closeCommentForm');
- },
- moveNote({ noteId, discussionId, coordinates }) {
- const position = this.getAnnotationPositon(coordinates);
- this.$emit('moveNote', { noteId, discussionId, position });
- },
- onPresentationMousedown({ clientX, clientY }) {
- if (!this.isDesignOverflowing()) return;
-
- this.lastDragPosition = {
- x: clientX,
- y: clientY,
- };
- },
- getDragDelta(clientX, clientY) {
- return {
- deltaX: this.lastDragPosition.x - clientX,
- deltaY: this.lastDragPosition.y - clientY,
- };
- },
- exceedsDragThreshold(clientX, clientY) {
- const { deltaX, deltaY } = this.getDragDelta(clientX, clientY);
-
- return Math.abs(deltaX) > CLICK_DRAG_BUFFER_PX || Math.abs(deltaY) > CLICK_DRAG_BUFFER_PX;
- },
- shouldDragDesign(clientX, clientY) {
- return (
- this.lastDragPosition &&
- (this.isDraggingDesign || this.exceedsDragThreshold(clientX, clientY))
- );
- },
- onPresentationMousemove({ clientX, clientY }) {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport || !this.shouldDragDesign(clientX, clientY)) return;
-
- this.isDraggingDesign = true;
-
- const { scrollLeft, scrollTop } = presentationViewport;
- const { deltaX, deltaY } = this.getDragDelta(clientX, clientY);
- presentationViewport.scrollTo(scrollLeft + deltaX, scrollTop + deltaY);
-
- this.lastDragPosition = {
- x: clientX,
- y: clientY,
- };
- },
- onPresentationMouseup() {
- this.lastDragPosition = null;
- this.isDraggingDesign = false;
- },
- isDesignOverflowing() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return false;
-
- return (
- presentationViewport.scrollWidth > presentationViewport.offsetWidth ||
- presentationViewport.scrollHeight > presentationViewport.offsetHeight
- );
- },
- },
-};
-</script>
-
-<template>
- <div
- ref="presentationViewport"
- class="h-100 w-100 p-3 overflow-auto position-relative"
- :style="presentationStyle"
- @mousedown="onPresentationMousedown"
- @mousemove="onPresentationMousemove"
- @mouseup="onPresentationMouseup"
- @mouseleave="onPresentationMouseup"
- @touchstart="onPresentationMousedown"
- @touchmove="onPresentationMousemove"
- @touchend="onPresentationMouseup"
- @touchcancel="onPresentationMouseup"
- >
- <div class="h-100 w-100 d-flex align-items-center position-relative">
- <design-image
- v-if="image"
- :image="image"
- :name="imageName"
- :scale="scale"
- @resize="onImageResize"
- />
- <design-overlay
- v-if="overlayDimensions && overlayPosition"
- :dimensions="overlayDimensions"
- :position="overlayPosition"
- :notes="discussionStartingNotes"
- :current-comment-form="currentCommentForm"
- :disable-commenting="isDraggingDesign"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- @openCommentForm="openCommentForm"
- @closeCommentForm="closeCommentForm"
- @moveNote="moveNote"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_scaler.vue b/app/assets/javascripts/design_management_legacy/components/design_scaler.vue
deleted file mode 100644
index 55dee74bef5..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_scaler.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-
-const SCALE_STEP_SIZE = 0.2;
-const DEFAULT_SCALE = 1;
-const MIN_SCALE = 1;
-const MAX_SCALE = 2;
-
-export default {
- components: {
- GlIcon,
- },
- data() {
- return {
- scale: DEFAULT_SCALE,
- };
- },
- computed: {
- disableReset() {
- return this.scale <= MIN_SCALE;
- },
- disableDecrease() {
- return this.scale === DEFAULT_SCALE;
- },
- disableIncrease() {
- return this.scale >= MAX_SCALE;
- },
- },
- methods: {
- setScale(scale) {
- if (scale < MIN_SCALE) {
- return;
- }
-
- this.scale = Math.round(scale * 100) / 100;
- this.$emit('scale', this.scale);
- },
- incrementScale() {
- this.setScale(this.scale + SCALE_STEP_SIZE);
- },
- decrementScale() {
- this.setScale(this.scale - SCALE_STEP_SIZE);
- },
- resetScale() {
- this.setScale(DEFAULT_SCALE);
- },
- },
-};
-</script>
-
-<template>
- <div class="design-scaler btn-group" role="group">
- <button class="btn" :disabled="disableDecrease" @click="decrementScale">
- <span class="d-flex-center gl-icon s16">
- –
- </span>
- </button>
- <button class="btn" :disabled="disableReset" @click="resetScale">
- <gl-icon name="redo" />
- </button>
- <button class="btn" :disabled="disableIncrease" @click="incrementScale">
- <gl-icon name="plus" />
- </button>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue b/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue
deleted file mode 100644
index 622120e2008..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue
+++ /dev/null
@@ -1,178 +0,0 @@
-<script>
-import Cookies from 'js-cookie';
-import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
-import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
-import DesignDiscussion from './design_notes/design_discussion.vue';
-import Participants from '~/sidebar/components/participants/participants.vue';
-
-export default {
- components: {
- DesignDiscussion,
- Participants,
- GlCollapse,
- GlButton,
- GlPopover,
- },
- props: {
- design: {
- type: Object,
- required: true,
- },
- resolvedDiscussionsExpanded: {
- type: Boolean,
- required: true,
- },
- markdownPreviewPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- isResolvedCommentsPopoverHidden: parseBoolean(Cookies.get(this.$options.cookieKey)),
- discussionWithOpenForm: '',
- };
- },
- computed: {
- discussions() {
- return extractDiscussions(this.design.discussions);
- },
- issue() {
- return {
- ...this.design.issue,
- webPath: this.design.issue.webPath.substr(1),
- };
- },
- discussionParticipants() {
- return extractParticipants(this.issue.participants);
- },
- resolvedDiscussions() {
- return this.discussions.filter(discussion => discussion.resolved);
- },
- unresolvedDiscussions() {
- return this.discussions.filter(discussion => !discussion.resolved);
- },
- resolvedCommentsToggleIcon() {
- return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right';
- },
- },
- methods: {
- handleSidebarClick() {
- this.isResolvedCommentsPopoverHidden = true;
- Cookies.set(this.$options.cookieKey, 'true', { expires: 365 * 10 });
- this.updateActiveDiscussion();
- },
- updateActiveDiscussion(id) {
- this.$apollo.mutate({
- mutation: updateActiveDiscussionMutation,
- variables: {
- id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
- },
- });
- },
- closeCommentForm() {
- this.comment = '';
- this.$emit('closeCommentForm');
- },
- updateDiscussionWithOpenForm(id) {
- this.discussionWithOpenForm = id;
- },
- },
- resolveCommentsToggleText: s__('DesignManagement|Resolved Comments'),
- cookieKey: 'hide_design_resolved_comments_popover',
-};
-</script>
-
-<template>
- <div class="image-notes" @click="handleSidebarClick">
- <h2 class="gl-font-weight-bold gl-mt-0">
- {{ issue.title }}
- </h2>
- <a
- class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
- :href="issue.webUrl"
- >{{ issue.webPath }}</a
- >
- <participants
- :participants="discussionParticipants"
- :show-participant-label="false"
- class="gl-mb-4"
- />
- <h2
- v-if="unresolvedDiscussions.length === 0"
- class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
- data-testid="new-discussion-disclaimer"
- >
- {{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }}
- </h2>
- <design-discussion
- v-for="discussion in unresolvedDiscussions"
- :key="discussion.id"
- :discussion="discussion"
- :design-id="$route.params.id"
- :noteable-id="design.id"
- :markdown-preview-path="markdownPreviewPath"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- :discussion-with-open-form="discussionWithOpenForm"
- data-testid="unresolved-discussion"
- @createNoteError="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
- @resolveDiscussionError="$emit('resolveDiscussionError', $event)"
- @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- @openForm="updateDiscussionWithOpenForm"
- />
- <template v-if="resolvedDiscussions.length > 0">
- <gl-button
- id="resolved-comments"
- data-testid="resolved-comments"
- :icon="resolvedCommentsToggleIcon"
- variant="link"
- class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
- @click="$emit('toggleResolvedComments')"
- >{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }})
- </gl-button>
- <gl-popover
- v-if="!isResolvedCommentsPopoverHidden"
- :show="!isResolvedCommentsPopoverHidden"
- target="resolved-comments"
- container="popovercontainer"
- placement="top"
- :title="s__('DesignManagement|Resolved Comments')"
- >
- <p>
- {{
- s__(
- 'DesignManagement|Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below',
- )
- }}
- </p>
- <a href="#" rel="noopener noreferrer" target="_blank">{{
- s__('DesignManagement|Learn more about resolving comments')
- }}</a>
- </gl-popover>
- <gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3">
- <design-discussion
- v-for="discussion in resolvedDiscussions"
- :key="discussion.id"
- :discussion="discussion"
- :design-id="$route.params.id"
- :noteable-id="design.id"
- :markdown-preview-path="markdownPreviewPath"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- :discussion-with-open-form="discussionWithOpenForm"
- data-testid="resolved-discussion"
- @error="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
- @openForm="updateDiscussionWithOpenForm"
- @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- />
- </gl-collapse>
- </template>
- <slot name="replyForm"></slot>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/image.vue b/app/assets/javascripts/design_management_legacy/components/image.vue
deleted file mode 100644
index 91b7b576e0c..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/image.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<script>
-import { throttle } from 'lodash';
-import { GlIcon } from '@gitlab/ui';
-
-export default {
- components: {
- GlIcon,
- },
- props: {
- image: {
- type: String,
- required: false,
- default: '',
- },
- name: {
- type: String,
- required: false,
- default: '',
- },
- scale: {
- type: Number,
- required: false,
- default: 1,
- },
- },
- data() {
- return {
- baseImageSize: null,
- imageStyle: null,
- imageError: false,
- };
- },
- watch: {
- scale(val) {
- this.zoom(val);
- },
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.resizeThrottled, false);
- },
- mounted() {
- this.onImgLoad();
-
- this.resizeThrottled = throttle(() => {
- // NOTE: if imageStyle is set, then baseImageSize
- // won't change due to resize. We must still emit a
- // `resize` event so that the parent can handle
- // resizes appropriately (e.g. for design_overlay)
- this.setBaseImageSize();
- }, 400);
- window.addEventListener('resize', this.resizeThrottled, false);
- },
- methods: {
- onImgLoad() {
- requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
- },
- onImgError() {
- this.imageError = true;
- },
- setBaseImageSize() {
- const { contentImg } = this.$refs;
- if (!contentImg || contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) return;
-
- this.baseImageSize = {
- height: contentImg.offsetHeight,
- width: contentImg.offsetWidth,
- };
- this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height });
- },
- onResize({ width, height }) {
- this.$emit('resize', { width, height });
- },
- zoom(amount) {
- if (amount === 1) {
- this.imageStyle = null;
- this.$nextTick(() => {
- this.setBaseImageSize();
- });
- return;
- }
- const width = this.baseImageSize.width * amount;
- const height = this.baseImageSize.height * amount;
-
- this.imageStyle = {
- width: `${width}px`,
- height: `${height}px`,
- };
-
- this.onResize({ width, height });
- },
- },
-};
-</script>
-
-<template>
- <div class="m-auto js-design-image">
- <gl-icon v-if="imageError" class="text-secondary-100" name="media-broken" :size="48" />
- <img
- v-show="!imageError"
- ref="contentImg"
- class="mh-100"
- :src="image"
- :alt="name"
- :style="imageStyle"
- :class="{ 'img-fluid': !imageStyle }"
- @error="onImgError"
- @load="onImgLoad"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/list/item.vue b/app/assets/javascripts/design_management_legacy/components/list/item.vue
deleted file mode 100644
index 13c703b8a88..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/list/item.vue
+++ /dev/null
@@ -1,174 +0,0 @@
-<script>
-import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
-import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
-import { n__, __ } from '~/locale';
-import { DESIGN_ROUTE_NAME } from '../../router/constants';
-
-export default {
- components: {
- GlLoadingIcon,
- GlIntersectionObserver,
- GlIcon,
- Icon,
- Timeago,
- },
- props: {
- id: {
- type: [Number, String],
- required: true,
- },
- event: {
- type: String,
- required: true,
- },
- notesCount: {
- type: Number,
- required: true,
- },
- image: {
- type: String,
- required: true,
- },
- filename: {
- type: String,
- required: true,
- },
- updatedAt: {
- type: String,
- required: false,
- default: null,
- },
- isUploading: {
- type: Boolean,
- required: false,
- default: true,
- },
- imageV432x230: {
- type: String,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- imageLoading: true,
- imageError: false,
- wasInView: false,
- };
- },
- computed: {
- icon() {
- const normalizedEvent = this.event.toLowerCase();
- const icons = {
- creation: {
- name: 'file-addition-solid',
- classes: 'text-success-500',
- tooltip: __('Added in this version'),
- },
- modification: {
- name: 'file-modified-solid',
- classes: 'text-primary-500',
- tooltip: __('Modified in this version'),
- },
- deletion: {
- name: 'file-deletion-solid',
- classes: 'text-danger-500',
- tooltip: __('Deleted in this version'),
- },
- };
-
- return icons[normalizedEvent] ? icons[normalizedEvent] : {};
- },
- notesLabel() {
- return n__('%d comment', '%d comments', this.notesCount);
- },
- imageLink() {
- return this.wasInView ? this.imageV432x230 || this.image : '';
- },
- showLoadingSpinner() {
- return this.imageLoading || this.isUploading;
- },
- showImageErrorIcon() {
- return this.wasInView && this.imageError;
- },
- showImage() {
- return !this.showLoadingSpinner && !this.showImageErrorIcon;
- },
- },
- methods: {
- onImageLoad() {
- this.imageLoading = false;
- this.imageError = false;
- },
- onImageError() {
- this.imageLoading = false;
- this.imageError = true;
- },
- onAppear() {
- // do nothing if image has previously
- // been in view
- if (this.wasInView) {
- return;
- }
-
- this.wasInView = true;
- this.imageLoading = true;
- },
- },
- DESIGN_ROUTE_NAME,
-};
-</script>
-
-<template>
- <router-link
- :to="{
- name: $options.DESIGN_ROUTE_NAME,
- params: { id: filename },
- query: $route.query,
- }"
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
- >
- <div class="card-body p-0 d-flex-center overflow-hidden position-relative">
- <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute">
- <span :title="icon.tooltip" :aria-label="icon.tooltip">
- <icon :name="icon.name" :size="18" :class="icon.classes" />
- </span>
- </div>
- <gl-intersection-observer @appear="onAppear">
- <gl-loading-icon v-if="showLoadingSpinner" size="md" />
- <gl-icon
- v-else-if="showImageErrorIcon"
- name="media-broken"
- class="text-secondary"
- :size="32"
- />
- <img
- v-show="showImage"
- :src="imageLink"
- :alt="filename"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- @load="onImageLoad"
- @error="onImageError"
- />
- </gl-intersection-observer>
- </div>
- <div class="card-footer d-flex w-100">
- <div class="d-flex flex-column str-truncated-100">
- <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{
- filename
- }}</span>
- <span v-if="updatedAt" class="str-truncated-100">
- {{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
- </span>
- </div>
- <div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary">
- <icon name="comments" class="ml-1" />
- <span :aria-label="notesLabel" class="ml-1">
- {{ notesCount }}
- </span>
- </div>
- </div>
- </router-link>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue
deleted file mode 100644
index b998dfc47b8..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import Pagination from './pagination.vue';
-import DeleteButton from '../delete_button.vue';
-import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
-import appDataQuery from '../../graphql/queries/app_data.query.graphql';
-import { DESIGNS_ROUTE_NAME } from '../../router/constants';
-
-export default {
- components: {
- Icon,
- Pagination,
- DeleteButton,
- GlDeprecatedButton,
- },
- mixins: [timeagoMixin],
- props: {
- id: {
- type: String,
- required: true,
- },
- isDeleting: {
- type: Boolean,
- required: true,
- },
- filename: {
- type: String,
- required: false,
- default: '',
- },
- updatedAt: {
- type: String,
- required: false,
- default: null,
- },
- updatedBy: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- isLatestVersion: {
- type: Boolean,
- required: true,
- },
- image: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- permissions: {
- createDesign: false,
- },
- projectPath: '',
- issueIid: null,
- };
- },
- apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
- permissions: {
- query: permissionsQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- };
- },
- update: data => data.project.issue.userPermissions,
- },
- },
- computed: {
- updatedText() {
- return sprintf(__('Updated %{updated_at} by %{updated_by}'), {
- updated_at: this.timeFormatted(this.updatedAt),
- updated_by: this.updatedBy.name,
- });
- },
- canDeleteDesign() {
- return this.permissions.createDesign;
- },
- },
- DESIGNS_ROUTE_NAME,
-};
-</script>
-
-<template>
- <header class="d-flex p-2 bg-white align-items-center js-design-header">
- <router-link
- :to="{
- name: $options.DESIGNS_ROUTE_NAME,
- query: $route.query,
- }"
- :aria-label="s__('DesignManagement|Go back to designs')"
- class="mr-3 text-plain d-flex justify-content-center align-items-center"
- >
- <icon :size="18" name="close" />
- </router-link>
- <div class="overflow-hidden d-flex align-items-center">
- <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2>
- <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small>
- </div>
- <pagination :id="id" class="ml-auto flex-shrink-0" />
- <gl-deprecated-button :href="image" class="mr-2">
- <icon :size="18" name="download" />
- </gl-deprecated-button>
- <delete-button
- v-if="isLatestVersion && canDeleteDesign"
- :is-deleting="isDeleting"
- button-variant="danger"
- @deleteSelectedDesigns="$emit('delete')"
- >
- <icon :size="18" name="remove" />
- </delete-button>
- </header>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue
deleted file mode 100644
index bf62a8f66a6..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<script>
-/* global Mousetrap */
-import 'mousetrap';
-import { s__, sprintf } from '~/locale';
-import PaginationButton from './pagination_button.vue';
-import allDesignsMixin from '../../mixins/all_designs';
-import { DESIGN_ROUTE_NAME } from '../../router/constants';
-
-export default {
- components: {
- PaginationButton,
- },
- mixins: [allDesignsMixin],
- props: {
- id: {
- type: String,
- required: true,
- },
- },
- computed: {
- designsCount() {
- return this.designs.length;
- },
- currentIndex() {
- return this.designs.findIndex(design => design.filename === this.id);
- },
- paginationText() {
- return sprintf(s__('DesignManagement|%{current_design} of %{designs_count}'), {
- current_design: this.currentIndex + 1,
- designs_count: this.designsCount,
- });
- },
- previousDesign() {
- if (!this.designsCount) return null;
-
- return this.designs[this.currentIndex - 1];
- },
- nextDesign() {
- if (!this.designsCount) return null;
-
- return this.designs[this.currentIndex + 1];
- },
- },
- mounted() {
- Mousetrap.bind('left', () => this.navigateToDesign(this.previousDesign));
- Mousetrap.bind('right', () => this.navigateToDesign(this.nextDesign));
- },
- beforeDestroy() {
- Mousetrap.unbind(['left', 'right'], this.navigateToDesign);
- },
- methods: {
- navigateToDesign(design) {
- if (design) {
- this.$router.push({
- name: DESIGN_ROUTE_NAME,
- params: { id: design.filename },
- query: this.$route.query,
- });
- }
- },
- },
-};
-</script>
-
-<template>
- <div v-if="designsCount" class="d-flex align-items-center">
- {{ paginationText }}
- <div class="btn-group ml-3 mr-3">
- <pagination-button
- :design="previousDesign"
- :title="s__('DesignManagement|Go to previous design')"
- icon-name="angle-left"
- class="js-previous-design"
- />
- <pagination-button
- :design="nextDesign"
- :title="s__('DesignManagement|Go to next design')"
- icon-name="angle-right"
- class="js-next-design"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue
deleted file mode 100644
index f00ecefca01..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<script>
-import Icon from '~/vue_shared/components/icon.vue';
-import { DESIGN_ROUTE_NAME } from '../../router/constants';
-
-export default {
- components: {
- Icon,
- },
- props: {
- design: {
- type: Object,
- required: false,
- default: null,
- },
- title: {
- type: String,
- required: true,
- },
- iconName: {
- type: String,
- required: true,
- },
- },
- computed: {
- designLink() {
- if (!this.design) return {};
-
- return {
- name: DESIGN_ROUTE_NAME,
- params: { id: this.design.filename },
- query: this.$route.query,
- };
- },
- },
-};
-</script>
-
-<template>
- <router-link
- :to="designLink"
- :disabled="!design"
- :class="{ disabled: !design }"
- :aria-label="title"
- class="btn btn-default"
- >
- <icon :name="iconName" />
- </router-link>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/upload/button.vue b/app/assets/javascripts/design_management_legacy/components/upload/button.vue
deleted file mode 100644
index 68555104a3c..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/upload/button.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script>
-import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
-
-export default {
- components: {
- GlDeprecatedButton,
- GlLoadingIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- isSaving: {
- type: Boolean,
- required: true,
- },
- },
- methods: {
- openFileUpload() {
- this.$refs.fileUpload.click();
- },
- onFileUploadChange(e) {
- this.$emit('upload', e.target.files);
- },
- },
- VALID_DESIGN_FILE_MIMETYPE,
-};
-</script>
-
-<template>
- <div>
- <gl-deprecated-button
- v-gl-tooltip.hover
- :title="
- s__(
- 'DesignManagement|Adding a design with the same filename replaces the file in a new version.',
- )
- "
- :disabled="isSaving"
- variant="success"
- @click="openFileUpload"
- >
- {{ s__('DesignManagement|Upload designs') }}
- <gl-loading-icon v-if="isSaving" inline class="ml-1" />
- </gl-deprecated-button>
-
- <input
- ref="fileUpload"
- type="file"
- name="design_file"
- :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
- class="hide"
- multiple
- @change="onFileUploadChange"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue
deleted file mode 100644
index e435c84c959..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<script>
-import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import uploadDesignMutation from '../../graphql/mutations/upload_design.mutation.graphql';
-import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages';
-import { isValidDesignFile } from '../../utils/design_management_utils';
-import { VALID_DATA_TRANSFER_TYPE, VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
-
-export default {
- components: {
- GlIcon,
- GlLink,
- GlSprintf,
- },
- data() {
- return {
- dragCounter: 0,
- isDragDataValid: false,
- };
- },
- computed: {
- dragging() {
- return this.dragCounter !== 0;
- },
- },
- methods: {
- isValidUpload(files) {
- return files.every(isValidDesignFile);
- },
- isValidDragDataType({ dataTransfer }) {
- return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE));
- },
- ondrop({ dataTransfer = {} }) {
- this.dragCounter = 0;
- // User already had feedback when dropzone was active, so bail here
- if (!this.isDragDataValid) {
- return;
- }
-
- const { files } = dataTransfer;
- if (!this.isValidUpload(Array.from(files))) {
- createFlash(UPLOAD_DESIGN_INVALID_FILETYPE_ERROR);
- return;
- }
-
- this.$emit('change', files);
- },
- ondragenter(e) {
- this.dragCounter += 1;
- this.isDragDataValid = this.isValidDragDataType(e);
- },
- ondragleave() {
- this.dragCounter -= 1;
- },
- openFileUpload() {
- this.$refs.fileUpload.click();
- },
- onDesignInputChange(e) {
- this.$emit('change', e.target.files);
- },
- },
- uploadDesignMutation,
- VALID_DESIGN_FILE_MIMETYPE,
-};
-</script>
-
-<template>
- <div
- class="w-100 position-relative"
- @dragstart.prevent.stop
- @dragend.prevent.stop
- @dragover.prevent.stop
- @dragenter.prevent.stop="ondragenter"
- @dragleave.prevent.stop="ondragleave"
- @drop.prevent.stop="ondrop"
- >
- <slot>
- <button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
- @click="openFileUpload"
- >
- <div class="d-flex-center flex-column text-center">
- <gl-icon name="doc-new" :size="48" class="mb-4" />
- <p>
- <gl-sprintf
- :message="
- __(
- '%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.',
- )
- "
- >
- <template #lineOne="{ content }"
- ><span class="d-block">{{ content }}</span>
- </template>
-
- <template #link="{ content }">
- <gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- </button>
-
- <input
- ref="fileUpload"
- type="file"
- name="design_file"
- :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
- class="hide"
- multiple
- @change="onDesignInputChange"
- />
- </slot>
- <transition name="design-dropzone-fade">
- <div
- v-show="dragging"
- class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
- >
- <div v-show="!isDragDataValid" class="mw-50 text-center">
- <h3>{{ __('Oh no!') }}</h3>
- <span>{{
- __(
- 'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.',
- )
- }}</span>
- </div>
- <div v-show="isDragDataValid" class="mw-50 text-center">
- <h3>{{ __('Incoming!') }}</h3>
- <span>{{ __('Drop your designs to start your upload.') }}</span>
- </div>
- </div>
- </transition>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue
deleted file mode 100644
index 879d2523848..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<script>
-import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import allVersionsMixin from '../../mixins/all_versions';
-import { findVersionId } from '../../utils/design_management_utils';
-
-export default {
- components: {
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
- },
- mixins: [allVersionsMixin],
- computed: {
- queryVersion() {
- return this.$route.query.version;
- },
- currentVersionIdx() {
- if (!this.queryVersion) return 0;
-
- const idx = this.allVersions.findIndex(
- version => this.findVersionId(version.node.id) === this.queryVersion,
- );
-
- // if the currentVersionId isn't a valid version (i.e. not in allVersions)
- // then return the latest version (index 0)
- return idx !== -1 ? idx : 0;
- },
- currentVersionId() {
- if (this.queryVersion) return this.queryVersion;
-
- const currentVersion = this.allVersions[this.currentVersionIdx];
- return this.findVersionId(currentVersion.node.id);
- },
- dropdownText() {
- if (this.isLatestVersion) {
- return __('Showing Latest Version');
- }
- // allVersions is sorted in reverse chronological order (latest first)
- const currentVersionNumber = this.allVersions.length - this.currentVersionIdx;
-
- return sprintf(__('Showing Version #%{versionNumber}'), {
- versionNumber: currentVersionNumber,
- });
- },
- },
- methods: {
- findVersionId,
- },
-};
-</script>
-
-<template>
- <gl-deprecated-dropdown :text="dropdownText" variant="link" class="design-version-dropdown">
- <gl-deprecated-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
- <router-link
- class="d-flex js-version-link"
- :to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }"
- >
- <div class="flex-grow-1 ml-2">
- <div>
- <strong
- >{{ __('Version') }} {{ allVersions.length - index }}
- <span v-if="findVersionId(version.node.id) === latestVersionId"
- >({{ __('latest') }})</span
- >
- </strong>
- </div>
- </div>
- <i
- v-if="findVersionId(version.node.id) === currentVersionId"
- class="fa fa-check float-right gl-mr-2"
- ></i>
- </router-link>
- </gl-deprecated-dropdown-item>
- </gl-deprecated-dropdown>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/constants.js b/app/assets/javascripts/design_management_legacy/constants.js
deleted file mode 100644
index 21ff361a277..00000000000
--- a/app/assets/javascripts/design_management_legacy/constants.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// WARNING: replace this with something
-// more sensical as per https://gitlab.com/gitlab-org/gitlab/issues/118611
-export const VALID_DESIGN_FILE_MIMETYPE = {
- mimetype: 'image/*',
- regex: /image\/.+/,
-};
-
-// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types
-export const VALID_DATA_TRANSFER_TYPE = 'Files';
-
-export const ACTIVE_DISCUSSION_SOURCE_TYPES = {
- pin: 'pin',
- discussion: 'discussion',
-};
-
-export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0'];
diff --git a/app/assets/javascripts/design_management_legacy/graphql.js b/app/assets/javascripts/design_management_legacy/graphql.js
deleted file mode 100644
index fae337aa75b..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { uniqueId } from 'lodash';
-import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
-import createDefaultClient from '~/lib/graphql';
-import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
-import typeDefs from './graphql/typedefs.graphql';
-
-Vue.use(VueApollo);
-
-const resolvers = {
- Mutation: {
- updateActiveDiscussion: (_, { id = null, source }, { cache }) => {
- const data = cache.readQuery({ query: activeDiscussionQuery });
- data.activeDiscussion = {
- __typename: 'ActiveDiscussion',
- id,
- source,
- };
- cache.writeQuery({ query: activeDiscussionQuery, data });
- },
- },
-};
-
-const defaultClient = createDefaultClient(
- resolvers,
- // This config is added temporarily to resolve an issue with duplicate design IDs.
- // Should be removed as soon as https://gitlab.com/gitlab-org/gitlab/issues/13495 is resolved
- {
- cacheConfig: {
- dataIdFromObject: object => {
- // eslint-disable-next-line no-underscore-dangle, @gitlab/require-i18n-strings
- if (object.__typename === 'Design') {
- return object.id && object.image ? `${object.id}-${object.image}` : uniqueId();
- }
- return defaultDataIdFromObject(object);
- },
- },
- typeDefs,
- },
-);
-
-export default new VueApollo({
- defaultClient,
-});
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql
deleted file mode 100644
index 4b1703e41c3..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql
+++ /dev/null
@@ -1,24 +0,0 @@
-#import "./design_note.fragment.graphql"
-#import "./design_list.fragment.graphql"
-#import "./diff_refs.fragment.graphql"
-#import "./discussion_resolved_status.fragment.graphql"
-
-fragment DesignItem on Design {
- ...DesignListItem
- fullPath
- diffRefs {
- ...DesignDiffRefs
- }
- discussions {
- nodes {
- id
- replyId
- ...ResolvedStatus
- notes {
- nodes {
- ...DesignNote
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql
deleted file mode 100644
index bc3132f9b42..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql
+++ /dev/null
@@ -1,8 +0,0 @@
-fragment DesignListItem on Design {
- id
- event
- filename
- notesCount
- image
- imageV432x230
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql
deleted file mode 100644
index 26edd2c0be1..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql
+++ /dev/null
@@ -1,29 +0,0 @@
-#import "./diff_refs.fragment.graphql"
-#import "~/graphql_shared/fragments/author.fragment.graphql"
-#import "./note_permissions.fragment.graphql"
-
-fragment DesignNote on Note {
- id
- author {
- ...Author
- }
- body
- bodyHtml
- createdAt
- resolved
- position {
- diffRefs {
- ...DesignDiffRefs
- }
- x
- y
- height
- width
- }
- userPermissions {
- ...DesignNotePermissions
- }
- discussion {
- id
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql
deleted file mode 100644
index 984a55814b0..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-fragment DesignDiffRefs on DiffRefs {
- baseSha
- startSha
- headSha
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql
deleted file mode 100644
index 7483b508721..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-fragment ResolvedStatus on Discussion {
- resolvable
- resolved
- resolvedAt
- resolvedBy {
- name
- webUrl
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql
deleted file mode 100644
index c243e39f3d3..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-fragment DesignNotePermissions on NotePermissions {
- adminNote
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql
deleted file mode 100644
index 7eb40b12f51..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql
+++ /dev/null
@@ -1,4 +0,0 @@
-fragment VersionListItem on DesignVersion {
- id
- sha
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql
deleted file mode 100644
index c8ade328120..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql
+++ /dev/null
@@ -1,21 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-
-mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
- createImageDiffNote(input: $input) {
- note {
- ...DesignNote
- discussion {
- id
- replyId
- notes {
- edges {
- node {
- ...DesignNote
- }
- }
- }
- }
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql
deleted file mode 100644
index 184ee6955dc..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-
-mutation createNote($input: CreateNoteInput!) {
- createNote(input: $input) {
- note {
- ...DesignNote
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql
deleted file mode 100644
index 0b3cf636cdb..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "../fragments/version.fragment.graphql"
-
-mutation destroyDesign($filenames: [String!]!, $projectPath: ID!, $iid: ID!) {
- designManagementDelete(input: { projectPath: $projectPath, iid: $iid, filenames: $filenames }) {
- version {
- ...VersionListItem
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql
deleted file mode 100644
index 1157fc05d5f..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql
+++ /dev/null
@@ -1,17 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-#import "../fragments/discussion_resolved_status.fragment.graphql"
-
-mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) {
- discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
- discussion {
- id
- ...ResolvedStatus
- notes {
- nodes {
- ...DesignNote
- }
- }
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql
deleted file mode 100644
index a24b6737159..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-mutation updateActiveDiscussion($id: String, $source: String) {
- updateActiveDiscussion(id: $id, source: $source) @client
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql
deleted file mode 100644
index 5562ca9d89f..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-
-mutation updateImageDiffNote($input: UpdateImageDiffNoteInput!) {
- updateImageDiffNote(input: $input) {
- errors
- note {
- ...DesignNote
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql
deleted file mode 100644
index b995e99fb6a..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-
-mutation updateNote($input: UpdateNoteInput!) {
- updateNote(input: $input) {
- note {
- ...DesignNote
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql
deleted file mode 100644
index d694e6558a0..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql
+++ /dev/null
@@ -1,21 +0,0 @@
-#import "../fragments/design.fragment.graphql"
-
-mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
- designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
- designs {
- ...DesignItem
- versions {
- edges {
- node {
- id
- sha
- }
- }
- }
- }
- skippedDesigns {
- filename
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql
deleted file mode 100644
index 111023cea68..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql
+++ /dev/null
@@ -1,6 +0,0 @@
-query activeDiscussion {
- activeDiscussion @client {
- id
- source
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql
deleted file mode 100644
index e1269761206..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql
+++ /dev/null
@@ -1,4 +0,0 @@
-query projectFullPath {
- projectPath @client
- issueIid @client
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql
deleted file mode 100644
index a87b256dc95..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-query permissions($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- id
- issue(iid: $iid) {
- userPermissions {
- createDesign
- }
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql
deleted file mode 100644
index 07a9af55787..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql
+++ /dev/null
@@ -1,31 +0,0 @@
-#import "../fragments/design.fragment.graphql"
-#import "~/graphql_shared/fragments/author.fragment.graphql"
-
-query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [String!]) {
- project(fullPath: $fullPath) {
- id
- issue(iid: $iid) {
- designCollection {
- designs(atVersion: $atVersion, filenames: $filenames) {
- edges {
- node {
- ...DesignItem
- issue {
- title
- webPath
- webUrl
- participants {
- edges {
- node {
- ...Author
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql
deleted file mode 100644
index 121a50555b3..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql
+++ /dev/null
@@ -1,26 +0,0 @@
-#import "../fragments/design_list.fragment.graphql"
-#import "../fragments/version.fragment.graphql"
-
-query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) {
- project(fullPath: $fullPath) {
- id
- issue(iid: $iid) {
- designCollection {
- designs(atVersion: $atVersion) {
- edges {
- node {
- ...DesignListItem
- }
- }
- }
- versions {
- edges {
- node {
- ...VersionListItem
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql b/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql
deleted file mode 100644
index fdbad4a90e0..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql
+++ /dev/null
@@ -1,12 +0,0 @@
-type ActiveDiscussion {
- id: ID
- source: String
-}
-
-extend type Query {
- activeDiscussion: ActiveDiscussion
-}
-
-extend type Mutation {
- updateActiveDiscussion(id: ID!, source: String!): Boolean
-}
diff --git a/app/assets/javascripts/design_management_legacy/index.js b/app/assets/javascripts/design_management_legacy/index.js
deleted file mode 100644
index 1fc5779515a..00000000000
--- a/app/assets/javascripts/design_management_legacy/index.js
+++ /dev/null
@@ -1,61 +0,0 @@
-// This application is being moved, please do not touch this files
-// Please see https://gitlab.com/gitlab-org/gitlab/-/issues/14744#note_364468096 for details
-
-import $ from 'jquery';
-import Vue from 'vue';
-import createRouter from './router';
-import App from './components/app.vue';
-import apolloProvider from './graphql';
-import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
-import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
-
-export default () => {
- const el = document.querySelector('.js-design-management');
- const badge = document.querySelector('.js-designs-count');
- const { issueIid, projectPath, issuePath } = el.dataset;
- const router = createRouter(issuePath);
-
- $('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
- if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
- router.push({ name: DESIGNS_ROUTE_NAME });
- } else if (id === 'discussion') {
- router.push({ name: ROOT_ROUTE_NAME });
- }
- });
-
- apolloProvider.clients.defaultClient.cache.writeData({
- data: {
- projectPath,
- issueIid,
- activeDiscussion: {
- __typename: 'ActiveDiscussion',
- id: null,
- source: null,
- },
- },
- });
-
- apolloProvider.clients.defaultClient
- .watchQuery({
- query: getDesignListQuery,
- variables: {
- fullPath: projectPath,
- iid: issueIid,
- atVersion: null,
- },
- })
- .subscribe(({ data }) => {
- if (badge) {
- badge.textContent = data.project.issue.designCollection.designs.edges.length;
- }
- });
-
- return new Vue({
- el,
- router,
- apolloProvider,
- render(createElement) {
- return createElement(App);
- },
- });
-};
diff --git a/app/assets/javascripts/design_management_legacy/mixins/all_designs.js b/app/assets/javascripts/design_management_legacy/mixins/all_designs.js
deleted file mode 100644
index 544429928d2..00000000000
--- a/app/assets/javascripts/design_management_legacy/mixins/all_designs.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { propertyOf } from 'lodash';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__ } from '~/locale';
-import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import { extractNodes } from '../utils/design_management_utils';
-import allVersionsMixin from './all_versions';
-import { DESIGNS_ROUTE_NAME } from '../router/constants';
-
-export default {
- mixins: [allVersionsMixin],
- apollo: {
- designs: {
- query: getDesignListQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- atVersion: this.designsVersion,
- };
- },
- update: data => {
- const designEdges = propertyOf(data)(['project', 'issue', 'designCollection', 'designs']);
- if (designEdges) {
- return extractNodes(designEdges);
- }
- return [];
- },
- error() {
- this.error = true;
- },
- result() {
- if (this.$route.query.version && !this.hasValidVersion) {
- createFlash(
- s__(
- 'DesignManagement|Requested design version does not exist. Showing latest version instead',
- ),
- );
- this.$router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: undefined } });
- }
- },
- },
- },
- data() {
- return {
- designs: [],
- error: false,
- };
- },
-};
diff --git a/app/assets/javascripts/design_management_legacy/mixins/all_versions.js b/app/assets/javascripts/design_management_legacy/mixins/all_versions.js
deleted file mode 100644
index 3966fe71732..00000000000
--- a/app/assets/javascripts/design_management_legacy/mixins/all_versions.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import appDataQuery from '../graphql/queries/app_data.query.graphql';
-import { findVersionId } from '../utils/design_management_utils';
-
-export default {
- apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
- allVersions: {
- query: getDesignListQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- atVersion: null,
- };
- },
- update: data => data.project.issue.designCollection.versions.edges,
- },
- },
- computed: {
- hasValidVersion() {
- return (
- this.$route.query.version &&
- this.allVersions &&
- this.allVersions.some(version => version.node.id.endsWith(this.$route.query.version))
- );
- },
- designsVersion() {
- return this.hasValidVersion
- ? `gid://gitlab/DesignManagement::Version/${this.$route.query.version}`
- : null;
- },
- latestVersionId() {
- const latestVersion = this.allVersions[0];
- return latestVersion && findVersionId(latestVersion.node.id);
- },
- isLatestVersion() {
- if (this.allVersions.length > 0) {
- return (
- !this.$route.query.version ||
- !this.latestVersionId ||
- this.$route.query.version === this.latestVersionId
- );
- }
- return true;
- },
- },
- data() {
- return {
- allVersions: [],
- projectPath: '',
- issueIid: null,
- };
- },
-};
diff --git a/app/assets/javascripts/design_management_legacy/pages/design/index.vue b/app/assets/javascripts/design_management_legacy/pages/design/index.vue
deleted file mode 100644
index 2ada9eff8c6..00000000000
--- a/app/assets/javascripts/design_management_legacy/pages/design/index.vue
+++ /dev/null
@@ -1,378 +0,0 @@
-<script>
-import Mousetrap from 'mousetrap';
-import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import { ApolloMutation } from 'vue-apollo';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { fetchPolicies } from '~/lib/graphql';
-import allVersionsMixin from '../../mixins/all_versions';
-import Toolbar from '../../components/toolbar/index.vue';
-import DesignDestroyer from '../../components/design_destroyer.vue';
-import DesignScaler from '../../components/design_scaler.vue';
-import DesignPresentation from '../../components/design_presentation.vue';
-import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
-import DesignSidebar from '../../components/design_sidebar.vue';
-import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
-import appDataQuery from '../../graphql/queries/app_data.query.graphql';
-import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
-import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
-import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
-import {
- extractDiscussions,
- extractDesign,
- updateImageDiffNoteOptimisticResponse,
-} from '../../utils/design_management_utils';
-import {
- updateStoreAfterAddImageDiffNote,
- updateStoreAfterUpdateImageDiffNote,
-} from '../../utils/cache_update';
-import {
- ADD_DISCUSSION_COMMENT_ERROR,
- ADD_IMAGE_DIFF_NOTE_ERROR,
- UPDATE_IMAGE_DIFF_NOTE_ERROR,
- DESIGN_NOT_FOUND_ERROR,
- DESIGN_VERSION_NOT_EXIST_ERROR,
- UPDATE_NOTE_ERROR,
- designDeletionError,
-} from '../../utils/error_messages';
-import { trackDesignDetailView } from '../../utils/tracking';
-import { DESIGNS_ROUTE_NAME } from '../../router/constants';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
-
-export default {
- components: {
- ApolloMutation,
- DesignReplyForm,
- DesignPresentation,
- DesignScaler,
- DesignDestroyer,
- Toolbar,
- GlLoadingIcon,
- GlAlert,
- DesignSidebar,
- },
- mixins: [allVersionsMixin],
- props: {
- id: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- design: {},
- comment: '',
- annotationCoordinates: null,
- projectPath: '',
- errorMessage: '',
- issueIid: '',
- scale: 1,
- resolvedDiscussionsExpanded: false,
- };
- },
- apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
- design: {
- query: getDesignQuery,
- // We want to see cached design version if we have one, and fetch newer version on the background to update discussions
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
- variables() {
- return this.designVariables;
- },
- update: data => extractDesign(data),
- result(res) {
- this.onDesignQueryResult(res);
- },
- error() {
- this.onQueryError(DESIGN_NOT_FOUND_ERROR);
- },
- },
- },
- computed: {
- isFirstLoading() {
- // We only want to show spinner on initial design load (when opened from a deep link to design)
- // If we already have cached a design, loading shouldn't be indicated to user
- return this.$apollo.queries.design.loading && !this.design.filename;
- },
- discussions() {
- if (!this.design.discussions) {
- return [];
- }
- return extractDiscussions(this.design.discussions);
- },
- markdownPreviewPath() {
- return `/${this.projectPath}/preview_markdown?target_type=Issue`;
- },
- isSubmitButtonDisabled() {
- return this.comment.trim().length === 0;
- },
- designVariables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- filenames: [this.$route.params.id],
- atVersion: this.designsVersion,
- };
- },
- mutationPayload() {
- const { x, y, width, height } = this.annotationCoordinates;
- return {
- noteableId: this.design.id,
- body: this.comment,
- position: {
- headSha: this.design.diffRefs.headSha,
- baseSha: this.design.diffRefs.baseSha,
- startSha: this.design.diffRefs.startSha,
- x,
- y,
- width,
- height,
- paths: {
- newPath: this.design.fullPath,
- },
- },
- };
- },
- isAnnotating() {
- return Boolean(this.annotationCoordinates);
- },
- resolvedDiscussions() {
- return this.discussions.filter(discussion => discussion.resolved);
- },
- },
- watch: {
- resolvedDiscussions(val) {
- if (!val.length) {
- this.resolvedDiscussionsExpanded = false;
- }
- },
- },
- mounted() {
- Mousetrap.bind('esc', this.closeDesign);
- this.trackEvent();
- // We need to reset the active discussion when opening a new design
- this.updateActiveDiscussion();
- },
- beforeDestroy() {
- Mousetrap.unbind('esc', this.closeDesign);
- },
- methods: {
- addImageDiffNoteToStore(
- store,
- {
- data: { createImageDiffNote },
- },
- ) {
- updateStoreAfterAddImageDiffNote(
- store,
- createImageDiffNote,
- getDesignQuery,
- this.designVariables,
- );
- },
- updateImageDiffNoteInStore(
- store,
- {
- data: { updateImageDiffNote },
- },
- ) {
- return updateStoreAfterUpdateImageDiffNote(
- store,
- updateImageDiffNote,
- getDesignQuery,
- this.designVariables,
- );
- },
- onMoveNote({ noteId, discussionId, position }) {
- const discussion = this.discussions.find(({ id }) => id === discussionId);
- const note = discussion.notes.find(
- ({ discussion: noteDiscussion }) => noteDiscussion.id === discussionId,
- );
-
- const mutationPayload = {
- optimisticResponse: updateImageDiffNoteOptimisticResponse(note, {
- position,
- }),
- variables: {
- input: {
- id: noteId,
- position,
- },
- },
- mutation: updateImageDiffNoteMutation,
- update: this.updateImageDiffNoteInStore,
- };
-
- return this.$apollo.mutate(mutationPayload).catch(e => this.onUpdateImageDiffNoteError(e));
- },
- onDesignQueryResult({ data, loading }) {
- // On the initial load with cache-and-network policy data is undefined while loading is true
- // To prevent throwing an error, we don't perform any logic until loading is false
- if (loading) {
- return;
- }
-
- if (!data || !extractDesign(data)) {
- this.onQueryError(DESIGN_NOT_FOUND_ERROR);
- } else if (this.$route.query.version && !this.hasValidVersion) {
- this.onQueryError(DESIGN_VERSION_NOT_EXIST_ERROR);
- }
- },
- onQueryError(message) {
- // because we redirect user to /designs (the issue page),
- // we want to create these flashes on the issue page
- createFlash(message);
- this.$router.push({ name: this.$options.DESIGNS_ROUTE_NAME });
- },
- onError(message, e) {
- this.errorMessage = message;
- throw e;
- },
- onCreateImageDiffNoteError(e) {
- this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e);
- },
- onUpdateNoteError(e) {
- this.onError(UPDATE_NOTE_ERROR, e);
- },
- onDesignDiscussionError(e) {
- this.onError(ADD_DISCUSSION_COMMENT_ERROR, e);
- },
- onUpdateImageDiffNoteError(e) {
- this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
- },
- onDesignDeleteError(e) {
- this.onError(designDeletionError({ singular: true }), e);
- },
- onResolveDiscussionError(e) {
- this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
- },
- openCommentForm(annotationCoordinates) {
- this.annotationCoordinates = annotationCoordinates;
- if (this.$refs.newDiscussionForm) {
- this.$refs.newDiscussionForm.focusInput();
- }
- },
- closeCommentForm() {
- this.comment = '';
- this.annotationCoordinates = null;
- },
- closeDesign() {
- this.$router.push({
- name: this.$options.DESIGNS_ROUTE_NAME,
- query: this.$route.query,
- });
- },
- trackEvent() {
- // TODO: This needs to be made aware of referers, or if it's rendered in a different context than a Issue
- trackDesignDetailView(
- 'issue-design-collection',
- 'issue',
- this.$route.query.version || this.latestVersionId,
- this.isLatestVersion,
- );
- },
- updateActiveDiscussion(id) {
- this.$apollo.mutate({
- mutation: updateActiveDiscussionMutation,
- variables: {
- id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
- },
- });
- },
- toggleResolvedComments() {
- this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
- },
- },
- createImageDiffNoteMutation,
- DESIGNS_ROUTE_NAME,
-};
-</script>
-
-<template>
- <div
- class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
- >
- <gl-loading-icon v-if="isFirstLoading" size="xl" class="align-self-center" />
- <template v-else>
- <div class="d-flex overflow-hidden flex-grow-1 flex-column position-relative">
- <design-destroyer
- :filenames="[design.filename]"
- :project-path="projectPath"
- :iid="issueIid"
- @done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })"
- @error="onDesignDeleteError"
- >
- <template #default="{ mutate, loading }">
- <toolbar
- :id="id"
- :is-deleting="loading"
- :is-latest-version="isLatestVersion"
- v-bind="design"
- @delete="mutate"
- />
- </template>
- </design-destroyer>
-
- <div v-if="errorMessage" class="p-3">
- <gl-alert variant="danger" @dismiss="errorMessage = null">
- {{ errorMessage }}
- </gl-alert>
- </div>
- <design-presentation
- :image="design.image"
- :image-name="design.filename"
- :discussions="discussions"
- :is-annotating="isAnnotating"
- :scale="scale"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- @openCommentForm="openCommentForm"
- @closeCommentForm="closeCommentForm"
- @moveNote="onMoveNote"
- />
-
- <div class="design-scaler-wrapper position-absolute mb-4 d-flex-center">
- <design-scaler @scale="scale = $event" />
- </div>
- </div>
- <design-sidebar
- :design="design"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- :markdown-preview-path="markdownPreviewPath"
- @onDesignDiscussionError="onDesignDiscussionError"
- @onCreateImageDiffNoteError="onCreateImageDiffNoteError"
- @updateNoteError="onUpdateNoteError"
- @resolveDiscussionError="onResolveDiscussionError"
- @toggleResolvedComments="toggleResolvedComments"
- >
- <template #replyForm>
- <apollo-mutation
- v-if="isAnnotating"
- #default="{ mutate, loading }"
- :mutation="$options.createImageDiffNoteMutation"
- :variables="{
- input: mutationPayload,
- }"
- :update="addImageDiffNoteToStore"
- @done="closeCommentForm"
- @error="onCreateImageDiffNoteError"
- >
- <design-reply-form
- ref="newDiscussionForm"
- v-model="comment"
- :is-saving="loading"
- :markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="closeCommentForm"
- /> </apollo-mutation
- ></template>
- </design-sidebar>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/pages/index.vue b/app/assets/javascripts/design_management_legacy/pages/index.vue
deleted file mode 100644
index 66008a193ce..00000000000
--- a/app/assets/javascripts/design_management_legacy/pages/index.vue
+++ /dev/null
@@ -1,323 +0,0 @@
-<script>
-import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__, sprintf } from '~/locale';
-import UploadButton from '../components/upload/button.vue';
-import DeleteButton from '../components/delete_button.vue';
-import Design from '../components/list/item.vue';
-import DesignDestroyer from '../components/design_destroyer.vue';
-import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
-import DesignDropzone from '../components/upload/design_dropzone.vue';
-import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
-import permissionsQuery from '../graphql/queries/design_permissions.query.graphql';
-import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import allDesignsMixin from '../mixins/all_designs';
-import {
- UPLOAD_DESIGN_ERROR,
- EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
- EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
- designUploadSkippedWarning,
- designDeletionError,
-} from '../utils/error_messages';
-import { updateStoreAfterUploadDesign } from '../utils/cache_update';
-import {
- designUploadOptimisticResponse,
- isValidDesignFile,
-} from '../utils/design_management_utils';
-import { getFilename } from '~/lib/utils/file_upload';
-import { DESIGNS_ROUTE_NAME } from '../router/constants';
-
-const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
-
-export default {
- components: {
- GlLoadingIcon,
- GlAlert,
- GlDeprecatedButton,
- UploadButton,
- Design,
- DesignDestroyer,
- DesignVersionDropdown,
- DeleteButton,
- DesignDropzone,
- },
- mixins: [allDesignsMixin],
- apollo: {
- permissions: {
- query: permissionsQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- };
- },
- update: data => data.project.issue.userPermissions,
- },
- },
- data() {
- return {
- permissions: {
- createDesign: false,
- },
- filesToBeSaved: [],
- selectedDesigns: [],
- };
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.designs.loading || this.$apollo.queries.permissions.loading;
- },
- isSaving() {
- return this.filesToBeSaved.length > 0;
- },
- canCreateDesign() {
- return this.permissions.createDesign;
- },
- showToolbar() {
- return this.canCreateDesign && this.allVersions.length > 0;
- },
- hasDesigns() {
- return this.designs.length > 0;
- },
- hasSelectedDesigns() {
- return this.selectedDesigns.length > 0;
- },
- canDeleteDesigns() {
- return this.isLatestVersion && this.hasSelectedDesigns;
- },
- projectQueryBody() {
- return {
- query: getDesignListQuery,
- variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null },
- };
- },
- selectAllButtonText() {
- return this.hasSelectedDesigns
- ? s__('DesignManagement|Deselect all')
- : s__('DesignManagement|Select all');
- },
- },
- mounted() {
- this.toggleOnPasteListener(this.$route.name);
- },
- methods: {
- resetFilesToBeSaved() {
- this.filesToBeSaved = [];
- },
- /**
- * Determine if a design upload is valid, given [files]
- * @param {Array<File>} files
- */
- isValidDesignUpload(files) {
- if (!this.canCreateDesign) return false;
-
- if (files.length > MAXIMUM_FILE_UPLOAD_LIMIT) {
- createFlash(
- sprintf(
- s__(
- 'DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again.',
- ),
- {
- upload_limit: MAXIMUM_FILE_UPLOAD_LIMIT,
- },
- ),
- );
-
- return false;
- }
- return true;
- },
- onUploadDesign(files) {
- // convert to Array so that we have Array methods (.map, .some, etc.)
- this.filesToBeSaved = Array.from(files);
- if (!this.isValidDesignUpload(this.filesToBeSaved)) return null;
-
- const mutationPayload = {
- optimisticResponse: designUploadOptimisticResponse(this.filesToBeSaved),
- variables: {
- files: this.filesToBeSaved,
- projectPath: this.projectPath,
- iid: this.issueIid,
- },
- context: {
- hasUpload: true,
- },
- mutation: uploadDesignMutation,
- update: this.afterUploadDesign,
- };
-
- return this.$apollo
- .mutate(mutationPayload)
- .then(res => this.onUploadDesignDone(res))
- .catch(() => this.onUploadDesignError());
- },
- afterUploadDesign(
- store,
- {
- data: { designManagementUpload },
- },
- ) {
- updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody);
- },
- onUploadDesignDone(res) {
- const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || [];
- const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles);
- if (skippedWarningMessage) {
- createFlash(skippedWarningMessage, 'warning');
- }
-
- // if this upload resulted in a new version being created, redirect user to the latest version
- if (!this.isLatestVersion) {
- this.$router.push({ name: DESIGNS_ROUTE_NAME });
- }
- this.resetFilesToBeSaved();
- },
- onUploadDesignError() {
- this.resetFilesToBeSaved();
- createFlash(UPLOAD_DESIGN_ERROR);
- },
- changeSelectedDesigns(filename) {
- if (this.isDesignSelected(filename)) {
- this.selectedDesigns = this.selectedDesigns.filter(design => design !== filename);
- } else {
- this.selectedDesigns.push(filename);
- }
- },
- toggleDesignsSelection() {
- if (this.hasSelectedDesigns) {
- this.selectedDesigns = [];
- } else {
- this.selectedDesigns = this.designs.map(design => design.filename);
- }
- },
- isDesignSelected(filename) {
- return this.selectedDesigns.includes(filename);
- },
- isDesignToBeSaved(filename) {
- return this.filesToBeSaved.some(file => file.name === filename);
- },
- canSelectDesign(filename) {
- return this.isLatestVersion && this.canCreateDesign && !this.isDesignToBeSaved(filename);
- },
- onDesignDelete() {
- this.selectedDesigns = [];
- if (this.$route.query.version) this.$router.push({ name: DESIGNS_ROUTE_NAME });
- },
- onDesignDeleteError() {
- const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 });
- createFlash(errorMessage);
- },
- onExistingDesignDropzoneChange(files, existingDesignFilename) {
- const filesArr = Array.from(files);
-
- if (filesArr.length > 1) {
- createFlash(EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE);
- return;
- }
-
- if (!filesArr.some(({ name }) => existingDesignFilename === name)) {
- createFlash(EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE);
- return;
- }
-
- this.onUploadDesign(files);
- },
- onDesignPaste(event) {
- const { clipboardData } = event;
- const files = Array.from(clipboardData.files);
- if (clipboardData && files.length > 0) {
- if (!files.some(isValidDesignFile)) {
- return;
- }
- event.preventDefault();
- let filename = getFilename(event);
- if (!filename || filename === 'image.png') {
- filename = `design_${Date.now()}.png`;
- }
- const newFile = new File([files[0]], filename);
- this.onUploadDesign([newFile]);
- }
- },
- toggleOnPasteListener(route) {
- if (route === DESIGNS_ROUTE_NAME) {
- document.addEventListener('paste', this.onDesignPaste);
- } else {
- document.removeEventListener('paste', this.onDesignPaste);
- }
- },
- },
- beforeRouteUpdate(to, from, next) {
- this.toggleOnPasteListener(to.name);
- this.selectedDesigns = [];
- next();
- },
- beforeRouteLeave(to, from, next) {
- this.toggleOnPasteListener(to.name);
- next();
- },
-};
-</script>
-
-<template>
- <div>
- <header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
- <div class="d-flex justify-content-between align-items-center w-100">
- <design-version-dropdown />
- <div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]">
- <gl-deprecated-button
- v-if="isLatestVersion"
- variant="link"
- class="mr-2 js-select-all"
- @click="toggleDesignsSelection"
- >{{ selectAllButtonText }}</gl-deprecated-button
- >
- <design-destroyer
- #default="{ mutate, loading }"
- :filenames="selectedDesigns"
- :project-path="projectPath"
- :iid="issueIid"
- @done="onDesignDelete"
- @error="onDesignDeleteError"
- >
- <delete-button
- v-if="isLatestVersion"
- :is-deleting="loading"
- button-class="btn-danger btn-inverted mr-2"
- :has-selected-designs="hasSelectedDesigns"
- @deleteSelectedDesigns="mutate()"
- >
- {{ s__('DesignManagement|Delete selected') }}
- <gl-loading-icon v-if="loading" inline class="ml-1" />
- </delete-button>
- </design-destroyer>
- <upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" />
- </div>
- </div>
- </header>
- <div class="mt-4">
- <gl-loading-icon v-if="isLoading" size="md" />
- <gl-alert v-else-if="error" variant="danger" :dismissible="false">
- {{ __('An error occurred while loading designs. Please try again.') }}
- </gl-alert>
- <ol v-else class="list-unstyled row">
- <li class="col-md-6 col-lg-4 mb-3">
- <design-dropzone class="design-list-item" @change="onUploadDesign" />
- </li>
- <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
- <design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)"
- ><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)"
- /></design-dropzone>
-
- <input
- v-if="canSelectDesign(design.filename)"
- :checked="isDesignSelected(design.filename)"
- type="checkbox"
- class="design-checkbox"
- @change="changeSelectedDesigns(design.filename)"
- />
- </li>
- </ol>
- </div>
- <router-view :key="$route.fullPath" />
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/router/constants.js b/app/assets/javascripts/design_management_legacy/router/constants.js
deleted file mode 100644
index abeef520e33..00000000000
--- a/app/assets/javascripts/design_management_legacy/router/constants.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const ROOT_ROUTE_NAME = 'root';
-export const DESIGNS_ROUTE_NAME = 'designs';
-export const DESIGN_ROUTE_NAME = 'design';
diff --git a/app/assets/javascripts/design_management_legacy/router/index.js b/app/assets/javascripts/design_management_legacy/router/index.js
deleted file mode 100644
index 28a81ed0278..00000000000
--- a/app/assets/javascripts/design_management_legacy/router/index.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-import routes from './routes';
-import { DESIGN_ROUTE_NAME } from './constants';
-import { getPageLayoutElement } from '~/design_management_legacy/utils/design_management_utils';
-import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
-
-Vue.use(VueRouter);
-
-export default function createRouter(base) {
- const router = new VueRouter({
- base,
- mode: 'history',
- routes,
- });
- const pageEl = getPageLayoutElement();
-
- router.beforeEach(({ meta: { el }, name }, _, next) => {
- $(`#${el}`).tab('show');
-
- // apply a fullscreen layout style in Design View (a.k.a design detail)
- if (pageEl) {
- if (name === DESIGN_ROUTE_NAME) {
- pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
- } else {
- pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
- }
- }
-
- next();
- });
-
- return router;
-}
diff --git a/app/assets/javascripts/design_management_legacy/router/routes.js b/app/assets/javascripts/design_management_legacy/router/routes.js
deleted file mode 100644
index 788910e5514..00000000000
--- a/app/assets/javascripts/design_management_legacy/router/routes.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import Home from '../pages/index.vue';
-import DesignDetail from '../pages/design/index.vue';
-import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
-
-export default [
- {
- name: ROOT_ROUTE_NAME,
- path: '/',
- component: Home,
- meta: {
- el: 'discussion',
- },
- },
- {
- name: DESIGNS_ROUTE_NAME,
- path: '/designs',
- component: Home,
- meta: {
- el: 'designs',
- },
- children: [
- {
- name: DESIGN_ROUTE_NAME,
- path: ':id',
- component: DesignDetail,
- meta: {
- el: 'designs',
- },
- beforeEnter(
- {
- params: { id },
- },
- from,
- next,
- ) {
- if (typeof id === 'string') {
- next();
- }
- },
- props: ({ params: { id } }) => ({ id }),
- },
- ],
- },
-];
diff --git a/app/assets/javascripts/design_management_legacy/utils/cache_update.js b/app/assets/javascripts/design_management_legacy/utils/cache_update.js
deleted file mode 100644
index 5ba6f84c413..00000000000
--- a/app/assets/javascripts/design_management_legacy/utils/cache_update.js
+++ /dev/null
@@ -1,276 +0,0 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { extractCurrentDiscussion, extractDesign } from './design_management_utils';
-import {
- ADD_IMAGE_DIFF_NOTE_ERROR,
- UPDATE_IMAGE_DIFF_NOTE_ERROR,
- ADD_DISCUSSION_COMMENT_ERROR,
- designDeletionError,
-} from './error_messages';
-
-const deleteDesignsFromStore = (store, query, selectedDesigns) => {
- const data = store.readQuery(query);
-
- const changedDesigns = data.project.issue.designCollection.designs.edges.filter(
- ({ node }) => !selectedDesigns.includes(node.filename),
- );
- data.project.issue.designCollection.designs.edges = [...changedDesigns];
-
- store.writeQuery({
- ...query,
- data,
- });
-};
-
-/**
- * Adds a new version of designs to store
- *
- * @param {Object} store
- * @param {Object} query
- * @param {Object} version
- */
-const addNewVersionToStore = (store, query, version) => {
- if (!version) return;
-
- const data = store.readQuery(query);
- const newEdge = { node: version, __typename: 'DesignVersionEdge' };
-
- data.project.issue.designCollection.versions.edges = [
- newEdge,
- ...data.project.issue.designCollection.versions.edges,
- ];
-
- store.writeQuery({
- ...query,
- data,
- });
-};
-
-const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => {
- const data = store.readQuery({
- query,
- variables: queryVariables,
- });
-
- const design = extractDesign(data);
- const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId);
- currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note];
-
- design.notesCount += 1;
- if (
- !design.issue.participants.edges.some(
- participant => participant.node.username === createNote.note.author.username,
- )
- ) {
- design.issue.participants.edges = [
- ...design.issue.participants.edges,
- {
- __typename: 'UserEdge',
- node: {
- __typename: 'User',
- ...createNote.note.author,
- },
- },
- ];
- }
- store.writeQuery({
- query,
- variables: queryVariables,
- data: {
- ...data,
- design: {
- ...design,
- },
- },
- });
-};
-
-const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => {
- const data = store.readQuery({
- query,
- variables,
- });
- const newDiscussion = {
- __typename: 'Discussion',
- id: createImageDiffNote.note.discussion.id,
- replyId: createImageDiffNote.note.discussion.replyId,
- resolvable: true,
- resolved: false,
- resolvedAt: null,
- resolvedBy: null,
- notes: {
- __typename: 'NoteConnection',
- nodes: [createImageDiffNote.note],
- },
- };
- const design = extractDesign(data);
- const notesCount = design.notesCount + 1;
- design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
- if (
- !design.issue.participants.edges.some(
- participant => participant.node.username === createImageDiffNote.note.author.username,
- )
- ) {
- design.issue.participants.edges = [
- ...design.issue.participants.edges,
- {
- __typename: 'UserEdge',
- node: {
- __typename: 'User',
- ...createImageDiffNote.note.author,
- },
- },
- ];
- }
- store.writeQuery({
- query,
- variables,
- data: {
- ...data,
- design: {
- ...design,
- notesCount,
- },
- },
- });
-};
-
-const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => {
- const data = store.readQuery({
- query,
- variables,
- });
-
- const design = extractDesign(data);
- const discussion = extractCurrentDiscussion(
- design.discussions,
- updateImageDiffNote.note.discussion.id,
- );
-
- discussion.notes = {
- ...discussion.notes,
- nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
- };
-
- store.writeQuery({
- query,
- variables,
- data: {
- ...data,
- design,
- },
- });
-};
-
-const addNewDesignToStore = (store, designManagementUpload, query) => {
- const data = store.readQuery(query);
-
- const newDesigns = data.project.issue.designCollection.designs.edges.reduce((acc, design) => {
- if (!acc.find(d => d.filename === design.node.filename)) {
- acc.push(design.node);
- }
-
- return acc;
- }, designManagementUpload.designs);
-
- let newVersionNode;
- const findNewVersions = designManagementUpload.designs.find(design => design.versions);
-
- if (findNewVersions) {
- const findNewVersionsEdges = findNewVersions.versions.edges;
-
- if (findNewVersionsEdges && findNewVersionsEdges.length) {
- newVersionNode = [findNewVersionsEdges[0]];
- }
- }
-
- const newVersions = [
- ...(newVersionNode || []),
- ...data.project.issue.designCollection.versions.edges,
- ];
-
- const updatedDesigns = {
- __typename: 'DesignCollection',
- designs: {
- __typename: 'DesignConnection',
- edges: newDesigns.map(design => ({
- __typename: 'DesignEdge',
- node: design,
- })),
- },
- versions: {
- __typename: 'DesignVersionConnection',
- edges: newVersions,
- },
- };
-
- data.project.issue.designCollection = updatedDesigns;
-
- store.writeQuery({
- ...query,
- data,
- });
-};
-
-const onError = (data, message) => {
- createFlash(message);
- throw new Error(data.errors);
-};
-
-export const hasErrors = ({ errors = [] }) => errors?.length;
-
-/**
- * Updates a store after design deletion
- *
- * @param {Object} store
- * @param {Object} data
- * @param {Object} query
- * @param {Array} designs
- */
-export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
- if (hasErrors(data)) {
- onError(data, designDeletionError({ singular: designs.length === 1 }));
- } else {
- deleteDesignsFromStore(store, query, designs);
- addNewVersionToStore(store, query, data.version);
- }
-};
-
-export const updateStoreAfterAddDiscussionComment = (
- store,
- data,
- query,
- queryVariables,
- discussionId,
-) => {
- if (hasErrors(data)) {
- onError(data, ADD_DISCUSSION_COMMENT_ERROR);
- } else {
- addDiscussionCommentToStore(store, data, query, queryVariables, discussionId);
- }
-};
-
-export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => {
- if (hasErrors(data)) {
- onError(data, ADD_IMAGE_DIFF_NOTE_ERROR);
- } else {
- addImageDiffNoteToStore(store, data, query, queryVariables);
- }
-};
-
-export const updateStoreAfterUpdateImageDiffNote = (store, data, query, queryVariables) => {
- if (hasErrors(data)) {
- onError(data, UPDATE_IMAGE_DIFF_NOTE_ERROR);
- } else {
- updateImageDiffNoteInStore(store, data, query, queryVariables);
- }
-};
-
-export const updateStoreAfterUploadDesign = (store, data, query) => {
- if (hasErrors(data)) {
- onError(data, data.errors[0]);
- } else {
- addNewDesignToStore(store, data, query);
- }
-};
diff --git a/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js b/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js
deleted file mode 100644
index 22705cf67a1..00000000000
--- a/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import { uniqueId } from 'lodash';
-import { VALID_DESIGN_FILE_MIMETYPE } from '../constants';
-
-export const isValidDesignFile = ({ type }) =>
- (type.match(VALID_DESIGN_FILE_MIMETYPE.regex) || []).length > 0;
-
-/**
- * Returns formatted array that doesn't contain
- * `edges`->`node` nesting
- *
- * @param {Array} elements
- */
-
-export const extractNodes = elements => elements.edges.map(({ node }) => node);
-
-/**
- * Returns formatted array of discussions that doesn't contain
- * `edges`->`node` nesting for child notes
- *
- * @param {Array} discussions
- */
-
-export const extractDiscussions = discussions =>
- discussions.nodes.map((discussion, index) => ({
- ...discussion,
- index: index + 1,
- notes: discussion.notes.nodes,
- }));
-
-/**
- * Returns a discussion with the given id from discussions array
- *
- * @param {Array} discussions
- */
-
-export const extractCurrentDiscussion = (discussions, id) =>
- discussions.nodes.find(discussion => discussion.id === id);
-
-export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1];
-
-export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1];
-
-export const extractDesigns = data => data.project.issue.designCollection.designs.edges;
-
-export const extractDesign = data => (extractDesigns(data) || [])[0]?.node;
-
-/**
- * Generates optimistic response for a design upload mutation
- * @param {Array<File>} files
- */
-export const designUploadOptimisticResponse = files => {
- const designs = files.map(file => ({
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- // eslint-disable-next-line @gitlab/require-i18n-strings
- __typename: 'Design',
- id: -uniqueId(),
- image: '',
- imageV432x230: '',
- filename: file.name,
- fullPath: '',
- notesCount: 0,
- event: 'NONE',
- diffRefs: {
- __typename: 'DiffRefs',
- baseSha: '',
- startSha: '',
- headSha: '',
- },
- discussions: {
- __typename: 'DesignDiscussion',
- nodes: [],
- },
- versions: {
- __typename: 'DesignVersionConnection',
- edges: {
- __typename: 'DesignVersionEdge',
- node: {
- __typename: 'DesignVersion',
- id: -uniqueId(),
- sha: -uniqueId(),
- },
- },
- },
- }));
-
- return {
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- // eslint-disable-next-line @gitlab/require-i18n-strings
- __typename: 'Mutation',
- designManagementUpload: {
- __typename: 'DesignManagementUploadPayload',
- designs,
- skippedDesigns: [],
- errors: [],
- },
- };
-};
-
-/**
- * Generates optimistic response for a design upload mutation
- * @param {Array<File>} files
- */
-export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- // eslint-disable-next-line @gitlab/require-i18n-strings
- __typename: 'Mutation',
- updateImageDiffNote: {
- __typename: 'UpdateImageDiffNotePayload',
- note: {
- ...note,
- position: {
- ...note.position,
- ...position,
- },
- },
- errors: [],
- },
-});
-
-const normalizeAuthor = author => ({
- ...author,
- web_url: author.webUrl,
- avatar_url: author.avatarUrl,
-});
-
-export const extractParticipants = users => users.edges.map(({ node }) => normalizeAuthor(node));
-
-export const getPageLayoutElement = () => document.querySelector('.layout-page');
diff --git a/app/assets/javascripts/design_management_legacy/utils/error_messages.js b/app/assets/javascripts/design_management_legacy/utils/error_messages.js
deleted file mode 100644
index 7666c726c2f..00000000000
--- a/app/assets/javascripts/design_management_legacy/utils/error_messages.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { __, s__, n__, sprintf } from '~/locale';
-
-export const ADD_DISCUSSION_COMMENT_ERROR = s__(
- 'DesignManagement|Could not add a new comment. Please try again.',
-);
-
-export const ADD_IMAGE_DIFF_NOTE_ERROR = s__(
- 'DesignManagement|Could not create new discussion. Please try again.',
-);
-
-export const UPDATE_IMAGE_DIFF_NOTE_ERROR = s__(
- 'DesignManagement|Could not update discussion. Please try again.',
-);
-
-export const UPDATE_NOTE_ERROR = s__('DesignManagement|Could not update note. Please try again.');
-
-export const UPLOAD_DESIGN_ERROR = s__(
- 'DesignManagement|Error uploading a new design. Please try again.',
-);
-
-export const UPLOAD_DESIGN_INVALID_FILETYPE_ERROR = __(
- 'Could not upload your designs as one or more files uploaded are not supported.',
-);
-
-export const DESIGN_NOT_FOUND_ERROR = __('Could not find design.');
-
-export const DESIGN_VERSION_NOT_EXIST_ERROR = __('Requested design version does not exist.');
-
-const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped.');
-
-const ALL_DESIGNS_SKIPPED_MESSAGE = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__(
- 'The designs you tried uploading did not change.',
-)}`;
-
-export const EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE = __(
- 'You can only upload one design when dropping onto an existing design.',
-);
-
-export const EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE = __(
- 'You must upload a file with the same file name when dropping onto an existing design.',
-);
-
-const MAX_SKIPPED_FILES_LISTINGS = 5;
-
-const oneDesignSkippedMessage = filename =>
- `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${sprintf(s__('DesignManagement|%{filename} did not change.'), {
- filename,
- })}`;
-
-/**
- * Return warning message indicating that some (but not all) uploaded
- * files were skipped.
- * @param {Array<{ filename }>} skippedFiles
- */
-const someDesignsSkippedMessage = skippedFiles => {
- const designsSkippedMessage = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__(
- 'Some of the designs you tried uploading did not change:',
- )}`;
-
- const moreText = sprintf(s__(`DesignManagement|and %{moreCount} more.`), {
- moreCount: skippedFiles.length - MAX_SKIPPED_FILES_LISTINGS,
- });
-
- return `${designsSkippedMessage} ${skippedFiles
- .slice(0, MAX_SKIPPED_FILES_LISTINGS)
- .map(({ filename }) => filename)
- .join(', ')}${skippedFiles.length > MAX_SKIPPED_FILES_LISTINGS ? `, ${moreText}` : '.'}`;
-};
-
-export const designDeletionError = ({ singular = true } = {}) => {
- const design = singular ? __('a design') : __('designs');
- return sprintf(s__('Could not delete %{design}. Please try again.'), {
- design,
- });
-};
-
-/**
- * Return warning message, if applicable, that one, some or all uploaded
- * files were skipped.
- * @param {Array<{ filename }>} uploadedDesigns
- * @param {Array<{ filename }>} skippedFiles
- */
-export const designUploadSkippedWarning = (uploadedDesigns, skippedFiles) => {
- if (skippedFiles.length === 0) {
- return null;
- }
-
- if (skippedFiles.length === uploadedDesigns.length) {
- const { filename } = skippedFiles[0];
-
- return n__(oneDesignSkippedMessage(filename), ALL_DESIGNS_SKIPPED_MESSAGE, skippedFiles.length);
- }
-
- return someDesignsSkippedMessage(skippedFiles);
-};
diff --git a/app/assets/javascripts/design_management_legacy/utils/tracking.js b/app/assets/javascripts/design_management_legacy/utils/tracking.js
deleted file mode 100644
index b3ecc1453a6..00000000000
--- a/app/assets/javascripts/design_management_legacy/utils/tracking.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Tracking from '~/tracking';
-
-// Tracking Constants
-const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0';
-const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
-const DESIGN_TRACKING_EVENT_NAME = 'view_design';
-
-// eslint-disable-next-line import/prefer-default-export
-export function trackDesignDetailView(
- referer = '',
- owner = '',
- designVersion = 1,
- latestVersion = false,
-) {
- Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENT_NAME, {
- label: DESIGN_TRACKING_EVENT_NAME,
- context: {
- schema: DESIGN_TRACKING_CONTEXT_SCHEMA,
- data: {
- 'design-version-number': designVersion,
- 'design-is-current-version': latestVersion,
- 'internal-object-referrer': referer,
- 'design-collection-owner': owner,
- },
- },
- });
-}
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 0991f5282a8..1de00c9f08b 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -2,6 +2,7 @@
/* global CommentsStore */
import $ from 'jquery';
+import 'vendor/jquery.scrollTo';
import Vue from 'vue';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 5062006424e..dd5addbf1e3 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlLoadingIcon, GlButtonGroup, GlButton, GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui';
import Mousetrap from 'mousetrap';
import { __ } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
@@ -13,9 +13,13 @@ import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
import NoChanges from './no_changes.vue';
-import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue';
import TreeList from './tree_list.vue';
+
+import HiddenFilesWarning from './hidden_files_warning.vue';
+import MergeConflictWarning from './merge_conflict_warning.vue';
+import CollapsedFilesWarning from './collapsed_files_warning.vue';
+
import {
TREE_LIST_WIDTH_STORAGE_KEY,
INITIAL_TREE_WIDTH,
@@ -24,6 +28,9 @@ import {
TREE_HIDE_STATS_WIDTH,
MR_TREE_SHOW_KEY,
CENTERED_LIMITED_CONTAINER_CLASSES,
+ ALERT_OVERFLOW_HIDDEN,
+ ALERT_MERGE_CONFLICT,
+ ALERT_COLLAPSED_FILES,
} from '../constants';
export default {
@@ -33,15 +40,21 @@ export default {
DiffFile,
NoChanges,
HiddenFilesWarning,
+ MergeConflictWarning,
+ CollapsedFilesWarning,
CommitWidget,
TreeList,
GlLoadingIcon,
PanelResizer,
- GlButtonGroup,
- GlButton,
- GlAlert,
+ GlPagination,
+ GlSprintf,
},
mixins: [glFeatureFlagsMixin()],
+ alerts: {
+ ALERT_OVERFLOW_HIDDEN,
+ ALERT_MERGE_CONFLICT,
+ ALERT_COLLAPSED_FILES,
+ },
props: {
endpoint: {
type: String,
@@ -111,6 +124,7 @@ export default {
return {
treeWidth,
diffFilesLength: 0,
+ collapsedWarningDismissed: false,
};
},
computed: {
@@ -139,7 +153,7 @@ export default {
'canMerge',
'hasConflicts',
]),
- ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
+ ...mapGetters('diffs', ['hasCollapsedFile', 'isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
if (!this.viewDiffsFileByFile) {
@@ -169,6 +183,39 @@ export default {
isDiffHead() {
return parseBoolean(getParameterByName('diff_head'));
},
+ showFileByFileNavigation() {
+ return this.diffFiles.length > 1 && this.viewDiffsFileByFile;
+ },
+ currentFileNumber() {
+ return this.currentDiffIndex + 1;
+ },
+ previousFileNumber() {
+ const { currentDiffIndex } = this;
+
+ return currentDiffIndex >= 1 ? currentDiffIndex : null;
+ },
+ nextFileNumber() {
+ const { currentFileNumber, diffFiles } = this;
+
+ return currentFileNumber < diffFiles.length ? currentFileNumber + 1 : null;
+ },
+ visibleWarning() {
+ let visible = false;
+
+ if (this.renderOverflowWarning) {
+ visible = this.$options.alerts.ALERT_OVERFLOW_HIDDEN;
+ } else if (this.isDiffHead && this.hasConflicts) {
+ visible = this.$options.alerts.ALERT_MERGE_CONFLICT;
+ } else if (
+ this.hasCollapsedFile &&
+ !this.collapsedWarningDismissed &&
+ !this.viewDiffsFileByFile
+ ) {
+ visible = this.$options.alerts.ALERT_COLLAPSED_FILES;
+ }
+
+ return visible;
+ },
},
watch: {
commit(newCommit, oldCommit) {
@@ -186,7 +233,7 @@ export default {
}
},
diffViewType() {
- if (this.needsReload() || this.needsFirstLoad()) {
+ if (!this.glFeatures.unifiedDiffLines && (this.needsReload() || this.needsFirstLoad())) {
this.refetchDiffData();
}
this.adjustView();
@@ -212,7 +259,6 @@ export default {
projectPath: this.projectPath,
dismissEndpoint: this.dismissEndpoint,
showSuggestPopover: this.showSuggestPopover,
- useSingleDiffStyle: this.glFeatures.singleMrDiffView,
viewDiffsFileByFile: this.viewDiffsFileByFile,
});
@@ -262,7 +308,6 @@ export default {
...mapActions('diffs', [
'moveToNeighboringCommit',
'setBaseConfig',
- 'fetchDiffFiles',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
'fetchCoverageFiles',
@@ -274,6 +319,9 @@ export default {
'toggleShowTreeList',
'navigateToDiffFileIndex',
]),
+ navigateToDiffFileNumber(number) {
+ this.navigateToDiffFileIndex(number - 1);
+ },
refetchDiffData() {
this.fetchData(false);
},
@@ -286,60 +334,35 @@ export default {
);
},
needsReload() {
- return (
- this.glFeatures.singleMrDiffView &&
- this.diffFiles.length &&
- isSingleViewStyle(this.diffFiles[0])
- );
+ return this.diffFiles.length && isSingleViewStyle(this.diffFiles[0]);
},
needsFirstLoad() {
- return this.glFeatures.singleMrDiffView && !this.diffFiles.length;
+ return !this.diffFiles.length;
},
fetchData(toggleTree = true) {
- if (this.glFeatures.diffsBatchLoad) {
- this.fetchDiffFilesMeta()
- .then(({ real_size }) => {
- this.diffFilesLength = parseInt(real_size, 10);
- if (toggleTree) this.hideTreeListIfJustOneFile();
+ this.fetchDiffFilesMeta()
+ .then(({ real_size }) => {
+ this.diffFilesLength = parseInt(real_size, 10);
+ if (toggleTree) this.hideTreeListIfJustOneFile();
- this.startDiffRendering();
- })
- .catch(() => {
- createFlash(__('Something went wrong on our end. Please try again!'));
- });
-
- this.fetchDiffFilesBatch()
- .then(() => {
- // Guarantee the discussions are assigned after the batch finishes.
- // Just watching the length of the discussions or the diff files
- // isn't enough, because with split diff loading, neither will
- // change when loading the other half of the diff files.
- this.setDiscussions();
- })
- .then(() => this.startDiffRendering())
- .catch(() => {
- createFlash(__('Something went wrong on our end. Please try again!'));
- });
- } else {
- this.fetchDiffFiles()
- .then(({ real_size }) => {
- this.diffFilesLength = parseInt(real_size, 10);
- if (toggleTree) {
- this.hideTreeListIfJustOneFile();
- }
+ this.startDiffRendering();
+ })
+ .catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
- requestIdleCallback(
- () => {
- this.setDiscussions();
- this.startRenderDiffsQueue();
- },
- { timeout: 1000 },
- );
- })
- .catch(() => {
- createFlash(__('Something went wrong on our end. Please try again!'));
- });
- }
+ this.fetchDiffFilesBatch()
+ .then(() => {
+ // Guarantee the discussions are assigned after the batch finishes.
+ // Just watching the length of the discussions or the diff files
+ // isn't enough, because with split diff loading, neither will
+ // change when loading the other half of the diff files.
+ this.setDiscussions();
+ })
+ .then(() => this.startDiffRendering())
+ .catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
if (this.endpointCoverage) {
this.fetchCoverageFiles();
@@ -406,6 +429,9 @@ export default {
this.toggleShowTreeList(false);
}
},
+ dismissCollapsedWarning() {
+ this.collapsedWarningDismissed = true;
+ },
},
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
@@ -423,59 +449,27 @@ export default {
/>
<hidden-files-warning
- v-if="renderOverflowWarning"
+ v-if="visibleWarning == $options.alerts.ALERT_OVERFLOW_HIDDEN"
:visible="numVisibleFiles"
:total="numTotalFiles"
:plain-diff-path="plainDiffPath"
:email-patch-path="emailPatchPath"
/>
-
- <div
- v-if="isDiffHead && hasConflicts"
- :class="{
- [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
- }"
- >
- <gl-alert
- :dismissible="false"
- :title="__('There are merge conflicts')"
- variant="warning"
- class="w-100 mb-3"
- >
- <p class="mb-1">
- {{ __('The comparison view may be inaccurate due to merge conflicts.') }}
- </p>
- <p class="mb-0">
- {{
- __(
- 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.',
- )
- }}
- </p>
- <template #actions>
- <gl-button
- v-if="conflictResolutionPath"
- :href="conflictResolutionPath"
- variant="info"
- class="mr-3 gl-alert-action"
- >
- {{ __('Resolve conflicts') }}
- </gl-button>
- <gl-button
- v-if="canMerge"
- class="gl-alert-action"
- data-toggle="modal"
- data-target="#modal_merge_info"
- >
- {{ __('Merge locally') }}
- </gl-button>
- </template>
- </gl-alert>
- </div>
+ <merge-conflict-warning
+ v-if="visibleWarning == $options.alerts.ALERT_MERGE_CONFLICT"
+ :limited="isLimitedContainer"
+ :resolution-path="conflictResolutionPath"
+ :mergeable="canMerge"
+ />
+ <collapsed-files-warning
+ v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
+ :limited="isLimitedContainer"
+ @dismiss="dismissCollapsedWarning"
+ />
<div
:data-can-create-note="getNoteableData.current_user.can_create_note"
- class="files d-flex"
+ class="files d-flex gl-mt-2"
>
<div
v-if="showTreeList"
@@ -509,23 +503,22 @@ export default {
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
/>
- <div v-if="viewDiffsFileByFile" class="d-flex gl-justify-content-center">
- <gl-button-group>
- <gl-button
- :disabled="currentDiffIndex === 0"
- data-testid="singleFilePrevious"
- @click="navigateToDiffFileIndex(currentDiffIndex - 1)"
- >
- {{ __('Prev') }}
- </gl-button>
- <gl-button
- :disabled="currentDiffIndex === diffFiles.length - 1"
- data-testid="singleFileNext"
- @click="navigateToDiffFileIndex(currentDiffIndex + 1)"
- >
- {{ __('Next') }}
- </gl-button>
- </gl-button-group>
+ <div
+ v-if="showFileByFileNavigation"
+ data-testid="file-by-file-navigation"
+ class="gl-display-grid gl-text-center"
+ >
+ <gl-pagination
+ class="gl-mx-auto"
+ :value="currentFileNumber"
+ :prev-page="previousFileNumber"
+ :next-page="nextFileNumber"
+ @input="navigateToDiffFileNumber"
+ />
+ <gl-sprintf :message="__('File %{current} of %{total}')">
+ <template #current>{{ currentFileNumber }}</template>
+ <template #total>{{ diffFiles.length }}</template>
+ </gl-sprintf>
</div>
</template>
<no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" />
diff --git a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
new file mode 100644
index 00000000000..dded3643115
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
@@ -0,0 +1,71 @@
+<script>
+import { mapActions } from 'vuex';
+
+import { GlAlert, GlButton } from '@gitlab/ui';
+
+import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ },
+ props: {
+ limited: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ dismissed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isDismissed: this.dismissed,
+ };
+ },
+ computed: {
+ containerClasses() {
+ return {
+ [CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
+ };
+ },
+ },
+
+ methods: {
+ ...mapActions('diffs', ['expandAllFiles']),
+ dismiss() {
+ this.isDismissed = true;
+ this.$emit('dismiss');
+ },
+ expand() {
+ this.expandAllFiles();
+ this.dismiss();
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="!isDismissed" data-testid="root" :class="containerClasses">
+ <gl-alert
+ :dismissible="true"
+ :title="__('Some changes are not shown')"
+ variant="warning"
+ class="gl-mb-5"
+ @dismiss="dismiss"
+ >
+ <p class="gl-mb-0">
+ {{ __('For a faster browsing experience, some files are collapsed by default.') }}
+ </p>
+ <template #actions>
+ <gl-button category="secondary" variant="warning" class="gl-alert-action" @click="expand">
+ {{ __('Expand all files') }}
+ </gl-button>
+ </template>
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 274a4027e62..23669eecce2 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -1,11 +1,11 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions } from 'vuex';
import { GlButtonGroup, GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -39,7 +39,6 @@ import { setUrlParams } from '../../lib/utils/url_utility';
export default {
components: {
UserAvatarLink,
- Icon,
ClipboardButton,
TimeAgoTooltip,
CommitPipelineStatus,
@@ -150,14 +149,13 @@ export default {
<span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
- <button
+ <gl-button
v-if="commit.description_html && collapsible"
- class="text-expander js-toggle-button"
- type="button"
+ class="js-toggle-button"
+ size="small"
+ icon="ellipsis_h"
:aria-label="__('Toggle commit description')"
- >
- <icon :size="12" name="ellipsis_h" />
- </button>
+ />
<div class="committer">
<a
diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
index ed4edabd81c..8263e938e69 100644
--- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
+++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
@@ -1,10 +1,10 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- Icon,
+ GlIcon,
TimeAgo,
},
props: {
@@ -29,7 +29,7 @@ export default {
aria-expanded="false"
>
<span> {{ selectedVersionName }} </span>
- <icon :size="12" name="angle-down" class="position-absolute" />
+ <gl-icon :size="12" name="angle-down" class="position-absolute" />
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-content">
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 35e4527af69..b94874c5644 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -1,9 +1,8 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlSprintf } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import { polyfillSticky } from '~/lib/utils/sticky';
-import Icon from '~/vue_shared/components/icon.vue';
import CompareDropdownLayout from './compare_dropdown_layout.vue';
import SettingsDropdown from './settings_dropdown.vue';
import DiffStats from './diff_stats.vue';
@@ -12,9 +11,8 @@ import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default {
components: {
CompareDropdownLayout,
- Icon,
GlLink,
- GlDeprecatedButton,
+ GlButton,
GlSprintf,
SettingsDropdown,
DiffStats,
@@ -84,18 +82,15 @@ export default {
[CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
}"
>
- <button
+ <gl-button
v-gl-tooltip.hover
- type="button"
- class="btn btn-default gl-mr-3 js-toggle-tree-list"
- :class="{
- active: showTreeList,
- }"
+ variant="default"
+ icon="file-tree"
+ class="gl-mr-3 js-toggle-tree-list"
:title="toggleFileBrowserTitle"
+ :selected="showTreeList"
@click="toggleShowTreeList"
- >
- <icon name="file-tree" />
- </button>
+ />
<gl-sprintf
v-if="showDropdowns"
class="d-flex align-items-center compare-versions-container"
@@ -124,16 +119,22 @@ export default {
:added-lines="addedLines"
:removed-lines="removedLines"
/>
- <gl-deprecated-button
+ <gl-button
v-if="commit || startVersion"
:href="latestVersionPath"
+ variant="default"
class="gl-mr-3 js-latest-version"
>
{{ __('Show latest version') }}
- </gl-deprecated-button>
- <gl-deprecated-button v-show="hasCollapsedFile" class="gl-mr-3" @click="expandAllFiles">
+ </gl-button>
+ <gl-button
+ v-show="hasCollapsedFile"
+ variant="default"
+ class="gl-mr-3"
+ @click="expandAllFiles"
+ >
{{ __('Expand all') }}
- </gl-deprecated-button>
+ </gl-button>
<settings-dropdown />
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 087a558efdc..9ecb9a44443 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
@@ -32,7 +33,7 @@ export default {
userAvatarLink,
DiffFileDrafts,
},
- mixins: [diffLineNoteFormMixin, draftCommentsMixin],
+ mixins: [diffLineNoteFormMixin, draftCommentsMixin, glFeatureFlagsMixin()],
props: {
diffFile: {
type: Object,
@@ -48,8 +49,12 @@ export default {
...mapState({
projectPath: state => state.diffs.projectPath,
}),
- ...mapGetters('diffs', ['isInlineView', 'isParallelView']),
- ...mapGetters('diffs', ['getCommentFormForDiffFile']),
+ ...mapGetters('diffs', [
+ 'isInlineView',
+ 'isParallelView',
+ 'getCommentFormForDiffFile',
+ 'diffLines',
+ ]),
...mapGetters(['getNoteableData', 'noteableType', 'getUserData']),
diffMode() {
return getDiffMode(this.diffFile);
@@ -114,13 +119,15 @@ export default {
<inline-diff-view
v-if="isInlineView"
:diff-file="diffFile"
- :diff-lines="diffFile.highlighted_diff_lines || []"
+ :diff-lines="diffFile.highlighted_diff_lines"
:help-page-path="helpPagePath"
/>
<parallel-diff-view
v-else-if="isParallelView"
:diff-file="diffFile"
- :diff-lines="diffFile.parallel_diff_lines || []"
+ :diff-lines="
+ glFeatures.unifiedDiffLines ? diffLines(diffFile) : diffFile.parallel_diff_lines || []
+ "
:help-page-path="helpPagePath"
/>
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index b6a0724c201..7b55bd2104d 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -1,12 +1,12 @@
<script>
import { mapActions } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default {
components: {
noteableDiscussion,
- Icon,
+ GlIcon,
},
props: {
discussions: {
@@ -70,7 +70,7 @@ export default {
class="js-diff-notes-toggle"
@click="toggleDiscussion({ discussionId: discussion.id })"
>
- <icon v-if="discussion.expanded" name="collapse" class="collapse-icon" />
+ <gl-icon v-if="discussion.expanded" name="collapse" class="collapse-icon" />
<template v-else>
{{ index + 1 }}
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index e5e63bdcb43..0094b4f8707 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,8 +1,9 @@
<script>
import { mapState, mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
+import { s__, sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { UNFOLD_COUNT, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import * as utils from '../store/utils';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -17,17 +18,23 @@ const lineNumberByViewType = (viewType, diffLine) => {
[PARALLEL_DIFF_VIEW_TYPE]: line => (line?.right || line?.left)?.new_line,
};
const numberGetter = numberGetters[viewType];
-
return numberGetter && numberGetter(diffLine);
};
+const i18n = {
+ showMore: sprintf(s__('Diffs|Show %{unfoldCount} lines'), { unfoldCount: UNFOLD_COUNT }),
+ showAll: s__('Diffs|Show all unchanged lines'),
+};
+
export default {
+ i18n,
directives: {
tooltip,
},
components: {
- Icon,
+ GlIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
fileHash: {
type: String,
@@ -59,7 +66,9 @@ export default {
},
computed: {
...mapState({
- diffViewType: state => state.diffs.diffViewType,
+ diffViewType(state) {
+ return this.glFeatures.unifiedDiffLines ? INLINE_DIFF_VIEW_TYPE : state.diffs.diffViewType;
+ },
diffFiles: state => state.diffs.diffFiles,
}),
canExpandUp() {
@@ -226,32 +235,27 @@ export default {
</script>
<template>
- <td :colspan="colspan" class="text-center">
+ <td :colspan="colspan" class="text-center gl-font-regular">
<div class="content js-line-expansion-content">
<a
- v-if="canExpandUp"
- v-tooltip
- class="cursor-pointer js-unfold unfold-icon d-inline-block pt-2 pb-2"
- data-placement="top"
- data-container="body"
- :title="__('Expand up')"
- @click="handleExpandLines(EXPAND_UP)"
+ v-if="canExpandDown"
+ class="gl-mx-2 gl-cursor-pointer js-unfold-down gl-display-inline-block gl-py-4"
+ @click="handleExpandLines(EXPAND_DOWN)"
>
- <icon :size="12" name="expand-up" aria-hidden="true" />
+ <gl-icon :size="12" name="expand-down" aria-hidden="true" />
+ <span>{{ $options.i18n.showMore }}</span>
</a>
- <a class="mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
- <span>{{ s__('Diffs|Show unchanged lines') }}</span>
+ <a class="gl-mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
+ <gl-icon :size="12" name="expand" aria-hidden="true" />
+ <span>{{ $options.i18n.showAll }}</span>
</a>
<a
- v-if="canExpandDown"
- v-tooltip
- class="cursor-pointer js-unfold-down has-tooltip unfold-icon d-inline-block pt-2 pb-2"
- data-placement="top"
- data-container="body"
- :title="__('Expand down')"
- @click="handleExpandLines(EXPAND_DOWN)"
+ v-if="canExpandUp"
+ class="gl-mx-2 gl-cursor-pointer js-unfold gl-display-inline-block gl-py-4"
+ @click="handleExpandLines(EXPAND_UP)"
>
- <icon :size="12" name="expand-down" aria-hidden="true" />
+ <gl-icon :size="12" name="expand-up" aria-hidden="true" />
+ <span>{{ $options.i18n.showMore }}</span>
</a>
</div>
</td>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index eace673c2d7..9a7ed76bad3 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,23 +1,32 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { escape } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { __, sprintf } from '~/locale';
+import { sprintf } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants';
+import { GENERIC_ERROR, DIFF_FILE } from '../i18n';
export default {
components: {
DiffFileHeader,
DiffContent,
+ GlButton,
GlLoadingIcon,
},
+ directives: {
+ SafeHtml,
+ },
mixins: [glFeatureFlagsMixin()],
+ i18n: {
+ genericError: GENERIC_ERROR,
+ ...DIFF_FILE,
+ },
props: {
file: {
type: Object,
@@ -50,7 +59,7 @@ export default {
...mapGetters('diffs', ['getDiffFileDiscussions']),
viewBlobLink() {
return sprintf(
- __('You can %{linkStart}view the blob%{linkEnd} instead.'),
+ this.$options.i18n.blobView,
{
linkStart: `<a href="${escape(this.file.view_path)}">`,
linkEnd: '</a>',
@@ -72,9 +81,7 @@ export default {
},
forkMessage() {
return sprintf(
- __(
- "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
- ),
+ this.$options.i18n.editInFork,
{
tag_start: '<span class="js-file-fork-suggestion-section-action">',
tag_end: '</span>',
@@ -93,11 +100,7 @@ export default {
},
'file.file_hash': {
handler: function watchFileHash() {
- if (
- this.glFeatures.autoExpandCollapsedDiffs &&
- this.viewDiffsFileByFile &&
- this.file.viewer.collapsed
- ) {
+ if (this.viewDiffsFileByFile && this.file.viewer.collapsed) {
this.isCollapsed = false;
this.handleLoadCollapsedDiff();
} else {
@@ -107,7 +110,7 @@ export default {
immediate: true,
},
'file.viewer.collapsed': function setIsCollapsed(newVal) {
- if (!this.viewDiffsFileByFile && !this.glFeatures.autoExpandCollapsedDiffs) {
+ if (!this.viewDiffsFileByFile) {
this.isCollapsed = newVal;
}
},
@@ -149,7 +152,7 @@ export default {
})
.catch(() => {
this.isLoadingCollapsedDiff = false;
- createFlash(__('Something went wrong on our end. Please try again!'));
+ createFlash(this.$options.i18n.genericError);
});
},
showForkMessage() {
@@ -185,32 +188,38 @@ export default {
/>
<div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion">
- <span class="file-fork-suggestion-note" v-html="forkMessage"></span>
+ <span v-safe-html="forkMessage" class="file-fork-suggestion-note"></span>
<a
:href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
- >{{ __('Fork') }}</a
+ >{{ $options.i18n.fork }}</a
>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button"
@click="hideForkMessage"
>
- {{ __('Cancel') }}
+ {{ $options.i18n.cancel }}
</button>
</div>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
<template v-else>
<div :id="`diff-content-${file.file_hash}`">
<div v-if="errorMessage" class="diff-viewer">
- <div class="nothing-here-block" v-html="errorMessage"></div>
+ <div v-safe-html="errorMessage" class="nothing-here-block"></div>
</div>
<template v-else>
- <div v-show="isCollapsed" class="nothing-here-block diff-collapsed">
- {{ __('This diff is collapsed.') }}
- <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
- __('Click to expand it.')
- }}</a>
+ <div v-show="isCollapsed" class="gl-p-7 gl-text-center collapsed-file-warning">
+ <p class="gl-mb-8 gl-mt-5">
+ {{ $options.i18n.collapsed }}
+ </p>
+ <gl-button
+ class="gl-alert-action gl-mb-5"
+ data-testid="expandButton"
+ @click="handleToggle"
+ >
+ {{ $options.i18n.expand }}
+ </gl-button>
</div>
<diff-content
v-show="!isCollapsed && !isFileTooLarge"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 5727fbaaf68..fded391cc84 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,9 +1,16 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
-import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlDeprecatedButton,
+ GlTooltipDirective,
+ GlSafeHtmlDirective,
+ GlLoadingIcon,
+ GlIcon,
+ GlButton,
+} from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
@@ -18,12 +25,14 @@ export default {
GlDeprecatedButton,
ClipboardButton,
EditButton,
- Icon,
+ GlIcon,
FileIcon,
DiffStats,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
props: {
discussionPath: {
@@ -77,6 +86,21 @@ export default {
return this.discussionPath;
},
+ submoduleDiffCompareLinkText() {
+ if (this.diffFile.submodule_compare) {
+ const truncatedOldSha = escape(truncateSha(this.diffFile.submodule_compare.old_sha));
+ const truncatedNewSha = escape(truncateSha(this.diffFile.submodule_compare.new_sha));
+ return sprintf(
+ s__('Compare %{oldCommitId}...%{newCommitId}'),
+ {
+ oldCommitId: `<span class="commit-sha">${truncatedOldSha}</span>`,
+ newCommitId: `<span class="commit-sha">${truncatedNewSha}</span>`,
+ },
+ false,
+ );
+ }
+ return null;
+ },
filePath() {
if (this.diffFile.submodule) {
return `${this.diffFile.file_path} @ ${truncateSha(this.diffFile.blob.id)}`;
@@ -133,6 +157,7 @@ export default {
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
+ 'toggleActiveFileByHash',
]),
handleToggleFile() {
this.$emit('toggleFile');
@@ -149,6 +174,9 @@ export default {
const selector = this.diffContentIDSelector;
scrollToElement(document.querySelector(selector));
window.location.hash = selector;
+ if (!this.viewDiffsFileByFile) {
+ this.toggleActiveFileByHash(this.diffFile.file_hash);
+ }
}
},
},
@@ -162,7 +190,7 @@ export default {
@click.self="handleToggleFile"
>
<div class="file-header-content">
- <icon
+ <gl-icon
v-if="collapsible"
ref="collapseIcon"
:name="collapseIcon"
@@ -237,7 +265,7 @@ export default {
type="button"
@click="toggleFileDiscussionWrappers(diffFile)"
>
- <icon name="comment" />
+ <gl-icon name="comment" />
</gl-deprecated-button>
</span>
@@ -273,8 +301,8 @@ export default {
@click="toggleFullDiff(diffFile.file_path)"
>
<gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline />
- <icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" />
- <icon v-else name="doc-expand" />
+ <gl-icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" />
+ <gl-icon v-else name="doc-expand" />
</gl-deprecated-button>
<gl-deprecated-button
ref="viewButton"
@@ -287,7 +315,7 @@ export default {
data-track-property="diff_toggle_view_sha"
:title="viewFileButtonText"
>
- <icon name="doc-text" />
+ <gl-icon name="doc-text" />
</gl-deprecated-button>
<a
@@ -303,9 +331,22 @@ export default {
data-track-property="diff_toggle_external"
class="btn btn-file-option"
>
- <icon name="external-link" />
+ <gl-icon name="external-link" />
</a>
</div>
</div>
+
+ <div
+ v-if="diffFile.submodule_compare"
+ class="file-actions d-none d-sm-flex align-items-center flex-wrap"
+ >
+ <gl-button
+ v-gl-tooltip.hover
+ v-safe-html="submoduleDiffCompareLinkText"
+ class="submodule-compare"
+ :title="s__('Compare submodule commit revisions')"
+ :href="diffFile.submodule_compare.url"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file_row.vue b/app/assets/javascripts/diffs/components/diff_file_row.vue
index 43b669625f4..2856e6ae8eb 100644
--- a/app/assets/javascripts/diffs/components/diff_file_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_row.vue
@@ -3,9 +3,10 @@
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
*/
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import FileRow from '~/vue_shared/components/file_row.vue';
-import FileRowStats from './file_row_stats.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
+import FileRowStats from './file_row_stats.vue';
export default {
name: 'DiffFileRow',
@@ -14,6 +15,7 @@ export default {
FileRowStats,
ChangedFileIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
file: {
type: Object,
@@ -28,11 +30,28 @@ export default {
required: false,
default: null,
},
+ viewedFiles: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
showFileRowStats() {
return !this.hideFileStats && this.file.type === 'blob';
},
+ fileClasses() {
+ if (!this.glFeatures.highlightCurrentDiffRow) {
+ return '';
+ }
+
+ return this.file.type === 'blob' && !this.viewedFiles[this.file.fileHash]
+ ? 'gl-font-weight-bold'
+ : '';
+ },
+ isActive() {
+ return this.currentDiffFileId === this.file.fileHash;
+ },
},
};
</script>
@@ -41,8 +60,9 @@ export default {
<file-row
:file="file"
v-bind="$attrs"
- :class="{ 'is-active': currentDiffFileId === file.fileHash }"
+ :class="{ 'is-active': isActive }"
class="diff-file-row"
+ :file-classes="fileClasses"
v-on="$listeners"
>
<file-row-stats v-if="showFileRowStats" :file="file" class="mr-1" />
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index be19d8520b5..439319f487c 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -1,14 +1,13 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import { truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
export default {
components: {
- Icon,
+ GlIcon,
UserAvatarImage,
},
directives: {
@@ -68,7 +67,7 @@ export default {
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
@click="$emit('toggleLineDiscussions')"
>
- <icon :size="12" name="collapse" />
+ <gl-icon :size="12" name="collapse" />
</button>
<template v-else>
<user-avatar-image
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
index 439d8097e56..05fbbd39fae 100644
--- a/app/assets/javascripts/diffs/components/diff_stats.vue
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -1,10 +1,10 @@
<script>
import { isNumber } from 'lodash';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
export default {
- components: { Icon },
+ components: { GlIcon },
props: {
addedLines: {
type: Number,
@@ -46,7 +46,7 @@ export default {
}"
>
<div v-if="hasDiffFiles" class="diff-stats-group">
- <icon name="doc-code" class="diff-stats-icon text-secondary" />
+ <gl-icon name="doc-code" class="diff-stats-icon text-secondary" />
<span class="text-secondary bold">{{ diffFilesCountText }} {{ filesText }}</span>
</div>
<div
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
index 21fdb19287d..ff1af5569dc 100644
--- a/app/assets/javascripts/diffs/components/edit_button.vue
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -1,12 +1,11 @@
<script>
-import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
+import { GlTooltipDirective, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlDeprecatedButton,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,7 +58,7 @@ export default {
class="rounded-0 js-edit-blob"
@click.native="handleEditClick"
>
- <icon name="pencil" />
+ <gl-icon name="pencil" />
</gl-deprecated-button>
</span>
</template>
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
index be7e6789216..3956c2fab49 100644
--- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -2,12 +2,12 @@
import { mapActions, mapGetters } from 'vuex';
import { isArray } from 'lodash';
import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
name: 'ImageDiffOverlay',
components: {
- Icon,
+ GlIcon,
},
mixins: [imageDiffMixin],
props: {
@@ -112,7 +112,7 @@ export default {
type="button"
@click="clickedToggle(discussion)"
>
- <icon v-if="showCommentIcon" name="image-comment-dark" />
+ <gl-icon v-if="showCommentIcon" name="image-comment-dark" />
<template v-else>
{{ toggleText(discussion, index) }}
</template>
@@ -127,7 +127,7 @@ export default {
class="btn-transparent comment-indicator"
type="button"
>
- <icon name="image-comment-dark" />
+ <gl-icon name="image-comment-dark" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 168e8c6c14e..7fab750089e 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -1,7 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlTooltipDirective } from '@gitlab/ui';
-import DiffTableCell from './diff_table_cell.vue';
+import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import {
MATCH_LINE_TYPE,
NEW_LINE_TYPE,
@@ -10,14 +9,23 @@ import {
CONTEXT_LINE_CLASS_NAME,
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
+ LINE_HOVER_CLASS_NAME,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ EMPTY_CELL_TYPE,
} from '../constants';
+import { __ } from '~/locale';
+import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import DiffGutterAvatars from './diff_gutter_avatars.vue';
export default {
components: {
- DiffTableCell,
+ DiffGutterAvatars,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
props: {
fileHash: {
@@ -49,6 +57,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['isLoggedIn']),
...mapGetters('diffs', ['fileLineCoverage']),
...mapState({
isHighlighted(state) {
@@ -78,6 +87,70 @@ export default {
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.new_line);
},
+ isMetaLine() {
+ const { type } = this.line;
+
+ return (
+ type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
+ );
+ },
+ classNameMapCell() {
+ const { type } = this.line;
+
+ return [
+ type,
+ {
+ hll: this.isHighlighted,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && this.isHover && !this.isContextLine && !this.isMetaLine,
+ },
+ ];
+ },
+ addCommentTooltip() {
+ const brokenSymlinks = this.line.commentsDisabled;
+ let tooltip = __('Add a comment to this line');
+
+ if (brokenSymlinks) {
+ if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
+ tooltip = __(
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
+ );
+ } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
+ tooltip = __(
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
+ );
+ }
+ }
+
+ return tooltip;
+ },
+ shouldRenderCommentButton() {
+ if (this.isLoggedIn) {
+ const isDiffHead = parseBoolean(getParameterByName('diff_head'));
+ return !isDiffHead || gon.features?.mergeRefHeadComments;
+ }
+
+ return false;
+ },
+ shouldShowCommentButton() {
+ return this.isHover && !this.isContextLine && !this.isMetaLine && !this.hasDiscussions;
+ },
+ hasDiscussions() {
+ return this.line.discussions && this.line.discussions.length > 0;
+ },
+ lineHref() {
+ return `#${this.line.line_code || ''}`;
+ },
+ lineCode() {
+ return (
+ this.line.line_code ||
+ (this.line.left && this.line.left.line_code) ||
+ (this.line.right && this.line.right.line_code)
+ );
+ },
+ shouldShowAvatarsOnGutter() {
+ return this.hasDiscussions;
+ },
},
created() {
this.newLineType = NEW_LINE_TYPE;
@@ -89,12 +162,20 @@ export default {
this.scrollToLineIfNeededInline(this.line);
},
methods: {
- ...mapActions('diffs', ['scrollToLineIfNeededInline']),
+ ...mapActions('diffs', [
+ 'scrollToLineIfNeededInline',
+ 'showCommentForm',
+ 'setHighlightedRow',
+ 'toggleLineDiscussions',
+ ]),
handleMouseMove(e) {
// To show the comment icon on the gutter we need to know if we hover the line.
// Current table structure doesn't allow us to do this with CSS in both of the diff view types
this.isHover = e.type === 'mouseover';
},
+ handleCommentButton() {
+ this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
+ },
},
};
</script>
@@ -108,25 +189,52 @@ export default {
@mouseover="handleMouseMove"
@mouseout="handleMouseMove"
>
- <diff-table-cell
- :file-hash="fileHash"
- :line="line"
- :line-type="oldLineType"
- :is-bottom="isBottom"
- :is-hover="isHover"
- :show-comment-button="true"
- :is-highlighted="isHighlighted"
- class="diff-line-num old_line"
- />
- <diff-table-cell
- :file-hash="fileHash"
- :line="line"
- :line-type="newLineType"
- :is-bottom="isBottom"
- :is-hover="isHover"
- :is-highlighted="isHighlighted"
- class="diff-line-num new_line qa-new-diff-line"
- />
+ <td ref="oldTd" class="diff-line-num old_line" :class="classNameMapCell">
+ <span
+ v-if="shouldRenderCommentButton"
+ ref="addNoteTooltip"
+ v-gl-tooltip
+ class="add-diff-note tooltip-wrapper"
+ :title="addCommentTooltip"
+ >
+ <button
+ v-show="shouldShowCommentButton"
+ ref="addDiffNoteButton"
+ type="button"
+ class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ :disabled="line.commentsDisabled"
+ @click="handleCommentButton"
+ >
+ <gl-icon :size="12" name="comment" />
+ </button>
+ </span>
+ <a
+ v-if="line.old_line"
+ ref="lineNumberRefOld"
+ :data-linenumber="line.old_line"
+ :href="lineHref"
+ @click="setHighlightedRow(lineCode)"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="shouldShowAvatarsOnGutter"
+ :discussions="line.discussions"
+ :discussions-expanded="line.discussionsExpanded"
+ @toggleLineDiscussions="
+ toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
+ "
+ />
+ </td>
+ <td ref="newTd" class="diff-line-num new_line qa-new-diff-line" :class="classNameMapCell">
+ <a
+ v-if="line.new_line"
+ ref="lineNumberRefNew"
+ :data-linenumber="line.new_line"
+ :href="lineHref"
+ @click="setHighlightedRow(lineCode)"
+ >
+ </a>
+ </td>
<td
v-gl-tooltip.hover
:title="coverageState.text"
@@ -134,6 +242,7 @@ export default {
class="line-coverage"
></td>
<td
+ v-safe-html="line.rich_text"
:class="[
line.type,
{
@@ -141,7 +250,6 @@ export default {
},
]"
class="line_content with-coverage"
- v-html="line.rich_text"
></td>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index e82d06ee385..13805910648 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters, mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import InlineDraftCommentRow from '~/batch_comments/components/inline_draft_comment_row.vue';
import inlineDiffTableRow from './inline_diff_table_row.vue';
@@ -14,7 +15,7 @@ export default {
InlineDraftCommentRow,
inlineDiffExpansionRow,
},
- mixins: [draftCommentsMixin],
+ mixins: [draftCommentsMixin, glFeatureFlagsMixin()],
props: {
diffFile: {
type: Object,
diff --git a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
new file mode 100644
index 00000000000..e47bea8e589
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
@@ -0,0 +1,72 @@
+<script>
+import { GlButton, GlAlert } from '@gitlab/ui';
+import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ },
+ props: {
+ limited: {
+ type: Boolean,
+ required: true,
+ },
+ mergeable: {
+ type: Boolean,
+ required: true,
+ },
+ resolutionPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ containerClasses() {
+ return {
+ [CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="containerClasses">
+ <gl-alert
+ :dismissible="false"
+ :title="__('There are merge conflicts')"
+ variant="warning"
+ class="gl-mb-5"
+ >
+ <p class="gl-mb-2">
+ {{ __('The comparison view may be inaccurate due to merge conflicts.') }}
+ </p>
+ <p class="gl-mb-0">
+ {{
+ __(
+ 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.',
+ )
+ }}
+ </p>
+ <template #actions>
+ <gl-button
+ v-if="resolutionPath"
+ :href="resolutionPath"
+ variant="info"
+ class="gl-mr-5 gl-alert-action"
+ >
+ {{ __('Resolve conflicts') }}
+ </gl-button>
+ <gl-button
+ v-if="mergeable"
+ class="gl-alert-action"
+ data-toggle="modal"
+ data-target="#modal_merge_info"
+ >
+ {{ __('Merge locally') }}
+ </gl-button>
+ </template>
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue
index 93afa978862..a640dcb0a90 100644
--- a/app/assets/javascripts/diffs/components/no_changes.vue
+++ b/app/assets/javascripts/diffs/components/no_changes.vue
@@ -1,12 +1,11 @@
<script>
import { mapGetters } from 'vuex';
-import { escape } from 'lodash';
-import { GlButton } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
+import { GlButton, GlSprintf } from '@gitlab/ui';
export default {
components: {
GlButton,
+ GlSprintf,
},
props: {
changesEmptyStateIllustration: {
@@ -16,20 +15,6 @@ export default {
},
computed: {
...mapGetters(['getNoteableData']),
- emptyStateText() {
- return sprintf(
- __(
- 'No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}',
- ),
- {
- ref_start: '<span class="ref-name">',
- ref_end: '</span>',
- source_branch: escape(this.getNoteableData.source_branch),
- target_branch: escape(this.getNoteableData.target_branch),
- },
- false,
- );
- },
},
};
</script>
@@ -41,7 +26,14 @@ export default {
</div>
<div class="col-12">
<div class="text-content text-center">
- <span v-html="emptyStateText"></span>
+ <gl-sprintf :message="__('No changes between %{sourceBranch} and %{targetBranch}')">
+ <template #sourceBranch>
+ <span class="ref-name">{{ getNoteableData.source_branch }}</span>
+ </template>
+ <template #targetBranch>
+ <span class="ref-name">{{ getNoteableData.target_branch }}</span>
+ </template>
+ </gl-sprintf>
<div class="text-center">
<gl-button :href="getNoteableData.new_blob_path" variant="success" category="primary">{{
__('Create commit')
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index ccb32a2a745..0bf47dc77a6 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -1,8 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import $ from 'jquery';
-import { GlTooltipDirective } from '@gitlab/ui';
-import DiffTableCell from './diff_table_cell.vue';
+import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import {
MATCH_LINE_TYPE,
NEW_LINE_TYPE,
@@ -13,14 +12,20 @@ import {
PARALLEL_DIFF_VIEW_TYPE,
NEW_NO_NEW_LINE_TYPE,
EMPTY_CELL_TYPE,
+ LINE_HOVER_CLASS_NAME,
} from '../constants';
+import { __ } from '~/locale';
+import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import DiffGutterAvatars from './diff_gutter_avatars.vue';
export default {
components: {
- DiffTableCell,
+ GlIcon,
+ DiffGutterAvatars,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
props: {
fileHash: {
@@ -50,10 +55,12 @@ export default {
return {
isLeftHover: false,
isRightHover: false,
+ isCommentButtonRendered: false,
};
},
computed: {
...mapGetters('diffs', ['fileLineCoverage']),
+ ...mapGetters(['isLoggedIn']),
...mapState({
isHighlighted(state) {
if (this.isCommented) return true;
@@ -65,12 +72,15 @@ export default {
return lineCode ? lineCode === state.diffs.highlightedRow : false;
},
}),
- isContextLine() {
+ isContextLineLeft() {
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
},
+ isContextLineRight() {
+ return this.line.right && this.line.right.type === CONTEXT_LINE_TYPE;
+ },
classNameMap() {
return {
- [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
+ [CONTEXT_LINE_CLASS_NAME]: this.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: true,
};
},
@@ -97,6 +107,129 @@ export default {
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
+ classNameMapCellLeft() {
+ const { type } = this.line.left;
+
+ return [
+ type,
+ {
+ hll: this.isHighlighted,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && this.isLeftHover && !this.isContextLineLeft && !this.isMetaLineLeft,
+ },
+ ];
+ },
+ classNameMapCellRight() {
+ const { type } = this.line.right;
+
+ return [
+ type,
+ {
+ hll: this.isHighlighted,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn &&
+ this.isRightHover &&
+ !this.isContextLineRight &&
+ !this.isMetaLineRight,
+ },
+ ];
+ },
+ addCommentTooltipLeft() {
+ const brokenSymlinks = this.line.left.commentsDisabled;
+ let tooltip = __('Add a comment to this line');
+
+ if (brokenSymlinks) {
+ if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
+ tooltip = __(
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
+ );
+ } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
+ tooltip = __(
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
+ );
+ }
+ }
+
+ return tooltip;
+ },
+ addCommentTooltipRight() {
+ const brokenSymlinks = this.line.right.commentsDisabled;
+ let tooltip = __('Add a comment to this line');
+
+ if (brokenSymlinks) {
+ if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
+ tooltip = __(
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
+ );
+ } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
+ tooltip = __(
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
+ );
+ }
+ }
+
+ return tooltip;
+ },
+ shouldRenderCommentButton() {
+ if (!this.isCommentButtonRendered) {
+ return false;
+ }
+
+ if (this.isLoggedIn) {
+ const isDiffHead = parseBoolean(getParameterByName('diff_head'));
+ return !isDiffHead || gon.features?.mergeRefHeadComments;
+ }
+
+ return false;
+ },
+ shouldShowCommentButtonLeft() {
+ return (
+ this.isLeftHover &&
+ !this.isContextLineLeft &&
+ !this.isMetaLineLeft &&
+ !this.hasDiscussionsLeft
+ );
+ },
+ shouldShowCommentButtonRight() {
+ return (
+ this.isRightHover &&
+ !this.isContextLineRight &&
+ !this.isMetaLineRight &&
+ !this.hasDiscussionsRight
+ );
+ },
+ hasDiscussionsLeft() {
+ return this.line.left?.discussions?.length > 0;
+ },
+ hasDiscussionsRight() {
+ return this.line.right?.discussions?.length > 0;
+ },
+ lineHrefOld() {
+ return `#${this.line.left.line_code || ''}`;
+ },
+ lineHrefNew() {
+ return `#${this.line.right.line_code || ''}`;
+ },
+ lineCode() {
+ return (
+ (this.line.left && this.line.left.line_code) ||
+ (this.line.right && this.line.right.line_code)
+ );
+ },
+ isMetaLineLeft() {
+ const type = this.line.left?.type;
+
+ return (
+ type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
+ );
+ },
+ isMetaLineRight() {
+ const type = this.line.right?.type;
+
+ return (
+ type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
+ );
+ },
},
created() {
this.newLineType = NEW_LINE_TYPE;
@@ -105,9 +238,26 @@ export default {
},
mounted() {
this.scrollToLineIfNeededParallel(this.line);
+ this.unwatchShouldShowCommentButton = this.$watch(
+ vm => [vm.shouldShowCommentButtonLeft, vm.shouldShowCommentButtonRight].join(),
+ newVal => {
+ if (newVal) {
+ this.isCommentButtonRendered = true;
+ this.unwatchShouldShowCommentButton();
+ }
+ },
+ );
+ },
+ beforeDestroy() {
+ this.unwatchShouldShowCommentButton();
},
methods: {
- ...mapActions('diffs', ['scrollToLineIfNeededParallel']),
+ ...mapActions('diffs', [
+ 'scrollToLineIfNeededParallel',
+ 'showCommentForm',
+ 'setHighlightedRow',
+ 'toggleLineDiscussions',
+ ]),
handleMouseMove(e) {
const isHover = e.type === 'mouseover';
const hoveringCell = e.target.closest('td');
@@ -133,6 +283,9 @@ export default {
table.addClass(`${lineClass}-selected`);
}
},
+ handleCommentButton(line) {
+ this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash });
+ },
},
};
</script>
@@ -145,25 +298,53 @@ export default {
@mouseout="handleMouseMove"
>
<template v-if="line.left && !isMatchLineLeft">
- <diff-table-cell
- :file-hash="fileHash"
- :line="line.left"
- :line-type="oldLineType"
- :is-bottom="isBottom"
- :is-hover="isLeftHover"
- :is-highlighted="isHighlighted"
- :show-comment-button="true"
- :diff-view-type="parallelDiffViewType"
- line-position="left"
- class="diff-line-num old_line"
- />
+ <td ref="oldTd" :class="classNameMapCellLeft" class="diff-line-num old_line">
+ <span
+ v-if="shouldRenderCommentButton"
+ ref="addNoteTooltipLeft"
+ v-gl-tooltip
+ class="add-diff-note tooltip-wrapper"
+ :title="addCommentTooltipLeft"
+ >
+ <button
+ v-show="shouldShowCommentButtonLeft"
+ ref="addDiffNoteButtonLeft"
+ type="button"
+ class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ :disabled="line.left.commentsDisabled"
+ @click="handleCommentButton(line.left)"
+ >
+ <gl-icon :size="12" name="comment" />
+ </button>
+ </span>
+ <a
+ v-if="line.left.old_line"
+ ref="lineNumberRefOld"
+ :data-linenumber="line.left.old_line"
+ :href="lineHrefOld"
+ @click="setHighlightedRow(lineCode)"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="hasDiscussionsLeft"
+ :discussions="line.left.discussions"
+ :discussions-expanded="line.left.discussionsExpanded"
+ @toggleLineDiscussions="
+ toggleLineDiscussions({
+ lineCode: line.left.line_code,
+ fileHash,
+ expanded: !line.left.discussionsExpanded,
+ })
+ "
+ />
+ </td>
<td :class="parallelViewLeftLineType" class="line-coverage left-side"></td>
<td
:id="line.left.line_code"
+ v-safe-html="line.left.rich_text"
:class="parallelViewLeftLineType"
class="line_content with-coverage parallel left-side"
@mousedown="handleParallelLineMouseDown"
- v-html="line.left.rich_text"
></td>
</template>
<template v-else>
@@ -172,18 +353,46 @@ export default {
<td class="line_content with-coverage parallel left-side empty-cell"></td>
</template>
<template v-if="line.right && !isMatchLineRight">
- <diff-table-cell
- :file-hash="fileHash"
- :line="line.right"
- :line-type="newLineType"
- :is-bottom="isBottom"
- :is-hover="isRightHover"
- :is-highlighted="isHighlighted"
- :show-comment-button="true"
- :diff-view-type="parallelDiffViewType"
- line-position="right"
- class="diff-line-num new_line"
- />
+ <td ref="newTd" :class="classNameMapCellRight" class="diff-line-num new_line">
+ <span
+ v-if="shouldRenderCommentButton"
+ ref="addNoteTooltipRight"
+ v-gl-tooltip
+ class="add-diff-note tooltip-wrapper"
+ :title="addCommentTooltipRight"
+ >
+ <button
+ v-show="shouldShowCommentButtonRight"
+ ref="addDiffNoteButtonRight"
+ type="button"
+ class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ :disabled="line.right.commentsDisabled"
+ @click="handleCommentButton(line.right)"
+ >
+ <gl-icon :size="12" name="comment" />
+ </button>
+ </span>
+ <a
+ v-if="line.right.new_line"
+ ref="lineNumberRefNew"
+ :data-linenumber="line.right.new_line"
+ :href="lineHrefNew"
+ @click="setHighlightedRow(lineCode)"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="hasDiscussionsRight"
+ :discussions="line.right.discussions"
+ :discussions-expanded="line.right.discussionsExpanded"
+ @toggleLineDiscussions="
+ toggleLineDiscussions({
+ lineCode: line.right.line_code,
+ fileHash,
+ expanded: !line.right.discussionsExpanded,
+ })
+ "
+ />
+ </td>
<td
v-gl-tooltip.hover
:title="coverageState.text"
@@ -192,6 +401,7 @@ export default {
></td>
<td
:id="line.right.line_code"
+ v-safe-html="line.right.rich_text"
:class="[
line.right.type,
{
@@ -200,7 +410,6 @@ export default {
]"
class="line_content with-coverage parallel right-side"
@mousedown="handleParallelLineMouseDown"
- v-html="line.right.rich_text"
></td>
</template>
<template v-else>
diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue
index c45de481a17..78647065c8e 100644
--- a/app/assets/javascripts/diffs/components/settings_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue
@@ -1,17 +1,24 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlDeprecatedButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButtonGroup, GlButton, GlDropdown } from '@gitlab/ui';
+import { __ } from '~/locale';
export default {
components: {
- GlDeprecatedButton,
- Icon,
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
},
computed: {
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
...mapState('diffs', ['renderTreeList', 'showWhitespace']),
},
+ mounted() {
+ this.patchAriaLabel();
+ },
+ updated() {
+ this.patchAriaLabel();
+ },
methods: {
...mapActions('diffs', [
'setInlineDiffViewType',
@@ -19,74 +26,69 @@ export default {
'setRenderTreeList',
'setShowWhitespace',
]),
+ patchAriaLabel() {
+ this.$el
+ .querySelector('.js-show-diff-settings')
+ .setAttribute('aria-label', __('Diff view settings'));
+ },
},
};
</script>
<template>
- <div class="dropdown">
- <button
- type="button"
- class="btn btn-default js-show-diff-settings"
- data-toggle="dropdown"
- data-display="static"
- >
- <icon name="settings" /> <icon name="chevron-down" />
- </button>
- <div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3">
- <div>
- <span class="bold d-block mb-1">{{ __('File browser') }}</span>
- <div class="btn-group d-flex">
- <gl-deprecated-button
- :class="{ active: !renderTreeList }"
- class="w-100 js-list-view"
- @click="setRenderTreeList(false)"
- >
- {{ __('List view') }}
- </gl-deprecated-button>
- <gl-deprecated-button
- :class="{ active: renderTreeList }"
- class="w-100 js-tree-view"
- @click="setRenderTreeList(true)"
- >
- {{ __('Tree view') }}
- </gl-deprecated-button>
- </div>
- </div>
- <div class="mt-2">
- <span class="bold d-block mb-1">{{ __('Compare changes') }}</span>
- <div class="btn-group d-flex js-diff-view-buttons">
- <gl-deprecated-button
- id="inline-diff-btn"
- :class="{ active: isInlineView }"
- class="w-100 js-inline-diff-button"
- data-view-type="inline"
- @click="setInlineDiffViewType"
- >
- {{ __('Inline') }}
- </gl-deprecated-button>
- <gl-deprecated-button
- id="parallel-diff-btn"
- :class="{ active: isParallelView }"
- class="w-100 js-parallel-diff-button"
- data-view-type="parallel"
- @click="setParallelDiffViewType"
- >
- {{ __('Side-by-side') }}
- </gl-deprecated-button>
- </div>
- </div>
- <div class="mt-2">
- <label class="mb-0">
- <input
- id="show-whitespace"
- type="checkbox"
- :checked="showWhitespace"
- @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
- />
- {{ __('Show whitespace changes') }}
- </label>
- </div>
+ <gl-dropdown icon="settings" toggle-class="js-show-diff-settings" right>
+ <div class="gl-px-3">
+ <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('File browser') }}</span>
+ <gl-button-group class="gl-display-flex">
+ <gl-button
+ :class="{ selected: !renderTreeList }"
+ class="gl-w-half js-list-view"
+ @click="setRenderTreeList(false)"
+ >
+ {{ __('List view') }}
+ </gl-button>
+ <gl-button
+ :class="{ selected: renderTreeList }"
+ class="gl-w-half js-tree-view"
+ @click="setRenderTreeList(true)"
+ >
+ {{ __('Tree view') }}
+ </gl-button>
+ </gl-button-group>
+ </div>
+ <div class="gl-mt-3 gl-px-3">
+ <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('Compare changes') }}</span>
+ <gl-button-group class="gl-display-flex js-diff-view-buttons">
+ <gl-button
+ id="inline-diff-btn"
+ :class="{ selected: isInlineView }"
+ class="gl-w-half js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </gl-button>
+ <gl-button
+ id="parallel-diff-btn"
+ :class="{ selected: isParallelView }"
+ class="gl-w-half js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </gl-button>
+ </gl-button-group>
+ </div>
+ <div class="gl-mt-3 gl-px-3">
+ <label class="gl-mb-0">
+ <input
+ id="show-whitespace"
+ type="checkbox"
+ :checked="showWhitespace"
+ @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
+ />
+ {{ __('Show whitespace changes') }}
+ </label>
</div>
- </div>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 38fbd8e61d4..d03d450b12d 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -1,8 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import FileTree from '~/vue_shared/components/file_tree.vue';
import DiffFileRow from './diff_file_row.vue';
@@ -11,7 +10,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- Icon,
+ GlIcon,
FileTree,
},
props: {
@@ -26,7 +25,7 @@ export default {
};
},
computed: {
- ...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId']),
+ ...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
const search = this.search.toLowerCase().trim();
@@ -66,7 +65,7 @@ export default {
<div class="tree-list-holder d-flex flex-column">
<div class="gl-mb-3 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
- <icon name="search" class="position-absolute tree-list-icon" />
+ <gl-icon name="search" class="position-absolute tree-list-icon" />
<label for="diff-tree-search" class="sr-only">{{ $options.searchPlaceholder }}</label>
<input
id="diff-tree-search"
@@ -83,7 +82,7 @@ export default {
class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
>
- <icon name="close" />
+ <gl-icon name="close" />
</button>
</div>
</div>
@@ -94,6 +93,7 @@ export default {
:key="file.key"
:file="file"
:level="0"
+ :viewed-files="viewedDiffFileIds"
:hide-file-stats="hideFileStats"
:file-row-component="$options.DiffFileRow"
:current-diff-file-id="currentDiffFileId"
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 447136036ee..dc97d9993da 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -34,7 +34,6 @@ export const COUNT_OF_AVATARS_IN_GUTTER = 3;
export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
-export const MAX_LINES_TO_BE_RENDERED = 2000;
export const DIFF_FILE_SYMLINK_MODE = '120000';
export const DIFF_FILE_DELETED_MODE = '0';
@@ -69,6 +68,11 @@ export const DIFFS_PER_PAGE = 20;
export const DIFF_COMPARE_BASE_VERSION_INDEX = -1;
export const DIFF_COMPARE_HEAD_VERSION_INDEX = -2;
+// Diff View Alerts
+export const ALERT_OVERFLOW_HIDDEN = 'overflow';
+export const ALERT_MERGE_CONFLICT = 'merge-conflict';
+export const ALERT_COLLAPSED_FILES = 'collapsed';
+
// State machine states
export const STATE_IDLING = 'idle';
export const STATE_LOADING = 'loading';
diff --git a/app/assets/javascripts/diffs/diff_file.js b/app/assets/javascripts/diffs/diff_file.js
index 717c4a79ef9..610b71235d9 100644
--- a/app/assets/javascripts/diffs/diff_file.js
+++ b/app/assets/javascripts/diffs/diff_file.js
@@ -18,7 +18,6 @@ function fileSymlinkInformation(file, fileList) {
);
}
-/* eslint-disable-next-line import/prefer-default-export */
export function prepareRawDiffFile({ file, allFiles }) {
Object.assign(file, {
brokenSymlink: fileSymlinkInformation(file, allFiles),
diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js
new file mode 100644
index 00000000000..8b91543587c
--- /dev/null
+++ b/app/assets/javascripts/diffs/i18n.js
@@ -0,0 +1,14 @@
+import { __ } from '~/locale';
+
+export const GENERIC_ERROR = __('Something went wrong on our end. Please try again!');
+
+export const DIFF_FILE = {
+ blobView: __('You can %{linkStart}view the blob%{linkEnd} instead.'),
+ editInFork: __(
+ "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
+ ),
+ fork: __('Fork'),
+ cancel: __('Cancel'),
+ collapsed: __('This file is collapsed.'),
+ expand: __('Expand file'),
+};
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index d5581474c9b..0f275f1cb3e 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -52,7 +52,6 @@ export const setBaseConfig = ({ commit }, options) => {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
} = options;
commit(types.SET_BASE_CONFIG, {
endpoint,
@@ -62,61 +61,18 @@ export const setBaseConfig = ({ commit }, options) => {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
});
};
-export const fetchDiffFiles = ({ state, commit }) => {
- const worker = new TreeWorker();
- const urlParams = {
- w: state.showWhitespace ? '0' : '1',
- };
- let returnData;
-
- if (state.useSingleDiffStyle) {
- urlParams.view = state.diffViewType;
- }
-
- commit(types.SET_LOADING, true);
-
- worker.addEventListener('message', ({ data }) => {
- commit(types.SET_TREE_DATA, data);
-
- worker.terminate();
- });
-
- return axios
- .get(mergeUrlParams(urlParams, state.endpoint))
- .then(res => {
- commit(types.SET_LOADING, false);
-
- commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
- commit(types.SET_DIFF_DATA, res.data);
-
- worker.postMessage(state.diffFiles);
-
- returnData = res.data;
- return Vue.nextTick();
- })
- .then(() => {
- handleLocationHash();
- return returnData;
- })
- .catch(() => worker.terminate());
-};
-
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
const id = window?.location?.hash;
const isNoteLink = id.indexOf('#note') === 0;
const urlParams = {
per_page: DIFFS_PER_PAGE,
w: state.showWhitespace ? '0' : '1',
+ view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
};
- if (state.useSingleDiffStyle) {
- urlParams.view = state.diffViewType;
- }
-
commit(types.SET_BATCH_LOADING, true);
commit(types.SET_RETRIEVING_BATCHES, true);
@@ -128,7 +84,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.SET_BATCH_LOADING, false);
if (!isNoteLink && !state.currentDiffFileId) {
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, diff_files[0].file_hash);
+ commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash);
}
if (isNoteLink) {
@@ -144,7 +100,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
!state.diffFiles.some(f => f.file_hash === state.currentDiffFileId) &&
!isNoteLink
) {
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, state.diffFiles[0].file_hash);
+ commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash);
}
if (gon.features?.codeNavigation) {
@@ -175,11 +131,9 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
export const fetchDiffFilesMeta = ({ commit, state }) => {
const worker = new TreeWorker();
- const urlParams = {};
-
- if (state.useSingleDiffStyle) {
- urlParams.view = state.diffViewType;
- }
+ const urlParams = {
+ view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
+ };
commit(types.SET_LOADING, true);
@@ -229,7 +183,7 @@ export const fetchCoverageFiles = ({ commit, state }) => {
export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+ commit(types.VIEW_DIFF_FILE, fileHash);
};
// This is adding line discussions to the actual lines in the diff tree
@@ -240,10 +194,7 @@ export const assignDiscussionsToDiff = (
) => {
const id = window?.location?.hash;
const isNoteLink = id.indexOf('#note') === 0;
- const diffPositionByLineCode = getDiffPositionByLineCode(
- state.diffFiles,
- state.useSingleDiffStyle,
- );
+ const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
const hash = getLocationHash();
discussions
@@ -477,13 +428,17 @@ export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_FOLDER_OPEN, path);
};
+export const toggleActiveFileByHash = ({ commit }, hash) => {
+ commit(types.VIEW_DIFF_FILE, hash);
+};
+
export const scrollToFile = ({ state, commit }, path) => {
if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path];
document.location.hash = fileHash;
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+ commit(types.VIEW_DIFF_FILE, fileHash);
};
export const toggleShowTreeList = ({ commit, state }, saving = true) => {
@@ -751,7 +706,7 @@ export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, not
const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash;
if (fileHash && state.diffFiles.some(f => f.file_hash === fileHash)) {
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+ commit(types.VIEW_DIFF_FILE, fileHash);
}
};
@@ -759,5 +714,5 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
const fileHash = state.diffFiles[index].file_hash;
document.location.hash = fileHash;
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+ commit(types.VIEW_DIFF_FILE, fileHash);
};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index a24894b8d6b..42df5873a41 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,4 +1,5 @@
import { __, n__ } from '~/locale';
+import { parallelizeDiffLines } from './utils';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
export * from './getters_versions_dropdowns';
@@ -129,3 +130,11 @@ export const fileLineCoverage = state => (file, line) => {
*/
export const currentDiffIndex = state =>
Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
+
+export const diffLines = state => file => {
+ if (state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
+ return null;
+ }
+
+ return parallelizeDiffLines(file.highlighted_diff_lines || []);
+};
diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
index 1e8e736c028..135b1c61ef5 100644
--- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
+++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
@@ -11,17 +11,26 @@ export const diffCompareDropdownTargetVersions = (state, getters) => {
// startVersion only exists if the user has selected a version other
// than "base" so if startVersion is null then base must be selected
- const diffHead = parseBoolean(getParameterByName('diff_head'));
+ const defaultMergeRefForDiffs = window.gon?.features?.defaultMergeRefForDiffs || false;
+ const diffHeadParam = getParameterByName('diff_head');
+ const diffHead = parseBoolean(diffHeadParam) || (!diffHeadParam && defaultMergeRefForDiffs);
const isBaseSelected = !state.startVersion && !diffHead;
const isHeadSelected = !state.startVersion && diffHead;
+ let baseVersion = null;
- const baseVersion = {
- versionName: state.targetBranchName,
- version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
- href: state.mergeRequestDiff.base_version_path,
- isBase: true,
- selected: isBaseSelected,
- };
+ if (
+ !defaultMergeRefForDiffs ||
+ (defaultMergeRefForDiffs && !state.mergeRequestDiff.head_version_path)
+ ) {
+ baseVersion = {
+ versionName: state.targetBranchName,
+ version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
+ href: state.mergeRequestDiff.base_version_path,
+ isBase: true,
+ selected:
+ isBaseSelected || (defaultMergeRefForDiffs && !state.mergeRequestDiff.head_version_path),
+ };
+ }
const headVersion = {
versionName: state.targetBranchName,
@@ -40,7 +49,11 @@ export const diffCompareDropdownTargetVersions = (state, getters) => {
};
};
- return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
+ return [
+ ...state.mergeRequestDiffs.slice(1).map(formatVersion),
+ baseVersion,
+ state.mergeRequestDiff.head_version_path && headVersion,
+ ].filter(a => a);
};
export const diffCompareDropdownSourceVersions = (state, getters) => {
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index d31a600e354..001d9d9f594 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -34,6 +34,7 @@ export default () => ({
showTreeList: true,
currentDiffFileId: '',
projectPath: '',
+ viewedDiffFileIds: {},
commentForms: [],
highlightedRow: null,
renderTreeList: true,
@@ -41,5 +42,4 @@ export default () => ({
fileFinderVisible: false,
dismissEndpoint: '',
showSuggestPopover: true,
- useSingleDiffStyle: false,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 4b1dbc34902..5dba2e9d10d 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -19,7 +19,7 @@ export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
-export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
+export const VIEW_DIFF_FILE = 'VIEW_DIFF_FILE';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 0d41f1c2178..7925c620c4e 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,4 +1,6 @@
+import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { INLINE_DIFF_VIEW_TYPE } from '../constants';
import {
findDiffFile,
addLineReferences,
@@ -24,7 +26,6 @@ export default {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
} = options;
Object.assign(state, {
endpoint,
@@ -34,7 +35,6 @@ export default {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
});
},
@@ -57,10 +57,7 @@ export default {
[types.SET_DIFF_DATA](state, data) {
let files = state.diffFiles;
- if (
- !(gon?.features?.diffsBatchLoad && window.location.search.indexOf('diff_id') === -1) &&
- data.diff_files
- ) {
+ if (window.location.search.indexOf('diff_id') !== -1 && data.diff_files) {
files = prepareDiffData(data, files);
}
@@ -154,7 +151,9 @@ export default {
addContextLines({
inlineLines: diffFile.highlighted_diff_lines,
parallelLines: diffFile.parallel_diff_lines,
- diffViewType: state.diffViewType,
+ diffViewType: window.gon?.features?.unifiedDiffLines
+ ? INLINE_DIFF_VIEW_TYPE
+ : state.diffViewType,
contextLines: lines,
bottom,
lineNumbers,
@@ -249,7 +248,7 @@ export default {
});
}
- if (!file.parallel_diff_lines || !file.highlighted_diff_lines) {
+ if (!file.parallel_diff_lines.length || !file.highlighted_diff_lines.length) {
const newDiscussions = (file.discussions || [])
.filter(d => d.id !== discussion.id)
.concat(discussion);
@@ -293,8 +292,9 @@ export default {
[types.TOGGLE_SHOW_TREE_LIST](state) {
state.showTreeList = !state.showTreeList;
},
- [types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
+ [types.VIEW_DIFF_FILE](state, fileId) {
state.currentDiffFileId = fileId;
+ Vue.set(state.viewedDiffFileIds, fileId, true);
},
[types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
state.commentForms.push({
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index f014cddda32..69330ffae2f 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -11,7 +11,6 @@ import {
OLD_LINE_TYPE,
MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY,
- MAX_LINES_TO_BE_RENDERED,
TREE_TYPE,
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
@@ -20,6 +19,77 @@ import {
} from '../constants';
import { prepareRawDiffFile } from '../diff_file';
+export const isAdded = line => ['new', 'new-nonewline'].includes(line.type);
+export const isRemoved = line => ['old', 'old-nonewline'].includes(line.type);
+export const isUnchanged = line => !line.type;
+export const isMeta = line => ['match', 'new-nonewline', 'old-nonewline'].includes(line.type);
+
+/**
+ * Pass in the inline diff lines array which gets converted
+ * to the parallel diff lines.
+ * This allows for us to convert inline diff lines to parallel
+ * on the frontend without needing to send any requests
+ * to the API.
+ *
+ * This method has been taken from the already existing backend
+ * implementation at lib/gitlab/diff/parallel_diff.rb
+ *
+ * @param {Object[]} diffLines - inline diff lines
+ *
+ * @returns {Object[]} parallel lines
+ */
+export const parallelizeDiffLines = (diffLines = []) => {
+ let freeRightIndex = null;
+ const lines = [];
+
+ for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) {
+ const line = diffLines[i];
+
+ if (isRemoved(line)) {
+ lines.push({
+ [LINE_POSITION_LEFT]: line,
+ [LINE_POSITION_RIGHT]: null,
+ });
+
+ if (freeRightIndex === null) {
+ // Once we come upon a new line it can be put on the right of this old line
+ freeRightIndex = index;
+ }
+ index += 1;
+ } else if (isAdded(line)) {
+ if (freeRightIndex !== null) {
+ // If an old line came before this without a line on the right, this
+ // line can be put to the right of it.
+ lines[freeRightIndex].right = line;
+
+ // If there are any other old lines on the left that don't yet have
+ // a new counterpart on the right, update the free_right_index
+ const nextFreeRightIndex = freeRightIndex + 1;
+ freeRightIndex = nextFreeRightIndex < index ? nextFreeRightIndex : null;
+ } else {
+ lines.push({
+ [LINE_POSITION_LEFT]: null,
+ [LINE_POSITION_RIGHT]: line,
+ });
+
+ freeRightIndex = null;
+ index += 1;
+ }
+ } else if (isMeta(line) || isUnchanged(line)) {
+ // line in the right panel is the same as in the left one
+ lines.push({
+ [LINE_POSITION_LEFT]: line,
+ [LINE_POSITION_RIGHT]: line,
+ });
+
+ freeRightIndex = null;
+ index += 1;
+ }
+ }
+
+ return lines;
+};
+
export function findDiffFile(files, match, matchKey = 'file_hash') {
return files.find(file => file[matchKey] === match);
}
@@ -281,7 +351,7 @@ function mergeTwoFiles(target, source) {
function ensureBasicDiffFileLines(file) {
const missingInline = !file.highlighted_diff_lines;
- const missingParallel = !file.parallel_diff_lines;
+ const missingParallel = !file.parallel_diff_lines || window.gon?.features?.unifiedDiffLines;
Object.assign(file, {
highlighted_diff_lines: missingInline ? [] : file.highlighted_diff_lines,
@@ -379,12 +449,10 @@ function getVisibleDiffLines(file) {
}
function finalizeDiffFile(file) {
- const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
const lines = getVisibleDiffLines(file);
Object.assign(file, {
renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY,
- collapsed: name === diffViewerModes.text && lines > MAX_LINES_TO_BE_RENDERED,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
@@ -417,11 +485,11 @@ export function prepareDiffData(diff, priorFiles = []) {
return deduplicateFilesList([...priorFiles, ...cleanedFiles]);
}
-export function getDiffPositionByLineCode(diffFiles, useSingleDiffStyle) {
+export function getDiffPositionByLineCode(diffFiles) {
let lines = [];
const hasInlineDiffs = diffFiles.some(file => file.highlighted_diff_lines.length > 0);
- if (!useSingleDiffStyle || hasInlineDiffs) {
+ if (hasInlineDiffs) {
// In either of these cases, we can use `highlighted_diff_lines` because
// that will include all of the parallel diff lines, too
diff --git a/app/assets/javascripts/diffs/utils/uuids.js b/app/assets/javascripts/diffs/utils/uuids.js
index 1a529c07ccc..12448350e62 100644
--- a/app/assets/javascripts/diffs/utils/uuids.js
+++ b/app/assets/javascripts/diffs/utils/uuids.js
@@ -11,9 +11,6 @@
* @typedef {String} UUIDv4
*/
-// https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20
-/* eslint-disable import/prefer-default-export */
-
import MersenneTwister from 'mersenne-twister';
import stringHash from 'string-hash';
import { isString } from 'lodash';
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 6ebbeecae1d..5674cc8495d 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -6,6 +6,7 @@ import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import boardsStore from './boards/stores/boards_store';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
@@ -35,7 +36,7 @@ class DueDateSelect {
}
initGlDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
opened: () => {
const calendar = this.$datePicker.data('pikaday');
calendar.show();
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
new file mode 100644
index 00000000000..9ee692e953a
--- /dev/null
+++ b/app/assets/javascripts/editor/constants.js
@@ -0,0 +1,7 @@
+import { __ } from '~/locale';
+
+export const EDITOR_LITE_INSTANCE_ERROR_NO_EL = __(
+ '"el" parameter is required for createInstance()',
+);
+
+export const URI_PREFIX = 'gitlab';
diff --git a/app/assets/javascripts/editor/editor_file_template_ext.js b/app/assets/javascripts/editor/editor_file_template_ext.js
new file mode 100644
index 00000000000..343908b831d
--- /dev/null
+++ b/app/assets/javascripts/editor/editor_file_template_ext.js
@@ -0,0 +1,7 @@
+import { Position } from 'monaco-editor';
+
+export default {
+ navigateFileStart() {
+ this.setPosition(new Position(1, 1));
+ },
+};
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js
index 0af0c3ecdcf..bbd5461ae4d 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/editor_lite.js
@@ -1,18 +1,15 @@
-import { editor as monacoEditor, languages as monacoLanguages, Position, Uri } from 'monaco-editor';
+import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import languages from '~/ide/lib/languages';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { clearDomElement } from './utils';
+import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from './constants';
export default class Editor {
constructor(options = {}) {
- this.editorEl = null;
- this.blobContent = '';
- this.blobPath = '';
- this.instance = null;
- this.model = null;
+ this.instances = [];
this.options = {
extraEditorClassName: 'gl-editor-lite',
...defaultEditorOptions,
@@ -31,6 +28,17 @@ export default class Editor {
monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
}
+ static updateModelLanguage(path, instance) {
+ if (!instance) return;
+ const model = instance.getModel();
+ const ext = `.${path.split('.').pop()}`;
+ const language = monacoLanguages
+ .getLanguages()
+ .find(lang => lang.extensions.indexOf(ext) !== -1);
+ const id = language ? language.id : 'plaintext';
+ monacoEditor.setModelLanguage(model, id);
+ }
+
/**
* Creates a monaco instance with the given options.
*
@@ -40,74 +48,53 @@ export default class Editor {
* @param {string} options.blobContent The content to initialize the monacoEditor.
* @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
*/
- createInstance({ el = undefined, blobPath = '', blobContent = '', blobGlobalId = '' } = {}) {
- if (!el) return;
- this.editorEl = el;
- this.blobContent = blobContent;
- this.blobPath = blobPath;
-
- clearDomElement(this.editorEl);
-
- const uriFilePath = joinPaths('gitlab', blobGlobalId, blobPath);
-
- this.model = monacoEditor.createModel(this.blobContent, undefined, Uri.file(uriFilePath));
-
- monacoEditor.onDidCreateEditor(this.renderEditor.bind(this));
-
- this.instance = monacoEditor.create(this.editorEl, this.options);
- this.instance.setModel(this.model);
- }
-
- dispose() {
- if (this.model) {
- this.model.dispose();
- this.model = null;
+ createInstance({
+ el = undefined,
+ blobPath = '',
+ blobContent = '',
+ blobGlobalId = '',
+ ...instanceOptions
+ } = {}) {
+ if (!el) {
+ throw new Error(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
}
- return this.instance && this.instance.dispose();
- }
+ clearDomElement(el);
- renderEditor() {
- delete this.editorEl.dataset.editorLoading;
- }
+ const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
- onChangeContent(fn) {
- return this.model.onDidChangeContent(fn);
- }
+ const model = monacoEditor.createModel(blobContent, undefined, Uri.file(uriFilePath));
- updateModelLanguage(path) {
- if (path === this.blobPath) return;
- this.blobPath = path;
- const ext = `.${path.split('.').pop()}`;
- const language = monacoLanguages
- .getLanguages()
- .find(lang => lang.extensions.indexOf(ext) !== -1);
- const id = language ? language.id : 'plaintext';
- monacoEditor.setModelLanguage(this.model, id);
- }
+ monacoEditor.onDidCreateEditor(() => {
+ delete el.dataset.editorLoading;
+ });
- getValue() {
- return this.instance.getValue();
- }
-
- setValue(val) {
- this.instance.setValue(val);
- }
+ const instance = monacoEditor.create(el, {
+ ...this.options,
+ ...instanceOptions,
+ });
+ instance.setModel(model);
+ instance.onDidDispose(() => {
+ const index = this.instances.findIndex(inst => inst === instance);
+ this.instances.splice(index, 1);
+ model.dispose();
+ });
+ instance.updateModelLanguage = path => Editor.updateModelLanguage(path, instance);
- focus() {
- this.instance.focus();
+ this.instances.push(instance);
+ return instance;
}
- navigateFileStart() {
- this.instance.setPosition(new Position(1, 1));
- }
-
- updateOptions(options = {}) {
- this.instance.updateOptions(options);
+ dispose() {
+ this.instances.forEach(instance => instance.dispose());
}
- use(exts = []) {
+ use(exts = [], instance = null) {
const extensions = Array.isArray(exts) ? exts : [exts];
- Object.assign(this, ...extensions);
+ if (instance) {
+ Object.assign(instance, ...extensions);
+ } else {
+ this.instances.forEach(inst => Object.assign(inst, ...extensions));
+ }
}
}
diff --git a/app/assets/javascripts/editor/editor_markdown_ext.js b/app/assets/javascripts/editor/editor_markdown_ext.js
index 9d09663e643..c46f5736912 100644
--- a/app/assets/javascripts/editor/editor_markdown_ext.js
+++ b/app/assets/javascripts/editor/editor_markdown_ext.js
@@ -1,7 +1,7 @@
export default {
getSelectedText(selection = this.getSelection()) {
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
- const valArray = this.instance.getValue().split('\n');
+ const valArray = this.getValue().split('\n');
let text = '';
if (startLineNumber === endLineNumber) {
text = valArray[startLineNumber - 1].slice(startColumn - 1, endColumn - 1);
@@ -20,20 +20,16 @@ export default {
return text;
},
- getSelection() {
- return this.instance.getSelection();
- },
-
replaceSelectedText(text, select = undefined) {
const forceMoveMarkers = !select;
- this.instance.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]);
+ this.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]);
},
moveCursor(dx = 0, dy = 0) {
- const pos = this.instance.getPosition();
+ const pos = this.getPosition();
pos.column += dx;
pos.lineNumber += dy;
- this.instance.setPosition(pos);
+ this.setPosition(pos);
},
/**
@@ -94,6 +90,6 @@ export default {
.setStartPosition(newStartLineNumber, newStartColumn)
.setEndPosition(newEndLineNumber, newEndColumn);
- this.instance.setSelection(newSelection);
+ this.setSelection(newSelection);
},
};
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index f0723e96ddf..b2a8571820b 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
/**
* Render modal to confirm rollback/redeploy.
*/
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index d2978422224..035b276bc3b 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,8 +1,7 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -11,7 +10,8 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlButton,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -69,38 +69,37 @@ export default {
</script>
<template>
<div class="btn-group" role="group">
- <button
+ <gl-button
v-tooltip
:title="title"
:aria-label="title"
:disabled="isLoading"
- type="button"
- class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown"
+ class="dropdown dropdown-new js-environment-actions-dropdown"
data-container="body"
data-toggle="dropdown"
>
<span>
- <icon name="play" />
- <icon name="chevron-down" />
+ <gl-icon name="play" />
+ <gl-icon name="chevron-down" />
<gl-loading-icon v-if="isLoading" />
</span>
- </button>
+ </gl-button>
<ul class="dropdown-menu dropdown-menu-right">
- <li v-for="(action, i) in actions" :key="i">
- <button
+ <li v-for="(action, i) in actions" :key="i" class="gl-display-flex">
+ <gl-button
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
- type="button"
- class="js-manual-action-link no-btn btn d-flex align-items-center"
+ variant="link"
+ class="js-manual-action-link gl-flex-fill-1"
@click="onClickAction(action)"
>
- <span class="flex-fill">{{ action.name }}</span>
- <span v-if="action.scheduledAt" class="text-secondary">
- <icon name="clock" />
+ <span class="gl-flex-fill-1">{{ action.name }}</span>
+ <span v-if="action.scheduledAt" class="text-secondary float-right">
+ <gl-icon name="clock" />
{{ remainingTime(action) }}
</span>
- </button>
+ </gl-button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue
index b53c5fa6583..55aaa6d57bd 100644
--- a/app/assets/javascripts/environments/components/environment_delete.vue
+++ b/app/assets/javascripts/environments/components/environment_delete.vue
@@ -5,15 +5,14 @@
*/
import $ from 'jquery';
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default {
components: {
- Icon,
+ GlIcon,
LoadingButton,
},
directives: {
@@ -65,6 +64,6 @@ export default {
data-target="#delete-environment-modal"
@click="onClick"
>
- <icon name="remove" />
+ <gl-icon name="remove" />
</loading-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index fa3d217f148..8850ed19a4b 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,13 +1,12 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { isEmpty } from 'lodash';
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import eventHub from '../event_hub';
import ActionsComponent from './environment_actions.vue';
@@ -30,7 +29,7 @@ export default {
ActionsComponent,
CommitComponent,
ExternalUrlComponent,
- Icon,
+ GlIcon,
MonitoringButtonComponent,
PinComponent,
DeleteComponent,
@@ -535,7 +534,7 @@ export default {
</div>
<span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
- <icon :name="deployIconName" />
+ <gl-icon :name="deployIconName" />
</span>
<span
@@ -560,9 +559,9 @@ export default {
role="button"
@click="onClickFolder"
>
- <icon :name="folderIconName" class="folder-icon" />
+ <gl-icon :name="folderIconName" class="folder-icon" />
- <icon name="folder" class="folder-icon" />
+ <gl-icon name="folder" class="folder-icon" />
<span> {{ model.folderName }} </span>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index bd6feb14319..4dc2c0ec1bd 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -1,15 +1,12 @@
<script>
-import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
-import Icon from '~/vue_shared/components/icon.vue';
-
export default {
components: {
- Icon,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -28,15 +25,14 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
+ <gl-button
v-gl-tooltip
:href="monitoringUrl"
:title="title"
:aria-label="title"
- class="monitoring-url d-none d-sm-none d-md-block"
+ class="monitoring-url gl-display-none gl-display-sm-none gl-display-md-block"
+ icon="chart"
rel="noopener noreferrer nofollow"
variant="default"
- >
- <icon name="chart" />
- </gl-deprecated-button>
+ />
</template>
diff --git a/app/assets/javascripts/environments/components/environment_pin.vue b/app/assets/javascripts/environments/components/environment_pin.vue
index 3a219e98b08..52ac7725bde 100644
--- a/app/assets/javascripts/environments/components/environment_pin.vue
+++ b/app/assets/javascripts/environments/components/environment_pin.vue
@@ -3,15 +3,14 @@
* Renders a prevent auto-stop button.
* Used in environments table.
*/
-import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
- Icon,
- GlDeprecatedButton,
+ GlIcon,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -31,12 +30,7 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
- v-gl-tooltip
- :title="$options.title"
- :aria-label="$options.title"
- @click="onPinClick"
- >
- <icon name="thumbtack" />
- </gl-deprecated-button>
+ <gl-button v-gl-tooltip :title="$options.title" :aria-label="$options.title" @click="onPinClick">
+ <gl-icon name="thumbtack" />
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 7a728961b37..32528e6c6ea 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -5,21 +5,13 @@
*
* Makes a post request when the button is clicked.
*/
-import {
- GlTooltipDirective,
- GlLoadingIcon,
- GlModalDirective,
- GlDeprecatedButton,
-} from '@gitlab/ui';
+import { GlTooltipDirective, GlModalDirective, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
export default {
components: {
- Icon,
- GlLoadingIcon,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -73,15 +65,13 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
+ <gl-button
v-gl-tooltip
v-gl-modal.confirm-rollback-modal
- :disabled="isLoading"
+ class="gl-display-none gl-display-md-block text-secondary"
+ :loading="isLoading"
:title="title"
- class="d-none d-md-block text-secondary"
+ :icon="isLastDeployment ? 'repeat' : 'redo'"
@click="onClick"
- >
- <icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" />
- <gl-loading-icon v-if="isLoading" />
- </gl-deprecated-button>
+ />
</template>
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 37f94f9f5ab..b5a7be90204 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -3,13 +3,12 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -42,6 +41,6 @@ export default {
:class="{ disabled: disabled }"
class="btn terminal-button d-none d-sm-none d-md-block text-secondary"
>
- <icon name="terminal" />
+ <gl-icon name="terminal" />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 1bf705dcda2..c06ab265915 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -14,6 +14,7 @@ export default {
DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'),
CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'),
+ EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'),
},
props: {
environments: {
@@ -111,6 +112,9 @@ export default {
shouldShowCanaryCallout(env) {
return env.showCanaryCallout && this.showCanaryDeploymentCallout;
},
+ shouldRenderAlert(env) {
+ return env?.has_opened_alert;
+ },
sortEnvironments(environments) {
/*
* The sorting algorithm should sort in the following priorities:
@@ -185,6 +189,11 @@ export default {
/>
</div>
</div>
+ <environment-alert
+ v-if="shouldRenderAlert(model)"
+ :key="`alert-row-${i}`"
+ :environment="model"
+ />
<template v-if="shouldRenderFolderContent(model)">
<div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`">
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 7448fd584c6..88612376b6e 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
+/* eslint-disable @gitlab/vue-require-i18n-strings, vue/no-v-html */
import { GlTooltipDirective } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 56896ac4d43..6c547c3713a 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,20 +1,33 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import canaryCalloutMixin from '../mixins/canary_callout_mixin';
import environmentsFolderApp from './environments_folder_view.vue';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
+import createDefaultClient from '~/lib/graphql';
Vue.use(Translate);
+Vue.use(VueApollo);
-export default () =>
- new Vue({
- el: '#environments-folder-list-view',
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const el = document.getElementById('environments-folder-list-view');
+
+ return new Vue({
+ el,
components: {
environmentsFolderApp,
},
mixins: [canaryCalloutMixin],
+ apolloProvider,
+ provide: {
+ projectPath: el.dataset.projectPath,
+ },
data() {
- const environmentsData = document.querySelector(this.$options.el).dataset;
+ const environmentsData = el.dataset;
return {
endpoint: environmentsData.environmentsDataEndpoint,
@@ -35,3 +48,4 @@ export default () =>
});
},
});
+};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index e1e356a977f..16d25615779 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -23,7 +23,8 @@ export default {
},
cssContainerClass: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
canReadEnvironment: {
type: Boolean,
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index 4848cb0f13d..8e8af3f32f7 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -1,20 +1,32 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import canaryCalloutMixin from './mixins/canary_callout_mixin';
import environmentsComponent from './components/environments_app.vue';
import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
+import createDefaultClient from '~/lib/graphql';
Vue.use(Translate);
+Vue.use(VueApollo);
-export default () =>
- new Vue({
- el: '#environments-list-view',
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const el = document.getElementById('environments-list-view');
+ return new Vue({
+ el,
components: {
environmentsComponent,
},
mixins: [canaryCalloutMixin],
+ apolloProvider,
+ provide: {
+ projectPath: el.dataset.projectPath,
+ },
data() {
- const environmentsData = document.querySelector(this.$options.el).dataset;
+ const environmentsData = el.dataset;
return {
endpoint: environmentsData.environmentsDataEndpoint,
@@ -39,3 +51,4 @@ export default () =>
});
},
});
+};
diff --git a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
index 398576a31cb..e9f1a144cb3 100644
--- a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
+++ b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
@@ -2,7 +2,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
export default {
data() {
- const data = document.querySelector(this.$options.el).dataset;
+ const data = this.$options.el.dataset;
return {
canaryDeploymentFeatureId: data.environmentsDataCanaryDeploymentFeatureId,
diff --git a/app/assets/javascripts/environments/stores/helpers.js b/app/assets/javascripts/environments/stores/helpers.js
index 8eba6c00601..eb47ba29412 100644
--- a/app/assets/javascripts/environments/stores/helpers.js
+++ b/app/assets/javascripts/environments/stores/helpers.js
@@ -4,5 +4,4 @@
* @param {Object} environment
* @returns {Object}
*/
-// eslint-disable-next-line import/prefer-default-export
export const setDeployBoard = (oldEnvironmentState, environment) => environment;
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 3d1fdc4f168..5dee3ef3ffe 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -11,10 +11,10 @@ import {
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlDeprecatedDropdownDivider,
+ GlIcon,
} from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, sprintf, n__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
@@ -38,7 +38,7 @@ export default {
GlLink,
GlLoadingIcon,
TooltipOnTruncate,
- Icon,
+ GlIcon,
Stacktrace,
GlBadge,
GlAlert,
@@ -397,7 +397,7 @@ export default {
data-testid="external-url-link"
>
<span class="text-truncate">{{ error.externalUrl }}</span>
- <icon name="external-link" class="ml-1 flex-shrink-0" />
+ <gl-icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link>
</li>
<li v-if="error.firstReleaseVersion">
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index c6825d7af45..a4938fe13ed 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,14 +1,14 @@
<script>
-import { GlTooltip, GlSprintf } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlTooltip, GlSprintf, GlIcon } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
ClipboardButton,
FileIcon,
- Icon,
+ GlIcon,
GlSprintf,
},
directives: {
@@ -80,7 +80,7 @@ export default {
<div ref="header" class="file-title file-title-flex-parent">
<div class="file-header-content d-flex align-content-center">
<div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()">
- <icon :name="collapseIcon" :size="16" aria-hidden="true" class="gl-mr-2" />
+ <gl-icon :name="collapseIcon" :size="16" aria-hidden="true" class="gl-mr-2" />
</div>
<file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" />
<strong
diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js
index 28806b3915c..df5be5224a7 100644
--- a/app/assets/javascripts/error_tracking/store/details/actions.js
+++ b/app/assets/javascripts/error_tracking/store/details/actions.js
@@ -10,7 +10,6 @@ const stopPolling = poll => {
if (poll) poll.stop();
};
-// eslint-disable-next-line import/prefer-default-export
export function startPollingStacktrace({ commit }, endpoint) {
stackTracePoll = new Poll({
resource: service,
diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js
index f2778fbb2c7..a3b31436c81 100644
--- a/app/assets/javascripts/error_tracking/store/details/getters.js
+++ b/app/assets/javascripts/error_tracking/store/details/getters.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const stacktrace = state =>
state.stacktraceData.stack_trace_entries
? state.stacktraceData.stack_trace_entries.reverse()
diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
index 0de67a8bcc7..f1fb1a44758 100644
--- a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
@@ -1,11 +1,10 @@
<script>
import { mapActions, mapState } from 'vuex';
-import { GlFormInput } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlFormInput, GlIcon } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
export default {
- components: { GlFormInput, Icon, LoadingButton },
+ components: { GlFormInput, GlIcon, LoadingButton },
computed: {
...mapState(['apiHost', 'connectError', 'connectSuccessful', 'isLoadingProjects', 'token']),
tokenInputState() {
@@ -64,7 +63,7 @@ export default {
:loading="isLoadingProjects"
@click="fetchProjects"
/>
- <icon
+ <gl-icon
v-show="connectSuccessful"
class="js-error-tracking-connect-success gl-ml-2 text-success align-middle"
:aria-label="__('Projects Successfully Retrieved')"
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index 9bea7aa7b04..d2ac80fa190 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -86,6 +86,11 @@ export const conditions = flattenDeep(
value: __('Any'),
},
{
+ url: 'author_username=support-bot',
+ tokenKey: 'author',
+ value: 'support-bot',
+ },
+ {
url: 'milestone_title=None',
tokenKey: 'milestone',
value: __('None'),
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 7697d97cb2c..f2dd8d5ace5 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -175,4 +175,4 @@ export {
removeFlashClickListener,
FLASH_TYPES,
};
-window.Flash = deprecatedCreateFlash;
+window.Flash = createFlash;
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index c0dadedbc51..1203f389931 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable vue/require-default-prop */
+/* eslint-disable vue/require-default-prop, vue/no-v-html */
import Identicon from '~/vue_shared/components/identicon.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
index 40add09f25d..19cb09f0dcc 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
@@ -1,13 +1,13 @@
<script>
import { debounce } from 'lodash';
import { mapActions } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import eventHub from '../event_hub';
import frequentItemsMixin from './frequent_items_mixin';
export default {
components: {
- Icon,
+ GlIcon,
},
mixins: [frequentItemsMixin],
data() {
@@ -49,6 +49,6 @@ export default {
type="search"
class="form-control"
/>
- <icon v-if="!searchQuery" name="search" class="search-icon" />
+ <gl-icon v-if="!searchQuery" name="search" class="search-icon" />
</div>
</template>
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
index c074f173776..1998bf4358a 100644
--- a/app/assets/javascripts/frequent_items/index.js
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -28,8 +28,8 @@ export default function initFrequentItemDropdowns() {
return;
}
- $(navEl).on('shown.bs.dropdown', () =>
- import('./components/app.vue').then(({ default: FrequentItems }) => {
+ import('./components/app.vue')
+ .then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
@@ -59,9 +59,11 @@ export default function initFrequentItemDropdowns() {
});
},
});
+ })
+ .catch(() => {});
- eventHub.$emit(`${namespace}-dropdownOpen`);
- }),
- );
+ $(navEl).on('shown.bs.dropdown', () => {
+ eventHub.$emit(`${namespace}-dropdownOpen`);
+ });
});
}
diff --git a/app/assets/javascripts/frequent_items/store/getters.js b/app/assets/javascripts/frequent_items/store/getters.js
index 73e66643f06..36cc9020d8d 100644
--- a/app/assets/javascripts/frequent_items/store/getters.js
+++ b/app/assets/javascripts/frequent_items/store/getters.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const hasSearchQuery = state => state.searchQuery !== '';
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 36c586ddfd2..409733c73b9 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import '@gitlab/at.js';
+import '~/lib/utils/jquery_at_who';
import { escape, template } from 'lodash';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import glRegexp from './lib/utils/regexp';
@@ -52,6 +52,7 @@ export const defaultAutocompleteConfig = {
milestones: true,
labels: true,
snippets: true,
+ vulnerabilities: true,
};
class GfmAutoComplete {
@@ -71,12 +72,15 @@ class GfmAutoComplete {
setupLifecycle() {
this.input.each((i, input) => {
const $input = $(input);
- $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
- $input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
- // This triggers at.js again
- // Needed for quick actions with suffixes (ex: /label ~)
- $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
- $input.on('clear-commands-cache.atwho', () => this.clearCache());
+ if (!$input.hasClass('js-gfm-input-initialized')) {
+ $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+ $input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
+ // This triggers at.js again
+ // Needed for quick actions with suffixes (ex: /label ~)
+ $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
+ $input.on('clear-commands-cache.atwho', () => this.clearCache());
+ $input.addClass('js-gfm-input-initialized');
+ }
});
}
@@ -644,7 +648,8 @@ class GfmAutoComplete {
// https://github.com/ichord/At.js
const atSymbolsWithBar = Object.keys(controllers)
.join('|')
- .replace(/[$]/, '\\$&');
+ .replace(/[$]/, '\\$&')
+ .replace(/[+]/, '\\+');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
@@ -675,6 +680,7 @@ GfmAutoComplete.atTypeMap = {
'~': 'labels',
'%': 'milestones',
'/': 'commands',
+ '+': 'vulnerabilities',
$: 'snippets',
};
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index 099c46f4b8d..e0f64c8e843 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -13,7 +13,8 @@ export default class GpgBadges {
const badges = $('.js-loading-gpg-badge');
- badges.html('<i class="fa fa-spinner fa-spin"></i>');
+ badges.html('<span class="gl-spinner gl-spinner-orange gl-spinner-sm"></span>');
+ badges.children().attr('aria-label', __('Loading'));
const displayError = () => createFlash(__('An error occurred while loading commit signatures'));
diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
index 59327e36f5f..79494cb173b 100644
--- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
+++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
@@ -1,7 +1,6 @@
<script>
-import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
@@ -9,7 +8,7 @@ export default {
GlFormCheckbox,
GlFormGroup,
GlFormInput,
- Icon,
+ GlIcon,
},
data() {
return { placeholderUrl: 'https://my-url.grafana.net/' };
@@ -89,7 +88,7 @@ export default {
rel="noopener noreferrer"
>
{{ __('More information') }}
- <icon name="external-link" class="vertical-align-middle" />
+ <gl-icon name="external-link" class="vertical-align-middle" />
</a>
</p>
</gl-form-group>
diff --git a/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql
index 9a2ff1c1648..0855ac2af45 100644
--- a/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql
@@ -1,4 +1,5 @@
fragment Author on User {
+ id
avatarUrl
name
username
diff --git a/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql
new file mode 100644
index 00000000000..286ebbd019e
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql
@@ -0,0 +1,10 @@
+fragment EpicNode on Epic {
+ id
+ iid
+ title
+ state
+ reference
+ webUrl
+ createdAt
+ closedAt
+}
diff --git a/app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql
new file mode 100644
index 00000000000..1a2ea0bda1b
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql
@@ -0,0 +1,7 @@
+fragment Label on Label {
+ id
+ title
+ description
+ color
+ textColor
+}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql
index 4d59b4d94cd..4d59b4d94cd 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql
+++ b/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 0b401f4d732..871f5c9a845 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -2,6 +2,7 @@
/* global Flash */
import $ from 'jquery';
+import 'vendor/jquery.scrollTo';
import { GlLoadingIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index be90ba12678..44349b33386 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { visitUrl } from '../../lib/utils/url_utility';
import tooltip from '../../vue_shared/directives/tooltip';
diff --git a/app/assets/javascripts/groups/components/invite_members_banner.vue b/app/assets/javascripts/groups/components/invite_members_banner.vue
new file mode 100644
index 00000000000..da7adab1d86
--- /dev/null
+++ b/app/assets/javascripts/groups/components/invite_members_banner.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlBanner } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
+import Tracking from '~/tracking';
+
+const trackingMixin = Tracking.mixin();
+
+export default {
+ components: {
+ GlBanner,
+ },
+ mixins: [trackingMixin],
+ inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey', 'trackLabel'],
+ data() {
+ return {
+ isDismissed: parseBoolean(getCookie(this.isDismissedKey)),
+ tracking: {
+ label: this.trackLabel,
+ },
+ };
+ },
+ created() {
+ this.$nextTick(() => {
+ this.addTrackingAttributesToButton();
+ });
+ },
+ mounted() {
+ this.trackOnShow();
+ },
+ methods: {
+ handleClose() {
+ setCookie(this.isDismissedKey, true);
+ this.isDismissed = true;
+ this.track(this.$options.dismissEvent);
+ },
+ trackOnShow() {
+ if (!this.isDismissed) this.track(this.$options.displayEvent);
+ },
+ addTrackingAttributesToButton() {
+ if (this.$refs.banner === undefined) return;
+
+ const button = this.$refs.banner.$el.querySelector(`[href='${this.inviteMembersPath}']`);
+
+ if (button) {
+ button.setAttribute('data-track-event', this.$options.buttonClickEvent);
+ button.setAttribute('data-track-label', this.trackLabel);
+ }
+ },
+ },
+ i18n: {
+ title: s__('InviteMembersBanner|Collaborate with your team'),
+ body: s__(
+ "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge.",
+ ),
+ button_text: s__('InviteMembersBanner|Invite your colleagues'),
+ },
+ displayEvent: 'invite_members_banner_displayed',
+ buttonClickEvent: 'invite_members_banner_button_clicked',
+ dismissEvent: 'invite_members_banner_dismissed',
+};
+</script>
+
+<template>
+ <gl-banner
+ v-if="!isDismissed"
+ ref="banner"
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.button_text"
+ :svg-path="svgPath"
+ :button-link="inviteMembersPath"
+ @close="handleClose"
+ >
+ <p>{{ $options.i18n.body }}</p>
+ </gl-banner>
+</template>
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index ac4c12dda24..5487e25066e 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -1,12 +1,12 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
export default {
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -56,7 +56,7 @@ export default {
class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
@click.prevent="onLeaveGroup"
>
- <icon name="leave" class="position-top-0" />
+ <gl-icon name="leave" class="position-top-0" />
</a>
<a
v-if="group.canEdit"
@@ -68,7 +68,7 @@ export default {
data-placement="bottom"
class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
>
- <icon name="settings" class="position-top-0 align-middle" />
+ <gl-icon name="settings" class="position-top-0 align-middle" />
</a>
</div>
</template>
diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue
index cd3e3de4cb4..e23b0fa7413 100644
--- a/app/assets/javascripts/groups/components/item_caret.vue
+++ b/app/assets/javascripts/groups/components/item_caret.vue
@@ -1,9 +1,9 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
components: {
- icon,
+ GlIcon,
},
props: {
isGroupOpen: {
@@ -21,5 +21,5 @@ export default {
</script>
<template>
- <span class="folder-caret gl-mr-2"> <icon :size="10" :name="iconClass" /> </span>
+ <span class="folder-caret gl-mr-2"> <gl-icon :size="10" :name="iconClass" /> </span>
</template>
diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue
index 27b1c632643..18efd8c6823 100644
--- a/app/assets/javascripts/groups/components/item_stats_value.vue
+++ b/app/assets/javascripts/groups/components/item_stats_value.vue
@@ -1,10 +1,10 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -57,6 +57,6 @@ export default {
:title="title"
data-container="body"
>
- <icon :name="iconName" /> <span v-if="isValuePresent" class="stat-value"> {{ value }} </span>
+ <gl-icon :name="iconName" /> <span v-if="isValuePresent" class="stat-value"> {{ value }} </span>
</span>
</template>
diff --git a/app/assets/javascripts/groups/components/item_type_icon.vue b/app/assets/javascripts/groups/components/item_type_icon.vue
index ae69fbd7bde..c3787c2df21 100644
--- a/app/assets/javascripts/groups/components/item_type_icon.vue
+++ b/app/assets/javascripts/groups/components/item_type_icon.vue
@@ -1,10 +1,10 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import { ITEM_TYPE } from '../constants';
export default {
components: {
- icon,
+ GlIcon,
},
props: {
itemType: {
@@ -29,5 +29,5 @@ export default {
</script>
<template>
- <span class="item-type-icon"> <icon :name="iconClass" /> </span>
+ <span class="item-type-icon"> <gl-icon :name="iconClass" /> </span>
</template>
diff --git a/app/assets/javascripts/groups/init_invite_members_banner.js b/app/assets/javascripts/groups/init_invite_members_banner.js
new file mode 100644
index 00000000000..c7967827917
--- /dev/null
+++ b/app/assets/javascripts/groups/init_invite_members_banner.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
+
+export default function initInviteMembersBanner() {
+ const el = document.querySelector('.js-group-invite-members-banner');
+
+ if (!el) {
+ return false;
+ }
+
+ const { svgPath, inviteMembersPath, isDismissedKey, trackLabel } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ svgPath,
+ inviteMembersPath,
+ isDismissedKey,
+ trackLabel,
+ },
+ render: createElement => createElement(InviteMembersBanner),
+ });
+}
diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/groups/members/components/app.vue
new file mode 100644
index 00000000000..e94b28f5773
--- /dev/null
+++ b/app/assets/javascripts/groups/members/components/app.vue
@@ -0,0 +1,11 @@
+<script>
+export default {
+ name: 'GroupMembersApp',
+};
+</script>
+
+<template>
+ <span>
+ <!-- Temporary empty template -->
+ </span>
+</template>
diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/groups/members/index.js
new file mode 100644
index 00000000000..4ca1756f10c
--- /dev/null
+++ b/app/assets/javascripts/groups/members/index.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import App from './components/app.vue';
+import membersModule from '~/vuex_shared/modules/members';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export default el => {
+ if (!el) {
+ return () => {};
+ }
+
+ Vue.use(Vuex);
+
+ const { members, groupId } = el.dataset;
+
+ const store = new Vuex.Store({
+ ...membersModule({
+ members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
+ sourceId: parseInt(groupId, 10),
+ currentUserId: gon.current_user_id || null,
+ }),
+ });
+
+ return new Vue({
+ el,
+ components: { App },
+ store,
+ render: createElement => createElement('app'),
+ });
+};
diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js
index e94b163dfb1..cefd803c631 100644
--- a/app/assets/javascripts/groups/transfer_dropdown.js
+++ b/app/assets/javascripts/groups/transfer_dropdown.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { __ } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class TransferDropdown {
constructor() {
@@ -16,7 +17,7 @@ export default class TransferDropdown {
buildDropdown() {
const extraOptions = [{ id: '-1', text: __('No parent group') }, { type: 'divider' }];
- this.groupDropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.groupDropdown, {
selectable: true,
filterable: true,
toggleLabel: item => item.text,
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 4c50bb3a9ac..aac23db8fd6 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -97,7 +97,10 @@ const groupsSelect = () => {
});
};
-export default () =>
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(groupsSelect)
- .catch(() => {});
+export default () => {
+ if ($('.ajax-groups-select').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(groupsSelect)
+ .catch(() => {});
+ }
+};
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 3f9163e924d..b833cca1db6 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -2,9 +2,6 @@ import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility';
-import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
-import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
/**
@@ -17,7 +14,7 @@ import Tracking from '~/tracking';
export default function initTodoToggle() {
$(document).on('todo:toggle', (e, count) => {
const updatedCount = count || e?.detail?.count || 0;
- const $todoPendingCount = $('.todos-count');
+ const $todoPendingCount = $('.js-todos-count');
$todoPendingCount.text(highCountTrim(updatedCount));
$todoPendingCount.toggleClass('hidden', updatedCount === 0);
@@ -26,51 +23,43 @@ export default function initTodoToggle() {
function initStatusTriggers() {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
- const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
- if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
- Vue.use(Translate);
+ if (setStatusModalTriggerEl) {
+ setStatusModalTriggerEl.addEventListener('click', () => {
+ import(
+ /* webpackChunkName: 'statusModalBundle' */ './set_status_modal/set_status_modal_wrapper.vue'
+ )
+ .then(({ default: SetStatusModalWrapper }) => {
+ const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
+ const statusModalElement = document.createElement('div');
+ setStatusModalWrapperEl.appendChild(statusModalElement);
- // eslint-disable-next-line no-new
- new Vue({
- el: setStatusModalTriggerEl,
- data() {
- const { hasStatus } = this.$options.el.dataset;
+ Vue.use(Translate);
- return {
- hasStatus: parseBoolean(hasStatus),
- };
- },
- render(createElement) {
- return createElement(SetStatusModalTrigger, {
- props: {
- hasStatus: this.hasStatus,
- },
- });
- },
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el: setStatusModalWrapperEl,
- data() {
- const { currentEmoji, currentMessage } = this.$options.el.dataset;
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: statusModalElement,
+ data() {
+ const { currentEmoji, currentMessage } = setStatusModalWrapperEl.dataset;
- return {
- currentEmoji,
- currentMessage,
- };
- },
- render(createElement) {
- const { currentEmoji, currentMessage } = this;
+ return {
+ currentEmoji,
+ currentMessage,
+ };
+ },
+ render(createElement) {
+ const { currentEmoji, currentMessage } = this;
- return createElement(SetStatusModalWrapper, {
- props: {
- currentEmoji,
- currentMessage,
- },
- });
- },
+ return createElement(SetStatusModalWrapper, {
+ props: {
+ currentEmoji,
+ currentMessage,
+ },
+ });
+ },
+ });
+ })
+ .catch(() => {});
});
}
}
@@ -101,5 +90,5 @@ export function initNavUserDropdownTracking() {
document.addEventListener('DOMContentLoaded', () => {
requestIdleCallback(initStatusTriggers);
- initNavUserDropdownTracking();
+ requestIdleCallback(initNavUserDropdownTracking);
});
diff --git a/app/assets/javascripts/helpers/startup_css_helper.js b/app/assets/javascripts/helpers/startup_css_helper.js
new file mode 100644
index 00000000000..8e25e1421c0
--- /dev/null
+++ b/app/assets/javascripts/helpers/startup_css_helper.js
@@ -0,0 +1,46 @@
+const CSS_LOADED_EVENT = 'CSSLoaded';
+const STARTUP_LINK_LOADED_EVENT = 'CSSStartupLinkLoaded';
+
+const getAllStartupLinks = (() => {
+ let links = null;
+ return () => {
+ if (!links) {
+ links = Array.from(document.querySelectorAll('link[data-startupcss]'));
+ }
+ return links;
+ };
+})();
+const isStartupLinkLoaded = ({ dataset }) => dataset.startupcss === 'loaded';
+const allLinksLoaded = () => getAllStartupLinks().every(isStartupLinkLoaded);
+
+const handleStartupEvents = () => {
+ if (allLinksLoaded()) {
+ document.dispatchEvent(new CustomEvent(CSS_LOADED_EVENT));
+ document.removeEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
+ }
+};
+
+/* Wait for.... The methods can be used:
+ - with a callback (preferred),
+ waitFor(action)
+
+ - with then (discouraged),
+ await waitFor().then(action);
+
+ - with await,
+ await waitFor;
+ action();
+-*/
+export const waitForCSSLoaded = (action = () => {}) => {
+ if (!gon.features.startupCss || allLinksLoaded()) {
+ return new Promise(resolve => {
+ action();
+ resolve();
+ });
+ }
+
+ return new Promise(resolve => {
+ document.addEventListener(CSS_LOADED_EVENT, resolve, { once: true });
+ document.addEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
+ }).then(action);
+};
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index a65af55fcac..183816921c1 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -1,13 +1,13 @@
<script>
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import { leftSidebarViews } from '../constants';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -44,11 +44,12 @@ export default {
:aria-label="s__('IDE|Edit')"
data-container="body"
data-placement="right"
+ data-qa-selector="edit_mode_tab"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.edit.name)"
>
- <icon name="code" />
+ <gl-icon name="code" />
</button>
</li>
<li>
@@ -65,7 +66,7 @@ export default {
class="ide-sidebar-link js-ide-review-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.review.name)"
>
- <icon name="file-modified" />
+ <gl-icon name="file-modified" />
</button>
</li>
<li>
@@ -78,11 +79,12 @@ export default {
:aria-label="s__('IDE|Commit')"
data-container="body"
data-placement="right"
+ data-qa-selector="commit_mode_tab"
type="button"
- class="ide-sidebar-link js-ide-commit-mode qa-commit-mode-tab"
+ class="ide-sidebar-link js-ide-commit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)"
>
- <icon name="commit" />
+ <gl-icon name="commit" />
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue
index 49744d573da..2fe435b92ab 100644
--- a/app/assets/javascripts/ide/components/branches/item.vue
+++ b/app/assets/javascripts/ide/components/branches/item.vue
@@ -1,11 +1,11 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- Icon,
+ GlIcon,
Timeago,
},
props: {
@@ -34,7 +34,7 @@ export default {
<template>
<a :href="branchHref" class="btn-link d-flex align-items-center">
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
- <icon v-if="isActive" :size="18" name="mobile-issue-close" />
+ <gl-icon v-if="isActive" :size="18" name="mobile-issue-close" />
</span>
<span>
<strong> {{ item.name }} </strong>
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index dd2d726d525..c317fadb656 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -1,14 +1,13 @@
<script>
import { mapActions, mapState } from 'vuex';
import { debounce } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import Item from './item.vue';
export default {
components: {
Item,
- Icon,
+ GlIcon,
GlLoadingIcon,
},
data() {
@@ -67,7 +66,7 @@ export default {
class="form-control dropdown-input-field"
@input="searchBranches"
/>
- <icon :size="18" name="search" class="ml-3 input-icon" />
+ <gl-icon :size="18" name="search" class="ml-3 input-icon" />
</label>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 407e4c57cd8..de4b0a34002 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, s__ } from '~/locale';
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 9342ab87c1a..73c56514fce 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui';
import { n__, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
@@ -8,6 +8,7 @@ import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
import consts from '../../stores/modules/commit/constants';
+import { createUnexpectedCommitError } from '../../lib/errors';
export default {
components: {
@@ -17,15 +18,20 @@ export default {
SuccessMessage,
GlModal,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
data() {
return {
isCompact: true,
componentHeight: null,
+ // Keep track of "lastCommitError" so we hold onto the value even when "commitError" is cleared.
+ lastCommitError: createUnexpectedCommitError(),
};
},
computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
- ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading', 'commitError']),
...mapGetters(['someUncommittedChanges']),
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() {
@@ -38,11 +44,28 @@ export default {
currentViewIsCommitView() {
return this.currentActivityView === leftSidebarViews.commit.name;
},
+ commitErrorPrimaryAction() {
+ if (!this.lastCommitError?.canCreateBranch) {
+ return undefined;
+ }
+
+ return {
+ text: __('Create new branch'),
+ };
+ },
},
watch: {
currentActivityView: 'handleCompactState',
someUncommittedChanges: 'handleCompactState',
lastCommitMsg: 'handleCompactState',
+ commitError(val) {
+ if (!val) {
+ return;
+ }
+
+ this.lastCommitError = val;
+ this.$refs.commitErrorModal.show();
+ },
},
methods: {
...mapActions(['updateActivityBarView']),
@@ -53,9 +76,7 @@ export default {
'updateCommitAction',
]),
commit() {
- return this.commitChanges().catch(() => {
- this.$refs.createBranchModal.show();
- });
+ return this.commitChanges();
},
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit());
@@ -164,17 +185,14 @@ export default {
</button>
</div>
<gl-modal
- ref="createBranchModal"
- modal-id="ide-create-branch-modal"
- :ok-title="__('Create new branch')"
- :title="__('Branch has changed')"
- ok-variant="success"
+ ref="commitErrorModal"
+ modal-id="ide-commit-error-modal"
+ :title="lastCommitError.title"
+ :action-primary="commitErrorPrimaryAction"
+ :action-cancel="{ text: __('Cancel') }"
@ok="forceCreateNewBranch"
>
- {{
- __(`This branch has changed since you started editing.
- Would you like to create a new branch?`)
- }}
+ <div v-safe-html="lastCommitError.messageHTML"></div>
</gl-modal>
</form>
</transition>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index d1422a506e7..609ce287d3f 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,14 +1,13 @@
<script>
import { mapActions } from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
export default {
components: {
- Icon,
+ GlIcon,
ListItem,
GlModal,
},
@@ -74,7 +73,7 @@ export default {
<div class="ide-commit-list-container">
<header class="multi-file-commit-panel-header d-flex mb-0">
<div class="d-flex align-items-center flex-fill">
- <icon v-once :name="iconName" :size="18" class="gl-mr-3" />
+ <gl-icon v-once :name="iconName" :size="18" class="gl-mr-3" />
<strong> {{ titleText }} </strong>
<div class="d-flex ml-auto">
<button
@@ -93,7 +92,7 @@ export default {
data-boundary="viewport"
@click="openDiscardModal"
>
- <icon :size="16" name="remove-all" class="ml-auto mr-auto position-top-0" />
+ <gl-icon :size="16" name="remove-all" class="ml-auto mr-auto position-top-0" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 1b257ca11cc..4821b8389ff 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -1,11 +1,11 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import { sprintf, n__, __ } from '~/locale';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -77,7 +77,7 @@ export default {
data-placement="left"
class="gl-mb-5"
>
- <icon v-once :name="iconName" :size="18" />
+ <gl-icon v-once :name="iconName" :size="18" />
</div>
<div
v-tooltip
@@ -86,7 +86,7 @@ export default {
data-placement="left"
class="gl-mb-3"
>
- <icon :name="additionIconName" :size="18" :class="addedFilesIconClass" />
+ <gl-icon :name="additionIconName" :size="18" :class="addedFilesIconClass" />
</div>
{{ addedFilesLength }}
<div
@@ -96,7 +96,7 @@ export default {
data-placement="left"
class="gl-mt-3 gl-mb-3"
>
- <icon :name="modifiedIconName" :size="18" :class="modifiedFilesClass" />
+ <gl-icon :name="modifiedIconName" :size="18" :class="modifiedFilesClass" />
</div>
{{ modifiedFilesLength }}
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index c65169f5d31..a0d6cf3c42d 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,14 +1,14 @@
<script>
import { mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { viewerTypes } from '../../constants';
import getCommitIconMap from '../../commit_icon';
export default {
components: {
- Icon,
+ GlIcon,
FileIcon,
},
directives: {
@@ -95,7 +95,7 @@ export default {
</span>
<div class="ml-auto d-flex align-items-center">
<div class="d-flex align-items-center ide-commit-list-changed-icon">
- <icon :name="iconName" :size="16" :class="iconClass" />
+ <gl-icon :name="iconName" :size="16" :class="iconClass" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index b37c7280a30..2787b10a48b 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -1,6 +1,6 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '../../../locale';
-import Icon from '../../../vue_shared/components/icon.vue';
import popover from '../../../vue_shared/directives/popover';
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
@@ -9,7 +9,7 @@ export default {
popover,
},
components: {
- Icon,
+ GlIcon,
},
props: {
text: {
@@ -84,7 +84,7 @@ export default {
<li>
{{ __('Commit Message') }}
<span v-popover="$options.popoverOptions" class="form-text text-muted gl-ml-3">
- <icon name="question" />
+ <gl-icon name="question" />
</span>
</li>
</ul>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
index 327b0b8172f..977efb0ca22 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapState } from 'vuex';
export default {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index d36adbd798e..08635b43b91 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions } from 'vuex';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index f7cf7a5b251..48ab58e1cb7 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue';
import MrFileIcon from './mr_file_icon.vue';
@@ -13,7 +13,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
NewDropdown,
ChangedFileIcon,
MrFileIcon,
@@ -69,7 +69,7 @@ export default {
<mr-file-icon v-if="file.mrChange" />
<span v-if="showTreeChangesCount" class="ide-tree-changes">
{{ changesCount }}
- <icon
+ <gl-icon
v-tooltip
:title="folderChangesTooltip"
:size="12"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 55b3eaf9737..1b03d9eee8b 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -47,6 +47,7 @@ export default {
'emptyRepo',
'currentTree',
'editorTheme',
+ 'getUrlForPath',
]),
themeName() {
return window.gon?.user_color_scheme;
@@ -71,7 +72,7 @@ export default {
return returnValue;
},
openFile(file) {
- this.$router.push(`/project${file.url}`);
+ this.$router.push(this.getUrlForPath(file.path));
},
createNewFile() {
this.$refs.newModal.open(modalTypes.blob);
diff --git a/app/assets/javascripts/ide/components/ide_file_row.vue b/app/assets/javascripts/ide/components/ide_file_row.vue
index b777d89f0bb..248677d6a99 100644
--- a/app/assets/javascripts/ide/components/ide_file_row.vue
+++ b/app/assets/javascripts/ide/components/ide_file_row.vue
@@ -3,6 +3,7 @@
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
*/
+import { mapGetters } from 'vuex';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowExtra from './file_row_extra.vue';
@@ -23,6 +24,9 @@ export default {
dropdownOpen: false,
};
},
+ computed: {
+ ...mapGetters(['getUrlForPath']),
+ },
methods: {
toggleDropdown(val) {
this.dropdownOpen = val;
@@ -32,7 +36,13 @@ export default {
</script>
<template>
- <file-row :file="file" v-bind="$attrs" @mouseleave="toggleDropdown(false)" v-on="$listeners">
+ <file-row
+ :file="file"
+ :file-url="getUrlForPath(file.path)"
+ v-bind="$attrs"
+ @mouseleave="toggleDropdown(false)"
+ v-on="$listeners"
+ >
<file-row-extra :file="file" :dropdown-open="dropdownOpen" @toggle="toggleDropdown($event)" />
</file-row>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
index 95348711e1d..e36d0a5a5b1 100644
--- a/app/assets/javascripts/ide/components/ide_review.vue
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -10,7 +10,7 @@ export default {
EditorModeDropdown,
},
computed: {
- ...mapGetters(['currentMergeRequest', 'activeFile']),
+ ...mapGetters(['currentMergeRequest', 'activeFile', 'getUrlForPath']),
...mapState(['viewer', 'currentMergeRequestId']),
showLatestChangesText() {
return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
@@ -24,7 +24,7 @@ export default {
},
mounted() {
if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) {
- this.$router.push(`/project${this.activeFile.url}`, () => {
+ this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
this.updateViewer('editor');
});
} else if (this.activeFile && this.activeFile.deleted) {
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 1eb89b41495..ed68ca5cae9 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapGetters } from 'vuex';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index ddc126c3d77..146e818d654 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,9 +1,9 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import IdeStatusList from './ide_status_list.vue';
import IdeStatusMr from './ide_status_mr.vue';
-import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
@@ -12,7 +12,7 @@ import { rightSidebarViews } from '../constants';
export default {
components: {
- icon,
+ GlIcon,
userAvatarImage,
CiIcon,
IdeStatusList,
@@ -97,12 +97,13 @@ export default {
{{ latestPipeline.details.status.text }} for
</span>
- <icon name="commit" />
+ <gl-icon name="commit" />
<a
v-tooltip
:title="lastCommit.message"
:href="getCommitPath(lastCommit.short_id)"
class="commit-sha"
+ data-qa-selector="commit_sha_content"
>{{ lastCommit.short_id }}</a
>
by
diff --git a/app/assets/javascripts/ide/components/ide_status_list.vue b/app/assets/javascripts/ide/components/ide_status_list.vue
index 1354fdc3d98..caa122f6ed2 100644
--- a/app/assets/javascripts/ide/components/ide_status_list.vue
+++ b/app/assets/javascripts/ide/components/ide_status_list.vue
@@ -2,7 +2,7 @@
import { mapGetters } from 'vuex';
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue';
-import { getFileEOL } from '../utils';
+import { isTextFile, getFileEOL } from '~/ide/utils';
export default {
components: {
@@ -17,6 +17,9 @@ export default {
activeFileEOL() {
return getFileEOL(this.activeFile.content);
},
+ activeFileIsText() {
+ return isTextFile(this.activeFile);
+ },
},
};
</script>
@@ -30,7 +33,7 @@ export default {
</gl-link>
</div>
<div>{{ activeFileEOL }}</div>
- <div v-if="!activeFile.binary">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div>
+ <div v-if="activeFileIsText">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div>
<div>{{ activeFile.fileLanguage }}</div>
</template>
<terminal-sync-status-safe />
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index 647f4d4be85..747d5044790 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -15,13 +15,13 @@ export default {
},
computed: {
...mapState(['currentBranchId']),
- ...mapGetters(['currentProject', 'currentTree', 'activeFile']),
+ ...mapGetters(['currentProject', 'currentTree', 'activeFile', 'getUrlForPath']),
},
mounted() {
if (!this.activeFile) return;
if (this.activeFile.pending && !this.activeFile.deleted) {
- this.$router.push(`/project${this.activeFile.url}`, () => {
+ this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
this.updateViewer('editor');
});
} else if (this.activeFile.deleted) {
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 36e8951bea3..776d8459515 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import FileTree from '~/vue_shared/components/file_tree.vue';
import IdeFileRow from './ide_file_row.vue';
import NavDropdown from './nav_dropdown.vue';
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 975d54c7a4e..11033a5cc88 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -1,9 +1,10 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions, mapState } from 'vuex';
import { throttle } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
import { __ } from '../../../locale';
import tooltip from '../../../vue_shared/directives/tooltip';
-import Icon from '../../../vue_shared/components/icon.vue';
import ScrollButton from './detail/scroll_button.vue';
import JobDescription from './detail/description.vue';
@@ -17,7 +18,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
ScrollButton,
JobDescription,
},
@@ -39,10 +40,10 @@ export default {
},
},
mounted() {
- this.getTrace();
+ this.getLogs();
},
methods: {
- ...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']),
+ ...mapActions('pipelines', ['fetchJobLogs', 'setDetailJob']),
scrollDown() {
if (this.$refs.buildTrace) {
this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight);
@@ -65,8 +66,8 @@ export default {
this.scrollPos = '';
}
}),
- getTrace() {
- return this.fetchJobTrace().then(() => this.scrollDown());
+ getLogs() {
+ return this.fetchJobLogs().then(() => this.scrollDown());
},
},
};
@@ -76,7 +77,7 @@ export default {
<div class="ide-pipeline build-page d-flex flex-column flex-fill">
<header class="ide-job-header d-flex align-items-center">
<button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null)">
- <icon name="chevron-left" /> {{ __('View jobs') }}
+ <gl-icon name="chevron-left" /> {{ __('View jobs') }}
</button>
</header>
<div class="top-bar d-flex border-left-0 mr-3">
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index f1ba102fffe..9eaeabad5ef 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -1,10 +1,10 @@
<script>
-import Icon from '../../../../vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import CiIcon from '../../../../vue_shared/components/ci_icon.vue';
export default {
components: {
- Icon,
+ GlIcon,
CiIcon,
},
props: {
@@ -26,8 +26,14 @@ export default {
<ci-icon :status="job.status" :borderless="true" :size="24" class="d-flex" />
<span class="gl-ml-3">
{{ job.name }}
- <a :href="job.path" target="_blank" class="ide-external-link position-relative">
- {{ jobId }} <icon :size="12" name="external-link" />
+ <a
+ v-if="job.path"
+ :href="job.path"
+ target="_blank"
+ class="ide-external-link gl-relative"
+ data-testid="description-detail-link"
+ >
+ {{ jobId }} <gl-icon :size="12" name="external-link" />
</a>
</span>
</div>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
index 5674d3ffa80..2c679a3edc7 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
@@ -1,6 +1,6 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __ } from '../../../../locale';
-import Icon from '../../../../vue_shared/components/icon.vue';
import tooltip from '../../../../vue_shared/directives/tooltip';
const directions = {
@@ -13,7 +13,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
},
props: {
direction: {
@@ -58,7 +58,7 @@ export default {
type="button"
@click="clickedScroll"
>
- <icon :name="iconName" />
+ <gl-icon :name="iconName" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 75441e8c1c8..0b643947139 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -1,7 +1,6 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import tooltip from '../../../vue_shared/directives/tooltip';
-import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Item from './item.vue';
@@ -10,7 +9,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
CiIcon,
Item,
GlLoadingIcon,
@@ -78,7 +77,7 @@ export default {
<div v-if="!stage.isLoading || stage.jobs.length" class="gl-mr-3 gl-ml-2">
<span class="badge badge-pill"> {{ jobsCount }} </span>
</div>
- <icon :name="collapseIcon" class="ide-stage-collapse-icon" />
+ <gl-icon :name="collapseIcon" class="ide-stage-collapse-icon" />
</div>
<div v-show="!stage.isCollapsed" ref="jobList" class="card-body p-0">
<gl-loading-icon v-if="showLoadingIcon" />
diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue
index 8b7b8d5a91c..7aa9a4f864a 100644
--- a/app/assets/javascripts/ide/components/merge_requests/item.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/item.vue
@@ -1,9 +1,9 @@
<script>
-import Icon from '../../../vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
item: {
@@ -41,7 +41,7 @@ export default {
<template>
<a :href="mergeRequestHref" class="btn-link d-flex align-items-center">
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
- <icon v-if="isActive" :size="18" name="mobile-issue-close" />
+ <gl-icon v-if="isActive" :size="18" name="mobile-issue-close" />
</span>
<span>
<strong> {{ item.title }} </strong>
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index af45d88b84a..4b3c6e61e11 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -1,9 +1,8 @@
<script>
import { mapActions, mapState } from 'vuex';
import { debounce } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
@@ -16,7 +15,7 @@ export default {
components: {
TokenedInput,
Item,
- Icon,
+ GlIcon,
GlLoadingIcon,
},
data() {
@@ -85,7 +84,7 @@ export default {
@input="searchMergeRequests"
@removeToken="setSearchType(null)"
/>
- <icon :size="18" name="search" class="ml-3 input-icon" />
+ <gl-icon :size="18" name="search" class="ml-3 input-icon" />
</label>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
@@ -103,7 +102,7 @@ export default {
@click.stop="setSearchType(searchType)"
>
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
- <icon :size="18" name="search" />
+ <gl-icon :size="18" name="search" />
</span>
<span>{{ searchType.label }}</span>
</button>
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
index 4fab57b6f3e..c8629a869e0 100644
--- a/app/assets/javascripts/ide/components/mr_file_icon.vue
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -1,10 +1,10 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -13,7 +13,7 @@ export default {
</script>
<template>
- <icon
+ <gl-icon
v-tooltip
:title="__('Part of merge request changes')"
:size="12"
diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
index 8dc22620eca..116d3cec03e 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
@@ -1,13 +1,13 @@
<script>
import { mapState } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const EMPTY_LABEL = '-';
export default {
components: {
- Icon,
+ GlIcon,
DropdownButton,
},
props: {
@@ -33,10 +33,10 @@ export default {
<dropdown-button>
<span class="row flex-nowrap">
<span class="col-auto flex-fill text-truncate">
- <icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
+ <gl-icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
</span>
<span v-if="showMergeRequests" class="col-5 pl-0 text-truncate">
- <icon :size="16" :aria-label="__('Merge Request')" name="merge-request" />
+ <gl-icon :size="16" :aria-label="__('Merge Request')" name="merge-request" />
{{ mergeRequestLabel }}
</span>
</span>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue
index 5bd6642930c..8ae8f97f237 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/button.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue
@@ -1,5 +1,5 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -7,7 +7,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
},
props: {
label: {
@@ -52,7 +52,7 @@ export default {
class="btn-blank"
@click.stop.prevent="clicked"
>
- <icon :name="icon" :class="iconClasses" />
+ <gl-icon :name="icon" :class="iconClasses" />
<template v-if="showLabel">
{{ label }}
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index b656e35f150..692878de5e1 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions } from 'vuex';
-import icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import upload from './upload.vue';
import ItemButton from './button.vue';
import { modalTypes } from '../../constants';
@@ -8,7 +8,7 @@ import NewModal from './modal.vue';
export default {
components: {
- icon,
+ GlIcon,
upload,
ItemButton,
NewModal,
@@ -67,7 +67,7 @@ export default {
data-qa-selector="dropdown_button"
@click.stop="openDropdown()"
>
- <icon name="ellipsis_v" /> <icon name="chevron-down" />
+ <gl-icon name="ellipsis_v" /> <gl-icon name="chevron-down" />
</button>
<ul ref="dropdownMenu" class="dropdown-menu dropdown-menu-right">
<template v-if="type === 'tree'">
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 44986c8c575..528475849de 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __, sprintf, s__ } from '~/locale';
import { modalTypes } from '../../constants';
@@ -9,6 +9,7 @@ import { trimPathComponents, getPathParent } from '../../utils';
export default {
components: {
GlModal,
+ GlButton,
},
data() {
return {
@@ -156,13 +157,14 @@ export default {
/>
<ul v-if="isCreatingNewFile" class="file-templates gl-mt-3 list-inline qa-template-list">
<li v-for="(template, index) in templateTypes" :key="index" class="list-inline-item">
- <button
- type="button"
- class="btn btn-missing p-1 pr-2 pl-2"
+ <gl-button
+ variant="dashed"
+ category="secondary"
+ class="p-1 pr-2 pl-2"
@click="createFromTemplate(template)"
>
{{ template.name }}
- </button>
+ </gl-button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index b2141c13d9f..84ff05c9750 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -28,14 +28,13 @@ export default {
const { name } = file;
const encodedContent = target.result.split('base64,')[1];
const rawContent = encodedContent ? atob(encodedContent) : '';
- const isText = isTextFile(rawContent, file.type, name);
+ const isText = isTextFile({ content: rawContent, mimeType: file.type, name });
const emitCreateEvent = content =>
this.$emit('create', {
name: `${this.path ? `${this.path}/` : ''}${name}`,
type: 'blob',
content,
- binary: !isText,
rawPath: !isText ? target.result : '',
});
diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
index 4e8e1e3a470..f1b882d8f29 100644
--- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
+++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
@@ -1,7 +1,6 @@
<script>
import { mapActions, mapState } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import IdeSidebarNav from '../ide_sidebar_nav.vue';
export default {
@@ -10,7 +9,6 @@ export default {
tooltip,
},
components: {
- Icon,
IdeSidebarNav,
},
props: {
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 6038e92f254..91bd64a2c9c 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,9 +1,8 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { escape } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
-import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Tabs from '../../../vue_shared/components/tabs/tabs';
import Tab from '../../../vue_shared/components/tabs/tab.vue';
@@ -14,7 +13,7 @@ import IDEServices from '~/ide/services';
export default {
components: {
- Icon,
+ GlIcon,
CiIcon,
Tabs,
Tab,
@@ -22,6 +21,9 @@ export default {
EmptyState,
GlLoadingIcon,
},
+ directives: {
+ SafeHtml,
+ },
computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
...mapGetters(['currentProject']),
@@ -70,7 +72,7 @@ export default {
target="_blank"
class="ide-external-link position-relative"
>
- #{{ latestPipeline.id }} <icon :size="12" name="external-link" />
+ #{{ latestPipeline.id }} <gl-icon :size="12" name="external-link" />
</a>
</span>
</header>
@@ -84,7 +86,7 @@ export default {
<div v-else-if="latestPipeline.yamlError" class="bs-callout bs-callout-danger">
<p class="gl-mb-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p>
<p class="gl-mb-0 break-word">{{ latestPipeline.yamlError }}</p>
- <p class="gl-mb-0" v-html="ciLintText"></p>
+ <p v-safe-html="ciLintText" class="gl-mb-0"></p>
</div>
<tabs v-else class="ide-pipeline-list">
<tab :active="!pipelineFailed">
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index 0de9dfd8827..60710251fef 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -1,11 +1,10 @@
<script>
import { listen } from 'codesandbox-api';
-import { GlLoadingIcon } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -97,7 +96,7 @@ export default {
class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
@click="back"
>
- <icon :size="24" name="chevron-left" class="m-auto" />
+ <gl-icon :size="24" name="chevron-left" class="m-auto" />
</button>
<button
:aria-label="s__('IDE|Back')"
@@ -109,7 +108,7 @@ export default {
class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
@click="forward"
>
- <icon :size="24" name="chevron-right" class="m-auto" />
+ <gl-icon :size="24" name="chevron-right" class="m-auto" />
</button>
<button
:aria-label="s__('IDE|Refresh preview')"
@@ -117,7 +116,7 @@ export default {
class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
@click="refresh"
>
- <icon :size="18" name="retry" class="m-auto" />
+ <gl-icon :size="18" name="retry" class="m-auto" />
</button>
<div class="position-relative w-100 gl-ml-2">
<input
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index d22d430cb4a..f342ce1739c 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -14,7 +14,7 @@ import Editor from '../lib/editor';
import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale';
import { extractMarkdownImagesFromEntries } from '../stores/utils';
-import { getPathParent, readFileAsDataURL } from '../utils';
+import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
@@ -48,6 +48,7 @@ export default {
'renderWhitespaceInCode',
'editorTheme',
'entries',
+ 'currentProjectId',
]),
...mapGetters([
'currentMergeRequest',
@@ -55,10 +56,11 @@ export default {
'isEditModeActive',
'isCommitModeActive',
'currentBranch',
+ 'getJsonSchemaForPath',
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() {
- return this.file && this.file.binary;
+ return this.file && !isTextFile(this.file);
},
showContentViewer() {
return (
@@ -196,6 +198,8 @@ export default {
this.editor.clearEditor();
+ this.registerSchemaForFile();
+
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
.then(() => {
this.createEditorInstance();
@@ -329,6 +333,10 @@ export default {
// do nothing if no image is found in the clipboard
return Promise.resolve();
},
+ registerSchemaForFile() {
+ const schema = this.getJsonSchemaForPath(this.file.path);
+ registerSchema(schema);
+ },
},
viewerTypes,
FILE_VIEW_MODE_EDITOR,
@@ -379,7 +387,7 @@ export default {
:path="file.rawPath || file.path"
:file-path="file.path"
:file-size="file.size"
- :project-path="file.projectId"
+ :project-path="currentProjectId"
:commit-sha="currentBranchCommit"
:type="fileType"
/>
@@ -390,7 +398,7 @@ export default {
:new-sha="currentMergeRequest.sha"
:old-path="file.mrChange.old_path"
:old-sha="currentMergeRequest.baseCommitSha"
- :project-path="file.projectId"
+ :project-path="currentProjectId"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 9773e835a5c..1402f7aaf39 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,12 +1,12 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
export default {
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -29,6 +29,6 @@ export default {
<template>
<span v-if="file.file_lock" v-tooltip :title="lockTooltip" data-container="body">
- <icon name="lock" class="file-status-icon" />
+ <gl-icon name="lock" class="file-status-icon" />
</span>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 8370833233a..60a80a31a8b 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,9 +1,9 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import FileStatusIcon from './repo_file_status_icon.vue';
@@ -11,7 +11,7 @@ export default {
components: {
FileStatusIcon,
FileIcon,
- Icon,
+ GlIcon,
ChangedFileIcon,
},
props: {
@@ -26,6 +26,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['getUrlForPath']),
closeLabel() {
if (this.fileHasChanged) {
return sprintf(__(`%{tabname} changed`), { tabname: this.tab.name });
@@ -52,7 +53,7 @@ export default {
if (tab.pending) {
this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
- this.$router.push(`/project${tab.url}`);
+ this.$router.push(this.getUrlForPath(tab.path));
}
},
mouseOverTab() {
@@ -79,7 +80,7 @@ export default {
@mouseover="mouseOverTab"
@mouseout="mouseOutTab"
>
- <div :title="tab.url" class="multi-file-tab">
+ <div :title="getUrlForPath(tab.path)" class="multi-file-tab">
<file-icon :file-name="tab.name" :size="16" />
{{ tab.name }}
<file-status-icon :file="tab" />
@@ -91,7 +92,7 @@ export default {
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
>
- <icon v-if="!showChangedIcon" :size="12" name="close" />
+ <gl-icon v-if="!showChangedIcon" :size="12" name="close" />
<changed-file-icon v-else :file="tab" />
</button>
</li>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 47c75be3f7c..c03694e3619 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import RepoTab from './repo_tab.vue';
export default {
@@ -20,6 +20,9 @@ export default {
required: true,
},
},
+ computed: {
+ ...mapGetters(['getUrlForPath']),
+ },
methods: {
...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
@@ -27,7 +30,7 @@ export default {
if (this.activeFile.pending) {
return this.removePendingTab(this.activeFile).then(() => {
- this.$router.push(`/project${this.activeFile.url}`);
+ this.$router.push(this.getUrlForPath(this.activeFile.path));
});
}
diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue
index de3e71dad92..e7a4c5487d1 100644
--- a/app/assets/javascripts/ide/components/shared/tokened_input.vue
+++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue
@@ -1,10 +1,10 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
placeholder: {
@@ -81,7 +81,7 @@ export default {
>
<div class="value-container rounded">
<div class="value">{{ token.label }}</div>
- <div class="remove-token inverted"><icon :size="10" name="close" /></div>
+ <div class="remove-token inverted"><gl-icon :size="10" name="close" /></div>
</div>
</button>
</div>
diff --git a/app/assets/javascripts/ide/components/terminal/empty_state.vue b/app/assets/javascripts/ide/components/terminal/empty_state.vue
index 5dd12e62820..3668dd24e81 100644
--- a/app/assets/javascripts/ide/components/terminal/empty_state.vue
+++ b/app/assets/javascripts/ide/components/terminal/empty_state.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlLoadingIcon } from '@gitlab/ui';
export default {
diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
index deb13b5615e..c3f722d6052 100644
--- a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
+++ b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
@@ -1,8 +1,7 @@
<script>
import { throttle } from 'lodash';
-import { GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mapState } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
import {
MSG_TERMINAL_SYNC_CONNECTING,
MSG_TERMINAL_SYNC_UPLOADING,
@@ -11,7 +10,7 @@ import {
export default {
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
directives: {
@@ -70,7 +69,7 @@ export default {
<span>{{ __('Terminal') }}:</span>
<span class="square s16 d-flex-center ml-1" :aria-label="status.text">
<gl-loading-icon v-if="isLoading" inline size="sm" class="d-flex-center" />
- <icon v-else-if="status.icon" :name="status.icon" :size="16" />
+ <gl-icon v-else-if="status.icon" :name="status.icon" :size="16" />
</span>
</div>
</template>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 82cf8d7a10a..396aedbfa10 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -33,7 +33,6 @@ const EmptyRouterComponent = {
},
};
-// eslint-disable-next-line import/prefer-default-export
export const createRouter = store => {
const router = new IdeRouter({
mode: 'history',
diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js
index 3a456b7c4d6..62ec798b372 100644
--- a/app/assets/javascripts/ide/lib/diff/diff.js
+++ b/app/assets/javascripts/ide/lib/diff/diff.js
@@ -1,8 +1,6 @@
import { diffLines } from 'diff';
import { defaultDiffOptions } from '../editor_options';
-// See: https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20
-// eslint-disable-next-line import/prefer-default-export
export const computeDiff = (originalContent, newContent) => {
// prevent EOL changes from highlighting the entire file
const changes = diffLines(
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index f061fcb1259..2b12230c7cd 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -7,10 +7,9 @@ import ModelManager from './common/model_manager';
import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
import { themes } from './themes';
import languages from './languages';
-import schemas from './schemas';
import keymap from './keymap.json';
import { clearDomElement } from '~/editor/utils';
-import { registerLanguages, registerSchemas } from '../utils';
+import { registerLanguages } from '../utils';
function setupThemes() {
themes.forEach(theme => {
@@ -46,10 +45,6 @@ export default class Editor {
setupThemes();
registerLanguages(...languages);
- if (gon.features?.schemaLinting) {
- registerSchemas(...schemas);
- }
-
this.debouncedUpdate = debounce(() => {
this.updateDimensions();
}, 200);
diff --git a/app/assets/javascripts/ide/lib/editorconfig/parser.js b/app/assets/javascripts/ide/lib/editorconfig/parser.js
index a30a8cb868d..1597e4a8bfa 100644
--- a/app/assets/javascripts/ide/lib/editorconfig/parser.js
+++ b/app/assets/javascripts/ide/lib/editorconfig/parser.js
@@ -42,7 +42,6 @@ function getRulesWithConfigs(filePath, configFiles = [], rules = {}) {
return isRoot ? result : getRulesWithConfigs(filePath, nextConfigs, result);
}
-// eslint-disable-next-line import/prefer-default-export
export function getRulesWithTraversal(filePath, getFileContent) {
const editorconfigPaths = [
...getPathParents(filePath).map(x => `${x}/.editorconfig`),
diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js
new file mode 100644
index 00000000000..6ae18bc8180
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/errors.js
@@ -0,0 +1,39 @@
+import { escape } from 'lodash';
+import { __ } from '~/locale';
+
+const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
+const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
+
+export const createUnexpectedCommitError = () => ({
+ title: __('Unexpected error'),
+ messageHTML: __('Could not commit. An unexpected error occurred.'),
+ canCreateBranch: false,
+});
+
+export const createCodeownersCommitError = message => ({
+ title: __('CODEOWNERS rule violation'),
+ messageHTML: escape(message),
+ canCreateBranch: true,
+});
+
+export const createBranchChangedCommitError = message => ({
+ title: __('Branch changed'),
+ messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`,
+ canCreateBranch: true,
+});
+
+export const parseCommitError = e => {
+ const { message } = e?.response?.data || {};
+
+ if (!message) {
+ return createUnexpectedCommitError();
+ }
+
+ if (CODEOWNERS_REGEX.test(message)) {
+ return createCodeownersCommitError(message);
+ } else if (BRANCH_CHANGED_REGEX.test(message)) {
+ return createBranchChangedCommitError(message);
+ }
+
+ return createUnexpectedCommitError();
+};
diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js
index 6d85e225fd5..789e09fa8f2 100644
--- a/app/assets/javascripts/ide/lib/files.js
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -1,4 +1,3 @@
-import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../stores/utils';
export const splitParent = path => {
@@ -13,15 +12,7 @@ export const splitParent = path => {
/**
* Create file objects from a list of file paths.
*/
-export const decorateFiles = ({
- data,
- projectId,
- branchId,
- tempFile = false,
- content = '',
- binary = false,
- rawPath = '',
-}) => {
+export const decorateFiles = ({ data, tempFile = false, content = '', rawPath = '' }) => {
const treeList = [];
const entries = {};
@@ -41,12 +32,9 @@ export const decorateFiles = ({
parentPath = parentFolder && parentFolder.path;
const tree = decorateData({
- projectId,
- branchId,
id: path,
name,
path,
- url: `/${projectId}/tree/${branchId}/-/${path}/`,
type: 'tree',
tempFile,
changed: tempFile,
@@ -73,21 +61,16 @@ export const decorateFiles = ({
const fileFolder = parent && insertParent(parent);
if (name) {
- const previewMode = viewerInformationForPath(name);
parentPath = fileFolder && fileFolder.path;
file = decorateData({
- projectId,
- branchId,
id: path,
name,
path,
- url: `/${projectId}/blob/${branchId}/-/${path}`,
type: 'blob',
tempFile,
changed: tempFile,
content,
- binary: (previewMode && previewMode.binary) || binary,
rawPath,
parentPath,
});
diff --git a/app/assets/javascripts/ide/lib/schemas/index.js b/app/assets/javascripts/ide/lib/schemas/index.js
deleted file mode 100644
index 38a2f81921b..00000000000
--- a/app/assets/javascripts/ide/lib/schemas/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import json from './json';
-import yaml from './yaml';
-
-export default [json, yaml];
diff --git a/app/assets/javascripts/ide/lib/schemas/json/index.js b/app/assets/javascripts/ide/lib/schemas/json/index.js
deleted file mode 100644
index 900d5442bec..00000000000
--- a/app/assets/javascripts/ide/lib/schemas/json/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export default {
- language: 'json',
- options: {
- validate: true,
- enableSchemaRequest: true,
- schemas: [],
- },
-};
diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js b/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js
deleted file mode 100644
index af20744abb3..00000000000
--- a/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default {
- uri: 'https://json.schemastore.org/gitlab-ci',
- fileMatch: ['*.gitlab-ci.yml'],
-};
diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/index.js b/app/assets/javascripts/ide/lib/schemas/yaml/index.js
deleted file mode 100644
index e3fc406df4b..00000000000
--- a/app/assets/javascripts/ide/lib/schemas/yaml/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import gitlabCi from './gitlab_ci';
-
-export default {
- language: 'yaml',
- options: {
- validate: true,
- enableSchemaRequest: true,
- hover: true,
- completion: true,
- schemas: [gitlabCi],
- },
-};
diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js
index 211cc78bd99..89dda187360 100644
--- a/app/assets/javascripts/ide/services/gql.js
+++ b/app/assets/javascripts/ide/services/gql.js
@@ -17,5 +17,4 @@ const getClient = memoize(() =>
),
);
-// eslint-disable-next-line import/prefer-default-export
export const query = (...args) => getClient().query(...args);
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index ae4a1ba3db5..70a6a6b423d 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -33,7 +33,7 @@ export default {
})
.then(({ data }) => data);
},
- getBaseRawFileData(file, sha) {
+ getBaseRawFileData(file, projectId, ref) {
if (file.tempFile || file.baseRaw) return Promise.resolve(file.baseRaw);
// if files are renamed, their base path has changed
@@ -44,10 +44,10 @@ export default {
.get(
joinPaths(
gon.relative_url_root || '/',
- file.projectId,
+ projectId,
'-',
'raw',
- sha,
+ ref,
escapeFileUrl(filePath),
),
{
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index b083dc6325f..b8d59f8bd36 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -25,15 +25,7 @@ export const setResizingStatus = ({ commit }, resizing) => {
export const createTempEntry = (
{ state, commit, dispatch, getters },
- {
- name,
- type,
- content = '',
- binary = false,
- rawPath = '',
- openFile = true,
- makeFileActive = true,
- },
+ { name, type, content = '', rawPath = '', openFile = true, makeFileActive = true },
) => {
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
@@ -54,21 +46,14 @@ export const createTempEntry = (
const data = decorateFiles({
data: [fullName],
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
type,
tempFile: true,
content,
- binary,
rawPath,
});
const { file, parentPath } = data;
- commit(types.CREATE_TMP_ENTRY, {
- data,
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
- });
+ commit(types.CREATE_TMP_ENTRY, { data });
if (type === 'blob') {
if (openFile) commit(types.TOGGLE_FILE_OPEN, file.path);
@@ -90,7 +75,6 @@ export const addTempImage = ({ dispatch, getters }, { name, rawPath = '' }) =>
name: getters.getAvailableFileName(name),
type: 'blob',
content: rawPath.split('base64,')[1],
- binary: true,
rawPath,
openFile: false,
makeFileActive: false,
@@ -254,7 +238,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
}
if (newEntry.opened) {
- dispatch('router/push', `/project${newEntry.url}`, { root: true });
+ dispatch('router/push', getters.getUrlForPath(newEntry.path), { root: true });
}
}
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index c0cb924e749..3515d1fc933 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -6,7 +6,7 @@ import * as types from '../mutation_types';
import { setPageTitleForFile } from '../utils';
import { viewerTypes, stageKeys } from '../../constants';
-export const closeFile = ({ commit, state, dispatch }, file) => {
+export const closeFile = ({ commit, state, dispatch, getters }, file) => {
const { path } = file;
const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
const fileWasActive = file.active;
@@ -29,10 +29,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
});
} else {
- dispatch('router/push', `/project${nextFileToOpen.url}`, { root: true });
+ dispatch('router/push', getters.getUrlForPath(nextFileToOpen.path), { root: true });
}
} else if (!state.openFiles.length) {
- dispatch('router/push', `/project/${file.projectId}/tree/${file.branchId}/`, { root: true });
+ dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, {
+ root: true,
+ });
}
eventHub.$emit(`editor.update.model.dispose.${file.key}`);
@@ -121,7 +123,7 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) =
const baseSha =
(getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || '';
- return service.getBaseRawFileData(file, baseSha).then(baseRaw => {
+ return service.getBaseRawFileData(file, state.currentProjectId, baseSha).then(baseRaw => {
commit(types.SET_FILE_BASE_RAW_DATA, {
file,
baseRaw,
@@ -218,7 +220,7 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
if (!isDestructiveDiscard && file.path === getters.activeFile?.path) {
dispatch('updateDelayViewerUpdated', true)
.then(() => {
- dispatch('router/push', `/project${file.url}`, { root: true });
+ dispatch('router/push', getters.getUrlForPath(file.path), { root: true });
})
.catch(e => {
throw e;
@@ -274,7 +276,7 @@ export const openPendingTab = ({ commit, dispatch, getters, state }, { file, key
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
- dispatch('router/push', `/project/${file.projectId}/tree/${state.currentBranchId}/`, {
+ dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, {
root: true,
});
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 1ca608f1287..3a7daf30cc4 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -61,11 +61,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
service
.getFiles(selectedProject.path_with_namespace, ref)
.then(({ data }) => {
- const { entries, treeList } = decorateFiles({
- data,
- projectId,
- branchId,
- });
+ const { entries, treeList } = decorateFiles({ data });
commit(types.SET_ENTRIES, entries);
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 53734fa626b..b8304a9b68d 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -6,6 +6,7 @@ import {
PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE,
} from '../constants';
+import Api from '~/api';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
@@ -174,3 +175,21 @@ export const getAvailableFileName = (state, getters) => path => {
return newPath;
};
+
+export const getUrlForPath = state => path =>
+ `/project/${state.currentProjectId}/tree/${state.currentBranchId}/-/${path}/`;
+
+export const getJsonSchemaForPath = (state, getters) => path => {
+ const [namespace, ...project] = state.currentProjectId.split('/');
+ return {
+ uri:
+ // eslint-disable-next-line no-restricted-globals
+ location.origin +
+ Api.buildUrl(Api.projectFileSchemaPath)
+ .replace(':namespace_path', namespace)
+ .replace(':project_path', project.join('/'))
+ .replace(':ref', getters.currentBranch?.commit.id || state.currentBranchId)
+ .replace(':filename', path),
+ fileMatch: [`*${path}`],
+ };
+};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 277e6923f17..90a6c644d17 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,6 +1,5 @@
import { sprintf, __ } from '~/locale';
import { deprecatedCreateFlash as flash } from '~/flash';
-import httpStatusCodes from '~/lib/utils/http_status';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import service from '../../../services';
@@ -8,6 +7,7 @@ import * as types from './mutation_types';
import consts from './constants';
import { leftSidebarViews } from '../../../constants';
import eventHub from '../../../eventhub';
+import { parseCommitError } from '../../../lib/errors';
export const updateCommitMessage = ({ commit }, message) => {
commit(types.UPDATE_COMMIT_MESSAGE, message);
@@ -113,6 +113,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
? Promise.resolve()
: dispatch('stageAllChanges', null, { root: true });
+ commit(types.CLEAR_ERROR);
commit(types.UPDATE_LOADING, true);
return stageFilesPromise
@@ -128,6 +129,12 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return service.commit(rootState.currentProjectId, payload);
})
+ .catch(e => {
+ commit(types.UPDATE_LOADING, false);
+ commit(types.SET_ERROR, parseCommitError(e));
+
+ throw e;
+ })
.then(({ data }) => {
commit(types.UPDATE_LOADING, false);
@@ -214,24 +221,5 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
{ root: true },
),
);
- })
- .catch(err => {
- commit(types.UPDATE_LOADING, false);
-
- // don't catch bad request errors, let the view handle them
- if (err.response.status === httpStatusCodes.BAD_REQUEST) throw err;
-
- dispatch(
- 'setErrorMessage',
- {
- text: __('An error occurred while committing your changes.'),
- action: () =>
- dispatch('commitChanges').then(() => dispatch('setErrorMessage', null, { root: true })),
- actionText: __('Please try again'),
- },
- { root: true },
- );
-
- window.dispatchEvent(new Event('resize'));
});
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
index 7ad8f3570b7..47ec2ffbdde 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
@@ -3,3 +3,6 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
export const UPDATE_LOADING = 'UPDATE_LOADING';
export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR';
+
+export const CLEAR_ERROR = 'CLEAR_ERROR';
+export const SET_ERROR = 'SET_ERROR';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
index 73b618e250f..2cf6e8e6f36 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -24,4 +24,10 @@ export default {
shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR,
});
},
+ [types.CLEAR_ERROR](state) {
+ state.commitError = null;
+ },
+ [types.SET_ERROR](state, error) {
+ state.commitError = error;
+ },
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js
index f49737485f2..de092a569ad 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/state.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/state.js
@@ -4,4 +4,5 @@ export default () => ({
newBranchName: '',
submitCommitLoading: false,
shouldCreateMR: true,
+ commitError: null,
});
diff --git a/app/assets/javascripts/ide/stores/modules/pane/getters.js b/app/assets/javascripts/ide/stores/modules/pane/getters.js
index 7816172bb6f..ce597329df1 100644
--- a/app/assets/javascripts/ide/stores/modules/pane/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pane/getters.js
@@ -1,3 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const isAliveView = state => view =>
state.keepAliveViews[view] || (state.isOpen && state.currentView === view);
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 86b889546b0..99bd08ee876 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -118,31 +118,31 @@ export const setDetailJob = ({ commit, dispatch }, job) => {
});
};
-export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
-export const receiveJobTraceError = ({ commit, dispatch }) => {
+export const requestJobLogs = ({ commit }) => commit(types.REQUEST_JOB_LOGS);
+export const receiveJobLogsError = ({ commit, dispatch }) => {
dispatch(
'setErrorMessage',
{
- text: __('An error occurred while fetching the job trace.'),
+ text: __('An error occurred while fetching the job logs.'),
action: () =>
- dispatch('fetchJobTrace').then(() => dispatch('setErrorMessage', null, { root: true })),
+ dispatch('fetchJobLogs').then(() => dispatch('setErrorMessage', null, { root: true })),
actionText: __('Please try again'),
actionPayload: null,
},
{ root: true },
);
- commit(types.RECEIVE_JOB_TRACE_ERROR);
+ commit(types.RECEIVE_JOB_LOGS_ERROR);
};
-export const receiveJobTraceSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_JOB_TRACE_SUCCESS, data);
+export const receiveJobLogsSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_JOB_LOGS_SUCCESS, data);
-export const fetchJobTrace = ({ dispatch, state }) => {
- dispatch('requestJobTrace');
+export const fetchJobLogs = ({ dispatch, state }) => {
+ dispatch('requestJobLogs');
return axios
.get(`${state.detailJob.path}/trace`, { params: { format: 'json' } })
- .then(({ data }) => dispatch('receiveJobTraceSuccess', data))
- .catch(() => dispatch('receiveJobTraceError'));
+ .then(({ data }) => dispatch('receiveJobLogsSuccess', data))
+ .catch(() => dispatch('receiveJobLogsError'));
};
export const resetLatestPipeline = ({ commit }) => {
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
index f5b96327e40..bb4145934ff 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const states = {
failed: 'failed',
};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
index f4c36b9d96f..fea3055e0fe 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
@@ -10,6 +10,6 @@ export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
export const SET_DETAIL_JOB = 'SET_DETAIL_JOB';
-export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE';
-export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR';
-export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS';
+export const REQUEST_JOB_LOGS = 'REQUEST_JOB_LOGS';
+export const RECEIVE_JOB_LOGS_ERROR = 'RECEIVE_JOB_LOGS_ERROR';
+export const RECEIVE_JOB_LOGS_SUCCESS = 'RECEIVE_JOB_LOGS_SUCCESS';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index eaaa82cb339..3a3cb4a7cb2 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -66,13 +66,13 @@ export default {
[types.SET_DETAIL_JOB](state, job) {
state.detailJob = { ...job };
},
- [types.REQUEST_JOB_TRACE](state) {
+ [types.REQUEST_JOB_LOGS](state) {
state.detailJob.isLoading = true;
},
- [types.RECEIVE_JOB_TRACE_ERROR](state) {
+ [types.RECEIVE_JOB_LOGS_ERROR](state) {
state.detailJob.isLoading = false;
},
- [types.RECEIVE_JOB_TRACE_SUCCESS](state, data) {
+ [types.RECEIVE_JOB_LOGS_SUCCESS](state, data) {
state.detailJob.isLoading = false;
state.detailJob.output = data.html;
},
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
index a6caca2d2dc..95716e0a0c6 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const normalizeJob = job => ({
id: job.id,
name: job.name,
diff --git a/app/assets/javascripts/ide/stores/modules/router/actions.js b/app/assets/javascripts/ide/stores/modules/router/actions.js
index 849067599f2..321ac57817f 100644
--- a/app/assets/javascripts/ide/stores/modules/router/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/router/actions.js
@@ -1,6 +1,5 @@
import * as types from './mutation_types';
-// eslint-disable-next-line import/prefer-default-export
export const push = ({ commit }, fullPath) => {
commit(types.PUSH, fullPath);
};
diff --git a/app/assets/javascripts/ide/stores/modules/router/mutation_types.js b/app/assets/javascripts/ide/stores/modules/router/mutation_types.js
index ae99073cc4c..8f5f949bd5f 100644
--- a/app/assets/javascripts/ide/stores/modules/router/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/router/mutation_types.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const PUSH = 'PUSH';
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/getters.js b/app/assets/javascripts/ide/stores/modules/terminal/getters.js
index ef98547ccc4..b29d391845d 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/getters.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const allCheck = state => {
const checks = Object.values(state.checks);
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index c64839e5019..460d3ced381 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -7,7 +7,6 @@ import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
import {
sortTree,
- replaceFileUrl,
swapInParentTreeWithSorting,
updateFileCollections,
removeFromParentTree,
@@ -49,7 +48,7 @@ export default {
entries,
});
},
- [types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) {
+ [types.CREATE_TMP_ENTRY](state, { data }) {
Object.keys(data.entries).reduce((acc, key) => {
const entry = data.entries[key];
const foundEntry = state.entries[key];
@@ -72,13 +71,12 @@ export default {
return acc.concat(key);
}, []);
- const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find(
- e => e.path === data.treeList[0].path,
- );
+ const currentTree = state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
+ const foundEntry = currentTree.tree.find(e => e.path === data.treeList[0].path);
if (!foundEntry) {
- Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)),
+ Object.assign(currentTree, {
+ tree: sortTree(currentTree.tree.concat(data.treeList)),
});
}
},
@@ -139,7 +137,6 @@ export default {
prevId: undefined,
prevPath: undefined,
prevName: undefined,
- prevUrl: undefined,
prevKey: undefined,
prevParentPath: undefined,
});
@@ -195,9 +192,6 @@ export default {
const oldEntry = state.entries[path];
const newPath = parentPath ? `${parentPath}/${name}` : name;
const isRevert = newPath === oldEntry.prevPath;
-
- const newUrl = replaceFileUrl(oldEntry.url, oldEntry.path, newPath);
-
const newKey = oldEntry.key.replace(new RegExp(oldEntry.path, 'g'), newPath);
const baseProps = {
@@ -205,7 +199,6 @@ export default {
name,
id: newPath,
path: newPath,
- url: newUrl,
key: newKey,
parentPath: parentPath || '',
};
@@ -216,7 +209,6 @@ export default {
prevId: undefined,
prevPath: undefined,
prevName: undefined,
- prevUrl: undefined,
prevKey: undefined,
prevParentPath: undefined,
}
@@ -224,7 +216,6 @@ export default {
prevId: oldEntry.prevId || oldEntry.id,
prevPath: oldEntry.prevPath || oldEntry.path,
prevName: oldEntry.prevName || oldEntry.name,
- prevUrl: oldEntry.prevUrl || oldEntry.url,
prevKey: oldEntry.prevKey || oldEntry.key,
prevParentPath: oldEntry.prevParentPath || oldEntry.parentPath,
};
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index f074e6880d0..d9cdc7727ad 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -12,10 +12,7 @@ export const dataStructure = () => ({
// it can also contain a prefix `pending-` for files opened in review mode
key: '',
type: '',
- projectId: '',
- branchId: '',
name: '',
- url: '',
path: '',
tempFile: false,
tree: [],
@@ -26,7 +23,6 @@ export const dataStructure = () => ({
staged: false,
lastCommitSha: '',
rawPath: '',
- binary: false,
raw: '',
content: '',
editorRow: 1,
@@ -44,10 +40,7 @@ export const dataStructure = () => ({
export const decorateData = entity => {
const {
id,
- projectId,
- branchId,
type,
- url,
name,
path,
content = '',
@@ -55,7 +48,6 @@ export const decorateData = entity => {
active = false,
opened = false,
changed = false,
- binary = false,
rawPath = '',
file_lock,
parentPath = '',
@@ -63,19 +55,15 @@ export const decorateData = entity => {
return Object.assign(dataStructure(), {
id,
- projectId,
- branchId,
key: `${name}-${type}-${id}`,
type,
name,
- url,
path,
tempFile,
opened,
active,
changed,
content,
- binary,
rawPath,
file_lock,
parentPath,
@@ -189,11 +177,6 @@ export const mergeTrees = (fromTree, toTree) => {
return toTree;
};
-export const replaceFileUrl = (url, oldPath, newPath) => {
- // Add `/-/` so that we don't accidentally replace project path
- return url.replace(`/-/${oldPath}`, `/-/${newPath}`);
-};
-
export const swapInStateArray = (state, arr, key, entryPath) =>
Object.assign(state, {
[arr]: state[arr].map(f => (f.key === key ? state.entries[entryPath] : f)),
diff --git a/app/assets/javascripts/ide/sync_router_and_store.js b/app/assets/javascripts/ide/sync_router_and_store.js
index 1782c32b3b2..b33bcbb94ea 100644
--- a/app/assets/javascripts/ide/sync_router_and_store.js
+++ b/app/assets/javascripts/ide/sync_router_and_store.js
@@ -1,4 +1,3 @@
-/* eslint-disable import/prefer-default-export */
/**
* This method adds listeners to the given router and store and syncs their state with eachother
*
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index 58a6712c232..cde53e1ef00 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,5 +1,5 @@
import { languages } from 'monaco-editor';
-import { flatten } from 'lodash';
+import { flatten, isString } from 'lodash';
import { SIDE_LEFT, SIDE_RIGHT } from './constants';
const toLowerCase = x => x.toLowerCase();
@@ -42,15 +42,16 @@ const KNOWN_TYPES = [
},
];
-export function isTextFile(content, mimeType, fileName) {
- const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, fileName));
+export function isTextFile({ name, content, mimeType = '' }) {
+ const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, name));
if (knownType) return knownType.isText;
// does the string contain ascii characters only (ranges from space to tilde, tabs and new lines)
const asciiRegex = /^[ -~\t\n\r]+$/;
+
// for unknown types, determine the type by evaluating the file contents
- return asciiRegex.test(content);
+ return isString(content) && (content === '' || asciiRegex.test(content));
}
export const createPathWithExt = p => {
@@ -75,17 +76,17 @@ export function registerLanguages(def, ...defs) {
languages.setLanguageConfiguration(languageId, def.conf);
}
-export function registerSchemas({ language, options }, ...schemas) {
- schemas.forEach(schema => registerSchemas(schema));
-
- const defaults = {
- json: languages.json.jsonDefaults,
- yaml: languages.yaml.yamlDefaults,
- };
-
- if (defaults[language]) {
- defaults[language].setDiagnosticsOptions(options);
- }
+export function registerSchema(schema) {
+ const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults];
+ defaults.forEach(d =>
+ d.setDiagnosticsOptions({
+ validate: true,
+ enableSchemaRequest: true,
+ hover: true,
+ completion: true,
+ schemas: [schema],
+ }),
+ );
}
export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
index 72fdaca7e24..96100e4ac0c 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -1,27 +1,17 @@
<script>
-import { throttle } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
-import ImportedProjectTableRow from './imported_project_table_row.vue';
+import { GlButton, GlLoadingIcon, GlIntersectionObserver, GlModal } from '@gitlab/ui';
+import { n__, __, sprintf } from '~/locale';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
-import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue';
-import PageQueryParamSync from './page_query_param_sync.vue';
-import { isProjectImportable } from '../utils';
-
-const reposFetchThrottleDelay = 1000;
export default {
name: 'ImportProjectsTable',
components: {
- ImportedProjectTableRow,
ProviderRepoTableRow,
- IncompatibleRepoTableRow,
- PageQueryParamSync,
GlLoadingIcon,
GlButton,
- PaginationLinks,
+ GlModal,
+ GlIntersectionObserver,
},
props: {
providerTitle: {
@@ -41,14 +31,19 @@ export default {
},
computed: {
- ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
+ ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace']),
...mapGetters([
'isLoading',
'isImportingAnyRepo',
'hasImportableRepos',
'hasIncompatibleRepos',
+ 'importAllCount',
]),
+ pagePaginationStateKey() {
+ return `${this.filter}-${this.repositories.length}`;
+ },
+
availableNamespaces() {
const serializedNamespaces = this.namespaces.map(({ fullPath }) => ({
id: fullPath,
@@ -66,8 +61,12 @@ export default {
importAllButtonText() {
return this.hasIncompatibleRepos
- ? __('Import all compatible repositories')
- : __('Import all repositories');
+ ? n__(
+ 'Import %d compatible repository',
+ 'Import %d compatible repositories',
+ this.importAllCount,
+ )
+ : n__('Import %d repository', 'Import %d repositories', this.importAllCount);
},
emptyStateText() {
@@ -83,7 +82,11 @@ export default {
mounted() {
this.fetchNamespaces();
- this.fetchRepos();
+ this.fetchJobs();
+
+ if (!this.paginatable) {
+ this.fetchRepos();
+ }
},
beforeDestroy() {
@@ -94,105 +97,95 @@ export default {
methods: {
...mapActions([
'fetchRepos',
+ 'fetchJobs',
'fetchNamespaces',
'stopJobsPolling',
'clearJobsEtagPoll',
'setFilter',
'importAll',
- 'setPage',
]),
-
- handleFilterInput({ target }) {
- this.setFilter(target.value);
- },
-
- throttledFetchRepos: throttle(function fetch() {
- this.fetchRepos();
- }, reposFetchThrottleDelay),
-
- isProjectImportable,
},
};
</script>
<template>
<div>
- <page-query-param-sync :page="pageInfo.page" @popstate="setPage" />
-
<p class="light text-nowrap mt-2">
- {{ s__('ImportProjects|Select the projects you want to import') }}
+ {{ s__('ImportProjects|Select the repositories you want to import') }}
</p>
<template v-if="hasIncompatibleRepos">
<slot name="incompatible-repos-warning"></slot>
</template>
+ <div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
+ <gl-button
+ variant="success"
+ :loading="isImportingAnyRepo"
+ :disabled="!hasImportableRepos"
+ type="button"
+ @click="$refs.importAllModal.show()"
+ >{{ importAllButtonText }}</gl-button
+ >
+ <gl-modal
+ ref="importAllModal"
+ modal-id="import-all-modal"
+ :title="s__('ImportProjects|Import repositories')"
+ :ok-title="__('Import')"
+ @ok="importAll"
+ >
+ {{
+ n__(
+ 'Are you sure you want to import %d repository?',
+ 'Are you sure you want to import %d repositories?',
+ importAllCount,
+ )
+ }}
+ </gl-modal>
+
+ <slot name="actions"></slot>
+ <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
+ <input
+ data-qa-selector="githubish_import_filter_field"
+ class="form-control"
+ name="filter"
+ :placeholder="__('Filter your repositories by name')"
+ autofocus
+ size="40"
+ @keyup.enter="setFilter($event.target.value)"
+ />
+ </form>
+ </div>
+ <div v-if="repositories.length" class="table-responsive">
+ <table class="table import-table">
+ <thead>
+ <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
+ <th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
+ <th class="import-jobs-status-col">{{ __('Status') }}</th>
+ <th class="import-jobs-cta-col"></th>
+ </thead>
+ <tbody>
+ <template v-for="repo in repositories">
+ <provider-repo-table-row
+ :key="repo.importSource.providerLink"
+ :repo="repo"
+ :available-namespaces="availableNamespaces"
+ />
+ </template>
+ </tbody>
+ </table>
+ </div>
+ <gl-intersection-observer
+ v-if="paginatable"
+ :key="pagePaginationStateKey"
+ @appear="fetchRepos"
+ />
<gl-loading-icon
v-if="isLoading"
class="js-loading-button-icon import-projects-loading-icon"
size="md"
/>
- <template v-if="!isLoading">
- <div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
- <gl-button
- variant="success"
- :loading="isImportingAnyRepo"
- :disabled="!hasImportableRepos"
- type="button"
- @click="importAll"
- >{{ importAllButtonText }}</gl-button
- >
- <slot name="actions"></slot>
- <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
- <input
- :value="filter"
- data-qa-selector="githubish_import_filter_field"
- class="form-control"
- name="filter"
- :placeholder="__('Filter your projects by name')"
- autofocus
- size="40"
- @input="handleFilterInput($event)"
- @keyup.enter="throttledFetchRepos"
- />
- </form>
- </div>
- <div v-if="repositories.length" class="table-responsive">
- <table class="table import-table">
- <thead>
- <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
- <th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
- <th class="import-jobs-status-col">{{ __('Status') }}</th>
- <th class="import-jobs-cta-col"></th>
- </thead>
- <tbody>
- <template v-for="repo in repositories">
- <incompatible-repo-table-row
- v-if="repo.importSource.incompatible"
- :key="repo.importSource.id"
- :repo="repo"
- />
- <provider-repo-table-row
- v-else-if="isProjectImportable(repo)"
- :key="repo.importSource.id"
- :repo="repo"
- :available-namespaces="availableNamespaces"
- />
- <imported-project-table-row v-else :key="repo.importSource.id" :project="repo" />
- </template>
- </tbody>
- </table>
- </div>
- <div v-else class="text-center">
- <strong>{{ emptyStateText }}</strong>
- </div>
- <pagination-links
- v-if="paginatable"
- align="center"
- class="gl-mt-3"
- :page-info="pageInfo"
- :prev-page="pageInfo.page - 1"
- :next-page="repositories.length && pageInfo.page + 1"
- :change="setPage"
- />
- </template>
+
+ <div v-if="!isLoading && repositories.length === 0" class="text-center">
+ <strong>{{ emptyStateText }}</strong>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
deleted file mode 100644
index 50e735b4478..00000000000
--- a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import ImportStatus from './import_status.vue';
-import { STATUSES } from '../constants';
-
-export default {
- name: 'ImportedProjectTableRow',
- components: {
- ImportStatus,
- GlIcon,
- },
- props: {
- project: {
- type: Object,
- required: true,
- },
- },
-
- computed: {
- displayFullPath() {
- return this.project.importedProject.fullPath.replace(/^\//, '');
- },
-
- isFinished() {
- return this.project.importStatus === STATUSES.FINISHED;
- },
- },
-};
-</script>
-
-<template>
- <tr class="import-row">
- <td>
- <a
- :href="project.importSource.providerLink"
- rel="noreferrer noopener"
- target="_blank"
- data-testid="providerLink"
- >{{ project.importSource.fullName }}
- <gl-icon v-if="project.importSource.providerLink" name="external-link" />
- </a>
- </td>
- <td data-testid="fullPath">{{ displayFullPath }}</td>
- <td>
- <import-status :status="project.importStatus" />
- </td>
- <td>
- <a
- v-if="isFinished"
- class="btn btn-default"
- data-testid="goToProject"
- :href="project.importedProject.fullPath"
- rel="noreferrer noopener"
- target="_blank"
- >{{ __('Go to project') }}
- </a>
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
deleted file mode 100644
index 3140585ccd7..00000000000
--- a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
-import { GlIcon, GlBadge } from '@gitlab/ui';
-
-export default {
- components: {
- GlBadge,
- GlIcon,
- },
- props: {
- repo: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <tr class="import-row">
- <td>
- <a :href="repo.importSource.providerLink" rel="noreferrer noopener" target="_blank"
- >{{ repo.importSource.fullName }}
- <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
- </a>
- </td>
- <td></td>
- <td></td>
- <td>
- <gl-badge variant="danger">{{ __('Incompatible project') }}</gl-badge>
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/import_projects/components/page_query_param_sync.vue b/app/assets/javascripts/import_projects/components/page_query_param_sync.vue
deleted file mode 100644
index 5ba3d70f5d0..00000000000
--- a/app/assets/javascripts/import_projects/components/page_query_param_sync.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<script>
-import { queryToObject, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
-
-export default {
- props: {
- page: {
- type: Number,
- required: true,
- },
- },
-
- watch: {
- page(newPage) {
- updateHistory({
- url: setUrlParams({
- page: newPage === 1 ? null : newPage,
- }),
- });
- },
- },
-
- created() {
- window.addEventListener('popstate', this.updatePage);
- },
-
- beforeDestroy() {
- window.removeEventListener('popstate', this.updatePage);
- },
-
- methods: {
- updatePage() {
- const page = parseInt(queryToObject(window.location.search).page, 10) || 1;
- this.$emit('popstate', page);
- },
- },
-
- render: () => null,
-};
-</script>
diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
index d8cffc6a7d5..18971313dfe 100644
--- a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
@@ -1,9 +1,11 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlBadge } from '@gitlab/ui';
import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale';
import ImportStatus from './import_status.vue';
+import { STATUSES } from '../constants';
+import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
export default {
name: 'ProviderRepoTableRow',
@@ -11,6 +13,7 @@ export default {
Select2Select,
ImportStatus,
GlIcon,
+ GlBadge,
},
props: {
repo: {
@@ -27,6 +30,26 @@ export default {
...mapState(['ciCdOnly']),
...mapGetters(['getImportTarget']),
+ displayFullPath() {
+ return this.repo.importedProject.fullPath.replace(/^\//, '');
+ },
+
+ isFinished() {
+ return this.repo.importedProject?.importStatus === STATUSES.FINISHED;
+ },
+
+ isImportNotStarted() {
+ return isProjectImportable(this.repo);
+ },
+
+ isIncompatible() {
+ return isIncompatible(this.repo);
+ },
+
+ importStatus() {
+ return getImportStatus(this.repo);
+ },
+
importTarget() {
return this.getImportTarget(this.repo.importSource.id);
},
@@ -85,9 +108,9 @@ export default {
<gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</a>
</td>
- <td class="d-flex flex-wrap flex-lg-nowrap">
- <template v-if="repo.target">{{ repo.target }}</template>
- <template v-else>
+ <td class="d-flex flex-wrap flex-lg-nowrap" data-testid="fullPath">
+ <template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
+ <template v-else-if="isImportNotStarted">
<select2-select v-model="targetNamespaceSelect" :options="select2Options" />
<span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
>/</span
@@ -98,18 +121,31 @@ export default {
class="form-control import-project-name-input qa-project-path-field"
/>
</template>
+ <template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</td>
<td>
- <import-status :status="repo.importStatus" />
+ <import-status :status="importStatus" />
</td>
- <td>
+ <td data-testid="actions">
+ <a
+ v-if="isFinished"
+ class="btn btn-default"
+ :href="repo.importedProject.fullPath"
+ rel="noreferrer noopener"
+ target="_blank"
+ >{{ __('Go to project') }}
+ </a>
<button
+ v-if="isImportNotStarted"
type="button"
class="qa-import-button btn btn-default"
@click="fetchImport(repo.importSource.id)"
>
{{ importButtonText }}
</button>
+ <gl-badge v-else-if="isIncompatible" variant="danger">{{
+ __('Incompatible project')
+ }}</gl-badge>
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js
index af410f411d8..7b70d290278 100644
--- a/app/assets/javascripts/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_projects/store/actions.js
@@ -1,11 +1,7 @@
import Visibility from 'visibilityjs';
import * as types from './mutation_types';
import { isProjectImportable } from '../utils';
-import {
- convertObjectPropsToCamelCase,
- normalizeHeaders,
- parseIntPagination,
-} from '~/lib/utils/common_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as createFlash } from '~/flash';
@@ -54,12 +50,9 @@ const importAll = ({ state, dispatch }) => {
);
};
-const fetchReposFactory = ({ reposPath = isRequired(), hasPagination }) => ({
- state,
- dispatch,
- commit,
-}) => {
- dispatch('stopJobsPolling');
+const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) => {
+ const nextPage = state.pageInfo.page + 1;
+ commit(types.SET_PAGE, nextPage);
commit(types.REQUEST_REPOS);
const { provider, filter } = state;
@@ -68,21 +61,16 @@ const fetchReposFactory = ({ reposPath = isRequired(), hasPagination }) => ({
.get(
pathWithParams({
path: reposPath,
- filter,
- page: hasPagination ? state.pageInfo.page.toString() : '',
+ filter: filter ?? '',
+ page: nextPage === 1 ? '' : nextPage.toString(),
}),
)
- .then(({ data, headers }) => {
- const normalizedHeaders = normalizeHeaders(headers);
-
- if ('X-PAGE' in normalizedHeaders) {
- commit(types.SET_PAGE_INFO, parseIntPagination(normalizedHeaders));
- }
-
+ .then(({ data }) => {
commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
})
- .then(() => dispatch('fetchJobs'))
.catch(e => {
+ commit(types.SET_PAGE, nextPage - 1);
+
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else {
@@ -136,8 +124,6 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett
};
export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, dispatch }) => {
- const { filter } = state;
-
if (eTagPoll) {
stopJobsPolling();
clearJobsEtagPoll();
@@ -145,7 +131,7 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
eTagPoll = new Poll({
resource: {
- fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter })),
+ fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter: state.filter })),
},
method: 'fetchJobs',
successCallback: ({ data }) =>
@@ -157,7 +143,6 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
createFlash(s__('ImportProjects|Update of imported projects with realtime changes failed'));
}
},
- data: { filter },
});
if (!Visibility.hidden()) {
@@ -196,7 +181,7 @@ const setPage = ({ state, commit, dispatch }, page) => {
return dispatch('fetchRepos');
};
-export default ({ endpoints = isRequired(), hasPagination }) => ({
+export default ({ endpoints = isRequired() }) => ({
clearJobsEtagPoll,
stopJobsPolling,
restartJobsPolling,
@@ -204,7 +189,7 @@ export default ({ endpoints = isRequired(), hasPagination }) => ({
setImportTarget,
importAll,
setPage,
- fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath, hasPagination }),
+ fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath }),
fetchImport: fetchImportFactory(endpoints.importPath),
fetchJobs: fetchJobsFactory(endpoints.jobsPath),
fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath),
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
index 7d529c94d7d..b76c52beea2 100644
--- a/app/assets/javascripts/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -1,17 +1,20 @@
import { STATUSES } from '../constants';
+import { isProjectImportable, isIncompatible } from '../utils';
export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces;
export const isImportingAnyRepo = state =>
state.repositories.some(repo =>
- [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(repo.importStatus),
+ [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(
+ repo.importedProject?.importStatus,
+ ),
);
-export const hasIncompatibleRepos = state =>
- state.repositories.some(repo => repo.importSource.incompatible);
+export const hasIncompatibleRepos = state => state.repositories.some(isIncompatible);
-export const hasImportableRepos = state =>
- state.repositories.some(repo => repo.importStatus === STATUSES.NONE);
+export const hasImportableRepos = state => state.repositories.some(isProjectImportable);
+
+export const importAllCount = state => state.repositories.filter(isProjectImportable).length;
export const getImportTarget = state => repoId => {
if (state.customImportTargets[repoId]) {
diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js
index b3dbef896a6..6999253d4b2 100644
--- a/app/assets/javascripts/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_projects/store/mutations.js
@@ -2,46 +2,88 @@ import Vue from 'vue';
import * as types from './mutation_types';
import { STATUSES } from '../constants';
+const makeNewImportedProject = importedProject => ({
+ importSource: {
+ id: importedProject.id,
+ fullName: importedProject.importSource,
+ sanitizedName: importedProject.name,
+ providerLink: importedProject.providerLink,
+ },
+ importedProject,
+});
+
+const makeNewIncompatibleProject = project => ({
+ importSource: { ...project, incompatible: true },
+ importedProject: null,
+});
+
+const processLegacyEntries = ({ newRepositories, existingRepositories, factory }) => {
+ const newEntries = [];
+ newRepositories.forEach(project => {
+ const existingProject = existingRepositories.find(p => p.importSource.id === project.id);
+ const importedProjectShape = factory(project);
+
+ if (existingProject) {
+ Object.assign(existingProject, importedProjectShape);
+ } else {
+ newEntries.push(importedProjectShape);
+ }
+ });
+ return newEntries;
+};
+
export default {
[types.SET_FILTER](state, filter) {
state.filter = filter;
+ state.repositories = [];
+ state.pageInfo.page = 0;
},
[types.REQUEST_REPOS](state) {
state.isLoadingRepos = true;
},
- [types.RECEIVE_REPOS_SUCCESS](
- state,
- { importedProjects, providerRepos, incompatibleRepos = [] },
- ) {
- // Normalizing structure to support legacy backend format
- // See https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091 for details
-
+ [types.RECEIVE_REPOS_SUCCESS](state, repositories) {
state.isLoadingRepos = false;
- state.repositories = [
- ...importedProjects.map(({ importSource, providerLink, importStatus, ...project }) => ({
- importSource: {
- id: `finished-${project.id}`,
- fullName: importSource,
- sanitizedName: project.name,
- providerLink,
- },
- importStatus,
- importedProject: project,
- })),
- ...providerRepos.map(project => ({
- importSource: project,
- importStatus: STATUSES.NONE,
- importedProject: null,
- })),
- ...incompatibleRepos.map(project => ({
- importSource: { ...project, incompatible: true },
- importStatus: STATUSES.NONE,
- importedProject: null,
- })),
- ];
+ if (!Array.isArray(repositories)) {
+ // Legacy code path, will be removed when all importers will be switched to new pagination format
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091
+
+ const newImportedProjects = processLegacyEntries({
+ newRepositories: repositories.importedProjects,
+ existingRepositories: state.repositories,
+ factory: makeNewImportedProject,
+ });
+
+ const incompatibleRepos = repositories.incompatibleRepos ?? [];
+ const newIncompatibleProjects = processLegacyEntries({
+ newRepositories: incompatibleRepos,
+ existingRepositories: state.repositories,
+ factory: makeNewIncompatibleProject,
+ });
+
+ state.repositories = [
+ ...newImportedProjects,
+ ...state.repositories,
+ ...repositories.providerRepos.map(project => ({
+ importSource: project,
+ importedProject: null,
+ })),
+ ...newIncompatibleProjects,
+ ];
+
+ if (incompatibleRepos.length === 0 && repositories.providerRepos.length === 0) {
+ state.pageInfo.page -= 1;
+ }
+
+ return;
+ }
+
+ state.repositories = [...state.repositories, ...repositories];
+ if (repositories.length === 0) {
+ state.pageInfo.page -= 1;
+ }
},
[types.RECEIVE_REPOS_ERROR](state) {
@@ -50,31 +92,27 @@ export default {
[types.REQUEST_IMPORT](state, { repoId, importTarget }) {
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
- existingRepo.importStatus = STATUSES.SCHEDULING;
existingRepo.importedProject = {
+ importStatus: STATUSES.SCHEDULING,
fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`,
};
},
[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
- const { importStatus, ...project } = importedProject;
-
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
- existingRepo.importStatus = importStatus;
- existingRepo.importedProject = project;
+ existingRepo.importedProject = importedProject;
},
[types.RECEIVE_IMPORT_ERROR](state, repoId) {
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
- existingRepo.importStatus = STATUSES.NONE;
existingRepo.importedProject = null;
},
[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {
updatedProjects.forEach(updatedProject => {
const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id);
- if (repo) {
- repo.importStatus = updatedProject.importStatus;
+ if (repo?.importedProject) {
+ repo.importedProject.importStatus = updatedProject.importStatus;
}
});
},
@@ -105,10 +143,6 @@ export default {
}
},
- [types.SET_PAGE_INFO](state, pageInfo) {
- state.pageInfo = pageInfo;
- },
-
[types.SET_PAGE](state, page) {
state.pageInfo.page = page;
},
diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js
index 3318181e4af..ecd93561d52 100644
--- a/app/assets/javascripts/import_projects/store/state.js
+++ b/app/assets/javascripts/import_projects/store/state.js
@@ -8,6 +8,6 @@ export default () => ({
ciCdOnly: false,
filter: '',
pageInfo: {
- page: 1,
+ page: 0,
},
});
diff --git a/app/assets/javascripts/import_projects/utils.js b/app/assets/javascripts/import_projects/utils.js
index c2a2d5a607d..695b12cbcba 100644
--- a/app/assets/javascripts/import_projects/utils.js
+++ b/app/assets/javascripts/import_projects/utils.js
@@ -1,7 +1,13 @@
import { STATUSES } from './constants';
-// Will be expanded in future
-// eslint-disable-next-line import/prefer-default-export
+export function isIncompatible(project) {
+ return project.importSource.incompatible;
+}
+
+export function getImportStatus(project) {
+ return project.importedProject?.importStatus ?? STATUSES.NONE;
+}
+
export function isProjectImportable(project) {
- return project.importStatus === STATUSES.NONE && !project.importSource.incompatible;
+ return !isIncompatible(project) && getImportStatus(project) === STATUSES.NONE;
}
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 46852e4ddd9..670c42cbdac 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -23,6 +23,8 @@ import { s__ } from '~/locale';
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
+import SeverityToken from '~/sidebar/components/severity/severity.vue';
+import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants';
const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
@@ -45,6 +47,12 @@ export default {
statusTabs: INCIDENT_STATUS_TABS,
fields: [
{
+ key: 'severity',
+ label: s__('IncidentManagement|Severity'),
+ thClass: `gl-pointer-events-none`,
+ tdClass,
+ },
+ {
key: 'title',
label: s__('IncidentManagement|Incident'),
thClass: `gl-pointer-events-none gl-w-half`,
@@ -82,6 +90,7 @@ export default {
PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'),
GlBadge,
GlEmptyState,
+ SeverityToken,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -208,6 +217,31 @@ export default {
isEmpty() {
return !this.incidents.list?.length;
},
+ showList() {
+ return !this.isEmpty || this.errored || this.loading;
+ },
+ activeClosedTabHasNoIncidents() {
+ const { all, closed } = this.incidentsCount || {};
+ const isClosedTabActive = this.statusFilter === this.$options.statusTabs[1].filters;
+
+ return isClosedTabActive && all && !closed;
+ },
+ emptyStateData() {
+ const {
+ emptyState: { title, emptyClosedTabTitle, description },
+ createIncidentBtnLabel,
+ } = this.$options.i18n;
+
+ if (this.activeClosedTabHasNoIncidents) {
+ return { title: emptyClosedTabTitle };
+ }
+ return {
+ title,
+ description,
+ btnLink: this.newIncidentPath,
+ btnText: createIncidentBtnLabel,
+ };
+ },
},
methods: {
onInputChange: debounce(function debounceSearch(input) {
@@ -255,6 +289,9 @@ export default {
this.sort = `${sortingColumn}_${sortingDirection}`;
},
+ getSeverity(severity) {
+ return INCIDENT_SEVERITY[severity];
+ },
},
};
</script>
@@ -279,7 +316,7 @@ export default {
</gl-tabs>
<gl-button
- v-if="!isEmpty"
+ v-if="!isEmpty || activeClosedTabHasNoIncidents"
class="gl-my-3 gl-mr-5 create-incident-button"
data-testid="createIncidentBtn"
data-qa-selector="create_incident_button"
@@ -307,6 +344,7 @@ export default {
{{ s__('IncidentManagement|Incidents') }}
</h4>
<gl-table
+ v-if="showList"
:items="incidents.list || []"
:fields="availableFields"
:show-empty="true"
@@ -322,6 +360,10 @@ export default {
@row-clicked="navigateToIncidentDetails"
@sort-changed="fetchSortedData"
>
+ <template #cell(severity)="{ item }">
+ <severity-token :severity="getSeverity(item.severity)" />
+ </template>
+
<template #cell(title)="{ item }">
<div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }">
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
@@ -379,21 +421,20 @@ export default {
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
- <template #empty>
- <gl-empty-state
- v-if="!errored"
- :title="$options.i18n.emptyState.title"
- :svg-path="emptyListSvgPath"
- :description="$options.i18n.emptyState.description"
- :primary-button-link="newIncidentPath"
- :primary-button-text="$options.i18n.createIncidentBtnLabel"
- />
- <span v-else>
- {{ $options.i18n.noIncidents }}
- </span>
+ <template v-if="errored" #empty>
+ {{ $options.i18n.noIncidents }}
</template>
</gl-table>
+ <gl-empty-state
+ v-else
+ :title="emptyStateData.title"
+ :svg-path="emptyListSvgPath"
+ :description="emptyStateData.description"
+ :primary-button-link="emptyStateData.btnLink"
+ :primary-button-text="emptyStateData.btnText"
+ />
+
<gl-pagination
v-if="showPaginationControls"
:value="pagination.currentPage"
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index 02d8172533d..289b36d9848 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -9,6 +9,7 @@ export const I18N = {
searchPlaceholder: __('Search results…'),
emptyState: {
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
+ emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
description: s__(
'IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below.',
),
@@ -34,4 +35,4 @@ export const INCIDENT_STATUS_TABS = [
];
export const INCIDENT_SEARCH_DELAY = 300;
-export const DEFAULT_PAGE_SIZE = 10;
+export const DEFAULT_PAGE_SIZE = 20;
diff --git a/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
new file mode 100644
index 00000000000..eb2dde14464
--- /dev/null
+++ b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
@@ -0,0 +1,3 @@
+fragment IncidentFields on Issue {
+ severity
+}
diff --git a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
index 0f56e8640bd..dab130835e2 100644
--- a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
+++ b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
@@ -1,3 +1,5 @@
+#import "ee_else_ce/incidents/graphql/fragments/incident_fields.fragment.graphql"
+
query getIncidents(
$projectPath: ID!
$issueTypes: [IssueType!]
@@ -39,7 +41,7 @@ query getIncidents(
webUrl
}
}
- statusPagePublishedIncident
+ ...IncidentFields
}
pageInfo {
hasNextPage
diff --git a/app/assets/javascripts/incidents_settings/components/alerts_form.vue b/app/assets/javascripts/incidents_settings/components/alerts_form.vue
index 5872ac39c96..17a77f650e0 100644
--- a/app/assets/javascripts/incidents_settings/components/alerts_form.vue
+++ b/app/assets/javascripts/incidents_settings/components/alerts_form.vue
@@ -6,8 +6,8 @@ import {
GlIcon,
GlFormGroup,
GlFormCheckbox,
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
} from '@gitlab/ui';
import {
I18N_ALERT_SETTINGS_FORM,
@@ -24,8 +24,8 @@ export default {
GlFormGroup,
GlIcon,
GlFormCheckbox,
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
},
inject: ['service', 'alertSettings'],
data() {
@@ -34,6 +34,7 @@ export default {
createIssueEnabled: this.alertSettings.createIssue,
issueTemplate: this.alertSettings.issueTemplateKey,
sendEmailEnabled: this.alertSettings.sendEmail,
+ autoCloseIncident: this.alertSettings.autoCloseIncident,
loading: false,
};
},
@@ -49,6 +50,7 @@ export default {
create_issue: this.createIssueEnabled,
issue_template_key: this.issueTemplate,
send_email: this.sendEmailEnabled,
+ auto_close_incident: this.autoCloseIncident,
};
},
},
@@ -99,13 +101,13 @@ export default {
<gl-icon name="question" :size="12" />
</gl-link>
</label>
- <gl-new-dropdown
+ <gl-dropdown
id="alert-integration-settings-issue-template"
data-qa-selector="incident_templates_dropdown"
:text="issueTemplateHeader"
:block="true"
>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="template in templates"
:key="template.key"
data-qa-selector="incident_templates_item"
@@ -114,8 +116,8 @@ export default {
@click="selectIssueTemplate(template.key)"
>
{{ template.name }}
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</gl-form-group>
<gl-form-group class="gl-pl-0 gl-mb-5">
@@ -123,6 +125,11 @@ export default {
<span>{{ $options.i18n.sendEmail.label }}</span>
</gl-form-checkbox>
</gl-form-group>
+ <gl-form-group class="gl-pl-0 gl-mb-5">
+ <gl-form-checkbox v-model="autoCloseIncident">
+ <span>{{ $options.i18n.autoCloseIncidents.label }}</span>
+ </gl-form-checkbox>
+ </gl-form-group>
<div class="gl-display-flex gl-justify-content-end">
<gl-button
ref="submitBtn"
diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js
index 77f7ee2c4a3..42f1f645d16 100644
--- a/app/assets/javascripts/incidents_settings/constants.js
+++ b/app/assets/javascripts/incidents_settings/constants.js
@@ -42,6 +42,9 @@ export const I18N_ALERT_SETTINGS_FORM = {
sendEmail: {
label: __('Send a separate email notification to Developers.'),
},
+ autoCloseIncidents: {
+ label: __('Automatically close incident issues when the associated Prometheus alert resolves.'),
+ },
};
export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selected') };
diff --git a/app/assets/javascripts/incidents_settings/index.js b/app/assets/javascripts/incidents_settings/index.js
index 80e7d07feca..ad875d49768 100644
--- a/app/assets/javascripts/incidents_settings/index.js
+++ b/app/assets/javascripts/incidents_settings/index.js
@@ -20,6 +20,7 @@ export default () => {
pagerdutyActive,
pagerdutyWebhookUrl,
pagerdutyResetKeyPath,
+ autoCloseIncident,
},
} = el;
@@ -33,6 +34,7 @@ export default () => {
createIssue: parseBoolean(createIssue),
issueTemplateKey,
sendEmail: parseBoolean(sendEmail),
+ autoCloseIncident: parseBoolean(autoCloseIncident),
},
pagerDutySettings: {
active: parseBoolean(pagerdutyActive),
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
index e708e5d0978..c0dc6ce07b1 100644
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -1,10 +1,11 @@
import $ from 'jquery';
import { stickyMonitor } from './lib/utils/sticky';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default stickyTop => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
- $('.js-diff-stats-dropdown').glDropdown({
+ initDeprecatedJQueryDropdown($('.js-diff-stats-dropdown'), {
filterable: true,
remoteFilter: false,
});
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index e61b37a2d1f..528d5d8072f 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -4,8 +4,8 @@ import MilestoneSelect from './milestone_select';
import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context';
import Sidebar from './right_sidebar';
-
import DueDateSelectors from './due_date_select';
+import { mountSidebarLabels } from '~/sidebar/mount_sidebar';
export default () => {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
@@ -17,4 +17,6 @@ export default () => {
new IssuableContext(sidebarOptions.currentUser);
new DueDateSelectors();
Sidebar.initialize();
+
+ mountSidebarLabels();
};
diff --git a/app/assets/javascripts/integrations/edit/components/active_toggle.vue b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
index e6a96600539..6698984d02f 100644
--- a/app/assets/javascripts/integrations/edit/components/active_toggle.vue
+++ b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
@@ -1,36 +1,31 @@
<script>
import { mapGetters } from 'vuex';
-import { GlFormGroup, GlToggle } from '@gitlab/ui';
+import { GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
import eventHub from '../event_hub';
export default {
- name: 'ActiveToggle',
+ name: 'ActiveCheckbox',
components: {
GlFormGroup,
- GlToggle,
- },
- props: {
- initialActivated: {
- type: Boolean,
- required: true,
- },
+ GlFormCheckbox,
},
data() {
return {
- activated: this.initialActivated,
+ activated: false,
};
},
computed: {
- ...mapGetters(['isInheriting']),
+ ...mapGetters(['isInheriting', 'propsSource']),
},
mounted() {
+ this.activated = this.propsSource.initialActivated;
// Initialize view
this.$nextTick(() => {
- this.onToggle(this.activated);
+ this.onChange(this.activated);
});
},
methods: {
- onToggle(e) {
+ onChange(e) {
eventHub.$emit('toggle', e);
},
},
@@ -39,12 +34,15 @@ export default {
<template>
<gl-form-group :label="__('Enable integration')" label-for="service[active]">
- <gl-toggle
+ <input name="service[active]" type="hidden" :value="activated || false" />
+ <gl-form-checkbox
v-model="activated"
name="service[active]"
class="gl-display-block gl-line-height-0"
:disabled="isInheriting"
- @change="onToggle"
- />
+ @change="onChange"
+ >
+ {{ __('Active') }}
+ </gl-form-checkbox>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index 090381b8da4..9dde1ed1055 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapGetters } from 'vuex';
import { capitalize, lowerCase, isEmpty } from 'lodash';
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 5088664c3bd..0460ed6791e 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,9 +1,11 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
+import { GlButton } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import eventHub from '../event_hub';
import OverrideDropdown from './override_dropdown.vue';
-import ActiveToggle from './active_toggle.vue';
+import ActiveCheckbox from './active_checkbox.vue';
import JiraTriggerFields from './jira_trigger_fields.vue';
import JiraIssuesFields from './jira_issues_fields.vue';
import TriggerFields from './trigger_fields.vue';
@@ -13,16 +15,20 @@ export default {
name: 'IntegrationForm',
components: {
OverrideDropdown,
- ActiveToggle,
+ ActiveCheckbox,
JiraTriggerFields,
JiraIssuesFields,
TriggerFields,
DynamicField,
+ GlButton,
},
mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapGetters(['currentKey', 'propsSource']),
- ...mapState(['adminState', 'override']),
+ ...mapGetters(['currentKey', 'propsSource', 'isSavingOrTesting']),
+ ...mapState(['defaultState', 'override', 'isSaving', 'isTesting']),
+ isEditable() {
+ return this.propsSource.editable;
+ },
isJira() {
return this.propsSource.type === 'jira';
},
@@ -31,7 +37,15 @@ export default {
},
},
methods: {
- ...mapActions(['setOverride']),
+ ...mapActions(['setOverride', 'setIsSaving', 'setIsTesting']),
+ onSaveClick() {
+ this.setIsSaving(true);
+ eventHub.$emit('saveIntegration');
+ },
+ onTestClick() {
+ this.setIsTesting(true);
+ eventHub.$emit('testIntegration');
+ },
},
};
</script>
@@ -39,16 +53,13 @@ export default {
<template>
<div>
<override-dropdown
- v-if="adminState !== null"
- :inherit-from-id="adminState.id"
+ v-if="defaultState !== null"
+ :inherit-from-id="defaultState.id"
:override="override"
+ :learn-more-path="propsSource.learnMorePath"
@change="setOverride"
/>
- <active-toggle
- v-if="propsSource.showActive"
- :key="`${currentKey}-active-toggle`"
- v-bind="propsSource.activeToggleProps"
- />
+ <active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" />
<jira-trigger-fields
v-if="isJira"
:key="`${currentKey}-jira-trigger-fields`"
@@ -70,5 +81,29 @@ export default {
:key="`${currentKey}-jira-issues-fields`"
v-bind="propsSource.jiraIssuesProps"
/>
+ <div v-if="isEditable" class="footer-block row-content-block">
+ <gl-button
+ category="primary"
+ variant="success"
+ type="submit"
+ :loading="isSaving"
+ :disabled="isSavingOrTesting"
+ data-qa-selector="save_changes_button"
+ @click.prevent="onSaveClick"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <gl-button
+ v-if="propsSource.canTest"
+ :loading="isTesting"
+ :disabled="isSavingOrTesting"
+ :href="propsSource.testPath"
+ @click.prevent="onTestClick"
+ >
+ {{ __('Test settings') }}
+ </gl-button>
+
+ <gl-button class="btn-cancel" :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 5a1f86718b0..1baa2b440b0 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -37,6 +37,11 @@ export default {
required: false,
default: null,
},
+ gitlabIssuesEnabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
upgradePlanPath: {
type: String,
required: false,
@@ -133,7 +138,7 @@ export default {
:disabled="!enableJiraIssues"
/>
</gl-form-group>
- <p>
+ <p v-if="gitlabIssuesEnabled">
<gl-sprintf
:message="
s__(
diff --git a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
index accfc26974c..c31dada8d2f 100644
--- a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
+++ b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
@@ -1,6 +1,8 @@
<script>
-import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { defaultIntegrationLevel, overrideDropdownDescriptions } from '../constants';
const dropdownOptions = [
{
@@ -17,14 +19,20 @@ export default {
dropdownOptions,
name: 'OverrideDropdown',
components: {
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
+ GlLink,
},
props: {
inheritFromId: {
type: Number,
required: true,
},
+ learnMorePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
override: {
type: Boolean,
required: true,
@@ -35,6 +43,16 @@ export default {
selected: dropdownOptions.find(x => x.value === this.override),
};
},
+ computed: {
+ ...mapState(['defaultState']),
+ description() {
+ const level = this.defaultState.integrationLevel;
+
+ return (
+ overrideDropdownDescriptions[level] || overrideDropdownDescriptions[defaultIntegrationLevel]
+ );
+ },
+ },
methods: {
onClick(option) {
this.selected = option;
@@ -48,16 +66,21 @@ export default {
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-py-4 gl-mt-5 gl-mb-6 gl-border-t-1 gl-border-t-solid gl-border-b-1 gl-border-b-solid gl-border-gray-100"
>
- <span>{{ s__('Integrations|Default settings are inherited from the instance level.') }}</span>
+ <span
+ >{{ description }}
+ <gl-link v-if="learnMorePath" :href="learnMorePath" target="_blank">{{
+ __('Learn more')
+ }}</gl-link>
+ </span>
<input name="service[inherit_from_id]" :value="override ? '' : inheritFromId" type="hidden" />
- <gl-new-dropdown :text="selected.text">
- <gl-new-dropdown-item
+ <gl-dropdown :text="selected.text">
+ <gl-dropdown-item
v-for="option in $options.dropdownOptions"
:key="option.value"
@click="onClick(option)"
>
{{ option.text }}
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/integrations/edit/constants.js b/app/assets/javascripts/integrations/edit/constants.js
new file mode 100644
index 00000000000..b74ae209eb7
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/constants.js
@@ -0,0 +1,17 @@
+import { s__ } from '~/locale';
+
+export const integrationLevels = {
+ GROUP: 'group',
+ INSTANCE: 'instance',
+};
+
+export const defaultIntegrationLevel = integrationLevels.INSTANCE;
+
+export const overrideDropdownDescriptions = {
+ [integrationLevels.GROUP]: s__(
+ 'Integrations|Default settings are inherited from the group level.',
+ ),
+ [integrationLevels.INSTANCE]: s__(
+ 'Integrations|Default settings are inherited from the instance level.',
+ ),
+};
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index ea5463832ce..248ee62d43a 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -19,27 +19,36 @@ function parseDatasetToProps(data) {
projectKey,
upgradePlanPath,
editProjectPath,
+ learnMorePath,
triggerEvents,
fields,
inheritFromId,
+ integrationLevel,
+ cancelPath,
+ testPath,
...booleanAttributes
} = data;
const {
showActive,
activated,
+ editable,
+ canTest,
commitEvents,
mergeRequestEvents,
enableComments,
showJiraIssuesIntegration,
enableJiraIssues,
+ gitlabIssuesEnabled,
} = parseBooleanInData(booleanAttributes);
return {
- activeToggleProps: {
- initialActivated: activated,
- },
+ initialActivated: activated,
showActive,
type,
+ cancelPath,
+ editable,
+ canTest,
+ testPath,
triggerFieldsProps: {
initialTriggerCommit: commitEvents,
initialTriggerMergeRequest: mergeRequestEvents,
@@ -50,17 +59,20 @@ function parseDatasetToProps(data) {
showJiraIssuesIntegration,
initialEnableJiraIssues: enableJiraIssues,
initialProjectKey: projectKey,
+ gitlabIssuesEnabled,
upgradePlanPath,
editProjectPath,
},
+ learnMorePath,
triggerEvents: JSON.parse(triggerEvents),
fields: JSON.parse(fields),
inheritFromId: parseInt(inheritFromId, 10),
+ integrationLevel,
id: parseInt(id, 10),
};
}
-export default (el, adminEl) => {
+export default (el, defaultEl) => {
if (!el) {
return null;
}
@@ -68,12 +80,12 @@ export default (el, adminEl) => {
const props = parseDatasetToProps(el.dataset);
const initialState = {
- adminState: null,
+ defaultState: null,
customState: props,
};
- if (adminEl) {
- initialState.adminState = Object.freeze(parseDatasetToProps(adminEl.dataset));
+ if (defaultEl) {
+ initialState.defaultState = Object.freeze(parseDatasetToProps(defaultEl.dataset));
}
return new Vue({
diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js
index 3decdaab55d..199c9074ead 100644
--- a/app/assets/javascripts/integrations/edit/store/actions.js
+++ b/app/assets/javascripts/integrations/edit/store/actions.js
@@ -1,4 +1,5 @@
import * as types from './mutation_types';
-// eslint-disable-next-line import/prefer-default-export
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
+export const setIsSaving = ({ commit }, isSaving) => commit(types.SET_IS_SAVING, isSaving);
+export const setIsTesting = ({ commit }, isTesting) => commit(types.SET_IS_TESTING, isTesting);
diff --git a/app/assets/javascripts/integrations/edit/store/getters.js b/app/assets/javascripts/integrations/edit/store/getters.js
index b68bd668980..4ee5f11855c 100644
--- a/app/assets/javascripts/integrations/edit/store/getters.js
+++ b/app/assets/javascripts/integrations/edit/store/getters.js
@@ -1,6 +1,8 @@
-export const isInheriting = state => (state.adminState === null ? false : !state.override);
+export const isInheriting = state => (state.defaultState === null ? false : !state.override);
+
+export const isSavingOrTesting = state => state.isSaving || state.isTesting;
export const propsSource = (state, getters) =>
- getters.isInheriting ? state.adminState : state.customState;
+ getters.isInheriting ? state.defaultState : state.customState;
export const currentKey = (state, getters) => (getters.isInheriting ? 'admin' : 'custom');
diff --git a/app/assets/javascripts/integrations/edit/store/index.js b/app/assets/javascripts/integrations/edit/store/index.js
index eea5e48780d..a8375f345c6 100644
--- a/app/assets/javascripts/integrations/edit/store/index.js
+++ b/app/assets/javascripts/integrations/edit/store/index.js
@@ -7,7 +7,6 @@ import createState from './state';
Vue.use(Vuex);
-// eslint-disable-next-line import/prefer-default-export
export const createStore = (initialState = {}) =>
new Vuex.Store({
actions,
diff --git a/app/assets/javascripts/integrations/edit/store/mutation_types.js b/app/assets/javascripts/integrations/edit/store/mutation_types.js
index 274afe3fb49..0dae8ea079e 100644
--- a/app/assets/javascripts/integrations/edit/store/mutation_types.js
+++ b/app/assets/javascripts/integrations/edit/store/mutation_types.js
@@ -1,2 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const SET_OVERRIDE = 'SET_OVERRIDE';
+export const SET_IS_SAVING = 'SET_IS_SAVING';
+export const SET_IS_TESTING = 'SET_IS_TESTING';
diff --git a/app/assets/javascripts/integrations/edit/store/mutations.js b/app/assets/javascripts/integrations/edit/store/mutations.js
index 8757d415197..8ac3c476f9e 100644
--- a/app/assets/javascripts/integrations/edit/store/mutations.js
+++ b/app/assets/javascripts/integrations/edit/store/mutations.js
@@ -4,4 +4,10 @@ export default {
[types.SET_OVERRIDE](state, override) {
state.override = override;
},
+ [types.SET_IS_SAVING](state, isSaving) {
+ state.isSaving = isSaving;
+ },
+ [types.SET_IS_TESTING](state, isTesting) {
+ state.isTesting = isTesting;
+ },
};
diff --git a/app/assets/javascripts/integrations/edit/store/state.js b/app/assets/javascripts/integrations/edit/store/state.js
index 95c1a2be500..a9ecee6c539 100644
--- a/app/assets/javascripts/integrations/edit/store/state.js
+++ b/app/assets/javascripts/integrations/edit/store/state.js
@@ -1,9 +1,11 @@
-export default ({ adminState = null, customState = {} } = {}) => {
- const override = adminState !== null ? adminState.id !== customState.inheritFromId : false;
+export default ({ defaultState = null, customState = {} } = {}) => {
+ const override = defaultState !== null ? defaultState.id !== customState.inheritFromId : false;
return {
override,
- adminState,
+ defaultState,
customState,
+ isSaving: false,
+ isTesting: false,
};
};
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 1135065b06c..1d0814125e6 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import axios from '../lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from '../flash';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import toast from '~/vue_shared/plugins/global_toast';
import initForm from './edit';
import eventHub from './edit/event_hub';
@@ -10,65 +10,63 @@ export default class IntegrationSettingsForm {
this.$form = $(formSelector);
this.formActive = false;
+ this.vue = null;
+
// Form Metadata
- this.canTestService = this.$form.data('canTest');
this.testEndPoint = this.$form.data('testUrl');
-
- // Form Child Elements
- this.$submitBtn = this.$form.find('button[type="submit"]');
- this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner');
- this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label');
}
init() {
// Init Vue component
- initForm(
+ this.vue = initForm(
document.querySelector('.js-vue-integration-settings'),
- document.querySelector('.js-vue-admin-integration-settings'),
+ document.querySelector('.js-vue-default-integration-settings'),
);
eventHub.$on('toggle', active => {
this.formActive = active;
- this.handleServiceToggle();
+ this.toggleServiceState();
+ });
+ eventHub.$on('testIntegration', () => {
+ this.testIntegration();
+ });
+ eventHub.$on('saveIntegration', () => {
+ this.saveIntegration();
});
-
- // Bind Event Listeners
- this.$submitBtn.on('click', e => this.handleSettingsSave(e));
}
- handleSettingsSave(e) {
- // Check if Service is marked active, as if not marked active,
- // We can skip testing it and directly go ahead to allow form to
- // be submitted
- if (!this.formActive) {
- return;
+ saveIntegration() {
+ // Service was marked active so now we check;
+ // 1) If form contents are valid
+ // 2) If this service can be saved
+ // If both conditions are true, we override form submission
+ // and save the service using provided configuration.
+ if (this.$form.get(0).checkValidity()) {
+ this.$form.submit();
+ } else {
+ eventHub.$emit('validateForm');
+ this.vue.$store.dispatch('setIsSaving', false);
}
+ }
+ testIntegration() {
// Service was marked active so now we check;
// 1) If form contents are valid
// 2) If this service can be tested
// If both conditions are true, we override form submission
// and test the service using provided configuration.
if (this.$form.get(0).checkValidity()) {
- if (this.canTestService) {
- e.preventDefault();
- // eslint-disable-next-line no-jquery/no-serialize
- this.testSettings(this.$form.serialize());
- }
+ // eslint-disable-next-line no-jquery/no-serialize
+ this.testSettings(this.$form.serialize());
} else {
- e.preventDefault();
eventHub.$emit('validateForm');
+ this.vue.$store.dispatch('setIsTesting', false);
}
}
- handleServiceToggle() {
- this.toggleServiceState();
- }
-
/**
* Change Form's validation enforcement based on service status (active/inactive)
*/
toggleServiceState() {
- this.toggleSubmitBtnLabel();
if (this.formActive) {
this.$form.removeAttr('novalidate');
} else if (!this.$form.attr('novalidate')) {
@@ -77,67 +75,23 @@ export default class IntegrationSettingsForm {
}
/**
- * Toggle Submit button label based on Integration status and ability to test service
- */
- toggleSubmitBtnLabel() {
- let btnLabel = __('Save changes');
-
- if (this.formActive && this.canTestService) {
- btnLabel = __('Test settings and save changes');
- }
-
- this.$submitBtnLabel.text(btnLabel);
- }
-
- /**
- * Toggle Submit button state based on provided boolean value of `saveTestActive`
- * When enabled, it does two things, and reverts back when disabled
- *
- * 1. It shows load spinner on submit button
- * 2. Makes submit button disabled
- */
- toggleSubmitBtnState(saveTestActive) {
- if (saveTestActive) {
- this.$submitBtn.disable();
- this.$submitBtnLoader.removeClass('hidden');
- } else {
- this.$submitBtn.enable();
- this.$submitBtnLoader.addClass('hidden');
- }
- }
-
- /**
* Test Integration config
*/
testSettings(formData) {
- this.toggleSubmitBtnState(true);
-
return axios
.put(this.testEndPoint, formData)
.then(({ data }) => {
if (data.error) {
- let flashActions;
-
- if (data.test_failed) {
- flashActions = {
- title: __('Save anyway'),
- clickHandler: e => {
- e.preventDefault();
- this.$form.submit();
- },
- };
- }
-
- flash(`${data.message} ${data.service_response}`, 'alert', document, flashActions);
+ toast(`${data.message} ${data.service_response}`);
} else {
- this.$form.submit();
+ toast(s__('Integrations|Connection successful.'));
}
-
- this.toggleSubmitBtnState(false);
})
.catch(() => {
- flash(__('Something went wrong on our end.'));
- this.toggleSubmitBtnState(false);
+ toast(__('Something went wrong on our end.'));
+ })
+ .finally(() => {
+ this.vue.$store.dispatch('setIsTesting', false);
});
}
}
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index 85c2a370ff3..7d9cefbe66a 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -7,7 +7,7 @@ import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';
-import issueableEventHub from './issuables_list/eventhub';
+import issueableEventHub from './issues_list/eventhub';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
diff --git a/app/assets/javascripts/issuable_create/components/issuable_create_root.vue b/app/assets/javascripts/issuable_create/components/issuable_create_root.vue
new file mode 100644
index 00000000000..1ef42976032
--- /dev/null
+++ b/app/assets/javascripts/issuable_create/components/issuable_create_root.vue
@@ -0,0 +1,44 @@
+<script>
+import IssuableForm from './issuable_form.vue';
+
+export default {
+ components: {
+ IssuableForm,
+ },
+ props: {
+ descriptionPreviewPath: {
+ type: String,
+ required: true,
+ },
+ descriptionHelpPath: {
+ type: String,
+ required: true,
+ },
+ labelsFetchPath: {
+ type: String,
+ required: true,
+ },
+ labelsManagePath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-create-container">
+ <slot name="title"></slot>
+ <hr />
+ <issuable-form
+ :description-preview-path="descriptionPreviewPath"
+ :description-help-path="descriptionHelpPath"
+ :labels-fetch-path="labelsFetchPath"
+ :labels-manage-path="labelsManagePath"
+ >
+ <template #actions="issuableMeta">
+ <slot name="actions" v-bind="issuableMeta"></slot>
+ </template>
+ </issuable-form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_create/components/issuable_form.vue b/app/assets/javascripts/issuable_create/components/issuable_form.vue
new file mode 100644
index 00000000000..17e51b3dbac
--- /dev/null
+++ b/app/assets/javascripts/issuable_create/components/issuable_form.vue
@@ -0,0 +1,127 @@
+<script>
+import { GlForm, GlFormInput } from '@gitlab/ui';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+
+import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
+
+export default {
+ LabelSelectVariant: DropdownVariant,
+ components: {
+ GlForm,
+ GlFormInput,
+ MarkdownField,
+ LabelsSelect,
+ },
+ props: {
+ descriptionPreviewPath: {
+ type: String,
+ required: true,
+ },
+ descriptionHelpPath: {
+ type: String,
+ required: true,
+ },
+ labelsFetchPath: {
+ type: String,
+ required: true,
+ },
+ labelsManagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ issuableTitle: '',
+ issuableDescription: '',
+ selectedLabels: [],
+ };
+ },
+ methods: {
+ handleUpdateSelectedLabels(labels) {
+ if (labels.length) {
+ this.selectedLabels = labels;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form class="common-note-form gfm-form" @submit.stop.prevent>
+ <div data-testid="issuable-title" class="form-group row">
+ <label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label>
+ <div class="col-sm-10">
+ <gl-form-input
+ id="issuable-title"
+ v-model="issuableTitle"
+ :autofocus="true"
+ :placeholder="__('Title')"
+ />
+ </div>
+ </div>
+ <div data-testid="issuable-description" class="form-group row">
+ <label for="issuable-description" class="col-form-label col-sm-2">{{
+ __('Description')
+ }}</label>
+ <div class="col-sm-10">
+ <markdown-field
+ :markdown-preview-path="descriptionPreviewPath"
+ :markdown-docs-path="descriptionHelpPath"
+ :add-spacing-classes="false"
+ :show-suggest-popover="true"
+ >
+ <textarea
+ id="issuable-description"
+ ref="textarea"
+ slot="textarea"
+ v-model="issuableDescription"
+ dir="auto"
+ class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ ></textarea>
+ </markdown-field>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6">
+ <div data-testid="issuable-labels" class="form-group row">
+ <label for="issuable-labels" class="col-form-label col-md-2 col-lg-4">{{
+ __('Labels')
+ }}</label>
+ <div class="col-md-8 col-sm-10">
+ <div class="issuable-form-select-holder">
+ <labels-select
+ :allow-label-edit="true"
+ :allow-label-create="true"
+ :allow-multiselect="true"
+ :allow-scoped-labels="true"
+ :labels-fetch-path="labelsFetchPath"
+ :labels-manage-path="labelsManagePath"
+ :selected-labels="selectedLabels"
+ :labels-list-title="__('Select label')"
+ :footer-create-label-title="__('Create project label')"
+ :footer-manage-label-title="__('Manage project labels')"
+ :variant="$options.LabelSelectVariant.Embedded"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ data-testid="issuable-create-actions"
+ class="footer-block row-content-block gl-display-flex"
+ >
+ <slot
+ name="actions"
+ :issuable-title="issuableTitle"
+ :issuable-description="issuableDescription"
+ :selected-labels="selectedLabels"
+ ></slot>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 2dcf5e6a0d6..ed34e2f5623 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -66,6 +66,7 @@ export default class IssuableForm {
gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
).setup();
this.usersSelect = new UsersSelect();
+ this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
this.zenMode = new ZenMode();
this.titleField = this.form.find('input[name*="[title]"]');
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
new file mode 100644
index 00000000000..d8cb1ab07cd
--- /dev/null
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -0,0 +1,140 @@
+<script>
+import { GlLink, GlLabel, GlTooltipDirective } from '@gitlab/ui';
+
+import { __, sprintf } from '~/locale';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+
+export default {
+ components: {
+ GlLink,
+ GlLabel,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ issuableSymbol: {
+ type: String,
+ required: true,
+ },
+ issuable: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ author() {
+ return this.issuable.author;
+ },
+ authorId() {
+ const id = parseInt(this.author.id, 10);
+
+ if (Number.isNaN(id)) {
+ return this.author.id.includes('gid')
+ ? this.author.id.split('gid://gitlab/User/').pop()
+ : '';
+ }
+
+ return id;
+ },
+ labels() {
+ return this.issuable.labels?.nodes || this.issuable.labels || [];
+ },
+ createdAt() {
+ return sprintf(__('created %{timeAgo}'), {
+ timeAgo: getTimeago().format(this.issuable.createdAt),
+ });
+ },
+ updatedAt() {
+ return sprintf(__('updated %{timeAgo}'), {
+ timeAgo: getTimeago().format(this.issuable.updatedAt),
+ });
+ },
+ },
+ methods: {
+ scopedLabel(label) {
+ return isScopedLabel(label);
+ },
+ /**
+ * This is needed as an independent method since
+ * when user changes current page, `$refs.authorLink`
+ * will be null until next page results are loaded & rendered.
+ */
+ getAuthorPopoverTarget() {
+ if (this.$refs.authorLink) {
+ return this.$refs.authorLink.$el;
+ }
+ return '';
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="issue">
+ <div class="issue-box">
+ <div class="issuable-info-container">
+ <div class="issuable-main-info">
+ <div data-testid="issuable-title" class="issue-title title">
+ <span class="issue-title-text" dir="auto">
+ <gl-link :href="issuable.webUrl">{{ issuable.title }}</gl-link>
+ </span>
+ </div>
+ <div class="issuable-info">
+ <span data-testid="issuable-reference" class="issuable-reference"
+ >{{ issuableSymbol }}{{ issuable.iid }}</span
+ >
+ <span class="issuable-authored d-none d-sm-inline-block">
+ &middot;
+ <span
+ v-gl-tooltip:tooltipcontainer.bottom
+ data-testid="issuable-created-at"
+ :title="tooltipTitle(issuable.createdAt)"
+ >{{ createdAt }}</span
+ >
+ {{ __('by') }}
+ <gl-link
+ :data-user-id="authorId"
+ :data-username="author.username"
+ :data-name="author.name"
+ :data-avatar-url="author.avatarUrl"
+ :href="author.webUrl"
+ data-testid="issuable-author"
+ class="author-link js-user-link"
+ >
+ <span class="author">{{ author.name }}</span>
+ </gl-link>
+ </span>
+ &nbsp;
+ <gl-label
+ v-for="(label, index) in labels"
+ :key="index"
+ :background-color="label.color"
+ :title="label.title"
+ :description="label.description"
+ :scoped="scopedLabel(label)"
+ :class="{ 'gl-ml-2': index }"
+ size="sm"
+ />
+ </div>
+ </div>
+ <div class="issuable-meta">
+ <div
+ data-testid="issuable-updated-at"
+ class="float-right issuable-updated-at d-none d-sm-inline-block"
+ >
+ <span
+ v-gl-tooltip:tooltipcontainer.bottom
+ :title="tooltipTitle(issuable.updatedAt)"
+ class="issuable-updated-at"
+ >{{ updatedAt }}</span
+ >
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
new file mode 100644
index 00000000000..7535203dea1
--- /dev/null
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -0,0 +1,153 @@
+<script>
+import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
+
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+import IssuableTabs from './issuable_tabs.vue';
+import IssuableItem from './issuable_item.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ IssuableTabs,
+ FilteredSearchBar,
+ IssuableItem,
+ GlPagination,
+ },
+ props: {
+ namespace: {
+ type: String,
+ required: true,
+ },
+ recentSearchesStorageKey: {
+ type: String,
+ required: true,
+ },
+ searchInputPlaceholder: {
+ type: String,
+ required: true,
+ },
+ searchTokens: {
+ type: Array,
+ required: true,
+ },
+ sortOptions: {
+ type: Array,
+ required: true,
+ },
+ initialFilterValue: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ initialSortBy: {
+ type: String,
+ required: false,
+ default: 'created_desc',
+ },
+ issuables: {
+ type: Array,
+ required: true,
+ },
+ tabs: {
+ type: Array,
+ required: true,
+ },
+ tabCounts: {
+ type: Object,
+ required: true,
+ },
+ currentTab: {
+ type: String,
+ required: true,
+ },
+ issuableSymbol: {
+ type: String,
+ required: false,
+ default: '#',
+ },
+ issuablesLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showPaginationControls: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ defaultPageSize: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ currentPage: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ previousPage: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ nextPage: {
+ type: Number,
+ required: false,
+ default: 2,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-list-container">
+ <issuable-tabs
+ :tabs="tabs"
+ :tab-counts="tabCounts"
+ :current-tab="currentTab"
+ @click="$emit('click-tab', $event)"
+ >
+ <template #nav-actions>
+ <slot name="nav-actions"></slot>
+ </template>
+ </issuable-tabs>
+ <filtered-search-bar
+ :namespace="namespace"
+ :recent-searches-storage-key="recentSearchesStorageKey"
+ :search-input-placeholder="searchInputPlaceholder"
+ :tokens="searchTokens"
+ :sort-options="sortOptions"
+ :initial-filter-value="initialFilterValue"
+ :initial-sort-by="initialSortBy"
+ class="gl-flex-grow-1 row-content-block"
+ @onFilter="$emit('filter', $event)"
+ @onSort="$emit('sort', $event)"
+ />
+ <div class="issuables-holder">
+ <gl-loading-icon v-if="issuablesLoading" size="md" class="gl-mt-5" />
+ <ul
+ v-if="!issuablesLoading && issuables.length"
+ class="content-list issuable-list issues-list"
+ >
+ <issuable-item
+ v-for="issuable in issuables"
+ :key="issuable.id"
+ :issuable-symbol="issuableSymbol"
+ :issuable="issuable"
+ />
+ </ul>
+ <slot v-if="!issuablesLoading && !issuables.length" name="empty-state"></slot>
+ <gl-pagination
+ v-if="showPaginationControls"
+ :per-page="defaultPageSize"
+ :value="currentPage"
+ :prev-page="previousPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-pagination gl-mt-3"
+ @input="$emit('page-change', $event)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
new file mode 100644
index 00000000000..df544ce69e7
--- /dev/null
+++ b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ GlBadge,
+ },
+ props: {
+ tabs: {
+ type: Array,
+ required: true,
+ },
+ tabCounts: {
+ type: Object,
+ required: true,
+ },
+ currentTab: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ isTabActive(tabName) {
+ return tabName === this.currentTab;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="top-area">
+ <gl-tabs class="nav-links mobile-separator issuable-state-filters">
+ <gl-tab
+ v-for="tab in tabs"
+ :key="tab.id"
+ :active="isTabActive(tab.name)"
+ @click="$emit('click', tab.name)"
+ >
+ <template #title>
+ <span :title="tab.titleTooltip">{{ tab.title }}</span>
+ <gl-badge variant="neutral" size="sm" class="gl-px-2 gl-py-1!">{{
+ tabCounts[tab.name]
+ }}</gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+ <div class="nav-controls">
+ <slot name="nav-actions"></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue
index 810ca7ac1bd..8a9a880e7ee 100644
--- a/app/assets/javascripts/issuable_suggestions/components/app.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/app.vue
@@ -1,14 +1,13 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import Suggestion from './item.vue';
import query from '../queries/issues.query.graphql';
export default {
components: {
Suggestion,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -70,7 +69,7 @@ export default {
<div v-show="showSuggestions" class="form-group row issuable-suggestions">
<div v-once class="col-form-label col-sm-2 pt-0">
{{ __('Similar issues') }}
- <icon
+ <gl-icon
v-gl-tooltip.bottom
:title="$options.helpText"
:aria-label="$options.helpText"
diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue
index dfadb9d2b24..6e265b1bf42 100644
--- a/app/assets/javascripts/issuable_suggestions/components/item.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/item.vue
@@ -1,9 +1,8 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { uniqueId } from 'lodash';
-import { GlLink, GlTooltip, GlTooltipDirective } from '@gitlab/ui';
+import { GlLink, GlTooltip, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeago from '~/vue_shared/mixins/timeago';
@@ -12,7 +11,7 @@ export default {
components: {
GlTooltip,
GlLink,
- Icon,
+ GlIcon,
UserAvatarImage,
TimeagoTooltip,
},
@@ -68,7 +67,7 @@ export default {
<template>
<div class="suggestion-item">
<div class="d-flex align-items-center">
- <icon
+ <gl-icon
v-if="suggestion.confidential"
v-gl-tooltip.bottom
:title="__('Confidential')"
@@ -84,7 +83,7 @@ export default {
</gl-link>
</div>
<div class="text-secondary suggestion-footer">
- <icon
+ <gl-icon
ref="state"
:name="stateIcon"
:class="{
@@ -134,7 +133,7 @@ export default {
:title="tooltipTitle"
class="suggestion-help-hover gl-ml-3 text-tertiary"
>
- <icon :name="icon" /> {{ count }}
+ <gl-icon :name="icon" /> {{ count }}
</span>
</span>
</div>
diff --git a/app/assets/javascripts/issuables_list/service_desk_helper.js b/app/assets/javascripts/issuables_list/service_desk_helper.js
deleted file mode 100644
index 4b4a38c2205..00000000000
--- a/app/assets/javascripts/issuables_list/service_desk_helper.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { __ } from '~/locale';
-
-/**
- * Returns the attributes used for gl-empty-state in the Service Desk issues list.
- */
-// eslint-disable-next-line import/prefer-default-export
-export function emptyStateHelper(emptyStateMeta) {
- const { isServiceDeskSupported, svgPath, serviceDeskHelpPage } = emptyStateMeta;
-
- if (isServiceDeskSupported) {
- const title = __(
- 'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab',
- );
- const commonMessage = __(
- 'Those emails automatically become issues (with the comments becoming the email conversation) listed here.',
- );
- const commonDescription = `
- <span>${commonMessage}</span>
- <a href="${serviceDeskHelpPage}">${__('Read more')}</a>`;
-
- if (emptyStateMeta.canEditProjectSettings && emptyStateMeta.isServiceDeskEnabled) {
- return {
- title,
- svgPath,
- description: `<p>${__('Have your users email')} <code>${
- emptyStateMeta.serviceDeskAddress
- }</code></p> ${commonDescription}`,
- };
- }
-
- if (emptyStateMeta.canEditProjectSettings && !emptyStateMeta.isServiceDeskEnabled) {
- return {
- title,
- svgPath,
- description: commonDescription,
- primaryLink: emptyStateMeta.editProjectPage,
- primaryText: __('Turn on Service Desk'),
- };
- }
-
- return {
- title,
- svgPath,
- description: commonDescription,
- };
- }
-
- return {
- title: __('Service Desk is enabled but not yet active'),
- svgPath,
- description: __('You must set up incoming email before it becomes active.'),
- primaryLink: emptyStateMeta.incomingEmailHelpPage,
- primaryText: __('More information'),
- };
-}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index f1b37525a6d..0a0cfe918af 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -100,6 +100,13 @@ export default class Issue {
initIssueBtnEventListeners() {
const issueFailMessage = __('Unable to update this issue at this time.');
+ $('.report-abuse-link').on('click', e => {
+ // this is needed because of the implementation of
+ // the dropdown toggle and Report Abuse needing to be
+ // linked to another page.
+ e.stopPropagation();
+ });
+
// NOTE: data attribute seems unnecessary but is actually necessary
return $('.js-issuable-buttons[data-action="close-reopen"]').on(
'click',
@@ -173,11 +180,15 @@ export default class Issue {
}
initIssueWarningBtnEventListener() {
- return $(document).on('click', '.js-close-blocked-issue-warning button.btn-secondary', e => {
- e.preventDefault();
- e.stopImmediatePropagation();
- this.toggleWarningAndCloseButton();
- });
+ return $(document).on(
+ 'click',
+ '.js-close-blocked-issue-warning .js-cancel-blocked-issue-warning',
+ e => {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ this.toggleWarningAndCloseButton();
+ },
+ );
}
initIssueMovedFromServiceDeskDismissHandler() {
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 992d87a969f..22db0f1cfc1 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -20,7 +20,6 @@ export default {
components: {
GlIcon,
GlIntersectionObserver,
- descriptionComponent,
titleComponent,
editedComponent,
formComponent,
@@ -152,6 +151,18 @@ export default {
required: false,
default: 0,
},
+ descriptionComponent: {
+ type: Object,
+ required: false,
+ default: () => {
+ return descriptionComponent;
+ },
+ },
+ showTitleBorder: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
const store = new Store({
@@ -209,6 +220,11 @@ export default {
isOpenStatus() {
return this.issuableStatus === IssuableStatus.Open;
},
+ pinnedLinkClasses() {
+ return this.showTitleBorder
+ ? 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'
+ : '';
+ },
statusIcon() {
return this.isOpenStatus ? 'issue-open-m' : 'mobile-issue-close';
},
@@ -231,7 +247,7 @@ export default {
});
if (!Visibility.hidden()) {
- this.poll.makeRequest();
+ this.poll.makeDelayedRequest(2000);
}
Visibility.change(() => {
@@ -447,10 +463,11 @@ export default {
<pinned-links
:zoom-meeting-url="zoomMeetingUrl"
:published-incident-url="publishedIncidentUrl"
+ :class="pinnedLinkClasses"
/>
- <description-component
- v-if="state.descriptionHtml"
+ <component
+ :is="descriptionComponent"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index abb63f606ae..2a6468c783b 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import animateMixin from '../mixins/animate';
@@ -7,6 +8,10 @@ import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
+ directives: {
+ SafeHtml,
+ },
+
mixins: [animateMixin, recaptchaModalImplementor],
props: {
@@ -20,7 +25,8 @@ export default {
},
descriptionText: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
taskStatus: {
type: String,
@@ -47,11 +53,16 @@ export default {
return {
preAnimation: false,
pulseAnimation: false,
+ initialUpdate: true,
};
},
watch: {
- descriptionHtml() {
- this.animateChange();
+ descriptionHtml(newDescription, oldDescription) {
+ if (!this.initialUpdate && newDescription !== oldDescription) {
+ this.animateChange();
+ } else {
+ this.initialUpdate = false;
+ }
this.$nextTick(() => {
this.renderGFM();
@@ -136,12 +147,12 @@ export default {
>
<div
ref="gfm-content"
+ v-safe-html="descriptionHtml"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
}"
class="md"
- v-html="descriptionHtml"
></div>
<textarea
v-if="descriptionText"
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 4ee44e50d2f..14ada5adcf6 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
@@ -10,6 +10,9 @@ const issuableTypes = {
};
export default {
+ components: {
+ GlButton,
+ },
mixins: [updateMixin],
props: {
canDestroy: {
@@ -64,28 +67,30 @@ export default {
<template>
<div class="gl-mt-3 gl-mb-3 clearfix">
- <button
- :class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
+ <gl-button
+ :loading="formState.updateLoading"
:disabled="formState.updateLoading || !isSubmitEnabled"
- class="btn btn-success float-left qa-save-button"
+ category="primary"
+ variant="success"
+ class="float-left qa-save-button"
type="submit"
@click.prevent="updateIssuable"
>
- Save changes
- <i v-if="formState.updateLoading" class="fa fa-spinner fa-spin" aria-hidden="true"> </i>
- </button>
- <button class="btn btn-default float-right" type="button" @click="closeForm">
+ {{ __('Save changes') }}
+ </gl-button>
+ <gl-button class="float-right" @click="closeForm">
{{ __('Cancel') }}
- </button>
- <button
+ </gl-button>
+ <gl-button
v-if="shouldShowDeleteButton"
- :class="{ disabled: deleteLoading }"
+ :loading="deleteLoading"
:disabled="deleteLoading"
- class="btn btn-danger float-right gl-mr-3 qa-delete-button"
- type="button"
+ category="primary"
+ variant="danger"
+ class="float-right gl-mr-3 qa-delete-button"
@click="deleteIssuable"
>
- Delete <i v-if="deleteLoading" class="fa fa-spinner fa-spin" aria-hidden="true"> </i>
- </button>
+ {{ __('Delete') }}
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
index 6d8a9950b6d..e1b308c6f57 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -1,9 +1,13 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import $ from 'jquery';
+import { GlIcon } from '@gitlab/ui';
import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
export default {
+ components: {
+ GlIcon,
+ },
props: {
formState: {
type: Object,
@@ -61,14 +65,14 @@ export default {
<i aria-hidden="true" class="fa fa-chevron-down"> </i>
</button>
<div class="dropdown-menu dropdown-select">
- <div class="dropdown-title">
- Choose a template
+ <div class="dropdown-title gl-display-flex gl-justify-content-center">
+ <span class="gl-ml-auto">Choose a template</span>
<button
- class="dropdown-title-button dropdown-menu-close"
+ class="dropdown-title-button dropdown-menu-close gl-ml-auto"
:aria-label="__('Close')"
type="button"
>
- <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon"> </i>
+ <gl-icon name="close" class="dropdown-menu-close-icon" :aria-hidden="true" />
</button>
</div>
<div class="dropdown-input">
@@ -79,12 +83,11 @@ export default {
autocomplete="off"
/>
<i aria-hidden="true" class="fa fa-search dropdown-input-search"> </i>
- <i
- role="button"
+ <gl-icon
+ name="close"
+ class="dropdown-input-clear js-dropdown-input-clear"
:aria-label="__('Clear templates search input')"
- class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
- >
- </i>
+ />
</div>
<div class="dropdown-content"></div>
<div class="dropdown-footer">
diff --git a/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql b/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql
new file mode 100644
index 00000000000..00ddc80432d
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql
@@ -0,0 +1,20 @@
+query getAlert($iid: String!, $fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ issue(iid: $iid) {
+ alertManagementAlert {
+ iid
+ title
+ detailsUrl
+ severity
+ status
+ startedAt
+ eventCount
+ monitoringTool
+ service
+ description
+ endedAt
+ details
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue b/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
new file mode 100644
index 00000000000..a47fe4c84cf
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ GlLink,
+ },
+ props: {
+ alert: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ startTime() {
+ return formatDate(this.alert.startedAt, 'yyyy-mm-dd Z');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between"
+ >
+ <div class="text-truncate gl-pr-3">
+ <span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
+ <gl-link :href="alert.detailsUrl">{{ alert.title }}</gl-link>
+ </div>
+
+ <div class="gl-pr-3 gl-white-space-nowrap">
+ <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span>
+ {{ startTime }}
+ </div>
+
+ <div class="gl-white-space-nowrap">
+ <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span>
+ <span>{{ alert.eventCount }}</span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
new file mode 100644
index 00000000000..4104ddbf06f
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import DescriptionComponent from '../description.vue';
+import HighlightBar from './highlight_bar.vue';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+
+import getAlert from './graphql/queries/get_alert.graphql';
+
+export default {
+ components: {
+ AlertDetailsTable,
+ DescriptionComponent,
+ GlTab,
+ GlTabs,
+ HighlightBar,
+ },
+ inject: ['fullPath', 'iid'],
+ apollo: {
+ alert: {
+ query: getAlert,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ };
+ },
+ update(data) {
+ return data?.project?.issue?.alertManagementAlert;
+ },
+ error() {
+ createFlash({
+ message: s__('Incident|There was an issue loading alert data. Please try again.'),
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ alert: null,
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.alert.loading;
+ },
+ alertTableFields() {
+ if (this.alert) {
+ const { detailsUrl, __typename, ...restDetails } = this.alert;
+ return restDetails;
+ }
+ return null;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs">
+ <gl-tab :title="s__('Incident|Summary')">
+ <highlight-bar v-if="alert" :alert="alert" />
+ <description-component v-bind="$attrs" />
+ </gl-tab>
+ <gl-tab v-if="alert" class="alert-management-details" :title="s__('Incident|Alert details')">
+ <alert-details-table :alert="alertTableFields" :loading="loading" />
+ </gl-tab>
+ </gl-tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue
index 19c7a11d87b..96f5a7c88e0 100644
--- a/app/assets/javascripts/issue_show/components/locked_warning.vue
+++ b/app/assets/javascripts/issue_show/components/locked_warning.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { __, sprintf } from '~/locale';
export default {
diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue
index a877aa2ac96..d38189307bd 100644
--- a/app/assets/javascripts/issue_show/components/pinned_links.vue
+++ b/app/assets/javascripts/issue_show/components/pinned_links.vue
@@ -20,20 +20,25 @@ export default {
},
computed: {
pinnedLinks() {
- return [
- {
+ const links = [];
+ if (this.publishedIncidentUrl) {
+ links.push({
id: 'publishedIncidentUrl',
url: this.publishedIncidentUrl,
text: STATUS_PAGE_PUBLISHED,
icon: 'tanuki',
- },
- {
+ });
+ }
+ if (this.zoomMeetingUrl) {
+ links.push({
id: 'zoomMeetingUrl',
url: this.zoomMeetingUrl,
text: JOIN_ZOOM_MEETING,
icon: 'brand-zoom',
- },
- ];
+ });
+ }
+
+ return links;
},
},
methods: {
@@ -45,7 +50,7 @@ export default {
</script>
<template>
- <div class="border-bottom gl-mb-6 gl-display-flex gl-justify-content-start">
+ <div v-if="pinnedLinks && pinnedLinks.length" class="gl-display-flex gl-justify-content-start">
<template v-for="(link, i) in pinnedLinks">
<div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }">
<gl-button
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index 1e1dce5f4fc..b03a91716fe 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -1,12 +1,15 @@
<script>
+import { GlButton, GlTooltipDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import animateMixin from '../mixins/animate';
import eventHub from '../event_hub';
-import tooltip from '../../vue_shared/directives/tooltip';
-import { spriteIcon } from '../../lib/utils/common_utils';
export default {
+ components: {
+ GlButton,
+ },
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
mixins: [animateMixin],
props: {
@@ -40,11 +43,6 @@ export default {
titleEl: document.querySelector('title'),
};
},
- computed: {
- pencilIcon() {
- return spriteIcon('pencil', 'link-highlight');
- },
- },
watch: {
titleHtml() {
this.setPageTitle();
@@ -67,25 +65,21 @@ export default {
<template>
<div class="title-container">
<h2
+ v-safe-html="titleHtml"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
}"
class="title qa-title"
dir="auto"
- v-html="titleHtml"
></h2>
- <button
+ <gl-button
v-if="showInlineEditButton && canUpdate"
- v-tooltip
- type="button"
- class="btn btn-default btn-edit btn-svg js-issuable-edit
- qa-edit-button"
+ v-gl-tooltip.bottom
+ icon="pencil"
+ class="btn-edit js-issuable-edit qa-edit-button"
title="Edit title and description"
- data-placement="bottom"
- data-container="body"
@click="edit"
- v-html="pencilIcon"
- ></button>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js
new file mode 100644
index 00000000000..a34e75ee64a
--- /dev/null
+++ b/app/assets/javascripts/issue_show/incident.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import issuableApp from './components/app.vue';
+import incidentTabs from './components/incidents/incident_tabs.vue';
+
+Vue.use(VueApollo);
+
+export default function initIssuableApp(issuableData = {}) {
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { projectNamespace, projectPath, iid } = issuableData;
+
+ return new Vue({
+ el: document.getElementById('js-issuable-app'),
+ apolloProvider,
+ components: {
+ issuableApp,
+ },
+ provide: {
+ fullPath: `${projectNamespace}/${projectPath}`,
+ iid,
+ },
+ render(createElement) {
+ return createElement('issuable-app', {
+ props: {
+ ...issuableData,
+ descriptionComponent: incidentTabs,
+ showTitleBorder: false,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/issue.js
index e170d338408..f9f61d5aa64 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/issue.js
@@ -1,8 +1,7 @@
import Vue from 'vue';
import issuableApp from './components/app.vue';
-import { parseIssuableData } from './utils/parse_data';
-export default function initIssueableApp() {
+export default function initIssuableApp(issuableData) {
return new Vue({
el: document.getElementById('js-issuable-app'),
components: {
@@ -10,7 +9,7 @@ export default function initIssueableApp() {
},
render(createElement) {
return createElement('issuable-app', {
- props: parseIssuableData(),
+ props: issuableData,
});
},
});
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 0cd094243b9..c6f7e892f9b 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -1,3 +1,4 @@
+import { sanitize } from 'dompurify';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import updateDescription from '../utils/update_description';
@@ -27,8 +28,8 @@ export default class Store {
const details =
descriptionSection != null && descriptionSection.getElementsByTagName('details');
- this.state.descriptionHtml = updateDescription(data.description, details);
- this.state.titleHtml = data.title;
+ this.state.descriptionHtml = updateDescription(sanitize(data.description), details);
+ this.state.titleHtml = sanitize(data.title);
this.state.lock_version = data.lock_version;
}
diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js
index 8cd1c1b0e56..a62a5167961 100644
--- a/app/assets/javascripts/issue_show/utils/parse_data.js
+++ b/app/assets/javascripts/issue_show/utils/parse_data.js
@@ -1,10 +1,22 @@
import { sanitize } from 'dompurify';
+// We currently load + parse the data from the issue app and related merge request
+let cachedParsedData;
+
export const parseIssuableData = () => {
try {
+ if (cachedParsedData) return cachedParsedData;
+
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
- return JSON.parse(sanitize(initialDataEl.textContent).replace(/&quot;/g, '"'));
+ const parsedData = JSON.parse(initialDataEl.textContent.replace(/&quot;/g, '"'));
+
+ parsedData.initialTitleHtml = sanitize(parsedData.initialTitleHtml);
+ parsedData.initialDescriptionHtml = sanitize(parsedData.initialDescriptionHtml);
+
+ cachedParsedData = parsedData;
+
+ return parsedData;
} catch (e) {
console.error(e); // eslint-disable-line no-console
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 75edff41a89..02b10730153 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,10 +1,11 @@
import $ from 'jquery';
import { __ } from './locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default function issueStatusSelect() {
$('.js-issue-status').each((i, el) => {
const fieldName = $(el).data('fieldName');
- return $(el).glDropdown({
+ initDeprecatedJQueryDropdown($(el), {
selectable: true,
fieldName,
toggleLabel(selected, element, instance) {
diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue
index 4fc614f8da4..adfb234fe7a 100644
--- a/app/assets/javascripts/issuables_list/components/issuable.vue
+++ b/app/assets/javascripts/issues_list/components/issuable.vue
@@ -6,7 +6,14 @@
// TODO: need to move this component to graphql - https://gitlab.com/gitlab-org/gitlab/-/issues/221246
import { escape, isNumber } from 'lodash';
-import { GlLink, GlTooltipDirective as GlTooltip, GlSprintf, GlLabel, GlIcon } from '@gitlab/ui';
+import {
+ GlLink,
+ GlTooltipDirective as GlTooltip,
+ GlSprintf,
+ GlLabel,
+ GlIcon,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg';
import {
dateInWords,
@@ -41,6 +48,7 @@ export default {
},
directives: {
GlTooltip,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -157,7 +165,7 @@ export default {
value: this.issuable.merge_requests_count,
title: __('Related merge requests'),
dataTestId: 'merge-requests',
- class: 'js-merge-requests icon-merge-request-unmerged',
+ class: 'js-merge-requests',
icon: 'merge-request',
},
{
@@ -298,9 +306,9 @@ export default {
<span class="js-ref-path gl-mr-4 mr-sm-0">
<span
v-if="isJiraIssue"
+ v-safe-html="jiraLogo"
class="svg-container jira-logo-container"
data-testid="jira-logo"
- v-html="jiraLogo"
></span>
{{ referencePath }}
</span>
diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
index fecb7353efb..0d4f5bce965 100644
--- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
@@ -3,7 +3,7 @@ import { toNumber, omit } from 'lodash';
import {
GlEmptyState,
GlPagination,
- GlSkeletonLoading,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { deprecatedCreateFlash as flash } from '~/flash';
diff --git a/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
index cc90d23eda7..61781c576c0 100644
--- a/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue
+++ b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
@@ -11,7 +11,7 @@ import {
} from '~/jira_import/utils/jira_import_utils';
export default {
- name: 'IssuableListRoot',
+ name: 'JiraIssuesList',
components: {
GlAlert,
GlLabel,
diff --git a/app/assets/javascripts/issuables_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index f008ba1bf4a..f008ba1bf4a 100644
--- a/app/assets/javascripts/issuables_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
diff --git a/app/assets/javascripts/issuables_list/eventhub.js b/app/assets/javascripts/issues_list/eventhub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/issuables_list/eventhub.js
+++ b/app/assets/javascripts/issues_list/eventhub.js
diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issues_list/index.js
index fa23d6c0eed..1ff41c20d08 100644
--- a/app/assets/javascripts/issuables_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -2,10 +2,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import IssuableListRootApp from './components/issuable_list_root_app.vue';
+import JiraIssuesListRoot from './components/jira_issues_list_root.vue';
import IssuablesListApp from './components/issuables_list_app.vue';
-function mountIssuableListRootApp() {
+function mountJiraIssuesListApp() {
const el = document.querySelector('.js-projects-issues-root');
if (!el) {
@@ -23,7 +23,7 @@ function mountIssuableListRootApp() {
el,
apolloProvider,
render(createComponent) {
- return createComponent(IssuableListRootApp, {
+ return createComponent(JiraIssuesListRoot, {
props: {
canEdit: parseBoolean(el.dataset.canEdit),
isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
@@ -62,6 +62,6 @@ function mountIssuablesListApp() {
}
export default function initIssuablesList() {
- mountIssuableListRootApp();
+ mountJiraIssuesListApp();
mountIssuablesListApp();
}
diff --git a/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
index 8f9b888d19b..8f9b888d19b 100644
--- a/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
diff --git a/app/assets/javascripts/issues_list/service_desk_helper.js b/app/assets/javascripts/issues_list/service_desk_helper.js
new file mode 100644
index 00000000000..0a34b754377
--- /dev/null
+++ b/app/assets/javascripts/issues_list/service_desk_helper.js
@@ -0,0 +1,111 @@
+import { __ } from '~/locale';
+
+/**
+ * Generates empty state messages for Service Desk issues list.
+ *
+ * @param {emptyStateMeta} emptyStateMeta - Meta data used to generate empty state messages
+ * @returns {Object} Object containing empty state messages generated using the meta data.
+ */
+export function generateMessages(emptyStateMeta) {
+ const {
+ svgPath,
+ serviceDeskHelpPage,
+ serviceDeskAddress,
+ editProjectPage,
+ incomingEmailHelpPage,
+ } = emptyStateMeta;
+
+ const serviceDeskSupportedTitle = __(
+ 'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab',
+ );
+
+ const serviceDeskSupportedMessage = __(
+ 'Those emails automatically become issues (with the comments becoming the email conversation) listed here.',
+ );
+
+ const commonDescription = `
+ <span>${serviceDeskSupportedMessage}</span>
+ <a href="${serviceDeskHelpPage}">${__('Read more')}</a>`;
+
+ return {
+ serviceDeskEnabledAndCanEditProjectSettings: {
+ title: serviceDeskSupportedTitle,
+ svgPath,
+ description: `<p>${__('Have your users email')}
+ <code>${serviceDeskAddress}</code>
+ </p>
+ ${commonDescription}`,
+ },
+ serviceDeskEnabledAndCannotEditProjectSettings: {
+ title: serviceDeskSupportedTitle,
+ svgPath,
+ description: commonDescription,
+ },
+ serviceDeskDisabledAndCanEditProjectSettings: {
+ title: serviceDeskSupportedTitle,
+ svgPath,
+ description: commonDescription,
+ primaryLink: editProjectPage,
+ primaryText: __('Turn on Service Desk'),
+ },
+ serviceDeskDisabledAndCannotEditProjectSettings: {
+ title: serviceDeskSupportedTitle,
+ svgPath,
+ description: commonDescription,
+ },
+ serviceDeskIsNotSupported: {
+ title: __('Service Desk is not supported'),
+ svgPath,
+ description: __(
+ 'In order to enable Service Desk for your instance, you must first set up incoming email.',
+ ),
+ primaryLink: incomingEmailHelpPage,
+ primaryText: __('More information'),
+ },
+ serviceDeskIsNotEnabled: {
+ title: __('Service Desk is not enabled'),
+ svgPath,
+ description: __(
+ 'For help setting up the Service Desk for your instance, please contact an administrator.',
+ ),
+ },
+ };
+}
+
+/**
+ * Returns the attributes used for gl-empty-state in the Service Desk issues list.
+ *
+ * @param {Object} emptyStateMeta - Meta data used to generate empty state messages
+ * @returns {Object}
+ */
+export function emptyStateHelper(emptyStateMeta) {
+ const messages = generateMessages(emptyStateMeta);
+
+ const { isServiceDeskSupported, canEditProjectSettings, isServiceDeskEnabled } = emptyStateMeta;
+
+ if (isServiceDeskSupported) {
+ if (isServiceDeskEnabled && canEditProjectSettings) {
+ return messages.serviceDeskEnabledAndCanEditProjectSettings;
+ }
+
+ if (isServiceDeskEnabled && !canEditProjectSettings) {
+ return messages.serviceDeskEnabledAndCannotEditProjectSettings;
+ }
+
+ // !isServiceDeskEnabled && canEditProjectSettings
+ if (canEditProjectSettings) {
+ return messages.serviceDeskDisabledAndCanEditProjectSettings;
+ }
+
+ // !isServiceDeskEnabled && !canEditProjectSettings
+ return messages.serviceDeskDisabledAndCannotEditProjectSettings;
+ }
+
+ // !serviceDeskSupported && canEditProjectSettings
+ if (canEditProjectSettings) {
+ return messages.serviceDeskIsNotSupported;
+ }
+
+ // !serviceDeskSupported && !canEditProjectSettings
+ return messages.serviceDeskIsNotEnabled;
+}
diff --git a/app/assets/javascripts/jira_connect.js b/app/assets/javascripts/jira_connect.js
new file mode 100644
index 00000000000..895cdc4562c
--- /dev/null
+++ b/app/assets/javascripts/jira_connect.js
@@ -0,0 +1,56 @@
+/* eslint-disable func-names, no-var, no-alert */
+/* global $ */
+/* global AP */
+
+/**
+ * This script is not going through Webpack bundling
+ * as it is only included in `app/views/jira_connect/subscriptions/index.html.haml`
+ * which is going to be rendered within iframe on Jira app dashboard
+ * hence any code written here needs to be IE11+ compatible (no fully ES6)
+ */
+
+function onLoaded() {
+ var reqComplete = function() {
+ AP.navigator.reload();
+ };
+
+ var reqFailed = function(res) {
+ alert(res.responseJSON.error);
+ };
+
+ $('#add-subscription-form').on('submit', function(e) {
+ var actionUrl = $(this).attr('action');
+ e.preventDefault();
+
+ AP.context.getToken(function(token) {
+ // eslint-disable-next-line no-jquery/no-ajax
+ $.post(actionUrl, {
+ jwt: token,
+ namespace_path: $('#namespace-input').val(),
+ format: 'json',
+ })
+ .done(reqComplete)
+ .fail(reqFailed);
+ });
+ });
+
+ $('.remove-subscription').on('click', function(e) {
+ var href = $(this).attr('href');
+ e.preventDefault();
+
+ AP.context.getToken(function(token) {
+ // eslint-disable-next-line no-jquery/no-ajax
+ $.ajax({
+ url: href,
+ method: 'DELETE',
+ data: {
+ jwt: token,
+ format: 'json',
+ },
+ })
+ .done(reqComplete)
+ .fail(reqFailed);
+ });
+ });
+}
+document.addEventListener('DOMContentLoaded', onLoaded);
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index 5bf98682f92..4339021d9a0 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -2,9 +2,9 @@
import {
GlAlert,
GlButton,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownText,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
GlFormGroup,
GlFormSelect,
GlIcon,
@@ -34,9 +34,9 @@ export default {
components: {
GlAlert,
GlButton,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownText,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
GlFormGroup,
GlFormSelect,
GlIcon,
@@ -293,7 +293,7 @@ export default {
<gl-icon name="arrow-right" :aria-label="__('Will be mapped to')" />
</template>
<template #cell(gitlabUsername)="data">
- <gl-new-dropdown
+ <gl-dropdown
:text="data.value || $options.currentUsername"
class="w-100"
:aria-label="
@@ -301,23 +301,23 @@ export default {
"
@hide="resetDropdown"
>
- <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" />
+ <gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" />
<gl-loading-icon v-if="isFetching" />
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="user in users"
v-else
:key="user.id"
@click="updateMapping(data.item.jiraAccountId, user.id, user.username)"
>
{{ user.username }} ({{ user.name }})
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
- <gl-new-dropdown-text v-show="shouldShowNoMatchesFoundText" class="text-secondary">
+ <gl-dropdown-text v-show="shouldShowNoMatchesFoundText" class="text-secondary">
{{ __('No matches found') }}
- </gl-new-dropdown-text>
- </gl-new-dropdown>
+ </gl-dropdown-text>
+ </gl-dropdown>
</template>
</gl-table>
diff --git a/app/assets/javascripts/jira_import/utils/jira_import_utils.js b/app/assets/javascripts/jira_import/utils/jira_import_utils.js
index a1186b087e1..edd6fad4aac 100644
--- a/app/assets/javascripts/jira_import/utils/jira_import_utils.js
+++ b/app/assets/javascripts/jira_import/utils/jira_import_utils.js
@@ -1,5 +1,5 @@
import { last } from 'lodash';
-import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issuables_list/constants';
+import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
export const IMPORT_STATE = {
FAILED: 'failed',
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 6183779acd4..2850a8e86fd 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -1,11 +1,12 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlIcon, GlLink } from '@gitlab/ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
TimeagoTooltip,
+ GlIcon,
GlLink,
},
mixins: [timeagoMixin],
@@ -14,6 +15,10 @@ export default {
type: Object,
required: true,
},
+ helpUrl: {
+ type: String,
+ required: true,
+ },
},
computed: {
isExpired() {
@@ -40,6 +45,14 @@ export default {
<span v-if="isExpired">{{ s__('Job|The artifacts were removed') }}</span>
<span v-if="willExpire">{{ s__('Job|The artifacts will be removed') }}</span>
<timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
+ <gl-link
+ :href="helpUrl"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ data-testid="artifact-expired-help-link"
+ >
+ <gl-icon name="question" />
+ </gl-link>
</p>
<p v-else-if="isLocked" class="build-detail-row">
<span data-testid="job-locked-message">{{
@@ -71,6 +84,7 @@ export default {
:href="artifact.browse_path"
class="btn btn-sm btn-default"
data-testid="browse-artifacts"
+ data-qa-selector="browse_artifacts_button"
>{{ s__('Job|Browse') }}</gl-link
>
</div>
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index e760706c97e..00ff3fb939d 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -1,13 +1,13 @@
<script>
+/* eslint-disable vue/no-v-html */
import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { polyfillSticky } from '~/lib/utils/sticky';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
@@ -27,7 +27,7 @@ export default {
EmptyState,
EnvironmentsBlock,
ErasedBlock,
- Icon,
+ GlIcon,
Log,
LogTopBar,
StuckBlock,
@@ -38,6 +38,11 @@ export default {
},
mixins: [delayedJobMixin],
props: {
+ artifactHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
runnerSettingsUrl: {
type: String,
required: false,
@@ -266,7 +271,7 @@ export default {
:class="{ 'sticky-top border-bottom-0': hasTrace }"
data-testid="archived-job"
>
- <icon name="lock" class="align-text-bottom" />
+ <gl-icon name="lock" class="align-text-bottom" />
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!-- job log -->
@@ -319,6 +324,7 @@ export default {
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen,
}"
+ :artifact-help-url="artifactHelpUrl"
:runner-help-url="runnerHelpUrl"
data-testid="job-sidebar"
/>
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index 7bd299bcfa0..79e6623eca8 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -1,15 +1,14 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { sprintf } from '~/locale';
export default {
components: {
CiIcon,
- Icon,
+ GlIcon,
GlLink,
},
directives: {
@@ -56,7 +55,7 @@ export default {
data-boundary="viewport"
class="js-job-link d-flex"
>
- <icon
+ <gl-icon
v-if="isActive"
name="arrow-right"
class="js-arrow-right icon-arrow-right position-absolute d-block"
@@ -66,7 +65,7 @@ export default {
<span class="text-truncate w-100">{{ job.name ? job.name : job.id }}</span>
- <icon v-if="job.retried" name="retry" class="js-retry-icon" />
+ <gl-icon v-if="job.retried" name="retry" class="js-retry-icon" />
</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 4d314eaa106..7e7d9a0549b 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -1,16 +1,15 @@
<script>
-import { GlTooltipDirective, GlLink, GlDeprecatedButton } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
import { polyfillSticky } from '~/lib/utils/sticky';
-import Icon from '~/vue_shared/components/icon.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __, sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
export default {
components: {
- Icon,
GlLink,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -87,18 +86,17 @@ export default {
<div class="controllers float-right">
<!-- links -->
- <gl-link
+ <gl-button
v-if="rawPath"
v-gl-tooltip.body
:title="s__('Job|Show complete raw')"
:href="rawPath"
class="controllers-buttons"
data-testid="job-raw-link-controller"
- >
- <icon name="doc-text" />
- </gl-link>
+ icon="doc-text"
+ />
- <gl-link
+ <gl-button
v-if="erasePath"
v-gl-tooltip.body
:title="s__('Job|Erase job log')"
@@ -107,30 +105,28 @@ export default {
class="controllers-buttons"
data-testid="job-log-erase-link"
data-method="post"
- >
- <icon name="remove" />
- </gl-link>
+ icon="remove"
+ />
<!-- eo links -->
<!-- scroll buttons -->
<div v-gl-tooltip :title="s__('Job|Scroll to top')" class="controllers-buttons">
- <gl-deprecated-button
+ <gl-button
:disabled="isScrollTopDisabled"
- type="button"
class="btn-scroll btn-transparent btn-blank"
data-testid="job-controller-scroll-top"
+ icon="scroll_up"
@click="handleScrollToTop"
- >
- <icon name="scroll_up" />
- </gl-deprecated-button>
+ />
</div>
<div v-gl-tooltip :title="s__('Job|Scroll to bottom')" class="controllers-buttons">
- <gl-deprecated-button
+ <gl-button
:disabled="isScrollBottomDisabled"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
- :class="{ animate: isScrollingDown }"
data-testid="job-controller-scroll-bottom"
+ icon="scroll_down"
+ :class="{ animate: isScrollingDown }"
@click="handleScrollToBottom"
v-html="$options.scrollDown"
/>
diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/jobs/components/log/line.vue
index 48f669ae8ed..e68d5b8eda4 100644
--- a/app/assets/javascripts/jobs/components/log/line.vue
+++ b/app/assets/javascripts/jobs/components/log/line.vue
@@ -20,7 +20,7 @@ export default {
return h(
'span',
{
- class: ['ws-pre-wrap', content.style],
+ class: ['gl-white-space-pre-wrap', content.style],
},
content.text,
);
diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/jobs/components/log/line_header.vue
index 85ccd5996b5..4c1c00cb2a7 100644
--- a/app/assets/javascripts/jobs/components/log/line_header.vue
+++ b/app/assets/javascripts/jobs/components/log/line_header.vue
@@ -1,11 +1,11 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import LineNumber from './line_number.vue';
import DurationBadge from './duration_badge.vue';
export default {
components: {
- Icon,
+ GlIcon,
LineNumber,
DurationBadge,
},
@@ -47,12 +47,12 @@ export default {
role="button"
@click="handleOnClick"
>
- <icon :name="iconName" class="arrow position-absolute" />
+ <gl-icon :name="iconName" class="arrow position-absolute" />
<line-number :line-number="line.lineNumber" :path="path" />
<span
v-for="(content, i) in line.content"
:key="i"
- class="line-text w-100 ws-pre-wrap"
+ class="line-text w-100 gl-white-space-pre-wrap"
:class="content.style"
>{{ content.text }}</span
>
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index 9236624a191..bf1930c9a37 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -1,15 +1,14 @@
<script>
+/* eslint-disable vue/no-v-html */
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'ManualVariablesForm',
components: {
- GlDeprecatedButton,
- Icon,
+ GlButton,
},
props: {
action: {
@@ -137,12 +136,12 @@ export default {
<div class="table-section section-10">
<div class="table-mobile-header" role="rowheader"></div>
<div class="table-mobile-content justify-content-end">
- <gl-deprecated-button
- class="btn-transparent btn-blank w-25"
+ <gl-button
+ category="tertiary"
+ icon="clear"
+ :aria-label="__('Delete variable')"
@click="deleteVariable(variable.id)"
- >
- <icon name="clear" />
- </gl-deprecated-button>
+ />
</div>
</div>
</div>
@@ -176,9 +175,14 @@ export default {
<p class="text-muted" v-html="helpText"></p>
</div>
<div class="d-flex justify-content-center">
- <gl-deprecated-button variant="primary" @click="triggerManualJob(variables)">
+ <gl-button
+ variant="info"
+ category="primary"
+ :aria-label="__('Trigger manual job')"
+ @click="triggerManualJob(variables)"
+ >
{{ action.button_title }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 517da16dcf8..aa589989e8a 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -1,11 +1,11 @@
<script>
import { isEmpty } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import { GlLink, GlDeprecatedButton } from '@gitlab/ui';
+import { GlLink, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
-import Icon from '~/vue_shared/components/icon.vue';
import DetailRow from './sidebar_detail_row.vue';
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
@@ -19,15 +19,21 @@ export default {
ArtifactsBlock,
CommitBlock,
DetailRow,
- Icon,
+ GlIcon,
TriggerBlock,
StagesDropdown,
JobsContainer,
GlLink,
GlDeprecatedButton,
+ TooltipOnTruncate,
},
mixins: [timeagoMixin],
props: {
+ artifactHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
runnerHelpUrl: {
type: String,
required: false,
@@ -112,7 +118,11 @@ export default {
<div class="sidebar-container">
<div class="blocks-container">
<div class="block d-flex flex-nowrap align-items-center">
- <h4 class="my-0 mr-2 text-break-word">{{ job.name }}</h4>
+ <tooltip-on-truncate :title="job.name" truncate-target="child"
+ ><h4 class="my-0 mr-2 gl-text-truncate">
+ {{ job.name }}
+ </h4>
+ </tooltip-on-truncate>
<div class="flex-grow-1 flex-shrink-0 text-right">
<gl-link
v-if="job.retry_path"
@@ -157,7 +167,7 @@ export default {
class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left"
target="_blank"
>
- {{ __('Debug') }} <icon name="external-link" :size="14" />
+ {{ __('Debug') }} <gl-icon name="external-link" :size="14" />
</gl-link>
</div>
@@ -203,7 +213,7 @@ export default {
</p>
</div>
- <artifacts-block v-if="hasArtifact" :artifact="job.artifact" />
+ <artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" />
<trigger-block v-if="hasTriggers" :trigger="job.trigger" />
<commit-block
:is-last-block="hasStages"
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index b826007ec2c..7d541f93bad 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -1,9 +1,10 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlIcon, GlLink } from '@gitlab/ui';
export default {
name: 'SidebarDetailRow',
components: {
+ GlIcon,
GlLink,
},
props: {
@@ -37,7 +38,7 @@ export default {
<span v-if="hasTitle" class="font-weight-bold">{{ title }}:</span> {{ value }}
<span v-if="hasHelpURL" class="help-button float-right">
<gl-link :href="helpUrl" target="_blank" rel="noopener noreferrer nofollow">
- <i class="fa fa-question-circle" aria-hidden="true"></i>
+ <gl-icon name="question-o" />
</gl-link>
</span>
</p>
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 024a13ce102..6e15360b66c 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -18,6 +18,7 @@ export default () => {
},
render(createElement) {
const {
+ artifactHelpUrl,
deploymentHelpUrl,
runnerHelpUrl,
runnerSettingsUrl,
@@ -32,6 +33,7 @@ export default () => {
return createElement('job-app', {
props: {
+ artifactHelpUrl,
deploymentHelpUrl,
runnerHelpUrl,
runnerSettingsUrl,
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 1fb8e270e0e..8e172b4827c 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -12,6 +12,7 @@ import { deprecatedCreateFlash as flash } from './flash';
import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class LabelsSelect {
constructor(els, options = {}) {
@@ -173,7 +174,7 @@ export default class LabelsSelect {
})
.catch(() => flash(__('Error saving label update.')));
};
- $dropdown.glDropdown({
+ initDeprecatedJQueryDropdown($dropdown, {
showMenuAbove,
data(term, callback) {
const labelUrl = $dropdown.attr('data-labels');
@@ -203,7 +204,7 @@ export default class LabelsSelect {
callback(data);
if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
+ $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
}
})
.catch(() => flash(__('Error fetching labels.')));
@@ -348,7 +349,7 @@ export default class LabelsSelect {
} else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
- $dropdown.data('glDropdown').clearMenu();
+ $dropdown.data('deprecatedJQueryDropdown').clearMenu();
}
}
}
@@ -455,7 +456,7 @@ export default class LabelsSelect {
if ($dropdown.hasClass('js-issue-board-sidebar')) {
const previousSelection = $dropdown.attr('data-selected');
this.selected = previousSelection ? previousSelection.split(',') : [];
- $dropdown.data('glDropdown').updateLabel();
+ $dropdown.data('deprecatedJQueryDropdown').updateLabel();
}
},
preserveContext: true,
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 0ecf3301250..d7f5e6f8a5e 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
-import initWhatsNew from '~/whats_new';
function hideEndFade($scrollingTabs) {
$scrollingTabs.each(function scrollTabsLoop() {
@@ -14,6 +13,17 @@ function hideEndFade($scrollingTabs) {
function initDeferred() {
$(document).trigger('init.scrolling-tabs');
+
+ const whatsNewTriggerEl = document.querySelector('.js-whats-new-trigger');
+ if (whatsNewTriggerEl) {
+ whatsNewTriggerEl.addEventListener('click', () => {
+ import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
+ .then(({ default: initWhatsNew }) => {
+ initWhatsNew();
+ })
+ .catch(() => {});
+ });
+ }
}
export default function initLayoutNav() {
@@ -21,7 +31,6 @@ export default function initLayoutNav() {
contextualSidebar.bindEvents();
initFlyOutNav();
- initWhatsNew();
// We need to init it on DomContentLoaded as others could also call it
$(document).on('init.scrolling-tabs', () => {
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 4fed121779e..d2907f401c0 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -4,6 +4,7 @@ import { createUploadLink } from 'apollo-upload-client';
import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
import csrf from '~/lib/utils/csrf';
+import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
export const fetchPolicies = {
CACHE_FIRST: 'cache-first',
@@ -32,13 +33,35 @@ export default (resolvers = {}, config = {}) => {
credentials: 'same-origin',
};
+ const uploadsLink = ApolloLink.split(
+ operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
+ createUploadLink(httpOptions),
+ new BatchHttpLink(httpOptions),
+ );
+
+ const performanceBarLink = new ApolloLink((operation, forward) => {
+ return forward(operation).map(response => {
+ const httpResponse = operation.getContext().response;
+
+ if (PerformanceBarService.interceptor) {
+ PerformanceBarService.interceptor({
+ config: {
+ url: httpResponse.url,
+ },
+ headers: {
+ 'x-request-id': httpResponse.headers.get('x-request-id'),
+ 'x-gitlab-from-cache': httpResponse.headers.get('x-gitlab-from-cache'),
+ },
+ });
+ }
+
+ return response;
+ });
+ });
+
return new ApolloClient({
typeDefs: config.typeDefs,
- link: ApolloLink.split(
- operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
- createUploadLink(httpOptions),
- new BatchHttpLink(httpOptions),
- ),
+ link: ApolloLink.from([performanceBarLink, uploadsLink]),
cache: new InMemoryCache({
...config.cacheConfig,
freezeResults: config.assumeImmutableResults,
diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js
index a047cebc8ab..7e2665b910c 100644
--- a/app/assets/javascripts/lib/utils/axios_startup_calls.js
+++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js
@@ -10,6 +10,32 @@ const getFullUrl = req => {
return mergeUrlParams(req.params || {}, url);
};
+const handleStartupCall = async ({ fetchCall }, req) => {
+ const res = await fetchCall;
+ if (!res.ok) {
+ throw new Error(res.statusText);
+ }
+
+ const fetchHeaders = {};
+ res.headers.forEach((val, key) => {
+ fetchHeaders[key] = val;
+ });
+
+ const data = await res.clone().json();
+
+ Object.assign(req, {
+ adapter: () =>
+ Promise.resolve({
+ data,
+ status: res.status,
+ statusText: res.statusText,
+ headers: fetchHeaders,
+ config: req,
+ request: req,
+ }),
+ });
+};
+
const setupAxiosStartupCalls = axios => {
const { startup_calls: startupCalls } = window.gl || {};
@@ -17,35 +43,28 @@ const setupAxiosStartupCalls = axios => {
return;
}
- // TODO: To save performance of future axios calls, we can
- // remove this interceptor once the "startupCalls" have been loaded
- axios.interceptors.request.use(req => {
+ const remainingCalls = new Map(Object.entries(startupCalls));
+
+ const interceptor = axios.interceptors.request.use(async req => {
const fullUrl = getFullUrl(req);
- const existing = startupCalls[fullUrl];
-
- if (existing) {
- // eslint-disable-next-line no-param-reassign
- req.adapter = () =>
- existing.fetchCall.then(res => {
- const fetchHeaders = {};
- res.headers.forEach((val, key) => {
- fetchHeaders[key] = val;
- });
-
- // eslint-disable-next-line promise/no-nesting
- return res
- .clone()
- .json()
- .then(data => ({
- data,
- status: res.status,
- statusText: res.statusText,
- headers: fetchHeaders,
- config: req,
- request: req,
- }));
- });
+ const startupCall = remainingCalls.get(fullUrl);
+
+ if (!startupCall?.fetchCall) {
+ return req;
+ }
+
+ try {
+ await handleStartupCall(startupCall, req);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn(`[gitlab] Something went wrong with the startup call for "${fullUrl}"`, e);
+ }
+
+ remainingCalls.delete(fullUrl);
+
+ if (remainingCalls.size === 0) {
+ axios.interceptors.request.eject(interceptor);
}
return req;
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index e26b63fbb85..b193a8b2c9a 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -216,8 +216,9 @@ export const timeFor = (time, expiredLabel) => {
return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim();
};
+export const millisecondsPerDay = 1000 * 60 * 60 * 24;
+
export const getDayDifference = (a, b) => {
- const millisecondsPerDay = 1000 * 60 * 60 * 24;
const date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
const date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
@@ -642,6 +643,16 @@ export const secondsToMilliseconds = seconds => seconds * 1000;
export const secondsToDays = seconds => Math.round(seconds / 86400);
/**
+ * Returns the date n days after the date provided
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfDays number of days after
+ * @return {Date} the date following the date provided
+ */
+export const nDaysAfter = (date, numberOfDays) =>
+ new Date(newDate(date)).setDate(date.getDate() + numberOfDays);
+
+/**
* Returns the date after the date provided
*
* @param {Date} date the initial date
@@ -702,20 +713,14 @@ export const approximateDuration = (seconds = 0) => {
* @return {Date} the date object from the params
*/
export const dateFromParams = (year, month, day) => {
- const date = new Date();
-
- date.setFullYear(year);
- date.setMonth(month);
- date.setDate(day);
-
- return date;
+ return new Date(year, month, day);
};
/**
* A utility function which computes the difference in seconds
* between 2 dates.
*
- * @param {Date} startDate the start sate
+ * @param {Date} startDate the start date
* @param {Date} endDate the end date
*
* @return {Int} the difference in seconds
@@ -723,3 +728,18 @@ export const dateFromParams = (year, month, day) => {
export const differenceInSeconds = (startDate, endDate) => {
return (endDate.getTime() - startDate.getTime()) / 1000;
};
+
+/**
+ * A utility function which computes the difference in milliseconds
+ * between 2 dates.
+ *
+ * @param {Date|Int} startDate the start date. Can be either a date object or a unix timestamp.
+ * @param {Date|Int} endDate the end date. Can be either a date object or a unix timestamp. Defaults to now.
+ *
+ * @return {Int} the difference in milliseconds
+ */
+export const differenceInMilliseconds = (startDate, endDate = Date.now()) => {
+ const startDateInMS = startDate instanceof Date ? startDate.getTime() : startDate;
+ const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate;
+ return endDateInMS - startDateInMS;
+};
diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js
index ced44ab9817..1c5f6cefeda 100644
--- a/app/assets/javascripts/lib/utils/forms.js
+++ b/app/assets/javascripts/lib/utils/forms.js
@@ -14,3 +14,40 @@ export const serializeForm = form => {
return serializeFormEntries(entries);
};
+
+/**
+ * Check if the value provided is empty or not
+ *
+ * It is being used to check if a form input
+ * value has been set or not
+ *
+ * @param {String, Number, Array} - Any form value
+ * @returns {Boolean} - returns false if a value is set
+ *
+ * @example
+ * returns true for '', [], null, undefined
+ */
+export const isEmptyValue = value => value == null || value.length === 0;
+
+/**
+ * A form object serializer
+ *
+ * @param {Object} - Form Object
+ * @returns {Object} - Serialized Form Object
+ *
+ * @example
+ * Input
+ * {"project": {"value": "hello", "state": false}, "username": {"value": "john"}}
+ *
+ * Returns
+ * {"project": "hello", "username": "john"}
+ */
+export const serializeFormObject = form =>
+ Object.fromEntries(
+ Object.entries(form).reduce((acc, [name, { value }]) => {
+ if (!isEmptyValue(value)) {
+ acc.push([name, value]);
+ }
+ return acc;
+ }, []),
+ );
diff --git a/app/assets/javascripts/lib/utils/image_utility.js b/app/assets/javascripts/lib/utils/image_utility.js
index 2977ec821cb..af531f9f830 100644
--- a/app/assets/javascripts/lib/utils/image_utility.js
+++ b/app/assets/javascripts/lib/utils/image_utility.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
export function isImageLoaded(element) {
return element.complete && element.naturalHeight !== 0;
}
diff --git a/app/assets/javascripts/lib/utils/jquery_at_who.js b/app/assets/javascripts/lib/utils/jquery_at_who.js
new file mode 100644
index 00000000000..88111cb4ae4
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/jquery_at_who.js
@@ -0,0 +1,3 @@
+import 'jquery';
+import 'jquery.caret'; // required by at.js
+import '@gitlab/at.js';
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 7f0c65868c2..e8583fa951b 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -88,7 +88,11 @@ export default class Poll {
}
makeDelayedRequest(delay = 0) {
- this.timeoutID = setTimeout(() => this.makeRequest(), delay);
+ // So we don't make our specs artificially slower
+ this.timeoutID = setTimeout(
+ () => this.makeRequest(),
+ process.env.NODE_ENV !== 'test' ? delay : 1,
+ );
}
makeRequest() {
diff --git a/app/assets/javascripts/lib/utils/set.js b/app/assets/javascripts/lib/utils/set.js
index 3845d648b61..541934c4221 100644
--- a/app/assets/javascripts/lib/utils/set.js
+++ b/app/assets/javascripts/lib/utils/set.js
@@ -4,6 +4,5 @@
* @param {Set} superset The set to be considered as the superset.
* @returns {boolean}
*/
-// eslint-disable-next-line import/prefer-default-export
export const isSubset = (subset, superset) =>
Array.from(subset).every(value => superset.has(value));
diff --git a/app/assets/javascripts/lib/utils/simple_poll.js b/app/assets/javascripts/lib/utils/simple_poll.js
index 576a9ec880c..e4e9fb2e6fa 100644
--- a/app/assets/javascripts/lib/utils/simple_poll.js
+++ b/app/assets/javascripts/lib/utils/simple_poll.js
@@ -1,10 +1,12 @@
+import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
+
export default (fn, { interval = 2000, timeout = 60000 } = {}) => {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => {
- if (timeout === 0 || Date.now() - startTime < timeout) {
+ if (timeout === 0 || differenceInMilliseconds(startTime) < timeout) {
setTimeout(fn.bind(null, next, stop), interval);
} else {
reject(new Error('SIMPLE_POLL_TIMEOUT'));
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 8d23d177410..f4c6e4e3584 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, no-param-reassign, operator-assignment, consistent-return */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
+import Shortcuts from '~/behaviors/shortcuts/shortcuts';
const LINK_TAG_PATTERN = '[{text}](url)';
@@ -303,23 +304,67 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo
});
}
+/* eslint-disable @gitlab/require-i18n-strings */
+export function keypressNoteText(e) {
+ if (this.selectionStart === this.selectionEnd) {
+ return;
+ }
+ const keys = {
+ '*': '**{text}**', // wraps with bold character
+ _: '_{text}_', // wraps with italic character
+ '`': '`{text}`', // wraps with inline character
+ "'": "'{text}'", // single quotes
+ '"': '"{text}"', // double quotes
+ '[': '[{text}]', // brackets
+ '{': '{{text}}', // braces
+ '(': '({text})', // parentheses
+ '<': '<{text}>', // angle brackets
+ };
+ const tag = keys[e.key];
+
+ if (tag) {
+ e.preventDefault();
+
+ updateText({
+ tag,
+ textArea: this,
+ blockTag: '',
+ wrap: true,
+ select: '',
+ tagContent: '',
+ });
+ }
+}
+/* eslint-enable @gitlab/require-i18n-strings */
+
+export function updateTextForToolbarBtn($toolbarBtn) {
+ return updateText({
+ textArea: $toolbarBtn.closest('.md-area').find('textarea'),
+ tag: $toolbarBtn.data('mdTag'),
+ cursorOffset: $toolbarBtn.data('mdCursorOffset'),
+ blockTag: $toolbarBtn.data('mdBlock'),
+ wrap: !$toolbarBtn.data('mdPrepend'),
+ select: $toolbarBtn.data('mdSelect'),
+ tagContent: $toolbarBtn.data('mdTagContent'),
+ });
+}
+
export function addMarkdownListeners(form) {
- return $('.js-md', form)
+ $('.markdown-area', form)
+ .on('keydown', keypressNoteText)
+ .each(function attachTextareaShortcutHandlers() {
+ Shortcuts.initMarkdownEditorShortcuts($(this), updateTextForToolbarBtn);
+ });
+
+ const $allToolbarBtns = $('.js-md', form)
.off('click')
.on('click', function() {
- const $this = $(this);
- const tag = this.dataset.mdTag;
-
- return updateText({
- textArea: $this.closest('.md-area').find('textarea'),
- tag,
- cursorOffset: $this.data('mdCursorOffset'),
- blockTag: $this.data('mdBlock'),
- wrap: !$this.data('mdPrepend'),
- select: $this.data('mdSelect'),
- tagContent: $this.data('mdTagContent'),
- });
+ const $toolbarBtn = $(this);
+
+ return updateTextForToolbarBtn($toolbarBtn);
});
+
+ return $allToolbarBtns;
}
export function addEditorMarkdownListeners(editor) {
@@ -342,5 +387,11 @@ export function addEditorMarkdownListeners(editor) {
}
export function removeMarkdownListeners(form) {
+ $('.markdown-area', form)
+ .off('keydown', keypressNoteText)
+ .each(function removeTextareaShortcutHandlers() {
+ Shortcuts.removeMarkdownEditorShortcuts($(this));
+ });
+
return $('.js-md', form).off('click');
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index e2953ce330c..8ac6a44cba9 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -275,6 +275,81 @@ export const convertToSentenceCase = string => {
*/
export const convertToTitleCase = string => string.replace(/\b[a-z]/g, s => s.toUpperCase());
+const unicodeConversion = [
+ [/[ÀÁÂÃÅĀĂĄ]/g, 'A'],
+ [/[Æ]/g, 'AE'],
+ [/[ÇĆĈĊČ]/g, 'C'],
+ [/[ÈÉÊËĒĔĖĘĚ]/g, 'E'],
+ [/[ÌÍÎÏĨĪĬĮİ]/g, 'I'],
+ [/[Ððĥħ]/g, 'h'],
+ [/[ÑŃŅŇʼn]/g, 'N'],
+ [/[ÒÓÔÕØŌŎŐ]/g, 'O'],
+ [/[ÙÚÛŨŪŬŮŰŲ]/g, 'U'],
+ [/[ÝŶŸ]/g, 'Y'],
+ [/[Þñþńņň]/g, 'n'],
+ [/[ߌŜŞŠ]/g, 'S'],
+ [/[àáâãåāăąĸ]/g, 'a'],
+ [/[æ]/g, 'ae'],
+ [/[çćĉċč]/g, 'c'],
+ [/[èéêëēĕėęě]/g, 'e'],
+ [/[ìíîïĩīĭį]/g, 'i'],
+ [/[òóôõøōŏő]/g, 'o'],
+ [/[ùúûũūŭůűų]/g, 'u'],
+ [/[ýÿŷ]/g, 'y'],
+ [/[ĎĐ]/g, 'D'],
+ [/[ďđ]/g, 'd'],
+ [/[ĜĞĠĢ]/g, 'G'],
+ [/[ĝğġģŊŋſ]/g, 'g'],
+ [/[ĤĦ]/g, 'H'],
+ [/[ıśŝşš]/g, 's'],
+ [/[IJ]/g, 'IJ'],
+ [/[ij]/g, 'ij'],
+ [/[Ĵ]/g, 'J'],
+ [/[ĵ]/g, 'j'],
+ [/[Ķ]/g, 'K'],
+ [/[ķ]/g, 'k'],
+ [/[ĹĻĽĿŁ]/g, 'L'],
+ [/[ĺļľŀł]/g, 'l'],
+ [/[Œ]/g, 'OE'],
+ [/[œ]/g, 'oe'],
+ [/[ŔŖŘ]/g, 'R'],
+ [/[ŕŗř]/g, 'r'],
+ [/[ŢŤŦ]/g, 'T'],
+ [/[ţťŧ]/g, 't'],
+ [/[Ŵ]/g, 'W'],
+ [/[ŵ]/g, 'w'],
+ [/[ŹŻŽ]/g, 'Z'],
+ [/[źżž]/g, 'z'],
+ [/ö/g, 'oe'],
+ [/ü/g, 'ue'],
+ [/ä/g, 'ae'],
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [/Ö/g, 'Oe'],
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [/Ü/g, 'Ue'],
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [/Ä/g, 'Ae'],
+];
+
+/**
+ * Converts each non-ascii character in a string to
+ * it's ascii equivalent (if defined).
+ *
+ * e.g. "Dĭd söméònê äšk fœŕ Ůnĭċődę?" => "Did someone aesk foer Unicode?"
+ *
+ * @param {String} string
+ * @returns {String}
+ */
+export const convertUnicodeToAscii = string => {
+ let convertedString = string;
+
+ unicodeConversion.forEach(([regex, replacer]) => {
+ convertedString = convertedString.replace(regex, replacer);
+ });
+
+ return convertedString;
+};
+
/**
* Splits camelCase or PascalCase words
* e.g. HelloWorld => Hello World
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index be86f336bcd..664c0dbbc84 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const isObject = obj => obj && obj.constructor === Object;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 8077570158a..e9c3fe0a406 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -75,7 +75,7 @@ export function getParameterValues(sParam, url = window.location) {
* @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs
*/
export function mergeUrlParams(params, url, options = {}) {
- const { spreadArrays = false } = options;
+ const { spreadArrays = false, sort = false } = options;
const re = /^([^?#]*)(\?[^#]*)?(.*)/;
let merged = {};
const [, fullpath, query, fragment] = url.match(re);
@@ -108,7 +108,9 @@ export function mergeUrlParams(params, url, options = {}) {
Object.assign(merged, params);
- const newQuery = Object.keys(merged)
+ const mergedKeys = sort ? Object.keys(merged).sort() : Object.keys(merged);
+
+ const newQuery = mergedKeys
.filter(key => merged[key] !== null)
.map(key => {
let value = merged[key];
@@ -334,17 +336,32 @@ export function getWebSocketUrl(path) {
* Convert search query into an object
*
* @param {String} query from "document.location.search"
+ * @param {Object} options
+ * @param {Boolean} options.gatherArrays - gather array values into an Array
* @returns {Object}
*
* ex: "?one=1&two=2" into {one: 1, two: 2}
*/
-export function queryToObject(query) {
+export function queryToObject(query, options = {}) {
+ const { gatherArrays = false } = options;
const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query;
return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => {
const [key, value] = curr.split('=');
- if (value !== undefined) {
- accumulator[decodeURIComponent(key)] = decodeURIComponent(value);
+ if (value === undefined) {
+ return accumulator;
}
+ const decodedValue = decodeURIComponent(value);
+
+ if (gatherArrays && key.endsWith('[]')) {
+ const decodedKey = decodeURIComponent(key.slice(0, -2));
+ if (!Array.isArray(accumulator[decodedKey])) {
+ accumulator[decodedKey] = [];
+ }
+ accumulator[decodedKey].push(decodedValue);
+ } else {
+ accumulator[decodeURIComponent(key)] = decodedValue;
+ }
+
return accumulator;
}, {});
}
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 390294afcb7..622c40e0f35 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -1,7 +1,6 @@
import { joinPaths } from '~/lib/utils/url_utility';
// tell webpack to load assets from origin so that web workers don't break
-// eslint-disable-next-line import/prefer-default-export
export function resetServiceWorkersPublicPath() {
// __webpack_public_path__ is a global variable that can be used to adjust
// the webpack publicPath setting at runtime.
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index ed10c7646a8..8621a133776 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return */
import $ from 'jquery';
+import 'vendor/jquery.scrollTo';
// LineHighlighter
//
diff --git a/app/assets/javascripts/logs/stores/getters.js b/app/assets/javascripts/logs/stores/getters.js
index d92969c5389..dc392af8381 100644
--- a/app/assets/javascripts/logs/stores/getters.js
+++ b/app/assets/javascripts/logs/stores/getters.js
@@ -6,8 +6,16 @@ const mapTrace = ({ timestamp = null, pod = '', message = '' }) =>
export const trace = state => state.logs.lines.map(mapTrace).join('\n');
export const showAdvancedFilters = state => {
- const environment = state.environments.options.find(
- ({ name }) => name === state.environments.current,
+ if (state.environments.current) {
+ const environment = state.environments.options.find(
+ ({ name }) => name === state.environments.current,
+ );
+
+ return Boolean(environment?.enable_advanced_logs_querying);
+ }
+ const managedApp = state.managedApps.options.find(
+ ({ name }) => name === state.managedApps.current,
);
- return Boolean(environment?.enable_advanced_logs_querying);
+
+ return Boolean(managedApp?.enable_advanced_logs_querying);
};
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 1572e82a66c..9fcf881a1ac 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -20,25 +20,22 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// everything else
-import loadAwardsHandler from './awards_handler';
import { deprecatedCreateFlash as Flash, removeFlashClickListener } from './flash';
-import './gl_dropdown';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
import initLayoutNav from './layout_nav';
+import initAlertHandler from './alert_handler';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initUsagePingConsent from './usage_ping_consent';
-import initPerformanceBar from './performance_bar';
-import initSearchAutocomplete from './search_autocomplete';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
import initPersistentUserCallouts from './persistent_user_callouts';
-import { initUserTracking } from './tracking';
+import { initUserTracking, initDefaultTrackers } from './tracking';
import { __ } from './locale';
import 'ee_else_ce/main_ee';
@@ -112,8 +109,23 @@ function deferredInitialisation() {
initBroadcastNotifications();
initFrequentItemDropdowns();
initPersistentUserCallouts();
-
- if (document.querySelector('.search')) initSearchAutocomplete();
+ initDefaultTrackers();
+
+ const search = document.querySelector('#search');
+ if (search) {
+ search.addEventListener(
+ 'focus',
+ () => {
+ import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete')
+ .then(({ default: initSearchAutocomplete }) => {
+ const searchDropdown = initSearchAutocomplete();
+ searchDropdown.onSearchInputFocus();
+ })
+ .catch(() => {});
+ },
+ { once: true },
+ );
+ }
addSelectOnFocusBehaviour('.js-select-on-focus');
@@ -155,8 +167,6 @@ function deferredInitialisation() {
viewport: '.layout-page',
});
- loadAwardsHandler();
-
// Adding a helper class to activate animations only after all is rendered
setTimeout(() => $body.addClass('page-initialised'), 1000);
}
@@ -166,10 +176,9 @@ document.addEventListener('DOMContentLoaded', () => {
const $document = $(document);
const bootstrapBreakpoint = bp.getBreakpointSize();
- if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' });
-
initUserTracking();
initLayoutNav();
+ initAlertHandler();
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index f220e9e0192..c3fbb5d6acf 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class Members {
constructor() {
@@ -37,7 +38,7 @@ export default class Members {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
- $btn.glDropdown({
+ initDeprecatedJQueryDropdown($btn, {
selectable: true,
isSelectable: (selected, $el) => this.dropdownIsSelectable(selected, $el),
fieldName: $btn.data('fieldName'),
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 8322d36faee..79a4c3700ef 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -66,6 +66,14 @@ MergeRequest.prototype.showAllCommits = function() {
MergeRequest.prototype.initMRBtnListeners = function() {
const _this = this;
+
+ $('.report-abuse-link').on('click', e => {
+ // this is needed because of the implementation of
+ // the dropdown toggle and Report Abuse needing to be
+ // linked to another page.
+ e.stopPropagation();
+ });
+
return $('.btn-close, .btn-reopen').on('click', function(e) {
const $this = $(this);
const shouldSubmit = $this.hasClass('btn-comment');
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 94b6ba7b1ce..b7cf39db00c 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,6 +1,7 @@
/* eslint-disable no-new, class-methods-use-this */
import $ from 'jquery';
+import 'vendor/jquery.scrollTo';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
import createEventHub from '~/helpers/event_hub_factory';
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index caa45184bfc..52f6786ca28 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -4,10 +4,11 @@
import $ from 'jquery';
import { template, escape } from 'lodash';
-import { __ } from '~/locale';
-import '~/gl_dropdown';
+import { __, sprintf } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import Api from '~/api';
import axios from './lib/utils/axios_utils';
-import { timeFor } from './lib/utils/datetime_utility';
+import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
import boardsStore, {
boardStoreIssueSet,
@@ -34,10 +35,10 @@ export default class MilestoneSelect {
$els.each((i, dropdown) => {
let milestoneLinkNoneTemplate,
milestoneLinkTemplate,
+ milestoneExpiredLinkTemplate,
selectedMilestone,
selectedMilestoneDefault;
const $dropdown = $(dropdown);
- const milestonesUrl = $dropdown.data('milestones');
const issueUpdateURL = $dropdown.data('issueUpdate');
const showNo = $dropdown.data('showNo');
const showAny = $dropdown.data('showAny');
@@ -63,59 +64,109 @@ export default class MilestoneSelect {
milestoneLinkTemplate = template(
'<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
);
+ milestoneExpiredLinkTemplate = template(
+ '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %> (Past due)</a>',
+ );
milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`;
}
- return $dropdown.glDropdown({
+ return initDeprecatedJQueryDropdown($dropdown, {
showMenuAbove,
- data: (term, callback) =>
- axios.get(milestonesUrl).then(({ data }) => {
- const extraOptions = [];
- if (showAny) {
- extraOptions.push({
- id: null,
- name: null,
- title: __('Any milestone'),
- });
- }
- if (showNo) {
- extraOptions.push({
- id: -1,
- name: __('No milestone'),
- title: __('No milestone'),
- });
- }
- if (showUpcoming) {
- extraOptions.push({
- id: -2,
- name: '#upcoming',
- title: __('Upcoming'),
- });
- }
- if (showStarted) {
- extraOptions.push({
- id: -3,
- name: '#started',
- title: __('Started'),
- });
- }
- if (extraOptions.length) {
- extraOptions.push({ type: 'divider' });
- }
+ data: (term, callback) => {
+ let contextId = parseInt($dropdown.get(0).dataset.projectId, 10);
+ let getMilestones = Api.projectMilestones.bind(Api);
+ const reqParams = { state: 'active', include_parent_milestones: true };
- callback(extraOptions.concat(data));
- if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
- }
- $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`).addClass('is-active');
- }),
- renderRow: milestone => `
- <li data-milestone-id="${escape(milestone.name)}">
+ if (term) {
+ reqParams.search = term.trim();
+ }
+
+ if (!contextId) {
+ contextId = $dropdown.get(0).dataset.groupId;
+ delete reqParams.include_parent_milestones;
+ getMilestones = Api.groupMilestones.bind(Api);
+ }
+
+ // We don't use $.data() as it caches initial value and never updates!
+ return getMilestones(contextId, reqParams)
+ .then(({ data }) =>
+ data
+ .map(m => ({
+ ...m,
+ // Public API includes `title` instead of `name`.
+ name: m.title,
+ }))
+ .sort((mA, mB) => {
+ // Move all expired milestones to the bottom.
+ if (mA.expired) {
+ return 1;
+ }
+ if (mB.expired) {
+ return -1;
+ }
+ return 0;
+ }),
+ )
+ .then(data => {
+ const extraOptions = [];
+ if (showAny) {
+ extraOptions.push({
+ id: null,
+ name: null,
+ title: __('Any milestone'),
+ });
+ }
+ if (showNo) {
+ extraOptions.push({
+ id: -1,
+ name: __('No milestone'),
+ title: __('No milestone'),
+ });
+ }
+ if (showUpcoming) {
+ extraOptions.push({
+ id: -2,
+ name: '#upcoming',
+ title: __('Upcoming'),
+ });
+ }
+ if (showStarted) {
+ extraOptions.push({
+ id: -3,
+ name: '#started',
+ title: __('Started'),
+ });
+ }
+ if (extraOptions.length) {
+ extraOptions.push({ type: 'divider' });
+ }
+
+ callback(extraOptions.concat(data));
+ if (showMenuAbove) {
+ $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
+ }
+ $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
+ });
+ },
+ renderRow: milestone => {
+ const milestoneName = milestone.title || milestone.name;
+ let milestoneDisplayName = escape(milestoneName);
+
+ if (milestone.expired) {
+ milestoneDisplayName = sprintf(__('%{milestone} (expired)'), {
+ milestone: milestoneDisplayName,
+ });
+ }
+
+ return `
+ <li data-milestone-id="${escape(milestoneName)}">
<a href='#' class='dropdown-menu-milestone-link'>
- ${escape(milestone.title)}
+ ${milestoneDisplayName}
</a>
</li>
- `,
+ `;
+ },
filterable: true,
+ filterRemote: true,
search: {
fields: ['title'],
},
@@ -149,7 +200,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
- $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
+ $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: clickEvent => {
@@ -237,7 +288,16 @@ export default class MilestoneSelect {
if (data.milestone != null) {
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
- $value.html(milestoneLinkTemplate(data.milestone));
+ $value.html(
+ data.milestone.expired
+ ? milestoneExpiredLinkTemplate({
+ ...data.milestone,
+ remaining: sprintf(__('%{due_date} (Past due)'), {
+ due_date: dateInWords(parsePikadayDate(data.milestone.due_date)),
+ }),
+ })
+ : milestoneLinkTemplate(data.milestone),
+ );
return $sidebarCollapsedValue
.attr(
'data-original-title',
diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue
index d0179ab5509..5ee917573ce 100644
--- a/app/assets/javascripts/milestones/project_milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue
@@ -1,9 +1,9 @@
<script>
import {
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIcon,
@@ -13,12 +13,14 @@ import { __, sprintf } from '~/locale';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+const SEARCH_DEBOUNCE_MS = 250;
+
export default {
components: {
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIcon,
@@ -89,10 +91,21 @@ export default {
return this.requestCount !== 0;
},
},
+ created() {
+ // This method is defined here instead of in `methods`
+ // because we need to access the .cancel() method
+ // lodash attaches to the function, which is
+ // made inaccessible by Vue. More info:
+ // https://stackoverflow.com/a/52988020/1063392
+ this.debouncedSearchMilestones = debounce(this.searchMilestones, SEARCH_DEBOUNCE_MS);
+ },
mounted() {
this.fetchMilestones();
},
methods: {
+ focusSearchBox() {
+ this.$refs.searchBox.$el.querySelector('input').focus();
+ },
fetchMilestones() {
this.requestCount += 1;
@@ -108,7 +121,7 @@ export default {
this.requestCount -= 1;
});
},
- searchMilestones: debounce(function searchMilestones() {
+ searchMilestones() {
this.requestCount += 1;
const options = {
search: this.searchQuery,
@@ -133,7 +146,14 @@ export default {
.finally(() => {
this.requestCount -= 1;
});
- }, 100),
+ },
+ onSearchBoxInput() {
+ this.debouncedSearchMilestones();
+ },
+ onSearchBoxEnter() {
+ this.debouncedSearchMilestones.cancel();
+ this.searchMilestones();
+ },
toggleMilestoneSelection(clickedMilestone) {
if (!clickedMilestone) return [];
@@ -168,7 +188,7 @@ export default {
</script>
<template>
- <gl-new-dropdown>
+ <gl-dropdown v-bind="$attrs" class="project-milestone-combobox" @shown="focusSearchBox">
<template slot="button-content">
<span ref="buttonText" class="flex-grow-1 ml-1 text-muted">{{
selectedMilestonesLabel
@@ -176,39 +196,41 @@ export default {
<gl-icon name="chevron-down" />
</template>
- <gl-new-dropdown-header>
+ <gl-dropdown-section-header>
<span class="text-center d-block">{{ $options.translations.selectMilestone }}</span>
- </gl-new-dropdown-header>
+ </gl-dropdown-section-header>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
<gl-search-box-by-type
+ ref="searchBox"
v-model.trim="searchQuery"
- class="m-2"
+ class="gl-m-3"
:placeholder="this.$options.translations.searchMilestones"
- @input="searchMilestones"
+ @input="onSearchBoxInput"
+ @keydown.enter.prevent="onSearchBoxEnter"
/>
- <gl-new-dropdown-item @click="onMilestoneClicked(null)">
+ <gl-dropdown-item @click="onMilestoneClicked(null)">
<span :class="{ 'pl-4': true, 'selected-item': selectedMilestones.length === 0 }">
{{ $options.translations.noMilestone }}
</span>
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
<template v-if="isLoading">
<gl-loading-icon />
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
</template>
<template v-else-if="noResults">
<div class="dropdown-item-space">
<span ref="noResults" class="pl-4">{{ $options.translations.noResultsLabel }}</span>
</div>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
</template>
<template v-else-if="dropdownItems.length">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="item in dropdownItems"
:key="item"
role="milestone option"
@@ -217,12 +239,12 @@ export default {
<span :class="{ 'pl-4': true, 'selected-item': isSelectedMilestone(item) }">
{{ item }}
</span>
- </gl-new-dropdown-item>
- <gl-new-dropdown-divider />
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
</template>
- <gl-new-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url">
+ <gl-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url">
<span class="pl-4">{{ item.text }}</span>
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
index 909ae2980d2..8f9c181258f 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget.vue
@@ -236,7 +236,7 @@ export default {
>
<gl-badge :variant="isFiring ? 'danger' : 'neutral'" class="d-flex-center text-truncate">
<gl-icon name="warning" :size="16" class="flex-shrink-0" />
- <span class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me">
+ <span class="text-truncate gl-pl-2">
<gl-sprintf
:message="
hasMultipleAlerts ? multipleAlertsSummary.message : singleAlertSummary.message
diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
index 5fa0da53a04..132df9c9516 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
@@ -7,16 +7,16 @@ import {
GlButtonGroup,
GlFormGroup,
GlFormInput,
- GlNewDropdown as GlDropdown,
- GlNewDropdownItem as GlDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlModal,
GlTooltipDirective,
+ GlIcon,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import Translate from '~/vue_shared/translate';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import Icon from '~/vue_shared/components/icon.vue';
import { alertsValidator, queriesValidator } from '../validators';
import { OPERATORS } from '../constants';
@@ -44,7 +44,7 @@ export default {
GlDropdownItem,
GlModal,
GlLink,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -242,7 +242,7 @@ export default {
<template #description>
<div class="d-flex align-items-center">
{{ __('Single or combined queries') }}
- <icon
+ <gl-icon
v-gl-tooltip="$options.alertQueryText.descriptionTooltip"
name="question"
class="gl-ml-2"
diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
index ad176637538..446ca8e5090 100644
--- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
+++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg';
import { chartHeight } from '../../constants';
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 054111c203e..6bae3fdcc2e 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,10 +1,9 @@
<script>
import { isEmpty, omit, throttle } from 'lodash';
-import { GlLink, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
+import { GlLink, GlTooltip, GlResizeObserverDirective, GlIcon } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
-import Icon from '~/vue_shared/components/icon.vue';
import { panelTypes, chartHeight, lineTypes, lineWidths, legendLayoutTypes } from '../../constants';
import { getYAxisOptions, getTimeAxisOptions, getChartGrid, getTooltipFormatter } from './options';
import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
@@ -27,7 +26,7 @@ export default {
GlTooltip,
GlChartSeriesLabel,
GlLink,
- Icon,
+ GlIcon,
},
directives: {
GlResizeObserverDirective,
@@ -407,7 +406,7 @@ export default {
{{ __('Deployed') }}
</template>
<div slot="tooltipContent" class="d-flex align-items-center">
- <icon name="commit" class="mr-2" />
+ <gl-icon name="commit" class="mr-2" />
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 24aa7b3f504..cbfacd73b5b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -2,7 +2,7 @@
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import Mousetrap from 'mousetrap';
-import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlModalDirective, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import DashboardHeader from './dashboard_header.vue';
import DashboardPanel from './dashboard_panel.vue';
import { s__ } from '~/locale';
@@ -10,7 +10,6 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import Icon from '~/vue_shared/components/icon.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
@@ -33,7 +32,7 @@ export default {
VueDraggable,
DashboardHeader,
DashboardPanel,
- Icon,
+ GlIcon,
GlButton,
GraphGroup,
EmptyState,
@@ -473,7 +472,7 @@ export default {
@click="removePanel(groupData.key, groupData.panels, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')">
- <icon name="close" />
+ <gl-icon name="close" />
</a>
</div>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
index 68afa2ace01..070277fe2dc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -2,9 +2,9 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import {
GlDeprecatedButton,
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
GlModal,
GlIcon,
GlModalDirective,
@@ -23,9 +23,9 @@ import { getAddMetricTrackingOptions } from '../utils';
export default {
components: {
GlDeprecatedButton,
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
GlModal,
GlIcon,
DuplicateDashboardModal,
@@ -143,7 +143,7 @@ export default {
as part of https://gitlab.com/gitlab-org/gitlab-ui/-/issues/936
The variant will create a dropdown with an icon, no text and no caret
-->
- <gl-new-dropdown
+ <gl-dropdown
v-gl-tooltip
data-testid="actions-menu"
data-qa-selector="actions_menu_dropdown"
@@ -157,13 +157,13 @@ export default {
</template>
<template v-if="addingMetricsAvailable">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-gl-modal="$options.modalIds.addMetric"
data-qa-selector="add_metric_button"
data-testid="add-metric-item"
>
{{ $options.i18n.addMetric }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<gl-modal
ref="addMetricModal"
:modal-id="$options.modalIds.addMetric"
@@ -194,20 +194,20 @@ export default {
</gl-modal>
</template>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-if="isMenuItemEnabled.addPanel"
data-testid="add-panel-item-enabled"
:to="newPanelPageLocation"
>
{{ $options.i18n.addPanel }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<!--
wrapper for tooltip as button can be `disabled`
https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
-->
<div v-else v-gl-tooltip :title="$options.i18n.addPanelInfo">
- <gl-new-dropdown-item
+ <gl-dropdown-item
:alt="$options.i18n.addPanelInfo"
:to="newPanelPageLocation"
data-testid="add-panel-item-disabled"
@@ -215,24 +215,24 @@ export default {
class="gl-cursor-not-allowed"
>
<span class="gl-text-gray-400">{{ $options.i18n.addPanel }}</span>
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
</div>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-if="isMenuItemEnabled.editDashboard"
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
data-qa-selector="edit_dashboard_button_enabled"
data-testid="edit-dashboard-item-enabled"
>
{{ $options.i18n.editDashboard }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<!--
wrapper for tooltip as button can be `disabled`
https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
-->
<div v-else v-gl-tooltip :title="$options.i18n.editDashboardInfo">
- <gl-new-dropdown-item
+ <gl-dropdown-item
:alt="$options.i18n.editDashboardInfo"
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
data-testid="edit-dashboard-item-disabled"
@@ -240,16 +240,16 @@ export default {
class="gl-cursor-not-allowed"
>
<span class="gl-text-gray-400">{{ $options.i18n.editDashboard }}</span>
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
</div>
<template v-if="isMenuItemShown.duplicateDashboard">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-gl-modal="$options.modalIds.duplicateDashboard"
data-testid="duplicate-dashboard-item"
>
{{ $options.i18n.duplicateDashboard }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<duplicate-dashboard-modal
:default-branch="defaultBranch"
@@ -259,25 +259,25 @@ export default {
/>
</template>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-if="selectedDashboard"
data-testid="star-dashboard-item"
:disabled="isUpdatingStarredValue"
@click="toggleStarredValue()"
>
{{ selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-gl-modal="$options.modalIds.createDashboard"
data-testid="create-dashboard-item"
:disabled="!isMenuItemEnabled.createDashboard"
:class="{ 'monitoring-actions-item-disabled': !isMenuItemEnabled.createDashboard }"
>
{{ $options.i18n.createDashboard }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<template v-if="isMenuItemEnabled.createDashboard">
<create-dashboard-modal
@@ -287,5 +287,5 @@ export default {
:project-path="projectPath"
/>
</template>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 6a7bf81c643..e468728a954 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -3,17 +3,17 @@ import { debounce } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlButton,
- GlNewDropdown,
+ GlDropdown,
GlLoadingIcon,
- GlNewDropdownItem,
- GlNewDropdownHeader,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
GlModalDirective,
GlTooltipDirective,
+ GlIcon,
} from '@gitlab/ui';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import Icon from '~/vue_shared/components/icon.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
@@ -26,12 +26,12 @@ import { timezones } from '../format_date';
export default {
components: {
- Icon,
+ GlIcon,
GlButton,
- GlNewDropdown,
+ GlDropdown,
GlLoadingIcon,
- GlNewDropdownItem,
- GlNewDropdownHeader,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
@@ -181,7 +181,7 @@ export default {
<span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
<div class="mb-2 pr-2 d-flex d-sm-block">
- <gl-new-dropdown
+ <gl-dropdown
id="monitor-environments-dropdown"
ref="monitorEnvironmentsDropdown"
class="flex-grow-1"
@@ -191,12 +191,12 @@ export default {
:text="environmentDropdownText"
>
<div class="d-flex flex-column overflow-hidden">
- <gl-new-dropdown-header>{{ __('Environment') }}</gl-new-dropdown-header>
- <gl-search-box-by-type class="m-2" @input="debouncedEnvironmentsSearch" />
+ <gl-dropdown-section-header>{{ __('Environment') }}</gl-dropdown-section-header>
+ <gl-search-box-by-type class="gl-m-3" @input="debouncedEnvironmentsSearch" />
<gl-loading-icon v-if="environmentsLoading" :inline="true" />
<div v-else class="flex-fill overflow-auto">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="environment in filteredEnvironments"
:key="environment.id"
:is-check-item="true"
@@ -204,7 +204,7 @@ export default {
:href="getEnvironmentPath(environment.id)"
>
{{ environment.name }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
</div>
<div
v-show="shouldShowEnvironmentsDropdownNoMatchedMsg"
@@ -214,7 +214,7 @@ export default {
{{ __('No matching results') }}
</div>
</div>
- </gl-new-dropdown>
+ </gl-dropdown>
</div>
<div class="mb-2 pr-2 d-flex d-sm-block">
@@ -260,7 +260,7 @@ export default {
target="_blank"
rel="noopener noreferrer"
>
- {{ __('View full dashboard') }} <icon name="external-link" />
+ {{ __('View full dashboard') }} <gl-icon name="external-link" />
</gl-button>
</div>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 278858d3a94..18310f7c71e 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -6,9 +6,9 @@ import {
GlIcon,
GlLink,
GlLoadingIcon,
- GlNewDropdown as GlDropdown,
- GlNewDropdownItem as GlDropdownItem,
- GlNewDropdownDivider as GlDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
GlModal,
GlModalDirective,
GlSprintf,
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index aed27b5ea51..932efeaaf0e 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -2,10 +2,10 @@
import { mapState, mapGetters } from 'vuex';
import {
GlIcon,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownHeader,
- GlNewDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
GlSearchBoxByType,
GlModalDirective,
} from '@gitlab/ui';
@@ -17,10 +17,10 @@ const events = {
export default {
components: {
GlIcon,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownHeader,
- GlNewDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
GlSearchBoxByType,
},
directives: {
@@ -73,21 +73,21 @@ export default {
};
</script>
<template>
- <gl-new-dropdown
+ <gl-dropdown
toggle-class="dropdown-menu-toggle"
menu-class="monitor-dashboard-dropdown-menu"
:text="selectedDashboardText"
>
<div class="d-flex flex-column overflow-hidden">
- <gl-new-dropdown-header>{{ __('Dashboard') }}</gl-new-dropdown-header>
+ <gl-dropdown-section-header>{{ __('Dashboard') }}</gl-dropdown-section-header>
<gl-search-box-by-type
ref="monitorDashboardsDropdownSearch"
v-model="searchTerm"
- class="m-2"
+ class="gl-m-3"
/>
<div class="flex-fill overflow-auto">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="dashboard in starredDashboards"
:key="dashboard.path"
:is-check-item="true"
@@ -95,28 +95,28 @@ export default {
@click="selectDashboard(dashboard)"
>
<div class="gl-display-flex">
- <div class="gl-flex-grow-1 gl-min-w-0">
- <div class="gl-word-break-all">
- {{ dashboardDisplayName(dashboard) }}
- </div>
- </div>
- <gl-icon class="text-muted gl-flex-shrink-0" name="star" />
+ <span class="gl-flex-grow-1 gl-min-w-0 gl-overflow-hidden gl-overflow-wrap-break">
+ {{ dashboardDisplayName(dashboard) }}
+ </span>
+ <gl-icon class="text-muted gl-flex-shrink-0 gl-ml-3 gl-align-self-center" name="star" />
</div>
- </gl-new-dropdown-item>
- <gl-new-dropdown-divider
+ </gl-dropdown-item>
+ <gl-dropdown-divider
v-if="starredDashboards.length && nonStarredDashboards.length"
ref="starredListDivider"
/>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="dashboard in nonStarredDashboards"
:key="dashboard.path"
:is-check-item="true"
:is-checked="dashboard.path === selectedDashboardPath"
@click="selectDashboard(dashboard)"
>
- {{ dashboardDisplayName(dashboard) }}
- </gl-new-dropdown-item>
+ <span class="gl-overflow-hidden gl-overflow-wrap-break">
+ {{ dashboardDisplayName(dashboard) }}
+ </span>
+ </gl-dropdown-item>
</div>
<div
@@ -127,5 +127,5 @@ export default {
{{ __('No matching results') }}
</div>
</div>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue
index b60c87fee82..f07483c34b8 100644
--- a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue
+++ b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue
@@ -1,14 +1,14 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import sum from 'lodash/sum';
-import { GlDeprecatedButton, GlCard, GlIcon } from '@gitlab/ui';
+import { GlButton, GlCard, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import { monitoringDashboard } from '~/monitoring/stores';
import MetricEmbed from './metric_embed.vue';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlCard,
GlIcon,
MetricEmbed,
@@ -78,15 +78,16 @@ export default {
:body-class="bodyClass"
>
<template #header>
- <gl-deprecated-button
- class="collapsible-card-btn d-flex text-decoration-none"
+ <gl-button
+ class="collapsible-card-btn gl-display-flex gl-text-decoration-none gl-reset-color! gl-hover-text-blue-800! gl-shadow-none!"
:aria-label="buttonLabel"
variant="link"
+ category="tertiary"
@click="toggleCollapsed"
>
<gl-icon class="mr-1" :name="arrowIconName" />
{{ buttonLabel }}
- </gl-deprecated-button>
+ </gl-button>
</template>
<div class="d-flex flex-wrap">
<metric-embed
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
index 9cf492dd537..499823fae3f 100644
--- a/app/assets/javascripts/monitoring/components/group_empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlEmptyState } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { metricStates } from '../constants';
diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue
index 0e9605450ed..e0d9f92440b 100644
--- a/app/assets/javascripts/monitoring/components/refresh_button.vue
+++ b/app/assets/javascripts/monitoring/components/refresh_button.vue
@@ -4,9 +4,9 @@ import { mapActions } from 'vuex';
import {
GlButtonGroup,
GlButton,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
import { n__, __ } from '~/locale';
@@ -48,9 +48,9 @@ export default {
components: {
GlButtonGroup,
GlButton,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -152,27 +152,27 @@ export default {
icon="retry"
@click="refresh"
/>
- <gl-new-dropdown
+ <gl-dropdown
v-if="!disableMetricDashboardRefreshRate"
v-gl-tooltip
:title="s__('Metrics|Set refresh rate')"
:text="dropdownText"
>
- <gl-new-dropdown-item
+ <gl-dropdown-item
:is-check-item="true"
:is-checked="refreshInterval === null"
@click="removeRefreshInterval()"
- >{{ __('Off') }}</gl-new-dropdown-item
+ >{{ __('Off') }}</gl-dropdown-item
>
- <gl-new-dropdown-divider />
- <gl-new-dropdown-item
+ <gl-dropdown-divider />
+ <gl-dropdown-item
v-for="(option, i) in $options.refreshIntervals"
:key="i"
:is-check-item="true"
:is-checked="isChecked(option)"
@click="setRefreshInterval(option)"
- >{{ option.label }}</gl-new-dropdown-item
+ >{{ option.label }}</gl-dropdown-item
>
- </gl-new-dropdown>
+ </gl-dropdown>
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
index 25d900b07ad..7c4fb135ec8 100644
--- a/app/assets/javascripts/monitoring/components/variables_section.vue
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -37,7 +37,11 @@ export default {
};
</script>
<template>
- <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
+ <div
+ ref="variablesSection"
+ class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section"
+ data-qa-selector="variables_content"
+ >
<div v-for="variable in variables" :key="variable.name" class="mb-1 pr-2 d-flex d-sm-block">
<component
:is="variableField(variable.type)"
@@ -46,6 +50,7 @@ export default {
:value="variable.value"
:name="variable.name"
:options="variable.options"
+ data-qa-selector="variable_item"
@input="refreshDashboard(variable, $event)"
/>
</div>
diff --git a/app/assets/javascripts/monitoring/csv_export.js b/app/assets/javascripts/monitoring/csv_export.js
index 734e8dc07a7..20cfa23e9b4 100644
--- a/app/assets/javascripts/monitoring/csv_export.js
+++ b/app/assets/javascripts/monitoring/csv_export.js
@@ -125,7 +125,6 @@ const csvData = (metricHeaders, metricValues) => {
* @param {Object} graphData - Panel contents
* @returns {String}
*/
-// eslint-disable-next-line import/prefer-default-export
export const graphDataToCsv = graphData => {
const delimiter = ',';
const br = '\r\n';
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/actions.js b/app/assets/javascripts/monitoring/stores/embed_group/actions.js
index 4a7572bdbd9..ca0d2e5ba35 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/actions.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/actions.js
@@ -1,4 +1,3 @@
import * as types from './mutation_types';
-// eslint-disable-next-line import/prefer-default-export
export const addModule = ({ commit }, data) => commit(types.ADD_MODULE, data);
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/getters.js b/app/assets/javascripts/monitoring/stores/embed_group/getters.js
index 096d8d03096..47db787dea5 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/getters.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/getters.js
@@ -1,3 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const metricsWithData = (state, getters, rootState, rootGetters) =>
state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length);
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
index 7fd3f0f8647..288e6db4151 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const ADD_MODULE = 'ADD_MODULE';
diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue
index e6bf7a6ec02..bf810978648 100644
--- a/app/assets/javascripts/mr_popover/components/mr_popover.vue
+++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue
@@ -1,6 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
+import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import query from '../queries/merge_request.query.graphql';
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index b96a111cf13..8e123c14814 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,16 +1,16 @@
import $ from 'jquery';
-import '~/gl_dropdown';
import Api from './api';
import { mergeUrlParams } from './lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from './locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class NamespaceSelect {
constructor(opts) {
const isFilter = parseBoolean(opts.dropdown.dataset.isFilter);
const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
- $(opts.dropdown).glDropdown({
+ initDeprecatedJQueryDropdown($(opts.dropdown), {
filterable: true,
selectable: true,
filterRemote: true,
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index fa1afdcd16f..3bbaa44ec42 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import marked from 'marked';
import { sanitize } from 'dompurify';
import katex from 'katex';
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index b36761993ea..856c8f31796 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { sanitize } from 'dompurify';
import Prompt from '../prompt.vue';
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 3940b4b4724..340fbe4d887 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -11,13 +11,12 @@ old_notes_spec.js is the spec for the legacy, jQuery notes application. It has n
*/
import $ from 'jquery';
+import '~/lib/utils/jquery_at_who';
import { escape, uniqueId } from 'lodash';
import Cookies from 'js-cookie';
import Autosize from 'autosize';
-import 'jquery.caret'; // required by at.js
-import '@gitlab/at.js';
import Vue from 'vue';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import AjaxCache from '~/lib/utils/ajax_cache';
import syntaxHighlight from '~/syntax_highlight';
import axios from './lib/utils/axios_utils';
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 7cfff98e9f7..54fcf41ca50 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -3,7 +3,7 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { isEmpty } from 'lodash';
import Autosize from 'autosize';
-import { GlAlert, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlIntersperse, GlLink, GlSprintf, GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
@@ -20,7 +20,6 @@ import eventHub from '../event_hub';
import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
@@ -33,7 +32,7 @@ export default {
discussionLockedWidget,
markdownField,
userAvatarLink,
- loadingButton,
+ GlButton,
TimelineEntryItem,
GlAlert,
GlIntersperse,
@@ -102,6 +101,9 @@ export default {
noteable: this.noteableDisplayName,
});
},
+ buttonVariant() {
+ return this.isOpen ? 'warning' : 'default';
+ },
actionButtonClassNames() {
return {
'btn-reopen': !this.isOpen,
@@ -378,7 +380,7 @@ export default {
dir="auto"
:disabled="isSubmitting"
name="note[note]"
- class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
+ class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area qa-comment-input"
data-supports-quick-actions="true"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
@@ -395,7 +397,7 @@ export default {
:secondary-button-text="__('Cancel')"
variant="warning"
:dismissible="false"
- @primaryAction="forceCloseIssue"
+ @primaryAction="toggleBlockedIssueWarning(false) && forceCloseIssue()"
@secondaryAction="toggleBlockedIssueWarning(false) && enableButton()"
>
<p>
@@ -421,27 +423,28 @@ export default {
<div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
- <button
+ <gl-button
:disabled="isSubmitButtonDisabled"
- class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button"
+ class="js-comment-button js-comment-submit-button qa-comment-button"
type="submit"
+ category="primary"
+ variant="success"
:data-track-label="trackingLabel"
data-track-event="click_button"
@click.prevent="handleSave()"
+ >{{ commentButtonTitle }}</gl-button
>
- {{ commentButtonTitle }}
- </button>
- <button
+ <gl-button
:disabled="isSubmitButtonDisabled"
name="button"
- type="button"
- class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
+ category="primary"
+ variant="success"
+ class="note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
data-display="static"
data-toggle="dropdown"
+ icon="chevron-down"
:aria-label="__('Open comment type dropdown')"
- >
- <i aria-hidden="true" class="fa fa-caret-down toggle-icon"></i>
- </button>
+ />
<ul class="note-type-dropdown dropdown-open-top dropdown-menu">
<li :class="{ 'droplab-item-selected': noteType === 'comment' }">
@@ -465,11 +468,7 @@ export default {
</li>
<li class="divider droplab-item-ignore"></li>
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
- <button
- type="button"
- class="btn btn-transparent qa-discussion-option"
- @click.prevent="setNoteType('discussion')"
- >
+ <button class="qa-discussion-option" @click.prevent="setNoteType('discussion')">
<i aria-hidden="true" class="fa fa-check icon"></i>
<div class="description">
<strong>{{ __('Start thread') }}</strong>
@@ -480,17 +479,19 @@ export default {
</ul>
</div>
- <loading-button
+ <gl-button
v-if="canToggleIssueState && !isToggleBlockedIssueWarning"
:loading="isToggleStateButtonLoading"
- :container-class="[
+ category="secondary"
+ :variant="buttonVariant"
+ :class="[
actionButtonClassNames,
- 'btn btn-comment btn-comment-and-close js-action-button',
+ 'btn-comment btn-comment-and-close js-action-button',
]"
:disabled="isToggleStateButtonLoading || isSubmitting"
- :label="issueActionButtonTitle"
@click="handleSave(true)"
- />
+ >{{ issueActionButtonTitle }}</gl-button
+ >
</div>
</form>
</div>
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index 50d224a2f08..8e6c01ba63f 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions } from 'vuex';
import { escape } from 'lodash';
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 8897b54fac7..c01cd8f8037 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapState, mapActions } from 'vuex';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 4a1a1086329..c6fab271376 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,7 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import discussionNavigation from '../mixins/discussion_navigation';
export default {
@@ -9,7 +8,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- Icon,
+ GlIcon,
},
mixins: [discussionNavigation],
computed: {
@@ -60,7 +59,7 @@ export default {
:class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }"
>
<template v-if="allResolved">
- <icon name="check-circle-filled" />
+ <gl-icon name="check-circle-filled" />
{{ __('All threads resolved') }}
</template>
<template v-else>
@@ -79,7 +78,7 @@ export default {
:title="s__('Resolve all threads in new issue')"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
>
- <icon name="issue-new" />
+ <gl-icon name="issue-new" />
</a>
</div>
<div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group">
@@ -92,7 +91,7 @@ export default {
data-track-property="click_next_unresolved_thread_top"
@click="jumpToNextDiscussion"
>
- <icon name="comment-next" />
+ <gl-icon name="comment-next" />
</button>
</div>
<div class="btn-group btn-group-sm" role="group">
@@ -102,7 +101,7 @@ export default {
class="btn btn-default toggle-all-discussions-btn"
@click="handleExpandDiscussions"
>
- <icon :name="allExpanded ? 'angle-up' : 'angle-down'" />
+ <gl-icon :name="allExpanded ? 'angle-up' : 'angle-down'" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 6b1e3298f9a..989ce9ff144 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -1,8 +1,8 @@
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
-import Icon from '~/vue_shared/components/icon.vue';
import {
DISCUSSION_FILTERS_DEFAULT_VALUE,
HISTORY_ONLY_FILTER_VALUE,
@@ -14,7 +14,7 @@ import notesEventHub from '../event_hub';
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
filters: {
@@ -120,7 +120,7 @@ export default {
data-toggle="dropdown"
aria-expanded="false"
>
- {{ currentFilter.title }} <icon name="chevron-down" />
+ {{ currentFilter.title }} <gl-icon name="chevron-down" />
</button>
<div
ref="dropdownMenu"
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index 8dc4b43d69a..ae6646cf96c 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -1,6 +1,6 @@
<script>
-import { GlButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+/* eslint-disable vue/no-v-html */
+import { GlButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import notesEventHub from '../event_hub';
@@ -8,7 +8,7 @@ import notesEventHub from '../event_hub';
export default {
components: {
GlButton,
- Icon,
+ GlIcon,
},
computed: {
timelineContent() {
@@ -35,7 +35,7 @@ export default {
<template>
<li class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note">
<div class="timeline-icon d-none d-lg-flex">
- <icon name="comment" />
+ <gl-icon name="comment" />
</div>
<div class="timeline-content">
<div ref="timelineContent" v-html="timelineContent"></div>
diff --git a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
index b71ce1b6a0a..f94d0060b41 100644
--- a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
@@ -1,12 +1,11 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import discussionNavigation from '../mixins/discussion_navigation';
export default {
name: 'JumpToNextDiscussionButton',
components: {
- icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -33,7 +32,7 @@ export default {
data-track-property="click_next_unresolved_thread"
@click="jumpToNextRelativeDiscussion(fromDiscussionId)"
>
- <icon name="comment-next" />
+ <gl-icon name="comment-next" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index 8636984c6af..2f215e36d5b 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -1,13 +1,12 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLink, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import Issuable from '~/vue_shared/mixins/issuable';
import issuableStateMixin from '../mixins/issuable_state';
export default {
components: {
- Icon,
+ GlIcon,
GlLink,
},
mixins: [Issuable, issuableStateMixin],
@@ -28,7 +27,7 @@ export default {
<template>
<div class="disabled-comment text-center">
<span class="issuable-note-warning inline">
- <icon :size="16" name="lock" class="icon" />
+ <gl-icon :size="16" name="lock" class="icon" />
<span v-if="isProjectArchived">
{{ projectArchivedWarning }}
<gl-link :href="archivedProjectDocsPath" target="_blank" class="learn-more">
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
index 77f6f1e51c5..e060a6affd4 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
@@ -1,10 +1,10 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
export default {
name: 'ResolveDiscussionButton',
components: {
- GlLoadingIcon,
+ GlButton,
},
props: {
isResolving: {
@@ -21,8 +21,7 @@ export default {
</script>
<template>
- <button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')">
- <gl-loading-icon v-if="isResolving" ref="isResolvingIcon" inline />
+ <gl-button :loading="isResolving" class="ml-sm-2" @click="$emit('onClick')">
{{ buttonTitle }}
- </button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index a8ae7fb48f0..a8057276f1a 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,18 +1,18 @@
<script>
import { mapGetters } from 'vuex';
-import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
-import Icon from '~/vue_shared/components/icon.vue';
import ReplyButton from './note_actions/reply_button.vue';
import eventHub from '~/sidebar/event_hub';
import Api from '~/api';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { splitCamelCase } from '../../lib/utils/text_utility';
export default {
name: 'NoteActions',
components: {
- Icon,
+ GlIcon,
ReplyButton,
GlLoadingIcon,
},
@@ -48,6 +48,26 @@ export default {
required: false,
default: null,
},
+ isAuthor: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isContributor: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ noteableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ projectName: {
+ type: String,
+ required: false,
+ default: '',
+ },
showReply: {
type: Boolean,
required: true,
@@ -122,6 +142,9 @@ export default {
targetType() {
return this.getNoteableData.targetType;
},
+ noteableDisplayName() {
+ return splitCamelCase(this.noteableType).toLowerCase();
+ },
assignees() {
return this.getNoteableData.assignees || [];
},
@@ -131,6 +154,22 @@ export default {
canAssign() {
return this.getNoteableData.current_user?.can_update && this.isIssue;
},
+ displayAuthorBadgeText() {
+ return sprintf(__('This user is the author of this %{noteable}.'), {
+ noteable: this.noteableDisplayName,
+ });
+ },
+ displayMemberBadgeText() {
+ return sprintf(__('This user is a %{access} of the %{name} project.'), {
+ access: this.accessLevel.toLowerCase(),
+ name: this.projectName,
+ });
+ },
+ displayContributorBadgeText() {
+ return sprintf(__('This user has previously committed to the %{name} project.'), {
+ name: this.projectName,
+ });
+ },
},
methods: {
onEdit() {
@@ -176,7 +215,24 @@ export default {
<template>
<div class="note-actions">
- <span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span>
+ <span
+ v-if="isAuthor"
+ class="note-role user-access-role has-tooltip d-none d-md-inline-block"
+ :title="displayAuthorBadgeText"
+ >{{ __('Author') }}</span
+ >
+ <span
+ v-if="accessLevel"
+ class="note-role user-access-role has-tooltip"
+ :title="displayMemberBadgeText"
+ >{{ accessLevel }}</span
+ >
+ <span
+ v-else-if="isContributor"
+ class="note-role user-access-role has-tooltip"
+ :title="displayContributorBadgeText"
+ >{{ __('Contributor') }}</span
+ >
<div v-if="canResolve" class="note-actions-item">
<button
ref="resolveButton"
@@ -189,7 +245,7 @@ export default {
@click="onResolve"
>
<template v-if="!isResolving">
- <icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" />
+ <gl-icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" />
</template>
<gl-loading-icon v-else inline />
</button>
@@ -203,9 +259,9 @@ export default {
title="Add reaction"
data-position="right"
>
- <icon class="link-highlight award-control-icon-neutral" name="slight-smile" />
- <icon class="link-highlight award-control-icon-positive" name="smiley" />
- <icon class="link-highlight award-control-icon-super-positive" name="smiley" />
+ <gl-icon class="link-highlight award-control-icon-neutral" name="slight-smile" />
+ <gl-icon class="link-highlight award-control-icon-positive" name="smiley" />
+ <gl-icon class="link-highlight award-control-icon-super-positive" name="smiley" />
</a>
</div>
<reply-button
@@ -222,7 +278,7 @@ export default {
class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button"
@click="onEdit"
>
- <icon name="pencil" class="link-highlight" />
+ <gl-icon name="pencil" class="link-highlight" />
</button>
</div>
<div v-if="showDeleteAction" class="note-actions-item">
@@ -233,7 +289,7 @@ export default {
class="note-action-button js-note-delete btn btn-transparent"
@click="onDelete"
>
- <icon name="remove" class="link-highlight" />
+ <gl-icon name="remove" class="link-highlight" />
</button>
</div>
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item">
@@ -245,7 +301,7 @@ export default {
data-toggle="dropdown"
@click="closeTooltip"
>
- <icon class="icon" name="ellipsis_v" />
+ <gl-icon class="icon" name="ellipsis_v" />
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse">
diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
index 30cb7967c34..f19b7667fb2 100644
--- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue
+++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
@@ -1,12 +1,10 @@
<script>
-import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
export default {
name: 'ReplyButton',
components: {
- Icon,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -16,17 +14,17 @@ export default {
<template>
<div class="note-actions-item">
- <gl-deprecated-button
+ <gl-button
ref="button"
v-gl-tooltip
- class="note-action-button"
data-track-event="click_button"
data-track-label="reply_comment_button"
- variant="transparent"
+ category="tertiary"
+ size="small"
+ icon="comment"
:title="__('Reply to comment')"
+ :aria-label="__('Reply to comment')"
@click="$emit('startReplying')"
- >
- <icon name="comment" class="link-highlight" />
- </gl-deprecated-button>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 42b78929f8a..314fa762768 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions, mapGetters, mapState } from 'vuex';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 24227d55ebf..88b4461cf38 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapGetters, mapActions, mapState } from 'vuex';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
@@ -336,7 +337,7 @@ export default {
v-model="updatedNoteBody"
:data-supports-quick-actions="!isEditing"
name="note[note]"
- class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
+ class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form qa-reply-input"
dir="auto"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 9ded5ab648e..a13a0dbbf30 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions } from 'vuex';
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
@@ -9,6 +10,7 @@ export default {
GitlabTeamMemberBadge: () =>
import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
GlIcon,
+ GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -194,13 +196,12 @@ export default {
class="gl-ml-1 gl-text-gray-700 align-middle"
/>
<slot name="extra-controls"></slot>
- <i
+ <gl-loading-icon
v-if="showSpinner"
ref="spinner"
- class="fa fa-spinner fa-spin editing-spinner"
- :aria-label="__('Comment is being updated')"
- aria-hidden="true"
- ></i>
+ class="editing-spinner"
+ :label="__('Comment is being updated')"
+ />
</span>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index ccfe84ab098..593933016e1 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -1,8 +1,12 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { __, sprintf } from '~/locale';
export default {
+ directives: {
+ SafeHtml,
+ },
computed: {
...mapGetters(['getNotesDataByProp']),
registerLink() {
@@ -30,5 +34,5 @@ export default {
</script>
<template>
- <div class="disabled-comment text-center" v-html="signedOutText"></div>
+ <div v-safe-html="signedOutText" class="disabled-comment text-center"></div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index b4176c6063b..62ee7f30c57 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,10 +1,9 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import { s__, __ } from '~/locale';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
-import icon from '~/vue_shared/components/icon.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
@@ -22,7 +21,7 @@ import DiscussionActions from './discussion_actions.vue';
export default {
name: 'NoteableDiscussion',
components: {
- icon,
+ GlIcon,
userAvatarLink,
diffDiscussionHeader,
noteSignedOutWidget,
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index ce771e67cbb..4f45fcb0062 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -2,7 +2,7 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'lodash';
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@@ -34,6 +34,9 @@ export default {
NoteBody,
TimelineEntryItem,
},
+ directives: {
+ SafeHtml,
+ },
mixins: [noteable, resolvable, glFeatureFlagsMixin()],
props: {
note: {
@@ -358,7 +361,7 @@ export default {
</template>
</gl-sprintf>
</div>
- <div v-once class="timeline-icon">
+ <div class="timeline-icon">
<user-avatar-link
:link-href="author.path"
:img-src="author.avatar_url"
@@ -371,14 +374,13 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header
- v-once
:author="author"
:created-at="note.created_at"
:note-id="note.id"
:is-confidential="note.confidential"
>
<slot slot="note-header-info" name="note-header-info"></slot>
- <span v-if="commit" v-html="actionText"></span>
+ <span v-if="commit" v-safe-html="actionText"></span>
<span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>
</note-header>
<note-actions
@@ -387,6 +389,10 @@ export default {
:note-id="note.id"
:note-url="note.noteable_note_url"
:access-level="note.human_access"
+ :is-contributor="note.is_contributor"
+ :is-author="note.is_noteable_author"
+ :project-name="note.project_name"
+ :noteable-type="note.noteable_type"
:show-reply="showReplyButton"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index dd132d4f608..bddac60647d 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -1,12 +1,12 @@
<script>
import { uniqBy } from 'lodash';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- Icon,
+ GlIcon,
UserAvatarLink,
TimeAgoTooltip,
},
@@ -44,7 +44,7 @@ export default {
<template>
<li :class="className" class="replies-toggle js-toggle-replies">
<template v-if="collapsed">
- <icon name="chevron-right" @click.native="toggle" />
+ <gl-icon name="chevron-right" @click.native="toggle" />
<div>
<user-avatar-link
v-for="author in uniqueAuthors"
@@ -71,7 +71,7 @@ export default {
class="collapse-replies-btn js-collapse-replies qa-collapse-replies"
@click="toggle"
>
- <icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }}
+ <gl-icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }}
</span>
</li>
</template>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index c1449237f8a..b81aae7c257 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -22,6 +22,8 @@ export const TIME_DIFFERENCE_VALUE = 10;
export const ASC = 'asc';
export const DESC = 'desc';
+export const DISCUSSION_FETCH_TIMEOUT = 750;
+
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE,
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index f6069b509e8..9c63a7e3cd4 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -87,6 +87,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi
return axios.get(path, config).then(({ data }) => {
commit(types.SET_INITIAL_DISCUSSIONS, data);
+ commit(types.SET_FETCHING_DISCUSSIONS, false);
dispatch('updateResolvableDiscussionsCounts');
});
@@ -136,6 +137,23 @@ export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => {
const { notesById } = getters;
+ const debouncedFetchDiscussions = isFetching => {
+ if (!isFetching) {
+ commit(types.SET_FETCHING_DISCUSSIONS, true);
+ dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
+ } else {
+ if (isFetching !== true) {
+ clearTimeout(state.currentlyFetchingDiscussions);
+ }
+
+ commit(
+ types.SET_FETCHING_DISCUSSIONS,
+ setTimeout(() => {
+ dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
+ }, constants.DISCUSSION_FETCH_TIMEOUT),
+ );
+ }
+ };
notes.forEach(note => {
if (notesById[note.id]) {
@@ -146,7 +164,7 @@ export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes)
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
} else if (note.type === constants.DIFF_NOTE) {
- dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
+ debouncedFetchDiscussions(state.currentlyFetchingDiscussions);
} else {
commit(types.ADD_NEW_NOTE, note);
}
@@ -457,7 +475,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
});
if (!Visibility.hidden()) {
- eTagPoll.makeRequest();
+ eTagPoll.makeDelayedRequest(2500);
} else {
dispatch('fetchData');
}
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 1649e63c61f..161c9b8b1b5 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -12,6 +12,7 @@ export default () => ({
lastFetchedAt: null,
currentDiscussionId: null,
batchSuggestionsInfo: [],
+ currentlyFetchingDiscussions: false,
/**
* selectedCommentPosition & selectedCommentPosition structures are the same as `position.line_range`:
* {
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index eb3447291bc..23515cdd9e3 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -36,6 +36,7 @@ export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID';
export const SET_DISCUSSIONS_SORT = 'SET_DISCUSSIONS_SORT';
export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION';
export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER';
+export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index aa078f00569..a8bd94cc763 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -379,4 +379,7 @@ export default {
[types.UPDATE_ASSIGNEES](state, assignees) {
state.noteableData.assignees = assignees;
},
+ [types.SET_FETCHING_DISCUSSIONS](state, value) {
+ state.currentlyFetchingDiscussions = value;
+ },
};
diff --git a/app/assets/javascripts/packages/details/components/additional_metadata.vue b/app/assets/javascripts/packages/details/components/additional_metadata.vue
index a3de6dd46c7..76e0976ac05 100644
--- a/app/assets/javascripts/packages/details/components/additional_metadata.vue
+++ b/app/assets/javascripts/packages/details/components/additional_metadata.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import DetailsRow from '~/registry/shared/components/details_row.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import { generateConanRecipe } from '../utils';
import { PackageType } from '../../shared/constants';
diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue
index dbb5f7be0a0..af3220840a6 100644
--- a/app/assets/javascripts/packages/details/components/app.vue
+++ b/app/assets/javascripts/packages/details/components/app.vue
@@ -25,8 +25,9 @@ import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { __, s__ } from '~/locale';
-import { PackageType, TrackingActions } from '../../shared/constants';
+import { PackageType, TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils';
+import { objectToQueryString } from '~/lib/utils/common_utils';
export default {
name: 'PackagesApp',
@@ -62,17 +63,15 @@ export default {
'packageFiles',
'isLoading',
'canDelete',
- 'destroyPath',
'svgPath',
'npmPath',
'npmHelpPath',
+ 'projectListUrl',
+ 'groupListUrl',
]),
isValidPackage() {
return Boolean(this.packageEntity.name);
},
- canDeletePackage() {
- return this.canDelete && this.destroyPath;
- },
filesTableRows() {
return this.packageFiles.map(x => ({
name: x.file_name,
@@ -100,7 +99,7 @@ export default {
},
},
methods: {
- ...mapActions(['fetchPackageVersions']),
+ ...mapActions(['deletePackage', 'fetchPackageVersions']),
formatSize(size) {
return numberToHumanSize(size);
},
@@ -112,6 +111,16 @@ export default {
this.fetchPackageVersions();
}
},
+ async confirmPackageDeletion() {
+ this.track(TrackingActions.DELETE_PACKAGE);
+ await this.deletePackage();
+ const returnTo =
+ !this.groupListUrl || document.referrer.includes(this.projectName)
+ ? this.projectListUrl
+ : this.groupListUrl; // to avoid security issue url are supplied from backend
+ const modalQuery = objectToQueryString({ [SHOW_DELETE_SUCCESS_ALERT]: true });
+ window.location.replace(`${returnTo}?${modalQuery}`);
+ },
},
i18n: {
deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
@@ -147,12 +156,10 @@ export default {
/>
<div v-else class="packages-app">
- <div class="detail-page-header d-flex justify-content-between flex-column flex-sm-row">
- <package-title />
-
- <div class="mt-sm-2">
+ <package-title>
+ <template #delete-button>
<gl-button
- v-if="canDeletePackage"
+ v-if="canDelete"
v-gl-modal="'delete-modal'"
class="js-delete-button"
variant="danger"
@@ -161,8 +168,8 @@ export default {
>
{{ __('Delete') }}
</gl-button>
- </div>
- </div>
+ </template>
+ </package-title>
<gl-tabs>
<gl-tab :title="__('Detail')">
@@ -268,22 +275,22 @@ export default {
</template>
</gl-sprintf>
- <div slot="modal-footer" class="w-100">
- <div class="float-right">
- <gl-button @click="cancelDelete()">{{ __('Cancel') }}</gl-button>
- <gl-button
- ref="modal-delete-button"
- data-method="delete"
- :to="destroyPath"
- variant="danger"
- category="primary"
- data-qa-selector="delete_modal_button"
- @click="track($options.trackingActions.DELETE_PACKAGE)"
- >
- {{ __('Delete') }}
- </gl-button>
+ <template #modal-footer>
+ <div class="gl-w-full">
+ <div class="float-right">
+ <gl-button @click="cancelDelete">{{ __('Cancel') }}</gl-button>
+ <gl-button
+ ref="modal-delete-button"
+ variant="danger"
+ category="primary"
+ data-qa-selector="delete_modal_button"
+ @click="confirmPackageDeletion"
+ >
+ {{ __('Delete') }}
+ </gl-button>
+ </div>
</div>
- </div>
+ </template>
</gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/packages/details/components/code_instruction.vue b/app/assets/javascripts/packages/details/components/code_instruction.vue
deleted file mode 100644
index 0719ddfcd2b..00000000000
--- a/app/assets/javascripts/packages/details/components/code_instruction.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Tracking from '~/tracking';
-import { TrackingLabels } from '../constants';
-
-export default {
- name: 'CodeInstruction',
- components: {
- ClipboardButton,
- },
- mixins: [
- Tracking.mixin({
- label: TrackingLabels.CODE_INSTRUCTION,
- }),
- ],
- props: {
- instruction: {
- type: String,
- required: true,
- },
- copyText: {
- type: String,
- required: true,
- },
- multiline: {
- type: Boolean,
- required: false,
- default: false,
- },
- trackingAction: {
- type: String,
- required: false,
- default: '',
- },
- },
- methods: {
- trackCopy() {
- if (this.trackingAction) {
- this.track(this.trackingAction);
- }
- },
- },
-};
-</script>
-
-<template>
- <div v-if="!multiline" class="input-group gl-mb-3">
- <input
- :value="instruction"
- type="text"
- class="form-control monospace js-instruction-input"
- readonly
- @copy="trackCopy"
- />
- <span class="input-group-append js-instruction-button" @click="trackCopy">
- <clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
- </span>
- </div>
-
- <div v-else>
- <pre class="js-instruction-pre" @copy="trackCopy">{{ instruction }}</pre>
- </div>
-</template>
diff --git a/app/assets/javascripts/packages/details/components/composer_installation.vue b/app/assets/javascripts/packages/details/components/composer_installation.vue
index 1934da149ce..60ad468c293 100644
--- a/app/assets/javascripts/packages/details/components/composer_installation.vue
+++ b/app/assets/javascripts/packages/details/components/composer_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'ComposerInstallation',
@@ -26,28 +26,30 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
<template>
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base" data-testid="registry-include-title">
- {{ $options.i18n.registryInclude }}
- </h4>
<code-instruction
+ :label="$options.i18n.registryInclude"
:instruction="composerRegistryInclude"
:copy-text="$options.i18n.copyRegistryInclude"
:tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ data-testid="registry-include"
/>
- <h4 class="gl-font-base" data-testid="package-include-title">
- {{ $options.i18n.packageInclude }}
- </h4>
+
<code-instruction
+ :label="$options.i18n.packageInclude"
:instruction="composerPackageInclude"
:copy-text="$options.i18n.copyPackageInclude"
:tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ data-testid="package-include"
/>
<span data-testid="help-text">
<gl-sprintf :message="$options.i18n.infoLine">
diff --git a/app/assets/javascripts/packages/details/components/conan_installation.vue b/app/assets/javascripts/packages/details/components/conan_installation.vue
index cff7d73f1e8..a5df87c9c5b 100644
--- a/app/assets/javascripts/packages/details/components/conan_installation.vue
+++ b/app/assets/javascripts/packages/details/components/conan_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'ConanInstallation',
@@ -22,30 +22,30 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
<template>
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Conan Command') }}
- </h4>
<code-instruction
+ :label="s__('PackageRegistry|Conan Command')"
:instruction="conanInstallationCommand"
:copy-text="s__('PackageRegistry|Copy Conan Command')"
:tracking-action="$options.trackingActions.COPY_CONAN_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Add Conan Remote') }}
- </h4>
+
<code-instruction
+ :label="s__('PackageRegistry|Add Conan Remote')"
:instruction="conanSetupCommand"
:copy-text="s__('PackageRegistry|Copy Conan Setup Command')"
:tracking-action="$options.trackingActions.COPY_CONAN_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages/details/components/dependency_row.vue b/app/assets/javascripts/packages/details/components/dependency_row.vue
index 788673d2881..1a2202b23c8 100644
--- a/app/assets/javascripts/packages/details/components/dependency_row.vue
+++ b/app/assets/javascripts/packages/details/components/dependency_row.vue
@@ -26,7 +26,7 @@ export default {
<div
v-if="showVersion"
- class="table-section section-50 gl-display-flex justify-content-md-end"
+ class="table-section section-50 gl-display-flex gl-md-justify-content-end"
data-testid="version-pattern"
>
<span class="gl-text-body">{{ dependency.version_pattern }}</span>
diff --git a/app/assets/javascripts/packages/details/components/maven_installation.vue b/app/assets/javascripts/packages/details/components/maven_installation.vue
index d6641c886a0..c2f6f76967b 100644
--- a/app/assets/javascripts/packages/details/components/maven_installation.vue
+++ b/app/assets/javascripts/packages/details/components/maven_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'MavenInstallation',
@@ -28,6 +28,7 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
@@ -35,9 +36,6 @@ export default {
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Maven XML') }}
- </h4>
<p>
<gl-sprintf :message="$options.i18n.xmlText">
<template #code="{ content }">
@@ -45,20 +43,22 @@ export default {
</template>
</gl-sprintf>
</p>
+
<code-instruction
+ :label="s__('PackageRegistry|Maven XML')"
:instruction="mavenInstallationXml"
:copy-text="s__('PackageRegistry|Copy Maven XML')"
multiline
:tracking-action="$options.trackingActions.COPY_MAVEN_XML"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Maven Command') }}
- </h4>
<code-instruction
+ :label="s__('PackageRegistry|Maven Command')"
:instruction="mavenInstallationCommand"
:copy-text="s__('PackageRegistry|Copy Maven command')"
:tracking-action="$options.trackingActions.COPY_MAVEN_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
@@ -74,6 +74,7 @@ export default {
:copy-text="s__('PackageRegistry|Copy Maven registry XML')"
multiline
:tracking-action="$options.trackingActions.COPY_MAVEN_SETUP"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages/details/components/npm_installation.vue b/app/assets/javascripts/packages/details/components/npm_installation.vue
index d7ff8428370..37ba279d098 100644
--- a/app/assets/javascripts/packages/details/components/npm_installation.vue
+++ b/app/assets/javascripts/packages/details/components/npm_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { NpmManager, TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { NpmManager, TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'NpmInstallation',
@@ -34,41 +34,46 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
<template>
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">{{ s__('PackageRegistry|npm command') }}</h4>
<code-instruction
+ :label="s__('PackageRegistry|npm command')"
:instruction="npmCommand"
:copy-text="s__('PackageRegistry|Copy npm command')"
:tracking-action="$options.trackingActions.COPY_NPM_INSTALL_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
- <h4 class="gl-font-base">{{ s__('PackageRegistry|yarn command') }}</h4>
<code-instruction
+ :label="s__('PackageRegistry|yarn command')"
:instruction="yarnCommand"
:copy-text="s__('PackageRegistry|Copy yarn command')"
:tracking-action="$options.trackingActions.COPY_YARN_INSTALL_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
- <h4 class="gl-font-base">{{ s__('PackageRegistry|npm command') }}</h4>
<code-instruction
+ :label="s__('PackageRegistry|npm command')"
:instruction="npmSetup"
:copy-text="s__('PackageRegistry|Copy npm setup command')"
:tracking-action="$options.trackingActions.COPY_NPM_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
- <h4 class="gl-font-base">{{ s__('PackageRegistry|yarn command') }}</h4>
<code-instruction
+ :label="s__('PackageRegistry|yarn command')"
:instruction="yarnSetupCommand"
:copy-text="s__('PackageRegistry|Copy yarn setup command')"
:tracking-action="$options.trackingActions.COPY_YARN_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
diff --git a/app/assets/javascripts/packages/details/components/nuget_installation.vue b/app/assets/javascripts/packages/details/components/nuget_installation.vue
index 150b6e3ab0f..36887703716 100644
--- a/app/assets/javascripts/packages/details/components/nuget_installation.vue
+++ b/app/assets/javascripts/packages/details/components/nuget_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'NugetInstallation',
@@ -22,29 +22,28 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
<template>
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|NuGet Command') }}
- </h4>
<code-instruction
+ :label="s__('PackageRegistry|NuGet Command')"
:instruction="nugetInstallationCommand"
:copy-text="s__('PackageRegistry|Copy NuGet Command')"
:tracking-action="$options.trackingActions.COPY_NUGET_INSTALL_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Add NuGet Source') }}
- </h4>
<code-instruction
+ :label="s__('PackageRegistry|Add NuGet Source')"
:instruction="nugetSetupCommand"
:copy-text="s__('PackageRegistry|Copy NuGet Setup Command')"
:tracking-action="$options.trackingActions.COPY_NUGET_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages/details/components/package_history.vue b/app/assets/javascripts/packages/details/components/package_history.vue
index 96ce106884d..413ab1d15cb 100644
--- a/app/assets/javascripts/packages/details/components/package_history.vue
+++ b/app/assets/javascripts/packages/details/components/package_history.vue
@@ -2,7 +2,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import HistoryElement from './history_element.vue';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
export default {
name: 'PackageHistory',
@@ -16,7 +16,7 @@ export default {
components: {
GlLink,
GlSprintf,
- HistoryElement,
+ HistoryItem,
TimeAgoTooltip,
},
props: {
@@ -46,7 +46,7 @@ export default {
<div class="issuable-discussion">
<h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
- <history-element icon="clock" data-testid="created-on">
+ <history-item icon="clock" data-testid="created-on">
<gl-sprintf :message="$options.i18n.createdOn">
<template #name>
<strong>{{ packageEntity.name }}</strong>
@@ -58,8 +58,8 @@ export default {
<time-ago-tooltip :time="packageEntity.created_at" />
</template>
</gl-sprintf>
- </history-element>
- <history-element icon="pencil" data-testid="updated-at">
+ </history-item>
+ <history-item icon="pencil" data-testid="updated-at">
<gl-sprintf :message="$options.i18n.updatedAtText">
<template #name>
<strong>{{ packageEntity.name }}</strong>
@@ -71,9 +71,9 @@ export default {
<time-ago-tooltip :time="packageEntity.updated_at" />
</template>
</gl-sprintf>
- </history-element>
+ </history-item>
<template v-if="packagePipeline">
- <history-element icon="commit" data-testid="commit">
+ <history-item icon="commit" data-testid="commit">
<gl-sprintf :message="$options.i18n.commitText">
<template #link>
<gl-link :href="packagePipeline.project.commit_url">{{
@@ -84,8 +84,8 @@ export default {
<strong>{{ packagePipeline.ref }}</strong>
</template>
</gl-sprintf>
- </history-element>
- <history-element icon="pipeline" data-testid="pipeline">
+ </history-item>
+ <history-item icon="pipeline" data-testid="pipeline">
<gl-sprintf :message="$options.i18n.pipelineText">
<template #link>
<gl-link :href="packagePipeline.project.pipeline_url"
@@ -97,9 +97,9 @@ export default {
</template>
<template #author>{{ packagePipeline.user.name }}</template>
</gl-sprintf>
- </history-element>
+ </history-item>
</template>
- <history-element icon="package" data-testid="published">
+ <history-item icon="package" data-testid="published">
<gl-sprintf :message="$options.i18n.publishText">
<template #project>
<strong>{{ projectName }}</strong>
@@ -108,7 +108,7 @@ export default {
<time-ago-tooltip :time="packageEntity.created_at" />
</template>
</gl-sprintf>
- </history-element>
+ </history-item>
</ul>
</div>
</template>
diff --git a/app/assets/javascripts/packages/details/components/package_title.vue b/app/assets/javascripts/packages/details/components/package_title.vue
index d07883e3e7a..69dd494f11a 100644
--- a/app/assets/javascripts/packages/details/components/package_title.vue
+++ b/app/assets/javascripts/packages/details/components/package_title.vue
@@ -1,19 +1,21 @@
<script>
import { mapState, mapGetters } from 'vuex';
-import { GlAvatar, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import PackageTags from '../../shared/components/package_tags.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { __ } from '~/locale';
export default {
name: 'PackageTitle',
components: {
- GlAvatar,
+ TitleArea,
GlIcon,
- GlLink,
GlSprintf,
PackageTags,
+ MetadataItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -36,77 +38,49 @@ export default {
</script>
<template>
- <div class="gl-flex-direction-column">
- <div class="gl-display-flex">
- <gl-avatar
- v-if="packageIcon"
- :src="packageIcon"
- shape="rect"
- class="gl-align-self-center gl-mr-4"
- data-testid="package-icon"
- />
-
- <div class="gl-display-flex gl-flex-direction-column">
- <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2">
- {{ packageEntity.name }}
- </h1>
+ <title-area :title="packageEntity.name" :avatar="packageIcon" data-qa-selector="package_title">
+ <template #sub-header>
+ <gl-icon name="eye" class="gl-mr-3" />
+ <gl-sprintf :message="$options.i18n.packageInfo">
+ <template #version>
+ {{ packageEntity.version }}
+ </template>
- <div class="gl-display-flex gl-align-items-center gl-text-gray-500">
- <gl-icon name="eye" class="gl-mr-3" />
- <gl-sprintf :message="$options.i18n.packageInfo">
- <template #version>
- {{ packageEntity.version }}
- </template>
+ <template #timeAgo>
+ <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
+ &nbsp;{{ timeFormatted(packageEntity.created_at) }}
+ </span>
+ </template>
+ </gl-sprintf>
+ </template>
- <template #timeAgo>
- <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
- &nbsp;{{ timeFormatted(packageEntity.created_at) }}
- </span>
- </template>
- </gl-sprintf>
- </div>
- </div>
- </div>
+ <template v-if="packageTypeDisplay" #metadata_type>
+ <metadata-item data-testid="package-type" icon="package" :text="packageTypeDisplay" />
+ </template>
- <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3">
- <div v-if="packageTypeDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
- <gl-icon name="package" class="gl-text-gray-500 gl-mr-3" />
- <span data-testid="package-type" class="gl-font-weight-bold">{{ packageTypeDisplay }}</span>
- </div>
+ <template #metadata_size>
+ <metadata-item data-testid="package-size" icon="disk" :text="totalSize" />
+ </template>
- <div v-if="hasTagsToDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
- <package-tags :tag-display-limit="1" :tags="packageEntity.tags" />
- </div>
+ <template v-if="packagePipeline" #metadata_pipeline>
+ <metadata-item
+ data-testid="pipeline-project"
+ icon="review-list"
+ :text="packagePipeline.project.name"
+ :link="packagePipeline.project.web_url"
+ />
+ </template>
- <div v-if="packagePipeline" class="gl-display-flex gl-align-items-center gl-mr-5">
- <gl-icon name="review-list" class="gl-text-gray-500 gl-mr-3" />
- <gl-link
- data-testid="pipeline-project"
- :href="packagePipeline.project.web_url"
- class="gl-font-weight-bold text-truncate"
- >
- {{ packagePipeline.project.name }}
- </gl-link>
- </div>
+ <template v-if="packagePipeline" #metadata_ref>
+ <metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
+ </template>
- <div
- v-if="packagePipeline"
- data-testid="package-ref"
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <gl-icon name="branch" class="gl-text-gray-500 gl-mr-3" />
- <span
- v-gl-tooltip
- class="gl-font-weight-bold text-truncate mw-xs"
- :title="packagePipeline.ref"
- >{{ packagePipeline.ref }}</span
- >
- </div>
+ <template v-if="hasTagsToDisplay" #metadata_tags>
+ <package-tags :tag-display-limit="2" :tags="packageEntity.tags" hide-label />
+ </template>
- <div class="gl-display-flex gl-align-items-center gl-mr-5">
- <gl-icon name="disk" class="gl-text-gray-500 gl-mr-3" />
- <span data-testid="package-size" class="gl-font-weight-bold">{{ totalSize }}</span>
- </div>
- </div>
- </div>
+ <template #right-actions>
+ <slot name="delete-button"></slot>
+ </template>
+ </title-area>
</template>
diff --git a/app/assets/javascripts/packages/details/components/pypi_installation.vue b/app/assets/javascripts/packages/details/components/pypi_installation.vue
index f1c619fd6d3..f87be68be48 100644
--- a/app/assets/javascripts/packages/details/components/pypi_installation.vue
+++ b/app/assets/javascripts/packages/details/components/pypi_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'PyPiInstallation',
@@ -25,6 +25,7 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
@@ -32,15 +33,13 @@ export default {
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Pip Command') }}
- </h4>
-
<code-instruction
+ :label="s__('PackageRegistry|Pip Command')"
:instruction="pypiPipCommand"
:copy-text="s__('PackageRegistry|Copy Pip command')"
data-testid="pip-command"
:tracking-action="$options.trackingActions.COPY_PIP_INSTALL_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
@@ -58,6 +57,7 @@ export default {
data-testid="pypi-setup-content"
multiline
:tracking-action="$options.trackingActions.COPY_PYPI_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages/details/store/actions.js b/app/assets/javascripts/packages/details/store/actions.js
index cda80056e19..340f60258a0 100644
--- a/app/assets/javascripts/packages/details/store/actions.js
+++ b/app/assets/javascripts/packages/details/store/actions.js
@@ -1,9 +1,10 @@
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
+import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import * as types from './mutation_types';
-export default ({ commit, state }) => {
+export const fetchPackageVersions = ({ commit, state }) => {
commit(types.SET_LOADING, true);
const { project_id, id } = state.packageEntity;
@@ -21,3 +22,13 @@ export default ({ commit, state }) => {
commit(types.SET_LOADING, false);
});
};
+
+export const deletePackage = ({
+ state: {
+ packageEntity: { project_id, id },
+ },
+}) => {
+ return Api.deleteProjectPackage(project_id, id).catch(() => {
+ createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
+ });
+};
diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js
index d1814d506ad..ede6d39bde7 100644
--- a/app/assets/javascripts/packages/details/store/getters.js
+++ b/app/assets/javascripts/packages/details/store/getters.js
@@ -84,10 +84,10 @@ export const npmSetupCommand = ({ packageEntity, npmPath }) => (type = NpmManage
const scope = packageEntity.name.substring(0, packageEntity.name.indexOf('/'));
if (type === NpmManager.NPM) {
- return `echo ${scope}:registry=${npmPath} >> .npmrc`;
+ return `echo ${scope}:registry=${npmPath}/ >> .npmrc`;
}
- return `echo \\"${scope}:registry\\" \\"${npmPath}\\" >> .yarnrc`;
+ return `echo \\"${scope}:registry\\" \\"${npmPath}/\\" >> .yarnrc`;
};
export const nugetInstallationCommand = ({ packageEntity }) =>
diff --git a/app/assets/javascripts/packages/details/store/index.js b/app/assets/javascripts/packages/details/store/index.js
index 9687eb98544..15e17bcfaac 100644
--- a/app/assets/javascripts/packages/details/store/index.js
+++ b/app/assets/javascripts/packages/details/store/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import fetchPackageVersions from './actions';
+import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
@@ -8,9 +8,7 @@ Vue.use(Vuex);
export default (initialState = {}) =>
new Vuex.Store({
- actions: {
- fetchPackageVersions,
- },
+ actions,
getters,
mutations,
state: {
diff --git a/app/assets/javascripts/packages/list/components/packages_list.vue b/app/assets/javascripts/packages/list/components/packages_list.vue
index b26c6087e14..7067f70a923 100644
--- a/app/assets/javascripts/packages/list/components/packages_list.vue
+++ b/app/assets/javascripts/packages/list/components/packages_list.vue
@@ -82,11 +82,11 @@ export default {
</script>
<template>
- <div class="d-flex flex-column">
+ <div class="gl-display-flex gl-flex-direction-column">
<slot v-if="isListEmpty && !isLoading" name="empty-state"></slot>
<div v-else-if="isLoading">
- <packages-list-loader :is-group="isGroupPage" />
+ <packages-list-loader />
</div>
<template v-else>
@@ -106,7 +106,7 @@ export default {
:per-page="perPage"
:total-items="totalItems"
align="center"
- class="w-100 mt-2"
+ class="gl-w-full gl-mt-3"
/>
<gl-modal
diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue
index ef242ea5f75..6304f723f6a 100644
--- a/app/assets/javascripts/packages/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue
@@ -2,11 +2,14 @@
import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
+import createFlash from '~/flash';
import PackageFilter from './packages_filter.vue';
import PackageList from './packages_list.vue';
import PackageSort from './packages_sort.vue';
-import { PACKAGE_REGISTRY_TABS } from '../constants';
+import { PACKAGE_REGISTRY_TABS, DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackagesComingSoon from '../coming_soon/packages_coming_soon.vue';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
+import { historyReplaceState } from '~/lib/utils/common_utils';
export default {
components: {
@@ -34,6 +37,7 @@ export default {
},
mounted() {
this.requestPackagesList();
+ this.checkDeleteAlert();
},
methods: {
...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']),
@@ -64,6 +68,16 @@ export default {
return s__('PackageRegistry|There are no packages yet');
},
+ checkDeleteAlert() {
+ const urlParams = new URLSearchParams(window.location.search);
+ const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
+ if (showAlert) {
+ // to be refactored to use gl-alert
+ createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
+ const cleanUrl = window.location.href.split('?')[0];
+ historyReplaceState(cleanUrl);
+ }
+ },
},
i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
@@ -77,7 +91,9 @@ export default {
<template>
<gl-tabs @input="tabChanged">
<template #tabs-end>
- <div class="d-flex align-self-center ml-md-auto py-1 py-md-0">
+ <div
+ class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end"
+ >
<package-filter class="mr-1" @filter="requestPackagesList" />
<package-sort @sort:changed="requestPackagesList" />
</div>
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index 510d04965cb..0ff8c86362d 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -5,7 +5,6 @@ export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __(
'Something went wrong while fetching the packages list.',
);
export const FETCH_PACKAGE_ERROR_MESSAGE = __('Something went wrong while fetching the package.');
-export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
export const DELETE_PACKAGE_SUCCESS_MESSAGE = __('Package deleted successfully');
export const DEFAULT_PAGE = 1;
diff --git a/app/assets/javascripts/packages/list/stores/actions.js b/app/assets/javascripts/packages/list/stores/actions.js
index 0ed24aee2c5..bbc11e3cf13 100644
--- a/app/assets/javascripts/packages/list/stores/actions.js
+++ b/app/assets/javascripts/packages/list/stores/actions.js
@@ -1,10 +1,10 @@
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import * as types from './mutation_types';
import {
FETCH_PACKAGES_LIST_ERROR_MESSAGE,
- DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue
index e000279b794..f93bc51d185 100644
--- a/app/assets/javascripts/packages/shared/components/package_list_row.vue
+++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue
@@ -1,9 +1,10 @@
<script>
-import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
import PackageTags from './package_tags.vue';
import PublishMethod from './publish_method.vue';
import { getPackageTypeLabel } from '../utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
export default {
name: 'PackageListRow',
@@ -12,8 +13,10 @@ export default {
GlIcon,
GlLink,
GlSprintf,
+ GlTruncate,
PackageTags,
PublishMethod,
+ ListItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,15 +62,15 @@ export default {
</script>
<template>
- <div class="gl-responsive-table-row" data-qa-selector="packages-row">
- <div class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap">
- <div class="d-flex align-items-center mr-2">
+ <list-item data-qa-selector="package_row">
+ <template #left-primary>
+ <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<gl-link
:href="packageLink"
+ class="gl-text-body gl-min-w-0"
data-qa-selector="package_link"
- class="text-dark font-weight-bold mb-md-1"
>
- {{ packageEntity.name }}
+ <gl-truncate :text="packageEntity.name" />
</gl-link>
<package-tags
@@ -78,41 +81,42 @@ export default {
:tag-display-limit="1"
/>
</div>
-
- <div class="d-flex text-secondary text-truncate mt-md-2">
+ </template>
+ <template #left-secondary>
+ <div class="gl-display-flex">
<span>{{ packageEntity.version }}</span>
- <div v-if="hasPipeline" class="d-none d-md-inline-block ml-1">
+ <div v-if="hasPipeline" class="gl-display-none gl-display-sm-flex gl-ml-2">
<gl-sprintf :message="s__('PackageRegistry|published by %{author}')">
<template #author>{{ packageEntity.pipeline.user.name }}</template>
</gl-sprintf>
</div>
- <div v-if="hasProjectLink" class="d-flex align-items-center">
- <gl-icon name="review-list" class="text-secondary ml-2 mr-1" />
+ <div v-if="hasProjectLink" class="gl-display-flex gl-align-items-center">
+ <gl-icon name="review-list" class="gl-ml-3 gl-mr-2 gl-min-w-0" />
<gl-link
+ class="gl-text-body gl-min-w-0"
data-testid="packages-row-project"
:href="`/${packageEntity.project_path}`"
- class="text-secondary"
- >{{ packageEntity.projectPathName }}</gl-link
>
+ <gl-truncate :text="packageEntity.projectPathName" />
+ </gl-link>
</div>
<div v-if="showPackageType" class="d-flex align-items-center" data-testid="package-type">
- <gl-icon name="package" class="text-secondary ml-2 mr-1" />
+ <gl-icon name="package" class="gl-ml-3 gl-mr-2" />
<span>{{ packageType }}</span>
</div>
</div>
- </div>
+ </template>
- <div
- class="table-section d-flex flex-md-column justify-content-between align-items-md-end"
- :class="disableDelete ? 'section-50' : 'section-40'"
- >
+ <template #right-primary>
<publish-method :package-entity="packageEntity" :is-group="isGroup" />
+ </template>
- <div class="text-secondary order-0 order-md-1 mt-md-2">
+ <template #right-secondary>
+ <span>
<gl-sprintf :message="__('Created %{timestamp}')">
<template #timestamp>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
@@ -120,10 +124,10 @@ export default {
</span>
</template>
</gl-sprintf>
- </div>
- </div>
+ </span>
+ </template>
- <div v-if="!disableDelete" class="table-section section-10 d-flex justify-content-end">
+ <template v-if="!disableDelete" #right-action>
<gl-button
data-testid="action-delete"
icon="remove"
@@ -134,6 +138,6 @@ export default {
:disabled="!packageEntity._links.delete_api_path"
@click="$emit('packageToDelete', packageEntity)"
/>
- </div>
- </div>
+ </template>
+ </list-item>
</template>
diff --git a/app/assets/javascripts/packages/shared/components/package_tags.vue b/app/assets/javascripts/packages/shared/components/package_tags.vue
index 391f53c225b..3d7e233c1ba 100644
--- a/app/assets/javascripts/packages/shared/components/package_tags.vue
+++ b/app/assets/javascripts/packages/shared/components/package_tags.vue
@@ -80,6 +80,7 @@ export default {
data-testid="tagBadge"
:class="tagBadgeClass(index)"
variant="info"
+ size="sm"
>{{ tag.name }}</gl-badge
>
@@ -89,7 +90,8 @@ export default {
data-testid="moreBadge"
variant="muted"
:title="moreTagsTooltip"
- class="gl-display-none d-md-flex gl-ml-2"
+ size="sm"
+ class="gl-display-none gl-display-md-flex gl-ml-2"
><gl-sprintf :message="__('+%{tags} more')">
<template #tags>
{{ moreTagsDisplay }}
@@ -101,7 +103,7 @@ export default {
v-if="moreTagsDisplay && hideLabel"
data-testid="moreBadge"
variant="muted"
- class="d-md-none gl-ml-2"
+ class="gl-display-md-none gl-ml-2"
>{{ tagsDisplay }}</gl-badge
>
</div>
diff --git a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
index cd9ef74d467..efd9f8db908 100644
--- a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
+++ b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
@@ -5,40 +5,13 @@ export default {
components: {
GlSkeletonLoader,
},
- props: {
- isGroup: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- desktopShapes() {
- return this.isGroup ? this.$options.shapes.groups : this.$options.shapes.projects;
- },
- desktopHeight() {
- return this.isGroup ? 38 : 54;
- },
- mobileHeight() {
- return this.isGroup ? 160 : 170;
- },
- },
- shapes: {
- groups: [
- { type: 'rect', width: '100', height: '10', x: '0', y: '15' },
- { type: 'rect', width: '100', height: '10', x: '195', y: '15' },
- { type: 'rect', width: '60', height: '10', x: '475', y: '15' },
- { type: 'rect', width: '60', height: '10', x: '675', y: '15' },
- { type: 'rect', width: '100', height: '10', x: '900', y: '15' },
- ],
- projects: [
- { type: 'rect', width: '220', height: '10', x: '0', y: '20' },
- { type: 'rect', width: '60', height: '10', x: '305', y: '20' },
- { type: 'rect', width: '60', height: '10', x: '535', y: '20' },
- { type: 'rect', width: '100', height: '10', x: '760', y: '20' },
- { type: 'rect', width: '30', height: '30', x: '970', y: '10', ref: 'button-loader' },
- ],
- },
+ shapes: [
+ { type: 'rect', width: '220', height: '10', x: '0', y: '20' },
+ { type: 'rect', width: '60', height: '10', x: '305', y: '20' },
+ { type: 'rect', width: '60', height: '10', x: '535', y: '20' },
+ { type: 'rect', width: '100', height: '10', x: '760', y: '20' },
+ { type: 'rect', width: '30', height: '30', x: '970', y: '10', ref: 'button-loader' },
+ ],
rowsToRender: {
mobile: 5,
desktop: 20,
@@ -48,34 +21,35 @@ export default {
<template>
<div>
- <div class="d-xs-flex flex-column d-md-none">
+ <div class="gl-flex-direction-column gl-display-sm-none" data-testid="mobile-loader">
<gl-skeleton-loader
v-for="index in $options.rowsToRender.mobile"
:key="index"
:width="500"
- :height="mobileHeight"
+ :height="170"
preserve-aspect-ratio="xMinYMax meet"
>
<rect width="500" height="10" x="0" y="15" rx="4" />
<rect width="500" height="10" x="0" y="45" rx="4" />
<rect width="500" height="10" x="0" y="75" rx="4" />
<rect width="500" height="10" x="0" y="105" rx="4" />
- <rect v-if="isGroup" width="500" height="10" x="0" y="135" rx="4" />
- <rect v-else width="30" height="30" x="470" y="135" rx="4" />
+ <rect width="500" height="10" x="0" y="135" rx="4" />
</gl-skeleton-loader>
</div>
-
- <div class="d-none d-md-flex flex-column">
+ <div
+ class="gl-display-none gl-display-sm-flex gl-flex-direction-column"
+ data-testid="desktop-loader"
+ >
<gl-skeleton-loader
v-for="index in $options.rowsToRender.desktop"
:key="index"
:width="1000"
- :height="desktopHeight"
+ :height="54"
preserve-aspect-ratio="xMinYMax meet"
>
<component
:is="r.type"
- v-for="(r, rIndex) in desktopShapes"
+ v-for="(r, rIndex) in $options.shapes"
:key="rIndex"
rx="4"
v-bind="r"
diff --git a/app/assets/javascripts/packages/shared/components/publish_method.vue b/app/assets/javascripts/packages/shared/components/publish_method.vue
index 1e18562a421..d17e23c4032 100644
--- a/app/assets/javascripts/packages/shared/components/publish_method.vue
+++ b/app/assets/javascripts/packages/shared/components/publish_method.vue
@@ -36,26 +36,28 @@ export default {
</script>
<template>
- <div class="d-flex align-items-center text-secondary order-1 order-md-0 mb-md-1">
+ <div class="gl-display-flex gl-align-items-center">
<template v-if="hasPipeline">
- <gl-icon name="git-merge" class="mr-1" />
- <strong ref="pipeline-ref" class="mr-1 text-dark">{{ packageEntity.pipeline.ref }}</strong>
+ <gl-icon name="git-merge" class="gl-mr-2" />
+ <span data-testid="pipeline-ref" class="gl-mr-2">{{ packageEntity.pipeline.ref }}</span>
- <gl-icon name="commit" class="mr-1" />
- <gl-link ref="pipeline-sha" :href="linkToCommit" class="mr-1">{{ packageShaShort }}</gl-link>
+ <gl-icon name="commit" class="gl-mr-2" />
+ <gl-link data-testid="pipeline-sha" :href="linkToCommit" class="gl-mr-2">{{
+ packageShaShort
+ }}</gl-link>
<clipboard-button
:text="packageEntity.pipeline.sha"
:title="__('Copy commit SHA')"
- css-class="border-0 text-secondary py-0 px-1"
+ css-class="gl-border-0 gl-py-0 gl-px-2"
/>
</template>
<template v-else>
- <gl-icon name="upload" class="mr-1" />
- <strong ref="manual-ref" class="text-dark">{{
- s__('PackageRegistry|Manually Published')
- }}</strong>
+ <gl-icon name="upload" class="gl-mr-2" />
+ <span data-testid="manually-published">
+ {{ s__('PackageRegistry|Manually Published') }}
+ </span>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
index 279c2959fa9..e5131db59bf 100644
--- a/app/assets/javascripts/packages/shared/constants.js
+++ b/app/assets/javascripts/packages/shared/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const PackageType = {
CONAN: 'conan',
MAVEN: 'maven',
@@ -22,3 +24,6 @@ export const TrackingCategories = {
[PackageType.NPM]: 'NpmPackages',
[PackageType.CONAN]: 'ConanPackages',
};
+
+export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
+export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
index bbaaeb55c65..a2fca238613 100644
--- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
@@ -1,3 +1,3 @@
-import setup from 'ee_else_ce/admin/application_settings/setup_metrics_and_profiling';
+import setup from '~/admin/application_settings/setup_metrics_and_profiling';
document.addEventListener('DOMContentLoaded', setup);
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
index e7b468f039f..f8fc53799a8 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
@@ -3,9 +3,9 @@ import { __ } from '../../../locale';
import { deprecatedCreateFlash as flash } from '../../../flash';
export default class PayloadPreviewer {
- constructor(trigger, container) {
+ constructor(trigger) {
this.trigger = trigger;
- this.container = container;
+ this.container = document.querySelector(trigger.dataset.payloadSelector);
this.isVisible = false;
this.isInserted = false;
}
diff --git a/app/assets/javascripts/pages/admin/clusters/new/index.js b/app/assets/javascripts/pages/admin/clusters/new/index.js
new file mode 100644
index 00000000000..876bab0b339
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/clusters/new/index.js
@@ -0,0 +1,5 @@
+import initNewCluster from '~/clusters/new_cluster';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initNewCluster();
+});
diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js
new file mode 100644
index 00000000000..1cc54df15a1
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/cohorts/index.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import UsagePingDisabled from '~/admin/cohorts/components/usage_ping_disabled.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const emptyStateContainer = document.getElementById('js-cohorts-empty-state');
+
+ if (!emptyStateContainer) return false;
+
+ const { emptyStateSvgPath, enableUsagePingLink, docsLink } = emptyStateContainer.dataset;
+
+ return new Vue({
+ el: emptyStateContainer,
+ provide: {
+ svgPath: emptyStateSvgPath,
+ primaryButtonPath: enableUsagePingLink,
+ docsLink,
+ },
+ render(h) {
+ return h(UsagePingDisabled);
+ },
+ });
+});
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
new file mode 100644
index 00000000000..643497003ba
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import UserCallout from '~/user_callout';
+import UsagePingDisabled from '~/admin/dev_ops_report/components/usage_ping_disabled.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ // eslint-disable-next-line no-new
+ new UserCallout();
+
+ const emptyStateContainer = document.getElementById('js-devops-empty-state');
+
+ if (!emptyStateContainer) return false;
+
+ const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
+
+ return new Vue({
+ el: emptyStateContainer,
+ provide: {
+ isAdmin: Boolean(isAdmin),
+ svgPath: emptyStateSvgPath,
+ primaryButtonPath: enableUsagePingLink,
+ docsLink,
+ },
+ render(h) {
+ return h(UsagePingDisabled);
+ },
+ });
+});
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index 8bb093da771..b92fc8d125d 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -1,11 +1,14 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml, GlModal } from '@gitlab/ui';
import { escape } from 'lodash';
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
export default {
components: {
- DeprecatedModal,
+ GlModal,
+ },
+ directives: {
+ SafeHtml,
},
props: {
deleteProjectUrl: {
@@ -62,51 +65,57 @@ export default {
false,
);
},
- primaryButtonLabel() {
- return s__('AdminProjects|Delete project');
- },
canSubmit() {
return this.enteredProjectName === this.projectName;
},
+ primaryProps() {
+ return {
+ text: s__('Delete project'),
+ attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.canSubmit }],
+ };
+ },
},
methods: {
onCancel() {
this.enteredProjectName = '';
},
onSubmit() {
+ if (!this.canSubmit) {
+ return;
+ }
this.$refs.form.submit();
this.enteredProjectName = '';
},
},
+ cancelProps: {
+ text: __('Cancel'),
+ },
};
</script>
<template>
- <deprecated-modal
- id="delete-project-modal"
+ <gl-modal
+ modal-id="delete-project-modal"
:title="title"
- :text="text"
- :primary-button-label="primaryButtonLabel"
- :submit-disabled="!canSubmit"
- kind="danger"
- @submit="onSubmit"
+ :action-primary="primaryProps"
+ :action-cancel="$options.cancelProps"
+ :ok-disabled="!canSubmit"
+ @primary="onSubmit"
@cancel="onCancel"
>
- <template #body="props">
- <p v-html="props.text"></p>
- <p v-html="confirmationTextLabel"></p>
- <form ref="form" :action="deleteProjectUrl" method="post">
- <input ref="method" type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <input
- v-model="enteredProjectName"
- name="projectName"
- class="form-control"
- type="text"
- aria-labelledby="input-label"
- autocomplete="off"
- />
- </form>
- </template>
- </deprecated-modal>
+ <p v-safe-html="text"></p>
+ <p v-safe-html="confirmationTextLabel"></p>
+ <form ref="form" :action="deleteProjectUrl" method="post">
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <input
+ v-model="enteredProjectName"
+ name="projectName"
+ class="form-control"
+ type="text"
+ aria-labelledby="input-label"
+ autocomplete="off"
+ />
+ </form>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js
index 6fa8760545d..ebb1a74e970 100644
--- a/app/assets/javascripts/pages/admin/projects/index/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index/index.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
@@ -17,6 +16,18 @@ document.addEventListener('DOMContentLoaded', () => {
deleteProjectUrl: '',
projectName: '',
},
+ mounted() {
+ const deleteProjectButtons = document.querySelectorAll('.delete-project-button');
+ deleteProjectButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const buttonProps = button.dataset;
+ deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
+ deleteModal.projectName = buttonProps.projectName;
+
+ this.$root.$emit('bv::show::modal', 'delete-project-modal');
+ });
+ });
+ },
render(createElement) {
return createElement(deleteProjectModal, {
props: {
@@ -27,12 +38,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
-
- $(document).on('shown.bs.modal', event => {
- if (event.relatedTarget.classList.contains('delete-project-button')) {
- const buttonProps = event.relatedTarget.dataset;
- deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
- deleteModal.projectName = buttonProps.projectName;
- }
- });
});
diff --git a/app/assets/javascripts/pages/admin/services/index/index.js b/app/assets/javascripts/pages/admin/services/index/index.js
new file mode 100644
index 00000000000..b2dfbb5a9fc
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/services/index/index.js
@@ -0,0 +1,6 @@
+import PersistentUserCallout from '~/persistent_user_callout';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const callout = document.querySelector('.js-service-templates-deprecated');
+ PersistentUserCallout.factory(callout);
+});
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index e09b8e1bdd5..9c303cc6445 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,6 +1,5 @@
<script>
-import { escape } from 'lodash';
-import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
+import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
@@ -8,6 +7,7 @@ export default {
GlModal,
GlButton,
GlFormInput,
+ GlSprintf,
},
props: {
title: {
@@ -52,27 +52,6 @@ export default {
modalTitle() {
return sprintf(this.title, { username: this.username });
},
- text() {
- return sprintf(
- this.content,
- {
- username: `<strong>${escape(this.username)}</strong>`,
- strong_start: '<strong>',
- strong_end: '</strong>',
- },
- false,
- );
- },
- confirmationTextLabel() {
- return sprintf(
- s__('AdminUsers|To confirm, type %{username}'),
- {
- username: `<code>${escape(this.username)}</code>`,
- },
- false,
- );
- },
-
secondaryButtonLabel() {
return s__('AdminUsers|Block user');
},
@@ -107,8 +86,25 @@ export default {
<template>
<gl-modal ref="modal" modal-id="delete-user-modal" :title="modalTitle" kind="danger">
<template>
- <p v-html="text"></p>
- <p v-html="confirmationTextLabel"></p>
+ <p>
+ <gl-sprintf :message="content">
+ <template #username>
+ <strong>{{ username }}</strong>
+ </template>
+ <template #strong="props">
+ <strong>{{ props.content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p>
+ <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
+ <template #username>
+ <code>{{ username }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+
<form ref="form" :action="deleteUserUrl" method="post" @submit.prevent>
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
diff --git a/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue b/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue
index 4c335cfb018..4ca6ce6f1c3 100644
--- a/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
import { sprintf } from '~/locale';
diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js
index 35c67190b62..a9773807212 100644
--- a/app/assets/javascripts/pages/constants.js
+++ b/app/assets/javascripts/pages/constants.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
export const FILTERED_SEARCH = {
MERGE_REQUESTS: 'merge_requests',
ISSUES: 'issues',
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
index 6b907f31a37..9fa441348c7 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
+++ b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
@@ -2,11 +2,15 @@
import { GlBanner } from '@gitlab/ui';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import Tracking from '~/tracking';
+
+const trackingMixin = Tracking.mixin();
export default {
components: {
GlBanner,
},
+ mixins: [trackingMixin],
inject: {
svgPath: {
default: '',
@@ -20,6 +24,9 @@ export default {
calloutsFeatureId: {
default: '',
},
+ trackLabel: {
+ default: '',
+ },
},
i18n: {
title: s__('CustomizeHomepageBanner|Do you want to customize this page?'),
@@ -31,8 +38,19 @@ export default {
data() {
return {
visible: true,
+ tracking: {
+ label: this.trackLabel,
+ },
};
},
+ created() {
+ this.$nextTick(() => {
+ this.addTrackingAttributesToButton();
+ });
+ },
+ mounted() {
+ this.trackOnShow();
+ },
methods: {
handleClose() {
axios
@@ -45,6 +63,23 @@ export default {
});
this.visible = false;
+ this.track('click_dismiss');
+ },
+ trackOnShow() {
+ if (this.visible) this.track('show_home_page_banner');
+ },
+ addTrackingAttributesToButton() {
+ // we can't directly add these on the button like we need to due to
+ // button not being modifiable currently
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/blob/9209ec424e5cca14bc8a1b5c9fa12636d8c83dad/src/components/base/banner/banner.vue#L60
+ const button = this.$refs.banner.$el.querySelector(
+ `[href='${this.preferencesBehaviorPath}']`,
+ );
+
+ if (button) {
+ button.setAttribute('data-track-event', 'click_go_to_preferences');
+ button.setAttribute('data-track-label', this.trackLabel);
+ }
},
},
};
@@ -53,6 +88,7 @@ export default {
<template>
<gl-banner
v-if="visible"
+ ref="banner"
:title="$options.i18n.title"
:button-text="$options.i18n.button_text"
:button-link="preferencesBehaviorPath"
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index f76b4b44452..6f8d954d798 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this, no-unneeded-ternary */
import $ from 'jquery';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils';
@@ -50,7 +50,7 @@ export default class Todos {
}
initFilterDropdown($dropdown, fieldName, searchFields) {
- $dropdown.glDropdown({
+ initDeprecatedJQueryDropdown($dropdown, {
fieldName,
selectable: true,
filterable: searchFields ? true : false,
diff --git a/app/assets/javascripts/pages/groups/clusters/new/index.js b/app/assets/javascripts/pages/groups/clusters/new/index.js
new file mode 100644
index 00000000000..876bab0b339
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/new/index.js
@@ -0,0 +1,5 @@
+import initNewCluster from '~/clusters/new_cluster';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initNewCluster();
+});
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index e146592e134..3fa3a132dfa 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -4,6 +4,7 @@ import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
+import initGroupMembersApp from '~/groups/members';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
@@ -25,6 +26,11 @@ document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
+ initGroupMembersApp(document.querySelector('.js-group-members-list'));
+ initGroupMembersApp(document.querySelector('.js-group-linked-list'));
+ initGroupMembersApp(document.querySelector('.js-group-invited-members-list'));
+ initGroupMembersApp(document.querySelector('.js-group-access-requests-list'));
+
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 2496003919a..ae481d16ee9 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,5 +1,5 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import initIssuablesList from '~/issuables_list';
+import initIssuablesList from '~/issues_list';
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 37b253d7c48..8546b1f759f 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -8,6 +8,7 @@ import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GroupTabs from './group_tabs';
+import initInviteMembersBanner from '~/groups/init_invite_members_banner';
export default function initGroupDetails(actionName = 'show') {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
@@ -27,4 +28,5 @@ export default function initGroupDetails(actionName = 'show') {
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
+ initInviteMembersBanner();
}
diff --git a/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js b/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js
deleted file mode 100644
index c1056537f90..00000000000
--- a/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import UserCallout from '~/user_callout';
-
-document.addEventListener('DOMContentLoaded', () => new UserCallout());
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 5be8e6697a2..983062c79f1 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -1,4 +1,5 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash } from '~/flash';
@@ -11,6 +12,9 @@ export default {
components: {
DeprecatedModal,
},
+ directives: {
+ SafeHtml,
+ },
props: {
issueCount: {
type: Number,
@@ -124,7 +128,7 @@ Once deleted, it cannot be undone or recovered.`),
@submit="onSubmit"
>
<template #body="props">
- <p v-html="props.text"></p>
+ <p v-safe-html="props.text"></p>
</template>
</deprecated-modal>
</template>
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index cb7198e9789..46e59cd6572 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -6,6 +6,33 @@ import GpgBadges from '~/gpg_badges';
import '~/sourcegraph/load';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
+const createGitlabCiYmlVisualization = (containerId = '#js-blob-toggle-graph-preview') => {
+ const el = document.querySelector(containerId);
+ const { filename, blobData } = el?.dataset;
+
+ const nameRegexp = /\.gitlab-ci.yml/;
+
+ if (!el || !nameRegexp.test(filename)) {
+ return;
+ }
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ GitlabCiYamlVisualization: () =>
+ import('~/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue'),
+ },
+ render(createElement) {
+ return createElement('gitlabCiYamlVisualization', {
+ props: {
+ blobData,
+ },
+ });
+ },
+ });
+};
+
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
initBlob();
@@ -63,4 +90,8 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
}
+
+ if (gon?.features?.gitlabCiYmlPreview) {
+ createGitlabCiYmlVisualization();
+ }
});
diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js
index 37e8c75f299..623d0a10606 100644
--- a/app/assets/javascripts/pages/projects/branches/index/index.js
+++ b/app/assets/javascripts/pages/projects/branches/index/index.js
@@ -1,4 +1,4 @@
-import AjaxLoadingSpinner from '~/ajax_loading_spinner';
+import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
import DeleteModal from '~/branches/branches_delete_modal';
import initDiverganceGraph from '~/branches/divergence_graph';
diff --git a/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
index 9ab73be80a0..d270bee25c7 100644
--- a/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
@@ -1,19 +1,48 @@
+import createFlash from '~/flash';
+import { BLOB_EDITOR_ERROR } from '~/blob_edit/constants';
+
export default class CILintEditor {
constructor() {
- this.editor = window.ace.edit('ci-editor');
- this.textarea = document.querySelector('#content');
+ const monacoEnabled = window?.gon?.features?.monacoCi;
this.clearYml = document.querySelector('.clear-yml');
-
- this.editor.getSession().setMode('ace/mode/yaml');
- this.editor.on('input', () => {
- const content = this.editor.getSession().getValue();
- this.textarea.value = content;
- });
-
this.clearYml.addEventListener('click', this.clear.bind(this));
+
+ return monacoEnabled ? this.initEditorLite() : this.initAce();
}
clear() {
this.editor.setValue('');
}
+
+ initEditorLite() {
+ import(/* webpackChunkName: 'monaco_editor_lite' */ '~/editor/editor_lite')
+ .then(({ default: EditorLite }) => {
+ const editorEl = document.getElementById('editor');
+ const fileContentEl = document.getElementById('content');
+ const form = document.querySelector('.js-ci-lint-form');
+
+ const rootEditor = new EditorLite();
+
+ this.editor = rootEditor.createInstance({
+ el: editorEl,
+ blobPath: '.gitlab-ci.yml',
+ blobContent: editorEl.innerText,
+ });
+
+ form.addEventListener('submit', () => {
+ fileContentEl.value = this.editor.getValue();
+ });
+ })
+ .catch(() => createFlash({ message: BLOB_EDITOR_ERROR }));
+ }
+
+ initAce() {
+ this.editor = window.ace.edit('ci-editor');
+ this.textarea = document.getElementById('content');
+
+ this.editor.getSession().setMode('ace/mode/yaml');
+ this.editor.on('input', () => {
+ this.textarea.value = this.editor.getSession().getValue();
+ });
+ }
}
diff --git a/app/assets/javascripts/pages/projects/ci/lints/new/index.js b/app/assets/javascripts/pages/projects/ci/lints/new/index.js
index 8e8a843da0b..02bfee9810f 100644
--- a/app/assets/javascripts/pages/projects/ci/lints/new/index.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/new/index.js
@@ -1,3 +1,11 @@
import CILintEditor from '../ci_lint_editor';
+import initCILint from '~/ci_lint/index';
-document.addEventListener('DOMContentLoaded', () => new CILintEditor());
+document.addEventListener('DOMContentLoaded', () => {
+ if (gon?.features?.ciLintVue) {
+ initCILint();
+ } else {
+ // eslint-disable-next-line no-new
+ new CILintEditor();
+ }
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
new file mode 100644
index 00000000000..876bab0b339
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/new/index.js
@@ -0,0 +1,5 @@
+import initNewCluster from '~/clusters/new_cluster';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initNewCluster();
+});
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index a245af72d93..d5fb2a8be3c 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -14,24 +14,23 @@ import axios from '~/lib/utils/axios_utils';
import syntaxHighlight from '~/syntax_highlight';
import flash from '~/flash';
import { __ } from '~/locale';
+import loadAwardsHandler from '~/awards_handler';
document.addEventListener('DOMContentLoaded', () => {
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
+ initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
+ new ZenMode();
+ new ShortcutsNavigation();
+ new MiniPipelineGraph({
+ container: '.js-commit-pipeline-graph',
+ }).bindEvents();
+ initNotes();
+ // eslint-disable-next-line no-jquery/no-load
+ $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
+ fetchCommitMergeRequests();
+
const filesContainer = $('.js-diffs-batch');
- const initAfterPageLoad = () => {
- new Diff();
- new ZenMode();
- new ShortcutsNavigation();
- new MiniPipelineGraph({
- container: '.js-commit-pipeline-graph',
- }).bindEvents();
- initNotes();
- initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
- // eslint-disable-next-line no-jquery/no-load
- $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
- fetchCommitMergeRequests();
- };
if (filesContainer.length) {
const batchPath = filesContainer.data('diffFilesPath');
@@ -42,12 +41,13 @@ document.addEventListener('DOMContentLoaded', () => {
filesContainer.html($(data.html));
syntaxHighlight(filesContainer);
handleLocationHash();
- initAfterPageLoad();
+ new Diff();
})
.catch(() => {
flash(__('An error occurred while retrieving diff files'));
});
} else {
- initAfterPageLoad();
+ new Diff();
}
+ loadAwardsHandler();
});
diff --git a/app/assets/javascripts/pages/projects/constants.js b/app/assets/javascripts/pages/projects/constants.js
index 9efbf7cd36e..8dc765e5d10 100644
--- a/app/assets/javascripts/pages/projects/constants.js
+++ b/app/assets/javascripts/pages/projects/constants.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
export const ISSUABLE_INDEX = {
MERGE_REQUEST: 'merge_request_',
ISSUE: 'issue_',
diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js
index 10e3e28f024..5d3a153cbd1 100644
--- a/app/assets/javascripts/pages/projects/environments/show/index.js
+++ b/app/assets/javascripts/pages/projects/environments/show/index.js
@@ -1,3 +1,3 @@
import initShowEnvironment from '~/environments/mount_show';
-document.addEventListener('DOMContentLoaded', () => initShowEnvironment());
+document.addEventListener('DOMContentLoaded', initShowEnvironment);
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
index b4816fa2cb3..57838050d55 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
@@ -7,6 +7,7 @@ import {
GlTooltipDirective,
GlTooltip,
GlBadge,
+ GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants';
import { __ } from '~/locale';
@@ -23,6 +24,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
props: {
group: {
@@ -119,7 +121,7 @@ export default {
</span>
</div>
<div v-if="group.description" class="description">
- <span v-html="group.markdown_description"> </span>
+ <span v-safe-html="group.markdown_description"> </span>
</div>
</div>
<div class="gl-display-flex gl-flex-shrink-0">
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 09b440d1413..384216f29eb 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -1,154 +1,155 @@
import Vue from 'vue';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { waitForCSSLoaded } from '../../../../helpers/startup_css_helper';
import { __ } from '~/locale';
import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin';
document.addEventListener('DOMContentLoaded', () => {
- const languagesContainer = document.getElementById('js-languages-chart');
- const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
- const monthContainer = document.getElementById('js-month-chart');
- const weekdayContainer = document.getElementById('js-weekday-chart');
- const hourContainer = document.getElementById('js-hour-chart');
+ waitForCSSLoaded(() => {
+ const languagesContainer = document.getElementById('js-languages-chart');
+ const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
+ const monthContainer = document.getElementById('js-month-chart');
+ const weekdayContainer = document.getElementById('js-weekday-chart');
+ const hourContainer = document.getElementById('js-hour-chart');
+ const LANGUAGE_CHART_HEIGHT = 300;
+ const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
+ if (firstDayOfWeek === 0) {
+ return weekDays;
+ }
- const LANGUAGE_CHART_HEIGHT = 300;
+ return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
+ const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
- const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
- if (firstDayOfWeek === 0) {
- return weekDays;
- }
+ return {
+ ...acc,
+ [reorderedDayName]: weekDays[reorderedDayName],
+ };
+ }, {});
+ };
- return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
- const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
-
- return {
- ...acc,
- [reorderedDayName]: weekDays[reorderedDayName],
- };
- }, {});
- };
-
- // eslint-disable-next-line no-new
- new Vue({
- el: languagesContainer,
- components: {
- GlColumnChart,
- },
- data() {
- return {
- chartData: JSON.parse(languagesContainer.dataset.chartData),
- };
- },
- computed: {
- seriesData() {
- return { full: this.chartData.map(d => [d.label, d.value]) };
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: languagesContainer,
+ components: {
+ GlColumnChart,
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- data: this.seriesData,
- xAxisTitle: __('Used programming language'),
- yAxisTitle: __('Percentage'),
- xAxisType: 'category',
- },
- attrs: {
- height: LANGUAGE_CHART_HEIGHT,
+ data() {
+ return {
+ chartData: JSON.parse(languagesContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ return { full: this.chartData.map(d => [d.label, d.value]) };
},
- });
- },
- });
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Used programming language'),
+ yAxisTitle: __('Percentage'),
+ xAxisType: 'category',
+ },
+ attrs: {
+ height: LANGUAGE_CHART_HEIGHT,
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: codeCoverageContainer,
- render(h) {
- return h(CodeCoverage, {
- props: {
- graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint,
- },
- });
- },
- });
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: codeCoverageContainer,
+ render(h) {
+ return h(CodeCoverage, {
+ props: {
+ graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint,
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: monthContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(monthContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- data: this.seriesData,
- xAxisTitle: __('Day of month'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- });
- },
- });
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: monthContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(monthContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Day of month'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: weekdayContainer,
- components: {
- GlColumnChart,
- },
- data() {
- return {
- chartData: JSON.parse(weekdayContainer.dataset.chartData),
- };
- },
- computed: {
- seriesData() {
- const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
- const data = Object.keys(weekDays).reduce((acc, key) => {
- acc.push([key, weekDays[key]]);
- return acc;
- }, []);
- return { full: data };
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: weekdayContainer,
+ components: {
+ GlColumnChart,
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- data: this.seriesData,
- xAxisTitle: __('Weekday'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
+ data() {
+ return {
+ chartData: JSON.parse(weekdayContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
+ const data = Object.keys(weekDays).reduce((acc, key) => {
+ acc.push([key, weekDays[key]]);
+ return acc;
+ }, []);
+ return { full: data };
},
- });
- },
- });
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Weekday'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: hourContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(hourContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- data: this.seriesData,
- xAxisTitle: __('Hour (UTC)'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- });
- },
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: hourContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(hourContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Hour (UTC)'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
});
});
diff --git a/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js
index 260ba69b4bc..534614349bf 100644
--- a/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js
@@ -1,4 +1,4 @@
-import initIssuablesList from '~/issuables_list';
+import initIssuablesList from '~/issues_list';
document.addEventListener('DOMContentLoaded', () => {
initIssuablesList();
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 1711d122080..e1add4a2af3 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -7,7 +7,7 @@ import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
-import initIssuablesList from '~/issuables_list';
+import initIssuablesList from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { showLearnGitLabIssuesPopover } from '~/onboarding_issues';
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index 9304d9b6832..e0c1332796f 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,5 +1,5 @@
import FilteredSearchServiceDesk from './filtered_search';
-import initIssuablesList from '~/issuables_list';
+import initIssuablesList from '~/issues_list';
document.addEventListener('DOMContentLoaded', () => {
const supportBotData = JSON.parse(
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 5ac6c17e09d..98ae4e26257 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -1,27 +1,31 @@
+import loadAwardsHandler from '~/awards_handler';
import initIssuableSidebar from '~/init_issuable_sidebar';
import Issue from '~/issue';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import { store } from '~/notes/stores';
-import initIssueableApp from '~/issue_show';
+import initIssueApp from '~/issue_show/issue';
+import initIncidentApp from '~/issue_show/incident';
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
+import { parseIssuableData } from '~/issue_show/utils/parse_data';
export default function() {
- initIssueableApp();
+ const { issueType, ...issuableData } = parseIssuableData();
+
+ if (issueType === 'incident') {
+ initIncidentApp(issuableData);
+ } else {
+ initIssueApp(issuableData);
+ }
+
initIssuableHeaderWarning(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
- // This will be removed when we remove the `design_management_moved` feature flag
- // See https://gitlab.com/gitlab-org/gitlab/-/issues/223197
- import(/* webpackChunkName: 'design_management' */ '~/design_management_legacy')
- .then(module => module.default())
- .catch(() => {});
-
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then(module => module.default())
.catch(() => {});
@@ -34,4 +38,6 @@ export default function() {
} else {
initIssuableSidebar();
}
+
+ loadAwardsHandler();
}
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index ce74a6de11f..aef4feef42c 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,4 +1,5 @@
import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initRelatedIssues from '~/related_issues';
import initShow from '../show';
document.addEventListener('DOMContentLoaded', () => {
@@ -6,4 +7,5 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon.features && !gon.features.vueIssuableSidebar) {
initSidebarBundle();
}
+ initRelatedIssues();
});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
index b72fe6681df..e9f0e008435 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
@@ -1,8 +1,9 @@
import $ from 'jquery';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default () => {
const $targetProjectDropdown = $('.js-target-project');
- $targetProjectDropdown.glDropdown({
+ initDeprecatedJQueryDropdown($targetProjectDropdown, {
selectable: true,
fieldName: $targetProjectDropdown.data('fieldName'),
filterable: true,
@@ -16,7 +17,7 @@ export default () => {
$('.mr_target_commit').empty();
const $targetBranchDropdown = $('.js-target-branch');
$targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
- $targetBranchDropdown.data('glDropdown').clearMenu();
+ $targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu();
},
});
};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 25abb80cfae..11af50169f5 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -6,6 +6,7 @@ import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
import initSourcegraph from '~/sourcegraph';
+import loadAwardsHandler from '~/awards_handler';
export default function() {
new ZenMode(); // eslint-disable-line no-new
@@ -19,4 +20,5 @@ export default function() {
handleLocationHash();
howToMerge();
initSourcegraph();
+ loadAwardsHandler();
}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index da96e6f36b4..7a3923dfefd 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -1,6 +1,8 @@
<script>
+/* eslint-disable vue/no-v-html */
import Vue from 'vue';
import Cookies from 'js-cookie';
+import { GlIcon } from '@gitlab/ui';
import Translate from '../../../../../vue_shared/translate';
// Full path is needed for Jest to be able to correctly mock this file
import illustrationSvg from '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
@@ -12,6 +14,9 @@ const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
+ components: {
+ GlIcon,
+ },
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
@@ -33,7 +38,7 @@ export default {
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button id="dismiss-callout-btn" class="btn btn-default close" @click="dismissCallout">
- <i aria-hidden="true" class="fa fa-times"> </i>
+ <gl-icon name="close" aria-hidden="true" />
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
index 0057700c1b3..4b203891640 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class TargetBranchDropdown {
constructor() {
@@ -10,7 +11,7 @@ export default class TargetBranchDropdown {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.formatBranchesList(),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index a20a0526f12..2a58e015ff1 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
@@ -1,3 +1,5 @@
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+
const defaultTimezone = { name: 'UTC', offset: 0 };
const defaults = {
$inputEl: null,
@@ -42,7 +44,7 @@ export default class TimezoneDropdown {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.timezoneData,
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index bb285635425..2f27814a692 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -8,52 +8,59 @@ import { serializeForm } from '~/lib/utils/forms';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
import projectSelect from '../../project_select';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class Project {
constructor() {
const $cloneOptions = $('ul.clone-options-dropdown');
- const $projectCloneField = $('#project_clone');
- const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
- const mobileCloneField = document.querySelector(
- '.js-mobile-git-clone .js-clone-dropdown-label',
- );
-
- const selectedCloneOption = $cloneBtnLabel.text().trim();
- if (selectedCloneOption.length > 0) {
- $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
- }
+ if ($cloneOptions.length) {
+ const $projectCloneField = $('#project_clone');
+ const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
+ const mobileCloneField = document.querySelector(
+ '.js-mobile-git-clone .js-clone-dropdown-label',
+ );
+
+ const selectedCloneOption = $cloneBtnLabel.text().trim();
+ if (selectedCloneOption.length > 0) {
+ $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
+ }
- $('a', $cloneOptions).on('click', e => {
- e.preventDefault();
- const $this = $(e.currentTarget);
- const url = $this.attr('href');
- const cloneType = $this.data('cloneType');
-
- $('.is-active', $cloneOptions).removeClass('is-active');
- $(`a[data-clone-type="${cloneType}"]`).each(function() {
- const $el = $(this);
- const activeText = $el.find('.dropdown-menu-inner-title').text();
- const $container = $el.closest('.project-clone-holder');
- const $label = $container.find('.js-clone-dropdown-label');
-
- $el.toggleClass('is-active');
- $label.text(activeText);
+ $('a', $cloneOptions).on('click', e => {
+ e.preventDefault();
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+ const cloneType = $this.data('cloneType');
+
+ $('.is-active', $cloneOptions).removeClass('is-active');
+ $(`a[data-clone-type="${cloneType}"]`).each(function() {
+ const $el = $(this);
+ const activeText = $el.find('.dropdown-menu-inner-title').text();
+ const $container = $el.closest('.project-clone-holder');
+ const $label = $container.find('.js-clone-dropdown-label');
+
+ $el.toggleClass('is-active');
+ $label.text(activeText);
+ });
+
+ if (mobileCloneField) {
+ mobileCloneField.dataset.clipboardText = url;
+ } else {
+ $projectCloneField.val(url);
+ }
+ $('.js-git-empty .js-clone').text(url);
});
+ }
- if (mobileCloneField) {
- mobileCloneField.dataset.clipboardText = url;
- } else {
- $projectCloneField.val(url);
- }
- $('.js-git-empty .js-clone').text(url);
- });
// Ref switcher
- Project.initRefSwitcher();
- $('.project-refs-select').on('change', function() {
- return $(this)
- .parents('form')
- .submit();
- });
+ if (document.querySelector('.js-project-refs-dropdown')) {
+ Project.initRefSwitcher();
+ $('.project-refs-select').on('change', function() {
+ return $(this)
+ .parents('form')
+ .submit();
+ });
+ }
+
$('.hide-no-ssh-message').on('click', function(e) {
Cookies.set('hide_no_ssh_message', 'false');
$(this)
@@ -77,6 +84,7 @@ export default class Project {
.remove();
return e.preventDefault();
});
+
Project.projectSelectDropdown();
}
@@ -104,7 +112,7 @@ export default class Project {
const action = $form.attr('action');
const linkTarget = mergeUrlParams(serializeForm($form[0]), action);
- return $dropdown.glDropdown({
+ return initDeprecatedJQueryDropdown($dropdown, {
data(term, callback) {
axios
.get($dropdown.data('refsUrl'), {
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
index 92d23772565..b7546a6bed7 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
@@ -1,5 +1,10 @@
<script>
+import { GlIcon } from '@gitlab/ui';
+
export default {
+ components: {
+ GlIcon,
+ },
props: {
label: {
type: String,
@@ -25,7 +30,7 @@ export default {
<label v-if="label" class="label-bold">
{{ label }}
<a v-if="helpPath" :href="helpPath" target="_blank">
- <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"> </i>
+ <gl-icon name="question-o" />
</a>
</label>
<span v-if="helpText" class="form-text text-muted"> {{ helpText }} </span> <slot></slot>
diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js
index 85aaaa2c9da..92d01343bd5 100644
--- a/app/assets/javascripts/pages/search/show/index.js
+++ b/app/assets/javascripts/pages/search/show/index.js
@@ -1,3 +1,7 @@
import Search from './search';
+import initStateFilter from '~/search/state_filter';
-document.addEventListener('DOMContentLoaded', () => new Search());
+document.addEventListener('DOMContentLoaded', () => {
+ initStateFilter();
+ return new Search();
+});
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index cc2128490ec..6ff74325a5e 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { deprecatedCreateFlash as Flash } from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
@@ -20,7 +20,7 @@ export default class Search {
this.eventListeners();
refreshCounts();
- $groupDropdown.glDropdown({
+ initDeprecatedJQueryDropdown($groupDropdown, {
selectable: true,
filterable: true,
filterRemote: true,
@@ -46,7 +46,7 @@ export default class Search {
clicked: () => Search.submitSearch(),
});
- $projectDropdown.glDropdown({
+ initDeprecatedJQueryDropdown($projectDropdown, {
selectable: true,
filterable: true,
filterRemote: true,
diff --git a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
index a7b7d597fb7..653aad3d2f5 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
@@ -1,11 +1,12 @@
<script>
+import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { escape } from 'lodash';
-import { GlModal, GlModalDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
components: {
GlModal,
+ GlButton,
},
directives: {
'gl-modal': GlModalDirective,
@@ -55,14 +56,14 @@ export default {
<template>
<div class="d-inline-block">
- <button
+ <gl-button
v-gl-modal="modalId"
- type="button"
- class="btn btn-danger"
+ category="primary"
+ variant="danger"
data-qa-selector="delete_button"
>
{{ __('Delete') }}
- </button>
+ </gl-button>
<gl-modal
:title="title"
:action-primary="{
diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue
index 65f84e75e86..843c50cf9bc 100644
--- a/app/assets/javascripts/pdf/page/index.vue
+++ b/app/assets/javascripts/pdf/page/index.vue
@@ -52,19 +52,19 @@ export default {
<style>
.pdf-page {
- margin: 8px auto 0 auto;
+ margin: 8px auto 0;
border-top: 1px #ddd solid;
border-bottom: 1px #ddd solid;
width: 100%;
}
.pdf-page:first-child {
- margin-top: 0px;
- border-top: 0px;
+ margin-top: 0;
+ border-top: 0;
}
.pdf-page:last-child {
- margin-bottom: 0px;
- border-bottom: 0px;
+ margin-bottom: 0;
+ border-bottom: 0;
}
</style>
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index ef24dbfb6ce..9f05ee5c7c2 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,14 +1,14 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import RequestWarning from './request_warning.vue';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
RequestWarning,
GlModal: DeprecatedModal2,
- Icon,
+ GlIcon,
},
props: {
currentRequest: {
@@ -104,7 +104,7 @@ export default {
type="button"
:aria-label="__('Toggle backtrace')"
>
- <icon :size="12" name="ellipsis_h" />
+ <gl-icon :size="12" name="ellipsis_h" />
</button>
</div>
<pre v-if="item.backtrace" class="backtrace-row js-toggle-content mt-2">{{
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index cccb5e1be06..165feb1b6aa 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { glEmojiTag } from '~/emoji';
import AddRequest from './add_request.vue';
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index c22a648d17f..5a9d3a6d313 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlPopover } from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
import { n__ } from '~/locale';
diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue
index 0128d5bd733..b61e1e5b7a9 100644
--- a/app/assets/javascripts/performance_bar/components/request_warning.vue
+++ b/app/assets/javascripts/performance_bar/components/request_warning.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlPopover } from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index a294f3f36a6..f29b5f42d8f 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -5,14 +5,17 @@ import axios from '~/lib/utils/axios_utils';
import PerformanceBarService from './services/performance_bar_service';
import PerformanceBarStore from './stores/performance_bar_store';
-export default ({ container }) =>
- new Vue({
- el: container,
+import initPerformanceBarLog from './performance_bar_log';
+
+const initPerformanceBar = el => {
+ const performanceBarData = el.dataset;
+
+ return new Vue({
+ el,
components: {
PerformanceBarApp: () => import('./components/performance_bar_app.vue'),
},
data() {
- const performanceBarData = document.querySelector(this.$options.el).dataset;
const store = new PerformanceBarStore();
return {
@@ -24,15 +27,12 @@ export default ({ container }) =>
};
},
mounted() {
- this.interceptor = PerformanceBarService.registerInterceptor(
- this.peekUrl,
- this.loadRequestDetails,
- );
+ PerformanceBarService.registerInterceptor(this.peekUrl, this.loadRequestDetails);
this.loadRequestDetails(this.requestId, window.location.href);
},
beforeDestroy() {
- PerformanceBarService.removeInterceptor(this.interceptor);
+ PerformanceBarService.removeInterceptor();
},
methods: {
addRequestManually(urlOrRequestId) {
@@ -121,3 +121,15 @@ export default ({ container }) =>
});
},
});
+};
+
+document.addEventListener('DOMContentLoaded', () => {
+ const jsPeek = document.querySelector('#js-peek');
+ if (jsPeek) {
+ initPerformanceBar(jsPeek);
+ }
+});
+
+initPerformanceBarLog();
+
+export default initPerformanceBar;
diff --git a/app/assets/javascripts/performance_bar/performance_bar_log.js b/app/assets/javascripts/performance_bar/performance_bar_log.js
new file mode 100644
index 00000000000..638c544f2e1
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/performance_bar_log.js
@@ -0,0 +1,28 @@
+/* eslint-disable no-console */
+import { getCLS, getFID, getLCP } from 'web-vitals';
+
+const initVitalsLog = () => {
+ const reportVital = data => {
+ console.log(`${String.fromCodePoint(0x1f4c8)} ${data.name} : `, data);
+ };
+
+ console.log(
+ `${String.fromCodePoint(
+ 0x1f4d1,
+ )} To get the final web vital numbers reported you maybe need to switch away and back to the tab`,
+ );
+ getCLS(reportVital);
+ getFID(reportVital);
+ getLCP(reportVital);
+};
+
+const initPerformanceBarLog = () => {
+ console.log(
+ `%c ${String.fromCodePoint(0x1f98a)} GitLab performance bar`,
+ 'width:100%;background-color: #292961; color: #FFFFFF; font-size:24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; padding: 10px;display:block;padding-right: 100px;',
+ );
+
+ initVitalsLog();
+};
+
+export default initPerformanceBarLog;
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 61b35b4b8f5..3c8303d102e 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -2,12 +2,14 @@ import axios from '../../lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
export default class PerformanceBarService {
+ static interceptor = null;
+
static fetchRequestDetails(peekUrl, requestId) {
return axios.get(peekUrl, { params: { request_id: requestId } });
}
static registerInterceptor(peekUrl, callback) {
- const interceptor = response => {
+ PerformanceBarService.interceptor = response => {
const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams(
response,
peekUrl,
@@ -20,22 +22,20 @@ export default class PerformanceBarService {
return response;
};
- return axios.interceptors.response.use(interceptor);
+ return axios.interceptors.response.use(PerformanceBarService.interceptor);
}
- static removeInterceptor(interceptor) {
- axios.interceptors.response.eject(interceptor);
+ static removeInterceptor() {
+ axios.interceptors.response.eject(PerformanceBarService.interceptor);
+ PerformanceBarService.interceptor = null;
}
static callbackParams(response, peekUrl) {
const requestId = response.headers && response.headers['x-request-id'];
- // Get the request URL from response.config for Axios, and response for
- // Vue Resource.
- const requestUrl = (response.config || response).url;
- const apiRequest = requestUrl && requestUrl.match(/^\/api\//);
+ const requestUrl = response.config?.url;
const cachedResponse =
response.headers && parseBoolean(response.headers['x-gitlab-from-cache']);
- const fireCallback = requestUrl !== peekUrl && requestId && !apiRequest && !cachedResponse;
+ const fireCallback = requestUrl !== peekUrl && Boolean(requestId) && !cachedResponse;
return [fireCallback, requestId, requestUrl];
}
diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
index 6f443db47ed..8c88851f039 100644
--- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -47,7 +47,10 @@ export default class PerformanceBarStore {
}
canTrackRequest(requestUrl) {
- return this.requests.filter(request => request.url === requestUrl).length < 2;
+ return (
+ requestUrl.endsWith('/api/graphql') ||
+ this.requests.filter(request => request.url === requestUrl).length < 2
+ );
}
static truncateUrl(requestUrl) {
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index e079603a5d4..be8ce832d20 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -9,13 +9,13 @@ import {
GlFormInput,
GlFormSelect,
GlLink,
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
-import { s__, __ } from '~/locale';
-import Api from '~/api';
+import { s__, __, n__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
@@ -29,6 +29,8 @@ export default {
),
formElementClasses: 'gl-mr-3 gl-mb-3 table-section section-15',
errorTitle: __('The form contains the following error:'),
+ warningTitle: __('The form contains the following warning:'),
+ maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
components: {
GlAlert,
GlButton,
@@ -37,8 +39,8 @@ export default {
GlFormInput,
GlFormSelect,
GlLink,
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
},
@@ -74,13 +76,20 @@ export default {
required: false,
default: () => ({}),
},
+ maxWarnings: {
+ type: Number,
+ required: true,
+ },
},
data() {
return {
searchTerm: '',
refValue: this.refParam,
variables: {},
- error: false,
+ error: null,
+ warnings: [],
+ totalWarnings: 0,
+ isWarningDismissed: false,
};
},
computed: {
@@ -91,6 +100,18 @@ export default {
variablesLength() {
return Object.keys(this.variables).length;
},
+ overMaxWarningsLimit() {
+ return this.totalWarnings > this.maxWarnings;
+ },
+ warningsSummary() {
+ return n__('%d warning found:', '%d warnings found:', this.warnings.length);
+ },
+ summaryMessage() {
+ return this.overMaxWarningsLimit ? this.$options.maxWarningsSummary : this.warningsSummary;
+ },
+ shouldShowWarning() {
+ return this.warnings.length > 0 && !this.isWarningDismissed;
+ },
},
created() {
if (this.variableParams) {
@@ -145,13 +166,20 @@ export default {
({ key, value }) => key !== '' && value !== '',
);
- return Api.createPipeline(this.projectId, {
- ref: this.refValue,
- variables: filteredVariables,
- })
- .then(({ data }) => redirectTo(data.web_url))
+ return axios
+ .post(this.pipelinesPath, {
+ ref: this.refValue,
+ variables: filteredVariables,
+ })
+ .then(({ data }) => {
+ redirectTo(`${this.pipelinesPath}/${data.id}`);
+ })
.catch(err => {
- this.error = err.response.data.message.base;
+ const { errors, warnings, total_warnings: totalWarnings } = err.response.data;
+ const [error] = errors;
+ this.error = error;
+ this.warnings = warnings;
+ this.totalWarnings = totalWarnings;
});
},
},
@@ -166,16 +194,45 @@ export default {
:dismissible="false"
variant="danger"
class="gl-mb-4"
+ data-testid="run-pipeline-error-alert"
>{{ error }}</gl-alert
>
+ <gl-alert
+ v-if="shouldShowWarning"
+ :title="$options.warningTitle"
+ variant="warning"
+ class="gl-mb-4"
+ data-testid="run-pipeline-warning-alert"
+ @dismiss="isWarningDismissed = true"
+ >
+ <details>
+ <summary>
+ <gl-sprintf :message="summaryMessage">
+ <template #total>
+ {{ totalWarnings }}
+ </template>
+ <template #warningsDisplayed>
+ {{ maxWarnings }}
+ </template>
+ </gl-sprintf>
+ </summary>
+ <p
+ v-for="(warning, index) in warnings"
+ :key="`warning-${index}`"
+ data-testid="run-pipeline-warning"
+ >
+ {{ warning }}
+ </p>
+ </details>
+ </gl-alert>
<gl-form-group :label="s__('Pipeline|Run for')">
- <gl-new-dropdown :text="refValue" block>
+ <gl-dropdown :text="refValue" block>
<gl-search-box-by-type
v-model.trim="searchTerm"
:placeholder="__('Search branches and tags')"
class="gl-p-2"
/>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="(ref, index) in filteredRefs"
:key="index"
class="gl-font-monospace"
@@ -184,8 +241,8 @@ export default {
@click="setRefSelected(ref)"
>
{{ ref }}
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
<template #description>
<div>
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
index 1c4812c2e0e..f1ea86f8c5f 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -11,6 +11,7 @@ export default () => {
fileParam,
refNames,
settingsLink,
+ maxWarnings,
} = el?.dataset;
const variableParams = JSON.parse(varParam);
@@ -29,6 +30,7 @@ export default () => {
fileParams,
refs,
settingsLink,
+ maxWarnings: Number(maxWarnings),
},
});
},
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 137455bcae1..efa11580c41 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,10 +1,9 @@
<script>
-import { GlTooltipDirective, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Icon from '~/vue_shared/components/icon.vue';
/**
* Renders either a cancel, retry or play icon button and handles the post request
@@ -18,7 +17,7 @@ import Icon from '~/vue_shared/components/icon.vue';
*/
export default {
components: {
- Icon,
+ GlIcon,
GlButton,
GlLoadingIcon,
},
@@ -92,6 +91,6 @@ export default {
@click="onClickAction"
>
<gl-loading-icon v-if="isLoading" class="js-action-icon-loading" />
- <icon v-else :name="actionIcon" />
+ <gl-icon v-else :name="actionIcon" class="gl-mr-0!" />
</gl-button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index f5bf6a6ed34..924cdeebba1 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -44,6 +44,10 @@ export default {
return {
downstreamMarginTop: null,
jobName: null,
+ pipelineExpanded: {
+ jobName: '',
+ expanded: false,
+ },
};
},
computed: {
@@ -120,6 +124,19 @@ export default {
setJob(jobName) {
this.jobName = jobName;
},
+ setPipelineExpanded(jobName, expanded) {
+ if (expanded) {
+ this.pipelineExpanded = {
+ jobName,
+ expanded,
+ };
+ } else {
+ this.pipelineExpanded = {
+ expanded,
+ jobName: '',
+ };
+ }
+ },
},
};
</script>
@@ -181,6 +198,7 @@ export default {
:has-triggered-by="hasTriggeredBy"
:action="stage.status.action"
:job-hovered="jobName"
+ :pipeline-expanded="pipelineExpanded"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
@@ -193,6 +211,7 @@ export default {
graph-position="right"
@linkedPipelineClick="handleClickedDownstream"
@downstreamHovered="setJob"
+ @pipelineExpandToggle="setPipelineExpanded"
/>
<pipeline-graph
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 15c220a554d..11fb2b18e9d 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -69,9 +69,7 @@ export default {
>
<ci-icon :status="group.status" />
- <span
- class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom"
- >
+ <span class="ci-status-text text-truncate mw-70p gl-pl-2 d-inline-block align-bottom">
{{ group.name }}
</span>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 4d72cc55b34..0fe0b671273 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -31,7 +31,7 @@ import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
*/
export default {
- hoverClass: 'gl-inset-border-1-blue-500',
+ hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
JobNameComponent,
@@ -61,6 +61,11 @@ export default {
required: false,
default: '',
},
+ pipelineExpanded: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
boundary() {
@@ -101,8 +106,14 @@ export default {
hasAction() {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
+ relatedDownstreamHovered() {
+ return this.job.name === this.jobHovered;
+ },
+ relatedDownstreamExpanded() {
+ return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded;
+ },
jobClasses() {
- return this.job.name === this.jobHovered
+ return this.relatedDownstreamHovered || this.relatedDownstreamExpanded
? `${this.$options.hoverClass} ${this.cssClassJobName}`
: this.cssClassJobName;
},
@@ -121,8 +132,9 @@ export default {
v-gl-tooltip="{ boundary, placement: 'bottom' }"
:href="status.details_path"
:title="tooltipText"
- :class="cssClassJobName"
+ :class="jobClasses"
class="js-pipeline-graph-job-link qa-job-link menu-item"
+ data-testid="job-with-link"
>
<job-name-component :name="job.name" :status="job.status" />
</gl-link>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index 74a261f35d7..30ba243077e 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -27,9 +27,7 @@ export default {
<template>
<span class="ci-job-name-component mw-100">
<ci-icon :status="status" />
- <span
- class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom"
- >
+ <span class="ci-status-text text-truncate mw-70p gl-pl-2 d-inline-block align-bottom">
{{ name }}
</span>
</span>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index f0a8f9f7ab7..e359fc787c5 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __, sprintf } from '~/locale';
@@ -10,6 +10,8 @@ export default {
components: {
CiStatus,
GlButton,
+ GlLink,
+ GlLoadingIcon,
},
props: {
pipeline: {
@@ -25,6 +27,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ expanded: false,
+ };
+ },
computed: {
tooltipText() {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label}
@@ -66,11 +73,22 @@ export default {
? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name })
: '';
},
+ expandedIcon() {
+ if (this.parentPipeline) {
+ return this.expanded ? 'angle-right' : 'angle-left';
+ }
+ return this.expanded ? 'angle-left' : 'angle-right';
+ },
+ expandButtonPosition() {
+ return this.parentPipeline ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!';
+ },
},
methods: {
onClickLinkedPipeline() {
this.$root.$emit('bv::hide::tooltip', this.buttonId);
+ this.expanded = !this.expanded;
this.$emit('pipelineClicked', this.$refs.linkedPipeline);
+ this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded);
},
hideTooltips() {
this.$root.$emit('bv::hide::tooltip');
@@ -88,27 +106,48 @@ export default {
<template>
<li
ref="linkedPipeline"
+ v-gl-tooltip
class="linked-pipeline build"
+ :title="tooltipText"
:class="{ 'downstream-pipeline': isDownstream }"
data-qa-selector="child_pipeline"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
- <gl-button
- :id="buttonId"
- v-gl-tooltip
- :title="tooltipText"
- class="linked-pipeline-content"
- data-qa-selector="linked_pipeline_button"
- :class="`js-pipeline-expand-${pipeline.id}`"
- :loading="pipeline.isLoading"
- @click="onClickLinkedPipeline"
+ <div
+ class="gl-relative gl-bg-white gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1"
+ :class="{ 'gl-pl-9': parentPipeline }"
>
- <ci-status v-if="!pipeline.isLoading" :status="pipelineStatus" css-classes="gl-top-0" />
- <span class="str-truncated"> {{ downstreamTitle }} &#8226; #{{ pipeline.id }} </span>
+ <div class="gl-display-flex">
+ <ci-status
+ v-if="!pipeline.isLoading"
+ :status="pipelineStatus"
+ css-classes="gl-top-0 gl-pr-2"
+ />
+ <div v-else class="gl-pr-2"><gl-loading-icon inline /></div>
+ <div class="gl-display-flex gl-flex-direction-column gl-w-13">
+ <span class="gl-text-truncate">
+ {{ downstreamTitle }}
+ </span>
+ <div class="gl-text-truncate">
+ <gl-link class="gl-text-blue-500!" :href="pipeline.path" data-testid="pipelineLink"
+ >#{{ pipeline.id }}</gl-link
+ >
+ </div>
+ </div>
+ </div>
<div class="gl-pt-2">
<span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span>
</div>
- </gl-button>
+ <gl-button
+ :id="buttonId"
+ class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!"
+ :class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`"
+ :icon="expandedIcon"
+ data-testid="expandPipelineButton"
+ data-qa-selector="expand_pipeline_button"
+ @click="onClickLinkedPipeline"
+ />
+ </div>
</li>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index d82885ff8de..3ad28d88345 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -44,6 +44,9 @@ export default {
onDownstreamHovered(jobName) {
this.$emit('downstreamHovered', jobName);
},
+ onPipelineExpandToggle(jobName, expanded) {
+ this.$emit('pipelineExpandToggle', jobName, expanded);
+ },
},
};
</script>
@@ -65,6 +68,7 @@ export default {
:project-id="projectId"
@pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered"
+ @pipelineExpandToggle="onPipelineExpandToggle"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 9de6ba819c2..1453c349f44 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -41,6 +41,11 @@ export default {
required: false,
default: '',
},
+ pipelineExpanded: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
hasAction() {
@@ -86,6 +91,7 @@ export default {
v-if="group.size === 1"
:job="group.jobs[0]"
:job-hovered="jobHovered"
+ :pipeline-expanded="pipelineExpanded"
css-class-job-name="build-content"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue
new file mode 100644
index 00000000000..3cc76425e2a
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import jsYaml from 'js-yaml';
+import PipelineGraph from './pipeline_graph.vue';
+import { preparePipelineGraphData } from '../../utils';
+
+export default {
+ FILE_CONTENT_SELECTOR: '#blob-content',
+ EMPTY_FILE_SELECTOR: '.nothing-here-block',
+
+ components: {
+ GlTab,
+ GlTabs,
+ PipelineGraph,
+ },
+ props: {
+ blobData: {
+ required: true,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ selectedTabIndex: 0,
+ pipelineData: {},
+ };
+ },
+ computed: {
+ isVisualizationTab() {
+ return this.selectedTabIndex === 1;
+ },
+ },
+ async created() {
+ if (this.blobData) {
+ // The blobData in this case represents the gitlab-ci.yml data
+ const json = await jsYaml.load(this.blobData);
+ this.pipelineData = preparePipelineGraphData(json);
+ }
+ },
+ methods: {
+ // This is used because the blob page still uses haml, and we can't make
+ // our haml hide the unused section so we resort to a standard query here.
+ toggleFileContent({ isFileTab }) {
+ const el = document.querySelector(this.$options.FILE_CONTENT_SELECTOR);
+ const emptySection = document.querySelector(this.$options.EMPTY_FILE_SELECTOR);
+
+ const elementToHide = el || emptySection;
+
+ if (!elementToHide) {
+ return;
+ }
+
+ // Checking for the current style display prevents user
+ // from toggling visiblity on and off when clicking on the tab
+ if (!isFileTab && elementToHide.style.display !== 'none') {
+ elementToHide.style.display = 'none';
+ }
+
+ if (isFileTab && elementToHide.style.display === 'none') {
+ elementToHide.style.display = 'block';
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div>
+ <gl-tabs v-model="selectedTabIndex">
+ <gl-tab :title="__('File')" @click="toggleFileContent({ isFileTab: true })" />
+ <gl-tab :title="__('Visualization')" @click="toggleFileContent({ isFileTab: false })" />
+ </gl-tabs>
+ </div>
+ <pipeline-graph v-if="isVisualizationTab" :pipeline-data="pipelineData" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
new file mode 100644
index 00000000000..19d41b166c3
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
@@ -0,0 +1,24 @@
+<script>
+import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+export default {
+ components: {
+ tooltipOnTruncate,
+ },
+ props: {
+ jobName: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
+ <div
+ class="gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-inset-border-1-green-600 gl-mb-3 gl-px-5 gl-py-2 pipeline-job-pill "
+ >
+ {{ jobName }}
+ </div>
+ </tooltip-on-truncate>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
new file mode 100644
index 00000000000..6a0d3cce1f3
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -0,0 +1,57 @@
+<script>
+import { isEmpty } from 'lodash';
+import { GlAlert } from '@gitlab/ui';
+import JobPill from './job_pill.vue';
+import StagePill from './stage_pill.vue';
+
+export default {
+ components: {
+ GlAlert,
+ JobPill,
+ StagePill,
+ },
+ props: {
+ pipelineData: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ isPipelineDataEmpty() {
+ return isEmpty(this.pipelineData);
+ },
+ emptyClass() {
+ return !this.isPipelineDataEmpty ? 'gl-py-7' : '';
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto" :class="emptyClass">
+ <gl-alert v-if="isPipelineDataEmpty" variant="tip" :dismissible="false">
+ {{ __('No content to show') }}
+ </gl-alert>
+ <template v-else>
+ <div
+ v-for="(stage, index) in pipelineData.stages"
+ :key="`${stage.name}-${index}`"
+ class="gl-flex-direction-column"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
+ :class="{
+ 'stage-left-rounded': index === 0,
+ 'stage-right-rounded': index === pipelineData.stages.length - 1,
+ }"
+ >
+ <stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
+ </div>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
+ >
+ <job-pill v-for="group in stage.groups" :key="group.name" :job-name="group.name" />
+ </div>
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue
new file mode 100644
index 00000000000..7b2458db725
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue
@@ -0,0 +1,35 @@
+<script>
+import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+export default {
+ components: {
+ tooltipOnTruncate,
+ },
+ props: {
+ stageName: {
+ type: String,
+ required: true,
+ },
+ isEmpty: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ emptyClass() {
+ return this.isEmpty ? 'gl-bg-gray-200' : 'gl-bg-gray-600';
+ },
+ },
+};
+</script>
+<template>
+ <tooltip-on-truncate :title="stageName" truncate-target="child" placement="top">
+ <div
+ class="gl-px-5 gl-py-2 gl-text-white gl-text-center gl-text-truncate gl-rounded-pill pipeline-stage-pill"
+ :class="emptyClass"
+ >
+ {{ stageName }}
+ </div>
+ </tooltip-on-truncate>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
index a66bbb7e5ba..d7b6e033bd1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
@@ -1,12 +1,10 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { GlButton } from '@gitlab/ui';
export default {
name: 'PipelineNavControls',
components: {
- LoadingButton,
- GlDeprecatedButton,
+ GlButton,
},
props: {
newPipelinePath: {
@@ -42,25 +40,27 @@ export default {
</script>
<template>
<div class="nav-controls">
- <gl-deprecated-button
+ <gl-button
v-if="newPipelinePath"
:href="newPipelinePath"
variant="success"
+ category="primary"
class="js-run-pipeline"
>
{{ s__('Pipelines|Run Pipeline') }}
- </gl-deprecated-button>
+ </gl-button>
- <loading-button
+ <gl-button
v-if="resetCachePath"
:loading="isResetCacheButtonLoading"
- :label="s__('Pipelines|Clear Runner Caches')"
class="js-clear-cache"
@click="onClickResetCache"
- />
+ >
+ {{ s__('Pipelines|Clear Runner Caches') }}
+ </gl-button>
- <gl-deprecated-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint">
+ <gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint">
{{ s__('Pipelines|CI Lint') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
index f604edd8859..43a54090e18 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { isEmpty } from 'lodash';
import { GlLink } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index 2dfc6485d85..adba86d384b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -1,5 +1,6 @@
<script>
import { isEqual } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import PipelinesService from '../../services/pipelines_service';
@@ -9,7 +10,6 @@ import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import NavigationControls from './nav_controls.vue';
import { getParameterByName } from '~/lib/utils/common_utils';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
-import Icon from '~/vue_shared/components/icon.vue';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import { validateParams } from '../../utils';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
@@ -21,7 +21,7 @@ export default {
NavigationTabs,
NavigationControls,
PipelinesFilteredSearch,
- Icon,
+ GlIcon,
},
mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()],
props: {
@@ -285,8 +285,8 @@ export default {
v-if="shouldRenderTabs || shouldRenderButtons"
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
>
- <div class="fade-left"><icon name="chevron-lg-left" :size="12" /></div>
- <div class="fade-right"><icon name="chevron-lg-right" :size="12" /></div>
+ <div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div>
+ <div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div>
<navigation-tabs
v-if="shouldRenderTabs"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
index 098efe68b83..97595e5d2ce 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
@@ -1,10 +1,9 @@
<script>
-import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
import { s__, __, sprintf } from '~/locale';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../../event_hub';
export default {
@@ -12,9 +11,9 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- Icon,
+ GlIcon,
GlCountdown,
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
},
props: {
@@ -83,29 +82,32 @@ export default {
type="button"
:disabled="isLoading"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
- :title="__('Manual job')"
+ :title="__('Run manual or delayed jobs')"
data-toggle="dropdown"
- :aria-label="__('Manual job')"
+ :aria-label="__('Run manual or delayed jobs')"
>
- <icon name="play" class="icon-play" />
+ <gl-icon name="play" class="icon-play" />
<i class="fa fa-caret-down" aria-hidden="true"></i>
<gl-loading-icon v-if="isLoading" />
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li v-for="action in actions" :key="action.path">
- <gl-deprecated-button
+ <gl-button
+ category="tertiary"
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
- class="js-pipeline-action-link no-btn btn d-flex align-items-center justify-content-between flex-wrap"
+ class="js-pipeline-action-link"
@click="onClickAction(action)"
>
- {{ action.name }}
- <span v-if="action.scheduled_at">
- <icon name="clock" />
- <gl-countdown :end-date-string="action.scheduled_at" />
- </span>
- </gl-deprecated-button>
+ <div class="d-flex justify-content-between flex-wrap">
+ {{ action.name }}
+ <span v-if="action.scheduled_at">
+ <gl-icon name="clock" />
+ <gl-countdown :end-date-string="action.scheduled_at" />
+ </span>
+ </div>
+ </gl-button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
index 59c066b2683..4a3d134685e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
@@ -1,14 +1,13 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
- Icon,
+ GlIcon,
GlLink,
},
props: {
@@ -29,7 +28,7 @@ export default {
data-toggle="dropdown"
:aria-label="__('Artifacts')"
>
- <icon name="download" />
+ <gl-icon name="download" />
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right">
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
index f25994a7506..1bdb7d18f04 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import eventHub from '../../event_hub';
import PipelinesActionsComponent from './pipelines_actions.vue';
import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
@@ -8,8 +9,6 @@ import PipelineUrl from './pipeline_url.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelinesTimeago from './time_ago.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import { PIPELINES_TABLE } from '../../constants';
/**
@@ -27,8 +26,7 @@ export default {
PipelineTriggerer,
CiBadge,
PipelinesTimeago,
- LoadingButton,
- Icon,
+ GlButton,
},
props: {
pipeline: {
@@ -274,6 +272,7 @@ export default {
<ci-badge
:status="pipelineStatus"
:show-text="!isChildView"
+ :icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status"
/>
</div>
@@ -337,28 +336,30 @@ export default {
class="d-md-block"
/>
- <loading-button
+ <gl-button
v-if="pipeline.flags.retryable"
:loading="isRetrying"
:disabled="isRetrying"
- container-class="js-pipelines-retry-button btn btn-default btn-retry"
+ class="js-pipelines-retry-button btn-retry"
data-qa-selector="pipeline_retry_button"
+ icon="repeat"
+ variant="default"
+ category="secondary"
@click="handleRetryClick"
- >
- <icon name="repeat" />
- </loading-button>
+ />
- <loading-button
+ <gl-button
v-if="pipeline.flags.cancelable"
:loading="isCancelling"
:disabled="isCancelling"
data-toggle="modal"
data-target="#confirmation-modal"
- container-class="js-pipelines-cancel-button btn btn-remove"
+ icon="close"
+ variant="danger"
+ category="primary"
+ class="js-pipelines-cancel-button"
@click="handleCancelClick"
- >
- <icon name="close" />
- </loading-button>
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
index d992a4b7752..4045f450104 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
@@ -13,18 +13,17 @@
*/
import $ from 'jquery';
-import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import eventHub from '../../event_hub';
-import Icon from '~/vue_shared/components/icon.vue';
import JobItem from '../graph/job_item.vue';
import { PIPELINES_TABLE } from '../../constants';
export default {
components: {
- Icon,
+ GlIcon,
JobItem,
GlLoadingIcon,
},
@@ -170,7 +169,7 @@ export default {
@click="onClickStage"
>
<span :aria-label="stage.title" aria-hidden="true" class="no-pointer-events">
- <icon :name="borderlessIcon" />
+ <gl-icon :name="borderlessIcon" />
</span>
</button>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
index 8a01e1fe3f5..7d13ee582c6 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
@@ -1,5 +1,5 @@
<script>
-import iconTimerSvg from 'icons/_icon_timer.svg';
+import { GlIcon } from '@gitlab/ui';
import '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -8,6 +8,7 @@ export default {
directives: {
tooltip,
},
+ components: { GlIcon },
mixins: [timeagoMixin],
props: {
finishedTime: {
@@ -19,11 +20,6 @@ export default {
required: true,
},
},
- data() {
- return {
- iconTimerSvg,
- };
- },
computed: {
hasDuration() {
return this.duration > 0;
@@ -59,11 +55,12 @@ export default {
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Duration') }}</div>
<div class="table-mobile-content">
<p v-if="hasDuration" class="duration">
- <span v-html="iconTimerSvg"> </span> {{ durationFormatted }}
+ <gl-icon name="timer" class="gl-vertical-align-baseline!" aria-hidden="true" />
+ {{ durationFormatted }}
</p>
<p v-if="hasFinishedTime" class="finished-at d-none d-sm-none d-md-block">
- <i class="fa fa-calendar" aria-hidden="true"> </i>
+ <gl-icon name="calendar" class="gl-vertical-align-baseline!" aria-hidden="true" />
<time
v-tooltip
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
index bc1d22e2976..c3398e90895 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -55,13 +55,14 @@ export default {
<template>
<div v-if="isLoading">
- <gl-loading-icon size="lg" class="gl-mt-3 js-loading-spinner" />
+ <gl-loading-icon size="lg" class="gl-mt-3" />
</div>
<div
v-else-if="!isLoading && showTests"
ref="container"
- class="tests-detail position-relative js-tests-detail"
+ class="tests-detail position-relative"
+ data-testid="tests-detail"
>
<transition
name="slide"
@@ -85,7 +86,7 @@ export default {
<div v-else>
<div class="row gl-mt-3">
<div class="col-12">
- <p class="js-no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p>
+ <p data-testid="no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 478073e44d1..aa53c5040e8 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -1,15 +1,12 @@
<script>
import { mapGetters } from 'vuex';
-import { GlTooltipDirective, GlFriendlyWrap } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlFriendlyWrap, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
name: 'TestsSuiteTable',
components: {
- Icon,
- SmartVirtualList,
+ GlIcon,
GlFriendlyWrap,
},
directives: {
@@ -28,8 +25,6 @@ export default {
return this.getSuiteTests.length > 0;
},
},
- maxShownRows: 30,
- typicalRowHeight: 75,
wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'],
};
</script>
@@ -61,66 +56,60 @@ export default {
</div>
</div>
- <smart-virtual-list
- :length="getSuiteTests.length"
- :remain="$options.maxShownRows"
- :size="$options.typicalRowHeight"
+ <div
+ v-for="(testCase, index) in getSuiteTests"
+ :key="index"
+ class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row"
>
- <div
- v-for="(testCase, index) in getSuiteTests"
- :key="index"
- class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row"
- >
- <div class="table-section section-20 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
- <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
- <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" />
- </div>
+ <div class="table-section section-20 section-wrap">
+ <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
+ <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
+ <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" />
</div>
+ </div>
- <div class="table-section section-20 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
- <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
- <gl-friendly-wrap
- data-testid="caseName"
- :symbols="$options.wrapSymbols"
- :text="testCase.name"
- />
- </div>
+ <div class="table-section section-20 section-wrap">
+ <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
+ <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
+ <gl-friendly-wrap
+ data-testid="caseName"
+ :symbols="$options.wrapSymbols"
+ :text="testCase.name"
+ />
</div>
+ </div>
- <div class="table-section section-10 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
- <div class="table-mobile-content text-center">
- <div
- class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
- :class="`ci-status-icon-${testCase.status}`"
- >
- <icon :size="24" :name="testCase.icon" />
- </div>
+ <div class="table-section section-10 section-wrap">
+ <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
+ <div class="table-mobile-content text-center">
+ <div
+ class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
+ :class="`ci-status-icon-${testCase.status}`"
+ >
+ <gl-icon :size="24" :name="testCase.icon" />
</div>
</div>
+ </div>
- <div class="table-section flex-grow-1">
- <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div>
- <div class="table-mobile-content">
- <pre
- v-if="testCase.system_output"
- class="build-trace build-trace-rounded text-left"
- ><code class="bash p-0">{{testCase.system_output}}</code></pre>
- </div>
+ <div class="table-section flex-grow-1">
+ <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div>
+ <div class="table-mobile-content">
+ <pre
+ v-if="testCase.system_output"
+ class="build-trace build-trace-rounded text-left"
+ ><code class="bash p-0">{{testCase.system_output}}</code></pre>
</div>
+ </div>
- <div class="table-section section-10 section-wrap">
- <div role="rowheader" class="table-mobile-header">
- {{ __('Duration') }}
- </div>
- <div class="table-mobile-content text-right pr-sm-1">
- {{ testCase.formattedTime }}
- </div>
+ <div class="table-section section-10 section-wrap">
+ <div role="rowheader" class="table-mobile-header">
+ {{ __('Duration') }}
+ </div>
+ <div class="table-mobile-content text-right pr-sm-1">
+ {{ testCase.formattedTime }}
</div>
</div>
- </smart-virtual-list>
+ </div>
</div>
<div v-else>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
index 712ac5eb0e5..d33d4e7dfd0 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
@@ -1,15 +1,13 @@
<script>
-import { GlDeprecatedButton, GlProgressBar } from '@gitlab/ui';
+import { GlButton, GlProgressBar } from '@gitlab/ui';
import { __ } from '~/locale';
-import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
-import Icon from '~/vue_shared/components/icon.vue';
+import { formattedTime } from '../../stores/test_reports/utils';
export default {
name: 'TestSummary',
components: {
- GlDeprecatedButton,
+ GlButton,
GlProgressBar,
- Icon,
},
props: {
report: {
@@ -39,7 +37,7 @@ export default {
return 0;
},
formattedDuration() {
- return formatTime(secondsToMilliseconds(this.report.total_time));
+ return formattedTime(this.report.total_time);
},
progressBarVariant() {
if (this.successPercentage < 33) {
@@ -69,14 +67,13 @@ export default {
<div>
<div class="row">
<div class="col-12 d-flex gl-mt-3 align-items-center">
- <gl-deprecated-button
+ <gl-button
v-if="showBack"
- size="sm"
+ size="small"
class="gl-mr-3 js-back-button"
+ icon="angle-left"
@click="onBackClick"
- >
- <icon name="angle-left" />
- </gl-deprecated-button>
+ />
<h4>{{ heading }}</h4>
</div>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
index e774fe06fbe..5f9c0be3ccc 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
@@ -2,13 +2,11 @@
import { mapGetters } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
name: 'TestsSummaryTable',
components: {
GlIcon,
- SmartVirtualList,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -31,8 +29,6 @@ export default {
this.$emit('row-click', index);
},
},
- maxShownRows: 20,
- typicalRowHeight: 55,
};
</script>
@@ -69,83 +65,77 @@ export default {
</div>
</div>
- <smart-virtual-list
- :length="getTestSuites.length"
- :remain="$options.maxShownRows"
- :size="$options.typicalRowHeight"
+ <div
+ v-for="(testSuite, index) in getTestSuites"
+ :key="index"
+ role="row"
+ class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row"
+ :class="{
+ 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error,
+ }"
+ @click="tableRowClick(index)"
>
- <div
- v-for="(testSuite, index) in getTestSuites"
- :key="index"
- role="row"
- class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row"
- :class="{
- 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error,
- }"
- @click="tableRowClick(index)"
- >
- <div class="table-section section-25">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Suite') }}
- </div>
- <div class="table-mobile-content underline cgray pl-3">
- {{ testSuite.name }}
- <gl-icon
- v-if="testSuite.suite_error"
- ref="suiteErrorIcon"
- v-gl-tooltip
- name="error"
- :title="testSuite.suite_error"
- class="vertical-align-middle"
- />
- </div>
+ <div class="table-section section-25">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Suite') }}
</div>
+ <div class="table-mobile-content underline cgray pl-3">
+ {{ testSuite.name }}
+ <gl-icon
+ v-if="testSuite.suite_error"
+ ref="suiteErrorIcon"
+ v-gl-tooltip
+ name="error"
+ :title="testSuite.suite_error"
+ class="vertical-align-middle"
+ />
+ </div>
+ </div>
- <div class="table-section section-25">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Duration') }}
- </div>
- <div class="table-mobile-content text-md-left">
- {{ testSuite.formattedTime }}
- </div>
+ <div class="table-section section-25">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Duration') }}
+ </div>
+ <div class="table-mobile-content text-md-left">
+ {{ testSuite.formattedTime }}
</div>
+ </div>
- <div class="table-section section-10 text-center">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Failed') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.failed_count }}</div>
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Failed') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.failed_count }}</div>
+ </div>
- <div class="table-section section-10 text-center">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Errors') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.error_count }}</div>
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Errors') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.error_count }}</div>
+ </div>
- <div class="table-section section-10 text-center">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Skipped') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.skipped_count }}</div>
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Skipped') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.skipped_count }}</div>
+ </div>
- <div class="table-section section-10 text-center">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Passed') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.success_count }}</div>
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Passed') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.success_count }}</div>
+ </div>
- <div class="table-section section-10 text-right pr-md-3">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Total') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.total_count }}</div>
+ <div class="table-section section-10 text-right pr-md-3">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Total') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.total_count }}</div>
</div>
- </smart-virtual-list>
+ </div>
</div>
<div v-else>
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index c57be7c75b0..745f5b886a5 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -14,10 +14,20 @@ import createTestReportsStore from './stores/test_reports';
Vue.use(Translate);
+const SELECTORS = {
+ PIPELINE_DETAILS: '.js-pipeline-details-vue',
+ PIPELINE_GRAPH: '#js-pipeline-graph-vue',
+ PIPELINE_HEADER: '#js-pipeline-header-vue',
+ PIPELINE_TESTS: '#js-pipeline-tests-detail',
+};
+
const createPipelinesDetailApp = mediator => {
+ if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) {
+ return;
+ }
// eslint-disable-next-line no-new
new Vue({
- el: '#js-pipeline-graph-vue',
+ el: SELECTORS.PIPELINE_GRAPH,
components: {
pipelineGraph,
},
@@ -47,9 +57,12 @@ const createPipelinesDetailApp = mediator => {
};
const createPipelineHeaderApp = mediator => {
+ if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) {
+ return;
+ }
// eslint-disable-next-line no-new
new Vue({
- el: '#js-pipeline-header-vue',
+ el: SELECTORS.PIPELINE_HEADER,
components: {
pipelineHeader,
},
@@ -93,9 +106,8 @@ const createPipelineHeaderApp = mediator => {
};
const createTestDetails = () => {
- const el = document.querySelector('#js-pipeline-tests-detail');
+ const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
-
const testReportsStore = createTestReportsStore({
summaryEndpoint,
suiteEndpoint,
@@ -115,7 +127,7 @@ const createTestDetails = () => {
};
export default () => {
- const { dataset } = document.querySelector('.js-pipeline-details-vue');
+ const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
mediator.fetchPipeline();
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
index 6c670806cc4..c123014756d 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -1,4 +1,4 @@
-import { addIconStatus, formattedTime, sortTestCases } from './utils';
+import { addIconStatus, formattedTime } from './utils';
export const getTestSuites = state => {
const { test_suites: testSuites = [] } = state.testReports;
@@ -14,5 +14,5 @@ export const getSelectedSuite = state =>
export const getSuiteTests = state => {
const { test_cases: testCases = [] } = getSelectedSuite(state);
- return testCases.sort(sortTestCases).map(addIconStatus);
+ return testCases.map(addIconStatus);
};
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
index 16fa6935cbe..8f1ac305cda 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
@@ -1,5 +1,4 @@
-import { TestStatus } from '~/pipelines/constants';
-import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
+import { __, sprintf } from '../../../locale';
export function iconForTestStatus(status) {
switch (status) {
@@ -12,25 +11,16 @@ export function iconForTestStatus(status) {
}
}
-export const formattedTime = timeInSeconds => formatTime(secondsToMilliseconds(timeInSeconds));
+export const formattedTime = (seconds = 0) => {
+ if (seconds < 1) {
+ const milliseconds = seconds * 1000;
+ return sprintf(__('%{milliseconds}ms'), { milliseconds: milliseconds.toFixed(2) });
+ }
+ return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) });
+};
export const addIconStatus = testCase => ({
...testCase,
icon: iconForTestStatus(testCase.status),
formattedTime: formattedTime(testCase.execution_time),
});
-
-export const sortTestCases = (a, b) => {
- if (a.status === b.status) {
- return 0;
- }
-
- switch (b.status) {
- case TestStatus.SUCCESS:
- return -1;
- case TestStatus.FAILED:
- return 1;
- default:
- return 0;
- }
-};
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 2e08001f6b3..bd53b22784c 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,7 +1,49 @@
import { pickBy } from 'lodash';
import { SUPPORTED_FILTER_PARAMETERS } from './constants';
-// eslint-disable-next-line import/prefer-default-export
export const validateParams = params => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
+
+/**
+ * This function takes a json payload that comes from a yml
+ * file converted to json through `jsyaml` library. Because we
+ * naively convert the entire yaml to json, some keys (like `includes`)
+ * are irrelevant to rendering the graph and must be removed. We also
+ * restructure the data to have the structure from an API response for the
+ * pipeline data.
+ * @param {Object} jsonData
+ * @returns {Array} - Array of stages containing all jobs
+ */
+export const preparePipelineGraphData = jsonData => {
+ const jsonKeys = Object.keys(jsonData);
+ const jobNames = jsonKeys.filter(job => jsonData[job]?.stage);
+
+ // We merge both the stages from the "stages" key in the yaml and the stage associated
+ // with each job to show the user both the stages they explicitly defined, and those
+ // that they added under jobs. We also remove duplicates.
+ const jobStages = jobNames.map(job => jsonData[job].stage);
+ const userDefinedStages = jsonData?.stages ?? [];
+
+ // The order is important here. We always show the stages in order they were
+ // defined in the `stages` key first, and then stages that are under the jobs.
+ const stages = Array.from(new Set([...userDefinedStages, ...jobStages]));
+
+ const arrayOfJobsByStage = stages.map(val => {
+ return jobNames.filter(job => {
+ return jsonData[job].stage === val;
+ });
+ });
+
+ const pipelineData = stages.map((stage, index) => {
+ const stageJobs = arrayOfJobsByStage[index];
+ return {
+ name: stage,
+ groups: stageJobs.map(job => {
+ return { name: job, jobs: [{ ...jsonData[job] }] };
+ }),
+ };
+ });
+
+ return { stages: pipelineData };
+};
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 605859cfb6a..f06dc72d365 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -1,11 +1,12 @@
<script>
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+/* eslint-disable vue/no-v-html */
+import { GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
export default {
components: {
- DeprecatedModal,
+ GlModal,
},
props: {
actionUrl: {
@@ -54,21 +55,38 @@ You are about to permanently delete %{yourAccount}, and all of the issues, merge
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
{
yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
- deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
+ deleteAccount: `<strong>${s__('Profiles|Delete account')}</strong>`,
},
false,
);
},
- },
- methods: {
+ primaryProps() {
+ return {
+ text: s__('Delete account'),
+ attributes: [
+ { variant: 'danger', 'data-qa-selector': 'confirm_delete_account_button' },
+ { category: 'primary' },
+ { disabled: !this.canSubmit },
+ ],
+ };
+ },
+ cancelProps() {
+ return {
+ text: s__('Cancel'),
+ };
+ },
canSubmit() {
if (this.confirmWithPassword) {
return this.enteredPassword !== '';
}
-
return this.enteredUsername === this.username;
},
+ },
+ methods: {
onSubmit() {
+ if (!this.canSubmit) {
+ return;
+ }
this.$refs.form.submit();
},
},
@@ -76,42 +94,39 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
</script>
<template>
- <deprecated-modal
- id="delete-account-modal"
- :title="s__('Profiles|Delete your account?')"
- :text="text"
- :primary-button-label="s__('Profiles|Delete account')"
- :submit-disabled="!canSubmit()"
- kind="danger"
- @submit="onSubmit"
+ <gl-modal
+ modal-id="delete-account-modal"
+ title="Profiles"
+ :action-primary="primaryProps"
+ :action-cancel="cancelProps"
+ :ok-disabled="!canSubmit"
+ @primary="onSubmit"
>
- <template #body="props">
- <p v-html="props.text"></p>
+ <p v-html="text"></p>
- <form ref="form" :action="actionUrl" method="post">
- <input type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <form ref="form" :action="actionUrl" method="post">
+ <input type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <p id="input-label" v-html="inputLabel"></p>
+ <p id="input-label" v-html="inputLabel"></p>
- <input
- v-if="confirmWithPassword"
- v-model="enteredPassword"
- name="password"
- class="form-control"
- type="password"
- data-qa-selector="password_confirmation_field"
- aria-labelledby="input-label"
- />
- <input
- v-else
- v-model="enteredUsername"
- name="username"
- class="form-control"
- type="text"
- aria-labelledby="input-label"
- />
- </form>
- </template>
- </deprecated-modal>
+ <input
+ v-if="confirmWithPassword"
+ v-model="enteredPassword"
+ name="password"
+ class="form-control"
+ type="password"
+ data-qa-selector="password_confirmation_field"
+ aria-labelledby="input-label"
+ />
+ <input
+ v-else
+ v-model="enteredUsername"
+ name="username"
+ class="form-control"
+ type="text"
+ aria-labelledby="input-label"
+ />
+ </form>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index 58025381cb2..4aaa2cff2ac 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index f0d9642a2b2..5e89002b3bc 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -30,6 +30,9 @@ export default () => {
},
mounted() {
deleteAccountButton.classList.remove('disabled');
+ deleteAccountButton.addEventListener('click', () => {
+ this.$root.$emit('bv::show::modal', 'delete-account-modal', '#delete-account-button');
+ });
},
render(createElement) {
return createElement('delete-account-modal', {
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 788553636f9..db2b0856e1b 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -110,7 +110,10 @@ const projectSelect = () => {
});
};
-export default () =>
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(projectSelect)
- .catch(() => {});
+export default () => {
+ if ($('.ajax-project-select').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(projectSelect)
+ .catch(() => {});
+ }
+};
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index a8589b50899..2204ec3cbe7 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -2,11 +2,11 @@
import { debounce } from 'lodash';
import { mapState, mapActions } from 'vuex';
import {
- GlNewDropdown,
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlSearchBoxByType,
- GlNewDropdownDivider,
+ GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -18,11 +18,11 @@ const tooltipMessage = __('Searching by both author and message is currently not
export default {
name: 'AuthorSelect',
components: {
- GlNewDropdown,
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlSearchBoxByType,
- GlNewDropdownDivider,
+ GlDropdownDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -107,27 +107,27 @@ export default {
<template>
<div ref="dropdownContainer" v-gl-tooltip :title="tooltipTitle" :disabled="!hasSearchParam">
- <gl-new-dropdown
+ <gl-dropdown
:text="dropdownText"
:disabled="hasSearchParam"
toggle-class="gl-py-3 gl-border-0"
class="w-100 mt-2 mt-sm-0"
>
- <gl-new-dropdown-header>
+ <gl-dropdown-section-header>
{{ __('Search by author') }}
- </gl-new-dropdown-header>
- <gl-new-dropdown-divider />
+ </gl-dropdown-section-header>
+ <gl-dropdown-divider />
<gl-search-box-by-type
v-model.trim="authorInput"
- class="m-2"
+ class="gl-m-3"
:placeholder="__('Search')"
@input="searchAuthors"
/>
- <gl-new-dropdown-item :is-checked="!currentAuthor" @click="selectAuthor(null)">
+ <gl-dropdown-item :is-checked="!currentAuthor" @click="selectAuthor(null)">
{{ __('Any Author') }}
- </gl-new-dropdown-item>
- <gl-new-dropdown-divider />
- <gl-new-dropdown-item
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-item
v-for="author in commitsAuthors"
:key="author.id"
:is-checked="author.name === currentAuthor"
@@ -136,7 +136,7 @@ export default {
@click="selectAuthor(author)"
>
{{ author.name }}
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
index 4b27c5e3d30..2f3ff92d7ae 100644
--- a/app/assets/javascripts/projects/components/project_delete_button.vue
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -22,10 +22,10 @@ export default {
strings: {
alertTitle: __('You are about to permanently delete this project'),
alertBody: __(
- 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
+ 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
),
modalBody: __(
- "This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc.",
+ "This action cannot be undone. You will lose the project's repository and all content: issues, merge requests, etc.",
),
},
};
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
index e3f4500d404..051bfcb732a 100644
--- a/app/assets/javascripts/projects/components/shared/delete_button.vue
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -86,7 +86,7 @@ export default {
<slot name="modal-body"></slot>
<p class="gl-mb-1">{{ $options.strings.confirmText }}</p>
<p>
- <code>{{ confirmPhrase }}</code>
+ <code class="gl-white-space-pre-wrap">{{ confirmPhrase }}</code>
</p>
<gl-form-input
id="confirm_name_input"
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
index ee4a00dbc75..f404e6030f4 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
import WelcomePage from './welcome.vue';
import LegacyContainer from './legacy_container.vue';
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
index cd9a72996cf..022328cd8a2 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlPopover } from '@gitlab/ui';
import Tracking from '~/tracking';
import LegacyContainer from './legacy_container.vue';
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ec0a83b5736..599aa52831b 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,13 +1,18 @@
import $ from 'jquery';
import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
-import { convertToTitleCase, humanize, slugify } from '../lib/utils/text_utility';
+import {
+ convertToTitleCase,
+ humanize,
+ slugify,
+ convertUnicodeToAscii,
+} from '../lib/utils/text_utility';
let hasUserDefinedProjectPath = false;
let hasUserDefinedProjectName = false;
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
- const slug = slugify($projectNameInput.val());
+ const slug = slugify(convertUnicodeToAscii($projectNameInput.val()));
$projectPathInput.val(slug);
};
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index 4dbf6675357..5d51b7ea57b 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { n__, s__, __ } from '~/locale';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class AccessDropdown {
constructor(options) {
@@ -29,7 +30,7 @@ export default class AccessDropdown {
initDropdown() {
const { onSelect, onHide } = this.options;
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.getData.bind(this),
selectable: true,
filterable: true,
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index d61569fcd6e..81367f7d6b4 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -118,7 +118,10 @@ export default {
this.isTemplateSaving = true;
this.service
.updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled)
- .then(() => this.showAlert(__('Template was successfully saved.'), 'success'))
+ .then(({ data }) => {
+ this.incomingEmail = data?.service_desk_address;
+ this.showAlert(__('Changes were successfully made.'), 'success');
+ })
.catch(() =>
this.showAlert(
__('An error occurred while saving the template. Please check if the template exists.'),
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 0b7433d6aaa..6a0810ad3a1 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -157,14 +157,16 @@ export default {
}}
</span>
</template>
- <gl-button
- variant="success"
- class="gl-mt-5"
- :disabled="isTemplateSaving"
- @click="onSaveTemplate"
- >
- {{ __('Save template') }}
- </gl-button>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ variant="success"
+ class="gl-mt-5"
+ :disabled="isTemplateSaving"
+ @click="onSaveTemplate"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
index 05e769f5fc8..6f60141d7ab 100644
--- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
+++ b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlButton, GlFormGroup, GlFormInput, GlModal, GlModalDirective } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
index def2f091947..202286a5fb4 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
@@ -1,4 +1,5 @@
import { __ } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class ProtectedTagAccessDropdown {
constructor(options) {
@@ -8,7 +9,7 @@ export default class ProtectedTagAccessDropdown {
initDropdown() {
const { onSelect } = this.options;
- this.options.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.options.$dropdown, {
data: this.options.data,
selectable: true,
inputId: this.options.$dropdown.data('inputId'),
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index 03a5fe6b353..eb44f0c67fd 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -23,7 +23,7 @@ export default class ProtectedTagCreate {
});
// Select default
- $allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
+ $allowedToCreateDropdown.data('deprecatedJQueryDropdown').selectRowAtIndex(0);
// Protected tag dropdown
this.createItemDropdown = new CreateItemDropdown({
diff --git a/app/assets/javascripts/ref/components/ref_results_section.vue b/app/assets/javascripts/ref/components/ref_results_section.vue
index c8f5c66b0c1..dc74f86fd70 100644
--- a/app/assets/javascripts/ref/components/ref_results_section.vue
+++ b/app/assets/javascripts/ref/components/ref_results_section.vue
@@ -1,12 +1,12 @@
<script>
-import { GlNewDropdownHeader, GlNewDropdownItem, GlBadge, GlIcon } from '@gitlab/ui';
+import { GlDropdownSectionHeader, GlDropdownItem, GlBadge, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'RefResultsSection',
components: {
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlBadge,
GlIcon,
},
@@ -84,12 +84,12 @@ export default {
<template>
<div>
- <gl-new-dropdown-header>
+ <gl-dropdown-section-header>
<div class="gl-display-flex align-items-center" data-testid="section-header">
<span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span>
<gl-badge variant="neutral">{{ totalCountText }}</gl-badge>
</div>
- </gl-new-dropdown-header>
+ </gl-dropdown-section-header>
<template v-if="error">
<div class="gl-display-flex align-items-start text-danger gl-ml-4 gl-mr-4 gl-mb-3">
<gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" />
@@ -97,7 +97,7 @@ export default {
</div>
</template>
<template v-else>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="item in items"
:key="item.name"
@click="$emit('selected', item.value || item.name)"
@@ -118,7 +118,7 @@ export default {
s__('DefaultBranchLabel|default')
}}</gl-badge>
</div>
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index e388604ed92..85b123530b5 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -1,9 +1,9 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import {
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownHeader,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
@@ -18,9 +18,9 @@ export default {
name: 'RefSelector',
store: createStore(),
components: {
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownHeader,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
@@ -87,6 +87,15 @@ export default {
},
},
created() {
+ // This method is defined here instead of in `methods`
+ // because we need to access the .cancel() method
+ // lodash attaches to the function, which is
+ // made inaccessible by Vue. More info:
+ // https://stackoverflow.com/a/52988020/1063392
+ this.debouncedSearch = debounce(function search() {
+ this.search(this.query);
+ }, SEARCH_DEBOUNCE_MS);
+
this.setProjectId(this.projectId);
this.search(this.query);
},
@@ -95,9 +104,13 @@ export default {
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
- onSearchBoxInput: debounce(function search() {
+ onSearchBoxEnter() {
+ this.debouncedSearch.cancel();
this.search(this.query);
- }, SEARCH_DEBOUNCE_MS),
+ },
+ onSearchBoxInput() {
+ this.debouncedSearch();
+ },
selectRef(ref) {
this.setSelectedRef(ref);
this.$emit('input', this.selectedRef);
@@ -107,7 +120,7 @@ export default {
</script>
<template>
- <gl-new-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox">
+ <gl-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox">
<template slot="button-content">
<span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-400" data-testid="button-content">
<span v-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span>
@@ -117,11 +130,11 @@ export default {
</template>
<div class="gl-display-flex gl-flex-direction-column ref-selector-dropdown-content">
- <gl-new-dropdown-header>
+ <gl-dropdown-section-header>
<span class="gl-text-center gl-display-block">{{ i18n.dropdownHeader }}</span>
- </gl-new-dropdown-header>
+ </gl-dropdown-section-header>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
<gl-search-box-by-type
ref="searchBox"
@@ -129,6 +142,7 @@ export default {
class="gl-m-3"
:placeholder="i18n.searchPlaceholder"
@input="onSearchBoxInput"
+ @keydown.enter.prevent="onSearchBoxEnter"
/>
<div class="gl-flex-grow-1 gl-overflow-y-auto">
@@ -161,7 +175,7 @@ export default {
@selected="selectRef($event)"
/>
- <gl-new-dropdown-divider v-if="showTagsSection || showCommitsSection" />
+ <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" />
</template>
<template v-if="showTagsSection">
@@ -176,7 +190,7 @@ export default {
@selected="selectRef($event)"
/>
- <gl-new-dropdown-divider v-if="showCommitsSection" />
+ <gl-dropdown-divider v-if="showCommitsSection" />
</template>
<template v-if="showCommitsSection">
@@ -194,5 +208,5 @@ export default {
</template>
</div>
</div>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js
index 2e0113271df..af8729c1d08 100644
--- a/app/assets/javascripts/ref_select_dropdown.js
+++ b/app/assets/javascripts/ref_select_dropdown.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
class RefSelectDropdown {
constructor($dropdownButton, availableRefs) {
const availableRefsValue =
availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML);
- $dropdownButton.glDropdown({
+ initDeprecatedJQueryDropdown($dropdownButton, {
data: availableRefsValue,
filterable: true,
filterByText: true,
diff --git a/app/assets/javascripts/registry/explorer/components/delete_button.vue b/app/assets/javascripts/registry/explorer/components/delete_button.vue
index dab6a26ea16..ee856a3e546 100644
--- a/app/assets/javascripts/registry/explorer/components/delete_button.vue
+++ b/app/assets/javascripts/registry/explorer/components/delete_button.vue
@@ -47,7 +47,6 @@ export default {
:disabled="disabled"
:title="title"
:aria-label="title"
- category="secondary"
variant="danger"
icon="remove"
@click="$emit('delete')"
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
index c254dd05aa4..ff613daf7fa 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
@@ -1,9 +1,10 @@
<script>
import { GlSprintf } from '@gitlab/ui';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { DETAILS_PAGE_TITLE } from '../../constants/index';
export default {
- components: { GlSprintf },
+ components: { GlSprintf, TitleArea },
props: {
imageName: {
type: String,
@@ -18,13 +19,13 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-my-2 gl-align-items-center">
- <h4>
+ <title-area>
+ <template #title>
<gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName>
{{ imageName }}
</template>
</gl-sprintf>
- </h4>
- </div>
+ </template>
+ </title-area>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
index 8494967ab57..328026d0953 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
@@ -67,7 +67,6 @@ export default {
:key="tag.path"
:tag="tag"
:first="index === 0"
- :last="index === tags.length - 1"
:selected="selectedItems[tag.name]"
:is-desktop="isDesktop"
@select="updateSelectedItems(tag.name)"
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
index 8ec5cebbe8e..661213733ac 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
@@ -5,9 +5,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
-import ListItem from '../list_item.vue';
-import DetailsRow from '~/registry/shared/components/details_row.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
index 36a46ed58f4..85d87dab042 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
@@ -1,8 +1,8 @@
<script>
-import { GlDeprecatedDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
+import { GlDeprecatedDropdown } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import Tracking from '~/tracking';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
import {
QUICK_START,
LOGIN_COMMAND_LABEL,
@@ -13,22 +13,23 @@ import {
COPY_PUSH_TITLE,
} from '../../constants/index';
+const trackingLabel = 'quickstart_dropdown';
+
export default {
components: {
GlDeprecatedDropdown,
- GlFormGroup,
- GlFormInputGroup,
- ClipboardButton,
+ CodeInstruction,
},
- mixins: [Tracking.mixin({ label: 'quickstart_dropdown' })],
+ mixins: [Tracking.mixin({ label: trackingLabel })],
+ trackingLabel,
i18n: {
- dropdownTitle: QUICK_START,
- loginCommandLabel: LOGIN_COMMAND_LABEL,
- copyLoginTitle: COPY_LOGIN_TITLE,
- buildCommandLabel: BUILD_COMMAND_LABEL,
- copyBuildTitle: COPY_BUILD_TITLE,
- pushCommandLabel: PUSH_COMMAND_LABEL,
- copyPushTitle: COPY_PUSH_TITLE,
+ QUICK_START,
+ LOGIN_COMMAND_LABEL,
+ COPY_LOGIN_TITLE,
+ BUILD_COMMAND_LABEL,
+ COPY_BUILD_TITLE,
+ PUSH_COMMAND_LABEL,
+ COPY_PUSH_TITLE,
},
computed: {
...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
@@ -37,7 +38,7 @@ export default {
</script>
<template>
<gl-deprecated-dropdown
- :text="$options.i18n.dropdownTitle"
+ :text="$options.i18n.QUICK_START"
variant="primary"
size="sm"
right
@@ -45,59 +46,30 @@ export default {
>
<!-- This li is used as a container since gl-dropdown produces a root ul, this mimics the functionality exposed by b-dropdown-form -->
<li role="presentation" class="px-2 py-1 dropdown-menu-large">
- <form>
- <gl-form-group
- label-size="sm"
- label-for="docker-login-btn"
- :label="$options.i18n.loginCommandLabel"
- >
- <gl-form-input-group id="docker-login-btn" :value="dockerLoginCommand" readonly>
- <template #append>
- <clipboard-button
- class="border"
- :text="dockerLoginCommand"
- :title="$options.i18n.copyLoginTitle"
- @click.native="track('click_copy_login')"
- />
- </template>
- </gl-form-input-group>
- </gl-form-group>
+ <code-instruction
+ :label="$options.i18n.LOGIN_COMMAND_LABEL"
+ :instruction="dockerLoginCommand"
+ :copy-text="$options.i18n.COPY_LOGIN_TITLE"
+ tracking-action="click_copy_login"
+ :tracking-label="$options.trackingLabel"
+ />
- <gl-form-group
- label-size="sm"
- label-for="docker-build-btn"
- :label="$options.i18n.buildCommandLabel"
- >
- <gl-form-input-group id="docker-build-btn" :value="dockerBuildCommand" readonly>
- <template #append>
- <clipboard-button
- class="border"
- :text="dockerBuildCommand"
- :title="$options.i18n.copyBuildTitle"
- @click.native="track('click_copy_build')"
- />
- </template>
- </gl-form-input-group>
- </gl-form-group>
+ <code-instruction
+ :label="$options.i18n.BUILD_COMMAND_LABEL"
+ :instruction="dockerBuildCommand"
+ :copy-text="$options.i18n.COPY_BUILD_TITLE"
+ tracking-action="click_copy_build"
+ :tracking-label="$options.trackingLabel"
+ />
- <gl-form-group
- class="mb-0"
- label-size="sm"
- label-for="docker-push-btn"
- :label="$options.i18n.pushCommandLabel"
- >
- <gl-form-input-group id="docker-push-btn" :value="dockerPushCommand" readonly>
- <template #append>
- <clipboard-button
- class="border"
- :text="dockerPushCommand"
- :title="$options.i18n.copyPushTitle"
- @click.native="track('click_copy_push')"
- />
- </template>
- </gl-form-input-group>
- </gl-form-group>
- </form>
+ <code-instruction
+ class="mb-0"
+ :label="$options.i18n.PUSH_COMMAND_LABEL"
+ :instruction="dockerPushCommand"
+ :copy-text="$options.i18n.COPY_PUSH_TITLE"
+ tracking-action="click_copy_push"
+ :tracking-label="$options.trackingLabel"
+ />
</li>
</gl-deprecated-dropdown>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
index 65cf51fd1d1..d1b9894da0e 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
@@ -38,7 +38,6 @@ export default {
:key="index"
:item="listItem"
:first="index === 0"
- :last="index === images.length - 1"
@delete="$emit('delete', $event)"
/>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
index 102311c6062..32bf27f1143 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import ListItem from '../list_item.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
import {
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
index c7b4fd5f4b4..7be68e77def 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
@@ -1,6 +1,8 @@
<script>
-import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
-import { n__ } from '~/locale';
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import { n__, sprintf } from '~/locale';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
import {
@@ -13,9 +15,10 @@ import {
export default {
components: {
- GlIcon,
GlSprintf,
GlLink,
+ TitleArea,
+ MetadataItem,
},
props: {
expirationPolicy: {
@@ -56,11 +59,12 @@ export default {
},
computed: {
imagesCountText() {
- return n__(
+ const pluralisedString = n__(
'ContainerRegistry|%{count} Image repository',
'ContainerRegistry|%{count} Image repositories',
this.imagesCount,
);
+ return sprintf(pluralisedString, { count: this.imagesCount });
},
timeTillRun() {
const difference = calculateRemainingMilliseconds(this.expirationPolicy?.next_run_at);
@@ -71,7 +75,7 @@ export default {
},
expirationPolicyText() {
return this.expirationPolicyEnabled
- ? EXPIRATION_POLICY_WILL_RUN_IN
+ ? sprintf(EXPIRATION_POLICY_WILL_RUN_IN, { time: this.timeTillRun })
: EXPIRATION_POLICY_DISABLED_TEXT;
},
showExpirationPolicyTip() {
@@ -85,37 +89,29 @@ export default {
<template>
<div>
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
- data-testid="header"
- >
- <h4 data-testid="title">{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
- <div class="gl-display-none d-sm-block" data-testid="commands-slot">
+ <title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE">
+ <template #right-actions>
<slot name="commands"></slot>
- </div>
- </div>
- <div
- v-if="imagesCount"
- class="gl-display-flex gl-align-items-center gl-mt-1 gl-mb-3 gl-text-gray-500"
- data-testid="subheader"
- >
- <span class="gl-mr-3" data-testid="images-count">
- <gl-icon class="gl-mr-1" name="container-image" />
- <gl-sprintf :message="imagesCountText">
- <template #count>
- {{ imagesCount }}
- </template>
- </gl-sprintf>
- </span>
- <span v-if="!hideExpirationPolicyData" data-testid="expiration-policy">
- <gl-icon class="gl-mr-1" name="expire" />
- <gl-sprintf :message="expirationPolicyText">
- <template #time>
- {{ timeTillRun }}
- </template>
- </gl-sprintf>
- </span>
- </div>
+ </template>
+ <template #metadata_count>
+ <metadata-item
+ v-if="imagesCount"
+ data-testid="images-count"
+ icon="container-image"
+ :text="imagesCountText"
+ />
+ </template>
+ <template #metadata_exp_policies>
+ <metadata-item
+ v-if="!hideExpirationPolicyData"
+ data-testid="expiration-policy"
+ icon="expire"
+ :text="expirationPolicyText"
+ size="xl"
+ />
+ </template>
+ </title-area>
+
<div data-testid="info-area">
<p>
<span data-testid="default-intro">
diff --git a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
index d935ca091a1..146d1434b18 100644
--- a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
+++ b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
@@ -1,7 +1,9 @@
<script>
import { initial, first, last } from 'lodash';
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
export default {
+ directives: { SafeHtml },
props: {
crumbs: {
type: Array,
@@ -41,14 +43,14 @@ export default {
<li
v-for="(crumb, index) in rootCrumbs"
:key="index"
+ v-safe-html="crumb.innerHTML"
:class="crumb.className"
- v-html="crumb.innerHTML"
></li>
<li v-if="!isRootRoute">
<router-link ref="rootRouteLink" :to="rootRoute.path">
{{ rootRoute.meta.nameGenerator(rootRoute) }}
</router-link>
- <component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" />
+ <component :is="divider.tagName" v-safe-html="divider.innerHTML" :class="divider.classList" />
</li>
<li>
<component :is="lastCrumb.tagName" ref="lastCrumb" :class="lastCrumb.className">
diff --git a/app/assets/javascripts/registry/explorer/stores/index.js b/app/assets/javascripts/registry/explorer/stores/index.js
index 54a8e0e1c1c..18e3351ed13 100644
--- a/app/assets/javascripts/registry/explorer/stores/index.js
+++ b/app/assets/javascripts/registry/explorer/stores/index.js
@@ -7,7 +7,6 @@ import state from './state';
Vue.use(Vuex);
-// eslint-disable-next-line import/prefer-default-export
export const createStore = () =>
new Vuex.Store({
state,
diff --git a/app/assets/javascripts/registry/explorer/utils.js b/app/assets/javascripts/registry/explorer/utils.js
index b1df87c6993..44262a6cbb6 100644
--- a/app/assets/javascripts/registry/explorer/utils.js
+++ b/app/assets/javascripts/registry/explorer/utils.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const decodeAndParse = param => JSON.parse(window.atob(param));
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index f129922c1d2..7a26fb5cbee 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -1,7 +1,7 @@
<script>
import { get } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlCard, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlCard, GlButton, GlLoadingIcon } from '@gitlab/ui';
import Tracking from '~/tracking';
import { mapComputed } from '~/vuex_shared/bindings';
import {
@@ -14,7 +14,7 @@ import { SET_CLEANUP_POLICY_BUTTON, CLEANUP_POLICY_CARD_HEADER } from '../consta
export default {
components: {
GlCard,
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
ExpirationPolicyFields,
},
@@ -104,24 +104,25 @@ export default {
</template>
<template #footer>
<div class="gl-display-flex gl-justify-content-end">
- <gl-deprecated-button
+ <gl-button
ref="cancel-button"
type="reset"
class="gl-mr-3 gl-display-block"
:disabled="isCancelButtonDisabled"
>
{{ __('Cancel') }}
- </gl-deprecated-button>
- <gl-deprecated-button
+ </gl-button>
+ <gl-button
ref="save-button"
type="submit"
:disabled="isSubmitButtonDisabled"
variant="success"
+ category="primary"
class="gl-display-flex gl-justify-content-center gl-align-items-center js-no-auto-disable"
>
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
<gl-loading-icon v-if="isLoading" class="gl-ml-3" />
- </gl-deprecated-button>
+ </gl-button>
</div>
</template>
</gl-card>
diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
new file mode 100644
index 00000000000..63d61989cba
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -0,0 +1,207 @@
+<script>
+import { GlFormGroup, GlFormRadioGroup, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import RelatedIssuableInput from './related_issuable_input.vue';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+
+import {
+ issuableTypesMap,
+ itemAddFailureTypesMap,
+ linkedIssueTypesMap,
+ addRelatedIssueErrorMap,
+ addRelatedItemErrorMap,
+} from '../constants';
+
+export default {
+ name: 'AddIssuableForm',
+ components: {
+ GlFormGroup,
+ GlFormRadioGroup,
+ RelatedIssuableInput,
+ GlButton,
+ },
+ props: {
+ inputValue: {
+ type: String,
+ required: true,
+ },
+ pendingReferences: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ autoCompleteSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ showCategorizedIssues: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isSubmitting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pathIdSeparator: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: issuableTypesMap.ISSUE,
+ },
+ hasError: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ itemAddFailureType: {
+ type: String,
+ required: false,
+ default: itemAddFailureTypesMap.NOT_FOUND,
+ },
+ itemAddFailureMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ confidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ linkedIssueType: linkedIssueTypesMap.RELATES_TO,
+ linkedIssueTypes: [
+ {
+ text: __('relates to'),
+ value: linkedIssueTypesMap.RELATES_TO,
+ },
+ {
+ text: __('blocks'),
+ value: linkedIssueTypesMap.BLOCKS,
+ },
+ {
+ text: __('is blocked by'),
+ value: linkedIssueTypesMap.IS_BLOCKED_BY,
+ },
+ ],
+ };
+ },
+ computed: {
+ isSubmitButtonDisabled() {
+ return (
+ (this.inputValue.length === 0 && this.pendingReferences.length === 0) || this.isSubmitting
+ );
+ },
+ addRelatedErrorMessage() {
+ if (this.itemAddFailureMessage) {
+ return this.itemAddFailureMessage;
+ } else if (this.itemAddFailureType === itemAddFailureTypesMap.NOT_FOUND) {
+ return addRelatedIssueErrorMap[this.issuableType];
+ }
+ // Only other failure is MAX_NUMBER_OF_CHILD_EPICS at the moment
+ return addRelatedItemErrorMap[this.itemAddFailureType];
+ },
+ transformedAutocompleteSources() {
+ if (!this.confidential) {
+ return this.autoCompleteSources;
+ }
+
+ if (!this.autoCompleteSources?.issues || !this.autoCompleteSources?.epics) {
+ return this.autoCompleteSources;
+ }
+
+ return {
+ ...this.autoCompleteSources,
+ issues: mergeUrlParams({ confidential_only: true }, this.autoCompleteSources.issues),
+ epics: mergeUrlParams({ confidential_only: true }, this.autoCompleteSources.epics),
+ };
+ },
+ },
+ methods: {
+ onPendingIssuableRemoveRequest(params) {
+ this.$emit('pendingIssuableRemoveRequest', params);
+ },
+ onFormSubmit() {
+ this.$emit('addIssuableFormSubmit', {
+ pendingReferences: this.$refs.relatedIssuableInput.$refs.input.value,
+ linkedIssueType: this.linkedIssueType,
+ });
+ },
+ onFormCancel() {
+ this.$emit('addIssuableFormCancel');
+ },
+ onAddIssuableFormInput(params) {
+ this.$emit('addIssuableFormInput', params);
+ },
+ onAddIssuableFormBlur(params) {
+ this.$emit('addIssuableFormBlur', params);
+ },
+ },
+};
+</script>
+
+<template>
+ <form @submit.prevent="onFormSubmit">
+ <template v-if="showCategorizedIssues">
+ <gl-form-group
+ :label="__('The current issue')"
+ label-for="linked-issue-type-radio"
+ label-class="label-bold"
+ class="mb-2"
+ >
+ <gl-form-radio-group
+ id="linked-issue-type-radio"
+ v-model="linkedIssueType"
+ :options="linkedIssueTypes"
+ :checked="linkedIssueType"
+ />
+ </gl-form-group>
+ <p class="bold">
+ {{ __('the following issue(s)') }}
+ </p>
+ </template>
+ <related-issuable-input
+ ref="relatedIssuableInput"
+ input-id="add-related-issues-form-input"
+ :confidential="confidential"
+ :focus-on-mount="true"
+ :references="pendingReferences"
+ :path-id-separator="pathIdSeparator"
+ :input-value="inputValue"
+ :auto-complete-sources="transformedAutocompleteSources"
+ :auto-complete-options="{ issues: true, epics: true }"
+ :issuable-type="issuableType"
+ @pendingIssuableRemoveRequest="onPendingIssuableRemoveRequest"
+ @formCancel="onFormCancel"
+ @addIssuableFormBlur="onAddIssuableFormBlur"
+ @addIssuableFormInput="onAddIssuableFormInput"
+ />
+ <p v-if="hasError" class="gl-field-error">
+ {{ addRelatedErrorMessage }}
+ </p>
+ <div class="add-issuable-form-actions clearfix">
+ <gl-button
+ ref="addButton"
+ category="primary"
+ variant="success"
+ :disabled="isSubmitButtonDisabled"
+ :loading="isSubmitting"
+ type="submit"
+ class="js-add-issuable-form-add-button float-left qa-add-issue-button"
+ >
+ {{ __('Add') }}
+ </gl-button>
+ <gl-button class="float-right" @click="onFormCancel">
+ {{ __('Cancel') }}
+ </gl-button>
+ </div>
+ </form>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/issue_token.vue b/app/assets/javascripts/related_issues/components/issue_token.vue
new file mode 100644
index 00000000000..31d0c7dbbb0
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/issue_token.vue
@@ -0,0 +1,115 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
+
+export default {
+ name: 'IssueToken',
+ components: {
+ GlIcon,
+ },
+ mixins: [relatedIssuableMixin],
+ props: {
+ isCondensed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ removeButtonLabel() {
+ const { displayReference } = this;
+ /*
+ * Giving false as third argument to prevent unescaping of ampersand in
+ * epic reference. Eg. &42 will remain &42 instead of &amp;42
+ *
+ * https://docs.gitlab.com/ee/development/i18n/externalization.html#interpolation
+ */
+ return sprintf(__('Remove %{displayReference}'), { displayReference }, false);
+ },
+ stateTitle() {
+ if (this.isCondensed) return '';
+
+ return this.isOpen ? __('Open') : __('Closed');
+ },
+ innerComponentType() {
+ return this.isCondensed ? 'span' : 'div';
+ },
+ issueTitle() {
+ return this.isCondensed ? this.title : '';
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="{
+ 'issue-token': isCondensed,
+ 'flex-row issuable-info-container': !isCondensed,
+ }"
+ >
+ <component
+ :is="computedLinkElementType"
+ ref="link"
+ v-tooltip
+ :class="{
+ 'issue-token-link': isCondensed,
+ 'issuable-main-info': !isCondensed,
+ }"
+ :href="computedPath"
+ :title="issueTitle"
+ data-placement="top"
+ >
+ <component
+ :is="innerComponentType"
+ v-if="hasTitle"
+ ref="title"
+ :class="{
+ 'issue-token-title issue-token-end': isCondensed,
+ 'issue-title block-truncated': !isCondensed,
+ 'issue-token-title-standalone': !canRemove,
+ }"
+ class="js-issue-token-title"
+ >
+ <span class="issue-token-title-text">{{ title }}</span>
+ </component>
+ <component
+ :is="innerComponentType"
+ ref="reference"
+ :class="{
+ 'issue-token-reference': isCondensed,
+ 'issuable-info': !isCondensed,
+ }"
+ >
+ <gl-icon
+ v-if="hasState"
+ v-tooltip
+ :class="iconClass"
+ :name="iconName"
+ :size="12"
+ :title="stateTitle"
+ :aria-label="state"
+ />
+ {{ displayReference }}
+ </component>
+ </component>
+ <button
+ v-if="canRemove"
+ ref="removeButton"
+ v-tooltip
+ :class="{
+ 'issue-token-remove-button': isCondensed,
+ 'btn btn-default': !isCondensed,
+ }"
+ :title="removeButtonLabel"
+ :aria-label="removeButtonLabel"
+ :disabled="removeDisabled"
+ type="button"
+ class="js-issue-token-remove-button"
+ @click="onRemoveRequest"
+ >
+ <gl-icon name="close" aria-hidden="true" />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
new file mode 100644
index 00000000000..1931cfb2c00
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
@@ -0,0 +1,231 @@
+<script>
+import $ from 'jquery';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
+import issueToken from './issue_token.vue';
+import {
+ autoCompleteTextMap,
+ inputPlaceholderConfidentialTextMap,
+ inputPlaceholderTextMap,
+ issuableTypesMap,
+} from '../constants';
+
+const SPACE_FACTOR = 1;
+
+export default {
+ name: 'RelatedIssuableInput',
+ components: {
+ issueToken,
+ },
+ props: {
+ inputId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ references: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ pathIdSeparator: {
+ type: String,
+ required: true,
+ },
+ inputValue: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ focusOnMount: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ autoCompleteSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ autoCompleteOptions: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: issuableTypesMap.ISSUE,
+ },
+ confidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isInputFocused: false,
+ isAutoCompleteOpen: false,
+ areEventsAssigned: false,
+ };
+ },
+ computed: {
+ inputPlaceholder() {
+ const { issuableType, allowAutoComplete, confidential } = this;
+ const inputPlaceholderMapping = confidential
+ ? inputPlaceholderConfidentialTextMap
+ : inputPlaceholderTextMap;
+ const allowAutoCompleteText = autoCompleteTextMap[allowAutoComplete][issuableType];
+ return `${inputPlaceholderMapping[issuableType]}${allowAutoCompleteText}`;
+ },
+ allowAutoComplete() {
+ return Object.keys(this.autoCompleteSources).length > 0;
+ },
+ },
+ mounted() {
+ this.setupAutoComplete();
+ if (this.focusOnMount) {
+ this.$nextTick()
+ .then(() => {
+ this.$refs.input.focus();
+ })
+ .catch(() => {});
+ }
+ },
+ beforeUpdate() {
+ this.setupAutoComplete();
+ },
+ beforeDestroy() {
+ const $input = $(this.$refs.input);
+ $input.off('shown-issues.atwho');
+ $input.off('hidden-issues.atwho');
+ $input.off('inserted-issues.atwho', this.onInput);
+ },
+ methods: {
+ onAutoCompleteToggled(isOpen) {
+ this.isAutoCompleteOpen = isOpen;
+ },
+ onInputWrapperClick() {
+ this.$refs.input.focus();
+ },
+ onInput() {
+ const { value } = this.$refs.input;
+ const caretPos = this.$refs.input.selectionStart;
+ const rawRefs = value.split(/\s/);
+ let touchedReference;
+ let position = 0;
+
+ const untouchedRawRefs = rawRefs
+ .filter(ref => {
+ let isTouched = false;
+
+ if (caretPos >= position && caretPos <= position + ref.length) {
+ touchedReference = ref;
+ isTouched = true;
+ }
+
+ position = position + ref.length + SPACE_FACTOR;
+
+ return !isTouched;
+ })
+ .filter(ref => ref.trim().length > 0);
+
+ this.$emit('addIssuableFormInput', {
+ newValue: value,
+ untouchedRawReferences: untouchedRawRefs,
+ touchedReference,
+ caretPos,
+ });
+ },
+ onBlur(event) {
+ // Early exit if this Blur event is caused by card header
+ const container = this.$root.$el.querySelector('.js-button-container');
+ if (container && container.contains(event.relatedTarget)) {
+ return;
+ }
+
+ this.isInputFocused = false;
+
+ // Avoid tokenizing partial input when clicking an autocomplete item
+ if (!this.isAutoCompleteOpen) {
+ const { value } = this.$refs.input;
+ // Avoid event emission when only pathIdSeparator has been typed
+ if (value !== this.pathIdSeparator) {
+ this.$emit('addIssuableFormBlur', value);
+ }
+ }
+ },
+ onFocus() {
+ this.isInputFocused = true;
+ },
+ setupAutoComplete() {
+ const $input = $(this.$refs.input);
+
+ if (this.allowAutoComplete) {
+ this.gfmAutoComplete = new GfmAutoComplete(this.autoCompleteSources);
+ this.gfmAutoComplete.setup($input, this.autoCompleteOptions);
+ }
+
+ if (!this.areEventsAssigned) {
+ $input.on('shown-issues.atwho', this.onAutoCompleteToggled.bind(this, true));
+ $input.on('hidden-issues.atwho', this.onAutoCompleteToggled.bind(this, true));
+ }
+ this.areEventsAssigned = true;
+ },
+ onIssuableFormWrapperClick() {
+ this.$refs.input.focus();
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ ref="issuableFormWrapper"
+ :class="{ focus: isInputFocused }"
+ class="add-issuable-form-input-wrapper form-control gl-field-error-outline"
+ role="button"
+ @click="onIssuableFormWrapperClick"
+ >
+ <ul class="add-issuable-form-input-token-list">
+ <!--
+ We need to ensure this key changes any time the pendingReferences array is updated
+ else two consecutive pending ref strings in an array with the same name will collide
+ and cause odd behavior when one is removed.
+ -->
+ <li
+ v-for="(reference, index) in references"
+ :key="`related-issues-token-${reference}`"
+ class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item"
+ >
+ <issue-token
+ :id-key="index"
+ :display-reference="reference.text || reference"
+ :can-remove="true"
+ :is-condensed="true"
+ :path-id-separator="pathIdSeparator"
+ event-namespace="pendingIssuable"
+ @pendingIssuableRemoveRequest="
+ params => {
+ $emit('pendingIssuableRemoveRequest', params);
+ }
+ "
+ />
+ </li>
+ <li class="add-issuable-form-input-list-item">
+ <input
+ :id="inputId"
+ ref="input"
+ :value="inputValue"
+ :placeholder="inputPlaceholder"
+ type="text"
+ class="js-add-issuable-form-input add-issuable-form-input qa-add-issue-input"
+ @input="onInput"
+ @focus="onFocus"
+ @blur="onBlur"
+ @keyup.escape.exact="$emit('addIssuableFormCancel')"
+ />
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
new file mode 100644
index 00000000000..f7a79c62716
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -0,0 +1,215 @@
+<script>
+import { GlLink, GlIcon, GlButton } from '@gitlab/ui';
+import AddIssuableForm from './add_issuable_form.vue';
+import RelatedIssuesList from './related_issues_list.vue';
+import {
+ issuableIconMap,
+ issuableQaClassMap,
+ linkedIssueTypesMap,
+ linkedIssueTypesTextMap,
+} from '../constants';
+
+export default {
+ name: 'RelatedIssuesBlock',
+ components: {
+ GlLink,
+ GlButton,
+ GlIcon,
+ AddIssuableForm,
+ RelatedIssuesList,
+ },
+ props: {
+ isFetching: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isSubmitting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ relatedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ canAdmin: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ canReorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isFormVisible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pendingReferences: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ inputValue: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ pathIdSeparator: {
+ type: String,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ autoCompleteSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ showCategorizedIssues: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ hasRelatedIssues() {
+ return this.relatedIssues.length > 0;
+ },
+ categorisedIssues() {
+ if (this.showCategorizedIssues) {
+ return Object.values(linkedIssueTypesMap)
+ .map(linkType => ({
+ linkType,
+ issues: this.relatedIssues.filter(issue => issue.linkType === linkType),
+ }))
+ .filter(obj => obj.issues.length > 0);
+ }
+
+ return [{ issues: this.relatedIssues }];
+ },
+ shouldShowTokenBody() {
+ return this.hasRelatedIssues || this.isFetching;
+ },
+ hasBody() {
+ return this.isFormVisible || this.shouldShowTokenBody;
+ },
+ badgeLabel() {
+ return this.isFetching && this.relatedIssues.length === 0 ? '...' : this.relatedIssues.length;
+ },
+ hasHelpPath() {
+ return this.helpPath.length > 0;
+ },
+ issuableTypeIcon() {
+ return issuableIconMap[this.issuableType];
+ },
+ qaClass() {
+ return issuableQaClassMap[this.issuableType];
+ },
+ },
+ linkedIssueTypesTextMap,
+};
+</script>
+
+<template>
+ <div id="related-issues" class="related-issues-block">
+ <div class="card card-slim gl-overflow-hidden">
+ <div
+ :class="{ 'panel-empty-heading border-bottom-0': !hasBody }"
+ class="card-header gl-display-flex gl-justify-content-space-between"
+ >
+ <h3
+ class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
+ >
+ <gl-link
+ id="user-content-related-issues"
+ class="anchor position-absolute gl-text-decoration-none"
+ href="#related-issues"
+ aria-hidden="true"
+ />
+ <slot name="headerText">{{ __('Linked issues') }}</slot>
+ <gl-link
+ v-if="hasHelpPath"
+ :href="helpPath"
+ target="_blank"
+ class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
+ :aria-label="__('Read more about related issues')"
+ >
+ <gl-icon name="question" :size="12" role="text" />
+ </gl-link>
+
+ <div class="gl-display-inline-flex">
+ <div class="js-related-issues-header-issue-count gl-display-inline-flex gl-mx-5">
+ <span class="gl-display-inline-flex gl-align-items-center">
+ <gl-icon :name="issuableTypeIcon" class="gl-mr-2 gl-text-gray-500" />
+ {{ badgeLabel }}
+ </span>
+ </div>
+ <gl-button
+ v-if="canAdmin"
+ data-qa-selector="related_issues_plus_button"
+ icon="plus"
+ :aria-label="__('Add a related issue')"
+ :class="qaClass"
+ class="js-issue-count-badge-add-button"
+ @click="$emit('toggleAddRelatedIssuesForm', $event)"
+ />
+ </div>
+ </h3>
+ <slot name="headerActions"></slot>
+ </div>
+ <div
+ class="linked-issues-card-body bg-gray-light"
+ :class="{
+ 'gl-p-5': isFormVisible || shouldShowTokenBody,
+ }"
+ >
+ <div
+ v-if="isFormVisible"
+ class="js-add-related-issues-form-area card-body bordered-box bg-white"
+ >
+ <add-issuable-form
+ :show-categorized-issues="showCategorizedIssues"
+ :is-submitting="isSubmitting"
+ :issuable-type="issuableType"
+ :input-value="inputValue"
+ :pending-references="pendingReferences"
+ :auto-complete-sources="autoCompleteSources"
+ :path-id-separator="pathIdSeparator"
+ @pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event)"
+ @addIssuableFormInput="$emit('addIssuableFormInput', $event)"
+ @addIssuableFormBlur="$emit('addIssuableFormBlur', $event)"
+ @addIssuableFormSubmit="$emit('addIssuableFormSubmit', $event)"
+ @addIssuableFormCancel="$emit('addIssuableFormCancel', $event)"
+ />
+ </div>
+ <template v-if="shouldShowTokenBody">
+ <related-issues-list
+ v-for="category in categorisedIssues"
+ :key="category.linkType"
+ :heading="$options.linkedIssueTypesTextMap[category.linkType]"
+ :can-admin="canAdmin"
+ :can-reorder="canReorder"
+ :is-fetching="isFetching"
+ :issuable-type="issuableType"
+ :path-id-separator="pathIdSeparator"
+ :related-issues="category.issues"
+ @relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
+ @saveReorder="$emit('saveReorder', $event)"
+ />
+ </template>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue
new file mode 100644
index 00000000000..a75fe4397bb
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue
@@ -0,0 +1,146 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import Sortable from 'sortablejs';
+import sortableConfig from 'ee_else_ce/sortable/sortable_config';
+import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ name: 'RelatedIssuesList',
+ directives: {
+ tooltip,
+ },
+ components: {
+ GlLoadingIcon,
+ RelatedIssuableItem,
+ },
+ props: {
+ canAdmin: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ canReorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ heading: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isFetching: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ pathIdSeparator: {
+ type: String,
+ required: true,
+ },
+ relatedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ mounted() {
+ if (this.canReorder) {
+ this.sortable = Sortable.create(this.$refs.list, {
+ ...sortableConfig,
+ onStart: this.addDraggingCursor,
+ onEnd: this.reordered,
+ });
+ }
+ },
+ methods: {
+ getBeforeAfterId(itemEl) {
+ const prevItemEl = itemEl.previousElementSibling;
+ const nextItemEl = itemEl.nextElementSibling;
+
+ return {
+ beforeId: prevItemEl && parseInt(prevItemEl.dataset.orderingId, 0),
+ afterId: nextItemEl && parseInt(nextItemEl.dataset.orderingId, 0),
+ };
+ },
+ reordered(event) {
+ this.removeDraggingCursor();
+ const { beforeId, afterId } = this.getBeforeAfterId(event.item);
+ const { oldIndex, newIndex } = event;
+
+ this.$emit('saveReorder', {
+ issueId: parseInt(event.item.dataset.key, 10),
+ oldIndex,
+ newIndex,
+ afterId,
+ beforeId,
+ });
+ },
+ addDraggingCursor() {
+ document.body.classList.add('is-dragging');
+ },
+ removeDraggingCursor() {
+ document.body.classList.remove('is-dragging');
+ },
+ issuableOrderingId({ epicIssueId, id }) {
+ return this.issuableType === 'issue' ? epicIssueId : id;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h4 v-if="heading" class="gl-font-base mt-0">{{ heading }}</h4>
+ <div
+ class="related-issues-token-body bordered-box bg-white"
+ :class="{ 'sortable-container': canReorder }"
+ >
+ <div v-if="isFetching" class="related-issues-loading-icon qa-related-issues-loading-icon">
+ <gl-loading-icon ref="loadingIcon" label="Fetching linked issues" class="gl-mt-2" />
+ </div>
+ <ul ref="list" :class="{ 'content-list': !canReorder }" class="related-items-list">
+ <li
+ v-for="issue in relatedIssues"
+ :key="issue.id"
+ :class="{
+ 'user-can-drag': canReorder,
+ 'sortable-row': canReorder,
+ 'card card-slim': canReorder,
+ }"
+ :data-key="issue.id"
+ :data-ordering-id="issuableOrderingId(issue)"
+ class="js-related-issues-token-list-item list-item pt-0 pb-0"
+ >
+ <related-issuable-item
+ :id-key="issue.id"
+ :display-reference="issue.reference"
+ :confidential="issue.confidential"
+ :title="issue.title"
+ :path="issue.path"
+ :state="issue.state"
+ :milestone="issue.milestone"
+ :assignees="issue.assignees"
+ :created-at="issue.createdAt"
+ :closed-at="issue.closedAt"
+ :weight="issue.weight"
+ :due-date="issue.dueDate"
+ :can-remove="canAdmin"
+ :can-reorder="canReorder"
+ :path-id-separator="pathIdSeparator"
+ :is-locked="issue.lockIssueRemoval"
+ :locked-message="issue.lockedMessage"
+ event-namespace="relatedIssue"
+ class="qa-related-issuable-item"
+ @relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
+ />
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue
new file mode 100644
index 00000000000..6f68b25b6fb
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue
@@ -0,0 +1,247 @@
+<script>
+/*
+`rawReferences` are separated by spaces.
+Given `abc 123 zxc`, `rawReferences = ['abc', '123', 'zxc']`
+
+Consider you are typing `abc 123 zxc` in the input and your caret position is
+at position 4 right before the `123` `rawReference`. Then you type `#` and
+it becomes a valid reference, `#123`, but we don't want to jump it straight into
+`pendingReferences` because you could still want to type. Say you typed `999`
+and now we have `#999123`. Only when you move your caret away from that `rawReference`
+do we actually put it in the `pendingReferences`.
+
+Your caret can stop touching a `rawReference` can happen in a variety of ways:
+
+ - As you type, we only tokenize after you type a space or move with the arrow keys
+ - On blur, we consider your caret not touching anything
+
+---
+
+ - When you click the "Add related issues"(in the `AddIssuableForm`),
+ we submit the `pendingReferences` to the server and they come back as actual `relatedIssues`
+ - When you click the "Cancel"(in the `AddIssuableForm`), we clear out `pendingReferences`
+ and hide the `AddIssuableForm` area.
+
+*/
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import { __ } from '~/locale';
+import RelatedIssuesBlock from './related_issues_block.vue';
+import RelatedIssuesStore from '../stores/related_issues_store';
+import RelatedIssuesService from '../services/related_issues_service';
+import {
+ relatedIssuesRemoveErrorMap,
+ pathIndeterminateErrorMap,
+ addRelatedIssueErrorMap,
+ issuableTypesMap,
+ PathIdSeparator,
+} from '../constants';
+
+export default {
+ name: 'RelatedIssuesRoot',
+ components: {
+ relatedIssuesBlock: RelatedIssuesBlock,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ canAdmin: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ canReorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: issuableTypesMap.ISSUE,
+ },
+ allowAutoComplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ pathIdSeparator: {
+ type: String,
+ required: false,
+ default: PathIdSeparator.Issue,
+ },
+ cssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCategorizedIssues: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ this.store = new RelatedIssuesStore();
+
+ return {
+ state: this.store.state,
+ isFetching: false,
+ isSubmitting: false,
+ isFormVisible: false,
+ inputValue: '',
+ };
+ },
+ computed: {
+ autoCompleteSources() {
+ if (!this.allowAutoComplete) return {};
+ return gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources;
+ },
+ },
+ created() {
+ this.service = new RelatedIssuesService(this.endpoint);
+ this.fetchRelatedIssues();
+ },
+ methods: {
+ findRelatedIssueById(id) {
+ return this.state.relatedIssues.find(issue => issue.id === id);
+ },
+ onRelatedIssueRemoveRequest(idToRemove) {
+ const issueToRemove = this.findRelatedIssueById(idToRemove);
+
+ if (issueToRemove) {
+ RelatedIssuesService.remove(issueToRemove.relationPath)
+ .then(({ data }) => {
+ this.store.setRelatedIssues(data.issuables);
+ })
+ .catch(res => {
+ if (res && res.status !== 404) {
+ Flash(relatedIssuesRemoveErrorMap[this.issuableType]);
+ }
+ });
+ } else {
+ Flash(pathIndeterminateErrorMap[this.issuableType]);
+ }
+ },
+ onToggleAddRelatedIssuesForm() {
+ this.isFormVisible = !this.isFormVisible;
+ },
+ onPendingIssueRemoveRequest(indexToRemove) {
+ this.store.removePendingRelatedIssue(indexToRemove);
+ },
+ onPendingFormSubmit(event) {
+ this.processAllReferences(event.pendingReferences);
+
+ if (this.state.pendingReferences.length > 0) {
+ this.isSubmitting = true;
+ this.service
+ .addRelatedIssues(this.state.pendingReferences, event.linkedIssueType)
+ .then(({ data }) => {
+ // We could potentially lose some pending issues in the interim here
+ this.store.setPendingReferences([]);
+ this.store.setRelatedIssues(data.issuables);
+
+ // Close the form on submission
+ this.isFormVisible = false;
+ })
+ .catch(({ response }) => {
+ let errorMessage = addRelatedIssueErrorMap[this.issuableType];
+ if (response && response.data && response.data.message) {
+ errorMessage = response.data.message;
+ }
+ Flash(errorMessage);
+ })
+ .finally(() => {
+ this.isSubmitting = false;
+ });
+ }
+ },
+ onPendingFormCancel() {
+ this.isFormVisible = false;
+ this.store.setPendingReferences([]);
+ this.inputValue = '';
+ },
+ fetchRelatedIssues() {
+ this.isFetching = true;
+ this.service
+ .fetchRelatedIssues()
+ .then(({ data }) => {
+ this.store.setRelatedIssues(data);
+ })
+ .catch(() => {
+ this.store.setRelatedIssues([]);
+ Flash(__('An error occurred while fetching issues.'));
+ })
+ .finally(() => {
+ this.isFetching = false;
+ });
+ },
+ saveIssueOrder({ issueId, beforeId, afterId, oldIndex, newIndex }) {
+ const issueToReorder = this.findRelatedIssueById(issueId);
+
+ if (issueToReorder) {
+ RelatedIssuesService.saveOrder({
+ endpoint: issueToReorder.relationPath,
+ move_before_id: beforeId,
+ move_after_id: afterId,
+ })
+ .then(({ data }) => {
+ if (!data.message) {
+ this.store.updateIssueOrder(oldIndex, newIndex);
+ }
+ })
+ .catch(() => {
+ Flash(__('An error occurred while reordering issues.'));
+ });
+ }
+ },
+ onInput({ untouchedRawReferences, touchedReference }) {
+ this.store.addPendingReferences(untouchedRawReferences);
+
+ this.inputValue = `${touchedReference}`;
+ },
+ onBlur(newValue) {
+ this.processAllReferences(newValue);
+ },
+ processAllReferences(value = '') {
+ const rawReferences = value.split(/\s+/).filter(reference => reference.trim().length > 0);
+
+ this.store.addPendingReferences(rawReferences);
+ this.inputValue = '';
+ },
+ },
+};
+</script>
+
+<template>
+ <related-issues-block
+ :class="cssClass"
+ :help-path="helpPath"
+ :is-fetching="isFetching"
+ :is-submitting="isSubmitting"
+ :related-issues="state.relatedIssues"
+ :can-admin="canAdmin"
+ :can-reorder="canReorder"
+ :pending-references="state.pendingReferences"
+ :is-form-visible="isFormVisible"
+ :input-value="inputValue"
+ :auto-complete-sources="autoCompleteSources"
+ :issuable-type="issuableType"
+ :path-id-separator="pathIdSeparator"
+ :show-categorized-issues="showCategorizedIssues"
+ @saveReorder="saveIssueOrder"
+ @toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm"
+ @addIssuableFormInput="onInput"
+ @addIssuableFormBlur="onBlur"
+ @addIssuableFormSubmit="onPendingFormSubmit"
+ @addIssuableFormCancel="onPendingFormCancel"
+ @pendingIssuableRemoveRequest="onPendingIssueRemoveRequest"
+ @relatedIssueRemoveRequest="onRelatedIssueRemoveRequest"
+ />
+</template>
diff --git a/app/assets/javascripts/related_issues/constants.js b/app/assets/javascripts/related_issues/constants.js
new file mode 100644
index 00000000000..89eae069a24
--- /dev/null
+++ b/app/assets/javascripts/related_issues/constants.js
@@ -0,0 +1,106 @@
+import { __, sprintf } from '~/locale';
+
+export const issuableTypesMap = {
+ ISSUE: 'issue',
+ EPIC: 'epic',
+ MERGE_REQUEST: 'merge_request',
+};
+
+export const linkedIssueTypesMap = {
+ BLOCKS: 'blocks',
+ IS_BLOCKED_BY: 'is_blocked_by',
+ RELATES_TO: 'relates_to',
+};
+
+export const linkedIssueTypesTextMap = {
+ [linkedIssueTypesMap.RELATES_TO]: __('Relates to'),
+ [linkedIssueTypesMap.BLOCKS]: __('Blocks'),
+ [linkedIssueTypesMap.IS_BLOCKED_BY]: __('Is blocked by'),
+};
+
+export const autoCompleteTextMap = {
+ true: {
+ [issuableTypesMap.ISSUE]: sprintf(
+ __(' or %{emphasisStart}#issue id%{emphasisEnd}'),
+ { emphasisStart: '<', emphasisEnd: '>' },
+ false,
+ ),
+ [issuableTypesMap.EPIC]: sprintf(
+ __(' or %{emphasisStart}&epic id%{emphasisEnd}'),
+ { emphasisStart: '<', emphasisEnd: '>' },
+ false,
+ ),
+ [issuableTypesMap.MERGE_REQUEST]: sprintf(
+ __(' or %{emphasisStart}!merge request id%{emphasisEnd}'),
+ { emphasisStart: '<', emphasisEnd: '>' },
+ false,
+ ),
+ },
+ false: {
+ [issuableTypesMap.ISSUE]: '',
+ [issuableTypesMap.EPIC]: '',
+ [issuableTypesMap.MERGE_REQUEST]: __(' or references (e.g. path/to/project!merge_request_id)'),
+ },
+};
+
+export const inputPlaceholderTextMap = {
+ [issuableTypesMap.ISSUE]: __('Paste issue link'),
+ [issuableTypesMap.EPIC]: __('Paste epic link'),
+ [issuableTypesMap.MERGE_REQUEST]: __('Enter merge request URLs'),
+};
+
+export const inputPlaceholderConfidentialTextMap = {
+ [issuableTypesMap.ISSUE]: __('Paste confidential issue link'),
+ [issuableTypesMap.EPIC]: __('Paste confidential epic link'),
+ [issuableTypesMap.MERGE_REQUEST]: __('Enter merge request URLs'),
+};
+
+export const relatedIssuesRemoveErrorMap = {
+ [issuableTypesMap.ISSUE]: __('An error occurred while removing issues.'),
+ [issuableTypesMap.EPIC]: __('An error occurred while removing epics.'),
+};
+
+export const pathIndeterminateErrorMap = {
+ [issuableTypesMap.ISSUE]: __('We could not determine the path to remove the issue'),
+ [issuableTypesMap.EPIC]: __('We could not determine the path to remove the epic'),
+};
+
+export const itemAddFailureTypesMap = {
+ NOT_FOUND: 'not_found',
+ MAX_NUMBER_OF_CHILD_EPICS: 'conflict',
+};
+
+export const addRelatedIssueErrorMap = {
+ [issuableTypesMap.ISSUE]: __('Issue cannot be found.'),
+ [issuableTypesMap.EPIC]: __('Epic cannot be found.'),
+};
+
+export const addRelatedItemErrorMap = {
+ [itemAddFailureTypesMap.MAX_NUMBER_OF_CHILD_EPICS]: __(
+ 'This epic already has the maximum number of child epics.',
+ ),
+};
+
+/**
+ * These are used to map issuableType to the correct icon.
+ * Since these are never used for any display purposes, don't wrap
+ * them inside i18n functions.
+ */
+export const issuableIconMap = {
+ [issuableTypesMap.ISSUE]: 'issues',
+ [issuableTypesMap.EPIC]: 'epic',
+};
+
+/**
+ * These are used to map issuableType to the correct QA class.
+ * Since these are never used for any display purposes, don't wrap
+ * them inside i18n functions.
+ */
+export const issuableQaClassMap = {
+ [issuableTypesMap.EPIC]: 'qa-add-epics-button',
+};
+
+export const PathIdSeparator = {
+ Epic: '&',
+ Issue: '#',
+};
diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js
new file mode 100644
index 00000000000..2e8626890cb
--- /dev/null
+++ b/app/assets/javascripts/related_issues/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import RelatedIssuesRoot from './components/related_issues_root.vue';
+
+export default function initRelatedIssues() {
+ const relatedIssuesRootElement = document.querySelector('.js-related-issues-root');
+ if (relatedIssuesRootElement) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: relatedIssuesRootElement,
+ components: {
+ relatedIssuesRoot: RelatedIssuesRoot,
+ },
+ render: createElement =>
+ createElement('related-issues-root', {
+ props: {
+ endpoint: relatedIssuesRootElement.dataset.endpoint,
+ canAdmin: parseBoolean(relatedIssuesRootElement.dataset.canAddRelatedIssues),
+ helpPath: relatedIssuesRootElement.dataset.helpPath,
+ showCategorizedIssues: parseBoolean(
+ relatedIssuesRootElement.dataset.showCategorizedIssues,
+ ),
+ },
+ }),
+ });
+ }
+}
diff --git a/app/assets/javascripts/related_issues/services/related_issues_service.js b/app/assets/javascripts/related_issues/services/related_issues_service.js
new file mode 100644
index 00000000000..3c19f63157e
--- /dev/null
+++ b/app/assets/javascripts/related_issues/services/related_issues_service.js
@@ -0,0 +1,34 @@
+import axios from '~/lib/utils/axios_utils';
+import { linkedIssueTypesMap } from '../constants';
+
+class RelatedIssuesService {
+ constructor(endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ fetchRelatedIssues() {
+ return axios.get(this.endpoint);
+ }
+
+ addRelatedIssues(newIssueReferences, linkType = linkedIssueTypesMap.RELATES_TO) {
+ return axios.post(this.endpoint, {
+ issuable_references: newIssueReferences,
+ link_type: linkType,
+ });
+ }
+
+ static saveOrder({ endpoint, move_before_id, move_after_id }) {
+ return axios.put(endpoint, {
+ epic: {
+ move_before_id,
+ move_after_id,
+ },
+ });
+ }
+
+ static remove(endpoint) {
+ return axios.delete(endpoint);
+ }
+}
+
+export default RelatedIssuesService;
diff --git a/app/assets/javascripts/related_issues/stores/related_issues_store.js b/app/assets/javascripts/related_issues/stores/related_issues_store.js
new file mode 100644
index 00000000000..14d71628cad
--- /dev/null
+++ b/app/assets/javascripts/related_issues/stores/related_issues_store.js
@@ -0,0 +1,50 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+class RelatedIssuesStore {
+ constructor() {
+ this.state = {
+ // Stores issue objects of the known related issues
+ relatedIssues: [],
+ // Stores references of the "staging area" related issues that are planned to be added
+ pendingReferences: [],
+ };
+ }
+
+ setRelatedIssues(issues = []) {
+ this.state.relatedIssues = convertObjectPropsToCamelCase(issues, { deep: true });
+ }
+
+ addRelatedIssues(issues = []) {
+ this.setRelatedIssues(this.state.relatedIssues.concat(issues));
+ }
+
+ removeRelatedIssue(issue) {
+ this.state.relatedIssues = this.state.relatedIssues.filter(x => x.id !== issue.id);
+ }
+
+ updateIssueOrder(oldIndex, newIndex) {
+ if (this.state.relatedIssues.length > 0) {
+ const updatedIssue = this.state.relatedIssues.splice(oldIndex, 1)[0];
+ this.state.relatedIssues.splice(newIndex, 0, updatedIssue);
+ }
+ }
+
+ setPendingReferences(issues) {
+ // Remove duplicates but retain order.
+ // If you don't do this, Vue will be confused by duplicates and refuse to delete them all.
+ this.state.pendingReferences = issues.filter((ref, idx) => issues.indexOf(ref) === idx);
+ }
+
+ addPendingReferences(references = []) {
+ const issues = this.state.pendingReferences.concat(references);
+ this.setPendingReferences(issues);
+ }
+
+ removePendingRelatedIssue(indexToRemove) {
+ this.state.pendingReferences = this.state.pendingReferences.filter(
+ (reference, index) => index !== indexToRemove,
+ );
+ }
+}
+
+export default RelatedIssuesStore;
diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
index 15e9b8559d4..7db76ed576c 100644
--- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
@@ -1,15 +1,14 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { sprintf, n__, s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { parseIssuableData } from '../../issue_show/utils/parse_data';
export default {
name: 'RelatedMergeRequests',
components: {
- Icon,
+ GlIcon,
GlLink,
GlLoadingIcon,
RelatedIssuableItem,
@@ -85,7 +84,7 @@ export default {
<div class="mr-count-badge gl-display-inline-flex">
<div class="mr-count-badge-count">
<svg class="s16 mr-1 text-secondary">
- <icon name="merge-request" class="mr-1 text-secondary" />
+ <gl-icon name="merge-request" class="mr-1 text-secondary" />
</svg>
<span class="js-items-count">{{ totalCount }}</span>
</div>
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index 7b7c80a6269..e1edf3d689d 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
@@ -139,7 +140,7 @@ export default {
class="form-control"
/>
</gl-form-group>
- <gl-form-group class="w-50" @keydown.enter.prevent.capture>
+ <gl-form-group class="w-50" data-testid="milestones-field">
<label>{{ __('Milestones') }}</label>
<div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
<milestone-combobox
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 67085ecca2b..b8cf6ce478f 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,6 +1,11 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlSkeletonLoading, GlEmptyState, GlLink, GlButton } from '@gitlab/ui';
+import {
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlEmptyState,
+ GlLink,
+ GlButton,
+} from '@gitlab/ui';
import {
getParameterByName,
historyPushState,
@@ -20,27 +25,16 @@ export default {
GlLink,
GlButton,
},
- props: {
- projectId: {
- type: String,
- required: true,
- },
- documentationPath: {
- type: String,
- required: true,
- },
- illustrationPath: {
- type: String,
- required: true,
- },
- newReleasePath: {
- type: String,
- required: false,
- default: '',
- },
- },
computed: {
- ...mapState('list', ['isLoading', 'releases', 'hasError', 'pageInfo']),
+ ...mapState('list', [
+ 'documentationPath',
+ 'illustrationPath',
+ 'newReleasePath',
+ 'isLoading',
+ 'releases',
+ 'hasError',
+ 'pageInfo',
+ ]),
shouldRenderEmptyState() {
return !this.releases.length && !this.hasError && !this.isLoading;
},
@@ -56,14 +50,13 @@ export default {
created() {
this.fetchReleases({
page: getParameterByName('page'),
- projectId: this.projectId,
});
},
methods: {
...mapActions('list', ['fetchReleases']),
onChangePage(page) {
historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
- this.fetchReleases({ page, projectId: this.projectId });
+ this.fetchReleases({ page });
},
},
};
diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
index 0e65d722952..8b89f0cf3fc 100644
--- a/app/assets/javascripts/releases/components/app_show.vue
+++ b/app/assets/javascripts/releases/components/app_show.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import ReleaseBlock from './release_block.vue';
export default {
diff --git a/app/assets/javascripts/releases/components/evidence_block.vue b/app/assets/javascripts/releases/components/evidence_block.vue
index 6468e2ded62..3724162f6d5 100644
--- a/app/assets/javascripts/releases/components/evidence_block.vue
+++ b/app/assets/javascripts/releases/components/evidence_block.vue
@@ -80,9 +80,7 @@ export default {
<span class="js-short monospace">{{ shortSha(index) }}</span>
</template>
<template #expanded>
- <span class="js-expanded monospace gl-pl-1-deprecated-no-really-do-not-use-me">{{
- sha(index)
- }}</span>
+ <span class="js-expanded monospace gl-pl-2">{{ sha(index) }}</span>
</template>
</expand-button>
<clipboard-button
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index e0061d88ccb..2629df08be7 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { isEmpty } from 'lodash';
import $ from 'jquery';
import { slugify } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/releases/components/release_block_assets.vue b/app/assets/javascripts/releases/components/release_block_assets.vue
index 9583f5737df..8824cbefd7e 100644
--- a/app/assets/javascripts/releases/components/release_block_assets.vue
+++ b/app/assets/javascripts/releases/components/release_block_assets.vue
@@ -1,7 +1,6 @@
<script>
import { GlTooltipDirective, GlLink, GlButton, GlCollapse, GlIcon, GlBadge } from '@gitlab/ui';
import { difference, get } from 'lodash';
-import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ASSET_LINK_TYPE } from '../constants';
import { __, s__, sprintf } from '~/locale';
@@ -13,7 +12,6 @@ export default {
GlButton,
GlCollapse,
GlIcon,
- Icon,
GlBadge,
},
directives: {
@@ -157,7 +155,7 @@ export default {
<ul v-if="assets.links.length" class="pl-0 mb-0 gl-mt-3 list-unstyled js-assets-list">
<li v-for="link in assets.links" :key="link.name" class="gl-mb-3">
<gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.directAssetUrl">
- <icon name="package" class="align-middle gl-mr-2 align-text-bottom" />
+ <gl-icon name="package" class="align-middle gl-mr-2 align-text-bottom" />
{{ link.name }}
<span v-if="link.external" data-testid="external-link-indicator">{{
__('(external source)')
@@ -174,9 +172,9 @@ export default {
aria-haspopup="true"
aria-expanded="false"
>
- <icon name="doc-code" class="align-top gl-mr-2" />
+ <gl-icon name="doc-code" class="align-top gl-mr-2" />
{{ __('Source code') }}
- <icon name="chevron-down" />
+ <gl-icon name="chevron-down" />
</button>
<div class="js-sources-dropdown dropdown-menu">
diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue
index 26154272d39..3beec466c54 100644
--- a/app/assets/javascripts/releases/components/release_block_footer.vue
+++ b/app/assets/javascripts/releases/components/release_block_footer.vue
@@ -1,6 +1,5 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { __, sprintf } from '~/locale';
@@ -8,7 +7,7 @@ import { __, sprintf } from '~/locale';
export default {
name: 'ReleaseBlockFooter',
components: {
- Icon,
+ GlIcon,
GlLink,
UserAvatarLink,
},
@@ -68,7 +67,7 @@ export default {
<template>
<div>
<div v-if="commit" class="float-left mr-3 d-flex align-items-center js-commit-info">
- <icon ref="commitIcon" name="commit" class="mr-1" />
+ <gl-icon ref="commitIcon" name="commit" class="mr-1" />
<div v-gl-tooltip.bottom :title="commit.title">
<gl-link v-if="commitPath" :href="commitPath">
{{ commit.shortId }}
@@ -78,7 +77,7 @@ export default {
</div>
<div v-if="tagName" class="float-left mr-3 d-flex align-items-center js-tag-info">
- <icon name="tag" class="mr-1" />
+ <gl-icon name="tag" class="mr-1" />
<div v-gl-tooltip.bottom :title="__('Tag')">
<gl-link v-if="tagPath" :href="tagPath">
{{ tagName }}
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index 310fba0fe76..95292a26bce 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -1,6 +1,5 @@
<script>
-import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlLink, GlBadge, GlButton, GlIcon } from '@gitlab/ui';
import { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
@@ -9,7 +8,7 @@ export default {
components: {
GlLink,
GlBadge,
- Icon,
+ GlIcon,
GlButton,
},
directives: {
@@ -42,7 +41,7 @@ export default {
<template>
<div class="card-header d-flex align-items-center bg-white pr-0">
- <h2 class="card-title my-2 mr-auto gl-font-size-20-deprecated-no-really-do-not-use-me">
+ <h2 class="card-title my-2 mr-auto">
<gl-link v-if="selfLink" :href="selfLink" class="font-size-inherit">
{{ release.name }}
</gl-link>
@@ -60,7 +59,7 @@ export default {
:title="__('Edit this release')"
:href="editLink"
>
- <icon name="pencil" />
+ <gl-icon name="pencil" />
</gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/releases/components/release_block_metadata.vue b/app/assets/javascripts/releases/components/release_block_metadata.vue
index 861c2e11798..2247b4c0064 100644
--- a/app/assets/javascripts/releases/components/release_block_metadata.vue
+++ b/app/assets/javascripts/releases/components/release_block_metadata.vue
@@ -1,7 +1,6 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import ReleaseBlockAuthor from './release_block_author.vue';
import ReleaseBlockMilestones from './release_block_milestones.vue';
@@ -9,7 +8,7 @@ import ReleaseBlockMilestones from './release_block_milestones.vue';
export default {
name: 'ReleaseBlockMetadata',
components: {
- Icon,
+ GlIcon,
GlLink,
ReleaseBlockAuthor,
ReleaseBlockMilestones,
@@ -58,7 +57,7 @@ export default {
<template>
<div class="card-subtitle d-flex flex-wrap text-secondary">
<div class="gl-mr-3">
- <icon name="commit" class="align-middle" />
+ <gl-icon name="commit" class="align-middle" />
<gl-link v-if="commitUrl" v-gl-tooltip.bottom :title="commit.title" :href="commitUrl">
{{ commit.shortId }}
</gl-link>
@@ -66,7 +65,7 @@ export default {
</div>
<div class="gl-mr-3">
- <icon name="tag" class="align-middle" />
+ <gl-icon name="tag" class="align-middle" />
<gl-link v-if="tagUrl" v-gl-tooltip.bottom :title="__('Tag')" :href="tagUrl">
{{ release.tagName }}
</gl-link>
diff --git a/app/assets/javascripts/releases/components/release_block_milestones.vue b/app/assets/javascripts/releases/components/release_block_milestones.vue
index 9abd3345b22..1da683764b3 100644
--- a/app/assets/javascripts/releases/components/release_block_milestones.vue
+++ b/app/assets/javascripts/releases/components/release_block_milestones.vue
@@ -1,13 +1,12 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'ReleaseBlockMilestones',
components: {
GlLink,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -29,7 +28,7 @@ export default {
<template>
<div>
<div class="js-milestone-list-label">
- <icon name="flag" class="align-middle" />
+ <gl-icon name="flag" class="align-middle" />
<span class="js-label-text">{{ labelText }}</span>
</div>
diff --git a/app/assets/javascripts/releases/components/releases_pagination.vue b/app/assets/javascripts/releases/components/releases_pagination.vue
new file mode 100644
index 00000000000..062c72b445b
--- /dev/null
+++ b/app/assets/javascripts/releases/components/releases_pagination.vue
@@ -0,0 +1,20 @@
+<script>
+import { mapGetters } from 'vuex';
+import ReleasesPaginationGraphql from './releases_pagination_graphql.vue';
+import ReleasesPaginationRest from './releases_pagination_rest.vue';
+
+export default {
+ name: 'ReleasesPagination',
+ components: { ReleasesPaginationGraphql, ReleasesPaginationRest },
+ computed: {
+ ...mapGetters(['useGraphQLEndpoint']),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-center">
+ <releases-pagination-graphql v-if="useGraphQLEndpoint" />
+ <releases-pagination-rest v-else />
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
new file mode 100644
index 00000000000..a4fe407a5bd
--- /dev/null
+++ b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
@@ -0,0 +1,35 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import { GlKeysetPagination } from '@gitlab/ui';
+import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
+
+export default {
+ name: 'ReleasesPaginationGraphql',
+ components: { GlKeysetPagination },
+ computed: {
+ ...mapState('list', ['graphQlPageInfo']),
+ showPagination() {
+ return this.graphQlPageInfo.hasPreviousPage || this.graphQlPageInfo.hasNextPage;
+ },
+ },
+ methods: {
+ ...mapActions('list', ['fetchReleasesGraphQl']),
+ onPrev(before) {
+ historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
+ this.fetchReleasesGraphQl({ before });
+ },
+ onNext(after) {
+ historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
+ this.fetchReleasesGraphQl({ after });
+ },
+ },
+};
+</script>
+<template>
+ <gl-keyset-pagination
+ v-if="showPagination"
+ v-bind="graphQlPageInfo"
+ @prev="onPrev($event)"
+ @next="onNext($event)"
+ />
+</template>
diff --git a/app/assets/javascripts/releases/components/releases_pagination_rest.vue b/app/assets/javascripts/releases/components/releases_pagination_rest.vue
new file mode 100644
index 00000000000..992cc4cd469
--- /dev/null
+++ b/app/assets/javascripts/releases/components/releases_pagination_rest.vue
@@ -0,0 +1,24 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
+
+export default {
+ name: 'ReleasesPaginationRest',
+ components: { TablePagination },
+ computed: {
+ ...mapState('list', ['pageInfo']),
+ },
+ methods: {
+ ...mapActions('list', ['fetchReleasesRest']),
+ onChangePage(page) {
+ historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
+ this.fetchReleasesRest({ page });
+ },
+ },
+};
+</script>
+
+<template>
+ <table-pagination :change="onChangePage" :page-info="pageInfo" />
+</template>
diff --git a/app/assets/javascripts/releases/mount_edit.js b/app/assets/javascripts/releases/mount_edit.js
index c7385b3c57f..623b18591a0 100644
--- a/app/assets/javascripts/releases/mount_edit.js
+++ b/app/assets/javascripts/releases/mount_edit.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
+import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
+Vue.use(Vuex);
+
export default () => {
const el = document.getElementById('js-edit-release-page');
diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js
index 5f0bf3b6459..cd4fa5c5df5 100644
--- a/app/assets/javascripts/releases/mount_index.js
+++ b/app/assets/javascripts/releases/mount_index.js
@@ -1,7 +1,10 @@
import Vue from 'vue';
+import Vuex from 'vuex';
import ReleaseListApp from './components/app_index.vue';
import createStore from './stores';
-import listModule from './stores/modules/list';
+import createListModule from './stores/modules/list';
+
+Vue.use(Vuex);
export default () => {
const el = document.getElementById('js-releases-page');
@@ -10,12 +13,14 @@ export default () => {
el,
store: createStore({
modules: {
- list: listModule,
+ list: createListModule(el.dataset),
+ },
+ featureFlags: {
+ graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData),
+ graphqlReleasesPage: Boolean(gon.features?.graphqlReleasesPage),
+ graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats),
},
}),
- render: h =>
- h(ReleaseListApp, {
- props: el.dataset,
- }),
+ render: h => h(ReleaseListApp),
});
};
diff --git a/app/assets/javascripts/releases/mount_new.js b/app/assets/javascripts/releases/mount_new.js
index 68003f6a346..10725e47740 100644
--- a/app/assets/javascripts/releases/mount_new.js
+++ b/app/assets/javascripts/releases/mount_new.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
+import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
+Vue.use(Vuex);
+
export default () => {
const el = document.getElementById('js-new-release-page');
diff --git a/app/assets/javascripts/releases/mount_show.js b/app/assets/javascripts/releases/mount_show.js
index 7ddc8e786c1..eef015ee0a6 100644
--- a/app/assets/javascripts/releases/mount_show.js
+++ b/app/assets/javascripts/releases/mount_show.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
+import Vuex from 'vuex';
import ReleaseShowApp from './components/app_show.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
+Vue.use(Vuex);
+
export default () => {
const el = document.getElementById('js-show-release-page');
diff --git a/app/assets/javascripts/releases/queries/all_releases.query.graphql b/app/assets/javascripts/releases/queries/all_releases.query.graphql
new file mode 100644
index 00000000000..7a99f32fdfa
--- /dev/null
+++ b/app/assets/javascripts/releases/queries/all_releases.query.graphql
@@ -0,0 +1,69 @@
+query allReleases($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ releases(first: 20) {
+ count
+ nodes {
+ name
+ tagName
+ tagPath
+ descriptionHtml
+ releasedAt
+ upcomingRelease
+ assets {
+ count
+ sources {
+ nodes {
+ format
+ url
+ }
+ }
+ links {
+ nodes {
+ id
+ name
+ url
+ directAssetUrl
+ linkType
+ external
+ }
+ }
+ }
+ evidences {
+ nodes {
+ filepath
+ collectedAt
+ sha
+ }
+ }
+ links {
+ editUrl
+ issuesUrl
+ mergeRequestsUrl
+ selfUrl
+ }
+ commit {
+ sha
+ webUrl
+ title
+ }
+ author {
+ webUrl
+ avatarUrl
+ username
+ }
+ milestones {
+ nodes {
+ id
+ title
+ description
+ webPath
+ stats {
+ totalIssuesCount
+ closedIssuesCount
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/releases/stores/index.js b/app/assets/javascripts/releases/stores/index.js
index 7f211145ccf..b2e93d789d7 100644
--- a/app/assets/javascripts/releases/stores/index.js
+++ b/app/assets/javascripts/releases/stores/index.js
@@ -1,8 +1,5 @@
-import Vue from 'vue';
import Vuex from 'vuex';
-Vue.use(Vuex);
-
export default ({ modules, featureFlags }) =>
new Vuex.Store({
modules,
diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js
index 90fba319e9f..945b093b983 100644
--- a/app/assets/javascripts/releases/stores/modules/list/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/list/actions.js
@@ -7,6 +7,8 @@ import {
parseIntPagination,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
+import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
+import { gqClient, convertGraphQLResponse } from '../../../util';
/**
* Commits a mutation to update the state while the main endpoint is being requested.
@@ -21,13 +23,31 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
*
* @param {String} projectId
*/
-export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => {
+export const fetchReleases = ({ dispatch, rootState, state }, { page = '1' }) => {
dispatch('requestReleases');
- api
- .releases(projectId, { page })
- .then(response => dispatch('receiveReleasesSuccess', response))
- .catch(() => dispatch('receiveReleasesError'));
+ if (
+ rootState.featureFlags.graphqlReleaseData &&
+ rootState.featureFlags.graphqlReleasesPage &&
+ rootState.featureFlags.graphqlMilestoneStats
+ ) {
+ gqClient
+ .query({
+ query: allReleasesQuery,
+ variables: {
+ fullPath: state.projectPath,
+ },
+ })
+ .then(response => {
+ dispatch('receiveReleasesSuccess', convertGraphQLResponse(response));
+ })
+ .catch(() => dispatch('receiveReleasesError'));
+ } else {
+ api
+ .releases(state.projectId, { page })
+ .then(response => dispatch('receiveReleasesSuccess', response))
+ .catch(() => dispatch('receiveReleasesError'));
+ }
};
export const receiveReleasesSuccess = ({ commit }, { data, headers }) => {
diff --git a/app/assets/javascripts/releases/stores/modules/list/index.js b/app/assets/javascripts/releases/stores/modules/list/index.js
index e4633b15a0c..0f97fa83ced 100644
--- a/app/assets/javascripts/releases/stores/modules/list/index.js
+++ b/app/assets/javascripts/releases/stores/modules/list/index.js
@@ -1,10 +1,10 @@
-import state from './state';
+import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
-export default {
+export default initialState => ({
namespaced: true,
actions,
mutations,
- state,
-};
+ state: createState(initialState),
+});
diff --git a/app/assets/javascripts/releases/stores/modules/list/state.js b/app/assets/javascripts/releases/stores/modules/list/state.js
index c251f56c9c5..9fe313745fc 100644
--- a/app/assets/javascripts/releases/stores/modules/list/state.js
+++ b/app/assets/javascripts/releases/stores/modules/list/state.js
@@ -1,4 +1,16 @@
-export default () => ({
+export default ({
+ projectId,
+ projectPath,
+ documentationPath,
+ illustrationPath,
+ newReleasePath = '',
+}) => ({
+ projectId,
+ projectPath,
+ documentationPath,
+ illustrationPath,
+ newReleasePath,
+
isLoading: false,
hasError: false,
releases: [],
diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js
index 842a423b142..d7fac7a9b65 100644
--- a/app/assets/javascripts/releases/util.js
+++ b/app/assets/javascripts/releases/util.js
@@ -1,3 +1,6 @@
+import { pick } from 'lodash';
+import createGqClient, { fetchPolicies } from '~/lib/graphql';
+import { truncateSha } from '~/lib/utils/text_utility';
import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
@@ -39,3 +42,89 @@ export const apiJsonToRelease = json => {
return release;
};
+
+export const gqClient = createGqClient({}, { fetchPolicy: fetchPolicies.NO_CACHE });
+
+const convertScalarProperties = graphQLRelease =>
+ pick(graphQLRelease, [
+ 'name',
+ 'tagName',
+ 'tagPath',
+ 'descriptionHtml',
+ 'releasedAt',
+ 'upcomingRelease',
+ ]);
+
+const convertAssets = graphQLRelease => ({
+ assets: {
+ count: graphQLRelease.assets.count,
+ sources: [...graphQLRelease.assets.sources.nodes],
+ links: graphQLRelease.assets.links.nodes.map(l => ({
+ ...l,
+ linkType: l.linkType?.toLowerCase(),
+ })),
+ },
+});
+
+const convertEvidences = graphQLRelease => ({
+ evidences: graphQLRelease.evidences.nodes.map(e => e),
+});
+
+const convertLinks = graphQLRelease => ({
+ _links: {
+ ...graphQLRelease.links,
+ self: graphQLRelease.links?.selfUrl,
+ },
+});
+
+const convertCommit = graphQLRelease => {
+ if (!graphQLRelease.commit) {
+ return {};
+ }
+
+ return {
+ commit: {
+ shortId: truncateSha(graphQLRelease.commit.sha),
+ title: graphQLRelease.commit.title,
+ },
+ commitPath: graphQLRelease.commit.webUrl,
+ };
+};
+
+const convertAuthor = graphQLRelease => ({ author: graphQLRelease.author });
+
+const convertMilestones = graphQLRelease => ({
+ milestones: graphQLRelease.milestones.nodes.map(m => ({
+ ...m,
+ webUrl: m.webPath,
+ webPath: undefined,
+ issueStats: {
+ total: m.stats.totalIssuesCount,
+ closed: m.stats.closedIssuesCount,
+ },
+ stats: undefined,
+ })),
+});
+
+/**
+ * Converts the response from the GraphQL endpoint into the
+ * same shape as is returned from the Releases REST API.
+ *
+ * This allows the release components to use the response
+ * from either endpoint interchangeably.
+ *
+ * @param response The response received from the GraphQL endpoint
+ */
+export const convertGraphQLResponse = response => {
+ const releases = response.data.project.releases.nodes.map(r => ({
+ ...convertScalarProperties(r),
+ ...convertAssets(r),
+ ...convertEvidences(r),
+ ...convertLinks(r),
+ ...convertCommit(r),
+ ...convertAuthor(r),
+ ...convertMilestones(r),
+ }));
+
+ return { data: releases };
+};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/index.js b/app/assets/javascripts/reports/accessibility_report/store/index.js
index 047964260ad..9fa2c589324 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/index.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/index.js
@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex);
-export default initialState =>
- new Vuex.Store({
- actions,
- getters,
- mutations,
- state: state(initialState),
- });
+export const getStoreConfig = initialState => ({
+ actions,
+ getters,
+ mutations,
+ state: state(initialState),
+});
+
+export default initialState => new Vuex.Store(getStoreConfig(initialState));
diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/reports/codequality_report/store/index.js
index 047964260ad..9fa2c589324 100644
--- a/app/assets/javascripts/reports/codequality_report/store/index.js
+++ b/app/assets/javascripts/reports/codequality_report/store/index.js
@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex);
-export default initialState =>
- new Vuex.Store({
- actions,
- getters,
- mutations,
- state: state(initialState),
- });
+export const getStoreConfig = initialState => ({
+ actions,
+ getters,
+ mutations,
+ state: state(initialState),
+});
+
+export default initialState => new Vuex.Store(getStoreConfig(initialState));
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue
index d79e3ddd798..bd41b8d23f1 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/reports/components/issue_status_icon.vue
@@ -1,11 +1,11 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '../constants';
export default {
name: 'IssueStatusIcon',
components: {
- Icon,
+ GlIcon,
},
props: {
status: {
@@ -49,6 +49,6 @@ export default {
}"
class="report-block-list-icon"
>
- <icon :name="iconName" :size="statusIconSize" :data-qa-selector="`status_${status}_icon`" />
+ <gl-icon :name="iconName" :size="statusIconSize" :data-qa-selector="`status_${status}_icon`" />
</div>
</template>
diff --git a/app/assets/javascripts/reports/store/getters.js b/app/assets/javascripts/reports/store/getters.js
index 6345be69f6f..d49e5760b3f 100644
--- a/app/assets/javascripts/reports/store/getters.js
+++ b/app/assets/javascripts/reports/store/getters.js
@@ -1,6 +1,5 @@
import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../constants';
-// eslint-disable-next-line import/prefer-default-export
export const summaryStatus = state => {
if (state.isLoading) {
return LOADING;
diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js
index 467c692b438..a2edfa94a48 100644
--- a/app/assets/javascripts/reports/store/index.js
+++ b/app/assets/javascripts/reports/store/index.js
@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex);
-export default () =>
- new Vuex.Store({
- actions,
- mutations,
- getters,
- state: state(),
- });
+export const getStoreConfig = () => ({
+ actions,
+ mutations,
+ getters,
+ state: state(),
+});
+
+export default () => new Vuex.Store(getStoreConfig());
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 368fa029d07..74437f286b4 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -4,10 +4,10 @@ import {
GlDeprecatedDropdownDivider,
GlDeprecatedDropdownHeader,
GlDeprecatedDropdownItem,
+ GlIcon,
} from '@gitlab/ui';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '../../locale';
-import Icon from '../../vue_shared/components/icon.vue';
import getRefMixin from '../mixins/get_ref';
import projectShortPathQuery from '../queries/project_short_path.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
@@ -24,7 +24,7 @@ export default {
GlDeprecatedDropdownDivider,
GlDeprecatedDropdownHeader,
GlDeprecatedDropdownItem,
- Icon,
+ GlIcon,
},
apollo: {
projectShortPath: {
@@ -249,8 +249,8 @@ export default {
<gl-deprecated-dropdown toggle-class="add-to-tree qa-add-to-tree ml-1">
<template #button-content>
<span class="sr-only">{{ __('Add to tree') }}</span>
- <icon name="plus" :size="16" class="float-left" />
- <icon name="chevron-down" :size="16" class="float-left" />
+ <gl-icon name="plus" :size="16" class="float-left" />
+ <gl-icon name="chevron-down" :size="16" class="float-left" />
</template>
<template v-for="(item, i) in dropdownItems">
<component :is="getComponent(item.type)" :key="i" v-bind="item.attrs">
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 3337ce6c6df..59831890a4e 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,8 +1,8 @@
<script>
-import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { sprintf, s__ } from '~/locale';
-import Icon from '../../vue_shared/components/icon.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
@@ -13,7 +13,7 @@ import pathLastCommitQuery from '../queries/path_last_commit.query.graphql';
export default {
components: {
- Icon,
+ GlIcon,
UserAvatarLink,
TimeagoTooltip,
ClipboardButton,
@@ -130,7 +130,7 @@ export default {
class="text-expander"
@click="toggleShowDescription"
>
- <icon name="ellipsis_h" :size="10" />
+ <gl-icon name="ellipsis_h" :size="10" />
</gl-deprecated-button>
<div class="committer">
<gl-link
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index 013092ffefd..eca53f73a7f 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { GlLink, GlLoadingIcon } from '@gitlab/ui';
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index d0cc617d755..c6652c57c1f 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
import getRefMixin from '../../mixins/get_ref';
import projectPathQuery from '../../queries/project_path.query.graphql';
@@ -13,6 +13,7 @@ export default {
TableHeader,
TableRow,
ParentRow,
+ GlButton,
},
mixins: [getRefMixin],
apollo: {
@@ -39,6 +40,10 @@ export default {
required: false,
default: '',
},
+ hasMore: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -65,6 +70,11 @@ export default {
return !this.isLoading && ['', '/'].indexOf(this.path) === -1;
},
},
+ methods: {
+ showMore() {
+ this.$emit('showMore');
+ },
+ },
};
</script>
@@ -110,6 +120,20 @@ export default {
<td><gl-skeleton-loading :lines="1" class="ml-auto h-auto w-50" /></td>
</tr>
</template>
+ <template v-if="hasMore">
+ <tr>
+ <td align="center" colspan="3" class="gl-p-0!">
+ <gl-button
+ variant="link"
+ class="gl-display-flex gl-w-full gl-py-4!"
+ :loading="isLoading"
+ @click="showMore"
+ >
+ {{ s__('ProjectFileTree|Show more') }}
+ </gl-button>
+ </td>
+ </tr>
+ </template>
</tbody>
</table>
</div>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index d2fef6693e2..d749a8c0dee 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,9 +1,10 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escapeRegExp } from 'lodash';
import {
GlBadge,
GlLink,
- GlSkeletonLoading,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlTooltipDirective,
GlLoadingIcon,
GlIcon,
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index fe3065a2145..365b6cbb550 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,5 +1,4 @@
<script>
-import { GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '../../locale';
import FileTable from './table/index.vue';
@@ -17,7 +16,6 @@ export default {
components: {
FileTable,
FilePreview,
- GlButton,
},
mixins: [getRefMixin],
apollo: {
@@ -127,7 +125,7 @@ export default {
.concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo)
.find(({ hasNextPage }) => hasNextPage);
},
- showMore() {
+ handleShowMore() {
this.clickedShowMore = true;
this.fetchFiles();
},
@@ -142,20 +140,9 @@ export default {
:entries="entries"
:is-loading="isLoadingFiles"
:loading-path="loadingPath"
+ :has-more="hasShowMore"
+ @showMore="handleShowMore"
/>
- <div
- v-if="hasShowMore"
- class="gl-border-1 gl-border-gray-100 gl-rounded-base gl-border-t-none gl-border-b-solid gl-border-l-solid gl-border-r-solid gl-rounded-top-right-none gl-rounded-top-left-none gl-mt-n1"
- >
- <gl-button
- variant="link"
- class="gl-display-flex gl-w-full gl-py-4!"
- :loading="isLoadingFiles"
- @click="showMore"
- >
- {{ s__('ProjectFileTree|Show more') }}
- </gl-button>
- </div>
<file-preview v-if="readme" :blob="readme" />
</div>
</template>
diff --git a/app/assets/javascripts/repository/components/web_ide_link.vue b/app/assets/javascripts/repository/components/web_ide_link.vue
deleted file mode 100644
index 6549d5a3878..00000000000
--- a/app/assets/javascripts/repository/components/web_ide_link.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import TreeActionLink from './tree_action_link.vue';
-import { __ } from '~/locale';
-import { webIDEUrl } from '~/lib/utils/url_utility';
-
-export default {
- components: {
- TreeActionLink,
- },
- props: {
- projectPath: {
- type: String,
- required: true,
- },
- refSha: {
- type: String,
- required: true,
- },
- canPushCode: {
- type: Boolean,
- required: false,
- default: true,
- },
- forkPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- showLinkToFork() {
- return !this.canPushCode && this.forkPath;
- },
- text() {
- return this.showLinkToFork ? __('Edit fork in Web IDE') : __('Web IDE');
- },
- path() {
- const path = this.showLinkToFork ? this.forkPath : this.projectPath;
- return webIDEUrl(`/${path}/edit/${this.refSha}/-/${this.$route.params.path || ''}`);
- },
- },
-};
-</script>
-
-<template>
- <tree-action-link :path="path" :text="text" data-qa-selector="web_ide_button" />
-</template>
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index 450a1571165..8dd18027945 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -58,6 +58,7 @@ const defaultClient = createDefaultClient(
/* eslint-enable @gitlab/require-i18n-strings */
},
},
+ assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 187bbfed125..7f72524b6fe 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -1,30 +1,22 @@
import Vue from 'vue';
-import { escapeFileUrl } from '../lib/utils/url_utility';
+import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility';
import createRouter from './router';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import LastCommit from './components/last_commit.vue';
import TreeActionLink from './components/tree_action_link.vue';
-import WebIdeLink from './components/web_ide_link.vue';
+import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
import { updateFormAction } from './utils/dom';
-import { parseBoolean } from '../lib/utils/common_utils';
+import { convertObjectPropsToCamelCase, parseBoolean } from '../lib/utils/common_utils';
import { __ } from '../locale';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
const { dataset } = el;
- const {
- canPushCode,
- projectPath,
- projectShortPath,
- forkPath,
- ref,
- escapedRef,
- fullName,
- } = dataset;
+ const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
const router = createRouter(projectPath, escapedRef);
apolloProvider.clients.defaultClient.cache.writeData({
@@ -121,6 +113,10 @@ export default function setupVueRepositoryList() {
const webIdeLinkEl = document.getElementById('js-tree-web-ide-link');
if (webIdeLinkEl) {
+ const { ideBasePath, ...options } = convertObjectPropsToCamelCase(
+ JSON.parse(webIdeLinkEl.dataset.options),
+ );
+
// eslint-disable-next-line no-new
new Vue({
el: webIdeLinkEl,
@@ -128,10 +124,10 @@ export default function setupVueRepositoryList() {
render(h) {
return h(WebIdeLink, {
props: {
- projectPath,
- refSha: ref,
- forkPath,
- canPushCode: parseBoolean(canPushCode),
+ webIdeUrl: webIDEUrl(
+ joinPaths('/', ideBasePath, 'edit', ref, '-', this.$route.params.path || '', '/'),
+ ),
+ ...options,
},
});
},
diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js
index 704dd88aabe..361e0b62bb7 100644
--- a/app/assets/javascripts/repository/log_tree.js
+++ b/app/assets/javascripts/repository/log_tree.js
@@ -1,3 +1,4 @@
+import produce from 'immer';
import { normalizeData } from 'ee_else_ce/repository/utils/commit';
import axios from '~/lib/utils/axios_utils';
import commitsQuery from './queries/commits.query.graphql';
@@ -34,16 +35,18 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
params: { format: 'json', offset },
},
)
- .then(({ data, headers }) => {
+ .then(({ data: newData, headers }) => {
const headerLogsOffset = headers['more-logs-offset'];
- const { commits } = client.readQuery({ query: commitsQuery });
- const newCommitData = [...commits, ...normalizeData(data, path)];
+ const sourceData = client.readQuery({ query: commitsQuery });
+ const data = produce(sourceData, draftState => {
+ draftState.commits.push(...normalizeData(newData, path));
+ });
client.writeQuery({
query: commitsQuery,
- data: { commits: newCommitData },
+ data,
});
- resolvers.forEach(r => resolveCommit(newCommitData, path, r));
+ resolvers.forEach(r => resolveCommit(data.commits, path, r));
fetchpromise = null;
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index c5646c32850..38a596e229e 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
+import { escapeRegExp } from 'lodash';
import { joinPaths } from '../lib/utils/url_utility';
import IndexPage from './pages/index.vue';
import TreePage from './pages/tree.vue';
@@ -27,7 +28,7 @@ export default function createRouter(base, baseRef) {
{
name: 'treePath',
// Support without decoding as well just in case the ref doesn't need to be decoded
- path: `(/-)?/tree/${baseRef}/:path*`,
+ path: `(/-)?/tree/${escapeRegExp(baseRef)}/:path*`,
...treePathRoute,
},
{
diff --git a/app/assets/javascripts/repository/utils/commit.js b/app/assets/javascripts/repository/utils/commit.js
index 90ac01c5874..0704ac1627f 100644
--- a/app/assets/javascripts/repository/utils/commit.js
+++ b/app/assets/javascripts/repository/utils/commit.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export function normalizeData(data, path, extra = () => {}) {
return data.map(d => ({
sha: d.commit.id,
diff --git a/app/assets/javascripts/repository/utils/icon.js b/app/assets/javascripts/repository/utils/icon.js
index 661ebb6edfc..47b045c7eaf 100644
--- a/app/assets/javascripts/repository/utils/icon.js
+++ b/app/assets/javascripts/repository/utils/icon.js
@@ -88,7 +88,6 @@ const fileTypeIcons = [
},
];
-// eslint-disable-next-line import/prefer-default-export
export const getIconName = (type, path) => {
if (entryTypeIcons[type]) return entryTypeIcons[type];
diff --git a/app/assets/javascripts/repository/utils/readme.js b/app/assets/javascripts/repository/utils/readme.js
index 5b62271b02e..50692779b1a 100644
--- a/app/assets/javascripts/repository/utils/readme.js
+++ b/app/assets/javascripts/repository/utils/readme.js
@@ -28,5 +28,4 @@ const isPlainReadme = file => {
return re.test(file.name);
};
-// eslint-disable-next-line import/prefer-default-export
export const readmeFile = blobs => blobs.find(isRichReadme) || blobs.find(isPlainReadme);
diff --git a/app/assets/javascripts/search/state_filter/components/state_filter.vue b/app/assets/javascripts/search/state_filter/components/state_filter.vue
new file mode 100644
index 00000000000..f08adaf8c83
--- /dev/null
+++ b/app/assets/javascripts/search/state_filter/components/state_filter.vue
@@ -0,0 +1,94 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+import {
+ FILTER_STATES,
+ SCOPES,
+ FILTER_STATES_BY_SCOPE,
+ FILTER_HEADER,
+ FILTER_TEXT,
+} from '../constants';
+import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
+
+const FILTERS_ARRAY = Object.values(FILTER_STATES);
+
+export default {
+ name: 'StateFilter',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ },
+ props: {
+ scope: {
+ type: String,
+ required: true,
+ },
+ state: {
+ type: String,
+ required: false,
+ default: FILTER_STATES.ANY.value,
+ validator: v => FILTERS_ARRAY.some(({ value }) => value === v),
+ },
+ },
+ computed: {
+ selectedFilterText() {
+ const filter = FILTERS_ARRAY.find(({ value }) => value === this.selectedFilter);
+ if (!filter || filter === FILTER_STATES.ANY) {
+ return FILTER_TEXT;
+ }
+
+ return filter.label;
+ },
+ showDropdown() {
+ return Object.values(SCOPES).includes(this.scope);
+ },
+ selectedFilter: {
+ get() {
+ if (FILTERS_ARRAY.some(({ value }) => value === this.state)) {
+ return this.state;
+ }
+
+ return FILTER_STATES.ANY.value;
+ },
+ set(state) {
+ visitUrl(setUrlParams({ state }));
+ },
+ },
+ },
+ methods: {
+ dropDownItemClass(filter) {
+ return {
+ 'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
+ filter === FILTER_STATES.ANY,
+ };
+ },
+ isFilterSelected(filter) {
+ return filter === this.selectedFilter;
+ },
+ handleFilterChange(state) {
+ this.selectedFilter = state;
+ },
+ },
+ filterStates: FILTER_STATES,
+ filterHeader: FILTER_HEADER,
+ filtersByScope: FILTER_STATES_BY_SCOPE,
+};
+</script>
+
+<template>
+ <gl-dropdown v-if="showDropdown" :text="selectedFilterText" class="col-sm-3 gl-pt-4 gl-pl-0">
+ <header class="gl-text-center gl-font-weight-bold gl-font-lg">
+ {{ $options.filterHeader }}
+ </header>
+ <gl-dropdown-divider />
+ <gl-dropdown-item
+ v-for="filter in $options.filtersByScope[scope]"
+ :key="filter.value"
+ :is-check-item="true"
+ :is-checked="isFilterSelected(filter.value)"
+ :class="dropDownItemClass(filter)"
+ @click="handleFilterChange(filter.value)"
+ >{{ filter.label }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/search/state_filter/constants.js b/app/assets/javascripts/search/state_filter/constants.js
new file mode 100644
index 00000000000..2f11cab9044
--- /dev/null
+++ b/app/assets/javascripts/search/state_filter/constants.js
@@ -0,0 +1,39 @@
+import { __ } from '~/locale';
+
+export const FILTER_HEADER = __('Status');
+
+export const FILTER_TEXT = __('Any Status');
+
+export const FILTER_STATES = {
+ ANY: {
+ label: __('Any'),
+ value: 'all',
+ },
+ OPEN: {
+ label: __('Open'),
+ value: 'opened',
+ },
+ CLOSED: {
+ label: __('Closed'),
+ value: 'closed',
+ },
+ MERGED: {
+ label: __('Merged'),
+ value: 'merged',
+ },
+};
+
+export const SCOPES = {
+ ISSUES: 'issues',
+ MERGE_REQUESTS: 'merge_requests',
+};
+
+export const FILTER_STATES_BY_SCOPE = {
+ [SCOPES.ISSUES]: [FILTER_STATES.ANY, FILTER_STATES.OPEN, FILTER_STATES.CLOSED],
+ [SCOPES.MERGE_REQUESTS]: [
+ FILTER_STATES.ANY,
+ FILTER_STATES.OPEN,
+ FILTER_STATES.MERGED,
+ FILTER_STATES.CLOSED,
+ ],
+};
diff --git a/app/assets/javascripts/search/state_filter/index.js b/app/assets/javascripts/search/state_filter/index.js
new file mode 100644
index 00000000000..13708574cfb
--- /dev/null
+++ b/app/assets/javascripts/search/state_filter/index.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import StateFilter from './components/state_filter.vue';
+
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-search-filter-by-state');
+
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ components: {
+ StateFilter,
+ },
+ data() {
+ const { dataset } = this.$options.el;
+ return {
+ scope: dataset.scope,
+ state: dataset.state,
+ };
+ },
+
+ render(createElement) {
+ return createElement('state-filter', {
+ props: {
+ scope: this.scope,
+ state: this.state,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 990c8faf253..7073b9ca12d 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -13,6 +13,7 @@ import {
spriteIcon,
} from './lib/utils/common_utils';
import Tracking from '~/tracking';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
/**
* Search input in top navigation bar.
@@ -119,7 +120,7 @@ export class SearchAutocomplete {
}
createAutocomplete() {
- return this.searchInput.glDropdown({
+ return initDeprecatedJQueryDropdown(this.searchInput, {
filterInputBlur: false,
filterable: true,
filterRemote: true,
@@ -134,6 +135,7 @@ export class SearchAutocomplete {
data: this.getData.bind(this),
selectable: true,
clicked: this.onClick.bind(this),
+ trackSuggestionClickedLabel: 'search_autocomplete_suggestion',
});
}
@@ -145,10 +147,10 @@ export class SearchAutocomplete {
if (!term) {
const contents = this.getCategoryContents();
if (contents) {
- const glDropdownInstance = this.searchInput.data('glDropdown');
+ const deprecatedJQueryDropdownInstance = this.searchInput.data('deprecatedJQueryDropdown');
- if (glDropdownInstance) {
- glDropdownInstance.filter.options.callback(contents);
+ if (deprecatedJQueryDropdownInstance) {
+ deprecatedJQueryDropdownInstance.filter.options.callback(contents);
}
this.enableAutocomplete();
}
@@ -463,7 +465,7 @@ export class SearchAutocomplete {
}
highlightFirstRow() {
- this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
+ this.searchInput.data('deprecatedJQueryDropdown').highlightRowAtIndex(null, 0);
}
getAvatar(item) {
diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
index adc7d3c5ebd..1ccf5e9e032 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import Vue from 'vue';
import { GlFormGroup, GlDeprecatedButton, GlModal, GlToast, GlToggle } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index c44a14f1785..e15549f5864 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLink, GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/set_status_modal/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
deleted file mode 100644
index 0e8b6d93f42..00000000000
--- a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import eventHub from './event_hub';
-
-export default {
- props: {
- hasStatus: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- buttonText() {
- return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
- },
- },
- methods: {
- openModal() {
- eventHub.$emit('openModal');
- },
- },
-};
-</script>
-
-<template>
- <button type="button" class="btn menu-item" @click="openModal">{{ buttonText }}</button>
-</template>
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index cb047530c17..09e893ff285 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -1,12 +1,11 @@
<script>
+/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
-import { GlModal, GlTooltipDirective } from '@gitlab/ui';
+import { GlModal, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Icon from '~/vue_shared/components/icon.vue';
import { __, s__ } from '~/locale';
import Api from '~/api';
-import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
import * as Emoji from '~/emoji';
@@ -14,7 +13,7 @@ const emojiMenuClass = 'js-modal-status-emoji-menu';
export default {
components: {
- Icon,
+ GlIcon,
GlModal,
},
directives: {
@@ -48,15 +47,12 @@ export default {
},
},
mounted() {
- eventHub.$on('openModal', this.openModal);
+ this.$root.$emit('bv::show::modal', this.modalId);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
- openModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
- },
closeModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
},
@@ -196,9 +192,9 @@ export default {
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
>
- <icon name="slight-smile" class="award-control-icon-neutral" />
- <icon name="smiley" class="award-control-icon-positive" />
- <icon name="smile" class="award-control-icon-super-positive" />
+ <gl-icon name="slight-smile" class="award-control-icon-neutral" />
+ <gl-icon name="smiley" class="award-control-icon-positive" />
+ <gl-icon name="smile" class="award-control-icon-super-positive" />
</span>
</button>
</span>
@@ -223,7 +219,7 @@ export default {
class="js-clear-user-status-button clear-user-status btn"
@click="clearStatusInputs()"
>
- <icon name="close" />
+ <gl-icon name="close" />
</button>
</span>
</div>
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 0ff84dc4667..9ee02f923d5 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -16,5 +16,6 @@ export default (initGFM = true) => {
milestones: initGFM,
labels: initGFM,
snippets: initGFM,
+ vulnerabilities: initGFM,
});
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
index 9a60172db2e..878b331fb3c 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -59,7 +59,7 @@ export default {
};
},
assigneeUrl() {
- return this.user.web_url;
+ return this.user.web_url || this.user.webUrl;
},
},
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index 7375855f899..eabd4d88d52 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import CollapsedAssignee from './collapsed_assignee.vue';
@@ -12,6 +12,7 @@ export default {
},
components: {
CollapsedAssignee,
+ GlIcon,
},
props: {
users: {
@@ -102,7 +103,7 @@ export default {
:title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
>
- <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
+ <gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" />
<collapsed-assignee
v-for="user in collapsedUsers"
:key="user.id"
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
new file mode 100644
index 00000000000..4697d85472b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -0,0 +1,37 @@
+<script>
+import { n__ } from '~/locale';
+import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
+
+export default {
+ components: {
+ UncollapsedAssigneeList,
+ },
+ inject: ['rootPath'],
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ assigneesText() {
+ return n__('Assignee', '%d Assignees', this.users.length);
+ },
+ emptyUsers() {
+ return this.users.length === 0;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <label data-testid="assigneeLabel">{{ assigneesText }}</label>
+ <div v-if="emptyUsers" data-testid="none">
+ <span>
+ {{ __('None') }}
+ </span>
+ </div>
+ <uncollapsed-assignee-list v-else :users="users" :root-path="rootPath" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 14c14d0bad1..2f714ac3847 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -62,7 +62,7 @@ export default {
this.addAssignee = this.store.addAssignee.bind(this.store);
this.removeAllAssignees = this.store.removeAllAssignees.bind(this.store);
- // Get events from glDropdown
+ // Get events from deprecatedJQueryDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index fed9e5886c0..95934c0ef2a 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -73,9 +73,9 @@ export default {
:root-path="rootPath"
:issuable-type="issuableType"
>
- <div class="ml-2">
- <span class="author"> {{ user.name }} </span>
- <span class="username"> {{ username }} </span>
+ <div class="ml-2 gl-line-height-normal">
+ <div>{{ user.name }}</div>
+ <div>{{ username }}</div>
</div>
</assignee-avatar-link>
<div v-else>
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 c6f7d5e44ad..2530cb77acd 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,18 +1,17 @@
<script>
import { mapState } from 'vuex';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import EditForm from './edit_form.vue';
export default {
components: {
EditForm,
- Icon,
+ GlIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
fullPath: {
@@ -73,15 +72,12 @@ export default {
<div class="block issuable-sidebar-item confidentiality">
<div
ref="collapseIcon"
- v-tooltip
+ v-gl-tooltip.viewport.left
:title="tooltipLabel"
class="sidebar-collapsed-icon"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
@click="toggleForm"
>
- <icon :name="confidentialityIcon" aria-hidden="true" />
+ <gl-icon :name="confidentialityIcon" aria-hidden="true" />
</div>
<div class="title hide-collapsed">
{{ __('Confidentiality') }}
@@ -105,11 +101,11 @@ export default {
:issuable-type="issuableType"
/>
<div v-if="!confidential" class="no-value sidebar-item-value" data-testid="not-confidential">
- <icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
+ <gl-icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
</div>
<div v-else class="value sidebar-item-value hide-collapsed">
- <icon
+ <gl-icon
:size="16"
name="eye-slash"
aria-hidden="true"
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
new file mode 100644
index 00000000000..d7be8927c29
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -0,0 +1,97 @@
+<script>
+import $ from 'jquery';
+import { difference, union } from 'lodash';
+import { mapState, mapActions } from 'vuex';
+import flash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+
+export default {
+ components: {
+ LabelsSelect,
+ },
+ variant: DropdownVariant.Sidebar,
+ inject: [
+ 'allowLabelCreate',
+ 'allowLabelEdit',
+ 'allowScopedLabels',
+ 'iid',
+ 'initiallySelectedLabels',
+ 'issuableType',
+ 'labelsFetchPath',
+ 'labelsManagePath',
+ 'labelsUpdatePath',
+ 'projectIssuesPath',
+ 'projectPath',
+ ],
+ data: () => ({
+ labelsSelectInProgress: false,
+ }),
+ computed: {
+ ...mapState(['selectedLabels']),
+ },
+ mounted() {
+ this.setInitialState({
+ selectedLabels: this.initiallySelectedLabels,
+ });
+ },
+ methods: {
+ ...mapActions(['setInitialState', 'replaceSelectedLabels']),
+ handleDropdownClose() {
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ handleUpdateSelectedLabels(labels) {
+ const currentLabelIds = this.selectedLabels.map(label => label.id);
+ const userAddedLabelIds = labels.filter(label => label.set).map(label => label.id);
+ const userRemovedLabelIds = labels.filter(label => !label.set).map(label => label.id);
+
+ const issuableLabels = difference(
+ union(currentLabelIds, userAddedLabelIds),
+ userRemovedLabelIds,
+ );
+
+ this.labelsSelectInProgress = true;
+
+ axios({
+ data: {
+ [this.issuableType]: {
+ label_ids: issuableLabels,
+ },
+ },
+ method: 'put',
+ url: this.labelsUpdatePath,
+ })
+ .then(({ data }) => this.replaceSelectedLabels(data.labels))
+ .catch(() => flash(__('An error occurred while updating labels.')))
+ .finally(() => {
+ this.labelsSelectInProgress = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <labels-select
+ class="block labels js-labels-block"
+ :allow-label-create="allowLabelCreate"
+ :allow-label-edit="allowLabelEdit"
+ :allow-multiselect="true"
+ :allow-scoped-labels="allowScopedLabels"
+ :footer-create-label-title="__('Create project label')"
+ :footer-manage-label-title="__('Manage project labels')"
+ :labels-create-title="__('Create project label')"
+ :labels-fetch-path="labelsFetchPath"
+ :labels-filter-base-path="projectIssuesPath"
+ :labels-manage-path="labelsManagePath"
+ :labels-select-in-progress="labelsSelectInProgress"
+ :selected-labels="selectedLabels"
+ :variant="$options.sidebar"
+ @onDropdownClose="handleDropdownClose"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ >
+ {{ __('None') }}
+ </labels-select>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 1b4968fabf6..53ee7f46ad9 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
@@ -22,7 +22,7 @@ export default {
},
components: {
editForm,
- Icon,
+ GlIcon,
},
directives: {
@@ -88,7 +88,7 @@ export default {
data-boundary="viewport"
@click="toggleForm"
>
- <icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
+ <gl-icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
</div>
<div class="title hide-collapsed">
@@ -116,7 +116,7 @@ export default {
/>
<div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
- <icon
+ <gl-icon
:size="16"
:name="lockStatus.icon"
class="sidebar-item-icon"
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index d2904f4157c..e7dbc47aea1 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
@@ -10,6 +10,7 @@ export default {
},
components: {
userAvatarImage,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -94,7 +95,7 @@ export default {
data-boundary="viewport"
@click="onClickCollapsedIcon"
>
- <i class="fa fa-users" aria-hidden="true"> </i>
+ <gl-icon name="users" />
<gl-loading-icon v-if="loading" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div>
diff --git a/app/assets/javascripts/sidebar/components/severity/constants.js b/app/assets/javascripts/sidebar/components/severity/constants.js
new file mode 100644
index 00000000000..4f58ff38121
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/severity/constants.js
@@ -0,0 +1,41 @@
+import { __, s__ } from '~/locale';
+
+export const INCIDENT_SEVERITY = {
+ CRITICAL: {
+ value: 'CRITICAL',
+ icon: 'critical',
+ label: s__('IncidentManagement|Critical - S1'),
+ },
+ HIGH: {
+ value: 'HIGH',
+ icon: 'high',
+ label: s__('IncidentManagement|High - S2'),
+ },
+ MEDIUM: {
+ value: 'MEDIUM',
+ icon: 'medium',
+ label: s__('IncidentManagement|Medium - S3'),
+ },
+ LOW: {
+ value: 'LOW',
+ icon: 'low',
+ label: s__('IncidentManagement|Low - S4'),
+ },
+ UNKNOWN: {
+ value: 'UNKNOWN',
+ icon: 'unknown',
+ label: s__('IncidentManagement|Unknown'),
+ },
+};
+
+export const ISSUABLE_TYPES = {
+ INCIDENT: 'incident',
+};
+
+export const I18N = {
+ UPDATE_SEVERITY_ERROR: s__('SeverityWidget|There was an error while updating severity.'),
+ TRY_AGAIN: __('Please try again'),
+ EDIT: __('Edit'),
+ SEVERITY: s__('SeverityWidget|Severity'),
+ SEVERITY_VALUE: s__('SeverityWidget|Severity: %{severity}'),
+};
diff --git a/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql b/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql
new file mode 100644
index 00000000000..750e757971f
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateIssuableSeverity($projectPath: ID!, $severity: IssuableSeverity!, $iid: String!) {
+ issueSetSeverity(input: { iid: $iid, severity: $severity, projectPath: $projectPath }) {
+ errors
+ issue {
+ iid
+ severity
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/severity/severity.vue b/app/assets/javascripts/sidebar/components/severity/severity.vue
new file mode 100644
index 00000000000..7e7d62256c9
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/severity/severity.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ props: {
+ severity: {
+ type: Object,
+ required: true,
+ validator(severity) {
+ const { value, label, icon } = severity;
+ return value && label && icon;
+ },
+ },
+ iconSize: {
+ type: Number,
+ required: false,
+ default: 12,
+ },
+ iconOnly: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between"
+ >
+ <gl-icon
+ :size="iconSize"
+ :name="`severity-${severity.icon}`"
+ :class="[`icon-${severity.icon}`, { 'gl-mr-3': !iconOnly }]"
+ />
+ <span v-if="!iconOnly">{{ severity.label }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
new file mode 100644
index 00000000000..8f3610b912a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -0,0 +1,187 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlLoadingIcon,
+ GlTooltip,
+ GlSprintf,
+ GlLink,
+} from '@gitlab/ui';
+import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
+import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql';
+import SeverityToken from './severity.vue';
+import createFlash from '~/flash';
+
+export default {
+ i18n: I18N,
+ components: {
+ GlLoadingIcon,
+ GlTooltip,
+ GlSprintf,
+ GlDropdown,
+ GlDropdownItem,
+ GlLink,
+ SeverityToken,
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ iid: {
+ type: String,
+ required: true,
+ },
+ initialSeverity: {
+ type: String,
+ required: false,
+ default: INCIDENT_SEVERITY.UNKNOWN.value,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: ISSUABLE_TYPES.INCIDENT,
+ validator: value => {
+ // currently severity is supported only for incidents, but this list might be extended
+ return [ISSUABLE_TYPES.INCIDENT].includes(value);
+ },
+ },
+ },
+ data() {
+ return {
+ isDropdownShowing: false,
+ isUpdating: false,
+ severity: this.initialSeverity,
+ };
+ },
+ computed: {
+ severitiesList() {
+ switch (this.issuableType) {
+ case ISSUABLE_TYPES.INCIDENT:
+ return Object.values(INCIDENT_SEVERITY);
+ default:
+ return [];
+ }
+ },
+ dropdownClass() {
+ return this.isDropdownShowing ? 'show' : 'gl-display-none';
+ },
+ selectedItem() {
+ return this.severitiesList.find(severity => severity.value === this.severity);
+ },
+ },
+ mounted() {
+ document.addEventListener('click', this.handleOffClick);
+ },
+ beforeDestroy() {
+ document.removeEventListener('click', this.handleOffClick);
+ },
+ methods: {
+ handleOffClick(event) {
+ if (!this.isDropdownShowing) {
+ return;
+ }
+
+ if (!this.$refs.sidebarSeverity.contains(event.target)) {
+ this.hideDropdown();
+ }
+ },
+ hideDropdown() {
+ this.isDropdownShowing = false;
+ const event = new Event('hidden.gl.dropdown');
+ this.$el.dispatchEvent(event);
+ },
+ toggleFormDropdown() {
+ this.isDropdownShowing = !this.isDropdownShowing;
+ },
+ updateSeverity(value) {
+ this.hideDropdown();
+ this.isUpdating = true;
+ this.$apollo
+ .mutate({
+ mutation: updateIssuableSeverity,
+ variables: {
+ iid: this.iid,
+ severity: value,
+ projectPath: this.projectPath,
+ },
+ })
+ .then(resp => {
+ const {
+ data: {
+ issueSetSeverity: {
+ errors = [],
+ issue: { severity },
+ },
+ },
+ } = resp;
+
+ if (errors[0]) {
+ throw errors[0];
+ }
+ this.severity = severity;
+ })
+ .catch(() =>
+ createFlash({
+ message: `${this.$options.i18n.UPDATE_SEVERITY_ERROR} ${this.$options.i18n.TRY_AGAIN}`,
+ }),
+ )
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div ref="sidebarSeverity" class="block">
+ <div ref="severity" class="sidebar-collapsed-icon" @click="toggleFormDropdown">
+ <severity-token :severity="selectedItem" :icon-size="14" :icon-only="true" />
+ <gl-tooltip :target="() => $refs.severity" boundary="viewport" placement="left">
+ <gl-sprintf :message="$options.i18n.SEVERITY_VALUE">
+ <template #severity>
+ {{ selectedItem.label }}
+ </template>
+ </gl-sprintf>
+ </gl-tooltip>
+ </div>
+
+ <div class="hide-collapsed">
+ <p class="title gl-display-flex gl-justify-content-space-between">
+ {{ $options.i18n.SEVERITY }}
+ <gl-link
+ data-testid="editButton"
+ href="#"
+ @click="toggleFormDropdown"
+ @keydown.esc="hideDropdown"
+ >
+ {{ $options.i18n.EDIT }}
+ </gl-link>
+ </p>
+
+ <gl-dropdown
+ :class="dropdownClass"
+ block
+ :text="selectedItem.label"
+ toggle-class="dropdown-menu-toggle gl-mb-2"
+ @keydown.esc.native="hideDropdown"
+ >
+ <gl-dropdown-item
+ v-for="option in severitiesList"
+ :key="option.value"
+ data-testid="severityDropdownItem"
+ :is-check-item="true"
+ :is-checked="option.value === severity"
+ @click="updateSeverity(option.value)"
+ >
+ <severity-token :severity="option" />
+ </gl-dropdown-item>
+ </gl-dropdown>
+
+ <gl-loading-icon v-if="isUpdating" :inline="true" />
+
+ <severity-token v-else-if="!isDropdownShowing" :severity="selectedItem" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 3b92ead8859..0457aad8795 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -1,7 +1,7 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import Tracking from '~/tracking';
-import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
@@ -16,7 +16,7 @@ export default {
tooltip,
},
components: {
- icon,
+ GlIcon,
toggleButton,
},
mixins: [Tracking.mixin({ label: 'right_sidebar' })],
@@ -118,7 +118,7 @@ export default {
data-boundary="viewport"
@click="onClickCollapsedIcon"
>
- <icon
+ <gl-icon
:name="notificationIcon"
:size="16"
aria-hidden="true"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 65ecd5be05d..bc2319c0f36 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,12 +1,12 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -105,7 +105,7 @@ export default {
data-placement="left"
data-boundary="viewport"
>
- <icon name="timer" />
+ <gl-icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
<span :class="spanClass"> {{ text }} </span>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 67abde0c22a..b45746e789d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { sprintf, s__ } from '../../../locale';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
index c2f30310e2e..b2b3b289c5c 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { sprintf, s__ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 67a8f11b760..a2fb0ebcbc6 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -106,7 +106,7 @@ export default {
<div class="title hide-collapsed">
{{ __('Time tracking') }}
<div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true)">
- <i class="fa fa-question-circle" aria-hidden="true"> </i>
+ <gl-icon name="question-o" />
</div>
<div
v-if="showHelpState"
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 5281c03ab3f..51719df313f 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -1,10 +1,8 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
-
const MARK_TEXT = __('Mark as done');
const TODO_TEXT = __('Add a To-Do');
@@ -13,7 +11,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -85,7 +83,7 @@ export default {
data-boundary="viewport"
@click="handleButtonClick"
>
- <icon
+ <gl-icon
v-show="collapsedButtonIconVisible"
:class="collapsedButtonIconClasses"
:name="collapsedButtonIcon"
diff --git a/app/assets/javascripts/sidebar/event_hub.js b/app/assets/javascripts/sidebar/event_hub.js
index f35506fd5de..dd4bd9a5ab7 100644
--- a/app/assets/javascripts/sidebar/event_hub.js
+++ b/app/assets/javascripts/sidebar/event_hub.js
@@ -1,6 +1,6 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-const eventHub = new Vue();
+const eventHub = createEventHub();
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = (...args) => eventHub.$emit(...args);
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 0fb9cf22653..edeb1bba020 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import '~/gl_dropdown';
import { escape } from 'lodash';
import { __ } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
function isValidProjectId(id) {
return id > 0;
@@ -27,7 +27,7 @@ class SidebarMoveIssue {
}
initDropdown() {
- this.$dropdownToggle.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdownToggle, {
search: {
fields: ['name_with_namespace'],
},
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 015219200db..be559b16420 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,21 +1,26 @@
import $ from 'jquery';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import Vuex from 'vuex';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
+import SidebarLabels from './components/labels/sidebar_labels.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
+import SidebarSeverity from './components/severity/sidebar_severity.vue';
import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
import { store } from '~/notes/stores';
-import { isInIssuePage } from '~/lib/utils/common_utils';
+import { isInIssuePage, parseBoolean } from '~/lib/utils/common_utils';
import mergeRequestStore from '~/mr_notes/stores';
+import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
Vue.use(Translate);
Vue.use(VueApollo);
+Vue.use(Vuex);
function getSidebarOptions() {
return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
@@ -51,6 +56,29 @@ function mountAssigneesComponent(mediator) {
});
}
+export function mountSidebarLabels() {
+ const el = document.querySelector('.js-sidebar-labels');
+
+ if (!el) {
+ return false;
+ }
+
+ const labelsStore = new Vuex.Store(labelsSelectModule());
+
+ return new Vue({
+ el,
+ provide: {
+ ...el.dataset,
+ allowLabelCreate: parseBoolean(el.dataset.allowLabelCreate),
+ allowLabelEdit: parseBoolean(el.dataset.canEdit),
+ allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
+ initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
+ },
+ store: labelsStore,
+ render: createElement => createElement(SidebarLabels),
+ });
+}
+
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
@@ -159,6 +187,35 @@ function mountTimeTrackingComponent() {
});
}
+function mountSeverityComponent() {
+ const severityContainerEl = document.querySelector('#js-severity');
+
+ if (!severityContainerEl) {
+ return false;
+ }
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { fullPath, iid, severity } = getSidebarOptions();
+
+ return new Vue({
+ el: severityContainerEl,
+ apolloProvider,
+ components: {
+ SidebarSeverity,
+ },
+ render: createElement =>
+ createElement('sidebar-severity', {
+ props: {
+ projectPath: fullPath,
+ iid: String(iid),
+ initialSeverity: severity.toUpperCase(),
+ },
+ }),
+ });
+}
+
export function mountSidebar(mediator) {
mountAssigneesComponent(mediator);
mountConfidentialComponent(mediator);
@@ -173,6 +230,8 @@ export function mountSidebar(mediator) {
).init();
mountTimeTrackingComponent();
+
+ mountSeverityComponent();
}
export { getSidebarOptions };
diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql
deleted file mode 100644
index 2aff7da4605..00000000000
--- a/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-query($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- issue(iid: $iid) {
- iid
- }
- }
-}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 8714bea1729..a61af631661 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,5 +1,4 @@
import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
-import sidebarDetailsForHealthStatusFeatureFlagQuery from 'ee_else_ce/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql';
import axios from '~/lib/utils/axios_utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
@@ -27,14 +26,10 @@ export default class SidebarService {
}
get() {
- const hasHealthStatusFeatureFlag = gon.features && gon.features.saveIssuableHealthStatus;
-
return Promise.all([
axios.get(this.endpoint),
gqClient.query({
- query: hasHealthStatusFeatureFlag
- ? sidebarDetailsForHealthStatusFeatureFlagQuery
- : sidebarDetailsQuery,
+ query: sidebarDetailsQuery,
variables: {
fullPath: this.fullPath,
iid: this.iid.toString(),
diff --git a/app/assets/javascripts/snippet/snippet_edit.js b/app/assets/javascripts/snippet/snippet_edit.js
index b0d373b1a4b..3dc74922a77 100644
--- a/app/assets/javascripts/snippet/snippet_edit.js
+++ b/app/assets/javascripts/snippet/snippet_edit.js
@@ -14,6 +14,7 @@ document.addEventListener('DOMContentLoaded', () => {
milestones: false,
labels: false,
snippets: false,
+ vulnerabilities: false,
};
const projectSnippetOptions = {};
diff --git a/app/assets/javascripts/snippet/snippet_show.js b/app/assets/javascripts/snippet/snippet_show.js
index 9a463b4762b..bbddfc579c5 100644
--- a/app/assets/javascripts/snippet/snippet_show.js
+++ b/app/assets/javascripts/snippet/snippet_show.js
@@ -4,6 +4,7 @@ import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import snippetEmbed from '~/snippet/snippet_embed';
import { SnippetShowInit } from '~/snippets';
+import loadAwardsHandler from '~/awards_handler';
document.addEventListener('DOMContentLoaded', () => {
if (!gon.features.snippetsVue) {
@@ -16,4 +17,5 @@ document.addEventListener('DOMContentLoaded', () => {
SnippetShowInit();
initNotes();
}
+ loadAwardsHandler();
});
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 0978fcc7f93..1a539aa0876 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -4,21 +4,23 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { __, sprintf } from '~/locale';
import TitleField from '~/vue_shared/components/form/title.vue';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
+import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
import { getSnippetMixin } from '../mixins/snippets';
import {
- SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_CREATE_MUTATION_ERROR,
SNIPPET_UPDATE_MUTATION_ERROR,
+ SNIPPET_VISIBILITY_PRIVATE,
} from '../constants';
+import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
+
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue';
-import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
export default {
components: {
@@ -31,6 +33,15 @@ export default {
GlLoadingIcon,
},
mixins: [getSnippetMixin],
+ apollo: {
+ defaultVisibility: {
+ query: defaultVisibilityQuery,
+ manual: true,
+ result({ data: { selectedLevel } }) {
+ this.selectedLevelDefault = selectedLevel;
+ },
+ },
+ },
props: {
markdownPreviewPath: {
type: String,
@@ -56,6 +67,7 @@ export default {
isUpdating: false,
newSnippet: false,
actions: [],
+ selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE,
};
},
computed: {
@@ -88,7 +100,7 @@ export default {
},
cancelButtonHref() {
if (this.newSnippet) {
- return this.projectPath ? `/${this.projectPath}/-/snippets` : `/-/snippets`;
+ return joinPaths('/', gon.relative_url_root, this.projectPath, '-/snippets');
}
return this.snippet.webUrl;
},
@@ -98,6 +110,13 @@ export default {
descriptionFieldId() {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`;
},
+ newSnippetSchema() {
+ return {
+ title: '',
+ description: '',
+ visibilityLevel: this.selectedLevelDefault,
+ };
+ },
},
beforeCreate() {
performance.mark(SNIPPET_MARK_EDIT_APP_START);
@@ -126,7 +145,7 @@ export default {
},
onNewSnippetFetched() {
this.newSnippet = true;
- this.snippet = this.$options.newSnippetSchema;
+ this.snippet = this.newSnippetSchema;
},
onExistingSnippetFetched() {
this.newSnippet = false;
@@ -184,11 +203,6 @@ export default {
this.actions = actions;
},
},
- newSnippetSchema: {
- title: '',
- description: '',
- visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
- },
};
</script>
<template>
@@ -202,7 +216,7 @@ export default {
v-if="isLoading"
:label="__('Loading snippet')"
size="lg"
- class="loading-animation prepend-top-20 append-bottom-20"
+ class="loading-animation prepend-top-20 gl-mb-6"
/>
<template v-else>
<title-field
diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue
new file mode 100644
index 00000000000..589754a8b19
--- /dev/null
+++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue
@@ -0,0 +1,78 @@
+<script>
+import { escape as esc } from 'lodash';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownText,
+ GlFormInputGroup,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+
+const MSG_EMBED = __('Embed');
+const MSG_SHARE = __('Share');
+const MSG_COPY = __('Copy');
+
+export default {
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownText,
+ GlFormInputGroup,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ sections() {
+ return [
+ // eslint-disable-next-line no-useless-escape
+ { name: MSG_EMBED, value: `<script src="${esc(this.url)}.js"><\/script>` },
+ { name: MSG_SHARE, value: this.url },
+ ];
+ },
+ },
+ MSG_EMBED,
+ MSG_COPY,
+};
+</script>
+<template>
+ <gl-dropdown
+ right
+ :text="$options.MSG_EMBED"
+ menu-class="gl-px-1! gl-pb-5! gl-dropdown-menu-wide"
+ >
+ <template v-for="{ name, value } in sections">
+ <gl-dropdown-section-header :key="`header_${name}`" data-testid="header">{{
+ name
+ }}</gl-dropdown-section-header>
+ <gl-dropdown-text
+ :key="`input_${name}`"
+ tag="div"
+ class="gl-dropdown-text-py-0 gl-dropdown-text-block"
+ data-testid="input"
+ >
+ <gl-form-input-group :value="value" readonly select-on-click>
+ <template #append>
+ <gl-button
+ v-gl-tooltip.hover
+ :title="$options.MSG_COPY"
+ :data-clipboard-text="value"
+ icon="copy-to-clipboard"
+ data-qa-selector="copy_button"
+ :data-qa-action="name"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-dropdown-text>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index ca41fd0a2b1..43be2cb7ed8 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
+import EmbedDropdown from './embed_dropdown.vue';
import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue';
@@ -13,7 +13,7 @@ import { SNIPPET_MARK_VIEW_APP_START } from '~/performance_constants';
export default {
components: {
- BlobEmbeddable,
+ EmbedDropdown,
SnippetHeader,
SnippetTitle,
GlLoadingIcon,
@@ -40,13 +40,17 @@ export default {
v-if="isLoading"
:label="__('Loading snippet')"
size="lg"
- class="loading-animation prepend-top-20 append-bottom-20"
+ class="loading-animation prepend-top-20 gl-mb-6"
/>
<template v-else>
<snippet-header :snippet="snippet" />
<snippet-title :snippet="snippet" />
<div class="gl-display-flex gl-justify-content-end gl-mb-5">
- <blob-embeddable v-if="embeddable" class="gl-flex-fill-1" :url="snippet.webUrl" />
+ <embed-dropdown
+ v-if="embeddable"
+ :url="snippet.webUrl"
+ data-qa-selector="snippet_embed_dropdown"
+ />
<clone-dropdown-button
v-if="canBeCloned"
class="gl-ml-3"
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index ff03432f942..f3f894ed649 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -53,7 +53,10 @@ export default {
const url = joinPaths(baseUrl, this.blob.rawPath);
axios
- .get(url)
+ .get(url, {
+ // This prevents axios from automatically JSON.parse response
+ transformResponse: [f => f],
+ })
.then(res => {
this.notifyAboutUpdates({ content: res.data });
})
@@ -80,7 +83,7 @@ export default {
v-if="!blob.isLoaded"
:label="__('Loading snippet')"
size="lg"
- class="loading-animation prepend-top-20 append-bottom-20"
+ class="loading-animation prepend-top-20 gl-mb-6"
/>
<blob-content-edit
v-else
diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue
index a5107f09fc7..e462f20535b 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
export default {
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index ed087dcfaf9..0ca69f3161a 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -17,6 +17,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
import CanCreatePersonalSnippet from '../queries/userPermissions.query.graphql';
import CanCreateProjectSnippet from '../queries/projectPermissions.query.graphql';
+import { joinPaths } from '~/lib/utils/url_utility';
export default {
components: {
@@ -96,8 +97,8 @@ export default {
condition: this.canCreateSnippet,
text: __('New snippet'),
href: this.snippet.project
- ? `${this.snippet.project.webUrl}/-/snippets/new`
- : '/-/snippets/new',
+ ? joinPaths(this.snippet.project.webUrl, '-/snippets/new')
+ : joinPaths('/', gon.relative_url_root, '/-/snippets/new'),
variant: 'success',
category: 'secondary',
cssClass: 'ml-2',
@@ -137,7 +138,7 @@ export default {
redirectToSnippets() {
window.location.pathname = this.snippet.project
? `${this.snippet.project.fullPath}/-/snippets`
- : 'dashboard/snippets';
+ : `${gon.relative_url_root}dashboard/snippets`;
},
closeDeleteModal() {
this.$refs.deleteModal.hide();
diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
index 299bb8fcfad..25ad7c214b2 100644
--- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
@@ -1,11 +1,8 @@
<script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
-import {
- SNIPPET_VISIBILITY,
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PUBLIC,
-} from '~/snippets/constants';
+import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
+import { defaultSnippetVisibilityLevels } from '../utils/blob';
+import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
export default {
components: {
@@ -15,6 +12,16 @@ export default {
GlFormRadioGroup,
GlLink,
},
+ apollo: {
+ defaultVisibility: {
+ query: defaultVisibilityQuery,
+ manual: true,
+ result({ data: { visibilityLevels, multipleLevelsRestricted } }) {
+ this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels);
+ this.multipleLevelsRestricted = multipleLevelsRestricted;
+ },
+ },
+ },
props: {
helpLink: {
type: String,
@@ -28,19 +35,17 @@ export default {
},
value: {
type: String,
- required: false,
- default: SNIPPET_VISIBILITY_PRIVATE,
+ required: true,
},
},
- computed: {
- visibilityOptions() {
- return [
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PUBLIC,
- ].map(key => ({ value: key, ...SNIPPET_VISIBILITY[key] }));
- },
+ data() {
+ return {
+ visibilityLevels: [],
+ multipleLevelsRestricted: false,
+ };
},
+ SNIPPET_LEVELS_DISABLED,
+ SNIPPET_LEVELS_RESTRICTED,
};
</script>
<template>
@@ -51,10 +56,10 @@ export default {
><gl-icon :size="12" name="question"
/></gl-link>
</label>
- <gl-form-group id="visibility-level-setting">
- <gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners">
+ <gl-form-group id="visibility-level-setting" class="gl-mb-0">
+ <gl-form-radio-group :checked="value" stacked v-bind="$attrs" v-on="$listeners">
<gl-form-radio
- v-for="option in visibilityOptions"
+ v-for="option in visibilityLevels"
:key="option.value"
:value="option.value"
class="mb-3"
@@ -71,5 +76,12 @@ export default {
</gl-form-radio>
</gl-form-radio-group>
</gl-form-group>
+
+ <div class="text-muted" data-testid="restricted-levels-info">
+ <template v-if="!visibilityLevels.length">{{ $options.SNIPPET_LEVELS_DISABLED }}</template>
+ <template v-else-if="multipleLevelsRestricted">{{
+ $options.SNIPPET_LEVELS_RESTRICTED
+ }}</template>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js
index 12b83525bf7..e75922df15f 100644
--- a/app/assets/javascripts/snippets/constants.js
+++ b/app/assets/javascripts/snippets/constants.js
@@ -33,3 +33,15 @@ export const SNIPPET_BLOB_ACTION_MOVE = 'move';
export const SNIPPET_BLOB_ACTION_DELETE = 'delete';
export const SNIPPET_MAX_BLOBS = 10;
+
+export const SNIPPET_LEVELS_MAP = {
+ 0: SNIPPET_VISIBILITY_PRIVATE,
+ 10: SNIPPET_VISIBILITY_INTERNAL,
+ 20: SNIPPET_VISIBILITY_PUBLIC,
+};
+export const SNIPPET_LEVELS_RESTRICTED = __(
+ 'Other visibility settings have been disabled by the administrator.',
+);
+export const SNIPPET_LEVELS_DISABLED = __(
+ 'Visibility settings have been disabled by the administrator.',
+);
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
index bb5e7d6e3f0..c70ad9b95f8 100644
--- a/app/assets/javascripts/snippets/index.js
+++ b/app/assets/javascripts/snippets/index.js
@@ -5,6 +5,7 @@ import createDefaultClient from '~/lib/graphql';
import SnippetsShow from './components/show.vue';
import SnippetsEdit from './components/edit.vue';
+import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
Vue.use(VueApollo);
Vue.use(Translate);
@@ -18,13 +19,28 @@ function appFactory(el, Component) {
defaultClient: createDefaultClient(),
});
+ const {
+ visibilityLevels = '[]',
+ selectedLevel,
+ multipleLevelsRestricted,
+ ...restDataset
+ } = el.dataset;
+
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ visibilityLevels: JSON.parse(visibilityLevels),
+ selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
+ multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
+ },
+ });
+
return new Vue({
el,
apolloProvider,
render(createElement) {
return createElement(Component, {
props: {
- ...el.dataset,
+ ...restDataset,
},
});
},
diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js
index 3f5d64a768f..15daaa8d84a 100644
--- a/app/assets/javascripts/snippets/mixins/snippets.js
+++ b/app/assets/javascripts/snippets/mixins/snippets.js
@@ -2,7 +2,6 @@ import GetSnippetQuery from '../queries/snippet.query.graphql';
const blobsDefault = [];
-// eslint-disable-next-line import/prefer-default-export
export const getSnippetMixin = {
apollo: {
snippet: {
diff --git a/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql b/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql
new file mode 100644
index 00000000000..5bd6c131bab
--- /dev/null
+++ b/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql
@@ -0,0 +1,5 @@
+query defaultSnippetVisibility {
+ visibilityLevels @client
+ selectedLevel @client
+ multipleLevelsRestricted @client
+}
diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js
index fd5ff9a3d2e..21f52671801 100644
--- a/app/assets/javascripts/snippets/utils/blob.js
+++ b/app/assets/javascripts/snippets/utils/blob.js
@@ -4,6 +4,8 @@ import {
SNIPPET_BLOB_ACTION_UPDATE,
SNIPPET_BLOB_ACTION_MOVE,
SNIPPET_BLOB_ACTION_DELETE,
+ SNIPPET_LEVELS_MAP,
+ SNIPPET_VISIBILITY,
} from '../constants';
const createLocalId = () => uniqueId('blob_local_');
@@ -64,3 +66,16 @@ export const diffAll = (blobs, origBlobs) => {
return [...deletedEntries, ...newEntries];
};
+
+export const defaultSnippetVisibilityLevels = arr => {
+ if (Array.isArray(arr)) {
+ return arr.map(l => {
+ const translatedLevel = SNIPPET_LEVELS_MAP[l];
+ return {
+ value: translatedLevel,
+ ...SNIPPET_VISIBILITY[translatedLevel],
+ };
+ });
+ }
+ return [];
+};
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index 53fbb2a330d..e602f26acdf 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -2,6 +2,7 @@
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import PublishToolbar from './publish_toolbar.vue';
import EditHeader from './edit_header.vue';
+import EditDrawer from './edit_drawer.vue';
import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
@@ -15,6 +16,7 @@ export default {
RichContentEditor,
PublishToolbar,
EditHeader,
+ EditDrawer,
UnsavedChangesConfirmDialog,
},
props: {
@@ -48,6 +50,8 @@ export default {
parsedSource: parseSourceFile(this.preProcess(true, this.content)),
editorMode: EDITOR_TYPES.wysiwyg,
isModified: false,
+ hasMatter: false,
+ isDrawerOpen: false,
};
},
imageRepository: imageRepository(),
@@ -55,10 +59,19 @@ export default {
editableContent() {
return this.parsedSource.content(this.isWysiwygMode);
},
+ editableMatter() {
+ return this.isDrawerOpen ? this.parsedSource.matter() : {};
+ },
+ hasSettings() {
+ return this.hasMatter && this.isWysiwygMode;
+ },
isWysiwygMode() {
return this.editorMode === EDITOR_TYPES.wysiwyg;
},
},
+ created() {
+ this.refreshEditHelpers();
+ },
methods: {
preProcess(isWrap, value) {
const formattedContent = formatter(value);
@@ -67,9 +80,21 @@ export default {
: templater.unwrap(formattedContent);
return templatedContent;
},
- onInputChange(newVal) {
- this.parsedSource.sync(newVal, this.isWysiwygMode);
+ refreshEditHelpers() {
this.isModified = this.parsedSource.isModified();
+ this.hasMatter = this.parsedSource.hasMatter();
+ },
+ onDrawerOpen() {
+ this.isDrawerOpen = true;
+ this.refreshEditHelpers();
+ },
+ onDrawerClose() {
+ this.isDrawerOpen = false;
+ this.refreshEditHelpers();
+ },
+ onInputChange(newVal) {
+ this.parsedSource.syncContent(newVal, this.isWysiwygMode);
+ this.refreshEditHelpers();
},
onModeChange(mode) {
this.editorMode = mode;
@@ -77,6 +102,9 @@ export default {
const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent);
this.$refs.editor.resetInitialValue(preProcessedContent);
},
+ onUpdateSettings(settings) {
+ this.parsedSource.syncMatter(settings);
+ },
onUploadImage({ file, imageUrl }) {
this.$options.imageRepository.add(file, imageUrl);
},
@@ -93,12 +121,19 @@ export default {
<template>
<div class="d-flex flex-grow-1 flex-column h-100">
<edit-header class="py-2" :title="title" />
+ <edit-drawer
+ v-if="hasMatter"
+ :is-open="isDrawerOpen"
+ :settings="editableMatter"
+ @close="onDrawerClose"
+ @updateSettings="onUpdateSettings"
+ />
<rich-content-editor
ref="editor"
:content="editableContent"
:initial-edit-type="editorMode"
:image-root="imageRoot"
- class="mb-9 h-100"
+ class="mb-9 pb-6 h-100"
@modeChange="onModeChange"
@input="onInputChange"
@uploadImage="onUploadImage"
@@ -106,9 +141,11 @@ export default {
<unsaved-changes-confirm-dialog :modified="isModified" />
<publish-toolbar
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
+ :has-settings="hasSettings"
:return-url="returnUrl"
:saveable="isModified"
:saving-changes="savingChanges"
+ @editSettings="onDrawerOpen"
@submit="onSubmit"
/>
</div>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
new file mode 100644
index 00000000000..0484d38dde0
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlDrawer } from '@gitlab/ui';
+import FrontMatterControls from './front_matter_controls.vue';
+
+export default {
+ components: {
+ GlDrawer,
+ FrontMatterControls,
+ },
+ props: {
+ isOpen: {
+ type: Boolean,
+ required: true,
+ },
+ settings: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <gl-drawer class="gl-pt-8" :open="isOpen" @close="$emit('close')">
+ <template #header>{{ __('Page settings') }}</template>
+ <template>
+ <front-matter-controls
+ :settings="settings"
+ @updateSettings="$emit('updateSettings', $event)"
+ />
+ </template>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue
new file mode 100644
index 00000000000..dad3907c3ff
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { humanize } from '~/lib/utils/text_utility';
+
+export default {
+ components: {
+ GlForm,
+ GlFormInput,
+ GlFormGroup,
+ },
+ props: {
+ settings: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ editableSettings: { ...this.settings },
+ };
+ },
+ methods: {
+ getId(type, key) {
+ return `sse-front-matter-${type}-${key}`;
+ },
+ getIsSupported(val) {
+ return ['string', 'number'].includes(typeof val);
+ },
+ getLabel(str) {
+ return humanize(str);
+ },
+ onUpdate() {
+ this.$emit('updateSettings', { ...this.editableSettings });
+ },
+ },
+};
+</script>
+<template>
+ <gl-form>
+ <template v-for="(value, key) of editableSettings">
+ <gl-form-group
+ v-if="getIsSupported(value)"
+ :id="getId('form-group', key)"
+ :key="key"
+ :label="getLabel(key)"
+ :label-for="getId('control', key)"
+ >
+ <gl-form-input
+ :id="getId('control', key)"
+ v-model.lazy="editableSettings[key]"
+ type="text"
+ @input="onUpdate"
+ />
+ </gl-form-group>
+ </template>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
index 6cd2a4dd700..2d62964cb3b 100644
--- a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
+++ b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
@@ -6,6 +6,11 @@ export default {
GlButton,
},
props: {
+ hasSettings: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
returnUrl: {
type: String,
required: false,
@@ -31,12 +36,21 @@ export default {
s__('StaticSiteEditor|Return to site')
}}</gl-button>
<gl-button
+ v-if="hasSettings"
+ ref="settings"
+ :disabled="savingChanges"
+ @click="$emit('editSettings')"
+ >
+ {{ __('Settings') }}
+ </gl-button>
+ <gl-button
+ ref="submit"
variant="success"
:disabled="!saveable"
:loading="savingChanges"
@click="$emit('submit')"
>
- <span>{{ __('Submit Changes') }}</span>
+ {{ __('Submit changes') }}
</gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/static_site_editor/services/formatter.js b/app/assets/javascripts/static_site_editor/services/formatter.js
index 92d5e8a5df8..9a5dcd307eb 100644
--- a/app/assets/javascripts/static_site_editor/services/formatter.js
+++ b/app/assets/javascripts/static_site_editor/services/formatter.js
@@ -1,3 +1,45 @@
+import { repeat } from 'lodash';
+
+const topLevelOrderedRegexp = /^\d{1,3}/;
+const nestedLineRegexp = /^\s+/;
+
+/**
+ * DISCLAIMER: This is a temporary fix that corrects the indentation
+ * spaces of list items. This workaround originates in the usage of
+ * the Static Site Editor to edit the Handbook. The Handbook uses a
+ * Markdown parser called Kramdown interprets lines indented
+ * with two spaces as content within a list. For example:
+ *
+ * 1. ordered list
+ * - nested unordered list
+ *
+ * The Static Site Editor uses a different Markdown parser based on the
+ * CommonMark specification (official Markdown spec) called ToastMark.
+ * When the SSE encounters a nested list with only two spaces, it flattens
+ * the list:
+ *
+ * 1. ordered list
+ * - nested unordered list
+ *
+ * This function attempts to correct this problem before the content is loaded
+ * by Toast UI.
+ */
+const correctNestedContentIndenation = source => {
+ const lines = source.split('\n');
+ let topLevelOrderedListDetected = false;
+
+ return lines
+ .reduce((result, line) => {
+ if (topLevelOrderedListDetected && nestedLineRegexp.test(line)) {
+ return [...result, line.replace(nestedLineRegexp, repeat(' ', 4))];
+ }
+
+ topLevelOrderedListDetected = topLevelOrderedRegexp.test(line);
+ return [...result, line];
+ }, [])
+ .join('\n');
+};
+
const removeOrphanedBrTags = source => {
/* Until the underlying Squire editor of Toast UI Editor resolves duplicate `<br>` tags, this
`replace` solution will clear out orphaned `<br>` tags that it generates. Additionally,
@@ -8,7 +50,7 @@ const removeOrphanedBrTags = source => {
};
const format = source => {
- return removeOrphanedBrTags(source);
+ return correctNestedContentIndenation(removeOrphanedBrTags(source));
};
export default format;
diff --git a/app/assets/javascripts/static_site_editor/services/image_service.js b/app/assets/javascripts/static_site_editor/services/image_service.js
index edc69d0579a..25ab1084572 100644
--- a/app/assets/javascripts/static_site_editor/services/image_service.js
+++ b/app/assets/javascripts/static_site_editor/services/image_service.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const getBinary = file => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
diff --git a/app/assets/javascripts/static_site_editor/services/parse_source_file.js b/app/assets/javascripts/static_site_editor/services/parse_source_file.js
index 126dfe81b90..640186ee1d0 100644
--- a/app/assets/javascripts/static_site_editor/services/parse_source_file.js
+++ b/app/assets/javascripts/static_site_editor/services/parse_source_file.js
@@ -1,64 +1,40 @@
-const parseSourceFile = raw => {
- const frontMatterRegex = /(^---$[\s\S]*?^---$)/m;
- const preGroupedRegex = /([\s\S]*?)(^---$[\s\S]*?^---$)(\s*)([\s\S]*)/m; // preFrontMatter, frontMatter, spacing, and content
- let initial;
- let editable;
-
- const hasFrontMatter = source => frontMatterRegex.test(source);
+import grayMatter from 'gray-matter';
- const buildPayload = (source, header, spacing, body) => {
- return { raw: source, header, spacing, body };
- };
+const parseSourceFile = raw => {
+ const remake = source => grayMatter(source, {});
- const parse = source => {
- if (hasFrontMatter(source)) {
- const match = source.match(preGroupedRegex);
- const [, preFrontMatter, frontMatter, spacing, content] = match;
- const header = preFrontMatter + frontMatter;
+ let editable = remake(raw);
- return buildPayload(source, header, spacing, content);
+ const syncContent = (newVal, isBody) => {
+ if (isBody) {
+ editable.content = newVal;
+ } else {
+ editable = remake(newVal);
}
-
- return buildPayload(source, '', '', source);
};
- const syncEditable = () => {
- /*
- We re-parse as markdown editing could have added non-body changes (preFrontMatter, frontMatter, or spacing).
- Re-parsing additionally gets us the desired body that was extracted from the potentially mutated editable.raw
- */
- editable = parse(editable.raw);
- };
+ const trimmedEditable = () => grayMatter.stringify(editable).trim();
- const syncBodyToRaw = () => {
- editable.raw = `${editable.header}${editable.spacing}${editable.body}`;
- };
-
- const sync = (newVal, isBodyToRaw) => {
- const editableKey = isBodyToRaw ? 'body' : 'raw';
- editable[editableKey] = newVal;
+ const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96
- if (isBodyToRaw) {
- syncBodyToRaw();
- }
-
- syncEditable();
- };
+ const matter = () => editable.data;
- const content = (isBody = false) => {
- const editableKey = isBody ? 'body' : 'raw';
- return editable[editableKey];
+ const syncMatter = settings => {
+ const source = grayMatter.stringify(editable.content, settings);
+ syncContent(source);
};
- const isModified = () => initial.raw !== editable.raw;
+ const isModified = () => trimmedEditable() !== raw;
- initial = parse(raw);
- editable = parse(raw);
+ const hasMatter = () => editable.matter.length > 0;
return {
+ matter,
+ syncMatter,
content,
+ syncContent,
isModified,
- sync,
+ hasMatter,
};
};
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 7206bbd7109..354ee00a977 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,11 +1,12 @@
import $ from 'jquery';
import { __ } from './locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default function subscriptionSelect() {
$('.js-subscription-event').each((i, element) => {
const fieldName = $(element).data('fieldName');
- return $(element).glDropdown({
+ return initDeprecatedJQueryDropdown($(element), {
selectable: true,
fieldName,
toggleLabel(selected, el, instance) {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index 10ad4170930..22bbd083a5d 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -33,7 +33,7 @@ export default class IssuableTemplateSelector extends TemplateSelector {
this.templateWarningEl.find('.js-close-btn').on('click', () => {
// Explicitly check against 0 value
if (this.previousSelectedIndex !== undefined) {
- this.dropdown.data('glDropdown').selectRowAtIndex(this.previousSelectedIndex);
+ this.dropdown.data('deprecatedJQueryDropdown').selectRowAtIndex(this.previousSelectedIndex);
} else {
this.reset();
}
@@ -61,7 +61,7 @@ export default class IssuableTemplateSelector extends TemplateSelector {
}
setSelectedIndex() {
- this.previousSelectedIndex = this.dropdown.data('glDropdown').selectedIndex;
+ this.previousSelectedIndex = this.dropdown.data('deprecatedJQueryDropdown').selectedIndex;
}
onDropdownClicked(query) {
diff --git a/app/assets/javascripts/tooltips/components/tooltips.vue b/app/assets/javascripts/tooltips/components/tooltips.vue
new file mode 100644
index 00000000000..8307f878def
--- /dev/null
+++ b/app/assets/javascripts/tooltips/components/tooltips.vue
@@ -0,0 +1,116 @@
+<script>
+import { GlTooltip, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+
+const getTooltipTitle = element => {
+ return element.getAttribute('title') || element.dataset.title;
+};
+
+const newTooltip = (element, config = {}) => {
+ const { placement, container, boundary, html, triggers } = element.dataset;
+ const title = getTooltipTitle(element);
+
+ return {
+ id: uniqueId('gl-tooltip'),
+ target: element,
+ title,
+ html,
+ placement,
+ container,
+ boundary,
+ triggers,
+ disabled: !title,
+ ...config,
+ };
+};
+
+export default {
+ components: {
+ GlTooltip,
+ },
+ directives: {
+ SafeHtml,
+ },
+ data() {
+ return {
+ tooltips: [],
+ };
+ },
+ created() {
+ this.observer = new MutationObserver(mutations => {
+ mutations.forEach(mutation => {
+ mutation.removedNodes.forEach(this.dispose);
+ });
+ });
+ },
+ beforeDestroy() {
+ this.observer.disconnect();
+ },
+ methods: {
+ addTooltips(elements, config) {
+ const newTooltips = elements
+ .filter(element => !this.tooltipExists(element))
+ .map(element => newTooltip(element, config));
+
+ newTooltips.forEach(tooltip => this.observe(tooltip));
+
+ this.tooltips.push(...newTooltips);
+ },
+ observe(tooltip) {
+ this.observer.observe(tooltip.target.parentElement, {
+ childList: true,
+ });
+ },
+ dispose(target) {
+ if (!target) {
+ this.tooltips = [];
+ } else {
+ const index = this.tooltips.indexOf(this.findTooltipByTarget(target));
+
+ if (index > -1) {
+ this.tooltips.splice(index, 1);
+ }
+ }
+ },
+ fixTitle(target) {
+ const tooltip = this.findTooltipByTarget(target);
+
+ if (tooltip) {
+ tooltip.title = target.getAttribute('title');
+ }
+ },
+ triggerEvent(target, event) {
+ const tooltip = this.findTooltipByTarget(target);
+
+ if (tooltip) {
+ this.$refs[tooltip.id][0].$emit(event);
+ }
+ },
+ tooltipExists(element) {
+ return Boolean(this.findTooltipByTarget(element));
+ },
+ findTooltipByTarget(element) {
+ return this.tooltips.find(tooltip => tooltip.target === element);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-tooltip
+ v-for="(tooltip, index) in tooltips"
+ :id="tooltip.id"
+ :ref="tooltip.id"
+ :key="index"
+ :target="tooltip.target"
+ :triggers="tooltip.triggers"
+ :placement="tooltip.placement"
+ :container="tooltip.container"
+ :boundary="tooltip.boundary"
+ :disabled="tooltip.disabled"
+ >
+ <span v-if="tooltip.html" v-safe-html="tooltip.title"></span>
+ <span v-else>{{ tooltip.title }}</span>
+ </gl-tooltip>
+ </div>
+</template>
diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js
new file mode 100644
index 00000000000..cfbd88d6c40
--- /dev/null
+++ b/app/assets/javascripts/tooltips/index.js
@@ -0,0 +1,120 @@
+import Vue from 'vue';
+import jQuery from 'jquery';
+import { toArray, isFunction } from 'lodash';
+import Tooltips from './components/tooltips.vue';
+
+let app;
+
+const EVENTS_MAP = {
+ hover: 'mouseenter',
+ click: 'click',
+ focus: 'focus',
+};
+
+const DEFAULT_TRIGGER = 'hover focus';
+const APP_ELEMENT_ID = 'gl-tooltips-app';
+
+const tooltipsApp = () => {
+ if (!app) {
+ const container = document.createElement('div');
+
+ container.setAttribute('id', APP_ELEMENT_ID);
+ document.body.appendChild(container);
+
+ app = new Vue({
+ render(h) {
+ return h(Tooltips, {
+ props: {
+ elements: this.elements,
+ },
+ ref: 'tooltips',
+ });
+ },
+ }).$mount(container);
+ }
+
+ return app.$refs.tooltips;
+};
+
+const isTooltip = (node, selector) => node.matches && node.matches(selector);
+
+const addTooltips = (elements, config) => {
+ tooltipsApp().addTooltips(toArray(elements), config);
+};
+
+const handleTooltipEvent = (rootTarget, e, selector, config = {}) => {
+ for (let { target } = e; target && target !== rootTarget; target = target.parentNode) {
+ if (isTooltip(target, selector)) {
+ addTooltips([target], {
+ show: true,
+ ...config,
+ });
+ break;
+ }
+ }
+};
+
+const applyToElements = (elements, handler) => toArray(elements).forEach(handler);
+
+const invokeBootstrapApi = (elements, method) => {
+ if (isFunction(elements.tooltip)) {
+ jQuery(elements).tooltip(method);
+ }
+};
+
+const isGlTooltipsEnabled = () => Boolean(window.gon.glTooltipsEnabled);
+
+const tooltipApiInvoker = ({ glHandler, bsHandler }) => (elements, ...params) => {
+ if (isGlTooltipsEnabled()) {
+ applyToElements(elements, glHandler);
+ } else {
+ bsHandler(elements, ...params);
+ }
+};
+
+export const initTooltips = (config = {}) => {
+ if (isGlTooltipsEnabled()) {
+ const triggers = config?.triggers || DEFAULT_TRIGGER;
+ const events = triggers.split(' ').map(trigger => EVENTS_MAP[trigger]);
+
+ events.forEach(event => {
+ document.addEventListener(
+ event,
+ e => handleTooltipEvent(document, e, config.selector, config),
+ true,
+ );
+ });
+
+ return tooltipsApp();
+ }
+
+ return invokeBootstrapApi(document.body, config);
+};
+export const dispose = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().dispose(element),
+ bsHandler: elements => invokeBootstrapApi(elements, 'dispose'),
+});
+export const fixTitle = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().fixTitle(element),
+ bsHandler: elements => invokeBootstrapApi(elements, '_fixTitle'),
+});
+export const enable = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().triggerEvent(element, 'enable'),
+ bsHandler: elements => invokeBootstrapApi(elements, 'enable'),
+});
+export const disable = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().triggerEvent(element, 'disable'),
+ bsHandler: elements => invokeBootstrapApi(elements, 'disable'),
+});
+export const hide = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().triggerEvent(element, 'close'),
+ bsHandler: elements => invokeBootstrapApi(elements, 'hide'),
+});
+export const show = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().triggerEvent(element, 'open'),
+ bsHandler: elements => invokeBootstrapApi(elements, 'show'),
+});
+export const destroy = () => {
+ tooltipsApp().$destroy();
+ app = null;
+};
diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js
index 10510595570..37ebe6b6c4d 100644
--- a/app/assets/javascripts/tracking.js
+++ b/app/assets/javascripts/tracking.js
@@ -126,14 +126,18 @@ export function initUserTracking() {
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
window.snowplow('newTracker', opts.namespace, opts.hostname, opts);
+ document.dispatchEvent(new Event('SnowplowInitialized'));
+}
+
+export function initDefaultTrackers() {
+ if (!Tracking.enabled()) return;
+
window.snowplow('enableActivityTracking', 30, 30);
window.snowplow('trackPageView'); // must be after enableActivityTracking
- if (opts.formTracking) window.snowplow('enableFormTracking');
- if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking');
+ if (window.snowplowOptions.formTracking) window.snowplow('enableFormTracking');
+ if (window.snowplowOptions.linkClickTracking) window.snowplow('enableLinkClickTracking');
Tracking.bindDocument();
Tracking.trackLoadEvents();
-
- document.dispatchEvent(new Event('SnowplowInitialized'));
}
diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js
index be18ac5da24..4f32e143de8 100644
--- a/app/assets/javascripts/ui_development_kit.js
+++ b/app/assets/javascripts/ui_development_kit.js
@@ -1,8 +1,9 @@
import $ from 'jquery';
import Api from './api';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default () => {
- $('#js-project-dropdown').glDropdown({
+ initDeprecatedJQueryDropdown($('#js-project-dropdown'), {
data: (term, callback) => {
Api.projects(
term,
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index e45b0de9083..5f4260f26ff 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -11,8 +11,9 @@ import {
import axios from '../lib/utils/axios_utils';
import { s__, __, sprintf } from '../locale';
import ModalStore from '../boards/stores/modal_store';
-import { parseBoolean } from '../lib/utils/common_utils';
+import { parseBoolean, spriteIcon } from '../lib/utils/common_utils';
import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@@ -54,6 +55,7 @@ function UsersSelect(currentUser, els, options = {}) {
const defaultLabel = $dropdown.data('defaultLabel');
const issueURL = $dropdown.data('issueUpdate');
const $selectbox = $dropdown.closest('.selectbox');
+ const $assignToMeLink = $selectbox.next('.assign-to-me-link');
let $block = $selectbox.closest('.block');
const abilityName = $dropdown.data('abilityName');
let $value = $block.find('.value');
@@ -160,7 +162,7 @@ function UsersSelect(currentUser, els, options = {}) {
});
};
- $('.assign-to-me-link').on('click', e => {
+ $assignToMeLink.on('click', e => {
e.preventDefault();
$(e.currentTarget).hide();
@@ -224,7 +226,9 @@ function UsersSelect(currentUser, els, options = {}) {
});
};
collapsedAssigneeTemplate = template(
- '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>',
+ `<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> ${spriteIcon(
+ 'user',
+ )} <% } %>`,
);
assigneeTemplate = template(
`<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
@@ -233,14 +237,14 @@ function UsersSelect(currentUser, els, options = {}) {
closingTag: '</a>',
})}</span> <% } %>`,
);
- return $dropdown.glDropdown({
+ return initDeprecatedJQueryDropdown($dropdown, {
showMenuAbove,
data(term, callback) {
return userSelect.users(term, options, users => {
// GitLabDropdownFilter returns this.instance
// GitLabDropdownRemote returns this.options.instance
- const glDropdown = this.instance || this.options.instance;
- glDropdown.options.processData(term, users, callback);
+ const deprecatedJQueryDropdown = this.instance || this.options.instance;
+ deprecatedJQueryDropdown.options.processData(term, users, callback);
});
},
processData(term, data, callback) {
@@ -349,7 +353,7 @@ function UsersSelect(currentUser, els, options = {}) {
callback(users);
if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
+ $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
}
},
filterable: true,
@@ -359,13 +363,13 @@ function UsersSelect(currentUser, els, options = {}) {
},
selectable: true,
fieldName: $dropdown.data('fieldName'),
- toggleLabel(selected, el, glDropdown) {
- const inputValue = glDropdown.filterInput.val();
+ toggleLabel(selected, el, deprecatedJQueryDropdown) {
+ const inputValue = deprecatedJQueryDropdown.filterInput.val();
if (this.multiSelect && inputValue === '') {
// Remove non-users from the fullData array
- const users = glDropdown.filteredFullData();
- const callback = glDropdown.parseData.bind(glDropdown);
+ const users = deprecatedJQueryDropdown.filteredFullData();
+ const callback = deprecatedJQueryDropdown.parseData.bind(deprecatedJQueryDropdown);
// Update the data model
this.processData(inputValue, users, callback);
@@ -448,9 +452,9 @@ function UsersSelect(currentUser, els, options = {}) {
}
if (getSelected().find(u => u === gon.current_user_id)) {
- $('.assign-to-me-link').hide();
+ $assignToMeLink.hide();
} else {
- $('.assign-to-me-link').show();
+ $assignToMeLink.show();
}
}
@@ -557,92 +561,99 @@ function UsersSelect(currentUser, els, options = {}) {
},
});
});
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- $('.ajax-users-select').each((i, select) => {
- const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
- options.skipLdap = $(select).hasClass('skip_ldap');
- const showNullUser = $(select).data('nullUser');
- const showAnyUser = $(select).data('anyUser');
- const showEmailUser = $(select).data('emailUser');
- const firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: __('Search for a user'),
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query(query) {
- return userSelect.users(query.term, options, users => {
- let name;
- const data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- const ref = data.results;
-
- for (let index = 0, len = ref.length; index < len; index += 1) {
- const obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
+
+ if ($('.ajax-users-select').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-users-select').each((i, select) => {
+ const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ const showNullUser = $(select).data('nullUser');
+ const showAnyUser = $(select).data('anyUser');
+ const showEmailUser = $(select).data('emailUser');
+ const firstUser = $(select).data('firstUser');
+ return $(select).select2({
+ placeholder: __('Search for a user'),
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query(query) {
+ return userSelect.users(query.term, options, users => {
+ let name;
+ const data = {
+ results: users,
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ const ref = data.results;
+
+ for (let index = 0, len = ref.length; index < len; index += 1) {
+ const obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
}
}
- }
- if (showNullUser) {
- const nullUser = {
- name: s__('UsersSelect|Unassigned'),
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = s__('UsersSelect|Any User');
+ if (showNullUser) {
+ const nullUser = {
+ name: s__('UsersSelect|Unassigned'),
+ id: 0,
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = s__('UsersSelect|Any User');
+ }
+ const anyUser = {
+ name,
+ id: null,
+ };
+ data.results.unshift(anyUser);
}
- const anyUser = {
- name,
- id: null,
+ }
+ if (
+ showEmailUser &&
+ data.results.length === 0 &&
+ query.term.match(/^[^@]+@[^@]+$/)
+ ) {
+ const trimmed = query.term.trim();
+ const emailUser = {
+ name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
+ username: trimmed,
+ id: trimmed,
+ invite: true,
};
- data.results.unshift(anyUser);
+ data.results.unshift(emailUser);
}
- }
- if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
- const trimmed = query.term.trim();
- const emailUser = {
- name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
- });
- },
- initSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.initSelection.apply(userSelect, args);
- },
- formatResult() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatResult.apply(userSelect, args);
- },
- formatSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatSelection.apply(userSelect, args);
- },
- dropdownCssClass: 'ajax-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
+ return query.callback(data);
+ });
+ },
+ initSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.initSelection.apply(userSelect, args);
+ },
+ formatResult() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatResult.apply(userSelect, args);
+ },
+ formatSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatSelection.apply(userSelect, args);
+ },
+ dropdownCssClass: 'ajax-users-dropdown',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
});
- });
- })
- .catch(() => {});
+ })
+ .catch(() => {});
+ }
}
UsersSelect.prototype.initSelection = function(element, callback) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
index 24cd9d6428d..55fa24fb51a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
@@ -1,11 +1,10 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
export default {
components: {
GlLink,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,7 +34,7 @@ export default {
target="_blank"
class="d-flex-center pl-1"
>
- <icon name="question" />
+ <gl-icon name="question" />
</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
index a7ab11290eb..66de4f8b682 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
@@ -11,3 +11,9 @@ export const CANCELED = 'canceled';
export const STOPPING = 'stopping';
export const DEPLOYING = 'deploying';
export const REDEPLOYING = 'redeploying';
+
+export const ACT_BUTTON_ICONS = {
+ play: 'play',
+ repeat: 'repeat',
+ stop: 'stop',
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue
index 7d74d5531b4..cc3efae565a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue
@@ -1,12 +1,12 @@
<script>
-import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import { RUNNING } from './constants';
export default {
name: 'DeploymentActionButton',
components: {
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,6 +35,10 @@ export default {
required: false,
default: '',
},
+ icon: {
+ type: String,
+ required: true,
+ },
},
computed: {
isActionInProgress() {
@@ -58,18 +62,19 @@ export default {
</script>
<template>
- <span v-gl-tooltip :title="actionInProgressTooltip" class="d-inline-block" tabindex="0">
- <gl-deprecated-button
+ <span v-gl-tooltip :title="actionInProgressTooltip" class="gl-display-inline-block" tabindex="0">
+ <gl-button
v-gl-tooltip
+ category="primary"
+ size="small"
:title="buttonTitle"
:loading="isLoading"
:disabled="isActionInProgress"
- :class="`btn btn-default btn-sm inline gl-ml-2 ${containerClasses}`"
+ :class="`inline gl-ml-2 ${containerClasses}`"
+ :icon="icon"
@click="$emit('click')"
>
- <span class="d-inline-flex align-items-baseline">
- <slot> </slot>
- </span>
- </gl-deprecated-button>
+ <slot> </slot>
+ </gl-button>
</span>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
index af0b4087d46..208df03b6a4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
@@ -1,5 +1,4 @@
<script>
-import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -7,14 +6,22 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MRWidgetService from '../../services/mr_widget_service';
import DeploymentActionButton from './deployment_action_button.vue';
import DeploymentViewButton from './deployment_view_button.vue';
-import { MANUAL_DEPLOY, FAILED, SUCCESS, STOPPING, DEPLOYING, REDEPLOYING } from './constants';
+import {
+ MANUAL_DEPLOY,
+ FAILED,
+ SUCCESS,
+ STOPPING,
+ DEPLOYING,
+ REDEPLOYING,
+ ACT_BUTTON_ICONS,
+} from './constants';
export default {
name: 'DeploymentActions',
+ btnIcons: ACT_BUTTON_ICONS,
components: {
DeploymentActionButton,
DeploymentViewButton,
- GlIcon,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -151,10 +158,10 @@ export default {
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.DEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
+ :icon="$options.btnIcons.play"
container-classes="js-manual-deploy-action"
@click="deployManually"
>
- <gl-icon name="play" />
<span>{{ $options.actionsConfiguration[constants.DEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-action-button
@@ -162,10 +169,10 @@ export default {
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
+ :icon="$options.btnIcons.repeat"
container-classes="js-manual-redeploy-action"
@click="redeploy"
>
- <gl-icon name="repeat" />
<span>{{ $options.actionsConfiguration[constants.REDEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-view-button
@@ -181,10 +188,9 @@ export default {
:computed-deployment-status="computedDeploymentStatus"
:actions-configuration="$options.actionsConfiguration[constants.STOPPING]"
:button-title="$options.actionsConfiguration[constants.STOPPING].buttonText"
+ :icon="$options.btnIcons.stop"
container-classes="js-stop-env"
@click="stopEnvironment"
- >
- <gl-icon name="stop" />
- </deployment-action-button>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index b12250d1d1c..157d6d60290 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -1,17 +1,23 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
+import { GlButtonGroup, GlDropdown, GlDropdownItem, GlLink, GlSearchBoxByType } from '@gitlab/ui';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import ReviewAppLink from '../review_app_link.vue';
export default {
name: 'DeploymentViewButton',
components: {
- FilteredSearchDropdown,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
GlLink,
+ GlSearchBoxByType,
ReviewAppLink,
VisualReviewAppLink: () =>
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
+ directives: {
+ autofocusonshow,
+ },
props: {
appButtonText: {
type: Object,
@@ -37,6 +43,9 @@ export default {
}),
},
},
+ data() {
+ return { searchTerm: '' };
+ },
computed: {
deploymentExternalUrl() {
if (this.deployment.changes && this.deployment.changes.length === 1) {
@@ -47,44 +56,52 @@ export default {
shouldRenderDropdown() {
return this.deployment.changes && this.deployment.changes.length > 1;
},
+ filteredChanges() {
+ return this.deployment?.changes?.filter(change => change.path.includes(this.searchTerm));
+ },
},
};
</script>
-
<template>
<span>
- <filtered-search-dropdown
- v-if="shouldRenderDropdown"
- class="js-mr-wigdet-deployment-dropdown inline"
- :items="deployment.changes"
- :main-action-link="deploymentExternalUrl"
- filter-key="path"
- >
- <template #mainAction="{ className }">
- <review-app-link
- :display="appButtonText"
- :link="deploymentExternalUrl"
- :css-class="`deploy-link js-deploy-url inline ${className}`"
+ <gl-button-group v-if="shouldRenderDropdown" size="small">
+ <review-app-link
+ :display="appButtonText"
+ :link="deploymentExternalUrl"
+ size="small"
+ css-class="deploy-link js-deploy-url inline"
+ />
+ <gl-dropdown size="small" class="js-mr-wigdet-deployment-dropdown">
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ v-autofocusonshow
+ autofocus
+ class="gl-m-3"
/>
- </template>
-
- <template #result="{ result }">
- <gl-link
- :href="result.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-url-menu-item menu-item"
+ <gl-dropdown-item
+ v-for="change in filteredChanges"
+ :key="change.path"
+ class="js-filtered-dropdown-result"
>
- <strong class="str-truncated-100 gl-mb-0 d-block">{{ result.path }}</strong>
-
- <p class="text-secondary str-truncated-100 gl-mb-0 d-block">{{ result.external_url }}</p>
- </gl-link>
- </template>
- </filtered-search-dropdown>
+ <gl-link
+ :href="change.external_url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-url-menu-item menu-item"
+ >
+ <strong class="str-truncated-100 gl-mb-0 gl-display-block">{{ change.path }}</strong>
+ <p class="text-secondary str-truncated-100 gl-mb-0 d-block">
+ {{ change.external_url }}
+ </p>
+ </gl-link>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </gl-button-group>
<review-app-link
v-else
:display="appButtonText"
:link="deploymentExternalUrl"
+ size="small"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
/>
<visual-review-app-link
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
index fe41a15979e..9b2cd41092e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
@@ -1,5 +1,6 @@
<script>
-import { sprintf, s__ } from '~/locale';
+import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import { backOff } from '~/lib/utils/common_utils';
@@ -10,6 +11,9 @@ export default {
name: 'MemoryUsage',
components: {
MemoryGraph,
+ GlLoadingIcon,
+ GlSprintf,
+ GlLink,
},
props: {
metricsUrl: {
@@ -47,45 +51,22 @@ export default {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
},
memoryChangeMessage() {
- const messageProps = {
- memoryFrom: this.memoryFrom,
- memoryTo: this.memoryTo,
- metricsLinkStart: `<a href="${this.metricsMonitoringUrl}">`,
- metricsLinkEnd: '</a>',
- emphasisStart: '<b>',
- emphasisEnd: '</b>',
- };
const memoryTo = Number(this.memoryTo);
const memoryFrom = Number(this.memoryFrom);
- let memoryUsageMsg = '';
if (memoryTo > memoryFrom) {
- memoryUsageMsg = sprintf(
- s__(
- 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
- ),
- messageProps,
- false,
+ return s__(
+ 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
);
} else if (memoryTo < memoryFrom) {
- memoryUsageMsg = sprintf(
- s__(
- 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
- ),
- messageProps,
- false,
- );
- } else {
- memoryUsageMsg = sprintf(
- s__(
- 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB',
- ),
- messageProps,
- false,
+ return s__(
+ 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
);
}
- return memoryUsageMsg;
+ return s__(
+ 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB',
+ );
},
},
mounted() {
@@ -155,14 +136,23 @@ export default {
<template>
<div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
<p v-if="shouldShowLoading" class="usage-info js-usage-info usage-info-loading">
- <i class="fa fa-spinner fa-spin usage-info-load-spinner" aria-hidden="true"> </i
- >{{ s__('mrWidget|Loading deployment statistics') }}
+ <gl-loading-icon class="usage-info-load-spinner" />{{
+ s__('mrWidget|Loading deployment statistics')
+ }}
+ </p>
+ <p v-if="shouldShowMemoryGraph" class="usage-info js-usage-info">
+ <gl-sprintf :message="memoryChangeMessage">
+ <template #metricsLink="{ content }">
+ <gl-link :href="metricsMonitoringUrl">{{ content }}</gl-link>
+ </template>
+ <template #emphasis="{content}">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #memoryFrom>{{ memoryFrom }}</template>
+ <template #memoryTo>{{ memoryTo }}</template>
+ </gl-sprintf>
</p>
- <p
- v-if="shouldShowMemoryGraph"
- class="usage-info js-usage-info"
- v-html="memoryChangeMessage"
- ></p>
+
<p v-if="shouldShowLoadFailure" class="usage-info js-usage-info usage-info-failed">
{{ s__('mrWidget|Failed to load deployment statistics') }}
</p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
index 24174c29d51..b6b5b56e5aa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
@@ -1,13 +1,12 @@
<script>
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlButton,
GlLoadingIcon,
- Icon,
+ GlIcon,
},
props: {
title: {
@@ -66,7 +65,7 @@ export default {
@click="toggleCollapsed"
>
<gl-loading-icon v-if="isLoading" />
- <icon v-else :name="arrowIconName" class="js-icon" />
+ <gl-icon v-else :name="arrowIconName" class="js-icon" />
</button>
<gl-button
variant="link"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
index 19a222462b3..a2636ce52ad 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
@@ -1,13 +1,12 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLink, GlIcon } from '@gitlab/ui';
import { WARNING, DANGER, WARNING_MESSAGE_CLASS, DANGER_MESSAGE_CLASS } from '../constants';
export default {
name: 'MrWidgetAlertMessage',
components: {
GlLink,
- Icon,
+ GlIcon,
},
props: {
type: {
@@ -40,7 +39,7 @@ export default {
<div class="m-3 ml-7" :class="messageClass">
<slot></slot>
<gl-link v-if="helpPath" :href="helpPath" target="_blank">
- <icon :size="16" name="question-o" class="align-middle" />
+ <gl-icon :size="16" name="question-o" class="align-middle" />
</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 897f706290d..814d4e8341e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,24 +1,33 @@
<script>
+/* eslint-disable vue/no-v-html */
import Mousetrap from 'mousetrap';
import { escape } from 'lodash';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
-import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import MrWidgetIcon from './mr_widget_icon.vue';
export default {
name: 'MRWidgetHeader',
components: {
- Icon,
clipboardButton,
TooltipOnTruncate,
MrWidgetIcon,
+ GlButton,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
mr: {
@@ -124,62 +133,59 @@ export default {
<div class="branch-actions d-flex">
<template v-if="mr.isOpen">
- <a
+ <span
v-if="!mr.sourceBranchRemoved"
- v-tooltip
- :href="webIdePath"
+ v-gl-tooltip
:title="ideButtonTitle"
- :class="{ disabled: !mr.canPushToSourceBranch }"
- class="btn btn-default js-web-ide d-none d-md-inline-block gl-mr-3"
- data-placement="bottom"
- tabindex="0"
- role="button"
- data-qa-selector="open_in_web_ide_button"
+ class="gl-display-none d-md-inline-block gl-mr-3"
+ :tabindex="!mr.canPushToSourceBranch ? 0 : null"
>
- {{ s__('mrWidget|Open in Web IDE') }}
- </a>
- <button
+ <gl-button
+ :href="webIdePath"
+ :disabled="!mr.canPushToSourceBranch"
+ class="js-web-ide"
+ tabindex="0"
+ role="button"
+ data-qa-selector="open_in_web_ide_button"
+ >
+ {{ s__('mrWidget|Open in Web IDE') }}
+ </gl-button>
+ </span>
+ <gl-button
:disabled="mr.sourceBranchRemoved"
data-target="#modal_merge_info"
data-toggle="modal"
- class="btn btn-default js-check-out-branch gl-mr-3"
- type="button"
+ class="js-check-out-branch gl-mr-3"
>
{{ s__('mrWidget|Check out branch') }}
- </button>
+ </gl-button>
</template>
- <span class="dropdown">
- <button
- type="button"
- class="btn dropdown-toggle qa-dropdown-toggle"
- data-toggle="dropdown"
- :aria-label="__('Download as')"
- aria-haspopup="true"
- aria-expanded="false"
+ <gl-dropdown
+ v-gl-tooltip
+ :title="__('Download as')"
+ :aria-label="__('Download as')"
+ icon="download"
+ right
+ data-qa-selector="download_dropdown"
+ >
+ <gl-dropdown-section-header>{{ s__('Download as') }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ :href="mr.emailPatchesPath"
+ class="js-download-email-patches"
+ download
+ data-qa-selector="download_email_patches"
>
- <icon name="download" /> <i class="fa fa-caret-down" aria-hidden="true"> </i>
- </button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li>
- <a
- :href="mr.emailPatchesPath"
- class="js-download-email-patches qa-download-email-patches"
- download
- >
- {{ s__('mrWidget|Email patches') }}
- </a>
- </li>
- <li>
- <a
- :href="mr.plainDiffPath"
- class="js-download-plain-diff qa-download-plain-diff"
- download
- >
- {{ s__('mrWidget|Plain diff') }}
- </a>
- </li>
- </ul>
- </span>
+ {{ s__('mrWidget|Email patches') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ :href="mr.plainDiffPath"
+ class="js-download-plain-diff"
+ download
+ data-qa-selector="download_plain_diff"
+ >
+ {{ s__('mrWidget|Plain diff') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
index e1659d9a167..472df8e3110 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
@@ -1,8 +1,8 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
- components: { Icon },
+ components: { GlIcon },
props: {
name: {
type: String,
@@ -14,6 +14,6 @@ export default {
<template>
<div class="circle-icon-container gl-mr-3 align-self-start align-self-lg-center">
- <icon :name="name" :size="24" />
+ <gl-icon :name="name" :size="24" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 7326bd0804d..5066a88b52b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -1,8 +1,15 @@
<script>
-/* eslint-disable vue/require-default-prop */
-import { GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+/* eslint-disable vue/require-default-prop, vue/no-v-html */
+import {
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlSprintf,
+ GlTooltip,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
-import { s__ } from '~/locale';
+import { s__, n__ } from '~/locale';
import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
@@ -15,6 +22,7 @@ export default {
GlLoadingIcon,
GlIcon,
GlSprintf,
+ GlTooltip,
PipelineStage,
TooltipOnTruncate,
LinkedPipelinesMiniList: () =>
@@ -33,6 +41,11 @@ export default {
type: String,
required: false,
},
+ buildsWithCoverage: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
// This prop needs to be camelCase, html attributes are case insensive
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
hasCi: {
@@ -100,6 +113,16 @@ export default {
}
return '';
},
+ pipelineCoverageJobNumberText() {
+ return n__('from %d job', 'from %d jobs', this.buildsWithCoverage.length);
+ },
+ pipelineCoverageTooltipDescription() {
+ return n__(
+ 'Coverage value for this pipeline was calculated by the coverage value of %d job.',
+ 'Coverage value for this pipeline was calculated by averaging the resulting coverage values of %d jobs.',
+ this.buildsWithCoverage.length,
+ );
+ },
},
errorText: s__(
'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
@@ -139,7 +162,7 @@ export default {
>
<gl-icon
name="question"
- :small="12"
+ :size="12"
tabindex="0"
role="text"
:aria-label="__('Link to go to GitLab pipeline documentation')"
@@ -189,14 +212,30 @@ export default {
</div>
<div v-if="pipeline.coverage" class="coverage" data-testid="pipeline-coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
-
<span
v-if="pipelineCoverageDelta"
:class="coverageDeltaClass"
data-testid="pipeline-coverage-delta"
+ >({{ pipelineCoverageDelta }}%)</span
>
- ({{ pipelineCoverageDelta }}%)
+
+ {{ pipelineCoverageJobNumberText }}
+ <span ref="pipelineCoverageQuestion">
+ <gl-icon name="question" :size="12" />
</span>
+ <gl-tooltip
+ :target="() => $refs.pipelineCoverageQuestion"
+ data-testid="pipeline-coverage-tooltip"
+ >
+ {{ pipelineCoverageTooltipDescription }}
+ <div
+ v-for="(build, index) in buildsWithCoverage"
+ :key="`${build.name}-${index}`"
+ class="gl-mt-3 gl-text-left gl-px-4"
+ >
+ {{ build.name }} ({{ build.coverage }}%)
+ </div>
+ </gl-tooltip>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 5c307b5ff0c..55efd7e7d3b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -77,6 +77,7 @@ export default {
<mr-widget-pipeline
:pipeline="pipeline"
:pipeline-coverage-delta="mr.pipelineCoverageDelta"
+ :builds-with-coverage="mr.buildsWithCoverage"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
:pipeline-must-succeed="mr.onlyAllowMergeIfPipelineSucceeds"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index 1b3b589c32f..56a50b55f9d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { s__ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index 936fdc9aff5..a9d148505e1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -108,7 +108,9 @@ export default {
</div>
</template>
<div class="row">
- <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n1 pt-md-1 svg-content svg-225">
+ <div
+ class="col-md-5 order-md-last col-12 gl-mt-5 gl-mt-md-n2! gl-pt-md-2 svg-content svg-225"
+ >
<img data-testid="pipeline-image" :src="pipelineSvgPath" />
</div>
<div class="col-md-7 order-md-first col-12">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
index c38c41f13b6..ebd2b5cd22d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -1,10 +1,10 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlButton,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -22,20 +22,26 @@ export default {
type: String,
required: true,
},
+ size: {
+ type: String,
+ required: false,
+ default: 'medium',
+ },
},
};
</script>
<template>
- <a
+ <gl-button
v-gl-tooltip
:title="display.tooltip"
:href="link"
+ :size="size"
target="_blank"
rel="noopener noreferrer nofollow"
:class="cssClass"
data-track-event="open_review_app"
data-track-label="review_app"
>
- {{ display.text }} <icon class="fgray" name="external-link" />
- </a>
+ {{ display.text }} <gl-icon class="fgray" name="external-link" />
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
index dab0540f44e..859f2c57598 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlSprintf } from '@gitlab/ui';
import tooltip from '../../vue_shared/directives/tooltip';
import { __ } from '../../locale';
@@ -9,6 +9,7 @@ export default {
tooltipTitle: __('A user with write access to the source branch selected this option'),
},
components: {
+ GlIcon,
GlSprintf,
},
directives: {
@@ -26,12 +27,11 @@ export default {
</template>
</gl-sprintf>
</span>
- <i
+ <gl-icon
v-tooltip
:title="$options.i18n.tooltipTitle"
:aria-label="$options.i18n.tooltipTitle"
- class="fa fa-question-circle"
- >
- </i>
+ name="question-o"
+ />
</p>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
index d52e6d38ac6..bdcea9871ea 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -1,13 +1,12 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlButton } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, n__, sprintf, s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- Icon,
- GlDeprecatedButton,
+ GlButton,
},
props: {
isSquashEnabled: {
@@ -80,20 +79,19 @@ export default {
class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2"
@click="toggle()"
>
- <gl-deprecated-button
+ <gl-button
:aria-label="ariaLabel"
- variant="blank"
- class="commit-edit-toggle square s24 gl-mr-3"
+ category="tertiary"
+ class="commit-edit-toggle gl-mr-3"
+ :icon="collapseIcon"
@click.stop="toggle()"
- >
- <icon :name="collapseIcon" :size="16" />
- </gl-deprecated-button>
+ />
<span v-if="expanded">{{ __('Collapse') }}</span>
<span v-else>
<span class="vertical-align-middle" v-html="message"></span>
- <gl-deprecated-button variant="link" class="modify-message-button">
+ <gl-button variant="link" class="modify-message-button">
{{ modifyLinkMessage }}
- </gl-deprecated-button>
+ </gl-button>
</span>
</div>
<div v-show="expanded"><slot></slot></div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 9df0c045fe4..a5ec095b8ec 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { n__ } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -8,6 +9,7 @@ export default {
name: 'MRWidgetFailedToMerge',
components: {
+ GlButton,
statusIcon,
},
@@ -84,14 +86,14 @@ export default {
<span v-else> {{ s__('mrWidget|Merge failed.') }} </span>
<span :class="{ 'has-custom-error': mr.mergeError }"> {{ timerText }} </span>
</span>
- <button
- class="btn btn-default btn-sm js-refresh-button"
+ <gl-button
+ size="small"
+ data-testid="merge-request-failed-refresh-button"
data-qa-selector="merge_request_error_content"
- type="button"
@click="refresh"
>
{{ s__('mrWidget|Refresh now') }}
- </button>
+ </gl-button>
</div>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 166700dbcbf..58839251edc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,6 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import { s__, __ } from '~/locale';
@@ -19,6 +19,7 @@ export default {
statusIcon,
ClipboardButton,
GlLoadingIcon,
+ GlButton,
},
props: {
mr: {
@@ -112,48 +113,52 @@ export default {
:date-title="mr.metrics.mergedAt"
:date-readable="mr.metrics.readableMergedAt"
/>
- <a
+ <gl-button
v-if="mr.canRevertInCurrentMR"
v-tooltip
:title="revertTitle"
- class="btn btn-close btn-sm"
+ size="small"
+ category="secondary"
+ variant="warning"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
>
{{ revertLabel }}
- </a>
- <a
+ </gl-button>
+ <gl-button
v-else-if="mr.revertInForkPath"
v-tooltip
:href="mr.revertInForkPath"
:title="revertTitle"
- class="btn btn-close btn-sm"
+ size="small"
+ category="secondary"
+ variant="warning"
data-method="post"
>
{{ revertLabel }}
- </a>
- <a
+ </gl-button>
+ <gl-button
v-if="mr.canCherryPickInCurrentMR"
v-tooltip
:title="cherryPickTitle"
- class="btn btn-default btn-sm"
+ size="small"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
>
{{ cherryPickLabel }}
- </a>
- <a
+ </gl-button>
+ <gl-button
v-else-if="mr.cherryPickInForkPath"
v-tooltip
:href="mr.cherryPickInForkPath"
:title="cherryPickTitle"
- class="btn btn-default btn-sm"
+ size="small"
data-method="post"
>
{{ cherryPickLabel }}
- </a>
+ </gl-button>
</div>
<section class="mr-info-list" data-qa-selector="merged_status_content">
<p>
@@ -181,14 +186,14 @@ export default {
</p>
<p v-if="shouldShowRemoveSourceBranch" class="space-children">
<span>{{ s__('mrWidget|You can delete the source branch now') }}</span>
- <button
+ <gl-button
:disabled="isMakingRequest"
- type="button"
- class="btn btn-sm btn-default js-remove-branch-button"
+ size="small"
+ class="js-remove-branch-button"
@click="removeSourceBranch"
>
{{ s__('mrWidget|Delete source branch') }}
- </button>
+ </gl-button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<gl-loading-icon :inline="true" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
index 8f38ca69453..83783528cc1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -9,6 +10,7 @@ export default {
tooltip,
},
components: {
+ GlIcon,
statusIcon,
},
props: {
@@ -50,7 +52,7 @@ export default {
<span class="bold js-branch-text">
<span class="capitalize"> {{ missingBranchName }} </span>
{{ s__('mrWidget|branch does not exist.') }} {{ missingBranchNameMessage }}
- <i v-tooltip :title="message" :aria-label="message" class="fa fa-question-circle"> </i>
+ <gl-icon v-tooltip :title="message" :aria-label="message" name="question-o" />
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 794c994bffe..ec0934c5b4b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlLoadingIcon } from '@gitlab/ui';
import { escape } from 'lodash';
import simplePoll from '../../../lib/utils/simple_poll';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 4d7d49398eb..14a29483d3c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 930a2b68d8e..240bab58297 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import { isEmpty } from 'lodash';
-import { GlIcon, GlDeprecatedButton, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlIcon, GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
@@ -37,7 +38,7 @@ export default {
GlIcon,
GlSprintf,
GlLink,
- GlDeprecatedButton,
+ GlButton,
MergeTrainHelperText: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'),
MergeImmediatelyConfirmationDialog: () =>
@@ -297,16 +298,16 @@ export default {
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group">
- <gl-deprecated-button
- size="sm"
+ <gl-button
+ size="medium"
+ category="primary"
class="qa-merge-button accept-merge-request"
:variant="mergeButtonVariant"
:disabled="isMergeButtonDisabled"
:loading="isMakingRequest"
@click="handleMergeButtonClick(isAutoMergeAvailable)"
+ >{{ mergeButtonText }}</gl-button
>
- {{ mergeButtonText }}
- </gl-deprecated-button>
<button
v-if="shouldShowMergeImmediatelyDropdown"
:disabled="isMergeButtonDisabled"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index 3cf7dc3c4d1..6608381f348 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -1,11 +1,11 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import { __ } from '~/locale';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -62,7 +62,7 @@ export default {
rel="noopener noreferrer nofollow"
data-container="body"
>
- <icon name="question" />
+ <gl-icon name="question" />
</a>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 44668170fe4..61cc950f058 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -55,13 +55,15 @@ export default {
},
methods: {
removeWipMutation() {
+ const { mergeRequestQueryVariables } = this;
+
this.isMakingRequest = true;
this.$apollo
.mutate({
mutation: removeWipMutation,
variables: {
- ...this.mergeRequestQueryVariables,
+ ...mergeRequestQueryVariables,
wip: false,
},
update(
@@ -83,14 +85,14 @@ export default {
const data = store.readQuery({
query: getStateQuery,
- variables: this.mergeRequestQueryVariables,
+ variables: mergeRequestQueryVariables,
});
data.project.mergeRequest.workInProgress = workInProgress;
data.project.mergeRequest.title = title;
store.writeQuery({
query: getStateQuery,
data,
- variables: this.mergeRequestQueryVariables,
+ variables: mergeRequestQueryVariables,
});
},
optimisticResponse: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
index c7d9453a5c9..4de41dd5887 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 36a883869f1..43ce748b41d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -7,6 +7,7 @@ import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
+import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import { deprecatedCreateFlash as createFlash } from '../flash';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import Loading from './components/loading.vue';
@@ -96,12 +97,11 @@ export default {
variables() {
return this.mergeRequestQueryVariables;
},
- result({
- data: {
- project: { mergeRequest },
- },
- }) {
- this.mr.setGraphqlData(mergeRequest);
+ result({ data: { project } }) {
+ if (project) {
+ this.mr.setGraphqlData(project);
+ this.loading = false;
+ }
},
},
},
@@ -120,9 +120,17 @@ export default {
mr: store,
state: store && store.state,
service: store && this.createService(store),
+ loading: true,
};
},
computed: {
+ isLoaded() {
+ if (window.gon?.features?.mergeRequestWidgetGraphql) {
+ return !this.loading;
+ }
+
+ return this.mr;
+ },
shouldRenderApprovals() {
return this.mr.state !== 'nothingToMerge';
},
@@ -195,7 +203,10 @@ export default {
},
mounted() {
MRWidgetService.fetchInitialData()
- .then(({ data }) => this.initWidget(data))
+ .then(({ data, headers }) => {
+ this.startingPollInterval = Number(headers['POLL-INTERVAL']);
+ this.initWidget(data);
+ })
.catch(() =>
createFlash(__('Unable to load the merge request widget. Try reloading the page.')),
);
@@ -285,9 +296,10 @@ export default {
initPolling() {
this.pollingInterval = new SmartInterval({
callback: this.checkStatus,
- startingInterval: 10 * 1000,
- maxInterval: 240 * 1000,
- hiddenInterval: window.gon?.features?.widgetVisibilityPolling && 360 * 1000,
+ startingInterval: this.startingPollInterval,
+ maxInterval: this.startingPollInterval + secondsToMilliseconds(4 * 60),
+ hiddenInterval:
+ window.gon?.features?.widgetVisibilityPolling && secondsToMilliseconds(6 * 60),
incrementByFactorOf: 2,
});
},
@@ -409,7 +421,7 @@ export default {
};
</script>
<template>
- <div v-if="mr" class="mr-state-widget gl-mt-3">
+ <div v-if="isLoaded" class="mr-state-widget gl-mt-3">
<mr-widget-header :mr="mr" />
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
index 488397e7735..44fc1cc7f23 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -1,7 +1,27 @@
query getState($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ archived
+ onlyAllowMergeIfPipelineSucceeds
+
mergeRequest(iid: $iid) {
- title
+ autoMergeEnabled
+ commitCount
+ conflicts
+ diffHeadSha
+ mergeError
+ mergeStatus
+ mergeableDiscussionsState
+ pipelines(first: 1) {
+ nodes {
+ status
+ }
+ }
+ shouldBeRebased
+ sourceBranchExists
+ targetBranchExists
+ userPermissions {
+ canMerge
+ }
workInProgress
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index ee9e3cc6d08..2ad15f231bb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -1,3 +1,4 @@
+import { normalizeHeaders } from '~/lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
export default class MRWidgetService {
@@ -82,6 +83,11 @@ export default class MRWidgetService {
return Promise.all([
axios.get(window.gl.mrWidgetData.merge_request_cached_widget_path),
axios.get(window.gl.mrWidgetData.merge_request_widget_path),
- ]).then(axios.spread((res, cachedRes) => ({ data: Object.assign(res.data, cachedRes.data) })));
+ ]).then(
+ axios.spread((res, cachedRes) => ({
+ data: Object.assign(res.data, cachedRes.data),
+ headers: normalizeHeaders(res.headers),
+ })),
+ );
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js
index 5215d210e1c..15d67ea18ea 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js
@@ -1,6 +1,5 @@
import { s__, n__ } from '~/locale';
-// eslint-disable-next-line import/prefer-default-export
export const title = state => {
if (state.isLoading) {
return s__('BuildArtifacts|Loading artifacts');
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 3bd512c89bf..9d3f4eb01ed 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -17,7 +17,7 @@ export default function deviseState() {
return stateKey.pipelineFailed;
} else if (this.workInProgress) {
return stateKey.workInProgress;
- } else if (this.hasMergeableDiscussionsState) {
+ } else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
return stateKey.pipelineBlocked;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 8c98ba1b023..846b1c453a1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -43,15 +43,14 @@ export default class MergeRequestStore {
this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path;
- this.mergeStatus = data.merge_status;
this.commitMessage = data.default_merge_commit_message;
- this.shortMergeCommitSha = data.short_merge_commit_sha;
- this.mergeCommitSha = data.merge_commit_sha;
+ this.shortMergeCommitSha = data.short_merged_commit_sha;
+ this.mergeCommitSha = data.merged_commit_sha;
this.commitMessageWithDescription = data.default_merge_commit_message_with_description;
- this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.pipelineCoverageDelta = data.pipeline_coverage_delta;
+ this.buildsWithCoverage = data.builds_with_coverage;
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
@@ -60,9 +59,6 @@ export default class MergeRequestStore {
this.rebaseInProgress = data.rebase_in_progress;
this.mergeRequestDiffsPath = data.diffs_path;
this.approvalsWidgetType = data.approvals_widget_type;
- this.projectArchived = data.project_archived;
- this.branchMissing = data.branch_missing;
- this.hasConflicts = data.has_conflicts;
if (data.issues_links) {
const links = data.issues_links;
@@ -80,25 +76,18 @@ export default class MergeRequestStore {
this.setToAutoMergeBy = MergeRequestStore.formatUserObject(data.merge_user || {});
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
- this.mergeError = data.merge_error;
this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = data.remove_source_branch || false;
- this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
- this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
this.autoMergeStrategy = data.auto_merge_strategy;
this.availableAutoMergeStrategies = data.available_auto_merge_strategies;
this.preferredAutoMergeStrategy = MergeRequestStore.getPreferredAutoMergeStrategy(
this.availableAutoMergeStrategies,
);
this.ffOnlyEnabled = data.ff_only_enabled;
- this.shouldBeRebased = Boolean(data.should_be_rebased);
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.mergeRequestState = data.state;
this.isOpen = this.mergeRequestState === 'opened';
- this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
- this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.latestSHA = data.diff_head_sha;
- this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
this.allowCollaboration = data.allow_collaboration;
@@ -108,13 +97,13 @@ export default class MergeRequestStore {
// CI related
this.hasCI = data.has_ci;
this.ciStatus = data.ci_status;
- this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isPipelinePassing =
this.ciStatus === 'success' || this.ciStatus === 'success-with-warnings';
this.isPipelineSkipped = this.ciStatus === 'skipped';
this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
- this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
+ this.isPipelineBlocked =
+ data.only_allow_merge_if_pipeline_succeeds && pipelineStatus?.group === 'manual';
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.terraformReportsPath = data.terraform_reports_path;
this.testResultsPath = data.test_reports_path;
@@ -133,11 +122,24 @@ export default class MergeRequestStore {
this.removeWIPPath = data.remove_wip_path;
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergePath = data.merge_path;
- this.canMerge = Boolean(data.merge_path);
- this.mergeCommitPath = data.merge_commit_path;
+ this.mergeCommitPath = data.merged_commit_path;
this.canPushToSourceBranch = data.can_push_to_source_branch;
- if (data.work_in_progress !== undefined) {
+ if (!window.gon?.features?.mergeRequestWidgetGraphql) {
+ this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
+ this.canBeMerged = data.can_be_merged || false;
+ this.canMerge = Boolean(data.merge_path);
+ this.commitsCount = data.commits_count;
+ this.branchMissing = data.branch_missing;
+ this.hasConflicts = data.has_conflicts;
+ this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
+ this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
+ this.mergeError = data.merge_error;
+ this.mergeStatus = data.merge_status;
+ this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
+ this.projectArchived = data.project_archived;
+ this.isSHAMismatch = this.sha !== data.diff_head_sha;
+ this.shouldBeRebased = Boolean(data.should_be_rebased);
this.workInProgress = data.work_in_progress;
}
@@ -154,8 +156,27 @@ export default class MergeRequestStore {
this.setState();
}
- setGraphqlData(data) {
- this.workInProgress = data.workInProgress;
+ setGraphqlData(project) {
+ const { mergeRequest } = project;
+ const pipeline = mergeRequest.pipelines?.nodes?.[0];
+
+ this.projectArchived = project.archived;
+ this.onlyAllowMergeIfPipelineSucceeds = project.onlyAllowMergeIfPipelineSucceeds;
+
+ this.autoMergeEnabled = mergeRequest.autoMergeEnabled;
+ this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged';
+ this.canMerge = mergeRequest.userPermissions.canMerge;
+ this.ciStatus = pipeline?.status.toLowerCase();
+ this.commitsCount = mergeRequest.commitCount;
+ this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists;
+ this.hasConflicts = mergeRequest.conflicts;
+ this.hasMergeableDiscussionsState = mergeRequest.mergeableDiscussionsState === false;
+ this.mergeError = mergeRequest.mergeError;
+ this.mergeStatus = mergeRequest.mergeStatus;
+ this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
+ this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
+ this.shouldBeRebased = mergeRequest.shouldBeRebased;
+ this.workInProgress = mergeRequest.workInProgress;
this.setState();
}
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue
new file mode 100644
index 00000000000..f333ab49ead
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/actions_button.vue
@@ -0,0 +1,90 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlLink,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ actions: {
+ type: Array,
+ required: true,
+ },
+ selectedKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ hasMultipleActions() {
+ return this.actions.length > 1;
+ },
+ selectedAction() {
+ return this.actions.find(x => x.key === this.selectedKey) || this.actions[0];
+ },
+ },
+ methods: {
+ handleItemClick(action) {
+ this.$emit('select', action.key);
+ },
+ handleClick(action, evt) {
+ return action.handle?.(evt);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ v-if="hasMultipleActions"
+ v-gl-tooltip="selectedAction.tooltip"
+ class="gl-button-deprecated-adapter"
+ :text="selectedAction.text"
+ :split-href="selectedAction.href"
+ split
+ @click="handleClick(selectedAction, $event)"
+ >
+ <template slot="button-content">
+ <span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs">
+ {{ selectedAction.text }}
+ </span>
+ </template>
+ <template v-for="(action, index) in actions">
+ <gl-dropdown-item
+ :key="action.key"
+ class="gl-dropdown-item-deprecated-adapter"
+ :is-check-item="true"
+ :is-checked="action.key === selectedAction.key"
+ :secondary-text="action.secondaryText"
+ :data-testid="`action_${action.key}`"
+ @click="handleItemClick(action)"
+ >
+ {{ action.text }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
+ </template>
+ </gl-dropdown>
+ <gl-link
+ v-else-if="selectedAction"
+ v-gl-tooltip="selectedAction.tooltip"
+ v-bind="selectedAction.attrs"
+ class="btn"
+ :href="selectedAction.href"
+ @click="handleClick(selectedAction, $event)"
+ >
+ {{ selectedAction.text }}
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
new file mode 100644
index 00000000000..c94e784c01e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import {
+ capitalizeFirstCharacter,
+ convertToSentenceCase,
+ splitCamelCase,
+} from '~/lib/utils/text_utility';
+
+const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!';
+const tdClass = 'gl-border-gray-100! gl-p-5!';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ },
+ props: {
+ alert: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ fields: [
+ {
+ key: 'fieldName',
+ label: s__('AlertManagement|Key'),
+ thClass,
+ tdClass,
+ formatter: string => capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))),
+ },
+ {
+ key: 'value',
+ thClass: `${thClass} w-60p`,
+ tdClass,
+ label: s__('AlertManagement|Value'),
+ },
+ ],
+ computed: {
+ items() {
+ if (!this.alert) {
+ return [];
+ }
+ return Object.entries(this.alert).map(([fieldName, value]) => ({
+ fieldName,
+ value,
+ }));
+ },
+ },
+};
+</script>
+<template>
+ <gl-table
+ class="alert-management-details-table"
+ :busy="loading"
+ :empty-text="s__('AlertManagement|No alert data to display.')"
+ :items="items"
+ :fields="$options.fields"
+ show-empty
+ >
+ <template #table-busy>
+ <gl-loading-icon size="lg" color="dark" class="gl-mt-5" />
+ </template>
+ </gl-table>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index c0a42e08dee..e1f54b62223 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { groupBy } from 'lodash';
import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index 52ce05f0d99..d0f5570db6b 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -1,4 +1,5 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import ViewerMixin from './mixins';
import { handleBlobRichViewer } from '~/blob/viewer';
@@ -7,6 +8,9 @@ export default {
components: {
MarkdownFieldView,
},
+ directives: {
+ SafeHtml,
+ },
mixins: [ViewerMixin],
mounted() {
handleBlobRichViewer(this.$refs.content, this.type);
@@ -14,5 +18,5 @@ export default {
};
</script>
<template>
- <markdown-field-view ref="content" v-html="content" />
+ <markdown-field-view ref="content" v-safe-html="content" />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 55a6267f9ff..bbe72a2b122 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
import ViewerMixin from './mixins';
import { HIGHLIGHT_CLASS_NAME } from './constants';
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 7431b7e9ed4..f28e49df56e 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -1,12 +1,11 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import getCommitIconMap from '~/ide/commit_icon';
import { __ } from '~/locale';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -81,7 +80,7 @@ export default {
:class="{ 'ml-auto': isCentered }"
class="file-changed-icon d-inline-block"
>
- <icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
+ <gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 0e0bb8735b4..d7af3b3298e 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -39,6 +39,11 @@ export default {
required: false,
default: true,
},
+ iconClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
cssClass() {
@@ -55,7 +60,7 @@ export default {
:class="cssClass"
:title="!showText ? status.text : ''"
>
- <ci-icon :status="status" />
+ <ci-icon :status="status" :css-classes="iconClasses" />
<template v-if="showText">
{{ status.text }}
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 890dbe86c0d..ff665d9cc58 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,5 +1,5 @@
<script>
-import Icon from './icon.vue';
+import { GlIcon } from '@gitlab/ui';
/**
* Renders CI icon based on API response shared between all places where it is used.
@@ -28,7 +28,7 @@ const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
status: {
@@ -66,5 +66,5 @@ export default {
};
</script>
<template>
- <span :class="cssClass"> <icon :name="icon" :size="size" :class="cssClasses" /> </span>
+ <span :class="cssClass"> <gl-icon :name="icon" :size="size" :class="cssClasses" /> </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
index 6f5ea8dcbee..5c6bd5892ae 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import {
- GlNewDropdown,
- GlNewDropdownHeader,
+ GlDropdown,
+ GlDropdownSectionHeader,
GlFormInputGroup,
GlButton,
GlTooltipDirective,
@@ -11,8 +11,8 @@ import { getHTTPProtocol } from '~/lib/utils/url_utility';
export default {
components: {
- GlNewDropdown,
- GlNewDropdownHeader,
+ GlDropdown,
+ GlDropdownSectionHeader,
GlFormInputGroup,
GlButton,
},
@@ -45,10 +45,10 @@ export default {
};
</script>
<template>
- <gl-new-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info">
+ <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info">
<div class="pb-2 mx-1">
<template v-if="sshLink">
- <gl-new-dropdown-header>{{ $options.labels.ssh }}</gl-new-dropdown-header>
+ <gl-dropdown-section-header>{{ $options.labels.ssh }}</gl-dropdown-section-header>
<div class="mx-3">
<gl-form-input-group :value="sshLink" readonly select-on-click>
@@ -67,7 +67,7 @@ export default {
</template>
<template v-if="httpLink">
- <gl-new-dropdown-header>{{ httpLabel }}</gl-new-dropdown-header>
+ <gl-dropdown-section-header>{{ httpLabel }}</gl-dropdown-section-header>
<div class="mx-3">
<gl-form-input-group :value="httpLink" readonly select-on-click>
@@ -85,5 +85,5 @@ export default {
</div>
</template>
</div>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 23bea6c28b4..c1c8fb3a6e2 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,10 +1,9 @@
<script>
import { isString, isEmpty } from 'lodash';
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
-import Icon from './icon.vue';
export default {
directives: {
@@ -12,14 +11,14 @@ export default {
},
components: {
UserAvatarLink,
- Icon,
+ GlIcon,
GlLink,
TooltipOnTruncate,
},
props: {
/**
* Indicates the existence of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
+ * Used to render the correct GlIcon, if true will render `tag` GlIcon,
* if false will render a svg sprite fork icon
*/
tag: {
@@ -141,9 +140,9 @@ export default {
<div class="branch-commit cgray">
<template v-if="shouldShowRefInfo">
<div class="icon-container">
- <icon v-if="tag" name="tag" />
- <icon v-else-if="mergeRequestRef" name="git-merge" />
- <icon v-else name="branch" />
+ <gl-icon v-if="tag" name="tag" />
+ <gl-icon v-else-if="mergeRequestRef" name="git-merge" />
+ <gl-icon v-else name="branch" />
</div>
<gl-link
@@ -163,7 +162,7 @@ export default {
>{{ commitRef.name }}</gl-link
>
</template>
- <icon name="commit" class="commit-icon js-commit-icon" />
+ <gl-icon name="commit" class="commit-icon js-commit-icon" />
<gl-link :href="commitUrl" class="commit-sha mr-0">{{ shortSha }}</gl-link>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index 3bf629d4acb..f9d3d76e7f5 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -1,12 +1,11 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import Icon from '../../icon.vue';
+import { GlLink, GlIcon } from '@gitlab/ui';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
components: {
GlLink,
- Icon,
+ GlIcon,
},
props: {
path: {
@@ -52,7 +51,7 @@ export default {
:download="fileName"
target="_blank"
>
- <icon :size="16" name="download" class="float-left gl-mr-3" />
+ <gl-icon :size="16" name="download" class="float-left gl-mr-3" />
{{ __('Download') }}
</gl-link>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index f9b678e33cd..6bb05e59f6b 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,8 +1,9 @@
<script>
+/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { forEach, escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
index 3b6b0a91e97..a7e6438a935 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlIcon,
- GlButton,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
- GlFormGroup,
-} from '@gitlab/ui';
+import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
@@ -29,8 +23,8 @@ export default {
components: {
GlIcon,
GlButton,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlFormGroup,
TooltipOnTruncate,
DateTimePickerInput,
@@ -212,7 +206,7 @@ export default {
placement="top"
class="d-inline-block"
>
- <gl-deprecated-dropdown
+ <gl-dropdown
ref="dropdown"
:text="timeWindowText"
v-bind="$attrs"
@@ -228,15 +222,15 @@ export default {
<gl-icon class="gl-dropdown-caret" name="chevron-down" aria-hidden="true" />
</template>
- <div class="d-flex justify-content-between gl-p-2-deprecated-no-really-do-not-use-me">
+ <div class="d-flex justify-content-between gl-p-2">
<gl-form-group
v-if="customEnabled"
:label="customLabel"
label-for="custom-from-time"
- label-class="gl-pb-1-deprecated-no-really-do-not-use-me"
- class="custom-time-range-form-group col-md-7 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-0 m-0"
+ label-class="gl-pb-2"
+ class="custom-time-range-form-group col-md-7 gl-pl-2 gl-pr-0 m-0"
>
- <div class="gl-pt-2-deprecated-no-really-do-not-use-me">
+ <div class="gl-pt-3">
<date-time-picker-input
id="custom-time-from"
v-model="startInput"
@@ -264,15 +258,12 @@ export default {
</gl-button>
</gl-form-group>
</gl-form-group>
- <gl-form-group
- label-for="group-id-dropdown"
- class="col-md-5 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-1-deprecated-no-really-do-not-use-me m-0"
- >
+ <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-px-2 m-0">
<template #label>
- <span class="gl-pl-5-deprecated-no-really-do-not-use-me">{{ __('Quick range') }}</span>
+ <span class="gl-pl-7">{{ __('Quick range') }}</span>
</template>
- <gl-deprecated-dropdown-item
+ <gl-dropdown-item
v-for="(option, index) in options"
:key="index"
data-qa-selector="quick_range_item"
@@ -286,9 +277,9 @@ export default {
:class="{ invisible: !isOptionActive(option) }"
/>
{{ option.label }}
- </gl-deprecated-dropdown-item>
+ </gl-dropdown-item>
</gl-form-group>
</div>
- </gl-deprecated-dropdown>
+ </gl-dropdown>
</tooltip-on-truncate>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
index 986fa14349e..8494f99fd7d 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlAlert } from '@gitlab/ui';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
index 610bce9a705..7157337f8f3 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
@@ -41,12 +41,5 @@ export default {
autocomplete="off"
/>
<i class="fa fa-search dropdown-input-search" aria-hidden="true" data-hidden="true"> </i>
- <i
- class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
- aria-hidden="true"
- data-hidden="true"
- role="button"
- >
- </i>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
index e1f336f5250..4d85726065b 100644
--- a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
@@ -1,10 +1,9 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
-import Icon from './icon.vue';
+import { GlDeprecatedButton, GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
GlDeprecatedButton,
},
props: {
@@ -73,7 +72,7 @@ export default {
data-display="static"
data-toggle="dropdown"
>
- <icon name="chevron-down" :aria-label="__('toggle dropdown')" />
+ <gl-icon name="chevron-down" :aria-label="__('toggle dropdown')" />
</button>
<ul :class="dropdownClass" class="dropdown-menu dropdown-open-top">
<template v-for="(action, index) in actions">
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index d6f591ccca1..012aca8105a 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -2,6 +2,7 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
+import { GlIcon } from '@gitlab/ui';
import Item from './item.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
@@ -9,10 +10,11 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
-const originalStopCallback = Mousetrap.stopCallback;
+const originalStopCallback = Mousetrap.prototype.stopCallback;
export default {
components: {
+ GlIcon,
Item,
VirtualList,
},
@@ -126,7 +128,7 @@ export default {
this.focusedIndex = 0;
}
- Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ Mousetrap.bind(['t', 'mod+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
@@ -134,7 +136,18 @@ export default {
this.toggle(!this.visible);
});
- Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ Mousetrap.prototype.stopCallback = function customStopCallback(e, el, combo) {
+ if (
+ (combo === 't' && el.classList.contains('dropdown-input-field')) ||
+ el.classList.contains('inputarea')
+ ) {
+ return true;
+ } else if (combo === 'mod+p') {
+ return false;
+ }
+
+ return originalStopCallback.call(this, e, el, combo);
+ };
},
methods: {
toggle(visible) {
@@ -199,18 +212,6 @@ export default {
this.cancelMouseOver = false;
this.onMouseOver(index);
},
- mousetrapStopCallback(e, el, combo) {
- if (
- (combo === 't' && el.classList.contains('dropdown-input-field')) ||
- el.classList.contains('inputarea')
- ) {
- return true;
- } else if (combo === 'command+p' || combo === 'ctrl+p') {
- return false;
- }
-
- return originalStopCallback(e, el, combo);
- },
},
};
</script>
@@ -236,12 +237,13 @@ export default {
aria-hidden="true"
class="fa fa-search dropdown-input-search"
></i>
- <i
- :aria-label="__('Clear search input')"
+ <gl-icon
+ name="close"
+ class="dropdown-input-clear"
role="button"
- class="fa fa-times dropdown-input-clear"
+ :aria-label="__('Clear search input')"
@click="clearSearchInput"
- ></i>
+ />
</div>
<div>
<virtual-list ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul">
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
index 79c62cd9938..4c496ba3f9b 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/item.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
@@ -1,6 +1,6 @@
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import FileIcon from '../file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
@@ -8,7 +8,7 @@ const MAX_PATH_LENGTH = 60;
export default {
components: {
- Icon,
+ GlIcon,
ChangedFileIcon,
FileIcon,
},
@@ -103,10 +103,10 @@ export default {
<span v-if="file.changed || file.tempFile" v-once class="diff-changed-stats">
<span v-if="showDiffStats">
<span class="cgreen bold">
- <icon name="file-addition" class="align-text-top" /> {{ file.addedLines }}
+ <gl-icon name="file-addition" class="align-text-top" /> {{ file.addedLines }}
</span>
<span class="cred bold ml-1">
- <icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }}
+ <gl-icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }}
</span>
</span>
<changed-file-icon v-else :file="file" />
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 0952e37e46e..c1c4f437dee 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -14,10 +14,20 @@ export default {
type: Object,
required: true,
},
+ fileUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
level: {
type: Number,
required: true,
},
+ fileClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
isTree() {
@@ -43,6 +53,9 @@ export default {
// don't output a title if we don't have the expanded path
return this.file?.tree?.length ? this.file.tree[0].parentPath : false;
},
+ fileRouterUrl() {
+ return this.fileUrl || `/project${this.file.url}`;
+ },
},
watch: {
'file.active': function fileActiveWatch(active) {
@@ -69,7 +82,7 @@ export default {
this.toggleTreeOpen(this.file.path);
}
- if (this.$router) this.$router.push(`/project${this.file.url}`);
+ if (this.$router && !this.hasUrlAtCurrentRoute()) this.$router.push(this.fileRouterUrl);
if (this.isBlob) this.clickedFile(this.file.path);
},
@@ -99,7 +112,7 @@ export default {
hasUrlAtCurrentRoute() {
if (!this.$router || !this.$router.currentRoute) return true;
- return this.$router.currentRoute.path === `/project${escapeFileUrl(this.file.url)}`;
+ return this.$router.currentRoute.path === escapeFileUrl(this.fileRouterUrl);
},
},
};
@@ -123,6 +136,7 @@ export default {
:style="levelIndentation"
class="file-row-name str-truncated"
data-qa-selector="file_name_content"
+ :class="fileClasses"
>
<file-icon
class="file-row-icon"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 7b3d1d0afd6..3d8afd162cb 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -1,8 +1,11 @@
+/* eslint-disable @gitlab/require-i18n-strings */
import { __ } from '~/locale';
-export const ANY_AUTHOR = 'Any';
+const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') };
+export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') };
+export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') };
-export const NO_LABEL = 'No label';
+export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL];
export const DEBOUNCE_DELAY = 200;
@@ -11,13 +14,11 @@ export const SortDirection = {
ascending: 'ascending',
};
-export const defaultMilestones = [
- // eslint-disable-next-line @gitlab/require-i18n-strings
- { value: 'None', text: __('None') },
- // eslint-disable-next-line @gitlab/require-i18n-strings
- { value: 'Any', text: __('Any') },
- // eslint-disable-next-line @gitlab/require-i18n-strings
+export const DEFAULT_MILESTONES = [
+ DEFAULT_LABEL_NONE,
+ DEFAULT_LABEL_ANY,
{ value: 'Upcoming', text: __('Upcoming') },
- // eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Started', text: __('Started') },
];
+
+/* eslint-enable @gitlab/require-i18n-strings */
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index ee293d37b66..25478ad6f4f 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -3,8 +3,8 @@ import {
GlFilteredSearch,
GlButtonGroup,
GlButton,
- GlNewDropdown as GlDropdown,
- GlNewDropdownItem as GlDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlTooltipDirective,
} from '@gitlab/ui';
@@ -15,7 +15,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
-import { stripQuotes } from './filtered_search_utils';
+import { stripQuotes, uniqueTokens } from './filtered_search_utils';
import { SortDirection } from './constants';
export default {
@@ -120,10 +120,31 @@ export default {
? __('Sort direction: Ascending')
: __('Sort direction: Descending');
},
+ /**
+ * This prop fixes a behaviour affecting GlFilteredSearch
+ * where selecting duplicate token values leads to history
+ * dropdown also showing that selection.
+ */
filteredRecentSearches() {
- return this.recentSearchesStorageKey
- ? this.recentSearches.filter(item => typeof item !== 'string')
- : undefined;
+ if (this.recentSearchesStorageKey) {
+ const knownItems = [];
+ return this.recentSearches.reduce((historyItems, item) => {
+ // Only include non-string history items (discard items from legacy search)
+ if (typeof item !== 'string') {
+ const sanitizedItem = uniqueTokens(item);
+ const itemString = JSON.stringify(sanitizedItem);
+ // Only include items which aren't already part of history
+ if (!knownItems.includes(itemString)) {
+ historyItems.push(sanitizedItem);
+ // We're storing string for comparision as doing direct object compare
+ // won't work due to object reference not being the same.
+ knownItems.push(itemString);
+ }
+ }
+ return historyItems;
+ }, []);
+ }
+ return undefined;
},
},
watch: {
@@ -245,12 +266,14 @@ export default {
this.recentSearchesService.save(resultantSearches);
this.recentSearches = [];
},
- handleFilterSubmit(filters) {
+ handleFilterSubmit() {
+ const filterTokens = uniqueTokens(this.filterValue);
+ this.filterValue = filterTokens;
if (this.recentSearchesStorageKey) {
this.recentSearchesPromise
.then(() => {
- if (filters.length) {
- const resultantSearches = this.recentSearchesStore.addRecentSearch(filters);
+ if (filterTokens.length) {
+ const resultantSearches = this.recentSearchesStore.addRecentSearch(filterTokens);
this.recentSearchesService.save(resultantSearches);
this.recentSearches = resultantSearches;
}
@@ -260,7 +283,7 @@ export default {
});
}
this.blurSearchInput();
- this.$emit('onFilter', this.removeQuotesEnclosure(filters));
+ this.$emit('onFilter', this.removeQuotesEnclosure(filterTokens));
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
index 85f7f746b49..e7d7b7d9f1b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -1,4 +1,164 @@
-// eslint-disable-next-line import/prefer-default-export
-export const stripQuotes = value => {
- return value.includes(' ') ? value.slice(1, -1) : value;
+import { isEmpty } from 'lodash';
+import { queryToObject } from '~/lib/utils/url_utility';
+
+/**
+ * Strips enclosing quotations from a string if it has one.
+ *
+ * @param {String} value String to strip quotes from
+ *
+ * @returns {String} String without any enclosure
+ */
+export const stripQuotes = value => value.replace(/^('|")(.*)('|")$/, '$2');
+
+/**
+ * This method removes duplicate tokens from tokens array.
+ *
+ * @param {Array} tokens Array of tokens as defined by `GlFilteredSearch`
+ *
+ * @returns {Array} Unique array of tokens
+ */
+export const uniqueTokens = tokens => {
+ const knownTokens = [];
+ return tokens.reduce((uniques, token) => {
+ if (typeof token === 'object' && token.type !== 'filtered-search-term') {
+ const tokenString = `${token.type}${token.value.operator}${token.value.data}`;
+ if (!knownTokens.includes(tokenString)) {
+ uniques.push(token);
+ knownTokens.push(tokenString);
+ }
+ } else {
+ uniques.push(token);
+ }
+ return uniques;
+ }, []);
};
+
+/**
+ * Creates a token from a type and a filter. Example returned object
+ * { type: 'myType', value: { data: 'myData', operator: '= '} }
+ * @param {String} type the name of the filter
+ * @param {Object}
+ * @param {Object.value} filter value to be returned as token data
+ * @param {Object.operator} filter operator to be retuned as token operator
+ * @return {Object}
+ * @return {Object.type} token type
+ * @return {Object.value} token value
+ */
+function createToken(type, filter) {
+ return { type, value: { data: filter.value, operator: filter.operator } };
+}
+
+/**
+ * This function takes a filter object and translates it into a token array
+ * @param {Object} filters
+ * @param {Object.myFilterName} a single filter value or an array of filters
+ * @return {Array} tokens an array of tokens created from filter values
+ */
+export function prepareTokens(filters = {}) {
+ return Object.keys(filters).reduce((memo, key) => {
+ const value = filters[key];
+ if (!value) {
+ return memo;
+ }
+ if (Array.isArray(value)) {
+ return [...memo, ...value.map(filterValue => createToken(key, filterValue))];
+ }
+
+ return [...memo, createToken(key, value)];
+ }, []);
+}
+
+export function processFilters(filters) {
+ return filters.reduce((acc, token) => {
+ const { type, value } = token;
+ const { operator } = value;
+ const tokenValue = value.data;
+
+ if (!acc[type]) {
+ acc[type] = [];
+ }
+
+ acc[type].push({ value: tokenValue, operator });
+ return acc;
+ }, {});
+}
+
+/**
+ * This function takes a filter object and maps it into a query object. Example filter:
+ * { myFilterName: { value: 'foo', operator: '=' } }
+ * gets translated into:
+ * { myFilterName: 'foo', 'not[myFilterName]': null }
+ * @param {Object} filters
+ * @param {Object.myFilterName} a single filter value or an array of filters
+ * @return {Object} query object with both filter name and not-name with values
+ */
+export function filterToQueryObject(filters = {}) {
+ return Object.keys(filters).reduce((memo, key) => {
+ const filter = filters[key];
+
+ let selected;
+ let unselected;
+ if (Array.isArray(filter)) {
+ selected = filter.filter(item => item.operator === '=').map(item => item.value);
+ unselected = filter.filter(item => item.operator === '!=').map(item => item.value);
+ } else {
+ selected = filter?.operator === '=' ? filter.value : null;
+ unselected = filter?.operator === '!=' ? filter.value : null;
+ }
+
+ if (isEmpty(selected)) {
+ selected = null;
+ }
+ if (isEmpty(unselected)) {
+ unselected = null;
+ }
+
+ return { ...memo, [key]: selected, [`not[${key}]`]: unselected };
+ }, {});
+}
+
+/**
+ * Extracts filter name from url name, e.g. `not[my_filter]` => `my_filter`
+ * and returns the operator with it depending on the filter name
+ * @param {String} filterName from url
+ * @return {Object}
+ * @return {Object.filterName} extracted filtern ame
+ * @return {Object.operator} `=` or `!=`
+ */
+function extractNameAndOperator(filterName) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ if (filterName.startsWith('not[') && filterName.endsWith(']')) {
+ return { filterName: filterName.slice(4, -1), operator: '!=' };
+ }
+
+ return { filterName, operator: '=' };
+}
+
+/**
+ * This function takes a URL query string and maps it into a filter object. Example query string:
+ * '?myFilterName=foo'
+ * gets translated into:
+ * { myFilterName: { value: 'foo', operator: '=' } }
+ * @param {String} query URL quert string, e.g. from `window.location.search`
+ * @return {Object} filter object with filter names and their values
+ */
+export function urlQueryToFilter(query = '') {
+ const filters = queryToObject(query, { gatherArrays: true });
+ return Object.keys(filters).reduce((memo, key) => {
+ const value = filters[key];
+ if (!value) {
+ return memo;
+ }
+ const { filterName, operator } = extractNameAndOperator(key);
+ let previousValues = [];
+ if (Array.isArray(memo[filterName])) {
+ previousValues = memo[filterName];
+ }
+ if (Array.isArray(value)) {
+ const newAdditions = value.filter(Boolean).map(item => ({ value: item, operator }));
+ return { ...memo, [filterName]: [...previousValues, ...newAdditions] };
+ }
+
+ return { ...memo, [filterName]: { value, operator } };
+ }, {});
+}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index 969e914ef0c..ee0e00b0f5d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -3,7 +3,7 @@ import {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
- GlDeprecatedDropdownDivider,
+ GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
@@ -11,15 +11,14 @@ import { debounce } from 'lodash';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
-import { ANY_AUTHOR, DEBOUNCE_DELAY } from '../constants';
+import { DEFAULT_LABEL_ANY, DEBOUNCE_DELAY } from '../constants';
export default {
- anyAuthor: ANY_AUTHOR,
components: {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
- GlDeprecatedDropdownDivider,
+ GlDropdownDivider,
GlLoadingIcon,
},
props: {
@@ -35,6 +34,7 @@ export default {
data() {
return {
authors: this.config.initialAuthors || [],
+ defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY],
loading: true,
};
},
@@ -99,10 +99,14 @@ export default {
<span>{{ activeAuthor ? activeAuthor.name : inputValue }}</span>
</template>
<template #suggestions>
- <gl-filtered-search-suggestion :value="$options.anyAuthor">
- {{ __('Any') }}
+ <gl-filtered-search-suggestion
+ v-for="author in defaultAuthors"
+ :key="author.value"
+ :value="author.value"
+ >
+ {{ author.text }}
</gl-filtered-search-suggestion>
- <gl-deprecated-dropdown-divider />
+ <gl-dropdown-divider v-if="defaultAuthors.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
new file mode 100644
index 00000000000..c18bdfc5c20
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
@@ -0,0 +1,115 @@
+<script>
+import {
+ GlToken,
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+
+import { DEBOUNCE_DELAY } from '../constants';
+
+export default {
+ components: {
+ GlToken,
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlLoadingIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ branches: this.config.initialBranches || [],
+ defaultBranches: this.config.defaultBranches || [],
+ loading: true,
+ };
+ },
+ computed: {
+ currentValue() {
+ return this.value.data.toLowerCase();
+ },
+ activeBranch() {
+ return this.branches.find(branch => branch.name.toLowerCase() === this.currentValue);
+ },
+ },
+ watch: {
+ active: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.branches.length) {
+ this.fetchBranchBySearchTerm(this.value.data);
+ }
+ },
+ },
+ },
+ methods: {
+ fetchBranchBySearchTerm(searchTerm) {
+ this.loading = true;
+ this.config
+ .fetchBranches(searchTerm)
+ .then(({ data }) => {
+ this.branches = data;
+ })
+ .catch(() => createFlash({ message: __('There was a problem fetching branches.') }))
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ searchBranches: debounce(function debouncedSearch({ data }) {
+ this.fetchBranchBySearchTerm(data);
+ }, DEBOUNCE_DELAY),
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="searchBranches"
+ >
+ <template #view-token="{ inputValue }">
+ <gl-token variant="search-value">{{
+ activeBranch ? activeBranch.name : inputValue
+ }}</gl-token>
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="branch in defaultBranches"
+ :key="branch.value"
+ :value="branch.value"
+ >
+ {{ branch.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultBranches.length" />
+ <gl-loading-icon v-if="loading" />
+ <template v-else>
+ <gl-filtered-search-suggestion
+ v-for="branch in branches"
+ :key="branch.id"
+ :value="branch.name"
+ >
+ <div class="gl-display-flex">
+ <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span>
+ <div>{{ branch.name }}</div>
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 726a1c49993..7a9c5c277eb 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -3,7 +3,7 @@ import {
GlToken,
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
- GlNewDropdownDivider as GlDropdownDivider,
+ GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
@@ -14,10 +14,9 @@ import { __ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { stripQuotes } from '../filtered_search_utils';
-import { NO_LABEL, DEBOUNCE_DELAY } from '../constants';
+import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants';
export default {
- noLabel: NO_LABEL,
components: {
GlToken,
GlFilteredSearchToken,
@@ -38,6 +37,7 @@ export default {
data() {
return {
labels: this.config.initialLabels || [],
+ defaultLabels: this.config.defaultLabels || DEFAULT_LABELS,
loading: true,
};
},
@@ -105,10 +105,14 @@ export default {
>
</template>
<template #suggestions>
- <gl-filtered-search-suggestion :value="$options.noLabel">{{
- __('No label')
- }}</gl-filtered-search-suggestion>
- <gl-dropdown-divider />
+ <gl-filtered-search-suggestion
+ v-for="label in defaultLabels"
+ :key="label.value"
+ :value="label.value"
+ >
+ {{ label.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultLabels.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion v-for="label in labels" :key="label.id" :value="label.title">
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index cf1ac4e718b..89952623d0d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -2,7 +2,7 @@
import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
- GlNewDropdownDivider as GlDropdownDivider,
+ GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
@@ -11,10 +11,9 @@ import createFlash from '~/flash';
import { __ } from '~/locale';
import { stripQuotes } from '../filtered_search_utils';
-import { defaultMilestones, DEBOUNCE_DELAY } from '../constants';
+import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
export default {
- defaultMilestones,
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
@@ -34,6 +33,7 @@ export default {
data() {
return {
milestones: this.config.initialMilestones || [],
+ defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES,
loading: true,
};
},
@@ -89,12 +89,13 @@ export default {
</template>
<template #suggestions>
<gl-filtered-search-suggestion
- v-for="milestone in $options.defaultMilestones"
+ v-for="milestone in defaultMilestones"
:key="milestone.value"
:value="milestone.value"
- >{{ milestone.text }}</gl-filtered-search-suggestion
>
- <gl-dropdown-divider />
+ {{ milestone.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultMilestones.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
deleted file mode 100644
index 58afcebb7b3..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
+++ /dev/null
@@ -1,154 +0,0 @@
-<script>
-import $ from 'jquery';
-import { GlDeprecatedButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
-/**
- * Renders a split dropdown with
- * an input that allows to search through the given
- * array of options.
- *
- * When there are no results and `showCreateMode` is true
- * it renders a create button with the value typed.
- */
-export default {
- name: 'FilteredSearchDropdown',
- components: {
- Icon,
- GlDeprecatedButton,
- },
- props: {
- title: {
- type: String,
- required: false,
- default: '',
- },
- buttonType: {
- required: false,
- validator: value =>
- ['primary', 'default', 'secondary', 'success', 'info', 'warning', 'danger'].indexOf(
- value,
- ) !== -1,
- default: 'default',
- },
- size: {
- required: false,
- type: String,
- default: 'sm',
- },
- items: {
- type: Array,
- required: true,
- },
- visibleItems: {
- type: Number,
- required: false,
- default: 5,
- },
- filterKey: {
- type: String,
- required: true,
- },
- showCreateMode: {
- type: Boolean,
- required: false,
- default: false,
- },
- createButtonText: {
- type: String,
- required: false,
- default: __('Create'),
- },
- },
- data() {
- return {
- filter: '',
- };
- },
- computed: {
- className() {
- return `btn btn-${this.buttonType} btn-${this.size}`;
- },
- filteredResults() {
- if (this.filter !== '') {
- return this.items.filter(
- item =>
- item[this.filterKey] &&
- item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
- );
- }
-
- return this.items.slice(0, this.visibleItems);
- },
- computedCreateButtonText() {
- return `${this.createButtonText} ${this.filter}`;
- },
- shouldRenderCreateButton() {
- return this.showCreateMode && this.filteredResults.length === 0 && this.filter !== '';
- },
- },
- mounted() {
- /**
- * Resets the filter every time the user closes the dropdown
- */
- $(this.$el)
- .on('shown.bs.dropdown', () => {
- this.$nextTick(() => this.$refs.searchInput.focus());
- })
- .on('hidden.bs.dropdown', () => {
- this.filter = '';
- });
- },
-};
-</script>
-<template>
- <div class="dropdown">
- <div class="btn-group">
- <slot name="mainAction" :class-name="className">
- <button type="button" :class="className">{{ title }}</button>
- </slot>
-
- <button
- type="button"
- :class="className"
- class="dropdown-toggle dropdown-toggle-split"
- data-toggle="dropdown"
- aria-haspopup="true"
- aria-expanded="false"
- :aria-label="__('Expand dropdown')"
- >
- <icon name="angle-down" :size="12" />
- </button>
- <div class="dropdown-menu dropdown-menu-right">
- <div class="dropdown-input">
- <input
- ref="searchInput"
- v-model="filter"
- type="search"
- :placeholder="__('Filter')"
- class="js-filtered-dropdown-input dropdown-input-field"
- />
- <icon class="dropdown-input-search" name="search" />
- </div>
-
- <div class="dropdown-content">
- <ul>
- <li v-for="(result, i) in filteredResults" :key="i" class="js-filtered-dropdown-result">
- <slot name="result" :result="result">{{ result[filterKey] }}</slot>
- </li>
- </ul>
- </div>
-
- <div v-if="shouldRenderCreateButton" class="dropdown-footer">
- <slot name="footer" :filter="filter">
- <gl-deprecated-button
- class="js-dropdown-create-button btn-transparent"
- @click="$emit('createItem', filter)"
- >{{ computedCreateButtonText }}</gl-deprecated-button
- >
- </slot>
- </div>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
index 00bc46257ed..da4b0aedef5 100644
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
@@ -9,6 +9,7 @@ const AutoComplete = {
Issues: 'issues',
Labels: 'labels',
Members: 'members',
+ MergeRequests: 'mergeRequests',
};
function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
@@ -99,6 +100,14 @@ const autoCompleteMap = {
${icon}`;
},
},
+ [AutoComplete.MergeRequests]: {
+ filterValues() {
+ return this[AutoComplete.MergeRequests];
+ },
+ menuItemTemplate({ original }) {
+ return `<small>${original.reference || original.iid}</small> ${escape(original.title)}`;
+ },
+ },
};
export default {
@@ -139,6 +148,13 @@ export default {
: `~${original.title}`,
values: this.getValues(AutoComplete.Labels),
},
+ {
+ trigger: '!',
+ lookup: value => value.iid + value.title,
+ menuItemTemplate: autoCompleteMap[AutoComplete.MergeRequests].menuItemTemplate,
+ selectTemplate: ({ original }) => original.reference || `!${original.iid}`,
+ values: this.getValues(AutoComplete.MergeRequests),
+ },
],
});
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 2625fcc9d09..6ff6f10f786 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlLink, GlDeprecatedButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import CiIconBadge from './ci_badge_link.vue';
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index a57fa09f753..7154360611f 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import { inserted } from '~/feature_highlight/feature_highlight_helper';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
@@ -11,7 +11,7 @@ import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover
export default {
name: 'HelpPopover',
components: {
- Icon,
+ GlIcon,
},
props: {
options: {
@@ -44,6 +44,6 @@ export default {
</script>
<template>
<button type="button" class="btn btn-blank btn-transparent btn-help" tabindex="0">
- <icon name="question" />
+ <gl-icon name="question" />
</button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
deleted file mode 100644
index 68eeadf0f25..00000000000
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<script>
-import iconsPath from '@gitlab/svgs/dist/icons.svg';
-
-// only allow classes in images.scss e.g. s12
-const validSizes = [8, 10, 12, 14, 16, 18, 24, 32, 48, 72];
-let iconValidator = () => true;
-
-/*
- During development/tests we want to validate that we are just using icons that are actually defined
-*/
-if (process.env.NODE_ENV !== 'production') {
- // eslint-disable-next-line global-require
- const data = require('@gitlab/svgs/dist/icons.json');
- const { icons } = data;
- iconValidator = value => {
- if (icons.includes(value)) {
- return true;
- }
- // eslint-disable-next-line no-console
- console.warn(`Icon '${value}' is not a known icon of @gitlab/gitlab-svg`);
- return false;
- };
-}
-
-/** This is a re-usable vue component for rendering a svg sprite icon
- * @example
- * <icon
- * name="retry"
- * :size="32"
- * class="top"
- * />
- */
-export default {
- props: {
- name: {
- type: String,
- required: true,
- validator: iconValidator,
- },
-
- size: {
- type: Number,
- required: false,
- default: 16,
- validator: value => validSizes.includes(value),
- },
- },
-
- computed: {
- spriteHref() {
- return `${iconsPath}#${this.name}`;
- },
- iconTestClass() {
- return `ic-${this.name}`;
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
- },
-};
-</script>
-
-<template>
- <svg
- :key="spriteHref"
- :class="[iconSizeClass, iconTestClass]"
- aria-hidden="true"
- v-on="$listeners"
- >
- <use v-bind="{ 'xlink:href': spriteHref }" />
- </svg>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index cfbc5b0df3c..c745ea61f8b 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -1,13 +1,12 @@
<script>
-import { GlTooltip } from '@gitlab/ui';
+import { GlTooltip, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- Icon,
+ GlIcon,
GlTooltip,
},
mixins: [timeagoMixin],
@@ -73,7 +72,7 @@ export default {
</script>
<template>
<div ref="milestoneDetails" class="issue-milestone-details">
- <icon :size="16" class="inline icon" name="clock" />
+ <gl-icon :size="16" class="gl-mr-2" name="clock" />
<span class="milestone-title d-inline-block">{{ milestone.title }}</span>
<gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone">
<span class="bold">{{ __('Milestone') }}</span> <br />
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index 1662e7923b7..2ff4033a07e 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import '~/commons/bootstrap';
-import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui';
import { sprintf } from '~/locale';
import IssueMilestone from './issue_milestone.vue';
import IssueAssignees from './issue_assignees.vue';
@@ -18,6 +19,7 @@ export default {
GlTooltip,
IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
IssueDueDate,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -29,6 +31,16 @@ export default {
required: false,
default: false,
},
+ isLocked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ lockedMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
stateTitle() {
@@ -156,19 +168,27 @@ export default {
</div>
</div>
- <button
- v-if="canRemove"
+ <span
+ v-if="isLocked"
+ ref="lockIcon"
+ v-gl-tooltip
+ class="gl-px-3 gl-display-inline-block gl-cursor-not-allowed"
+ :title="lockedMessage"
+ >
+ <gl-icon name="lock" />
+ </span>
+ <gl-button
+ v-else-if="canRemove"
ref="removeButton"
v-gl-tooltip
+ icon="close"
+ category="tertiary"
:disabled="removeDisabled"
- type="button"
- class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button"
+ class="js-issue-item-remove-button gl-ml-3"
data-qa-selector="remove_related_issue_button"
:title="__('Remove')"
:aria-label="__('Remove')"
@click="onRemoveRequest"
- >
- <icon :size="16" class="btn-item-remove-icon" name="close" />
- </button>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js
index 188ab1769a4..221c4f5b8a8 100644
--- a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js
+++ b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
function trimFirstCharOfLineContent(text) {
if (!text) {
return text;
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 6df0119c3db..a48c279d0e3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,14 +1,15 @@
<script>
+/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
-import Icon from '../icon.vue';
import GlMentions from '~/vue_shared/components/gl_mentions.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -19,7 +20,7 @@ export default {
GlMentions,
MarkdownHeader,
MarkdownToolbar,
- Icon,
+ GlIcon,
Suggestions,
},
mixins: [glFeatureFlagsMixin()],
@@ -168,11 +169,12 @@ export default {
emojis: this.enableAutocomplete,
members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
- mergeRequests: this.enableAutocomplete,
+ mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
epics: this.enableAutocomplete,
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
snippets: this.enableAutocomplete,
+ vulnerabilities: this.enableAutocomplete,
});
},
beforeDestroy() {
@@ -254,7 +256,7 @@ export default {
href="#"
:aria-label="__('Leave zen mode')"
>
- <icon :size="16" name="screen-normal" />
+ <gl-icon :size="16" name="minimize" />
</a>
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 7e6edcfbd25..d0a0560846a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,15 +1,15 @@
<script>
import $ from 'jquery';
-import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm';
import ToolbarButton from './toolbar_button.vue';
-import Icon from '../icon.vue';
export default {
components: {
ToolbarButton,
- Icon,
+ GlIcon,
GlPopover,
GlButton,
},
@@ -55,6 +55,15 @@ export default {
mdSuggestion() {
return ['```suggestion:-0+0', `{text}`, '```'].join('\n');
},
+ isMac() {
+ // Accessing properties using ?. to allow tests to use
+ // this component without setting up window.gl.client.
+ // In production, window.gl.client should always be present.
+ return Boolean(window.gl?.client?.isMac);
+ },
+ modifierKey() {
+ return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
+ },
},
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
@@ -129,8 +138,22 @@ export default {
</li>
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
<div class="d-inline-block">
- <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" />
- <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" />
+ <toolbar-button
+ tag="**"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
+ "
+ shortcuts="mod+b"
+ icon="bold"
+ />
+ <toolbar-button
+ tag="_"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
+ "
+ shortcuts="mod+i"
+ icon="italic"
+ />
<toolbar-button
:prepend="true"
:tag="tag"
@@ -181,7 +204,10 @@ export default {
<toolbar-button
tag="[{text}](url)"
tag-select="url"
- :button-title="__('Add a link')"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
+ "
+ shortcuts="mod+k"
icon="link"
/>
</div>
@@ -221,7 +247,7 @@ export default {
:title="__('Go full screen')"
type="button"
>
- <icon name="screen-full" />
+ <gl-icon name="maximize" />
</button>
</div>
</li>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 4de80e9b4c2..1fc54d2f52e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -1,11 +1,10 @@
<script>
-import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
- components: { Icon, GlDeprecatedButton, GlLoadingIcon },
+ components: { GlIcon, GlButton, GlLoadingIcon },
directives: { 'gl-tooltip': GlTooltipDirective },
mixins: [glFeatureFlagsMixin()],
props: {
@@ -97,7 +96,7 @@ export default {
<div class="qa-suggestion-diff-header js-suggestion-diff-header font-weight-bold">
{{ __('Suggested change') }}
<a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn">
- <icon name="question-o" css-classes="link-highlight" />
+ <gl-icon name="question-o" css-classes="link-highlight" />
</a>
</div>
<div v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</div>
@@ -106,14 +105,14 @@ export default {
<span>{{ applyingSuggestionsMessage }}</span>
</div>
<div v-else-if="canApply && canBeBatched && isBatched" class="d-flex align-items-center">
- <gl-deprecated-button
+ <gl-button
class="btn-inverted js-remove-from-batch-btn btn-grouped"
:disabled="isApplying"
@click="removeSuggestionFromBatch"
>
{{ __('Remove from batch') }}
- </gl-deprecated-button>
- <gl-deprecated-button
+ </gl-button>
+ <gl-button
v-gl-tooltip.viewport="__('This also resolves all related threads')"
class="btn-inverted js-apply-batch-btn btn-grouped"
:disabled="isApplying"
@@ -124,26 +123,26 @@ export default {
<span class="badge badge-pill badge-pill-success">
{{ batchSuggestionsCount }}
</span>
- </gl-deprecated-button>
+ </gl-button>
</div>
<div v-else class="d-flex align-items-center">
- <gl-deprecated-button
+ <gl-button
v-if="canBeBatched && !isDisableButton"
class="btn-inverted js-add-to-batch-btn btn-grouped"
:disabled="isDisableButton"
@click="addSuggestionToBatch"
>
{{ __('Add suggestion to batch') }}
- </gl-deprecated-button>
+ </gl-button>
<span v-gl-tooltip.viewport="tooltipMessage" tabindex="0">
- <gl-deprecated-button
+ <gl-button
class="btn-inverted js-apply-btn btn-grouped"
:disabled="isDisableButton"
variant="success"
@click="applySuggestion"
>
{{ __('Apply suggestion') }}
- </gl-deprecated-button>
+ </gl-button>
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
index 112bd03b49b..9059f0d2a8b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
export default {
name: 'SuggestionDiffRow',
props: {
@@ -26,9 +27,14 @@ export default {
<td class="diff-line-num new_line border-top-0 border-bottom-0" :class="lineType">
{{ line.new_line }}
</td>
- <td class="line_content" :class="[{ 'd-table-cell': displayAsCell }, lineType]">
- <span v-if="line.rich_text" v-html="line.rich_text"></span>
- <span v-else-if="line.text">{{ line.text }}</span>
+ <td
+ class="line_content"
+ :class="[{ 'd-table-cell': displayAsCell }, lineType]"
+ data-testid="suggestion-diff-content"
+ >
+ <span v-if="line.rich_text" class="line" v-html="line.rich_text"></span>
+ <span v-else-if="line.text" class="line">{{ line.text }}</span>
+ <span v-else class="line"></span>
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 1216484b35f..083f581af05 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,10 +1,14 @@
<script>
import Vue from 'vue';
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
+ directives: {
+ SafeHtml,
+ },
props: {
lineType: {
type: String,
@@ -115,6 +119,6 @@ export default {
<template>
<div>
<div class="flash-container js-suggestions-flash"></div>
- <div v-show="isRendered" ref="container" class="md" v-html="noteHtml"></div>
+ <div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md"></div>
</div>
</template>
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 f37dd9e171c..6c35741e7e5 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,10 +1,9 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '../icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -47,6 +46,26 @@ export default {
required: false,
default: 0,
},
+
+ /**
+ * A string (or an array of strings) of
+ * [mousetrap](https://craig.is/killing/mice) keyboard shortcuts
+ * that should be attached to this button. For example:
+ * "command+k"
+ * ...or...
+ * ["command+k", "ctrl+k"]
+ */
+ shortcuts: {
+ type: [String, Array],
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ shortcutsString() {
+ const shortcutArray = Array.isArray(this.shortcuts) ? this.shortcuts : [this.shortcuts];
+ return JSON.stringify(shortcutArray);
+ },
},
};
</script>
@@ -60,6 +79,7 @@ export default {
:data-md-block="tagBlock"
:data-md-tag-content="tagContent"
:data-md-prepend="prepend"
+ :data-md-shortcuts="shortcutsString"
:title="buttonTitle"
:aria-label="buttonTitle"
type="button"
@@ -67,6 +87,6 @@ export default {
data-container="body"
@click="() => $emit('click')"
>
- <icon :name="icon" />
+ <gl-icon :name="icon" />
</button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index 69ba5cb97e2..35ba7c665d5 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -1,14 +1,13 @@
<script>
import $ from 'jquery';
-import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlDeprecatedButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlDeprecatedButton,
- Icon,
+ GlIcon,
},
directives: {
@@ -121,7 +120,7 @@ export default {
:title="title"
>
<slot>
- <icon name="copy-to-clipboard" />
+ <gl-icon name="copy-to-clipboard" />
</slot>
</gl-deprecated-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index f986b105f20..c12012d8419 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -1,8 +1,8 @@
<script>
-import { GlLink } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlLink, GlIcon } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
-import icon from '../icon.vue';
function buildDocsLinkStart(path) {
return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`;
@@ -16,7 +16,7 @@ const NoteableTypeText = {
export default {
components: {
- icon,
+ GlIcon,
GlLink,
},
props: {
@@ -89,7 +89,7 @@ export default {
</script>
<template>
<div class="issuable-note-warning">
- <icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
+ <gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
<span v-html="confidentialAndLockedDiscussionText"></span>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index e75ac8c54bc..53dbae39608 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
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 fe57d4f29ca..f30676e8ef3 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -1,4 +1,6 @@
<script>
+/* eslint-disable vue/no-v-html */
+
/**
* Common component to render a system note, icon and user information.
*
@@ -18,10 +20,15 @@
*/
import $ from 'jquery';
import { mapGetters, mapActions, mapState } from 'vuex';
-import { GlDeprecatedButton, GlSkeletonLoading, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlTooltipDirective,
+ GlIcon,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
import noteHeader from '~/notes/components/note_header.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
@@ -32,14 +39,15 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
export default {
name: 'SystemNote',
components: {
- Icon,
+ GlIcon,
noteHeader,
TimelineEntryItem,
- GlDeprecatedButton,
+ GlButton,
GlSkeletonLoading,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()],
props: {
@@ -104,25 +112,28 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
- <span v-html="actionTextHtml"></span>
+ <span v-safe-html="actionTextHtml"></span>
<template v-if="canSeeDescriptionVersion" slot="extra-controls">
&middot;
- <button type="button" class="btn-blank btn-link" @click="toggleDescriptionVersion">
- {{ __('Compare with previous version') }}
- <icon :name="descriptionVersionToggleIcon" :size="12" class="append-left-5" />
- </button>
+ <gl-button
+ variant="link"
+ :icon="descriptionVersionToggleIcon"
+ data-testid="compare-btn"
+ @click="toggleDescriptionVersion"
+ >{{ __('Compare with previous version') }}</gl-button
+ >
</template>
</note-header>
</div>
<div class="note-body">
<div
+ v-safe-html="note.note_html"
:class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }"
class="note-text md"
- v-html="note.note_html"
></div>
<div v-if="hasMoreCommits" class="flex-list">
<div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded">
- <icon :name="toggleIcon" :size="8" class="gl-mr-2" />
+ <gl-icon :name="toggleIcon" :size="8" class="gl-mr-2" />
<span>{{ __('Toggle commit list') }}</span>
</div>
</div>
@@ -130,17 +141,18 @@ export default {
<pre v-if="isLoadingDescriptionVersion" class="loading-state">
<gl-skeleton-loading />
</pre>
- <pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre>
- <gl-deprecated-button
+ <pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre>
+ <gl-button
v-if="displayDeleteButton"
- ref="deleteDescriptionVersionButton"
v-gl-tooltip
:title="__('Remove description history')"
- class="btn-transparent delete-description-history"
+ variant="default"
+ category="tertiary"
+ icon="remove"
+ class="delete-description-history"
+ data-testid="delete-description-version-button"
@click="deleteDescriptionVersion"
- >
- <icon name="remove" />
- </gl-deprecated-button>
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
index e053a9ddaa6..154671fe9fa 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -1,14 +1,14 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlButton, GlIcon } from '@gitlab/ui';
import { isString } from 'lodash';
-import Icon from '~/vue_shared/components/icon.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
export default {
name: 'ProjectListItem',
- components: { Icon, ProjectAvatar, GlDeprecatedButton },
+ components: { GlIcon, ProjectAvatar, GlButton },
props: {
project: {
type: Object,
@@ -40,17 +40,16 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
- class="d-flex align-items-center btn pt-1 pb-1 border-0 project-list-item"
+ <gl-button
+ category="tertiary"
+ class="gl-display-flex gl-align-items-center gl-justify-content-start! gl-mb-2 gl-w-full"
@click="onClick"
>
- <icon
- class="gl-ml-3 gl-mr-3 flex-shrink-0 position-top-0 js-selected-icon"
- :class="{ 'js-selected visible': selected, 'js-unselected invisible': !selected }"
- name="mobile-issue-close"
- />
- <project-avatar class="flex-shrink-0 js-project-avatar" :project="project" :size="32" />
- <div class="d-flex flex-wrap project-namespace-name-container">
+ <div
+ class="gl-display-flex gl-align-items-center gl-flex-wrap project-namespace-name-container"
+ >
+ <gl-icon v-if="selected" class="js-selected-icon" name="mobile-issue-close" />
+ <project-avatar class="gl-flex-shrink-0 js-project-avatar" :project="project" :size="32" />
<div
v-if="truncatedNamespace"
:title="projectNameWithNamespace"
@@ -65,5 +64,5 @@ export default {
v-html="highlightedProjectName"
></div>
</div>
- </gl-deprecated-button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index 0b91588a006..4e2029cd74f 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -100,7 +100,7 @@ export default {
@bottomReached="bottomReached"
>
<template v-if="!showLoadingIndicator" #items>
- <div class="d-flex flex-column">
+ <div class="gl-display-flex gl-flex-direction-column gl-p-3">
<project-list-item
v-for="project in projectSearchResults"
:key="project.id"
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index 25701df33f3..fc1f3675a3d 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import DeprecatedModal from './deprecated_modal.vue';
import { eventHub } from './recaptcha_eventhub';
diff --git a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
new file mode 100644
index 00000000000..08ee23d25bf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
@@ -0,0 +1,82 @@
+<script>
+import { uniqueId } from 'lodash';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Tracking from '~/tracking';
+
+export default {
+ name: 'CodeInstruction',
+ components: {
+ ClipboardButton,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ instruction: {
+ type: String,
+ required: true,
+ },
+ copyText: {
+ type: String,
+ required: true,
+ },
+ multiline: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ trackingAction: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ trackingLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ created() {
+ this.uniqueId = uniqueId();
+ },
+ methods: {
+ trackCopy() {
+ if (this.trackingAction) {
+ this.track(this.trackingAction, { label: this.trackingLabel });
+ }
+ },
+ generateFormId(name) {
+ return `${name}_${this.uniqueId}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="!multiline" class="gl-mb-3">
+ <label v-if="label" :for="generateFormId('instruction-input')">{{ label }}</label>
+ <div class="input-group gl-mb-3">
+ <input
+ :id="generateFormId('instruction-input')"
+ :value="instruction"
+ type="text"
+ class="form-control gl-font-monospace"
+ data-testid="instruction-input"
+ readonly
+ @copy="trackCopy"
+ />
+ <span class="input-group-append" data-testid="instruction-button" @click="trackCopy">
+ <clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
+ </span>
+ </div>
+ </div>
+
+ <div v-else>
+ <pre class="gl-font-monospace" data-testid="multiline-instruction" @copy="trackCopy">{{
+ instruction
+ }}</pre>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/shared/components/details_row.vue b/app/assets/javascripts/vue_shared/components/registry/details_row.vue
index 2e245fadead..2e245fadead 100644
--- a/app/assets/javascripts/registry/shared/components/details_row.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/details_row.vue
diff --git a/app/assets/javascripts/packages/details/components/history_element.vue b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
index 8a51c1528cf..a60b630b207 100644
--- a/app/assets/javascripts/packages/details/components/history_element.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
@@ -3,12 +3,11 @@ import { GlIcon } from '@gitlab/ui';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
- name: 'HistoryElement',
+ name: 'HistoryItem',
components: {
GlIcon,
TimelineEntryItem,
},
-
props: {
icon: {
type: String,
@@ -29,7 +28,9 @@ export default {
<slot></slot>
</span>
</div>
- <div class="note-body"></div>
+ <div class="note-body">
+ <slot name="body"></slot>
+ </div>
</div>
</timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index c57645cc3a1..50a19dc2156 100644
--- a/app/assets/javascripts/registry/explorer/components/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -10,11 +10,6 @@ export default {
default: false,
required: false,
},
- last: {
- type: Boolean,
- default: false,
- required: false,
- },
disabled: {
type: Boolean,
default: false,
@@ -35,12 +30,10 @@ export default {
computed: {
optionalClasses() {
return {
- 'gl-border-t-1': !this.first,
- 'gl-border-t-2': this.first,
- 'gl-border-b-1': !this.last,
- 'gl-border-b-2': this.last,
+ 'gl-border-t-transparent': !this.first && !this.selected,
+ 'gl-border-t-gray-100': this.first && !this.selected,
'disabled-content': this.disabled,
- 'gl-border-gray-100': !this.selected,
+ 'gl-border-b-gray-100': !this.selected,
'gl-bg-blue-50 gl-border-blue-200': this.selected,
};
},
@@ -58,21 +51,26 @@ export default {
<template>
<div
- class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid"
+ class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
:class="optionalClasses"
>
- <div class="gl-display-flex gl-align-items-center gl-py-4 gl-px-2">
+ <div class="gl-display-flex gl-align-items-center gl-py-5">
<div
v-if="$slots['left-action']"
class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
</div>
- <div class="gl-display-flex gl-flex-direction-column gl-flex-fill-1">
+ <div
+ class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
+ >
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-body gl-font-weight-bold"
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
>
- <div class="gl-display-flex gl-align-items-center">
+ <div
+ v-if="$slots['left-primary']"
+ class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
+ >
<slot name="left-primary"></slot>
<gl-button
v-if="detailsSlots.length > 0"
@@ -83,24 +81,33 @@ export default {
@click="toggleDetails"
/>
</div>
- <div>
- <slot name="right-primary"></slot>
+ <div
+ v-if="$slots['left-secondary']"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
+ >
+ <slot name="left-secondary"></slot>
</div>
</div>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-font-sm gl-text-gray-300"
+ class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
>
- <div>
- <slot name="left-secondary"></slot>
+ <div
+ v-if="$slots['right-primary']"
+ class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
+ >
+ <slot name="right-primary"></slot>
</div>
- <div>
+ <div
+ v-if="$slots['right-secondary']"
+ class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
+ >
<slot name="right-secondary"></slot>
</div>
</div>
</div>
<div
v-if="$slots['right-action']"
- class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-2"
+ class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1"
>
<slot name="right-action"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue
new file mode 100644
index 00000000000..8ef623b68eb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue
@@ -0,0 +1,63 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+export default {
+ name: 'MetadataItem',
+ components: {
+ GlIcon,
+ GlLink,
+ TooltipOnTruncate,
+ },
+ props: {
+ icon: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ text: {
+ type: String,
+ required: true,
+ },
+ link: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ size: {
+ type: String,
+ required: false,
+ default: 's',
+ validator(value) {
+ return !value || ['xs', 's', 'm', 'l', 'xl'].includes(value);
+ },
+ },
+ },
+ computed: {
+ sizeClass() {
+ return `mw-${this.size}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-inline-flex gl-align-items-center">
+ <gl-icon v-if="icon" :name="icon" class="gl-text-gray-500 gl-mr-3" />
+ <tooltip-on-truncate v-if="link" :title="text" class="gl-text-truncate" :class="sizeClass">
+ <gl-link :href="link" class="gl-font-weight-bold">
+ {{ text }}
+ </gl-link>
+ </tooltip-on-truncate>
+ <div
+ v-else
+ data-testid="metadata-item-text"
+ class="gl-font-weight-bold gl-display-inline-flex"
+ :class="sizeClass"
+ >
+ <tooltip-on-truncate :title="text" class="gl-text-truncate">
+ {{ text }}
+ </tooltip-on-truncate>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
new file mode 100644
index 00000000000..cc33b8f85cd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlAvatar } from '@gitlab/ui';
+
+export default {
+ name: 'TitleArea',
+ components: {
+ GlAvatar,
+ },
+ props: {
+ avatar: {
+ type: String,
+ default: null,
+ required: false,
+ },
+ title: {
+ type: String,
+ default: null,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ metadataSlots: [],
+ };
+ },
+ mounted() {
+ this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith('metadata_'));
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-space-between gl-py-3">
+ <div class="gl-flex-direction-column">
+ <div class="gl-display-flex">
+ <gl-avatar v-if="avatar" :src="avatar" shape="rect" class="gl-align-self-center gl-mr-4" />
+
+ <div class="gl-display-flex gl-flex-direction-column">
+ <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2" data-testid="title">
+ <slot name="title">{{ title }}</slot>
+ </h1>
+
+ <div
+ v-if="$slots['sub-header']"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ >
+ <slot name="sub-header"></slot>
+ </div>
+ </div>
+ </div>
+
+ <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3">
+ <div
+ v-for="(row, metadataIndex) in metadataSlots"
+ :key="metadataIndex"
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <slot :name="row"></slot>
+ </div>
+ </div>
+ </div>
+ <div v-if="$slots['right-actions']" class="gl-mt-3">
+ <slot name="right-actions"></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js
index edc5ffb7b77..68d86777995 100644
--- a/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js
+++ b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js
@@ -1,6 +1,6 @@
export const DEFAULT_RX = 0.4;
-export const DEFAULT_BAR_WIDTH = 6;
-export const DEFAULT_LABEL_WIDTH = 4;
-export const DEFAULT_LABEL_HEIGHT = 5;
+export const DEFAULT_BAR_WIDTH = 4;
+export const DEFAULT_LABEL_WIDTH = 3;
+export const DEFAULT_LABEL_HEIGHT = 3;
export const BAR_HEIGHTS = [5, 7, 9, 14, 21, 35, 50, 80];
export const GRID_YS = [30, 60, 90];
diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue
index 306fa61780f..a9f35a73db0 100644
--- a/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue
+++ b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue
@@ -61,35 +61,37 @@ export default {
};
</script>
<template>
- <gl-skeleton-loader :unique-key="uniqueKey">
- <rect
- v-for="(y, index) in $options.GRID_YS"
- :key="`grid-${index}`"
- data-testid="skeleton-chart-grid"
- x="0"
- :y="`${y}%`"
- width="100%"
- height="1px"
- />
- <rect
- v-for="(height, index) in $options.BAR_HEIGHTS"
- :key="`bar-${index}`"
- data-testid="skeleton-chart-bar"
- :x="`${getBarXPosition(index)}%`"
- :y="`${90 - height}%`"
- :width="`${barWidth}%`"
- :height="`${height}%`"
- :rx="`${rx}%`"
- />
- <rect
- v-for="(height, index) in $options.BAR_HEIGHTS"
- :key="`label-${index}`"
- data-testid="skeleton-chart-label"
- :x="`${labelCentering + getBarXPosition(index)}%`"
- :y="`${100 - labelHeight}%`"
- :width="`${labelWidth}%`"
- :height="`${labelHeight}%`"
- :rx="`${rx}%`"
- />
- </gl-skeleton-loader>
+ <div class="gl-px-8">
+ <gl-skeleton-loader :unique-key="uniqueKey" class="gl-p-8">
+ <rect
+ v-for="(y, index) in $options.GRID_YS"
+ :key="`grid-${index}`"
+ data-testid="skeleton-chart-grid"
+ x="0"
+ :y="`${y}%`"
+ width="100%"
+ height="1px"
+ />
+ <rect
+ v-for="(height, index) in $options.BAR_HEIGHTS"
+ :key="`bar-${index}`"
+ data-testid="skeleton-chart-bar"
+ :x="`${getBarXPosition(index)}%`"
+ :y="`${90 - height}%`"
+ :width="`${barWidth}%`"
+ :height="`${height}%`"
+ :rx="`${rx}%`"
+ />
+ <rect
+ v-for="(height, index) in $options.BAR_HEIGHTS"
+ :key="`label-${index}`"
+ data-testid="skeleton-chart-label"
+ :x="`${labelCentering + getBarXPosition(index)}%`"
+ :y="`${100 - labelHeight}%`"
+ :width="`${labelWidth}%`"
+ :height="`${labelHeight}%`"
+ :rx="`${rx}%`"
+ />
+ </gl-skeleton-loader>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
index a9c5d442f62..108c60c3edb 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
@@ -1,17 +1,19 @@
import { union, mapValues } from 'lodash';
import renderBlockHtml from './renderers/render_html_block';
-import renderKramdownList from './renderers/render_kramdown_list';
-import renderKramdownText from './renderers/render_kramdown_text';
+import renderHeading from './renderers/render_heading';
import renderIdentifierInstanceText from './renderers/render_identifier_instance_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
import renderSoftbreak from './renderers/render_softbreak';
+import renderAttributeDefinition from './renderers/render_attribute_definition';
+import renderListItem from './renderers/render_list_item';
const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
const htmlBlockRenderers = [renderBlockHtml];
-const listRenderers = [renderKramdownList];
-const paragraphRenderers = [renderIdentifierParagraph];
-const textRenderers = [renderKramdownText, renderIdentifierInstanceText];
+const headingRenderers = [renderHeading];
+const paragraphRenderers = [renderIdentifierParagraph, renderBlockHtml];
+const textRenderers = [renderIdentifierInstanceText, renderAttributeDefinition];
+const listItemRenderers = [renderListItem];
const softbreakRenderers = [renderSoftbreak];
const executeRenderer = (renderers, node, context) => {
@@ -25,7 +27,8 @@ const buildCustomHTMLRenderer = customRenderers => {
...customRenderers,
htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock),
htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline),
- list: union(listRenderers, customRenderers?.list),
+ heading: union(headingRenderers, customRenderers?.heading),
+ item: union(listItemRenderers, customRenderers?.listItem),
paragraph: union(paragraphRenderers, customRenderers?.paragraph),
text: union(textRenderers, customRenderers?.text),
softbreak: union(softbreakRenderers, customRenderers?.softbreak),
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
index 868ede9426e..2bce691e793 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
@@ -28,6 +28,8 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
const orderedListItemNode = 'OL LI';
const emphasisNode = 'EM, I';
const strongNode = 'STRONG, B';
+ const headingNode = 'H1, H2, H3, H4, H5, H6';
+ const preCodeNode = 'PRE CODE';
return {
TEXT_NODE(node) {
@@ -63,8 +65,10 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
},
[unorderedListItemNode](node, subContent) {
const baseResult = baseRenderer.convert(node, subContent);
+ const formatted = baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`);
+ const { attributeDefinition } = node.dataset;
- return baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`);
+ return attributeDefinition ? `${formatted.trimRight()}\n${attributeDefinition}\n` : formatted;
},
[orderedListItemNode](node, subContent) {
const baseResult = baseRenderer.convert(node, subContent);
@@ -82,6 +86,19 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
return result.replace(/^[*_]{2}/, strongSyntax).replace(/[*_]{2}$/, strongSyntax);
},
+ [headingNode](node, subContent) {
+ const result = baseRenderer.convert(node, subContent);
+ const { attributeDefinition } = node.dataset;
+
+ return attributeDefinition ? `${result.trimRight()}\n${attributeDefinition}\n\n` : result;
+ },
+ [preCodeNode](node, subContent) {
+ const isReferenceDefinition = Boolean(node.dataset.sseReferenceDefinition);
+
+ return isReferenceDefinition
+ ? `\n\n${node.innerText}\n\n`
+ : baseRenderer.convert(node, subContent);
+ },
};
};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js
new file mode 100644
index 00000000000..bd419447a48
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js
@@ -0,0 +1,7 @@
+import { isAttributeDefinition } from './render_utils';
+
+const canRender = ({ literal }) => isAttributeDefinition(literal);
+
+const render = () => ({ type: 'html', content: '<!-- sse-attribute-definition -->' });
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js
new file mode 100644
index 00000000000..71026fd0d65
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js
@@ -0,0 +1,6 @@
+import {
+ renderWithAttributeDefinitions as render,
+ willAlwaysRender as canRender,
+} from './render_utils';
+
+export default { render, canRender };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
index 4ec45ecd3a7..3f9c6291d1b 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
@@ -1,5 +1,3 @@
-import { renderUneditableBranch as render } from './render_utils';
-
const identifierRegex = /(^\[.+\]: .+)/;
const isIdentifier = text => {
@@ -10,4 +8,33 @@ const canRender = (node, context) => {
return isIdentifier(context.getChildrenText(node));
};
+const getReferenceDefinitions = (node, definitions = '') => {
+ if (!node) {
+ return definitions;
+ }
+
+ const definition = node.type === 'text' ? node.literal : '\n';
+
+ return getReferenceDefinitions(node.next, `${definitions}${definition}`);
+};
+
+const render = (node, { skipChildren }) => {
+ const content = getReferenceDefinitions(node.firstChild);
+
+ skipChildren();
+
+ return [
+ {
+ type: 'openTag',
+ tagName: 'pre',
+ classNames: ['code-block', 'language-markdown'],
+ attributes: { 'data-sse-reference-definition': true },
+ },
+ { type: 'openTag', tagName: 'code' },
+ { type: 'text', content },
+ { type: 'closeTag', tagName: 'code' },
+ { type: 'closeTag', tagName: 'pre' },
+ ];
+};
+
export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js
deleted file mode 100644
index 949ca0e5c2a..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { renderUneditableBranch as render } from './render_utils';
-
-const isKramdownTOC = ({ type, literal }) => type === 'text' && literal === 'TOC';
-
-const canRender = node => {
- let targetNode = node;
- while (targetNode !== null) {
- const { firstChild } = targetNode;
- const isLeaf = firstChild === null;
- if (isLeaf) {
- if (isKramdownTOC(targetNode)) {
- return true;
- }
-
- break;
- }
-
- targetNode = targetNode.firstChild;
- }
-
- return false;
-};
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js
deleted file mode 100644
index 0551894918c..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { renderUneditableLeaf as render } from './render_utils';
-
-const kramdownRegex = /(^{:.+}$)/;
-
-const canRender = ({ literal }) => {
- return kramdownRegex.test(literal);
-};
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js
new file mode 100644
index 00000000000..71026fd0d65
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js
@@ -0,0 +1,6 @@
+import {
+ renderWithAttributeDefinitions as render,
+ willAlwaysRender as canRender,
+} from './render_utils';
+
+export default { render, canRender };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
index cec6491557b..4cba2c70486 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
@@ -8,3 +8,31 @@ export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockToken
export const renderUneditableBranch = (_, { entering, origin }) =>
entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
+
+const attributeDefinitionRegexp = /(^{:.+}$)/;
+
+export const isAttributeDefinition = text => attributeDefinitionRegexp.test(text);
+
+const findAttributeDefinition = node => {
+ const literal =
+ node?.next?.firstChild?.literal || node?.firstChild?.firstChild?.next?.next?.literal; // for headings // for list items;
+
+ return isAttributeDefinition(literal) ? literal : null;
+};
+
+export const renderWithAttributeDefinitions = (node, { origin }) => {
+ const attributes = findAttributeDefinition(node);
+ const token = origin();
+
+ if (token.type === 'openTag' && attributes) {
+ Object.assign(token, {
+ attributes: {
+ 'data-attribute-definition': attributes,
+ },
+ });
+ }
+
+ return token;
+};
+
+export const willAlwaysRender = () => true;
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
index cc24fedceed..0ed5a050fe4 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -6,6 +7,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ GlIcon,
+ },
props: {
containerClass: {
type: String,
@@ -47,7 +51,7 @@ export default {
data-boundary="viewport"
@click="click"
>
- <i v-if="showIcon" class="fa fa-calendar" aria-hidden="true"> </i>
+ <gl-icon v-if="showIcon" name="calendar" />
<slot>
<span> {{ text }} </span>
</slot>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 5eef439aa90..1ef3d5627ae 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -14,7 +14,10 @@ import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue';
import DropdownCreateLabel from './dropdown_create_label.vue';
+import { DropdownVariant } from '../labels_select_vue/constants';
+
export default {
+ DropdownVariant,
components: {
DropdownTitle,
DropdownValue,
@@ -80,6 +83,11 @@ export default {
required: false,
default: false,
},
+ variant: {
+ type: String,
+ required: false,
+ default: DropdownVariant.Sidebar,
+ },
},
computed: {
hiddenInputName() {
@@ -123,7 +131,7 @@ export default {
<template>
<div class="block labels js-labels-block">
<dropdown-value-collapsed
- v-if="showCreate"
+ v-if="showCreate && variant === $options.DropdownVariant.Sidebar"
:labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/>
@@ -150,18 +158,21 @@ export default {
:labels-path="labelsPath"
:namespace="namespace"
:labels="context.labels"
- :show-extra-options="!showCreate"
+ :show-extra-options="!showCreate || variant !== $options.DropdownVariant.Sidebar"
:enable-scoped-labels="enableScopedLabels"
/>
<div
- class="dropdown-menu dropdown-select dropdown-menu-paging
-dropdown-menu-labels dropdown-menu-selectable"
+ class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"
>
<div class="dropdown-page-one">
- <dropdown-header v-if="showCreate" />
+ <dropdown-header v-if="showCreate && variant === $options.DropdownVariant.Sidebar" />
<dropdown-search-input />
<div class="dropdown-content" data-qa-selector="labels_dropdown_content"></div>
- <div class="dropdown-loading"><gl-loading-icon /></div>
+ <div class="dropdown-loading">
+ <gl-loading-icon
+ class="gl-display-flex gl-justify-content-center gl-align-items-center gl-h-full"
+ />
+ </div>
<dropdown-footer
v-if="showCreate"
:labels-web-url="labelsWebUrl"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
index 74c5e063c3d..434aabc3df9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
@@ -1,7 +1,14 @@
<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
headerTitle: {
type: String,
@@ -10,29 +17,35 @@ export default {
},
},
created() {
- this.suggestedColors = gon.suggested_label_colors;
+ const rawLabelsColors = gon.suggested_label_colors;
+ this.suggestedColors = Object.keys(rawLabelsColors).map(colorCode => ({
+ colorCode,
+ title: rawLabelsColors[colorCode],
+ }));
},
};
</script>
<template>
<div class="dropdown-page-two dropdown-new-label">
- <div class="dropdown-title">
- <button
+ <div
+ class="dropdown-title gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ >
+ <gl-button
:aria-label="__('Go back')"
- type="button"
- class="dropdown-title-button dropdown-menu-back"
- >
- <i aria-hidden="true" class="fa fa-arrow-left" data-hidden="true"> </i>
- </button>
+ category="tertiary"
+ class="dropdown-menu-back"
+ icon="arrow-left"
+ size="small"
+ />
{{ headerTitle }}
- <button
+ <gl-button
:aria-label="__('Close')"
- type="button"
- class="dropdown-title-button dropdown-menu-close"
- >
- <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon" data-hidden="true"> </i>
- </button>
+ category="tertiary"
+ class="dropdown-menu-close"
+ icon="close"
+ size="small"
+ />
</div>
<div class="dropdown-content">
<div class="dropdown-labels-error js-label-error"></div>
@@ -46,10 +59,12 @@ export default {
<a
v-for="(color, index) in suggestedColors"
:key="index"
- :data-color="color"
+ v-gl-tooltip
+ :data-color="color.colorCode"
:style="{
- backgroundColor: color,
+ backgroundColor: color.colorCode,
}"
+ :title="color.title"
href="#"
>
&nbsp;
@@ -65,12 +80,12 @@ export default {
/>
</div>
<div class="clearfix">
- <button type="button" class="btn btn-primary float-left js-new-label-btn disabled">
+ <gl-button category="secondary" class="float-left js-new-label-btn disabled">
{{ __('Create') }}
- </button>
- <button type="button" class="btn btn-default float-right js-cancel-label-btn">
+ </gl-button>
+ <gl-button category="secondary" class="float-right js-cancel-label-btn">
{{ __('Cancel') }}
- </button>
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
index eb837be165b..7b2802650a2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
@@ -1,16 +1,22 @@
<script>
-export default {};
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ },
+};
</script>
<template>
- <div class="dropdown-title">
- <span>{{ __('Assign labels') }}</span>
+ <div class="dropdown-title gl-display-flex gl-justify-content-center">
+ <span class="gl-ml-auto">{{ __('Assign labels') }}</span>
<button
:aria-label="__('Close')"
type="button"
- class="dropdown-title-button dropdown-menu-close"
+ class="dropdown-title-button dropdown-menu-close gl-ml-auto"
>
- <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon" data-hidden="true"> </i>
+ <gl-icon name="close" aria-hidden="true" class="dropdown-menu-close-icon" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index 05446903286..c2ebf78d541 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -6,6 +7,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ GlIcon,
+ },
props: {
labels: {
type: Array,
@@ -49,7 +53,7 @@ export default {
data-boundary="viewport"
@click="handleClick"
>
- <i aria-hidden="true" data-hidden="true" class="fa fa-tags"> </i>
+ <gl-icon name="labels" />
<span>{{ labels.length }}</span>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index 248e9929833..34f5517ef99 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -166,7 +166,11 @@ export default {
!state.showDropdownButton &&
!state.showDropdownContents
) {
- this.handleDropdownClose(state.labels.filter(label => label.touched));
+ let filterFn = label => label.touched;
+ if (this.isDropdownVariantEmbedded) {
+ filterFn = label => label.set;
+ }
+ this.handleDropdownClose(state.labels.filter(filterFn));
}
},
/**
@@ -186,7 +190,7 @@ export default {
].some(
className =>
target?.classList.contains(className) ||
- target?.parentElement.classList.contains(className),
+ target?.parentElement?.classList.contains(className),
);
const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
@@ -248,10 +252,10 @@ export default {
:allow-label-edit="allowLabelEdit"
:labels-select-in-progress="labelsSelectInProgress"
/>
- <dropdown-value v-show="!showDropdownButton">
+ <dropdown-value>
<slot></slot>
</dropdown-value>
- <dropdown-button v-show="dropdownButtonVisible" />
+ <dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
<dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
index e624bd1eaee..2d236566b3d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
@@ -54,5 +54,8 @@ export const createLabel = ({ state, dispatch }, label) => {
});
};
+export const replaceSelectedLabels = ({ commit }, selectedLabels) =>
+ commit(types.REPLACE_SELECTED_LABELS, selectedLabels);
+
export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels });
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
index 2e044dc3b3c..af92665d4eb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
@@ -15,6 +15,7 @@ export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE';
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
+export const REPLACE_SELECTED_LABELS = 'REPLACE_SELECTED_LABELS';
export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS';
export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 54f8c78b4e1..7edd290a819 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -57,6 +57,10 @@ export default {
state.labelCreateInProgress = false;
},
+ [types.REPLACE_SELECTED_LABELS](state, selectedLabels = []) {
+ state.selectedLabels = selectedLabels;
+ },
+
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
// Find the label to update from all the labels
// and change `set` prop value to represent their current state.
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
index 148bd501a8e..135b9842cbf 100644
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
@@ -1,12 +1,12 @@
<script>
-import { GlNewDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
export default {
name: 'TimezoneDropdown',
components: {
- GlNewDropdown,
+ GlDropdown,
GlDeprecatedDropdownItem,
GlSearchBoxByType,
GlIcon,
@@ -74,7 +74,7 @@ export default {
};
</script>
<template>
- <gl-new-dropdown :text="value" block lazy menu-class="gl-w-full!">
+ <gl-dropdown :text="value" block lazy menu-class="gl-w-full!">
<template #button-content>
<span class="gl-flex-grow-1" :class="{ 'gl-text-gray-300': !value }">
{{ selectedTimezoneLabel }}
@@ -98,5 +98,5 @@ export default {
<gl-deprecated-dropdown-item v-if="!filteredResults.length" data-testid="noMatchingResults">
{{ $options.tranlations.noResultsText }}
</gl-deprecated-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/todo_button.vue b/app/assets/javascripts/vue_shared/components/todo_button.vue
new file mode 100644
index 00000000000..debf19ccca6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/todo_button.vue
@@ -0,0 +1,28 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ isTodo: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ buttonLabel() {
+ return this.isTodo ? __('Mark as done') : __('Add a To-Do');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="$emit('click', $event)">
+ {{ buttonLabel }}
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 540edc9f61c..29d4516bece 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -73,7 +73,7 @@ export default {
'is-disabled': disabledInput,
'is-loading': isLoading,
}"
- @click="toggleFeature"
+ @click.prevent="toggleFeature"
>
<gl-loading-icon class="loading-icon" />
<span class="toggle-icon">
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
index 4ea3d162da2..579ad53e6db 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
@@ -61,9 +61,9 @@ export default {
v-tooltip
:title="title"
:data-placement="placement"
- class="js-show-tooltip"
+ class="js-show-tooltip gl-min-w-0"
>
<slot></slot>
</span>
- <span v-else> <slot></slot> </span>
+ <span v-else class="gl-min-w-0"> <slot></slot> </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/url_sync.vue b/app/assets/javascripts/vue_shared/components/url_sync.vue
index 389d42f0829..2844d9e9e94 100644
--- a/app/assets/javascripts/vue_shared/components/url_sync.vue
+++ b/app/assets/javascripts/vue_shared/components/url_sync.vue
@@ -1,6 +1,6 @@
<script>
import { historyPushState } from '~/lib/utils/common_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
export default {
props: {
@@ -14,7 +14,7 @@ export default {
immediate: true,
deep: true,
handler(newQuery) {
- historyPushState(setUrlParams(newQuery, window.location.href, true));
+ historyPushState(mergeUrlParams(newQuery, window.location.href, { spreadArrays: true }));
},
},
},
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 699e466e848..6aaff000845 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -1,6 +1,6 @@
<script>
-import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+/* eslint-disable vue/no-v-html */
+import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlIcon } from '@gitlab/ui';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji';
@@ -10,7 +10,7 @@ export default {
name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES,
components: {
- Icon,
+ GlIcon,
GlPopover,
GlSkeletonLoading,
UserAvatarImage,
@@ -74,16 +74,16 @@ export default {
</div>
<div class="gl-text-gray-500">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
- <icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
+ <gl-icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
<span ref="bio" class="gl-ml-2" v-html="user.bioHtml"></span>
</div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
- <icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
+ <gl-icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
</div>
</div>
<div v-if="user.location" class="js-location gl-text-gray-500 gl-display-flex">
- <icon name="location" class="gl-text-gray-400 flex-shrink-0" />
+ <gl-icon name="location" class="gl-text-gray-400 flex-shrink-0" />
<span class="gl-ml-2">{{ user.location }}</span>
</div>
<div v-if="statusHtml" class="js-user-status gl-mt-3">
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
new file mode 100644
index 00000000000..8307c6d3b55
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -0,0 +1,118 @@
+<script>
+import $ from 'jquery';
+import { __ } from '~/locale';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
+
+const KEY_WEB_IDE = 'webide';
+const KEY_GITPOD = 'gitpod';
+
+export default {
+ components: {
+ ActionsButton,
+ LocalStorageSync,
+ },
+ props: {
+ webIdeUrl: {
+ type: String,
+ required: true,
+ },
+ needsToFork: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showWebIdeButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showGitpodButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ gitpodUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ gitpodEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ selection: KEY_WEB_IDE,
+ };
+ },
+ computed: {
+ actions() {
+ return [this.webIdeAction, this.gitpodAction].filter(x => x);
+ },
+ webIdeAction() {
+ if (!this.showWebIdeButton) {
+ return null;
+ }
+
+ const handleOptions = this.needsToFork
+ ? { href: '#modal-confirm-fork', handle: () => this.showModal('#modal-confirm-fork') }
+ : { href: this.webIdeUrl };
+
+ return {
+ key: KEY_WEB_IDE,
+ text: __('Web IDE'),
+ secondaryText: __('Quickly and easily edit multiple files in your project.'),
+ tooltip: '',
+ attrs: {
+ 'data-qa-selector': 'web_ide_button',
+ },
+ ...handleOptions,
+ };
+ },
+ gitpodAction() {
+ if (!this.showGitpodButton) {
+ return null;
+ }
+
+ const handleOptions = this.gitpodEnabled
+ ? { href: this.gitpodUrl }
+ : { href: '#modal-enable-gitpod', handle: () => this.showModal('#modal-enable-gitpod') };
+
+ const secondaryText = __('Launch a ready-to-code development environment for your project.');
+
+ return {
+ key: KEY_GITPOD,
+ text: __('Gitpod'),
+ secondaryText,
+ tooltip: secondaryText,
+ attrs: {
+ 'data-qa-selector': 'gitpod_button',
+ },
+ ...handleOptions,
+ };
+ },
+ },
+ methods: {
+ select(key) {
+ this.selection = key;
+ },
+ showModal(id) {
+ $(id).modal('show');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <actions-button :actions="actions" :selected-key="selection" @select="select" />
+ <local-storage-sync
+ storage-key="gl-web-ide-button-selected"
+ :value="selection"
+ @input="select"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
index c628a67f7f5..be5f55a5220 100644
--- a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
@@ -2,7 +2,6 @@ import { isEmpty } from 'lodash';
import { sprintf, __ } from '~/locale';
import { formatDate } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
const mixins = {
@@ -100,9 +99,6 @@ const mixins = {
default: () => ({}),
},
},
- components: {
- icon,
- },
directives: {
tooltip,
},
diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js
index edc31cfa69e..cc18b41e2de 100644
--- a/app/assets/javascripts/vuex_shared/bindings.js
+++ b/app/assets/javascripts/vuex_shared/bindings.js
@@ -9,7 +9,6 @@
* @param {string} root - the key of the state where to search fo they keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
-// eslint-disable-next-line import/prefer-default-export
export const mapComputed = (list, defaultUpdateFn, root) => {
const result = {};
list.forEach(item => {
diff --git a/app/assets/javascripts/vuex_shared/modules/members/index.js b/app/assets/javascripts/vuex_shared/modules/members/index.js
new file mode 100644
index 00000000000..ec6a94178f3
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/members/index.js
@@ -0,0 +1,6 @@
+import createState from './state';
+
+export default initialState => ({
+ namespaced: true,
+ state: createState(initialState),
+});
diff --git a/app/assets/javascripts/vuex_shared/modules/members/state.js b/app/assets/javascripts/vuex_shared/modules/members/state.js
new file mode 100644
index 00000000000..1511961245c
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/members/state.js
@@ -0,0 +1,5 @@
+export default ({ members, sourceId, currentUserId }) => ({
+ members,
+ sourceId,
+ currentUserId,
+});
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index d974556cb9e..a00661c214d 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -1,16 +1,40 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlDrawer } from '@gitlab/ui';
+import { GlDrawer, GlBadge, GlIcon, GlLink } from '@gitlab/ui';
export default {
components: {
GlDrawer,
+ GlBadge,
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ features: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
...mapState(['open']),
+ parsedFeatures() {
+ let features;
+
+ try {
+ features = JSON.parse(this.$props.features) || [];
+ } catch (err) {
+ features = [];
+ }
+
+ return features;
+ },
+ },
+ mounted() {
+ this.openDrawer();
},
methods: {
- ...mapActions(['closeDrawer']),
+ ...mapActions(['openDrawer', 'closeDrawer']),
},
};
</script>
@@ -19,11 +43,31 @@ export default {
<div>
<gl-drawer class="mt-6" :open="open" @close="closeDrawer">
<template #header>
- <h4>{{ __("What's new at GitLab") }}</h4>
- </template>
- <template>
- <div></div>
+ <h4 class="page-title my-2">{{ __("What's new at GitLab") }}</h4>
</template>
+ <div class="pb-6">
+ <div v-for="feature in parsedFeatures" :key="feature.title" class="mb-6">
+ <gl-link :href="feature.url" target="_blank">
+ <h5 class="gl-font-base">{{ feature.title }}</h5>
+ </gl-link>
+ <div class="mb-2">
+ <template v-for="package_name in feature.packages">
+ <gl-badge :key="package_name" size="sm" class="whats-new-item-badge mr-1">
+ <gl-icon name="license" />{{ package_name }}
+ </gl-badge>
+ </template>
+ </div>
+ <gl-link :href="feature.url" target="_blank">
+ <img
+ :alt="feature.title"
+ :src="feature.image_url"
+ class="img-thumbnail px-6 py-2 whats-new-item-image"
+ />
+ </gl-link>
+ <p class="pt-2">{{ feature.body }}</p>
+ <gl-link :href="feature.url" target="_blank">{{ __('Learn more') }}</gl-link>
+ </div>
+ </div>
</gl-drawer>
</div>
</template>
diff --git a/app/assets/javascripts/whats_new/components/trigger.vue b/app/assets/javascripts/whats_new/components/trigger.vue
deleted file mode 100644
index e6c48e92888..00000000000
--- a/app/assets/javascripts/whats_new/components/trigger.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<script>
-import { mapActions } from 'vuex';
-import { GlButton } from '@gitlab/ui';
-
-export default {
- components: {
- GlButton,
- },
- methods: {
- ...mapActions(['openDrawer']),
- },
-};
-</script>
-
-<template>
- <li>
- <gl-button variant="link" @click="openDrawer">{{ __("See what's new at GitLab") }}</gl-button>
- </li>
-</template>
diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js
index c9ee3404d2a..19cdb590ae2 100644
--- a/app/assets/javascripts/whats_new/index.js
+++ b/app/assets/javascripts/whats_new/index.js
@@ -1,32 +1,28 @@
import Vue from 'vue';
import App from './components/app.vue';
-import Trigger from './components/trigger.vue';
import store from './store';
-export default () => {
- // eslint-disable-next-line no-new
- new Vue({
- el: document.getElementById('whats-new-app'),
- store,
- components: {
- App,
- },
-
- render(createElement) {
- return createElement('app');
- },
- });
+let whatsNewApp;
- // eslint-disable-next-line no-new
- new Vue({
- el: document.getElementById('whats-new-trigger'),
- store,
- components: {
- Trigger,
- },
+export default () => {
+ if (whatsNewApp) {
+ store.dispatch('openDrawer');
+ } else {
+ const whatsNewElm = document.getElementById('whats-new-app');
- render(createElement) {
- return createElement('trigger');
- },
- });
+ whatsNewApp = new Vue({
+ el: whatsNewElm,
+ store,
+ components: {
+ App,
+ },
+ render(createElement) {
+ return createElement('app', {
+ props: {
+ features: whatsNewElm.getAttribute('data-features'),
+ },
+ });
+ },
+ });
+ }
};
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
new file mode 100644
index 00000000000..f706b615e7e
--- /dev/null
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -0,0 +1,65 @@
+@import './pages/admin';
+@import './pages/alert_management/details';
+@import './pages/alert_management/severity-icons';
+@import './pages/boards';
+@import './pages/branches';
+@import './pages/builds';
+@import './pages/ci_projects';
+@import './pages/clusters';
+@import './pages/commits';
+@import './pages/cycle_analytics';
+@import './pages/deploy_keys';
+@import './pages/detail_page';
+@import './pages/dev_ops_report';
+@import './pages/diff';
+@import './pages/editor';
+@import './pages/environment_logs';
+@import './pages/environments';
+@import './pages/error_details';
+@import './pages/error_list';
+@import './pages/error_tracking_list';
+@import './pages/events';
+@import './pages/experience_level';
+@import './pages/experimental_separate_sign_up';
+@import './pages/graph';
+@import './pages/groups';
+@import './pages/help';
+@import './pages/import';
+@import './pages/incident_management_list';
+@import './pages/issuable';
+@import './pages/issues/issue_count_badge';
+@import './pages/issues/issues_list';
+@import './pages/issues';
+@import './pages/labels';
+@import './pages/login';
+@import './pages/members';
+@import './pages/merge_conflicts';
+@import './pages/merge_requests';
+@import './pages/milestone';
+@import './pages/monitor';
+@import './pages/note_form';
+@import './pages/notes';
+@import './pages/notifications';
+@import './pages/pages';
+@import './pages/pipeline_schedules';
+@import './pages/pipelines';
+@import './pages/profile';
+@import './pages/profiles/preferences';
+@import './pages/projects';
+@import './pages/prometheus';
+@import './pages/reports';
+@import './pages/runners';
+@import './pages/search';
+@import './pages/serverless';
+@import './pages/service_desk';
+@import './pages/settings';
+@import './pages/settings_ci_cd';
+@import './pages/sherlock';
+@import './pages/status';
+@import './pages/storage_quota';
+@import './pages/tags';
+@import './pages/tree';
+@import './pages/trials';
+@import './pages/ui_dev_kit';
+@import './pages/users';
+@import './pages/wiki';
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index f5393ef47d6..8acd338fff8 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -13,7 +13,7 @@
// directory.
@import '@gitlab/at.js/dist/css/jquery.atwho';
@import 'dropzone/dist/basic';
-@import 'select2/select2';
+@import 'select2';
// GitLab UI framework
@import 'framework';
@@ -22,7 +22,7 @@
@import 'fontawesome_custom';
// Page specific styles (issues, projects etc):
-@import 'pages/**/*';
+@import 'page_specific_files';
// Component specific styles, will be moved to gitlab-ui
@import 'components/**/*';
diff --git a/app/assets/stylesheets/components/batch_comments/review_bar.scss b/app/assets/stylesheets/components/batch_comments/review_bar.scss
new file mode 100644
index 00000000000..76bf7ac81e8
--- /dev/null
+++ b/app/assets/stylesheets/components/batch_comments/review_bar.scss
@@ -0,0 +1,122 @@
+.review-bar-component {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ background: $white;
+ z-index: 300;
+ padding: 7px 0 6px; // to keep aligned with "collapse sidebar" button on the left sidebar
+ border-top: 1px solid $border-color;
+ padding-left: $contextual-sidebar-width;
+ padding-right: $gutter_collapsed_width;
+ transition: padding $sidebar-transition-duration;
+
+ .page-with-icon-sidebar & {
+ padding-left: $contextual-sidebar-collapsed-width;
+ }
+
+ .right-sidebar-expanded & {
+ padding-right: $gutter_width;
+ }
+
+ @media (max-width: map-get($grid-breakpoints, sm)-1) {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .dropdown {
+ margin-left: $grid-size;
+ }
+}
+
+.review-bar-content {
+ max-width: $limited-layout-width;
+ padding: 0 $gl-padding;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.review-preview-dropdown {
+ .review-preview-item.menu-item {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 8px 16px;
+ cursor: pointer;
+
+ &:not(.is-last) {
+ border-bottom: 1px solid $list-border;
+ }
+ }
+
+ .dropdown-menu {
+ top: auto;
+ bottom: 36px;
+
+ &.show {
+ max-height: 400px;
+
+ @include media-breakpoint-down(xs) {
+ width: calc(100vw - 32px);
+ }
+ }
+ }
+
+ .dropdown-content {
+ max-height: 300px;
+ }
+
+ .dropdown-title {
+ padding: $grid-size 25px $gl-padding;
+ margin-bottom: 0;
+ }
+
+ .dropdown-footer {
+ margin-top: 0;
+ }
+
+ .dropdown-menu-close {
+ top: 6px;
+ }
+}
+
+.review-preview-dropdown-toggle {
+ svg.s16 {
+ width: 15px;
+ height: 15px;
+ margin-top: -1px;
+ top: 3px;
+ margin-left: 4px;
+ }
+}
+
+.review-preview-item-header {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ margin-bottom: 4px;
+
+ > .bold {
+ display: flex;
+ min-width: 0;
+ line-height: 16px;
+ }
+}
+
+.review-preview-item-footer {
+ display: flex;
+ align-items: center;
+ margin-top: 4px;
+}
+
+.review-preview-item-content {
+ width: 100%;
+
+ p {
+ display: block;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
index 80421598966..21133316291 100644
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -34,6 +34,10 @@
background-color: $gray-500;
}
}
+
+ .design-detail-overlay-add-comment {
+ cursor: crosshair;
+ }
}
.design-presentation-wrapper {
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index c0699844387..7c668666d70 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -48,7 +48,7 @@ $item-remove-button-space: 42px;
}
.confidential-icon {
- color: $orange-600;
+ color: $orange-500;
}
.item-title-wrapper {
@@ -123,21 +123,11 @@ $item-remove-button-space: 42px;
.item-milestone {
text-decoration: none;
max-width: $item-milestone-max-width;
-
- .ic-clock {
- margin-right: $gl-padding-4;
- }
}
.item-weight {
max-width: $item-weight-max-width;
}
-
- .item-milestone .ic-clock,
- .item-weight .ic-weight,
- .item-due-date .ic-calendar {
- color: $gl-text-color-secondary;
- }
}
.item-assignees {
diff --git a/app/assets/stylesheets/components/rich_content_editor.scss b/app/assets/stylesheets/components/rich_content_editor.scss
index ade1bb2099d..b1189137d59 100644
--- a/app/assets/stylesheets/components/rich_content_editor.scss
+++ b/app/assets/stylesheets/components/rich_content_editor.scss
@@ -6,7 +6,7 @@
// Toolbar buttons
.tui-editor-defaultUI-toolbar .toolbar-button {
- color: $gl-gray-600;
+ color: $gray-500;
border: 0;
&:hover,
diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss
new file mode 100644
index 00000000000..4fff900f5a5
--- /dev/null
+++ b/app/assets/stylesheets/components/whats_new.scss
@@ -0,0 +1,9 @@
+.gl-badge.whats-new-item-badge {
+ background-color: $purple-light;
+ color: $purple;
+ font-weight: bold;
+}
+
+.whats-new-item-image {
+ border-color: $gray-50;
+}
diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss
index a2117e9c012..30d56d99e1c 100644
--- a/app/assets/stylesheets/fontawesome_custom.scss
+++ b/app/assets/stylesheets/fontawesome_custom.scss
@@ -84,10 +84,6 @@
color: $white;
}
-.fa-question-circle::before {
- content: '\f059';
-}
-
.fa-chevron-down::before {
content: '\f078';
}
@@ -130,18 +126,10 @@
content: '\f101';
}
-.fa-trash::before {
- content: '\f1f8';
-}
-
.fa-angle-double-left::before {
content: '\f100';
}
-.fa-arrow-left::before {
- content: '\f060';
-}
-
.fa-trash-o::before {
content: '\f014';
}
@@ -170,14 +158,6 @@
content: '\f0c6';
}
-.fa-tag::before {
- content: '\f02b';
-}
-
-.fa-arrow-up::before {
- content: '\f062';
-}
-
.fa-bug::before {
content: '\f188';
}
@@ -186,10 +166,6 @@
content: '\f1a0';
}
-.fa-user::before {
- content: '\f007';
-}
-
.fa-exclamation-circle::before {
content: '\f06a';
}
@@ -198,10 +174,6 @@
content: '\f0f3';
}
-.fa-arrow-down::before {
- content: '\f063';
-}
-
.fa-bitbucket-square::before {
content: '\f172';
}
@@ -210,14 +182,6 @@
content: '\f016';
}
-.fa-users::before {
- content: '\f0c0';
-}
-
-.fa-tags::before {
- content: '\f02c';
-}
-
.fa-lightbulb-o::before {
content: '\f0eb';
}
@@ -250,10 +214,6 @@
content: '\f06d';
}
-.fa-download::before {
- content: '\f019';
-}
-
.fa-globe::before {
content: '\f0ac';
}
@@ -266,14 +226,6 @@
content: '\f04b';
}
-.fa-arrow-right::before {
- content: '\f061';
-}
-
-.fa-user-secret::before {
- content: '\f21b';
-}
-
.fa-search-plus::before {
content: '\f00e';
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 413e0dde535..f875420b9c9 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -23,7 +23,6 @@
@import 'framework/flash';
@import 'framework/forms';
@import 'framework/gfm';
-@import 'framework/gitlab_theme';
@import 'framework/header';
@import 'framework/highlight';
@import 'framework/issue_box';
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 893a494d240..a9c1652d00d 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -112,7 +112,7 @@
}
@mixin btn-orange {
- @include btn-color($orange-500, $orange-600, $orange-600, $orange-700, $orange-700, $orange-800, $white);
+ @include btn-color($orange-500, $orange-600, $orange-500, $orange-600, $orange-600, $orange-800, $white);
}
@mixin btn-red {
@@ -182,7 +182,7 @@
}
&.btn-warning {
- @include btn-outline($white, $orange-500, $orange-500, $orange-100, $orange-700, $orange-500, $orange-200, $orange-600, $orange-800);
+ @include btn-outline($white, $orange-500, $orange-500, $orange-50, $orange-600, $orange-600, $orange-100, $orange-700, $orange-700);
}
&.btn-primary,
@@ -202,7 +202,7 @@
&.btn-close,
&.btn-close-color {
- @include btn-outline($white, $orange-600, $orange-500, $orange-100, $orange-700, $orange-500, $orange-200, $orange-600, $orange-800);
+ @include btn-outline($white, $orange-500, $orange-500, $orange-50, $orange-600, $orange-600, $orange-100, $orange-700, $orange-700);
}
&.btn-spam {
@@ -229,7 +229,7 @@
}
&.btn-icon {
- color: $gl-gray-700;
+ color: $gray-700;
}
.fa-caret-down,
@@ -394,7 +394,7 @@
}
.clone-dropdown-btn a {
- color: $gl-gray-700;
+ color: $gray-700;
&:hover {
text-decoration: none;
@@ -542,3 +542,13 @@ fieldset[disabled] .btn,
.btn-no-padding {
padding: 0;
}
+
+// This class helps convert `.gl-button` children so that they consistently
+// match the style of `.btn` elements which might be around them. Ideally we
+// wouldn't need this class.
+//
+// Remove by upgrading all buttons in a container to use the new `.gl-button` style.
+.gl-button-deprecated-adapter .gl-button {
+ box-shadow: none;
+ border-width: 1px;
+}
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index c5bb2a1256a..d0d2328ea98 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -37,12 +37,12 @@
}
.bs-callout-warning {
- background-color: $orange-100;
+ background-color: $orange-50;
border-color: $orange-200;
- color: $orange-900;
+ color: $gray-900;
a {
- color: $orange-900;
+ color: $blue-600;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 00679cf20fa..714ef8b2175 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -74,7 +74,7 @@
.hint {
font-style: italic;
- color: $gl-gray-200;
+ color: $gray-200;
}
.light { color: $gl-text-color; }
@@ -162,13 +162,13 @@ table {
.loading {
margin: 20px auto;
height: 40px;
- color: $gl-gray-700;
+ color: $gray-700;
font-size: 32px;
text-align: center;
}
p.time {
- color: $gl-gray-200;
+ color: $gray-200;
font-size: 90%;
margin: 30px 3px 3px 2px;
}
@@ -237,7 +237,7 @@ li.note {
}
.warning_message {
- @include message($orange-100, $orange-200, $orange-800);
+ @include message($orange-50, $orange-200, $gray-900);
}
.danger_message {
@@ -246,7 +246,7 @@ li.note {
.gitlab-promo {
a {
- color: $gl-gray-350;
+ color: $gray-300;
margin-right: 30px;
}
}
@@ -416,7 +416,6 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; }
.ws-initial { white-space: initial; }
.ws-normal { white-space: normal; }
-.ws-pre-wrap { white-space: pre-wrap; }
.overflow-auto { overflow: auto; }
.overflow-visible { overflow: visible; }
@@ -454,35 +453,6 @@ img.emoji {
}
}
-/** COMMON SPACING CLASSES **/
-/**
- 🚨 Do not use these classes — they are deprecated and being removed. 🚨
- See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details.
-
- Instead, if you need a spacing class, please use one from Gitlab UI —
- https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss — which uses the following scale.
- $gl-spacing-scale-0: 0;
- $gl-spacing-scale-1: 2px;
- $gl-spacing-scale-2: 4px;
- $gl-spacing-scale-3: 8px;
- $gl-spacing-scale-4: 12px;
- $gl-spacing-scale-5: 16px;
- $gl-spacing-scale-6: 24px;
- $gl-spacing-scale-7: 32px;
- $gl-spacing-scale-8: 40px;
- $gl-spacing-scale-9: 48px;
- $gl-spacing-scale-10: 56px;
- $gl-spacing-scale-11: 64px;
- $gl-spacing-scale-12: 80px;
- $gl-spacing-scale-13: 96px;
-**/
-@each $index, $padding in $spacing-scale {
- #{'.gl-p-#{$index}-deprecated-no-really-do-not-use-me'} { padding: $padding; }
- #{'.gl-pl-#{$index}-deprecated-no-really-do-not-use-me'} { padding-left: $padding; }
- #{'.gl-pr-#{$index}-deprecated-no-really-do-not-use-me'} { padding-right: $padding; }
- #{'.gl-pt-#{$index}-deprecated-no-really-do-not-use-me'} { padding-top: $padding; }
- #{'.gl-pb-#{$index}-deprecated-no-really-do-not-use-me'} { padding-bottom: $padding; }
-}
/**
* Removes browser specific clear icon from input fields in
@@ -557,4 +527,3 @@ img.emoji {
See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details.
**/
.gl-line-height-14 { line-height: $gl-line-height-14; }
-.gl-font-size-20 { font-size: $gl-font-size-20; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 6b742853f8f..ad5864ef6d9 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -164,14 +164,20 @@
right: 8px;
}
- .ic-chevron-down {
+ .dropdown-menu-toggle-icon {
position: absolute;
- top: $gl-padding-8;
right: $gl-padding-8;
color: $gray-darkest;
}
}
+.labels {
+ // Prevent double scroll-bars for labels dropdown.
+ .dropdown-menu-toggle.wide + .dropdown-select {
+ max-height: unset;
+ }
+}
+
.gl-dropdown .dropdown-menu-toggle {
padding-right: $gl-padding-8;
@@ -410,7 +416,7 @@
flex-direction: column;
.reference {
- color: $gl-gray-400;
+ color: $gray-300;
margin-top: $gl-padding-4;
}
}
@@ -590,11 +596,8 @@
}
.dropdown-title-button {
- position: absolute;
- top: 0;
padding: 0;
color: $dropdown-title-btn-color;
- font-size: 14px;
border: 0;
background: none;
outline: 0;
@@ -604,20 +607,9 @@
}
}
-.dropdown-menu-close {
- top: $gl-padding-4;
- right: $gl-padding-8;
- width: 20px;
- height: 20px;
-}
-
-.dropdown-menu-close-icon {
- vertical-align: middle;
-}
-
.dropdown-menu-back {
- left: 7px;
- top: 2px;
+ left: 10px;
+ top: $gl-padding-8;
}
.dropdown-input {
@@ -627,7 +619,8 @@
.fa,
.input-icon,
- .ic-search {
+ .dropdown-input-clear,
+ .dropdown-input-search {
position: absolute;
top: $gl-padding-8;
right: 20px;
@@ -666,25 +659,27 @@
width: 100%;
min-height: 30px;
padding: 0 7px;
- color: $gl-gray-700;
+ color: $gray-700;
line-height: 30px;
border: 1px solid $dropdown-divider-bg;
border-radius: 2px;
outline: 0;
&:focus {
- color: $gl-gray-700;
+ color: $gray-700;
border-color: $blue-300;
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
- ~ .fa {
- color: $gl-gray-700;
+ ~ .fa,
+ ~ .dropdown-input-clear {
+ color: $gray-700;
}
}
&:hover {
- ~ .fa {
- color: $gl-gray-700;
+ ~ .fa,
+ ~ .dropdown-input-clear {
+ color: $gray-700;
}
}
}
@@ -1070,7 +1065,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
color: $dropdown-title-btn-color;
&:hover {
- color: $gl-gray-400;
+ color: $gray-300;
}
}
}
@@ -1105,3 +1100,42 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
}
}
}
+
+// This class won't be needed once we can directly add utility classes to the child
+// https://github.com/bootstrap-vue/bootstrap-vue/issues/5669
+.gl-dropdown-text-py-0 {
+ .b-dropdown-text {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+}
+
+// This class won't be needed once we can directly add utility classes to the child
+// https://github.com/bootstrap-vue/bootstrap-vue/issues/5669
+.gl-dropdown-text-block {
+ .b-dropdown-text {
+ display: block;
+ }
+}
+
+// This class won't be needed once we can add a prop for this in the GitLab UI component
+// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/966
+.gl-new-dropdown {
+ .gl-dropdown-menu-wide {
+ width: $gl-dropdown-width-wide;
+ }
+}
+
+.gl-dropdown-item-deprecated-adapter {
+ .dropdown-item {
+ align-items: flex-start;
+
+ .gl-new-dropdown-item-text-primary {
+ @include gl-font-weight-bold;
+ }
+
+ .gl-new-dropdown-item-text-secondary {
+ color: inherit;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index ef7d39a5e7e..76c6e03377c 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -183,7 +183,7 @@
&.line-numbers {
float: none;
- border-left: 1px solid $gl-gray-100;
+ border-left: 1px solid $gray-100;
i {
float: none;
@@ -300,7 +300,7 @@ span.idiff {
.renamed-file {
a {
- color: $orange-600;
+ color: $orange-500;
}
}
@@ -495,9 +495,12 @@ span.idiff {
max-height: 20rem;
}
-#js-openapi-viewer pre.version {
- background-color: transparent;
- border: transparent;
+#js-openapi-viewer {
+ pre.version,
+ code {
+ background-color: transparent;
+ border: transparent;
+ }
}
.code-navigation-line:hover {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index ed4281123cd..1a394ad124b 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -243,7 +243,7 @@
}
}
- .fa-times {
+ .clear-search-icon {
right: 10px;
color: $gray-darkest;
}
@@ -255,7 +255,7 @@
outline: none;
z-index: 1;
- &:hover .fa-times {
+ &:hover .clear-search-icon {
color: $common-gray-dark;
}
}
@@ -348,11 +348,11 @@
}
@include media-breakpoint-down(sm) {
- .issues-details-filters {
- padding-top: 0;
- padding-bottom: 0;
+ .issues-details-filters,
+ .epics-details-filters {
+ padding-top: $gl-padding-8;
+ padding-bottom: $gl-padding-8;
background-color: $white;
- border-top: 0;
}
.boards-switcher {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index d604d97d270..0fb91db0afb 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -65,8 +65,8 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
}
.flash-warning {
- background-color: $orange-100;
- color: $orange-800;
+ background-color: $orange-50;
+ color: $gray-900;
cursor: default;
}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index fbafb22cf37..579a68ac8e4 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -13,7 +13,7 @@
border-radius: $border-radius-default;
&.current-user {
- background-color: $orange-100;
+ background-color: $orange-50;
}
}
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
deleted file mode 100644
index 97bd6ca6fe2..00000000000
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ /dev/null
@@ -1,431 +0,0 @@
-/**
- * Styles the GitLab application with a specific color theme
- */
-
-@mixin gitlab-theme(
- $search-and-nav-links,
- $active-tab-border,
- $border-and-box-shadow,
- $sidebar-text,
- $nav-svg-color,
- $color-alternate
-) {
- // Header
-
- .navbar-gitlab {
- background-color: $nav-svg-color;
-
- .navbar-collapse {
- color: $search-and-nav-links;
- }
-
- .container-fluid {
- .navbar-toggler {
- border-left: 1px solid lighten($border-and-box-shadow, 10%);
-
- svg {
- fill: $search-and-nav-links;
- }
- }
- }
-
- .navbar-sub-nav,
- .navbar-nav {
- > li {
- > a,
- > button {
- &:hover,
- &:focus {
- background-color: rgba($search-and-nav-links, 0.2);
- }
- }
-
- &.active,
- &.dropdown.show {
- > a,
- > button {
- color: $nav-svg-color;
- background-color: $color-alternate;
- }
- }
-
- &.line-separator {
- border-left: 1px solid rgba($search-and-nav-links, 0.2);
- }
- }
- }
-
- .navbar-sub-nav {
- color: $search-and-nav-links;
- }
-
- .nav {
- > li {
- color: $search-and-nav-links;
-
- > a {
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $search-and-nav-links;
- }
-
- .header-user-notification-dot {
- border: 2px solid $nav-svg-color;
- }
- }
-
- &:hover,
- &:focus {
- @include media-breakpoint-up(sm) {
- background-color: rgba($search-and-nav-links, 0.2);
- }
-
- svg {
- fill: currentColor;
- }
-
- &.header-user-dropdown-toggle .header-user-notification-dot {
- border-color: $nav-svg-color + 33;
- }
- }
- }
-
- &.active > a,
- &.dropdown.show > a {
- color: $nav-svg-color;
- background-color: $color-alternate;
-
- &:hover {
- svg {
- fill: $nav-svg-color;
- }
- }
-
- &.header-user-dropdown-toggle .header-user-notification-dot {
- border-color: $white;
- }
- }
-
- .impersonated-user,
- .impersonated-user:hover {
- svg {
- fill: $nav-svg-color;
- }
- }
- }
- }
- }
-
- .navbar .title {
- > a {
- &:hover,
- &:focus {
- background-color: rgba($search-and-nav-links, 0.2);
- }
- }
- }
-
- .search {
- form {
- background-color: rgba($search-and-nav-links, 0.2);
-
- &:hover {
- background-color: rgba($search-and-nav-links, 0.3);
- }
- }
-
- .search-input::placeholder {
- color: rgba($search-and-nav-links, 0.8);
- }
-
- .search-input-wrap {
- .search-icon,
- .clear-icon {
- fill: rgba($search-and-nav-links, 0.8);
- }
- }
-
- &.search-active {
- form {
- background-color: $white;
- }
-
- .search-input-wrap {
- .search-icon {
- fill: rgba($search-and-nav-links, 0.8);
- }
- }
- }
- }
-
- // Sidebar
- .nav-sidebar li.active {
- box-shadow: inset 4px 0 0 $border-and-box-shadow;
-
- > a {
- color: $sidebar-text;
- }
-
- .nav-icon-container svg {
- fill: $sidebar-text;
- }
- }
-
- .sidebar-top-level-items > li.active .badge.badge-pill {
- color: $sidebar-text;
- }
-
- .nav-links li {
- &.active a,
- &.md-header-tab.active button,
- a.active {
- border-bottom: 2px solid $active-tab-border;
-
- .badge.badge-pill {
- font-weight: $gl-font-weight-bold;
- }
- }
- }
-
- .branch-header-title {
- color: $border-and-box-shadow;
- }
-
- .ide-sidebar-link {
- &.active {
- color: $border-and-box-shadow;
- box-shadow: inset 3px 0 $border-and-box-shadow;
-
- &.is-right {
- box-shadow: inset -3px 0 $border-and-box-shadow;
- }
- }
- }
-}
-
-body {
- &.ui-indigo {
- @include gitlab-theme(
- $indigo-200,
- $indigo-500,
- $indigo-700,
- $indigo-800,
- $indigo-900,
- $white
- );
- }
-
- &.ui-light-indigo {
- @include gitlab-theme(
- $indigo-200,
- $indigo-500,
- $indigo-500,
- $indigo-700,
- $indigo-700,
- $white
- );
- }
-
- &.ui-blue {
- @include gitlab-theme(
- $theme-blue-200,
- $theme-blue-500,
- $theme-blue-700,
- $theme-blue-800,
- $theme-blue-900,
- $white
- );
- }
-
- &.ui-light-blue {
- @include gitlab-theme(
- $theme-light-blue-200,
- $theme-light-blue-500,
- $theme-light-blue-500,
- $theme-light-blue-700,
- $theme-light-blue-700,
- $white
- );
- }
-
- &.ui-green {
- @include gitlab-theme(
- $theme-green-200,
- $theme-green-500,
- $theme-green-700,
- $theme-green-800,
- $theme-green-900,
- $white
- );
- }
-
- &.ui-light-green {
- @include gitlab-theme(
- $theme-green-200,
- $theme-green-500,
- $theme-green-500,
- $theme-light-green-700,
- $theme-light-green-700,
- $white
- );
- }
-
- &.ui-red {
- @include gitlab-theme(
- $theme-red-200,
- $theme-red-500,
- $theme-red-700,
- $theme-red-800,
- $theme-red-900,
- $white
- );
- }
-
- &.ui-light-red {
- @include gitlab-theme(
- $theme-light-red-200,
- $theme-light-red-500,
- $theme-light-red-500,
- $theme-light-red-700,
- $theme-light-red-700,
- $white
- );
- }
-
- &.ui-dark {
- @include gitlab-theme(
- $gray-200,
- $gray-300,
- $gray-500,
- $gray-700,
- $gray-900,
- $white
- );
- }
-
- &.ui-light {
- @include gitlab-theme(
- $gray-500,
- $gray-700,
- $gray-500,
- $gray-500,
- $gray-50,
- $gray-500
- );
-
- .navbar-gitlab {
- background-color: $gray-50;
- box-shadow: 0 1px 0 0 $border-color;
-
- .logo-text svg {
- fill: $gray-900;
- }
-
- .navbar-sub-nav,
- .navbar-nav {
- > li {
- > a:hover,
- > a:focus,
- > button:hover {
- color: $gray-900;
- }
-
- &.active > a,
- &.active > a:hover,
- &.active > button {
- color: $white;
- }
- }
- }
-
- .container-fluid {
- .navbar-toggler,
- .navbar-toggler:hover {
- color: $gray-500;
- border-left: 1px solid $gray-100;
- }
- }
- }
-
- .search {
- form {
- background-color: $white;
- box-shadow: inset 0 0 0 1px $border-color;
-
- &:hover {
- background-color: $white;
- box-shadow: inset 0 0 0 1px $blue-200;
- }
- }
-
- .search-input-wrap {
- .search-icon {
- fill: $gray-100;
- }
-
- .search-input {
- color: $gl-text-color;
- }
- }
- }
-
- .nav-sidebar li.active {
- > a {
- color: $gray-900;
- }
-
- svg {
- fill: $gray-900;
- }
- }
-
- .sidebar-top-level-items > li.active .badge.badge-pill {
- color: $gray-900;
- }
- }
-
- &.gl-dark {
- .logo-text svg {
- fill: $gl-text-color;
- }
-
- .navbar-gitlab {
- background-color: $gray-50;
-
- .navbar-sub-nav,
- .navbar-nav {
- li {
- > a:hover,
- > a:focus,
- > button:hover,
- > button:focus {
- color: $gl-text-color;
- background-color: $gray-200;
- }
- }
-
- li.active,
- li.dropdown.show {
- > a,
- > button {
- color: $gl-text-color;
- background-color: $gray-200;
- }
- }
- }
-
- .search {
- form {
- background-color: $gray-100;
- box-shadow: inset 0 0 0 1px $border-color;
-
- &:active,
- &:hover {
- background-color: $gray-100;
- box-shadow: inset 0 0 0 1px $blue-200;
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 50628c7de82..cf21c23cb17 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -235,9 +235,8 @@
border-top-left-radius: 0;
border-bottom-left-radius: 0;
- i {
+ svg {
color: $orange-500;
- font-size: 20px;
}
}
}
@@ -459,15 +458,15 @@
box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
&.green-badge {
- background-color: $green-500;
+ background-color: $green-400;
}
&.merge-requests-count {
- background-color: $orange-600;
+ background-color: $orange-400;
}
&.todos-count {
- background-color: $blue-500;
+ background-color: $blue-400;
}
}
diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss
index 2448be1bca3..d48a5116677 100644
--- a/app/assets/stylesheets/framework/job_log.scss
+++ b/app/assets/stylesheets/framework/job_log.scss
@@ -17,7 +17,7 @@
}
.line-number {
- color: $gl-gray-500;
+ color: $gray-500;
padding: 0 $gl-padding-8;
min-width: $job-line-number-width;
margin-left: -$job-line-number-margin;
@@ -28,7 +28,7 @@
&:active,
&:visited {
text-decoration: underline;
- color: $gl-gray-500;
+ color: $gray-500;
}
}
@@ -43,7 +43,7 @@
}
.log-duration-badge {
- background: $gl-gray-400;
+ background: $gray-300;
}
.loader-animation {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 738150dbd2e..2464ea3607b 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -34,17 +34,6 @@
}
}
- &.warning-row {
- background-color: $orange-100;
- border-color: $orange-200;
- color: $orange-700;
-
- &:hover {
- background: $orange-100;
- }
-
- }
-
&.smoke { background-color: $gray-light; }
&:last-child {
@@ -132,10 +121,10 @@ ul.content-list {
a {
color: $gl-text-color;
- }
- .member-group-link {
- color: $blue-600;
+ &.inline-link {
+ color: $blue-600;
+ }
}
.description {
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 1352fa13e1a..292d57f132c 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -97,13 +97,8 @@
}
/* Small devices (phones, tablets, 768px and lower) */
- @include media-breakpoint-down(xs) {
+ @include media-breakpoint-down(sm) {
width: 100%;
-
- &.mobile-separator {
- border-bottom: 1px solid $border-color;
- margin-bottom: $gl-padding-8;
- }
}
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index e81ecfb43d5..86a5aa1a16e 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -300,7 +300,7 @@
}
.group-path {
- color: $gl-gray-400;
+ color: $gray-300;
}
}
@@ -310,7 +310,7 @@
}
.project-path {
- color: $gl-gray-400;
+ color: $gray-300;
}
}
@@ -332,7 +332,7 @@
.namespace-result {
.namespace-kind {
- color: $gl-gray-350;
+ color: $gray-300;
font-weight: $gl-font-weight-normal;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 4ba9db811b7..d867cc96dbc 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -145,6 +145,13 @@
.value.dont-hide ~ .selectbox {
padding-top: $gl-padding-8;
}
+
+ // This is for sidebar components using gl-button for the Edit button to be consistent with the
+ // rest of the sidebar, and could be removed once the sidebar has been fully converted to use
+ // gitlab-ui components.
+ .title .gl-button {
+ color: $gl-text-color;
+ }
}
.pikaday-container {
diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss
index d734895c7dc..a74aeb9f220 100644
--- a/app/assets/stylesheets/framework/spinner.scss
+++ b/app/assets/stylesheets/framework/spinner.scss
@@ -31,7 +31,7 @@
border-style: solid;
display: inline-flex;
@include spinner-size(16px, 2px);
- @include spinner-color($orange-600);
+ @include spinner-color($orange-400);
&.spinner-md {
@include spinner-size(32px, 3px);
diff --git a/app/assets/stylesheets/framework/stacked_progress_bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss
index a3037549881..37283db8b71 100644
--- a/app/assets/stylesheets/framework/stacked_progress_bar.scss
+++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss
@@ -37,7 +37,7 @@
.status-neutral {
background-color: $gray-100;
- color: $gl-gray-dark;
+ color: $gray-900;
&:hover {
background-color: $gray-200;
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 1f60485aa36..59e83608d9d 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -180,3 +180,37 @@ table {
border-top: 0;
}
}
+
+.vulnerability-list {
+ @media (min-width: $breakpoint-sm) {
+ .checkbox {
+ padding-left: $gl-spacing-scale-4;
+ padding-right: 0;
+
+ + td,
+ + th {
+ padding-left: $gl-spacing-scale-4;
+ }
+ }
+
+ .detected {
+ width: 9%;
+ }
+
+ .status {
+ width: 8%;
+ }
+
+ .severity {
+ width: 10%;
+ }
+
+ .identifier {
+ width: 16%;
+ }
+
+ .scanner {
+ width: 15%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 8758fe15870..8b5fa6c1b6c 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -84,7 +84,7 @@
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
- color: $gl-gray-700;
+ color: $gray-700;
vertical-align: middle;
background-color: $gray-10;
border-width: 1px;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 69e00f9b2c4..8cebfc430e0 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -104,15 +104,6 @@ $t-gray-a-04: rgba($black, 0.04) !default;
$t-gray-a-06: rgba($black, 0.06) !default;
$t-gray-a-08: rgba($black, 0.08) !default;
-$gl-gray-100: #ddd !default;
-$gl-gray-200: #ccc !default;
-$gl-gray-350: #aaa !default;
-$gl-gray-400: #999 !default;
-$gl-gray-500: #777 !default;
-$gl-gray-600: #666 !default;
-$gl-gray-700: #555 !default;
-$gl-gray-800: #333 !default;
-
$green-50: #ecf4ee !default;
$green-100: #c3e6cd !default;
$green-200: #91d4a8 !default;
@@ -137,17 +128,17 @@ $blue-800: #064787 !default;
$blue-900: #033464 !default;
$blue-950: #002850 !default;
-$orange-50: #fffaf4 !default;
-$orange-100: #fff1de !default;
-$orange-200: #fed69f !default;
-$orange-300: #fdbc60 !default;
-$orange-400: #fca429 !default;
-$orange-500: #fc9403 !default;
-$orange-600: #de7e00 !default;
-$orange-700: #c26700 !default;
-$orange-800: #a35200 !default;
-$orange-900: #853c00 !default;
-$orange-950: #592800 !default;
+$orange-50: #fdf1dd !default;
+$orange-100: #f5d9a8 !default;
+$orange-200: #e9be74 !default;
+$orange-300: #d99530 !default;
+$orange-400: #c17d10 !default;
+$orange-500: #ab6100 !default;
+$orange-600: #9e5400 !default;
+$orange-700: #8f4700 !default;
+$orange-800: #703800 !default;
+$orange-900: #5c2900 !default;
+$orange-950: #421f00 !default;
$red-50: #fcf1ef !default;
$red-100: #fdd4cd !default;
@@ -161,6 +152,18 @@ $red-800: #8d1300 !default;
$red-900: #660e00 !default;
$red-950: #4d0a00 !default;
+$purple-50: #f4f0ff !default;
+$purple-100: #e1d8f9 !default;
+$purple-200: #cbbbf2 !default;
+$purple-300: #ac93e6 !default;
+$purple-400: #9475db !default;
+$purple-500: #7b58cf !default;
+$purple-600: #694cc0 !default;
+$purple-700: #5943b6 !default;
+$purple-800: #453894 !default;
+$purple-900: #2f2a6b !default;
+$purple-950: #232150 !default;
+
$gray-10: #fafafa !default;
$gray-50: #f0f0f0 !default;
$gray-100: #dbdbdb !default;
@@ -230,6 +233,20 @@ $reds: (
'950': $red-950
);
+$purples: (
+ '50': $purple-50,
+ '100': $purple-100,
+ '200': $purple-200,
+ '300': $purple-300,
+ '400': $purple-400,
+ '500': $purple-500,
+ '600': $purple-600,
+ '700': $purple-700,
+ '800': $purple-800,
+ '900': $purple-900,
+ '950': $purple-950
+);
+
$grays: (
'10': $gray-10,
'50': $gray-50,
@@ -357,13 +374,10 @@ $gl-text-color-inverted: $white;
$gl-text-color-secondary-inverted: rgba($white, 0.85);
$gl-text-color-disabled: $gray-400;
$gl-grayish-blue: #7f8fa4;
-$gl-gray-dark: #313236;
-$gl-gray-light: #5c5c5c;
$gl-header-color: #4c4e54;
$gl-font-size-12: 12px;
$gl-font-size-14: 14px;
$gl-font-size-16: 16px;
-$gl-font-size-20: 20px;
$gl-font-size-28: 28px;
$gl-font-size-42: 42px;
@@ -484,6 +498,22 @@ $line-added: #ecfdf0;
$line-added-dark: #c7f0d2 !default;
$line-removed: #fbe9eb;
$line-removed-dark: #fac5cd !default;
+/*
+ * The transparent colors are used in Monaco editor. Using full opacity colors
+ * would hide other layers (selected text, matching brackets).
+ *
+ * When the transparent colors get layered on white background, they create their
+ * full opacity counterparts (computed with https://stackoverflow.com/a/12228643/606571):
+ *
+ * - white + $line-added-transparent = $line-added
+ * - white + $line-added-transparent + $line-added-dark-transparent = $line-added-dark
+ *
+ * More details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41553
+ */
+$line-added-transparent: rgba(160, 245, 180, 0.2);
+$line-added-dark-transparent: rgba(51, 188, 90, 0.2);
+$line-removed-transparent: rgba(235, 145, 155, 0.2);
+$line-removed-dark-transparent: rgba(246, 53, 85, 0.2);
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
@@ -588,7 +618,6 @@ $award-emoji-width-xs: 90%;
$search-input-border-color: rgba($blue-400, 0.8);
$search-input-width: 200px;
$search-input-xl-width: 320px;
-$location-icon-color: #e7e9ed;
/*
* Notes
@@ -894,8 +923,3 @@ $compare-branches-sticky-header-height: 68px;
- Issue: https://gitlab.com/gitlab-org/design.gitlab.com/issues/242
*/
$enable-validation-icons: false;
-
-/*
-Licenses
-*/
-$license-header-cell-width: 150px;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 3eff1807403..55996a074c6 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -110,10 +110,6 @@
.dark-well {
background-color: $gray-normal;
-
- .btn {
- width: 100%;
- }
}
.card.card-body-centered {
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index dd5b99be57e..62abf4a7683 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -35,7 +35,7 @@
.zen-control {
padding: 0;
- color: $gl-gray-700;
+ color: $gray-700;
background: none;
border: 0;
}
diff --git a/app/assets/stylesheets/mailer.scss b/app/assets/stylesheets/mailer.scss
index f188b29a113..a5fc92237df 100644
--- a/app/assets/stylesheets/mailer.scss
+++ b/app/assets/stylesheets/mailer.scss
@@ -48,6 +48,22 @@ a {
font-weight: 500;
}
+.invite-header {
+ margin-top: 0;
+}
+
+.invite-actions {
+ margin-top: 24px;
+}
+
+.invite-btn-join {
+ border-radius: $border-radius-default;
+ padding: $gl-btn-vert-padding $gl-btn-horz-padding;
+ cursor: pointer;
+ background-color: $purple;
+ color: $white;
+}
+
tr td {
font-family: $mailer-font;
}
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index 6320c10fb51..d281f62c370 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -7,12 +7,12 @@ img {
p.details {
font-style: italic;
- color: $gl-gray-500;
+ color: $gray-500;
}
.footer > p {
font-size: small;
- color: $gl-gray-500;
+ color: $gray-500;
}
pre.commit-message {
diff --git a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
index 0b847902525..57053c7f0cb 100644
--- a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
@@ -19,14 +19,6 @@
display: none;
}
- .monaco-editor .selected-text {
- z-index: 1;
- }
-
- .monaco-editor .view-lines {
- z-index: 2;
- }
-
.is-readonly .editor.original {
.view-lines {
cursor: default;
@@ -98,11 +90,11 @@
}
.char-insert {
- background-color: $line-added-dark;
+ background-color: $line-added-dark-transparent;
}
.char-delete {
- background-color: $line-removed-dark;
+ background-color: $line-removed-dark-transparent;
}
.line-numbers {
@@ -111,11 +103,11 @@
.view-overlays {
.line-insert {
- background-color: $line-added;
+ background-color: $line-added-transparent;
}
.line-delete {
- background-color: $line-removed;
+ background-color: $line-removed-transparent;
}
}
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index dfd7fd355a4..1e239877428 100644
--- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -84,7 +84,8 @@
color: var(--ide-input-border, $gl-text-color-tertiary);
}
- .dropdown-input .fa {
+ .dropdown-input .fa,
+ .dropdown-input .dropdown-input-clear {
color: var(--ide-input-border, $dropdown-input-fa-color);
}
diff --git a/app/assets/stylesheets/page_bundles/_mixins_and_variables_and_functions.scss b/app/assets/stylesheets/page_bundles/_mixins_and_variables_and_functions.scss
new file mode 100644
index 00000000000..0e8ea5e2d52
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/_mixins_and_variables_and_functions.scss
@@ -0,0 +1,21 @@
+/**
+ This file contains only imports of Bootstrap, GitLab UI and GitLab mixins,
+ variables and functions, in the correct order.
+
+ It is meant to be used in page_bundles, but SHOULD NOT introduce any
+ styles of it's own. We actually check in CI that compiling _this_ file doesn't
+ result in any additional styles.
+
+ See: scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js
+ */
+@import 'framework/variables';
+@import 'framework/variables_overrides';
+@import 'framework/mixins';
+
+@import 'bootstrap/scss/functions';
+@import 'bootstrap/scss/variables';
+@import 'bootstrap/scss/mixins';
+
+@import '@gitlab/ui/src/scss/functions';
+@import '@gitlab/ui/src/scss/variables';
+@import '@gitlab/ui/src/scss/utility-mixins/index';
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 36587ecde3d..71e74297ee8 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1080,7 +1080,7 @@ $ide-commit-header-height: 48px;
max-width: 24px;
padding: 0;
margin: 0 ($grid-size / 2);
- color: var(--ide-text-color-secondary, $gl-gray-light);
+ color: var(--ide-text-color-secondary, $gray-600);
&:first-child {
margin-left: 0;
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
new file mode 100644
index 00000000000..83d16f29d49
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -0,0 +1,33 @@
+@import 'framework/variables';
+
+$atlaskit-border-color: #dfe1e6;
+
+.ac-content {
+ margin: 20px;
+
+ .subscription-form {
+ margin-bottom: 20px;
+
+ .field-group-input {
+ display: flex;
+ padding-top: $gl-padding-4;
+
+ .ak-button {
+ height: auto;
+ margin-left: $btn-margin-5;
+ }
+ }
+ }
+}
+
+.subscriptions {
+ tbody {
+ tr {
+ border-bottom: 1px solid $atlaskit-border-color;
+ }
+
+ td {
+ padding: $gl-padding-8;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/page_bundles/todos.scss
index c6f104a024b..3eec5b53a30 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/page_bundles/todos.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
/**
* Dashboard Todos
*
@@ -10,8 +12,8 @@
flex-direction: row;
&:hover {
- background-color: $blue-50;
- border-color: $blue-200;
+ background-color: var(--blue-50, $blue-50);
+ border-color: var(--blue-200, $blue-200);
cursor: pointer;
}
@@ -20,7 +22,7 @@
border-bottom: 1px solid transparent;
&:hover {
- border-color: $blue-200;
+ border-color: var(--blue-200, $blue-200);
}
}
@@ -44,11 +46,9 @@
}
&.todo-pending.done-reversible {
- background-color: $white;
-
&:hover {
- border-color: $white-normal;
- background-color: $gray-light;
+ border-color: var(--border-color, $border-color);
+ background-color: var(--gray-50, $gray-50);
border-top: 1px solid transparent;
.todo-avatar,
@@ -63,7 +63,7 @@
}
.btn {
- background-color: $gray-light;
+ background-color: var(--gray-50, $gray-50);
}
}
}
@@ -103,15 +103,15 @@
.todo-label,
.todo-project {
a {
- color: $blue-600;
font-weight: $gl-font-weight-normal;
+ color: var(--blue-600, $blue-600);
}
}
.todo-body {
.badge.badge-pill,
p {
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
}
.md {
@@ -125,9 +125,9 @@
pre {
border: 0;
- background: $gray-light;
+ background: var(--gray-50, $gray-50);
border-radius: 0;
- color: $gl-gray-500;
+ color: var(--gray-500, $gray-500);
margin: 0 20px;
overflow: hidden;
}
@@ -149,18 +149,6 @@
}
}
-@include media-breakpoint-down(sm) {
- .todos-filters {
- .dropdown-menu-toggle {
- width: 130px;
- }
-
- .dropdown-menu-toggle-sort {
- width: auto;
- }
- }
-}
-
@include media-breakpoint-down(lg) {
.todos-filters {
.filter-categories {
@@ -174,6 +162,10 @@
}
@include media-breakpoint-down(sm) {
+ .container-fluid .todos-list-container {
+ margin: 0 (-$gl-padding);
+ }
+
.todo {
.avatar {
display: none;
@@ -191,7 +183,7 @@
.todo-body {
margin: 0;
- border-left: 2px solid $gl-gray-100;
+ border-left: 2px solid var(--border-color, $border-color);
padding-left: 10px;
}
}
@@ -204,6 +196,10 @@
.dropdown-menu-toggle {
width: 100%;
}
+
+ .dropdown-menu-toggle-sort {
+ width: auto;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/alert_management/details.scss b/app/assets/stylesheets/pages/alert_management/details.scss
index 0f889935583..a104c06c853 100644
--- a/app/assets/stylesheets/pages/alert_management/details.scss
+++ b/app/assets/stylesheets/pages/alert_management/details.scss
@@ -1,39 +1,4 @@
.alert-management-details {
- // these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui
- table {
- tr {
- td {
- @include gl-border-0;
- @include gl-p-5;
- border-color: transparent;
- border-bottom: 1px solid $table-border-color;
-
- &:first-child {
- div {
- font-weight: bold;
- }
- }
-
- &:not(:first-child) {
- &::before {
- color: $gray-500;
- font-weight: normal !important;
- }
-
- div {
- color: $gray-500;
- }
- }
-
- @include media-breakpoint-up(sm) {
- div {
- text-align: left !important;
- }
- }
- }
- }
- }
-
@include media-breakpoint-down(xs) {
.alert-details-incident-button {
width: 100%;
@@ -67,6 +32,10 @@
}
}
+ .main-notes-list::before {
+ left: 15px !important;
+ }
+
.note-header-info {
@include gl-mt-1;
}
diff --git a/app/assets/stylesheets/pages/alert_management/severity-icons.scss b/app/assets/stylesheets/pages/alert_management/severity-icons.scss
index 6004697b3e1..f58ad87a673 100644
--- a/app/assets/stylesheets/pages/alert_management/severity-icons.scss
+++ b/app/assets/stylesheets/pages/alert_management/severity-icons.scss
@@ -1,3 +1,4 @@
+.incident-severity,
.incident-management-list,
.alert-management-details {
.icon-critical {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 51a65b88cd0..c4852974a4d 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -116,7 +116,6 @@
.board-title {
flex-direction: column;
- height: 100%;
}
.board-title-caret {
@@ -284,7 +283,7 @@
}
.confidential-icon {
- color: $orange-600;
+ color: $orange-500;
cursor: help;
}
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
index 3c49cc54ac4..d34d309eea3 100644
--- a/app/assets/stylesheets/pages/branches.scss
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -23,7 +23,7 @@
.bar {
height: 4px;
- background-color: $gl-gray-100;
+ background-color: $gray-100;
}
.count {
@@ -34,7 +34,7 @@
.graph-separator {
width: $graph-separator-width;
height: 18px;
- background-color: $gl-gray-100;
+ background-color: $gray-100;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index f367d9ea4d8..04167cbee1b 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -50,7 +50,7 @@
top: $header-height;
border-radius: 2px 2px 0 0;
color: $orange-600;
- background-color: $orange-100;
+ background-color: $orange-50;
border: 1px solid $border-gray-normal;
padding: 3px 12px;
margin: auto;
@@ -63,7 +63,7 @@
}
.top-bar {
- @include build-trace-top-bar(35px);
+ @include build-trace-top-bar(50px);
&.has-archived-block {
top: $header-height + 28px;
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 29422c1f7fa..4e27f438e36 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -69,7 +69,7 @@
align-self: flex-start;
font-weight: 500;
font-size: 20px;
- color: $orange-900;
+ color: $orange-500;
opacity: 1;
margin: $gl-padding-8 14px 0 0;
}
@@ -124,7 +124,7 @@
background-color: none;
border: 0;
font-weight: bold;
- color: $gl-gray-500;
+ color: $gray-500;
}
}
}
@@ -156,7 +156,7 @@
}
.cluster-deployments-warning {
- color: $orange-600;
+ color: $orange-500;
}
.badge.pods-badge {
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index b97709e140f..c509bf121bc 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -41,15 +41,6 @@
width: 20%;
}
- .fa,
- svg {
- color: $cycle-analytics-light-gray;
-
- &:hover {
- color: $gl-text-color;
- }
- }
-
.stage-header {
width: 20.5%;
}
@@ -360,23 +351,3 @@
}
}
}
-
-.cycle-analytics-overview {
- padding-top: 100px;
-
- .overview-details {
- display: flex;
- align-items: center;
- }
-
- .overview-image {
- text-align: right;
- }
-
- .overview-icon {
- svg {
- width: 365px;
- height: 227px;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/dev_ops_score.scss b/app/assets/stylesheets/pages/dev_ops_report.scss
index b9ee905f4b6..871cd9c4f02 100644
--- a/app/assets/stylesheets/pages/dev_ops_score.scss
+++ b/app/assets/stylesheets/pages/dev_ops_report.scss
@@ -25,6 +25,10 @@ $space-between-cards: 8px;
margin-left: 8px;
font-weight: $gl-font-weight-normal;
+ .devops-header-icon {
+ vertical-align: px-to-rem(-$gl-spacing-scale-1);
+ }
+
a {
font-size: 18px;
color: $gl-text-color-secondary;
@@ -88,7 +92,7 @@ $space-between-cards: 8px;
}
.devops-card-average {
- border-top-color: $orange-400;
+ border-top-color: $orange-200;
.board-card-score-big {
background-color: $orange-50;
@@ -247,7 +251,7 @@ $space-between-cards: 8px;
}
.devops-average-score {
- color: $orange-400;
+ color: $orange-500;
}
.devops-low-score {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index a7b93c9eab7..62af7103b39 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -102,7 +102,7 @@
.file-mode-changed {
padding: 10px;
- color: $gl-gray-500;
+ color: $gray-500;
}
.suppressed-container {
@@ -181,7 +181,7 @@
.swipe-wrap {
overflow: hidden;
- border-right: 1px solid $gl-gray-400;
+ border-right: 1px solid $gray-300;
position: absolute;
display: block;
top: 13px;
@@ -190,7 +190,7 @@
&.left-oriented {
/* only for commit view (different swipe viewer) */
border-right: 0;
- border-left: 1px solid $gl-gray-400;
+ border-left: 1px solid $gray-300;
}
}
@@ -1062,7 +1062,7 @@ table.code {
.diff-tree-list {
position: -webkit-sticky;
position: sticky;
- $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 11px;
+ $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
top: $top-pos;
max-height: calc(100vh - #{$top-pos});
z-index: 202;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index ef7b56ac210..5ce500dad1d 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -83,7 +83,7 @@
.x-axis path,
.y-axis path {
- stroke: $gl-gray-350;
+ stroke: $gray-300;
}
.label-x-axis-line,
@@ -93,7 +93,7 @@
.y-axis {
line {
- stroke: $gl-gray-350;
+ stroke: $gray-300;
stroke-width: 1;
}
}
@@ -117,7 +117,7 @@
}
.selected-metric-line {
- stroke: $gl-gray-dark;
+ stroke: $gray-900;
stroke-width: 1;
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 500f5816d38..5738cbb4b31 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -94,7 +94,7 @@
border: 0;
background: $gray-light;
border-radius: 0;
- color: $gl-gray-500;
+ color: $gray-500;
overflow: hidden;
}
@@ -111,7 +111,7 @@
}
.event-note-icon {
- color: $gl-gray-500;
+ color: $gray-500;
float: left;
font-size: $gl-font-size;
margin-right: 5px;
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index c4b6cdd703d..bca4d50973a 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -43,7 +43,7 @@
.y-axis-label {
line {
- stroke: $gl-gray-350;
+ stroke: $gray-300;
}
text {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index c309c8d157a..69fd094f83b 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -52,7 +52,7 @@
.save-group-loader {
margin-top: $gl-padding-50;
margin-bottom: $gl-padding-50;
- color: $gl-gray-700;
+ color: $gray-700;
}
.group-nav-container .nav-controls {
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index ab281bc7f23..540060d60de 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -1,6 +1,6 @@
.shortcut-mappings {
font-size: 12px;
- color: $gl-gray-700;
+ color: $gray-700;
tbody:first-child tr:first-child {
padding-top: 0;
@@ -22,7 +22,7 @@
.shortcut {
padding-right: 10px;
- color: $gl-gray-400;
+ color: $gray-300;
text-align: right;
white-space: nowrap;
}
diff --git a/app/assets/stylesheets/pages/incident_management_list.scss b/app/assets/stylesheets/pages/incident_management_list.scss
index 00ca3cc73e0..316066694a8 100644
--- a/app/assets/stylesheets/pages/incident_management_list.scss
+++ b/app/assets/stylesheets/pages/incident_management_list.scss
@@ -108,7 +108,7 @@
border-bottom-width: 0;
.gl-tab-nav-item {
- color: $gl-gray-600;
+ color: $gray-500;
> .gl-tab-counter-badge {
color: inherit;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 2f28361f62c..53525a4d877 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,5 +1,5 @@
.issuable-warning-icon {
- background-color: $orange-100;
+ background-color: $orange-50;
border-radius: $border-radius-default;
width: $issuable-warning-size;
height: $issuable-warning-size;
@@ -50,6 +50,7 @@
.title-container {
display: flex;
+ align-items: flex-start;
}
.title {
@@ -65,7 +66,6 @@
.btn-edit {
margin-left: auto;
- height: $gl-padding * 2;
}
.emoji-block {
@@ -119,11 +119,11 @@
.assignee {
.merge-icon {
- color: $orange-500;
+ color: $orange-400;
position: absolute;
bottom: 0;
right: 0;
- text-shadow: -1px -1px 0 $white, 1px -1px 0 $white, -1px 1px 0 $white, 1px 1px 0 $white;
+ text-shadow: -1px -1px 2px $white, 1px -1px 2px $white, -1px 1px 2px $white, 1px 1px 2px $white;
}
}
@@ -234,8 +234,8 @@
.title {
color: $gl-text-color;
- margin-bottom: $gl-padding-8;
- line-height: 1;
+ margin-bottom: $gl-padding-4;
+ line-height: $gl-line-height-20;
.avatar {
margin-left: 0;
@@ -252,7 +252,8 @@
}
}
- .cross-project-reference {
+ .cross-project-reference,
+ .sidebar-mr-source-branch {
color: inherit;
span {
@@ -360,13 +361,6 @@
margin: 0;
}
- .username {
- display: block;
- margin-top: 4px;
- font-size: 13px;
- font-weight: $gl-font-weight-normal;
- }
-
.hide-expanded {
display: none;
}
@@ -389,7 +383,8 @@
border-bottom: 0;
overflow: hidden;
- &:hover {
+ &.with-sub-blocks .sub-block:hover,
+ &:not(.with-sub-blocks):hover {
background-color: $gray-100;
}
@@ -444,11 +439,6 @@
}
}
- span {
- display: block;
- margin-top: 0;
- }
-
.sidebar-avatar-counter {
padding-top: 2px;
}
@@ -741,7 +731,6 @@
.issuable-info-container {
flex: 1;
display: flex;
- padding-right: $gl-padding;
.issuable-main-info {
flex: 1 auto;
@@ -919,12 +908,12 @@
}
.issuable-todo-btn {
- .fa-spinner {
+ .gl-spinner {
display: none;
}
&.is-loading {
- .fa-spinner {
+ .gl-spinner {
display: inline-block;
}
@@ -982,7 +971,7 @@
}
.suggestion-confidential {
- color: $orange-600;
+ color: $orange-500;
}
.suggestion-state-open {
@@ -1006,3 +995,15 @@
border: 0;
}
}
+
+@include media-breakpoint-down(sm) {
+ // Overriding the following rule with the negative margin
+ // https://gitlab.com/gitlab-org/gitlab/-/blob/146c43c931c3743a140529307aea616e4aa9ff21/app/assets/stylesheets/framework/sidebar.scss#L1-5
+ .container-fluid {
+ .issuable-list,
+ .issues-filters,
+ .epics-filters {
+ margin: 0 (-$gl-padding);
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 0c349ab73a3..03603f637c8 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -14,7 +14,7 @@
}
.issue {
- padding: 10px 0 10px $gl-padding;
+ padding: 10px $gl-padding;
position: relative;
.title {
@@ -294,12 +294,12 @@ ul.related-merge-requests > li {
&::after {
content: image-url('icon_anchor.svg');
- @include invisible(hidden);
+ visibility: hidden;
}
}
&:hover > a.anchor::after {
- @include invisible(visible);
+ visibility: visible;
}
}
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 54bca80194f..2d9a9f3029f 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -180,10 +180,6 @@
word-break: break-all;
}
- .member-group-link {
- display: inline-block;
- }
-
.form-control {
width: inherit;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index a6d1fc11c3f..8aaeb92eb7a 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -64,7 +64,7 @@ $mr-widget-min-height: 69px;
background-color: $gray-light;
&.clickable:hover {
- background-color: $gl-gray-100;
+ background-color: $gray-100;
cursor: pointer;
}
}
@@ -311,7 +311,7 @@ $mr-widget-min-height: 69px;
.bold {
font-weight: $gl-font-weight-bold;
- color: $gl-gray-light;
+ color: $gray-600;
margin-left: 10px;
}
@@ -873,7 +873,6 @@ $mr-widget-min-height: 69px;
.merge-request-tabs-container,
.epic-tabs-container {
flex-direction: column-reverse;
- padding-top: $gl-padding-8;
}
}
@@ -883,7 +882,6 @@ $mr-widget-min-height: 69px;
.epic-tabs-container {
flex-direction: column-reverse;
align-items: flex-start;
- padding-top: $gl-padding-8;
}
}
}
@@ -980,7 +978,7 @@ $mr-widget-min-height: 69px;
opacity: 0.65;
&:hover {
- color: $gl-gray-500;
+ color: $gray-500;
text-decoration: none;
}
}
@@ -1035,3 +1033,9 @@ $mr-widget-min-height: 69px;
.diff-file-row.is-active {
background-color: $gray-50;
}
+
+.merge-request-container {
+ .flash-container {
+ @include gl-mb-4;
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index c473cc44637..e9eb79b071c 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -178,7 +178,6 @@ $status-box-line-height: 26px;
.milestone-detail {
border-bottom: 1px solid $border-color;
- padding: 20px 0;
}
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 35a15214f68..8b3d3268a8c 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -97,7 +97,7 @@
.issuable-note-warning {
color: $orange-600;
- background-color: $orange-100;
+ background-color: $orange-50;
border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal;
border-bottom: 0;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e4e54501627..c144fb13322 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -186,8 +186,8 @@ $note-form-margin-left: 72px;
padding: $gl-padding;
.dummy-avatar {
- background-color: $gl-gray-100;
- border: 1px solid darken($gl-gray-100, 25%);
+ background-color: $gray-100;
+ border: 1px solid darken($gray-100, 25%);
}
.note-headline-light,
@@ -835,7 +835,7 @@ $note-form-margin-left: 72px;
&[disabled] {
background: $white;
border-color: $gray-200;
- color: $gl-gray-400;
+ color: $gray-300;
cursor: not-allowed;
}
}
diff --git a/app/assets/stylesheets/pages/packages.scss b/app/assets/stylesheets/pages/packages.scss
deleted file mode 100644
index 8f6eee524e5..00000000000
--- a/app/assets/stylesheets/pages/packages.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-.commit-row-description {
- border: 0;
- border-left: 3px solid $white-dark;
-}
-
-.package-list-table[aria-busy='true'] {
- td {
- padding-bottom: 0;
- padding-top: 0;
- }
-}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index fc3b786b365..8b104ce9017 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -193,7 +193,6 @@
.icon-container {
display: inline-block;
- width: 10px;
&.commit-icon {
width: 15px;
@@ -782,7 +781,7 @@
&.ci-status-icon-pending,
&.ci-status-icon-waiting-for-resource,
&.ci-status-icon-success-with-warnings {
- @include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700);
+ @include mini-pipeline-graph-color($white, $orange-50, $orange-100, $orange-500, $orange-600, $orange-700);
}
&.ci-status-icon-preparing,
@@ -1082,3 +1081,19 @@ button.mini-pipeline-graph-dropdown-toggle {
.progress-bar.bg-primary {
background-color: $blue-500 !important;
}
+
+.pipeline-stage-pill {
+ width: 10rem;
+}
+
+.pipeline-job-pill {
+ width: 8rem;
+}
+
+.stage-left-rounded {
+ border-radius: 2rem 0 0 2rem;
+}
+
+.stage-right-rounded {
+ border-radius: 0 2rem 2rem 0;
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 85836962b06..4dc1f2034f3 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -257,7 +257,8 @@
}
}
-table.u2f-registrations {
+table.u2f-registrations,
+.webauthn-registrations {
th:not(:last-child),
td:not(:last-child) {
border-right: solid 1px transparent;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index d4d6583312c..a2f8447c0b6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -353,7 +353,7 @@
.save-project-loader {
margin-top: 50px;
margin-bottom: 50px;
- color: $gl-gray-700;
+ color: $gray-700;
}
.transfer-project .select2-container {
@@ -429,7 +429,7 @@
> li + li::before {
padding: 0 3px;
- color: $gl-gray-400;
+ color: $gray-300;
}
a {
@@ -1268,18 +1268,10 @@ pre.light-well {
position: relative;
.clear-icon {
- @extend .fa-times;
display: none;
position: absolute;
- right: 7px;
- top: 7px;
- color: $location-icon-color;
-
- &::before {
- font-family: FontAwesome;
- font-weight: $gl-font-weight-normal;
- font-style: normal;
- }
+ right: 9px;
+ top: 9px;
}
&.has-value {
@@ -1444,7 +1436,7 @@ pre.light-well {
.project-filters {
.btn svg {
- color: $gl-gray-700;
+ color: $gray-700;
}
.button-filter-group {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 1fc6ad62237..4b8e1da4867 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -105,7 +105,7 @@ input[type='checkbox']:hover {
}
.dropdown-header {
- // Necessary because glDropdown doesn't support a second style of headers
+ // Necessary because deprecatedJQueryDropdown doesn't support a second style of headers
font-weight: $gl-font-weight-bold;
color: $gl-text-color;
font-size: $gl-font-size;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index b82c638a5b7..7b18e3774d8 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -124,8 +124,8 @@
.settings-message {
padding: 5px;
line-height: 1.3;
- color: $orange-700;
- background-color: $orange-100;
+ color: $gray-900;
+ background-color: $orange-50;
border: 1px solid $orange-200;
border-radius: $border-radius-base;
}
@@ -135,7 +135,7 @@
}
.warning-title {
- color: $orange-500;
+ color: $gray-900;
}
.danger-title {
@@ -301,7 +301,7 @@
}
.loading-metrics .metrics-load-spinner {
- color: $gl-gray-700;
+ color: $gray-700;
}
.metrics-list {
@@ -384,6 +384,13 @@
font-weight: $gl-font-weight-bold;
border: 0;
}
+
+ // When tables are "stacked", restore td padding
+ @media(max-width: map-get($grid-breakpoints, lg)) {
+ td {
+ padding-left: $gl-spacing-scale-5;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 4ad2dcbe92f..b37c5172ad2 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -43,7 +43,7 @@
&.ci-waiting-for-resource,
&.ci-failed-with-warnings,
&.ci-success-with-warnings {
- @include status-color($orange-100, $orange-500, $orange-700);
+ @include status-color($orange-50, $orange-500, $orange-700);
}
&.ci-info,
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index b6af395a802..73fe76f139f 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -173,7 +173,7 @@
.tree-truncated-warning {
color: $orange-600;
- background-color: $orange-100;
+ background-color: $orange-50;
}
.tree-time-ago {
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 7744fd814d0..288da4da5c3 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -6,7 +6,7 @@
.example {
padding: 15px;
- border: 1px dashed $gl-gray-100;
+ border: 1px dashed $gray-100;
margin-bottom: 15px;
&::before {
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index 8c4bfdf68cc..ccf11058b5b 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -151,3 +151,7 @@ ul.wiki-pages-list.content-list {
.empty-state-wiki .text-content {
max-width: 490px; // Widen to allow for the Confluence button
}
+
+.wiki-form .markdown-area {
+ max-height: none;
+}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index daeab80d373..dc127cd2554 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -11,15 +11,15 @@
height: $performance-bar-height;
background: $black;
line-height: $performance-bar-height;
- color: $gl-gray-400;
+ color: $gray-300;
select {
- color: $gl-gray-400;
+ color: $gray-300;
width: 200px;
}
input {
- color: $gl-gray-400;
+ color: $gray-300;
width: $input-short-width - 60px;
}
@@ -61,7 +61,7 @@
padding: 4px 6px;
font-family: Consolas, 'Liberation Mono', Courier, monospace;
line-height: 1;
- color: $gl-gray-100;
+ color: $gray-100;
border-radius: 3px;
box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from,
inset 0 1px 2px $perf-bar-bucket-box-shadow-to;
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
new file mode 100644
index 00000000000..d875f758ead
--- /dev/null
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -0,0 +1,1912 @@
+@charset "UTF-8";
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+html {
+ font-family: sans-serif;
+ line-height: 1.15;
+}
+ header, nav, section {
+ display: block;
+}
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #fafafa;
+ text-align: left;
+ background-color: #2e2e2e;
+}
+h1, h2, h3 {
+ margin-top: 0;
+ margin-bottom: 0.25rem;
+}
+p {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+ul {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+ul ul {
+ margin-bottom: 0;
+}
+
+strong {
+ font-weight: bolder;
+}
+sub {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline;
+}
+sub {
+ bottom: -.25em;
+}
+a {
+ color: #007bff;
+ text-decoration: none;
+ background-color: transparent;
+}
+a:not([href]) {
+ color: inherit;
+ text-decoration: none;
+}
+pre,
+code {
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ font-size: 1em;
+}
+pre {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ overflow: auto;
+}
+img {
+ vertical-align: middle;
+ border-style: none;
+}
+svg {
+ overflow: hidden;
+ vertical-align: middle;
+}
+table {
+ border-collapse: collapse;
+}
+th {
+ text-align: inherit;
+}
+button {
+ border-radius: 0;
+}
+input,
+button,
+textarea {
+ margin: 0;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+button,
+input {
+ overflow: visible;
+}
+button {
+ text-transform: none;
+}
+button:not(:disabled),
+[type="button"]:not(:disabled),
+[type="reset"]:not(:disabled) {
+ cursor: pointer;
+}
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner {
+ padding: 0;
+ border-style: none;
+}
+textarea {
+ overflow: auto;
+ resize: vertical;
+}
+[type="search"] {
+ outline-offset: -2px;
+}
+summary {
+ display: list-item;
+ cursor: pointer;
+}
+template {
+ display: none;
+}
+[hidden] {
+ display: none !important;
+}
+h1, h2, h3,
+.h1, .h2, .h3 {
+ margin-bottom: 0.25rem;
+ font-weight: 600;
+ line-height: 1.2;
+ color: #fafafa;
+}
+h1, .h1 {
+ font-size: 2.1875rem;
+}
+h2, .h2 {
+ font-size: 1.75rem;
+}
+h3, .h3 {
+ font-size: 1.53125rem;
+}
+.list-unstyled {
+ padding-left: 0;
+ list-style: none;
+}
+code {
+ font-size: 90%;
+ color: #fff;
+ word-wrap: break-word;
+}
+a > code {
+ color: inherit;
+}
+pre {
+ display: block;
+ font-size: 90%;
+ color: #fafafa;
+}
+pre code {
+ font-size: inherit;
+ color: inherit;
+ word-break: normal;
+}
+.container {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+.container-fluid {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: -15px;
+ margin-left: -15px;
+}
+.table {
+ width: 100%;
+ margin-bottom: 0.5rem;
+ color: #fafafa;
+}
+.table th,
+.table td {
+ padding: 0.75rem;
+ vertical-align: top;
+ border-top: 1px solid #4f4f4f;
+}
+ .search form {
+ display: block;
+ width: 100%;
+ height: 34px;
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #fafafa;
+ background-color: #4f4f4f;
+ background-clip: padding-box;
+ border: 1px solid #4f4f4f;
+ border-radius: 0.25rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+}
+ .search form:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #fafafa;
+}
+ .search form::placeholder {
+ color: #ccc;
+ opacity: 1;
+}
+ .search form:disabled {
+ background-color: #2e2e2e;
+ opacity: 1;
+}
+.form-inline {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+}
+
+@media (min-width: 576px) {
+ .form-inline .search form, .search .form-inline form {
+ display: inline-block;
+ width: auto;
+ vertical-align: middle;
+ }
+}
+.btn {
+ display: inline-block;
+ font-weight: 400;
+ color: #fafafa;
+ text-align: center;
+ vertical-align: middle;
+ cursor: pointer;
+ user-select: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ line-height: 20px;
+ border-radius: 0.25rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+}
+.btn.disabled, .btn:disabled {
+ opacity: 0.65;
+}
+a.btn.disabled {
+ pointer-events: none;
+}
+.collapse:not(.show) {
+ display: none;
+}
+
+.dropdown {
+ position: relative;
+}
+ .dropdown-menu-toggle {
+ white-space: nowrap;
+}
+ .dropdown-menu-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0;
+ border-left: 0.3em solid transparent;
+}
+ .dropdown-menu-toggle:empty::after {
+ margin-left: 0;
+}
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 10rem;
+ padding: 0.5rem 0;
+ margin: 0.125rem 0 0;
+ font-size: 1rem;
+ color: #fafafa;
+ text-align: left;
+ list-style: none;
+ background-color: #333;
+ background-clip: padding-box;
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 0.25rem;
+}
+.dropdown-menu-right {
+ right: 0;
+ left: auto;
+}
+ .divider {
+ height: 0;
+ margin: 4px 0;
+ overflow: hidden;
+ border-top: 1px solid #4f4f4f;
+}
+.dropdown-menu.show {
+ display: block;
+}
+.nav {
+ display: flex;
+ flex-wrap: wrap;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+.navbar {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.25rem 0.5rem;
+}
+.navbar .container,
+.navbar .container-fluid {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+}
+.navbar-nav {
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+.navbar-nav .dropdown-menu {
+ position: static;
+ float: none;
+}
+.navbar-collapse {
+ flex-basis: 100%;
+ flex-grow: 1;
+ align-items: center;
+}
+.navbar-toggler {
+ padding: 0.25rem 0.75rem;
+ font-size: 1.25rem;
+ line-height: 1;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 576px) {
+ .navbar-expand-sm {
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ }
+ .navbar-expand-sm .navbar-nav {
+ flex-direction: row;
+ }
+ .navbar-expand-sm .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid {
+ flex-wrap: nowrap;
+ }
+ .navbar-expand-sm .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+ .navbar-expand-sm .navbar-toggler {
+ display: none;
+ }
+}
+.card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #333;
+ background-clip: border-box;
+ border: 1px solid #4f4f4f;
+ border-radius: 0.25rem;
+}
+.badge {
+ display: inline-block;
+ padding: 0.25em 0.4em;
+ font-size: 75%;
+ font-weight: 600;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+}
+.badge:empty {
+ display: none;
+}
+.btn .badge {
+ position: relative;
+ top: -1px;
+}
+.badge-pill {
+ padding-right: 0.6em;
+ padding-left: 0.6em;
+ border-radius: 10rem;
+}
+.media {
+ display: flex;
+ align-items: flex-start;
+}
+.close {
+ float: right;
+ font-size: 1.5rem;
+ font-weight: 600;
+ line-height: 1;
+ color: #fff;
+ text-shadow: 0 1px 0 #333;
+ opacity: .5;
+}
+button.close {
+ padding: 0;
+ background-color: transparent;
+ border: 0;
+ appearance: none;
+}
+a.close.disabled {
+ pointer-events: none;
+}
+.modal-dialog {
+ position: relative;
+ width: auto;
+ margin: 0.5rem;
+ pointer-events: none;
+}
+
+@media (min-width: 576px) {
+ .modal-dialog {
+ max-width: 500px;
+ margin: 1.75rem auto;
+ }
+}
+.bg-transparent {
+ background-color: transparent !important;
+}
+.border {
+ border: 1px solid #4f4f4f !important;
+}
+.border-top {
+ border-top: 1px solid #4f4f4f !important;
+}
+.border-right {
+ border-right: 1px solid #4f4f4f !important;
+}
+.border-bottom {
+ border-bottom: 1px solid #4f4f4f !important;
+}
+.border-left {
+ border-left: 1px solid #4f4f4f !important;
+}
+.rounded {
+ border-radius: 0.25rem !important;
+}
+.clearfix::after {
+ display: block;
+ clear: both;
+ content: "";
+}
+.d-none {
+ display: none !important;
+}
+.d-inline-block {
+ display: inline-block !important;
+}
+.d-block {
+ display: block !important;
+}
+
+@media (min-width: 576px) {
+ .d-sm-none {
+ display: none !important;
+ }
+}
+
+@media (min-width: 768px) {
+ .d-md-block {
+ display: block !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .d-lg-none {
+ display: none !important;
+ }
+ .d-lg-block {
+ display: block !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .d-xl-block {
+ display: block !important;
+ }
+}
+.flex-wrap {
+ flex-wrap: wrap !important;
+}
+.float-right {
+ float: right !important;
+}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+.m-auto {
+ margin: auto !important;
+}
+.text-nowrap {
+ white-space: nowrap !important;
+}
+.visible {
+ visibility: visible !important;
+}
+ .search form.focus {
+ color: #fafafa;
+ background-color: #4f4f4f;
+ border-color: #80bdff;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
+}
+.gl-badge {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.75rem;
+ font-weight: 400;
+ line-height: 1rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ outline: none;
+}
+body, .search form,
+.search form {
+ font-size: 0.875rem;
+}
+button,
+html [type='button'],
+[type='reset'],
+[role='button'] {
+ cursor: pointer;
+}
+h1,
+.h1,
+h2,
+.h2,
+h3,
+.h3 {
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+input[type='file'] {
+ line-height: 1;
+}
+
+strong {
+ font-weight: bold;
+}
+a {
+ color: #418cd8;
+}
+code {
+ padding: 2px 4px;
+ color: #fff;
+ background-color: #2e2e2e;
+ border-radius: 4px;
+}
+.code > code {
+ background-color: inherit;
+ padding: unset;
+}
+table {
+ border-spacing: 0;
+}
+.hidden {
+ display: none !important;
+ visibility: hidden !important;
+}
+.hide {
+ display: none;
+}
+ .dropdown-menu-toggle::after {
+ display: none;
+}
+.badge:not(.gl-badge) {
+ padding: 4px 5px;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ display: inline-block;
+}
+pre code {
+ white-space: pre-wrap;
+}
+.toggle-sidebar-button .collapse-text,
+.toggle-sidebar-button .icon-chevron-double-lg-left,
+.toggle-sidebar-button .icon-chevron-double-lg-right {
+ color: #bababa;
+}
+svg {
+ vertical-align: baseline;
+}
+html {
+ overflow-y: scroll;
+}
+body {
+ text-decoration-skip: ink;
+}
+.content-wrapper {
+ margin-top: 40px;
+ padding-bottom: 100px;
+}
+.container {
+ padding-top: 0;
+ z-index: 5;
+}
+.container .content {
+ margin: 0;
+}
+
+@media (max-width: 575.98px) {
+ .container .content {
+ margin-top: 20px;
+ }
+}
+
+@media (max-width: 575.98px) {
+ .container .container .title {
+ padding-left: 15px !important;
+ }
+}
+.btn {
+ border-radius: 4px;
+ font-size: 0.875rem;
+ font-weight: 400;
+ padding: 6px 10px;
+ background-color: #333;
+ border-color: #4f4f4f;
+ color: #fafafa;
+ color: #fafafa;
+ white-space: nowrap;
+}
+.btn:active, .btn.active {
+ box-shadow: rgba(0, 0, 0, 0.16);
+ background-color: #444;
+ border-color: #fafafa;
+ color: #fafafa;
+}
+.btn svg {
+ height: 15px;
+ width: 15px;
+}
+.btn svg:not(:last-child),
+.btn .fa:not(:last-child) {
+ margin-right: 5px;
+}
+.badge.badge-pill:not(.gl-badge) {
+ font-weight: 400;
+ background-color: rgba(0, 0, 0, 0.07);
+ color: #dfdfdf;
+ vertical-align: baseline;
+}
+.hint {
+ font-style: italic;
+ color: #707070;
+}
+.bold {
+ font-weight: 600;
+}
+pre.wrap {
+ word-break: break-word;
+ white-space: pre-wrap;
+}
+table a code {
+ position: relative;
+ top: -2px;
+ margin-right: 3px;
+}
+.loading {
+ margin: 20px auto;
+ height: 40px;
+ color: #dfdfdf;
+ font-size: 32px;
+ text-align: center;
+}
+.highlight {
+ text-shadow: none;
+}
+.chart {
+ overflow: hidden;
+ height: 220px;
+}
+.break-word {
+ word-wrap: break-word;
+}
+.center {
+ text-align: center;
+}
+.block {
+ display: block;
+}
+.flex {
+ display: flex;
+}
+.flex-grow {
+ flex-grow: 1;
+}
+.dropdown {
+ position: relative;
+}
+.show.dropdown .dropdown-menu {
+ transform: translateY(0);
+ display: block;
+ min-height: 40px;
+ max-height: 312px;
+ overflow-y: auto;
+}
+
+@media (max-width: 575.98px) {
+ .show.dropdown .dropdown-menu {
+ width: 100%;
+ }
+}
+ .show.dropdown .dropdown-menu-toggle,
+.show.dropdown .dropdown-menu-toggle {
+ border-color: #c4c4c4;
+}
+.show.dropdown [data-toggle='dropdown'] {
+ outline: 0;
+}
+.search-input-container .dropdown-menu {
+ margin-top: 11px;
+}
+ .dropdown-menu-toggle {
+ padding: 6px 8px 6px 10px;
+ background-color: #333;
+ color: #fafafa;
+ font-size: 14px;
+ text-align: left;
+ border: 1px solid #4f4f4f;
+ border-radius: 0.25rem;
+ white-space: nowrap;
+}
+ .no-outline.dropdown-menu-toggle {
+ outline: 0;
+}
+ .dropdown-menu-toggle .fa {
+ color: #c4c4c4;
+}
+.dropdown-menu-toggle {
+ padding-right: 25px;
+ position: relative;
+ width: 160px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+.dropdown-menu-toggle .fa {
+ position: absolute;
+}
+.dropdown-menu {
+ display: none;
+ position: absolute;
+ width: auto;
+ top: 100%;
+ z-index: 300;
+ min-width: 240px;
+ max-width: 500px;
+ margin-top: 4px;
+ margin-bottom: 24px;
+ font-size: 14px;
+ font-weight: 400;
+ padding: 8px 0;
+ background-color: #333;
+ border: 1px solid #4f4f4f;
+ border-radius: 0.25rem;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+.dropdown-menu ul {
+ margin: 0;
+ padding: 0;
+}
+.dropdown-menu li {
+ display: block;
+ text-align: left;
+ list-style: none;
+ padding: 0 1px;
+}
+.dropdown-menu li > a,
+.dropdown-menu li button {
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+ display: block;
+ font-weight: 400;
+ position: relative;
+ padding: 8px 12px;
+ color: #fafafa;
+ line-height: 16px;
+ white-space: normal;
+ overflow: hidden;
+ text-align: left;
+ width: 100%;
+}
+.dropdown-menu .divider {
+ height: 1px;
+ margin: 0.25rem 0;
+ padding: 0;
+ background-color: #4f4f4f;
+}
+.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
+ margin-right: 40px;
+}
+.dropdown-select {
+ width: 300px;
+}
+
+@media (max-width: 767.98px) {
+ .dropdown-select {
+ width: 100%;
+ }
+}
+.dropdown-content {
+ max-height: 252px;
+ overflow-y: auto;
+}
+.dropdown-loading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: none;
+ z-index: 9;
+ background-color: rgba(51, 51, 51, 0.6);
+ font-size: 28px;
+}
+.dropdown-loading .fa {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -14px;
+ margin-left: -14px;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab li.dropdown {
+ position: static;
+ }
+ header.navbar-gitlab .dropdown .dropdown-menu {
+ width: 100%;
+ min-width: 100%;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
+}
+textarea {
+ resize: vertical;
+}
+input {
+ border-radius: 0.25rem;
+ color: #fafafa;
+ background-color: #4f4f4f;
+}
+ .search form {
+ border-radius: 4px;
+ padding: 6px 10px;
+}
+ .search form::placeholder {
+ color: #a7a7a7;
+}
+body.ui-indigo .navbar-gitlab {
+ background-color: #292961;
+}
+body.ui-indigo .navbar-gitlab .navbar-collapse {
+ color: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler {
+ border-left: 1px solid #6868b9;
+}
+body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler svg {
+ fill: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.active > a,
+body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.active > button, body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.dropdown.show > a,
+body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.dropdown.show > button,
+body.ui-indigo .navbar-gitlab .navbar-nav > li.active > a,
+body.ui-indigo .navbar-gitlab .navbar-nav > li.active > button,
+body.ui-indigo .navbar-gitlab .navbar-nav > li.dropdown.show > a,
+body.ui-indigo .navbar-gitlab .navbar-nav > li.dropdown.show > button {
+ color: #292961;
+ background-color: #333;
+}
+body.ui-indigo .navbar-gitlab .navbar-sub-nav {
+ color: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .nav > li {
+ color: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .nav > li > a.header-user-dropdown-toggle .header-user-avatar {
+ border-color: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .nav > li.active > a,
+body.ui-indigo .navbar-gitlab .nav > li.dropdown.show > a {
+ color: #292961;
+ background-color: #333;
+}
+body.ui-indigo .search form {
+ background-color: rgba(209, 209, 240, 0.2);
+}
+body.ui-indigo .search .search-input::placeholder {
+ color: rgba(209, 209, 240, 0.8);
+}
+body.ui-indigo .search .search-input-wrap .search-icon,
+body.ui-indigo .search .search-input-wrap .clear-icon {
+ fill: rgba(209, 209, 240, 0.8);
+}
+body.ui-indigo .nav-sidebar li.active {
+ box-shadow: inset 4px 0 0 #4b4ba3;
+}
+body.ui-indigo .nav-sidebar li.active > a {
+ color: #393982;
+}
+body.ui-indigo .nav-sidebar li.active .nav-icon-container svg {
+ fill: #393982;
+}
+body.ui-indigo .sidebar-top-level-items > li.active .badge.badge-pill {
+ color: #393982;
+}
+body.gl-dark .logo-text svg {
+ fill: #fafafa;
+}
+body.gl-dark .navbar-gitlab {
+ background-color: #2e2e2e;
+}
+body.gl-dark .navbar-gitlab .navbar-sub-nav li.active > a,
+body.gl-dark .navbar-gitlab .navbar-sub-nav li.active > button,
+body.gl-dark .navbar-gitlab .navbar-sub-nav li.dropdown.show > a,
+body.gl-dark .navbar-gitlab .navbar-sub-nav li.dropdown.show > button,
+body.gl-dark .navbar-gitlab .navbar-nav li.active > a,
+body.gl-dark .navbar-gitlab .navbar-nav li.active > button,
+body.gl-dark .navbar-gitlab .navbar-nav li.dropdown.show > a,
+body.gl-dark .navbar-gitlab .navbar-nav li.dropdown.show > button {
+ color: #fafafa;
+ background-color: #707070;
+}
+body.gl-dark .navbar-gitlab .search form {
+ background-color: #4f4f4f;
+ box-shadow: inset 0 0 0 1px #4f4f4f;
+}
+.navbar-gitlab {
+ padding: 0 16px;
+ z-index: 1000;
+ margin-bottom: 0;
+ min-height: 40px;
+ border: 0;
+ border-bottom: 1px solid #4f4f4f;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-radius: 0;
+}
+.navbar-gitlab .logo-text {
+ line-height: initial;
+}
+.navbar-gitlab .logo-text svg {
+ width: 55px;
+ height: 14px;
+ margin: 0;
+ fill: #333;
+}
+.navbar-gitlab .close-icon {
+ display: none;
+}
+.navbar-gitlab .header-content {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+ min-height: 40px;
+ padding-left: 0;
+}
+.navbar-gitlab .header-content .title-container {
+ display: flex;
+ align-items: stretch;
+ flex: 1 1 auto;
+ padding-top: 0;
+ overflow: visible;
+}
+.navbar-gitlab .header-content .title {
+ padding-right: 0;
+ color: currentColor;
+ display: flex;
+ position: relative;
+ margin: 0;
+ font-size: 18px;
+ vertical-align: top;
+ white-space: nowrap;
+}
+.navbar-gitlab .header-content .title img {
+ height: 28px;
+}
+.navbar-gitlab .header-content .title img + .logo-text {
+ margin-left: 8px;
+}
+.navbar-gitlab .header-content .title.wrap {
+ white-space: normal;
+}
+.navbar-gitlab .header-content .title a {
+ display: flex;
+ align-items: center;
+ padding: 2px 8px;
+ margin: 5px 2px 5px -8px;
+ border-radius: 4px;
+}
+.navbar-gitlab .header-content .dropdown.open > a {
+ border-bottom-color: #333;
+}
+.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
+ margin: 0 2px;
+}
+.navbar-gitlab .navbar-collapse {
+ flex: 0 0 auto;
+ border-top: 0;
+ padding: 0;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .navbar-collapse {
+ flex: 1 1 auto;
+ }
+}
+.navbar-gitlab .navbar-collapse .nav {
+ flex-wrap: nowrap;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a {
+ margin-left: 0;
+ }
+}
+.navbar-gitlab .container-fluid {
+ padding: 0;
+}
+.navbar-gitlab .container-fluid .user-counter svg {
+ margin-right: 3px;
+}
+.navbar-gitlab .container-fluid .navbar-toggler {
+ position: relative;
+ right: -10px;
+ border-radius: 0;
+ min-width: 45px;
+ padding: 0;
+ margin: 8px -7px 8px 0;
+ font-size: 14px;
+ text-align: center;
+ color: currentColor;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid .navbar-nav {
+ display: flex;
+ padding-right: 10px;
+ flex-direction: row;
+ }
+}
+.navbar-gitlab .container-fluid .navbar-nav li .badge.badge-pill {
+ box-shadow: none;
+ font-weight: 600;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid .nav > li.header-user {
+ padding-left: 10px;
+ }
+}
+.navbar-gitlab .container-fluid .nav > li > a {
+ will-change: color;
+ margin: 4px 0;
+ padding: 6px 8px;
+ height: 32px;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid .nav > li > a {
+ padding: 0;
+ }
+}
+.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle {
+ margin-left: 2px;
+}
+.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle .header-user-avatar {
+ margin-right: 0;
+}
+.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle {
+ margin-right: 0;
+}
+.navbar-sub-nav > li > a,
+.navbar-sub-nav > li > button,
+.navbar-nav > li > a,
+.navbar-nav > li > button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 8px;
+ margin: 4px 2px;
+ font-size: 12px;
+ color: currentColor;
+ border-radius: 4px;
+ height: 32px;
+ font-weight: 600;
+}
+.navbar-sub-nav > li > button,
+.navbar-nav > li > button {
+ background: transparent;
+ border: 0;
+}
+.navbar-sub-nav .dropdown-menu,
+.navbar-nav .dropdown-menu {
+ position: absolute;
+}
+.navbar-sub-nav {
+ display: flex;
+ margin: 0 0 0 6px;
+}
+.caret-down,
+.btn .caret-down {
+ top: 0;
+ height: 11px;
+ width: 11px;
+ margin-left: 4px;
+ fill: currentColor;
+}
+.header-user .dropdown-menu,
+.header-new .dropdown-menu {
+ margin-top: 4px;
+}
+.btn-sign-in {
+ background-color: #ebebfa;
+ color: #292961;
+ font-weight: 600;
+ line-height: 18px;
+ margin: 4px 0 4px 2px;
+}
+.title-container .badge.badge-pill,
+.navbar-nav .badge.badge-pill {
+ position: inherit;
+ font-weight: 400;
+ margin-left: -6px;
+ font-size: 11px;
+ color: #333;
+ padding: 0 5px;
+ line-height: 12px;
+ border-radius: 7px;
+ box-shadow: 0 1px 0 rgba(76, 78, 84, 0.2);
+}
+.title-container .badge.badge-pill.green-badge,
+.navbar-nav .badge.badge-pill.green-badge {
+ background-color: #1aaa55;
+}
+.title-container .badge.badge-pill.merge-requests-count,
+.navbar-nav .badge.badge-pill.merge-requests-count {
+ background-color: #fca429;
+}
+.title-container .badge.badge-pill.todos-count,
+.navbar-nav .badge.badge-pill.todos-count {
+ background-color: #1f78d1;
+}
+.title-container .canary-badge .badge,
+.navbar-nav .canary-badge .badge {
+ font-size: 12px;
+ line-height: 16px;
+ padding: 0 0.5rem;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid {
+ font-size: 18px;
+ }
+ .navbar-gitlab .container-fluid .navbar-nav {
+ table-layout: fixed;
+ width: 100%;
+ margin: 0;
+ text-align: right;
+ }
+ .navbar-gitlab .container-fluid .navbar-collapse {
+ margin-left: -8px;
+ margin-right: -10px;
+ }
+ .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) {
+ flex: 1;
+ }
+ .header-user-dropdown-toggle {
+ text-align: center;
+ }
+ .header-user-avatar {
+ float: none;
+ }
+}
+.header-user.show .dropdown-menu {
+ margin-top: 4px;
+ color: #fafafa;
+ left: auto;
+ max-height: 445px;
+}
+.header-user.show .dropdown-menu svg {
+ vertical-align: text-top;
+}
+.header-user-avatar {
+ float: left;
+ margin-right: 5px;
+ border-radius: 50%;
+ border: 1px solid #333;
+}
+.media {
+ display: flex;
+ align-items: flex-start;
+}
+.card {
+ margin-bottom: 16px;
+}
+.content-wrapper {
+ width: 100%;
+}
+.content-wrapper .container-fluid {
+ padding: 0 16px;
+}
+
+@media (min-width: 768px) {
+ .page-with-contextual-sidebar {
+ padding-left: 50px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .page-with-contextual-sidebar {
+ padding-left: 220px;
+ }
+}
+.context-header {
+ position: relative;
+ margin-right: 2px;
+ width: 220px;
+}
+.context-header > a,
+.context-header > button {
+ font-weight: 600;
+ display: flex;
+ width: 100%;
+ align-items: center;
+ padding: 10px 16px 10px 10px;
+ color: #fafafa;
+ background-color: transparent;
+ border: 0;
+ text-align: left;
+}
+.context-header .avatar-container {
+ flex: 0 0 40px;
+ background-color: #333;
+}
+.context-header .sidebar-context-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.context-header .sidebar-context-title.text-secondary {
+ font-weight: normal;
+ font-size: 0.8em;
+}
+.nav-sidebar {
+ position: fixed;
+ z-index: 600;
+ width: 220px;
+ top: 40px;
+ bottom: 0;
+ left: 0;
+ background-color: #2e2e2e;
+ box-shadow: inset -1px 0 0 #4f4f4f;
+ transform: translate3d(0, 0, 0);
+}
+
+@media (min-width: 576px) and (max-width: 576px) {
+ .nav-sidebar:not(.sidebar-collapsed-desktop) {
+ box-shadow: inset -1px 0 0 #4f4f4f, 2px 1px 3px rgba(0, 0, 0, 0.1);
+ }
+}
+.nav-sidebar.sidebar-collapsed-desktop {
+ width: 50px;
+}
+.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
+ overflow-x: hidden;
+}
+.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge),
+.nav-sidebar.sidebar-collapsed-desktop .sidebar-context-title,
+.nav-sidebar.sidebar-collapsed-desktop .nav-item-name {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a {
+ min-height: 45px;
+}
+.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item {
+ display: block;
+}
+.nav-sidebar.sidebar-collapsed-desktop .avatar-container {
+ margin: 0 auto;
+}
+.nav-sidebar.sidebar-expanded-mobile {
+ left: 0;
+}
+.nav-sidebar a {
+ text-decoration: none;
+}
+.nav-sidebar ul {
+ padding-left: 0;
+ list-style: none;
+}
+.nav-sidebar li {
+ white-space: nowrap;
+}
+.nav-sidebar li a {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ color: #bababa;
+}
+.nav-sidebar li .nav-item-name {
+ flex: 1;
+}
+.nav-sidebar li.active > a {
+ font-weight: 600;
+}
+
+@media (max-width: 767.98px) {
+ .nav-sidebar {
+ left: -220px;
+ }
+}
+.nav-sidebar .nav-icon-container {
+ display: flex;
+ margin-right: 8px;
+}
+.nav-sidebar .fly-out-top-item {
+ display: none;
+}
+.nav-sidebar svg {
+ height: 16px;
+ width: 16px;
+}
+
+@media (min-width: 768px) and (max-width: 1199px) {
+ .nav-sidebar:not(.sidebar-expanded-mobile) {
+ width: 50px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
+ overflow-x: hidden;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .badge.badge-pill:not(.fly-out-badge),
+ .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title,
+ .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a {
+ min-height: 45px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item {
+ display: block;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container {
+ margin: 0 auto;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
+ height: 60px;
+ width: 50px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
+ padding: 10px 4px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) {
+ display: none;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container {
+ margin-right: 0;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
+ padding: 16px;
+ width: 49px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .collapse-text,
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-left {
+ display: none;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-right {
+ display: block;
+ margin: 0;
+ }
+}
+.nav-sidebar-inner-scroll {
+ height: 100%;
+ width: 100%;
+ overflow: auto;
+}
+.sidebar-sub-level-items {
+ display: none;
+ padding-bottom: 8px;
+}
+.sidebar-sub-level-items > li a {
+ padding: 8px 16px 8px 40px;
+}
+.sidebar-top-level-items {
+ margin-bottom: 60px;
+}
+
+@media (min-width: 576px) {
+ .sidebar-top-level-items > li > a {
+ margin-right: 1px;
+ }
+}
+.sidebar-top-level-items > li .badge.badge-pill {
+ background-color: rgba(255, 255, 255, 0.08);
+ color: #bababa;
+}
+.sidebar-top-level-items > li.active {
+ background: rgba(255, 255, 255, 0.04);
+}
+.sidebar-top-level-items > li.active > a {
+ margin-left: 4px;
+ padding-left: 12px;
+}
+.sidebar-top-level-items > li.active .badge.badge-pill {
+ font-weight: 600;
+}
+.sidebar-top-level-items > li.active .sidebar-sub-level-items:not(.is-fly-out-only) {
+ display: block;
+}
+.toggle-sidebar-button,
+.close-nav-button {
+ width: 219px;
+ position: fixed;
+ height: 48px;
+ bottom: 0;
+ padding: 0 16px;
+ background-color: #2e2e2e;
+ border: 0;
+ border-top: 1px solid #4f4f4f;
+ color: #bababa;
+ display: flex;
+ align-items: center;
+}
+.toggle-sidebar-button svg,
+.close-nav-button svg {
+ margin-right: 8px;
+}
+.toggle-sidebar-button .icon-chevron-double-lg-right,
+.close-nav-button .icon-chevron-double-lg-right {
+ display: none;
+}
+.collapse-text {
+ white-space: nowrap;
+ overflow: hidden;
+}
+.sidebar-collapsed-desktop .context-header {
+ height: 60px;
+ width: 50px;
+}
+.sidebar-collapsed-desktop .context-header a {
+ padding: 10px 4px;
+}
+.sidebar-collapsed-desktop .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) {
+ display: none;
+}
+.sidebar-collapsed-desktop .nav-icon-container {
+ margin-right: 0;
+}
+.sidebar-collapsed-desktop .toggle-sidebar-button {
+ padding: 16px;
+ width: 49px;
+}
+.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text,
+.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left {
+ display: none;
+}
+.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-right {
+ display: block;
+ margin: 0;
+}
+.fly-out-top-item > a {
+ display: flex;
+}
+.fly-out-top-item .fly-out-badge {
+ margin-left: 8px;
+}
+.fly-out-top-item-name {
+ flex: 1;
+}
+.close-nav-button {
+ display: none;
+}
+
+@media (max-width: 767.98px) {
+ .close-nav-button {
+ display: flex;
+ }
+ .toggle-sidebar-button {
+ display: none;
+ }
+}
+table.table {
+ margin-bottom: 16px;
+}
+table.table .dropdown-menu a {
+ text-decoration: none;
+}
+table.table .success,
+table.table .info {
+ color: #333;
+}
+table.table .success a:not(.btn),
+table.table .info a:not(.btn) {
+ text-decoration: underline;
+ color: #333;
+}
+pre {
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ display: block;
+ padding: 8px 12px;
+ margin: 0 0 8px;
+ font-size: 13px;
+ word-break: break-all;
+ word-wrap: break-word;
+ color: #fafafa;
+ background-color: #2e2e2e;
+ border: 1px solid #4f4f4f;
+ border-radius: 2px;
+}
+.monospace {
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+}
+input::-moz-placeholder,
+textarea::-moz-placeholder {
+ color: #a7a7a7;
+ opacity: 1;
+}
+input::-ms-input-placeholder,
+textarea::-ms-input-placeholder {
+ color: #a7a7a7;
+}
+input:-ms-input-placeholder,
+textarea:-ms-input-placeholder {
+ color: #a7a7a7;
+}
+svg {
+ fill: currentColor;
+}
+
+svg.s12 {
+ width: 12px;
+ height: 12px;
+}
+
+svg.s16 {
+ width: 16px;
+ height: 16px;
+}
+
+svg.s18 {
+ width: 18px;
+ height: 18px;
+}
+
+svg.s12 {
+ vertical-align: -1px;
+}
+
+svg.s16 {
+ vertical-align: -3px;
+}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
+table.code {
+ width: 100%;
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ border: 0;
+ border-collapse: separate;
+ margin: 0;
+ padding: 0;
+ table-layout: fixed;
+ border-radius: 0 0 4px 4px;
+}
+.frame .badge.badge-pill {
+ position: absolute;
+ background-color: #1b69b6;
+ color: #333;
+ border: #333 1px solid;
+ min-height: 16px;
+ padding: 5px 8px;
+ border-radius: 12px;
+}
+.frame .badge.badge-pill {
+ transform: translate(-50%, -50%);
+}
+.color-label {
+ padding: 0 0.5rem;
+ line-height: 16px;
+ border-radius: 100px;
+ color: #333;
+}
+.label-link {
+ display: inline-flex;
+ vertical-align: text-bottom;
+}
+.milestones {
+ padding: 8px;
+ margin-top: 8px;
+ border-radius: 4px;
+ background-color: #4f4f4f;
+}
+.search {
+ margin: 0 8px;
+}
+.search form {
+ margin: 0;
+ padding: 4px;
+ width: 200px;
+ line-height: 24px;
+ height: 32px;
+ border: 0;
+ border-radius: 4px;
+}
+
+@media (min-width: 1200px) {
+ .search form {
+ width: 320px;
+ }
+}
+.search .search-input {
+ border: 0;
+ font-size: 14px;
+ padding: 0 20px 0 0;
+ margin-left: 5px;
+ line-height: 25px;
+ width: 98%;
+ color: #333;
+ background: none;
+}
+.search .search-input-container {
+ display: flex;
+ position: relative;
+}
+.search .search-input-wrap {
+ width: 100%;
+}
+.search .search-input-wrap .search-icon,
+.search .search-input-wrap .clear-icon {
+ position: absolute;
+ right: 5px;
+ top: 4px;
+}
+.search .search-input-wrap .search-icon {
+ -moz-user-select: none;
+ user-select: none;
+}
+.search .search-input-wrap .clear-icon {
+ display: none;
+}
+.search .search-input-wrap .dropdown {
+ position: static;
+}
+.search .search-input-wrap .dropdown-menu {
+ left: -5px;
+ max-height: 400px;
+ overflow: auto;
+}
+
+@media (min-width: 1200px) {
+ .search .search-input-wrap .dropdown-menu {
+ width: 320px;
+ }
+}
+.search .search-input-wrap .dropdown-content {
+ max-height: 382px;
+}
+.settings {
+ border-top: 1px solid #4f4f4f;
+}
+.settings:first-of-type {
+ margin-top: 10px;
+ border: 0;
+}
+.settings + div .settings:first-of-type {
+ margin-top: 0;
+ border-top: 1px solid #4f4f4f;
+}
+.avatar, .avatar-container {
+ float: left;
+ margin-right: 16px;
+ border-radius: 50%;
+ border: 1px solid #333;
+}
+.s16.avatar, .s16.avatar-container {
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+}
+.s18.avatar, .s18.avatar-container {
+ width: 18px;
+ height: 18px;
+ margin-right: 8px;
+}
+.s40.avatar, .s40.avatar-container {
+ width: 40px;
+ height: 40px;
+ margin-right: 8px;
+}
+.avatar {
+ transition-property: none;
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ background: #222;
+ overflow: hidden;
+ border-color: rgba(255, 255, 255, 0.1);
+}
+.avatar.center {
+ font-size: 14px;
+ line-height: 1.8em;
+ text-align: center;
+}
+.avatar.avatar-tile {
+ border-radius: 0;
+ border: 0;
+}
+.avatar-container {
+ overflow: hidden;
+ display: flex;
+}
+.avatar-container a {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ text-decoration: none;
+}
+.avatar-container .avatar {
+ border-radius: 0;
+ border: 0;
+ height: auto;
+ width: 100%;
+ margin: 0;
+ align-self: center;
+}
+.avatar-container.s40 {
+ min-width: 40px;
+ min-height: 40px;
+}
+.rect-avatar {
+ border-radius: 2px;
+}
+.rect-avatar.s16 {
+ border-radius: 2px;
+}
+.rect-avatar.s18 {
+ border-radius: 2px;
+}
+.rect-avatar.s40 {
+ border-radius: 4px;
+}
+.tab-width-8 {
+ -moz-tab-size: 8;
+ tab-size: 8;
+}
+.gl-sr-only {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+.gl-ml-3 {
+ margin-left: 0.5rem;
+}
+.content-wrapper > .alert-wrapper,
+#content-body, .modal-dialog {
+ display: block;
+}
+@import 'cloaking';
+@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 2a7a9255ded..a98e91b32eb 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -1,5 +1,1833 @@
-@charset "UTF-8";*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;overflow-y:scroll}header,nav{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans",Ubuntu,Cantarell,"Helvetica Neue",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-weight:400;line-height:1.5;color:#303030;text-align:left;background-color:#fff}hr{box-sizing:content-box;height:0;margin-top:.5rem;margin-bottom:.5rem;border:0;border-top:1px solid rgba(0,0,0,.1);overflow:hidden;margin:24px 0;border-top:1px solid #eee}p,ul{margin-top:0;margin-bottom:1rem}ul ul{margin-bottom:0}strong{font-weight:700}a{text-decoration:none;background-color:transparent;color:#1068bf}a:not([href]){color:inherit;text-decoration:none}code{font-family:"Menlo","DejaVu Sans Mono","Liberation Mono","Consolas","Ubuntu Mono","Courier New","andale mono","lucida console",monospace;font-size:90%;word-wrap:break-word;padding:2px 4px;color:#1f1f1f;background-color:#f0f0f0;border-radius:4px}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:baseline;fill:currentColor}button{border-radius:0;text-transform:none}button,input{margin:0;font-family:inherit;font-size:inherit;line-height:inherit;overflow:visible}[type=button]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}[type=search]{outline-offset:-2px}summary{display:list-item;cursor:pointer}[hidden]{display:none!important}.h1,h1{margin-bottom:.25rem;font-weight:600;line-height:1.2;color:#303030;font-size:2.1875rem}.list-unstyled{padding-left:0;list-style:none}a>code{color:inherit}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.search form{display:block;padding:.375rem .75rem;font-weight:400;color:#303030;background-color:#fff;background-clip:padding-box;border-radius:.25rem}.search form::-ms-expand{background-color:transparent;border:0}.search form:-moz-focusring{color:transparent;text-shadow:0 0 0 #303030}.search form::placeholder{opacity:1;color:#919191}.search form:disabled{background-color:#fafafa;opacity:1}.form-inline{display:flex;flex-flow:row wrap;align-items:center}@media (min-width:576px){.form-inline .search form,.search .form-inline form{display:inline-block;width:auto;vertical-align:middle}}.btn{display:inline-block;text-align:center;vertical-align:middle;cursor:pointer;user-select:none;border:1px solid transparent;padding:.375rem .75rem;line-height:20px;border-radius:.25rem}.btn:disabled{opacity:.65}.btn-success{color:#fff;background-color:#108548;border-color:#108548}.btn-success:disabled{color:#fff;background-color:#108548;border-color:#108548}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-menu-toggle{color:#fff;background-color:#0b572f;border-color:#094c29}.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.collapse:not(.show){display:none}.dropdown-menu-toggle::after{margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-menu-toggle:empty::after{margin-left:0}.dropdown-menu{left:0;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#303030;text-align:left;list-style:none;background-clip:padding-box;border:1px solid rgba(0,0,0,.15)}.dropdown-menu-right{right:0;left:auto}.divider{height:0;margin:4px 0;overflow:hidden;border-top:1px solid #dbdbdb}.dropdown-menu.show{display:block}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.navbar{position:relative;padding:.25rem .5rem}.navbar,.navbar .container,.navbar .container-fluid{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .dropdown-menu{float:none}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}.badge,.card{border-radius:.25rem}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid #dbdbdb}.card>hr{margin-right:0;margin-left:0}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:600;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.close{float:right;font-size:1.5rem;font-weight:600;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}button.close{padding:0;background-color:transparent;border:0;appearance:none}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dbdbdb!important}.rounded{border-radius:.25rem!important}.d-none{display:none!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}@media (min-width:576px){.d-sm-none{display:none!important}}@media (min-width:768px){.d-md-block{display:block!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-block{display:block!important}}@media (min-width:1200px){.d-xl-block{display:block!important}}.float-right{float:right!important}.sr-only{white-space:nowrap}.m-auto{margin:auto!important}.text-nowrap{white-space:nowrap!important}.search form,body{font-size:.875rem}[role=button],button,html [type=button]{cursor:pointer}.h1,h1{margin-top:20px;margin-bottom:10px}input[type=file]{line-height:1}.code>code{background-color:inherit;padding:unset}.hidden{display:none!important;visibility:hidden!important}.dropdown-menu-toggle::after,.hide{display:none}.badge:not(.gl-badge){padding:4px 5px;font-size:12px;font-style:normal;font-weight:400;display:inline-block}.toggle-sidebar-button .collapse-text,.toggle-sidebar-button .icon-chevron-double-lg-left,.toggle-sidebar-button .icon-chevron-double-lg-right{color:#707070}body{text-decoration-skip:ink}.container{padding-top:0;z-index:5}.container .content{margin:0}@media (max-width:575.98px){.container .content{margin-top:20px}.container .container .title{padding-left:15px!important}}.btn{border-radius:4px;font-size:.875rem;font-weight:400;padding:6px 10px;background-color:#fff;border-color:#dbdbdb;color:#303030;white-space:nowrap}.btn:active{box-shadow:none}.btn.active,.btn:active{box-shadow:rgba(0,0,0,.16);background-color:#eaeaea;border-color:#e3e3e3;color:#303030}.btn.btn-sm{padding:4px 10px;font-size:13px;line-height:18px}.btn.btn-success{background-color:#108548;border-color:#217645;color:#fff}.btn.btn-success.active,.btn.btn-success:active{box-shadow:rgba(0,0,0,.16);background-color:#24663b;border-color:#0d532a;color:#fff}.btn svg{height:15px;width:15px;position:relative;top:2px}.btn .fa:not(:last-child),.btn svg:not(:last-child){margin-right:5px}.badge.badge-pill:not(.gl-badge){font-weight:400;background-color:rgba(0,0,0,.07);color:#4f4f4f;vertical-align:baseline}.loading{margin:20px auto;height:40px;color:#555;font-size:32px;text-align:center}.chart{overflow:hidden;height:220px}.center{text-align:center}.flex{display:flex}.dropdown{position:relative}.show.dropdown .dropdown-menu{transform:translateY(0);display:block;min-height:40px;max-height:312px;overflow-y:auto}@media (max-width:575.98px){.show.dropdown .dropdown-menu{width:100%}}.show.dropdown .dropdown-menu-toggle{border-color:#c4c4c4}.search-input-container .dropdown-menu{margin-top:11px}.dropdown-menu,.dropdown-menu-toggle{font-size:14px;background-color:#fff;border:1px solid #dbdbdb;border-radius:.25rem}.dropdown-menu-toggle{color:#303030;text-align:left;white-space:nowrap;padding:6px 25px 6px 10px;position:relative;width:160px;text-overflow:ellipsis;overflow:hidden}.no-outline.dropdown-menu-toggle,.show.dropdown [data-toggle=dropdown]{outline:0}.dropdown-menu-toggle .fa{color:#c4c4c4;position:absolute}.dropdown-menu{display:none;position:absolute;width:auto;top:100%;z-index:300;min-width:240px;max-width:500px;margin-top:4px;margin-bottom:24px;font-weight:400;padding:8px 0;box-shadow:0 2px 4px rgba(0,0,0,.1)}.dropdown-menu ul{margin:0;padding:0}.dropdown-menu li{display:block;text-align:left;list-style:none;padding:0 1px}.dropdown-menu li button,.dropdown-menu li>a{background:0 0;border:0;border-radius:0;box-shadow:none;display:block;font-weight:400;position:relative;padding:8px 12px;color:#303030;line-height:16px;white-space:normal;overflow:hidden;text-align:left;width:100%}.dropdown-menu li button:active,.dropdown-menu li>a:active{background-color:#eee;color:#303030;outline:0;text-decoration:none}.dropdown-menu li button:active .avatar,.dropdown-menu li>a:active .avatar{border-color:#fff}.dropdown-menu li button:active .badge.badge-pill,.dropdown-menu li>a:active .badge.badge-pill{background-color:#d3e7f9}.dropdown-menu .divider{height:1px;margin:.25rem 0;padding:0;background-color:#dbdbdb}.dropdown-menu .badge.badge-pill+span:not(.badge.badge-pill){margin-right:40px}.dropdown-select{width:300px}@media (max-width:767.98px){.dropdown-select{width:100%}}.dropdown-content{max-height:252px;overflow-y:auto}.dropdown-loading{position:absolute;top:0;right:0;bottom:0;left:0;display:none;z-index:9;background-color:rgba(255,255,255,.6);font-size:28px}.dropdown-loading .fa{position:absolute;top:50%;left:50%;margin-top:-14px;margin-left:-14px}@media (max-width:575.98px){.navbar-gitlab li.dropdown{position:static}header.navbar-gitlab .dropdown .dropdown-menu{width:100%;min-width:100%}}@media (max-width:767.98px){.dropdown-menu-toggle{width:100%}}input{border-radius:.25rem;color:#303030;background-color:#fff}.search form{margin:0;padding:4px;width:200px;line-height:24px;height:32px;border:0;border-radius:4px}body.ui-indigo .navbar-gitlab{background-color:#292961}body.ui-indigo .navbar-gitlab .nav>li,body.ui-indigo .navbar-gitlab .navbar-collapse,body.ui-indigo .navbar-gitlab .navbar-sub-nav{color:#d1d1f0}body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler{border-left:1px solid #6868b9}body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler svg{fill:#d1d1f0}body.ui-indigo .navbar-gitlab .nav>li.active>a,body.ui-indigo .navbar-gitlab .nav>li.dropdown.show>a,body.ui-indigo .navbar-gitlab .navbar-nav>li.active>a,body.ui-indigo .navbar-gitlab .navbar-nav>li.active>button,body.ui-indigo .navbar-gitlab .navbar-nav>li.dropdown.show>a,body.ui-indigo .navbar-gitlab .navbar-nav>li.dropdown.show>button,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.active>a,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.active>button,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.dropdown.show>a,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.dropdown.show>button{color:#292961;background-color:#fff}body.ui-indigo .navbar-gitlab .nav>li>a.header-user-dropdown-toggle .header-user-avatar{border-color:#d1d1f0}body.ui-indigo .search form{background-color:rgba(209,209,240,.2)}body.ui-indigo .search .search-input::placeholder{color:rgba(209,209,240,.8)}body.ui-indigo .search .search-input-wrap .clear-icon,body.ui-indigo .search .search-input-wrap .search-icon{fill:rgba(209,209,240,.8)}body.ui-indigo .nav-sidebar li.active{box-shadow:inset 4px 0 0 #4b4ba3}body.ui-indigo .nav-sidebar li.active>a,body.ui-indigo .sidebar-top-level-items>li.active .badge.badge-pill{color:#393982}body.ui-indigo .nav-sidebar li.active .nav-icon-container svg{fill:#393982}.navbar-gitlab{padding:0 16px;z-index:1000;margin-bottom:0;min-height:40px;border:0;border-bottom:1px solid #dbdbdb;position:fixed;top:0;left:0;right:0;border-radius:0}.navbar-gitlab .logo-text{line-height:initial}.navbar-gitlab .logo-text svg{width:55px;height:14px;margin:0;fill:#fff}.navbar-gitlab .close-icon{display:none}.navbar-gitlab .header-content{width:100%;display:flex;justify-content:space-between;position:relative;min-height:40px;padding-left:0}.navbar-gitlab .header-content .title-container{display:flex;align-items:stretch;flex:1 1 auto;padding-top:0;overflow:visible}.navbar-gitlab .header-content .title{padding-right:0;color:currentColor;display:flex;position:relative;margin:0;font-size:18px;vertical-align:top;white-space:nowrap}.navbar-gitlab .header-content .title img{height:28px}.navbar-gitlab .header-content .title img+.logo-text{margin-left:8px}.navbar-gitlab .header-content .title a{display:flex;align-items:center;padding:2px 8px;margin:5px 2px 5px -8px;border-radius:4px}.navbar-gitlab .header-content .dropdown.open>a{border-bottom-color:#fff}.navbar-gitlab .header-content .navbar-collapse>ul.nav>li:not(.d-none){margin:0 2px}.navbar-gitlab .navbar-collapse{flex:0 0 auto;border-top:0;padding:0}@media (max-width:575.98px){.navbar-gitlab .navbar-collapse{flex:1 1 auto}}.navbar-gitlab .navbar-collapse .nav{flex-wrap:nowrap}@media (max-width:575.98px){.navbar-gitlab .navbar-collapse .nav>li:not(.d-none) a{margin-left:0}}.navbar-gitlab .container-fluid{padding:0}.navbar-gitlab .container-fluid .user-counter svg{margin-right:3px}.navbar-gitlab .container-fluid .navbar-toggler{position:relative;right:-10px;border-radius:0;min-width:45px;padding:0;margin:8px -7px 8px 0;font-size:14px;text-align:center;color:currentColor}.navbar-gitlab .container-fluid .navbar-toggler.active{color:currentColor;background-color:transparent}@media (max-width:575.98px){.navbar-gitlab .container-fluid .navbar-nav{display:flex;padding-right:10px;flex-direction:row}}.navbar-gitlab .container-fluid .navbar-nav li .badge.badge-pill{box-shadow:none;font-weight:600}@media (max-width:575.98px){.navbar-gitlab .container-fluid .nav>li.header-user{padding-left:10px}}.navbar-gitlab .container-fluid .nav>li>a{will-change:color;margin:4px 0;padding:6px 8px;height:32px}@media (max-width:575.98px){.navbar-gitlab .container-fluid .nav>li>a{padding:0}}.navbar-gitlab .container-fluid .nav>li>a.header-user-dropdown-toggle{margin-left:2px}.navbar-gitlab .container-fluid .nav>li .header-new-dropdown-toggle,.navbar-gitlab .container-fluid .nav>li>a.header-user-dropdown-toggle .header-user-avatar{margin-right:0}.navbar-nav>li>a,.navbar-nav>li>button,.navbar-sub-nav>li>a,.navbar-sub-nav>li>button{display:flex;align-items:center;justify-content:center;padding:6px 8px;margin:4px 2px;font-size:12px;color:currentColor;border-radius:4px;height:32px;font-weight:600}.navbar-nav>li>button,.navbar-sub-nav>li>button{background:0 0;border:0}.navbar-nav .dropdown-menu,.navbar-sub-nav .dropdown-menu{position:absolute}.navbar-sub-nav{display:flex;margin:0 0 0 6px}.btn .caret-down,.caret-down{top:0;height:11px;width:11px;margin-left:4px;fill:currentColor}.header-new .dropdown-menu,.header-user .dropdown-menu{margin-top:4px}.btn-sign-in{background-color:#ebebfa;color:#292961;font-weight:600;line-height:18px;margin:4px 0 4px 2px}.navbar-nav .badge.badge-pill,.title-container .badge.badge-pill{position:inherit;font-weight:400;margin-left:-6px;font-size:11px;color:#fff;padding:0 5px;line-height:12px;border-radius:7px;box-shadow:0 1px 0 rgba(76,78,84,.2)}.navbar-nav .badge.badge-pill.green-badge,.title-container .badge.badge-pill.green-badge{background-color:#108548}.navbar-nav .badge.badge-pill.merge-requests-count,.title-container .badge.badge-pill.merge-requests-count{background-color:#de7e00}.navbar-nav .badge.badge-pill.todos-count,.title-container .badge.badge-pill.todos-count{background-color:#1f75cb}.navbar-nav .canary-badge .badge,.title-container .canary-badge .badge{font-size:12px;line-height:16px;padding:0 .5rem}@media (max-width:575.98px){.navbar-gitlab .container-fluid{font-size:18px}.navbar-gitlab .container-fluid .navbar-nav{table-layout:fixed;width:100%;margin:0;text-align:right}.navbar-gitlab .container-fluid .navbar-collapse{margin-left:-8px;margin-right:-10px}.navbar-gitlab .container-fluid .navbar-collapse .nav>li:not(.d-none){flex:1}.header-user-dropdown-toggle{text-align:center}.header-user-avatar{float:none}}.header-user.show .dropdown-menu{margin-top:4px;color:#303030;left:auto;max-height:445px}.header-user.show .dropdown-menu svg{vertical-align:text-top}.header-user-avatar{float:left;margin-right:5px;border-radius:50%;border:1px solid #f5f5f5}.media{display:flex;align-items:flex-start}.card{margin-bottom:16px}@media (min-width:768px){.page-with-contextual-sidebar{padding-left:50px}}@media (min-width:1200px){.page-with-contextual-sidebar{padding-left:220px}}.context-header{position:relative;margin-right:2px;width:220px}.context-header>a,.context-header>button{font-weight:600;display:flex;width:100%;align-items:center;padding:10px 16px 10px 10px;color:#303030;background-color:transparent;border:0;text-align:left}.context-header .avatar-container{flex:0 0 40px;background-color:#fff}.context-header .sidebar-context-title{overflow:hidden;text-overflow:ellipsis}.context-header .sidebar-context-title.text-secondary{font-weight:400;font-size:.8em}.nav-sidebar{position:fixed;z-index:600;width:220px;top:40px;bottom:0;left:0;background-color:#fafafa;box-shadow:inset -1px 0 0 #dbdbdb;transform:translate3d(0,0,0)}@media (min-width:576px) and (max-width:576px){.nav-sidebar:not(.sidebar-collapsed-desktop){box-shadow:inset -1px 0 0 #dbdbdb,2px 1px 3px rgba(0,0,0,.1)}}.nav-sidebar a{text-decoration:none}.nav-sidebar ul{padding-left:0;list-style:none}.nav-sidebar li{white-space:nowrap}.nav-sidebar li a{display:flex;align-items:center;padding:12px 16px;color:#707070}.nav-sidebar li .nav-item-name{flex:1}.nav-sidebar li.active>a,.sidebar-top-level-items>li.active .badge.badge-pill{font-weight:600}@media (max-width:767.98px){.nav-sidebar{left:-220px}}.nav-sidebar .nav-icon-container{display:flex;margin-right:8px}.nav-sidebar .fly-out-top-item{display:none}.nav-sidebar svg{height:16px;width:16px}@media (min-width:768px) and (max-width:1199px){.nav-sidebar:not(.sidebar-expanded-mobile){width:50px}.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll{overflow-x:hidden}.nav-sidebar:not(.sidebar-expanded-mobile) .badge.badge-pill:not(.fly-out-badge),.nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name,.nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items>li>a{min-height:45px}.nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item{display:block}.nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container{margin:0 auto}.nav-sidebar:not(.sidebar-expanded-mobile) .context-header{height:60px;width:50px}.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a{padding:10px 4px}.nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items>li .sidebar-sub-level-items:not(.flyout-list),.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .collapse-text,.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-left{display:none}.nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container{margin-right:0}.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button{padding:16px;width:49px}.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-right{display:block;margin:0}}.nav-sidebar-inner-scroll{height:100%;width:100%;overflow:auto}.sidebar-sub-level-items{display:none;padding-bottom:8px}.sidebar-sub-level-items>li a{padding:8px 16px 8px 40px}.sidebar-sub-level-items>li.active a,.sidebar-top-level-items>li.active{background:rgba(0,0,0,.04)}.sidebar-top-level-items{margin-bottom:60px}@media (min-width:576px){.sidebar-top-level-items>li>a{margin-right:1px}}.sidebar-top-level-items>li .badge.badge-pill{background-color:rgba(0,0,0,.08);color:#707070}.sidebar-top-level-items>li.active>a{margin-left:4px;padding-left:12px}.sidebar-top-level-items>li.active .sidebar-sub-level-items:not(.is-fly-out-only){display:block}.close-nav-button,.toggle-sidebar-button{width:219px;position:fixed;height:48px;bottom:0;padding:0 16px;background-color:#fafafa;border:0;border-top:1px solid #dbdbdb;color:#707070;display:flex;align-items:center}.close-nav-button svg,.toggle-sidebar-button svg{margin-right:8px}.close-nav-button .icon-chevron-double-lg-right,.toggle-sidebar-button .icon-chevron-double-lg-right{display:none}.collapse-text{white-space:nowrap;overflow:hidden}.fly-out-top-item>a{display:flex}.fly-out-top-item .fly-out-badge{margin-left:8px}.fly-out-top-item-name{flex:1}.close-nav-button{display:none}@media (max-width:767.98px){.close-nav-button{display:flex}.toggle-sidebar-button{display:none}}input::-moz-placeholder{color:#919191;opacity:1}input:-ms-input-placeholder,input::-ms-input-placeholder{color:#919191}svg.s12{width:12px;height:12px}svg.s16{width:16px;height:16px}svg.s18{width:18px;height:18px}.feature-highlight-popover-sub-content{padding:16px 12px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.color-label{padding:0 .5rem;line-height:16px;border-radius:100px;color:#fff}.label-link{display:inline-flex;vertical-align:text-bottom}.milestones{padding:8px;margin-top:8px;border-radius:4px;background-color:#dbdbdb}.search{margin:0 8px}@media (min-width:1200px){.search form{width:320px}}.search .search-input{border:0;font-size:14px;padding:0 20px 0 0;margin-left:5px;line-height:25px;width:98%;color:#fff;background:0 0}.search .search-input-container{display:flex;position:relative}.search .search-input-wrap{width:100%}.search .search-input-wrap .clear-icon,.search .search-input-wrap .search-icon{position:absolute;right:5px;top:4px}.search .search-input-wrap .search-icon{-moz-user-select:none;user-select:none}.search .search-input-wrap .clear-icon{display:none}.search .search-input-wrap .dropdown{position:static}.search .search-input-wrap .dropdown-menu{left:-5px;max-height:400px;overflow:auto}@media (min-width:1200px){.search .search-input-wrap .dropdown-menu{width:320px}}.search .search-input-wrap .dropdown-content{max-height:382px}.search .identicon{flex-basis:16px;flex-shrink:0;margin-right:4px}.settings{border-top:1px solid #dbdbdb}.settings:first-of-type{margin-top:10px;border:0}.settings+div .settings:first-of-type{margin-top:0;border-top:1px solid #dbdbdb}.avatar,.avatar-container{float:left;margin-right:16px;border-radius:50%;border:1px solid #f5f5f5}.s16.avatar,.s16.avatar-container{width:16px;height:16px;margin-right:8px}.s18.avatar,.s18.avatar-container{width:18px;height:18px;margin-right:8px}.s40.avatar,.s40.avatar-container{width:40px;height:40px;margin-right:8px}.avatar{transition-property:none;width:40px;height:40px;padding:0;background:#fdfdfd;overflow:hidden;border-color:rgba(0,0,0,.1)}.avatar.center{font-size:14px;line-height:1.8em;text-align:center}.avatar.avatar-tile{border-radius:0;border:0}.identicon{text-align:center;vertical-align:top;color:#4f4f4f;background-color:#eee}.identicon.s16{font-size:10px;line-height:16px}.identicon.s40{font-size:16px;line-height:38px}.avatar-container{overflow:hidden;display:flex}.avatar-container a{width:100%;height:100%;display:flex;text-decoration:none}.avatar-container .avatar{border-radius:0;border:0;height:auto;width:100%;margin:0;align-self:center}.avatar-container.s40{min-width:40px;min-height:40px}.rect-avatar,.rect-avatar.s16,.rect-avatar.s18{border-radius:2px}.rect-avatar.s40{border-radius:4px}.tab-width-8{-moz-tab-size:8;tab-size:8}.gl-sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.gl-ml-3{margin-left:.5rem}
+@charset "UTF-8";
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+html {
+ font-family: sans-serif;
+ line-height: 1.15;
+}
+ header, nav, section {
+ display: block;
+}
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #303030;
+ text-align: left;
+ background-color: #fff;
+}
+h1, h2, h3 {
+ margin-top: 0;
+ margin-bottom: 0.25rem;
+}
+p {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
-/* Cloaking in order to prevent flickering of content */
+ul {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+ul ul {
+ margin-bottom: 0;
+}
+
+strong {
+ font-weight: bolder;
+}
+sub {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline;
+}
+sub {
+ bottom: -.25em;
+}
+a {
+ color: #007bff;
+ text-decoration: none;
+ background-color: transparent;
+}
+a:not([href]) {
+ color: inherit;
+ text-decoration: none;
+}
+pre,
+code {
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ font-size: 1em;
+}
+pre {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ overflow: auto;
+}
+img {
+ vertical-align: middle;
+ border-style: none;
+}
+svg {
+ overflow: hidden;
+ vertical-align: middle;
+}
+table {
+ border-collapse: collapse;
+}
+th {
+ text-align: inherit;
+}
+button {
+ border-radius: 0;
+}
+input,
+button,
+textarea {
+ margin: 0;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+button,
+input {
+ overflow: visible;
+}
+button {
+ text-transform: none;
+}
+button:not(:disabled),
+[type="button"]:not(:disabled),
+[type="reset"]:not(:disabled) {
+ cursor: pointer;
+}
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner {
+ padding: 0;
+ border-style: none;
+}
+textarea {
+ overflow: auto;
+ resize: vertical;
+}
+[type="search"] {
+ outline-offset: -2px;
+}
+summary {
+ display: list-item;
+ cursor: pointer;
+}
+template {
+ display: none;
+}
+[hidden] {
+ display: none !important;
+}
+h1, h2, h3,
+.h1, .h2, .h3 {
+ margin-bottom: 0.25rem;
+ font-weight: 600;
+ line-height: 1.2;
+ color: #303030;
+}
+h1, .h1 {
+ font-size: 2.1875rem;
+}
+h2, .h2 {
+ font-size: 1.75rem;
+}
+h3, .h3 {
+ font-size: 1.53125rem;
+}
+.list-unstyled {
+ padding-left: 0;
+ list-style: none;
+}
+code {
+ font-size: 90%;
+ color: #1f1f1f;
+ word-wrap: break-word;
+}
+a > code {
+ color: inherit;
+}
+pre {
+ display: block;
+ font-size: 90%;
+ color: #303030;
+}
+pre code {
+ font-size: inherit;
+ color: inherit;
+ word-break: normal;
+}
+.container {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+.container-fluid {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: -15px;
+ margin-left: -15px;
+}
+.table {
+ width: 100%;
+ margin-bottom: 0.5rem;
+ color: #303030;
+}
+.table th,
+.table td {
+ padding: 0.75rem;
+ vertical-align: top;
+ border-top: 1px solid #dbdbdb;
+}
+ .search form {
+ display: block;
+ width: 100%;
+ height: 34px;
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #303030;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.25rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+}
+ .search form:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #303030;
+}
+ .search form::placeholder {
+ color: #5e5e5e;
+ opacity: 1;
+}
+ .search form:disabled {
+ background-color: #fafafa;
+ opacity: 1;
+}
+.form-inline {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+}
+
+@media (min-width: 576px) {
+ .form-inline .search form, .search .form-inline form {
+ display: inline-block;
+ width: auto;
+ vertical-align: middle;
+ }
+}
+.btn {
+ display: inline-block;
+ font-weight: 400;
+ color: #303030;
+ text-align: center;
+ vertical-align: middle;
+ cursor: pointer;
+ user-select: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ line-height: 20px;
+ border-radius: 0.25rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+}
+.btn.disabled, .btn:disabled {
+ opacity: 0.65;
+}
+a.btn.disabled {
+ pointer-events: none;
+}
+.collapse:not(.show) {
+ display: none;
+}
+
+.dropdown {
+ position: relative;
+}
+ .dropdown-menu-toggle {
+ white-space: nowrap;
+}
+ .dropdown-menu-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0;
+ border-left: 0.3em solid transparent;
+}
+ .dropdown-menu-toggle:empty::after {
+ margin-left: 0;
+}
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 10rem;
+ padding: 0.5rem 0;
+ margin: 0.125rem 0 0;
+ font-size: 1rem;
+ color: #303030;
+ text-align: left;
+ list-style: none;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 0.25rem;
+}
+.dropdown-menu-right {
+ right: 0;
+ left: auto;
+}
+ .divider {
+ height: 0;
+ margin: 4px 0;
+ overflow: hidden;
+ border-top: 1px solid #dbdbdb;
+}
+.dropdown-menu.show {
+ display: block;
+}
+.nav {
+ display: flex;
+ flex-wrap: wrap;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+.navbar {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.25rem 0.5rem;
+}
+.navbar .container,
+.navbar .container-fluid {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+}
+.navbar-nav {
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+.navbar-nav .dropdown-menu {
+ position: static;
+ float: none;
+}
+.navbar-collapse {
+ flex-basis: 100%;
+ flex-grow: 1;
+ align-items: center;
+}
+.navbar-toggler {
+ padding: 0.25rem 0.75rem;
+ font-size: 1.25rem;
+ line-height: 1;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 576px) {
+ .navbar-expand-sm {
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ }
+ .navbar-expand-sm .navbar-nav {
+ flex-direction: row;
+ }
+ .navbar-expand-sm .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid {
+ flex-wrap: nowrap;
+ }
+ .navbar-expand-sm .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+ .navbar-expand-sm .navbar-toggler {
+ display: none;
+ }
+}
+.card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #fff;
+ background-clip: border-box;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.25rem;
+}
+.badge {
+ display: inline-block;
+ padding: 0.25em 0.4em;
+ font-size: 75%;
+ font-weight: 600;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+}
+.badge:empty {
+ display: none;
+}
+.btn .badge {
+ position: relative;
+ top: -1px;
+}
+.badge-pill {
+ padding-right: 0.6em;
+ padding-left: 0.6em;
+ border-radius: 10rem;
+}
+.media {
+ display: flex;
+ align-items: flex-start;
+}
+.close {
+ float: right;
+ font-size: 1.5rem;
+ font-weight: 600;
+ line-height: 1;
+ color: #000;
+ text-shadow: 0 1px 0 #fff;
+ opacity: .5;
+}
+button.close {
+ padding: 0;
+ background-color: transparent;
+ border: 0;
+ appearance: none;
+}
+a.close.disabled {
+ pointer-events: none;
+}
+.modal-dialog {
+ position: relative;
+ width: auto;
+ margin: 0.5rem;
+ pointer-events: none;
+}
+
+@media (min-width: 576px) {
+ .modal-dialog {
+ max-width: 500px;
+ margin: 1.75rem auto;
+ }
+}
+.bg-transparent {
+ background-color: transparent !important;
+}
+.border {
+ border: 1px solid #dbdbdb !important;
+}
+.border-top {
+ border-top: 1px solid #dbdbdb !important;
+}
+.border-right {
+ border-right: 1px solid #dbdbdb !important;
+}
+.border-bottom {
+ border-bottom: 1px solid #dbdbdb !important;
+}
+.border-left {
+ border-left: 1px solid #dbdbdb !important;
+}
+.rounded {
+ border-radius: 0.25rem !important;
+}
+.clearfix::after {
+ display: block;
+ clear: both;
+ content: "";
+}
+.d-none {
+ display: none !important;
+}
+.d-inline-block {
+ display: inline-block !important;
+}
+.d-block {
+ display: block !important;
+}
+
+@media (min-width: 576px) {
+ .d-sm-none {
+ display: none !important;
+ }
+}
+
+@media (min-width: 768px) {
+ .d-md-block {
+ display: block !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .d-lg-none {
+ display: none !important;
+ }
+ .d-lg-block {
+ display: block !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .d-xl-block {
+ display: block !important;
+ }
+}
+.flex-wrap {
+ flex-wrap: wrap !important;
+}
+.float-right {
+ float: right !important;
+}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+.m-auto {
+ margin: auto !important;
+}
+.text-nowrap {
+ white-space: nowrap !important;
+}
+.visible {
+ visibility: visible !important;
+}
+ .search form.focus {
+ color: #303030;
+ background-color: #fff;
+ border-color: #80bdff;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
+}
+.gl-badge {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.75rem;
+ font-weight: 400;
+ line-height: 1rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ outline: none;
+}
+body, .search form,
+.search form {
+ font-size: 0.875rem;
+}
+button,
+html [type='button'],
+[type='reset'],
+[role='button'] {
+ cursor: pointer;
+}
+h1,
+.h1,
+h2,
+.h2,
+h3,
+.h3 {
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+input[type='file'] {
+ line-height: 1;
+}
+
+strong {
+ font-weight: bold;
+}
+a {
+ color: #1068bf;
+}
+code {
+ padding: 2px 4px;
+ color: #1f1f1f;
+ background-color: #f0f0f0;
+ border-radius: 4px;
+}
+.code > code {
+ background-color: inherit;
+ padding: unset;
+}
+table {
+ border-spacing: 0;
+}
+.hidden {
+ display: none !important;
+ visibility: hidden !important;
+}
+.hide {
+ display: none;
+}
+ .dropdown-menu-toggle::after {
+ display: none;
+}
+.badge:not(.gl-badge) {
+ padding: 4px 5px;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ display: inline-block;
+}
+pre code {
+ white-space: pre-wrap;
+}
+.toggle-sidebar-button .collapse-text,
+.toggle-sidebar-button .icon-chevron-double-lg-left,
+.toggle-sidebar-button .icon-chevron-double-lg-right {
+ color: #666;
+}
+svg {
+ vertical-align: baseline;
+}
+html {
+ overflow-y: scroll;
+}
+body {
+ text-decoration-skip: ink;
+}
+.content-wrapper {
+ margin-top: 40px;
+ padding-bottom: 100px;
+}
+.container {
+ padding-top: 0;
+ z-index: 5;
+}
+.container .content {
+ margin: 0;
+}
+
+@media (max-width: 575.98px) {
+ .container .content {
+ margin-top: 20px;
+ }
+}
+
+@media (max-width: 575.98px) {
+ .container .container .title {
+ padding-left: 15px !important;
+ }
+}
+.btn {
+ border-radius: 4px;
+ font-size: 0.875rem;
+ font-weight: 400;
+ padding: 6px 10px;
+ background-color: #fff;
+ border-color: #dbdbdb;
+ color: #303030;
+ color: #303030;
+ white-space: nowrap;
+}
+.btn:active, .btn.active {
+ box-shadow: rgba(0, 0, 0, 0.16);
+ background-color: #eaeaea;
+ border-color: #e3e3e3;
+ color: #303030;
+}
+.btn svg {
+ height: 15px;
+ width: 15px;
+}
+.btn svg:not(:last-child),
+.btn .fa:not(:last-child) {
+ margin-right: 5px;
+}
+.badge.badge-pill:not(.gl-badge) {
+ font-weight: 400;
+ background-color: rgba(0, 0, 0, 0.07);
+ color: #525252;
+ vertical-align: baseline;
+}
+.hint {
+ font-style: italic;
+ color: #bfbfbf;
+}
+.bold {
+ font-weight: 600;
+}
+pre.wrap {
+ word-break: break-word;
+ white-space: pre-wrap;
+}
+table a code {
+ position: relative;
+ top: -2px;
+ margin-right: 3px;
+}
+.loading {
+ margin: 20px auto;
+ height: 40px;
+ color: #525252;
+ font-size: 32px;
+ text-align: center;
+}
+.highlight {
+ text-shadow: none;
+}
+.chart {
+ overflow: hidden;
+ height: 220px;
+}
+.break-word {
+ word-wrap: break-word;
+}
+.center {
+ text-align: center;
+}
+.block {
+ display: block;
+}
+.flex {
+ display: flex;
+}
+.flex-grow {
+ flex-grow: 1;
+}
+.dropdown {
+ position: relative;
+}
+.show.dropdown .dropdown-menu {
+ transform: translateY(0);
+ display: block;
+ min-height: 40px;
+ max-height: 312px;
+ overflow-y: auto;
+}
+
+@media (max-width: 575.98px) {
+ .show.dropdown .dropdown-menu {
+ width: 100%;
+ }
+}
+ .show.dropdown .dropdown-menu-toggle,
+.show.dropdown .dropdown-menu-toggle {
+ border-color: #c4c4c4;
+}
+.show.dropdown [data-toggle='dropdown'] {
+ outline: 0;
+}
+.search-input-container .dropdown-menu {
+ margin-top: 11px;
+}
+ .dropdown-menu-toggle {
+ padding: 6px 8px 6px 10px;
+ background-color: #fff;
+ color: #303030;
+ font-size: 14px;
+ text-align: left;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.25rem;
+ white-space: nowrap;
+}
+ .no-outline.dropdown-menu-toggle {
+ outline: 0;
+}
+ .dropdown-menu-toggle .fa {
+ color: #c4c4c4;
+}
+.dropdown-menu-toggle {
+ padding-right: 25px;
+ position: relative;
+ width: 160px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+.dropdown-menu-toggle .fa {
+ position: absolute;
+}
+.dropdown-menu {
+ display: none;
+ position: absolute;
+ width: auto;
+ top: 100%;
+ z-index: 300;
+ min-width: 240px;
+ max-width: 500px;
+ margin-top: 4px;
+ margin-bottom: 24px;
+ font-size: 14px;
+ font-weight: 400;
+ padding: 8px 0;
+ background-color: #fff;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.25rem;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+.dropdown-menu ul {
+ margin: 0;
+ padding: 0;
+}
+.dropdown-menu li {
+ display: block;
+ text-align: left;
+ list-style: none;
+ padding: 0 1px;
+}
+.dropdown-menu li > a,
+.dropdown-menu li button {
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+ display: block;
+ font-weight: 400;
+ position: relative;
+ padding: 8px 12px;
+ color: #303030;
+ line-height: 16px;
+ white-space: normal;
+ overflow: hidden;
+ text-align: left;
+ width: 100%;
+}
+.dropdown-menu .divider {
+ height: 1px;
+ margin: 0.25rem 0;
+ padding: 0;
+ background-color: #dbdbdb;
+}
+.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
+ margin-right: 40px;
+}
+.dropdown-select {
+ width: 300px;
+}
+
+@media (max-width: 767.98px) {
+ .dropdown-select {
+ width: 100%;
+ }
+}
+.dropdown-content {
+ max-height: 252px;
+ overflow-y: auto;
+}
+.dropdown-loading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: none;
+ z-index: 9;
+ background-color: rgba(255, 255, 255, 0.6);
+ font-size: 28px;
+}
+.dropdown-loading .fa {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -14px;
+ margin-left: -14px;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab li.dropdown {
+ position: static;
+ }
+ header.navbar-gitlab .dropdown .dropdown-menu {
+ width: 100%;
+ min-width: 100%;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
+}
+textarea {
+ resize: vertical;
+}
+input {
+ border-radius: 0.25rem;
+ color: #303030;
+ background-color: #fff;
+}
+ .search form {
+ border-radius: 4px;
+ padding: 6px 10px;
+}
+ .search form::placeholder {
+ color: #868686;
+}
+.navbar-gitlab {
+ padding: 0 16px;
+ z-index: 1000;
+ margin-bottom: 0;
+ min-height: 40px;
+ border: 0;
+ border-bottom: 1px solid #dbdbdb;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-radius: 0;
+}
+.navbar-gitlab .logo-text {
+ line-height: initial;
+}
+.navbar-gitlab .logo-text svg {
+ width: 55px;
+ height: 14px;
+ margin: 0;
+ fill: #fff;
+}
+.navbar-gitlab .close-icon {
+ display: none;
+}
+.navbar-gitlab .header-content {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+ min-height: 40px;
+ padding-left: 0;
+}
+.navbar-gitlab .header-content .title-container {
+ display: flex;
+ align-items: stretch;
+ flex: 1 1 auto;
+ padding-top: 0;
+ overflow: visible;
+}
+.navbar-gitlab .header-content .title {
+ padding-right: 0;
+ color: currentColor;
+ display: flex;
+ position: relative;
+ margin: 0;
+ font-size: 18px;
+ vertical-align: top;
+ white-space: nowrap;
+}
+.navbar-gitlab .header-content .title img {
+ height: 28px;
+}
+.navbar-gitlab .header-content .title img + .logo-text {
+ margin-left: 8px;
+}
+.navbar-gitlab .header-content .title.wrap {
+ white-space: normal;
+}
+.navbar-gitlab .header-content .title a {
+ display: flex;
+ align-items: center;
+ padding: 2px 8px;
+ margin: 5px 2px 5px -8px;
+ border-radius: 4px;
+}
+.navbar-gitlab .header-content .dropdown.open > a {
+ border-bottom-color: #fff;
+}
+.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
+ margin: 0 2px;
+}
+.navbar-gitlab .navbar-collapse {
+ flex: 0 0 auto;
+ border-top: 0;
+ padding: 0;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .navbar-collapse {
+ flex: 1 1 auto;
+ }
+}
+.navbar-gitlab .navbar-collapse .nav {
+ flex-wrap: nowrap;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a {
+ margin-left: 0;
+ }
+}
+.navbar-gitlab .container-fluid {
+ padding: 0;
+}
+.navbar-gitlab .container-fluid .user-counter svg {
+ margin-right: 3px;
+}
+.navbar-gitlab .container-fluid .navbar-toggler {
+ position: relative;
+ right: -10px;
+ border-radius: 0;
+ min-width: 45px;
+ padding: 0;
+ margin: 8px -7px 8px 0;
+ font-size: 14px;
+ text-align: center;
+ color: currentColor;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid .navbar-nav {
+ display: flex;
+ padding-right: 10px;
+ flex-direction: row;
+ }
+}
+.navbar-gitlab .container-fluid .navbar-nav li .badge.badge-pill {
+ box-shadow: none;
+ font-weight: 600;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid .nav > li.header-user {
+ padding-left: 10px;
+ }
+}
+.navbar-gitlab .container-fluid .nav > li > a {
+ will-change: color;
+ margin: 4px 0;
+ padding: 6px 8px;
+ height: 32px;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid .nav > li > a {
+ padding: 0;
+ }
+}
+.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle {
+ margin-left: 2px;
+}
+.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle .header-user-avatar {
+ margin-right: 0;
+}
+.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle {
+ margin-right: 0;
+}
+.navbar-sub-nav > li > a,
+.navbar-sub-nav > li > button,
+.navbar-nav > li > a,
+.navbar-nav > li > button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 8px;
+ margin: 4px 2px;
+ font-size: 12px;
+ color: currentColor;
+ border-radius: 4px;
+ height: 32px;
+ font-weight: 600;
+}
+.navbar-sub-nav > li > button,
+.navbar-nav > li > button {
+ background: transparent;
+ border: 0;
+}
+.navbar-sub-nav .dropdown-menu,
+.navbar-nav .dropdown-menu {
+ position: absolute;
+}
+.navbar-sub-nav {
+ display: flex;
+ margin: 0 0 0 6px;
+}
+.caret-down,
+.btn .caret-down {
+ top: 0;
+ height: 11px;
+ width: 11px;
+ margin-left: 4px;
+ fill: currentColor;
+}
+.header-user .dropdown-menu,
+.header-new .dropdown-menu {
+ margin-top: 4px;
+}
+.btn-sign-in {
+ background-color: #ebebfa;
+ color: #292961;
+ font-weight: 600;
+ line-height: 18px;
+ margin: 4px 0 4px 2px;
+}
+.title-container .badge.badge-pill,
+.navbar-nav .badge.badge-pill {
+ position: inherit;
+ font-weight: 400;
+ margin-left: -6px;
+ font-size: 11px;
+ color: #fff;
+ padding: 0 5px;
+ line-height: 12px;
+ border-radius: 7px;
+ box-shadow: 0 1px 0 rgba(76, 78, 84, 0.2);
+}
+.title-container .badge.badge-pill.green-badge,
+.navbar-nav .badge.badge-pill.green-badge {
+ background-color: #108548;
+}
+.title-container .badge.badge-pill.merge-requests-count,
+.navbar-nav .badge.badge-pill.merge-requests-count {
+ background-color: #de7e00;
+}
+.title-container .badge.badge-pill.todos-count,
+.navbar-nav .badge.badge-pill.todos-count {
+ background-color: #1f75cb;
+}
+.title-container .canary-badge .badge,
+.navbar-nav .canary-badge .badge {
+ font-size: 12px;
+ line-height: 16px;
+ padding: 0 0.5rem;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid {
+ font-size: 18px;
+ }
+ .navbar-gitlab .container-fluid .navbar-nav {
+ table-layout: fixed;
+ width: 100%;
+ margin: 0;
+ text-align: right;
+ }
+ .navbar-gitlab .container-fluid .navbar-collapse {
+ margin-left: -8px;
+ margin-right: -10px;
+ }
+ .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) {
+ flex: 1;
+ }
+ .header-user-dropdown-toggle {
+ text-align: center;
+ }
+ .header-user-avatar {
+ float: none;
+ }
+}
+.header-user.show .dropdown-menu {
+ margin-top: 4px;
+ color: #303030;
+ left: auto;
+ max-height: 445px;
+}
+.header-user.show .dropdown-menu svg {
+ vertical-align: text-top;
+}
+.header-user-avatar {
+ float: left;
+ margin-right: 5px;
+ border-radius: 50%;
+ border: 1px solid #f5f5f5;
+}
+.media {
+ display: flex;
+ align-items: flex-start;
+}
+.card {
+ margin-bottom: 16px;
+}
+.content-wrapper {
+ width: 100%;
+}
+.content-wrapper .container-fluid {
+ padding: 0 16px;
+}
+
+@media (min-width: 768px) {
+ .page-with-contextual-sidebar {
+ padding-left: 50px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .page-with-contextual-sidebar {
+ padding-left: 220px;
+ }
+}
+.context-header {
+ position: relative;
+ margin-right: 2px;
+ width: 220px;
+}
+.context-header > a,
+.context-header > button {
+ font-weight: 600;
+ display: flex;
+ width: 100%;
+ align-items: center;
+ padding: 10px 16px 10px 10px;
+ color: #303030;
+ background-color: transparent;
+ border: 0;
+ text-align: left;
+}
+.context-header .avatar-container {
+ flex: 0 0 40px;
+ background-color: #fff;
+}
+.context-header .sidebar-context-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.context-header .sidebar-context-title.text-secondary {
+ font-weight: normal;
+ font-size: 0.8em;
+}
+.nav-sidebar {
+ position: fixed;
+ z-index: 600;
+ width: 220px;
+ top: 40px;
+ bottom: 0;
+ left: 0;
+ background-color: #fafafa;
+ box-shadow: inset -1px 0 0 #dbdbdb;
+ transform: translate3d(0, 0, 0);
+}
+
+@media (min-width: 576px) and (max-width: 576px) {
+ .nav-sidebar:not(.sidebar-collapsed-desktop) {
+ box-shadow: inset -1px 0 0 #dbdbdb, 2px 1px 3px rgba(0, 0, 0, 0.1);
+ }
+}
+.nav-sidebar.sidebar-collapsed-desktop {
+ width: 50px;
+}
+.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
+ overflow-x: hidden;
+}
+.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge),
+.nav-sidebar.sidebar-collapsed-desktop .sidebar-context-title,
+.nav-sidebar.sidebar-collapsed-desktop .nav-item-name {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a {
+ min-height: 45px;
+}
+.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item {
+ display: block;
+}
+.nav-sidebar.sidebar-collapsed-desktop .avatar-container {
+ margin: 0 auto;
+}
+.nav-sidebar.sidebar-expanded-mobile {
+ left: 0;
+}
+.nav-sidebar a {
+ text-decoration: none;
+}
+.nav-sidebar ul {
+ padding-left: 0;
+ list-style: none;
+}
+.nav-sidebar li {
+ white-space: nowrap;
+}
+.nav-sidebar li a {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ color: #666;
+}
+.nav-sidebar li .nav-item-name {
+ flex: 1;
+}
+.nav-sidebar li.active > a {
+ font-weight: 600;
+}
+
+@media (max-width: 767.98px) {
+ .nav-sidebar {
+ left: -220px;
+ }
+}
+.nav-sidebar .nav-icon-container {
+ display: flex;
+ margin-right: 8px;
+}
+.nav-sidebar .fly-out-top-item {
+ display: none;
+}
+.nav-sidebar svg {
+ height: 16px;
+ width: 16px;
+}
+
+@media (min-width: 768px) and (max-width: 1199px) {
+ .nav-sidebar:not(.sidebar-expanded-mobile) {
+ width: 50px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
+ overflow-x: hidden;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .badge.badge-pill:not(.fly-out-badge),
+ .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title,
+ .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a {
+ min-height: 45px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item {
+ display: block;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container {
+ margin: 0 auto;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
+ height: 60px;
+ width: 50px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
+ padding: 10px 4px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) {
+ display: none;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container {
+ margin-right: 0;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
+ padding: 16px;
+ width: 49px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .collapse-text,
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-left {
+ display: none;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-right {
+ display: block;
+ margin: 0;
+ }
+}
+.nav-sidebar-inner-scroll {
+ height: 100%;
+ width: 100%;
+ overflow: auto;
+}
+.sidebar-sub-level-items {
+ display: none;
+ padding-bottom: 8px;
+}
+.sidebar-sub-level-items > li a {
+ padding: 8px 16px 8px 40px;
+}
+.sidebar-top-level-items {
+ margin-bottom: 60px;
+}
+
+@media (min-width: 576px) {
+ .sidebar-top-level-items > li > a {
+ margin-right: 1px;
+ }
+}
+.sidebar-top-level-items > li .badge.badge-pill {
+ background-color: rgba(0, 0, 0, 0.08);
+ color: #666;
+}
+.sidebar-top-level-items > li.active {
+ background: rgba(0, 0, 0, 0.04);
+}
+.sidebar-top-level-items > li.active > a {
+ margin-left: 4px;
+ padding-left: 12px;
+}
+.sidebar-top-level-items > li.active .badge.badge-pill {
+ font-weight: 600;
+}
+.sidebar-top-level-items > li.active .sidebar-sub-level-items:not(.is-fly-out-only) {
+ display: block;
+}
+.toggle-sidebar-button,
+.close-nav-button {
+ width: 219px;
+ position: fixed;
+ height: 48px;
+ bottom: 0;
+ padding: 0 16px;
+ background-color: #fafafa;
+ border: 0;
+ border-top: 1px solid #dbdbdb;
+ color: #666;
+ display: flex;
+ align-items: center;
+}
+.toggle-sidebar-button svg,
+.close-nav-button svg {
+ margin-right: 8px;
+}
+.toggle-sidebar-button .icon-chevron-double-lg-right,
+.close-nav-button .icon-chevron-double-lg-right {
+ display: none;
+}
+.collapse-text {
+ white-space: nowrap;
+ overflow: hidden;
+}
+.sidebar-collapsed-desktop .context-header {
+ height: 60px;
+ width: 50px;
+}
+.sidebar-collapsed-desktop .context-header a {
+ padding: 10px 4px;
+}
+.sidebar-collapsed-desktop .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) {
+ display: none;
+}
+.sidebar-collapsed-desktop .nav-icon-container {
+ margin-right: 0;
+}
+.sidebar-collapsed-desktop .toggle-sidebar-button {
+ padding: 16px;
+ width: 49px;
+}
+.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text,
+.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left {
+ display: none;
+}
+.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-right {
+ display: block;
+ margin: 0;
+}
+.fly-out-top-item > a {
+ display: flex;
+}
+.fly-out-top-item .fly-out-badge {
+ margin-left: 8px;
+}
+.fly-out-top-item-name {
+ flex: 1;
+}
+.close-nav-button {
+ display: none;
+}
+
+@media (max-width: 767.98px) {
+ .close-nav-button {
+ display: flex;
+ }
+ .toggle-sidebar-button {
+ display: none;
+ }
+}
+table.table {
+ margin-bottom: 16px;
+}
+table.table .dropdown-menu a {
+ text-decoration: none;
+}
+table.table .success,
+table.table .info {
+ color: #fff;
+}
+table.table .success a:not(.btn),
+table.table .info a:not(.btn) {
+ text-decoration: underline;
+ color: #fff;
+}
+pre {
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ display: block;
+ padding: 8px 12px;
+ margin: 0 0 8px;
+ font-size: 13px;
+ word-break: break-all;
+ word-wrap: break-word;
+ color: #303030;
+ background-color: #fafafa;
+ border: 1px solid #dbdbdb;
+ border-radius: 2px;
+}
+.monospace {
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+}
+input::-moz-placeholder,
+textarea::-moz-placeholder {
+ color: #868686;
+ opacity: 1;
+}
+input::-ms-input-placeholder,
+textarea::-ms-input-placeholder {
+ color: #868686;
+}
+input:-ms-input-placeholder,
+textarea:-ms-input-placeholder {
+ color: #868686;
+}
+svg {
+ fill: currentColor;
+}
+
+svg.s12 {
+ width: 12px;
+ height: 12px;
+}
+
+svg.s16 {
+ width: 16px;
+ height: 16px;
+}
+
+svg.s18 {
+ width: 18px;
+ height: 18px;
+}
+
+svg.s12 {
+ vertical-align: -1px;
+}
+
+svg.s16 {
+ vertical-align: -3px;
+}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
+table.code {
+ width: 100%;
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ border: 0;
+ border-collapse: separate;
+ margin: 0;
+ padding: 0;
+ table-layout: fixed;
+ border-radius: 0 0 4px 4px;
+}
+.frame .badge.badge-pill {
+ position: absolute;
+ background-color: #428fdc;
+ color: #fff;
+ border: #fff 1px solid;
+ min-height: 16px;
+ padding: 5px 8px;
+ border-radius: 12px;
+}
+.frame .badge.badge-pill {
+ transform: translate(-50%, -50%);
+}
+.color-label {
+ padding: 0 0.5rem;
+ line-height: 16px;
+ border-radius: 100px;
+ color: #fff;
+}
+.label-link {
+ display: inline-flex;
+ vertical-align: text-bottom;
+}
+.milestones {
+ padding: 8px;
+ margin-top: 8px;
+ border-radius: 4px;
+ background-color: #dbdbdb;
+}
+.search {
+ margin: 0 8px;
+}
+.search form {
+ margin: 0;
+ padding: 4px;
+ width: 200px;
+ line-height: 24px;
+ height: 32px;
+ border: 0;
+ border-radius: 4px;
+}
+
+@media (min-width: 1200px) {
+ .search form {
+ width: 320px;
+ }
+}
+.search .search-input {
+ border: 0;
+ font-size: 14px;
+ padding: 0 20px 0 0;
+ margin-left: 5px;
+ line-height: 25px;
+ width: 98%;
+ color: #fff;
+ background: none;
+}
+.search .search-input-container {
+ display: flex;
+ position: relative;
+}
+.search .search-input-wrap {
+ width: 100%;
+}
+.search .search-input-wrap .search-icon,
+.search .search-input-wrap .clear-icon {
+ position: absolute;
+ right: 5px;
+ top: 4px;
+}
+.search .search-input-wrap .search-icon {
+ -moz-user-select: none;
+ user-select: none;
+}
+.search .search-input-wrap .clear-icon {
+ display: none;
+}
+.search .search-input-wrap .dropdown {
+ position: static;
+}
+.search .search-input-wrap .dropdown-menu {
+ left: -5px;
+ max-height: 400px;
+ overflow: auto;
+}
+
+@media (min-width: 1200px) {
+ .search .search-input-wrap .dropdown-menu {
+ width: 320px;
+ }
+}
+.search .search-input-wrap .dropdown-content {
+ max-height: 382px;
+}
+.settings {
+ border-top: 1px solid #dbdbdb;
+}
+.settings:first-of-type {
+ margin-top: 10px;
+ border: 0;
+}
+.settings + div .settings:first-of-type {
+ margin-top: 0;
+ border-top: 1px solid #dbdbdb;
+}
+.avatar, .avatar-container {
+ float: left;
+ margin-right: 16px;
+ border-radius: 50%;
+ border: 1px solid #f5f5f5;
+}
+.s16.avatar, .s16.avatar-container {
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+}
+.s18.avatar, .s18.avatar-container {
+ width: 18px;
+ height: 18px;
+ margin-right: 8px;
+}
+.s40.avatar, .s40.avatar-container {
+ width: 40px;
+ height: 40px;
+ margin-right: 8px;
+}
+.avatar {
+ transition-property: none;
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ background: #fdfdfd;
+ overflow: hidden;
+ border-color: rgba(0, 0, 0, 0.1);
+}
+.avatar.center {
+ font-size: 14px;
+ line-height: 1.8em;
+ text-align: center;
+}
+.avatar.avatar-tile {
+ border-radius: 0;
+ border: 0;
+}
+.avatar-container {
+ overflow: hidden;
+ display: flex;
+}
+.avatar-container a {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ text-decoration: none;
+}
+.avatar-container .avatar {
+ border-radius: 0;
+ border: 0;
+ height: auto;
+ width: 100%;
+ margin: 0;
+ align-self: center;
+}
+.avatar-container.s40 {
+ min-width: 40px;
+ min-height: 40px;
+}
+.rect-avatar {
+ border-radius: 2px;
+}
+.rect-avatar.s16 {
+ border-radius: 2px;
+}
+.rect-avatar.s18 {
+ border-radius: 2px;
+}
+.rect-avatar.s40 {
+ border-radius: 4px;
+}
+.tab-width-8 {
+ -moz-tab-size: 8;
+ tab-size: 8;
+}
+.gl-sr-only {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+.gl-ml-3 {
+ margin-left: 0.5rem;
+}
+.content-wrapper > .alert-wrapper,
+#content-body, .modal-dialog {
+ display: block;
+}
@import 'cloaking';
@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
new file mode 100644
index 00000000000..b3e53e35f6e
--- /dev/null
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -0,0 +1,2409 @@
+@charset "UTF-8";
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+html {
+ font-family: sans-serif;
+ line-height: 1.15;
+}
+ header, nav, section {
+ display: block;
+}
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #303030;
+ text-align: left;
+ background-color: #fff;
+}
+hr {
+ box-sizing: content-box;
+ height: 0;
+ overflow: visible;
+}
+h1, h2, h3 {
+ margin-top: 0;
+ margin-bottom: 0.25rem;
+}
+p {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+address {
+ margin-bottom: 1rem;
+ font-style: normal;
+ line-height: inherit;
+}
+
+ul {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+ul ul {
+ margin-bottom: 0;
+}
+
+strong {
+ font-weight: bolder;
+}
+sub {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline;
+}
+sub {
+ bottom: -.25em;
+}
+a {
+ color: #007bff;
+ text-decoration: none;
+ background-color: transparent;
+}
+a:not([href]) {
+ color: inherit;
+ text-decoration: none;
+}
+pre,
+code {
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ font-size: 1em;
+}
+pre {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ overflow: auto;
+}
+img {
+ vertical-align: middle;
+ border-style: none;
+}
+svg {
+ overflow: hidden;
+ vertical-align: middle;
+}
+table {
+ border-collapse: collapse;
+}
+th {
+ text-align: inherit;
+}
+label {
+ display: inline-block;
+ margin-bottom: 0.5rem;
+}
+button {
+ border-radius: 0;
+}
+input,
+button,
+textarea {
+ margin: 0;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+button,
+input {
+ overflow: visible;
+}
+button {
+ text-transform: none;
+}
+button:not(:disabled),
+[type="button"]:not(:disabled),
+[type="reset"]:not(:disabled),
+[type="submit"]:not(:disabled) {
+ cursor: pointer;
+}
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ padding: 0;
+ border-style: none;
+}
+
+input[type="checkbox"] {
+ box-sizing: border-box;
+ padding: 0;
+}
+textarea {
+ overflow: auto;
+ resize: vertical;
+}
+fieldset {
+ min-width: 0;
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+[type="search"] {
+ outline-offset: -2px;
+}
+summary {
+ display: list-item;
+ cursor: pointer;
+}
+template {
+ display: none;
+}
+[hidden] {
+ display: none !important;
+}
+h1, h2, h3,
+.h1, .h2, .h3 {
+ margin-bottom: 0.25rem;
+ font-weight: 600;
+ line-height: 1.2;
+ color: #303030;
+}
+h1, .h1 {
+ font-size: 2.1875rem;
+}
+h2, .h2 {
+ font-size: 1.75rem;
+}
+h3, .h3 {
+ font-size: 1.53125rem;
+}
+hr {
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+ border: 0;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+}
+.list-unstyled {
+ padding-left: 0;
+ list-style: none;
+}
+code {
+ font-size: 90%;
+ color: #1f1f1f;
+ word-wrap: break-word;
+}
+a > code {
+ color: inherit;
+}
+pre {
+ display: block;
+ font-size: 90%;
+ color: #303030;
+}
+pre code {
+ font-size: inherit;
+ color: inherit;
+ word-break: normal;
+}
+.container {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+.container-fluid {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: -15px;
+ margin-left: -15px;
+}
+ .col-sm-5, .col-sm-7, .col-sm-12 {
+ position: relative;
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+}
+.order-1 {
+ order: 1;
+}
+.order-12 {
+ order: 12;
+}
+
+@media (min-width: 576px) {
+ .col-sm-5 {
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+ .col-sm-7 {
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+ .col-sm-12 {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ .order-sm-1 {
+ order: 1;
+ }
+ .order-sm-12 {
+ order: 12;
+ }
+}
+.table {
+ width: 100%;
+ margin-bottom: 0.5rem;
+ color: #303030;
+}
+.table th,
+.table td {
+ padding: 0.75rem;
+ vertical-align: top;
+ border-top: 1px solid #dbdbdb;
+}
+.form-control, .search form {
+ display: block;
+ width: 100%;
+ height: 34px;
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #303030;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.25rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+}
+.form-control:-moz-focusring, .search form:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #303030;
+}
+.form-control::placeholder, .search form::placeholder {
+ color: #5e5e5e;
+ opacity: 1;
+}
+.form-control:disabled, .search form:disabled {
+ background-color: #fafafa;
+ opacity: 1;
+}
+textarea.form-control {
+ height: auto;
+}
+.form-group {
+ margin-bottom: 1rem;
+}
+.form-inline {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+}
+
+@media (min-width: 576px) {
+ .form-inline label {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 0;
+ }
+ .form-inline .form-group {
+ display: flex;
+ flex: 0 0 auto;
+ flex-flow: row wrap;
+ align-items: center;
+ margin-bottom: 0;
+ }
+ .form-inline .form-control, .form-inline .search form, .search .form-inline form {
+ display: inline-block;
+ width: auto;
+ vertical-align: middle;
+ }
+}
+.btn {
+ display: inline-block;
+ font-weight: 400;
+ color: #303030;
+ text-align: center;
+ vertical-align: middle;
+ cursor: pointer;
+ user-select: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ line-height: 20px;
+ border-radius: 0.25rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+}
+.btn.disabled, .btn:disabled {
+ opacity: 0.65;
+}
+a.btn.disabled,
+fieldset:disabled a.btn {
+ pointer-events: none;
+}
+.btn-success {
+ color: #fff;
+ background-color: #108548;
+ border-color: #108548;
+}
+.btn-success.disabled, .btn-success:disabled {
+ color: #fff;
+ background-color: #108548;
+ border-color: #108548;
+}
+.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,
+.show > .btn-success.dropdown-menu-toggle {
+ color: #fff;
+ background-color: #0b572f;
+ border-color: #094c29;
+}
+ .login-page input[type='submit'] {
+ display: block;
+ width: 100%;
+}
+ .login-page input[type='submit'] + input[type='submit'] {
+ margin-top: 0.5rem;
+}
+ .login-page input[type="submit"][type='submit'],
+.login-page input[type="reset"][type='submit'],
+.login-page input[type="button"][type='submit'] {
+ width: 100%;
+}
+.collapse:not(.show) {
+ display: none;
+}
+
+.dropdown {
+ position: relative;
+}
+ .dropdown-menu-toggle {
+ white-space: nowrap;
+}
+ .dropdown-menu-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0;
+ border-left: 0.3em solid transparent;
+}
+ .dropdown-menu-toggle:empty::after {
+ margin-left: 0;
+}
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 10rem;
+ padding: 0.5rem 0;
+ margin: 0.125rem 0 0;
+ font-size: 1rem;
+ color: #303030;
+ text-align: left;
+ list-style: none;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 0.25rem;
+}
+.dropdown-menu-right {
+ right: 0;
+ left: auto;
+}
+ .divider {
+ height: 0;
+ margin: 4px 0;
+ overflow: hidden;
+ border-top: 1px solid #dbdbdb;
+}
+.dropdown-menu.show {
+ display: block;
+}
+.nav {
+ display: flex;
+ flex-wrap: wrap;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+.nav-link {
+ display: block;
+ padding: 0.5rem 1rem;
+}
+.nav-link.disabled {
+ color: #5e5e5e;
+ pointer-events: none;
+ cursor: default;
+}
+.nav-tabs {
+ border-bottom: 1px solid #999;
+}
+.nav-tabs .nav-item {
+ margin-bottom: -1px;
+}
+.nav-tabs .nav-link {
+ border: 1px solid transparent;
+ border-top-left-radius: 0.25rem;
+ border-top-right-radius: 0.25rem;
+}
+.nav-tabs .nav-link.disabled {
+ color: #5e5e5e;
+ background-color: transparent;
+ border-color: transparent;
+}
+.nav-tabs .nav-link.active,
+.nav-tabs .nav-item.show .nav-link {
+ color: #525252;
+ background-color: #fff;
+ border-color: #999 #999 #fff;
+}
+.nav-tabs .dropdown-menu {
+ margin-top: -1px;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+.tab-content > .tab-pane {
+ display: none;
+}
+.tab-content > .active {
+ display: block;
+}
+.navbar {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.25rem 0.5rem;
+}
+.navbar .container,
+.navbar .container-fluid {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+}
+.navbar-nav {
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+.navbar-nav .nav-link {
+ padding-right: 0;
+ padding-left: 0;
+}
+.navbar-nav .dropdown-menu {
+ position: static;
+ float: none;
+}
+.navbar-collapse {
+ flex-basis: 100%;
+ flex-grow: 1;
+ align-items: center;
+}
+.navbar-toggler {
+ padding: 0.25rem 0.75rem;
+ font-size: 1.25rem;
+ line-height: 1;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 576px) {
+ .navbar-expand-sm {
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ }
+ .navbar-expand-sm .navbar-nav {
+ flex-direction: row;
+ }
+ .navbar-expand-sm .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ .navbar-expand-sm .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid {
+ flex-wrap: nowrap;
+ }
+ .navbar-expand-sm .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+ .navbar-expand-sm .navbar-toggler {
+ display: none;
+ }
+}
+.card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #fff;
+ background-clip: border-box;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.25rem;
+}
+.card > hr {
+ margin-right: 0;
+ margin-left: 0;
+}
+.badge {
+ display: inline-block;
+ padding: 0.25em 0.4em;
+ font-size: 75%;
+ font-weight: 600;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+}
+.badge:empty {
+ display: none;
+}
+.btn .badge {
+ position: relative;
+ top: -1px;
+}
+.badge-pill {
+ padding-right: 0.6em;
+ padding-left: 0.6em;
+ border-radius: 10rem;
+}
+.media {
+ display: flex;
+ align-items: flex-start;
+}
+.close {
+ float: right;
+ font-size: 1.5rem;
+ font-weight: 600;
+ line-height: 1;
+ color: #000;
+ text-shadow: 0 1px 0 #fff;
+ opacity: .5;
+}
+button.close {
+ padding: 0;
+ background-color: transparent;
+ border: 0;
+ appearance: none;
+}
+a.close.disabled {
+ pointer-events: none;
+}
+.modal-dialog {
+ position: relative;
+ width: auto;
+ margin: 0.5rem;
+ pointer-events: none;
+}
+
+@media (min-width: 576px) {
+ .modal-dialog {
+ max-width: 500px;
+ margin: 1.75rem auto;
+ }
+}
+.bg-transparent {
+ background-color: transparent !important;
+}
+.border {
+ border: 1px solid #dbdbdb !important;
+}
+.border-top {
+ border-top: 1px solid #dbdbdb !important;
+}
+.border-right {
+ border-right: 1px solid #dbdbdb !important;
+}
+.border-bottom {
+ border-bottom: 1px solid #dbdbdb !important;
+}
+.border-left {
+ border-left: 1px solid #dbdbdb !important;
+}
+.rounded {
+ border-radius: 0.25rem !important;
+}
+.clearfix::after {
+ display: block;
+ clear: both;
+ content: "";
+}
+.d-none {
+ display: none !important;
+}
+.d-inline-block {
+ display: inline-block !important;
+}
+.d-block {
+ display: block !important;
+}
+.d-flex {
+ display: flex !important;
+}
+
+@media (min-width: 576px) {
+ .d-sm-none {
+ display: none !important;
+ }
+}
+
+@media (min-width: 768px) {
+ .d-md-block {
+ display: block !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .d-lg-none {
+ display: none !important;
+ }
+ .d-lg-block {
+ display: block !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .d-xl-block {
+ display: block !important;
+ }
+}
+.flex-wrap {
+ flex-wrap: wrap !important;
+}
+.justify-content-between {
+ justify-content: space-between !important;
+}
+.align-items-center {
+ align-items: center !important;
+}
+.float-right {
+ float: right !important;
+}
+.fixed-top {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1030;
+}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+.mt-3 {
+ margin-top: 1rem !important;
+}
+.mb-3 {
+ margin-bottom: 1rem !important;
+}
+.m-auto {
+ margin: auto !important;
+}
+
+@media (min-width: 576px) {
+ .mt-sm-0 {
+ margin-top: 0 !important;
+ }
+}
+.text-nowrap {
+ white-space: nowrap !important;
+}
+.text-left {
+ text-align: left !important;
+}
+.font-weight-normal {
+ font-weight: 400 !important;
+}
+.visible {
+ visibility: visible !important;
+}
+.form-control.focus, .search form.focus {
+ color: #303030;
+ background-color: #fff;
+ border-color: #80bdff;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
+}
+input[type="color"].form-control {
+ height: 34px;
+ padding: 0.125rem 0.25rem;
+}
+input[type="color"].form-control:disabled {
+ background-color: #666;
+ opacity: 0.65;
+}
+.gl-badge {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.75rem;
+ font-weight: 400;
+ line-height: 1rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ outline: none;
+}
+body, .form-control, .search form,
+.search form {
+ font-size: 0.875rem;
+}
+button,
+html [type='button'],
+[type='reset'],
+[type='submit'],
+[role='button'] {
+ cursor: pointer;
+}
+h1,
+.h1,
+h2,
+.h2,
+h3,
+.h3 {
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+input[type='file'] {
+ line-height: 1;
+}
+
+strong {
+ font-weight: bold;
+}
+a {
+ color: #1068bf;
+}
+hr {
+ overflow: hidden;
+}
+code {
+ padding: 2px 4px;
+ color: #1f1f1f;
+ background-color: #f0f0f0;
+ border-radius: 4px;
+}
+.code > code {
+ background-color: inherit;
+ padding: unset;
+}
+table {
+ border-spacing: 0;
+}
+.hidden {
+ display: none !important;
+ visibility: hidden !important;
+}
+.hide {
+ display: none;
+}
+ .dropdown-menu-toggle::after {
+ display: none;
+}
+.badge:not(.gl-badge),
+.label {
+ padding: 4px 5px;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ display: inline-block;
+}
+.nav-tabs {
+ border-bottom: 0;
+}
+.nav-tabs .nav-link {
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+}
+.nav-tabs .nav-item {
+ margin-bottom: 0;
+}
+pre code {
+ white-space: pre-wrap;
+}
+input[type="color"].form-control {
+ height: 34px;
+}
+.toggle-sidebar-button .collapse-text,
+.toggle-sidebar-button .icon-chevron-double-lg-left,
+.toggle-sidebar-button .icon-chevron-double-lg-right {
+ color: #666;
+}
+svg {
+ vertical-align: baseline;
+}
+html {
+ overflow-y: scroll;
+}
+body {
+ text-decoration-skip: ink;
+}
+body.navless {
+ background-color: #fff !important;
+}
+.content-wrapper {
+ margin-top: 40px;
+ padding-bottom: 100px;
+}
+.container {
+ padding-top: 0;
+ z-index: 5;
+}
+.container .content {
+ margin: 0;
+}
+
+@media (max-width: 575.98px) {
+ .container .content {
+ margin-top: 20px;
+ }
+}
+
+@media (max-width: 575.98px) {
+ .container .container .title {
+ padding-left: 15px !important;
+ }
+}
+.navless-container {
+ margin-top: 40px;
+ padding-top: 32px;
+}
+.btn {
+ border-radius: 4px;
+ font-size: 0.875rem;
+ font-weight: 400;
+ padding: 6px 10px;
+ background-color: #fff;
+ border-color: #dbdbdb;
+ color: #303030;
+ color: #303030;
+ white-space: nowrap;
+}
+.btn:active, .btn.active {
+ box-shadow: rgba(0, 0, 0, 0.16);
+ background-color: #eaeaea;
+ border-color: #e3e3e3;
+ color: #303030;
+}
+.btn.btn-success, .btn.btn-register {
+ background-color: #108548;
+ border-color: #217645;
+ color: #fff;
+}
+.btn.btn-success:active, .btn.btn-success.active, .btn.btn-register:active, .btn.btn-register.active {
+ box-shadow: rgba(0, 0, 0, 0.16);
+ background-color: #24663b;
+ border-color: #0d532a;
+ color: #fff;
+}
+.btn svg {
+ height: 15px;
+ width: 15px;
+}
+.btn svg:not(:last-child),
+.btn .fa:not(:last-child) {
+ margin-right: 5px;
+}
+ .login-page input[type='submit'] {
+ width: 100%;
+ margin: 0;
+ margin-bottom: 15px;
+}
+ .login-page input.btn[type='submit'] {
+ padding: 6px 0;
+}
+.badge.badge-pill:not(.gl-badge) {
+ font-weight: 400;
+ background-color: rgba(0, 0, 0, 0.07);
+ color: #525252;
+ vertical-align: baseline;
+}
+.hint {
+ font-style: italic;
+ color: #bfbfbf;
+}
+.bold {
+ font-weight: 600;
+}
+.tab-content {
+ overflow: visible;
+}
+pre.wrap {
+ word-break: break-word;
+ white-space: pre-wrap;
+}
+hr {
+ margin: 24px 0;
+ border-top: 1px solid #eee;
+}
+table a code {
+ position: relative;
+ top: -2px;
+ margin-right: 3px;
+}
+.loading {
+ margin: 20px auto;
+ height: 40px;
+ color: #525252;
+ font-size: 32px;
+ text-align: center;
+}
+.highlight {
+ text-shadow: none;
+}
+.chart {
+ overflow: hidden;
+ height: 220px;
+}
+.footer-links {
+ margin-bottom: 20px;
+}
+.footer-links a {
+ margin-right: 15px;
+}
+.break-word {
+ word-wrap: break-word;
+}
+.append-bottom-20 {
+ margin-bottom: 20px;
+}
+.center {
+ text-align: center;
+}
+.block {
+ display: block;
+}
+.flex {
+ display: flex;
+}
+.flex-grow {
+ flex-grow: 1;
+}
+.dropdown {
+ position: relative;
+}
+.show.dropdown .dropdown-menu {
+ transform: translateY(0);
+ display: block;
+ min-height: 40px;
+ max-height: 312px;
+ overflow-y: auto;
+}
+
+@media (max-width: 575.98px) {
+ .show.dropdown .dropdown-menu {
+ width: 100%;
+ }
+}
+ .show.dropdown .dropdown-menu-toggle,
+.show.dropdown .dropdown-menu-toggle {
+ border-color: #c4c4c4;
+}
+.show.dropdown [data-toggle='dropdown'] {
+ outline: 0;
+}
+.search-input-container .dropdown-menu {
+ margin-top: 11px;
+}
+ .dropdown-menu-toggle {
+ padding: 6px 8px 6px 10px;
+ background-color: #fff;
+ color: #303030;
+ font-size: 14px;
+ text-align: left;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.25rem;
+ white-space: nowrap;
+}
+ .no-outline.dropdown-menu-toggle {
+ outline: 0;
+}
+ .dropdown-menu-toggle .fa {
+ color: #c4c4c4;
+}
+.dropdown-menu-toggle {
+ padding-right: 25px;
+ position: relative;
+ width: 160px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+.dropdown-menu-toggle .fa {
+ position: absolute;
+}
+.dropdown-menu {
+ display: none;
+ position: absolute;
+ width: auto;
+ top: 100%;
+ z-index: 300;
+ min-width: 240px;
+ max-width: 500px;
+ margin-top: 4px;
+ margin-bottom: 24px;
+ font-size: 14px;
+ font-weight: 400;
+ padding: 8px 0;
+ background-color: #fff;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.25rem;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+.dropdown-menu ul {
+ margin: 0;
+ padding: 0;
+}
+.dropdown-menu li {
+ display: block;
+ text-align: left;
+ list-style: none;
+ padding: 0 1px;
+}
+.dropdown-menu li > a,
+.dropdown-menu li button {
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+ display: block;
+ font-weight: 400;
+ position: relative;
+ padding: 8px 12px;
+ color: #303030;
+ line-height: 16px;
+ white-space: normal;
+ overflow: hidden;
+ text-align: left;
+ width: 100%;
+}
+.dropdown-menu .divider {
+ height: 1px;
+ margin: 0.25rem 0;
+ padding: 0;
+ background-color: #dbdbdb;
+}
+.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
+ margin-right: 40px;
+}
+.dropdown-select {
+ width: 300px;
+}
+
+@media (max-width: 767.98px) {
+ .dropdown-select {
+ width: 100%;
+ }
+}
+.dropdown-content {
+ max-height: 252px;
+ overflow-y: auto;
+}
+.dropdown-loading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: none;
+ z-index: 9;
+ background-color: rgba(255, 255, 255, 0.6);
+ font-size: 28px;
+}
+.dropdown-loading .fa {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -14px;
+ margin-left: -14px;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab li.dropdown {
+ position: static;
+ }
+ header.navbar-gitlab .dropdown .dropdown-menu {
+ width: 100%;
+ min-width: 100%;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
+}
+.flash-container {
+ margin: 0;
+ margin-bottom: 16px;
+ font-size: 14px;
+ position: relative;
+ z-index: 1;
+}
+.flash-container.sticky {
+ position: sticky;
+ position: -webkit-sticky;
+ top: 48px;
+ z-index: 251;
+}
+.flash-container.flash-container-page {
+ margin-bottom: 0;
+}
+.flash-container:empty {
+ margin: 0;
+}
+textarea {
+ resize: vertical;
+}
+input {
+ border-radius: 0.25rem;
+ color: #303030;
+ background-color: #fff;
+}
+label {
+ font-weight: 600;
+}
+label.label-bold {
+ font-weight: 600;
+}
+.form-control, .search form {
+ border-radius: 4px;
+ padding: 6px 10px;
+}
+.form-control::placeholder, .search form::placeholder {
+ color: #868686;
+}
+.gl-field-error {
+ color: #dd2b0e;
+ font-size: 0.875rem;
+}
+.gl-show-field-errors .form-control:not(textarea), .gl-show-field-errors .search form:not(textarea), .search .gl-show-field-errors form:not(textarea) {
+ height: 34px;
+}
+.gl-show-field-errors .gl-field-hint {
+ color: #303030;
+}
+
+@media (max-width: 575.98px) {
+ .remember-me .remember-me-checkbox {
+ margin-top: 0;
+ }
+}
+body.ui-indigo .navbar-gitlab {
+ background-color: #292961;
+}
+body.ui-indigo .navbar-gitlab .navbar-collapse {
+ color: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler {
+ border-left: 1px solid #6868b9;
+}
+body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler svg {
+ fill: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.active > a,
+body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.active > button, body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.dropdown.show > a,
+body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.dropdown.show > button,
+body.ui-indigo .navbar-gitlab .navbar-nav > li.active > a,
+body.ui-indigo .navbar-gitlab .navbar-nav > li.active > button,
+body.ui-indigo .navbar-gitlab .navbar-nav > li.dropdown.show > a,
+body.ui-indigo .navbar-gitlab .navbar-nav > li.dropdown.show > button {
+ color: #292961;
+ background-color: #fff;
+}
+body.ui-indigo .navbar-gitlab .navbar-sub-nav {
+ color: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .nav > li {
+ color: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .nav > li > a.header-user-dropdown-toggle .header-user-avatar {
+ border-color: #d1d1f0;
+}
+body.ui-indigo .navbar-gitlab .nav > li.active > a,
+body.ui-indigo .navbar-gitlab .nav > li.dropdown.show > a {
+ color: #292961;
+ background-color: #fff;
+}
+body.ui-indigo .search form {
+ background-color: rgba(209, 209, 240, 0.2);
+}
+body.ui-indigo .search .search-input::placeholder {
+ color: rgba(209, 209, 240, 0.8);
+}
+body.ui-indigo .search .search-input-wrap .search-icon,
+body.ui-indigo .search .search-input-wrap .clear-icon {
+ fill: rgba(209, 209, 240, 0.8);
+}
+body.ui-indigo .nav-sidebar li.active {
+ box-shadow: inset 4px 0 0 #4b4ba3;
+}
+body.ui-indigo .nav-sidebar li.active > a {
+ color: #393982;
+}
+body.ui-indigo .nav-sidebar li.active .nav-icon-container svg {
+ fill: #393982;
+}
+body.ui-indigo .sidebar-top-level-items > li.active .badge.badge-pill {
+ color: #393982;
+}
+body.ui-indigo .nav-links li.active a,
+body.ui-indigo .nav-links li a.active {
+ border-bottom: 2px solid #6666c4;
+}
+body.ui-indigo .nav-links li.active a .badge.badge-pill,
+body.ui-indigo .nav-links li a.active .badge.badge-pill {
+ font-weight: 600;
+}
+.navbar-gitlab {
+ padding: 0 16px;
+ z-index: 1000;
+ margin-bottom: 0;
+ min-height: 40px;
+ border: 0;
+ border-bottom: 1px solid #dbdbdb;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-radius: 0;
+}
+.navbar-gitlab .logo-text {
+ line-height: initial;
+}
+.navbar-gitlab .logo-text svg {
+ width: 55px;
+ height: 14px;
+ margin: 0;
+ fill: #fff;
+}
+.navbar-gitlab .close-icon {
+ display: none;
+}
+.navbar-gitlab .header-content {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+ min-height: 40px;
+ padding-left: 0;
+}
+.navbar-gitlab .header-content .title-container {
+ display: flex;
+ align-items: stretch;
+ flex: 1 1 auto;
+ padding-top: 0;
+ overflow: visible;
+}
+.navbar-gitlab .header-content .title {
+ padding-right: 0;
+ color: currentColor;
+ display: flex;
+ position: relative;
+ margin: 0;
+ font-size: 18px;
+ vertical-align: top;
+ white-space: nowrap;
+}
+.navbar-gitlab .header-content .title img {
+ height: 28px;
+}
+.navbar-gitlab .header-content .title img + .logo-text {
+ margin-left: 8px;
+}
+.navbar-gitlab .header-content .title.wrap {
+ white-space: normal;
+}
+.navbar-gitlab .header-content .title a {
+ display: flex;
+ align-items: center;
+ padding: 2px 8px;
+ margin: 5px 2px 5px -8px;
+ border-radius: 4px;
+}
+.navbar-gitlab .header-content .dropdown.open > a {
+ border-bottom-color: #fff;
+}
+.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
+ margin: 0 2px;
+}
+.navbar-gitlab .navbar-collapse {
+ flex: 0 0 auto;
+ border-top: 0;
+ padding: 0;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .navbar-collapse {
+ flex: 1 1 auto;
+ }
+}
+.navbar-gitlab .navbar-collapse .nav {
+ flex-wrap: nowrap;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a {
+ margin-left: 0;
+ }
+}
+.navbar-gitlab .container-fluid {
+ padding: 0;
+}
+.navbar-gitlab .container-fluid .user-counter svg {
+ margin-right: 3px;
+}
+.navbar-gitlab .container-fluid .navbar-toggler {
+ position: relative;
+ right: -10px;
+ border-radius: 0;
+ min-width: 45px;
+ padding: 0;
+ margin: 8px -7px 8px 0;
+ font-size: 14px;
+ text-align: center;
+ color: currentColor;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid .navbar-nav {
+ display: flex;
+ padding-right: 10px;
+ flex-direction: row;
+ }
+}
+.navbar-gitlab .container-fluid .navbar-nav li .badge.badge-pill {
+ box-shadow: none;
+ font-weight: 600;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid .nav > li.header-user {
+ padding-left: 10px;
+ }
+}
+.navbar-gitlab .container-fluid .nav > li > a {
+ will-change: color;
+ margin: 4px 0;
+ padding: 6px 8px;
+ height: 32px;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid .nav > li > a {
+ padding: 0;
+ }
+}
+.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle {
+ margin-left: 2px;
+}
+.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle .header-user-avatar {
+ margin-right: 0;
+}
+.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle {
+ margin-right: 0;
+}
+.navbar-sub-nav > li > a,
+.navbar-sub-nav > li > button,
+.navbar-nav > li > a,
+.navbar-nav > li > button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 8px;
+ margin: 4px 2px;
+ font-size: 12px;
+ color: currentColor;
+ border-radius: 4px;
+ height: 32px;
+ font-weight: 600;
+}
+.navbar-sub-nav > li > button,
+.navbar-nav > li > button {
+ background: transparent;
+ border: 0;
+}
+.navbar-sub-nav .dropdown-menu,
+.navbar-nav .dropdown-menu {
+ position: absolute;
+}
+.navbar-sub-nav {
+ display: flex;
+ margin: 0 0 0 6px;
+}
+.caret-down,
+.btn .caret-down {
+ top: 0;
+ height: 11px;
+ width: 11px;
+ margin-left: 4px;
+ fill: currentColor;
+}
+.header-user .dropdown-menu,
+.header-new .dropdown-menu {
+ margin-top: 4px;
+}
+.btn-sign-in {
+ background-color: #ebebfa;
+ color: #292961;
+ font-weight: 600;
+ line-height: 18px;
+ margin: 4px 0 4px 2px;
+}
+.title-container .badge.badge-pill,
+.navbar-nav .badge.badge-pill {
+ position: inherit;
+ font-weight: 400;
+ margin-left: -6px;
+ font-size: 11px;
+ color: #fff;
+ padding: 0 5px;
+ line-height: 12px;
+ border-radius: 7px;
+ box-shadow: 0 1px 0 rgba(76, 78, 84, 0.2);
+}
+.title-container .badge.badge-pill.green-badge,
+.navbar-nav .badge.badge-pill.green-badge {
+ background-color: #108548;
+}
+.title-container .badge.badge-pill.merge-requests-count,
+.navbar-nav .badge.badge-pill.merge-requests-count {
+ background-color: #de7e00;
+}
+.title-container .badge.badge-pill.todos-count,
+.navbar-nav .badge.badge-pill.todos-count {
+ background-color: #1f75cb;
+}
+.title-container .canary-badge .badge,
+.navbar-nav .canary-badge .badge {
+ font-size: 12px;
+ line-height: 16px;
+ padding: 0 0.5rem;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-gitlab .container-fluid {
+ font-size: 18px;
+ }
+ .navbar-gitlab .container-fluid .navbar-nav {
+ table-layout: fixed;
+ width: 100%;
+ margin: 0;
+ text-align: right;
+ }
+ .navbar-gitlab .container-fluid .navbar-collapse {
+ margin-left: -8px;
+ margin-right: -10px;
+ }
+ .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) {
+ flex: 1;
+ }
+ .header-user-dropdown-toggle {
+ text-align: center;
+ }
+ .header-user-avatar {
+ float: none;
+ }
+}
+.header-user.show .dropdown-menu {
+ margin-top: 4px;
+ color: #303030;
+ left: auto;
+ max-height: 445px;
+}
+.header-user.show .dropdown-menu svg {
+ vertical-align: text-top;
+}
+.header-user-avatar {
+ float: left;
+ margin-right: 5px;
+ border-radius: 50%;
+ border: 1px solid #f5f5f5;
+}
+.navbar-empty {
+ justify-content: center;
+ height: 40px;
+ background: #fff;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+@media (max-width: 575.98px) {
+ .nav-links > li > a .badge.badge-pill {
+ display: none;
+ }
+}
+
+@media (max-width: 575.98px) {
+ .nav-links > li > a {
+ margin-right: 3px;
+ }
+}
+.media {
+ display: flex;
+ align-items: flex-start;
+}
+.card {
+ margin-bottom: 16px;
+}
+.nav-links:not(.quick-links) {
+ display: flex;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ height: auto;
+ border-bottom: 1px solid #dbdbdb;
+}
+.content-wrapper {
+ width: 100%;
+}
+.content-wrapper .container-fluid {
+ padding: 0 16px;
+}
+
+@media (min-width: 768px) {
+ .page-with-contextual-sidebar {
+ padding-left: 50px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .page-with-contextual-sidebar {
+ padding-left: 220px;
+ }
+}
+.context-header {
+ position: relative;
+ margin-right: 2px;
+ width: 220px;
+}
+.context-header > a,
+.context-header > button {
+ font-weight: 600;
+ display: flex;
+ width: 100%;
+ align-items: center;
+ padding: 10px 16px 10px 10px;
+ color: #303030;
+ background-color: transparent;
+ border: 0;
+ text-align: left;
+}
+.context-header .avatar-container {
+ flex: 0 0 40px;
+ background-color: #fff;
+}
+.context-header .sidebar-context-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.context-header .sidebar-context-title.text-secondary {
+ font-weight: normal;
+ font-size: 0.8em;
+}
+.nav-sidebar {
+ position: fixed;
+ z-index: 600;
+ width: 220px;
+ top: 40px;
+ bottom: 0;
+ left: 0;
+ background-color: #fafafa;
+ box-shadow: inset -1px 0 0 #dbdbdb;
+ transform: translate3d(0, 0, 0);
+}
+
+@media (min-width: 576px) and (max-width: 576px) {
+ .nav-sidebar:not(.sidebar-collapsed-desktop) {
+ box-shadow: inset -1px 0 0 #dbdbdb, 2px 1px 3px rgba(0, 0, 0, 0.1);
+ }
+}
+.nav-sidebar.sidebar-collapsed-desktop {
+ width: 50px;
+}
+.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
+ overflow-x: hidden;
+}
+.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge),
+.nav-sidebar.sidebar-collapsed-desktop .sidebar-context-title,
+.nav-sidebar.sidebar-collapsed-desktop .nav-item-name {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a {
+ min-height: 45px;
+}
+.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item {
+ display: block;
+}
+.nav-sidebar.sidebar-collapsed-desktop .avatar-container {
+ margin: 0 auto;
+}
+.nav-sidebar.sidebar-expanded-mobile {
+ left: 0;
+}
+.nav-sidebar a {
+ text-decoration: none;
+}
+.nav-sidebar ul {
+ padding-left: 0;
+ list-style: none;
+}
+.nav-sidebar li {
+ white-space: nowrap;
+}
+.nav-sidebar li a {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ color: #666;
+}
+.nav-sidebar li .nav-item-name {
+ flex: 1;
+}
+.nav-sidebar li.active > a {
+ font-weight: 600;
+}
+
+@media (max-width: 767.98px) {
+ .nav-sidebar {
+ left: -220px;
+ }
+}
+.nav-sidebar .nav-icon-container {
+ display: flex;
+ margin-right: 8px;
+}
+.nav-sidebar .fly-out-top-item {
+ display: none;
+}
+.nav-sidebar svg {
+ height: 16px;
+ width: 16px;
+}
+
+@media (min-width: 768px) and (max-width: 1199px) {
+ .nav-sidebar:not(.sidebar-expanded-mobile) {
+ width: 50px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
+ overflow-x: hidden;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .badge.badge-pill:not(.fly-out-badge),
+ .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title,
+ .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a {
+ min-height: 45px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item {
+ display: block;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container {
+ margin: 0 auto;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
+ height: 60px;
+ width: 50px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
+ padding: 10px 4px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) {
+ display: none;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container {
+ margin-right: 0;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
+ padding: 16px;
+ width: 49px;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .collapse-text,
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-left {
+ display: none;
+ }
+ .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-right {
+ display: block;
+ margin: 0;
+ }
+}
+.nav-sidebar-inner-scroll {
+ height: 100%;
+ width: 100%;
+ overflow: auto;
+}
+.sidebar-sub-level-items {
+ display: none;
+ padding-bottom: 8px;
+}
+.sidebar-sub-level-items > li a {
+ padding: 8px 16px 8px 40px;
+}
+.sidebar-top-level-items {
+ margin-bottom: 60px;
+}
+
+@media (min-width: 576px) {
+ .sidebar-top-level-items > li > a {
+ margin-right: 1px;
+ }
+}
+.sidebar-top-level-items > li .badge.badge-pill {
+ background-color: rgba(0, 0, 0, 0.08);
+ color: #666;
+}
+.sidebar-top-level-items > li.active {
+ background: rgba(0, 0, 0, 0.04);
+}
+.sidebar-top-level-items > li.active > a {
+ margin-left: 4px;
+ padding-left: 12px;
+}
+.sidebar-top-level-items > li.active .badge.badge-pill {
+ font-weight: 600;
+}
+.sidebar-top-level-items > li.active .sidebar-sub-level-items:not(.is-fly-out-only) {
+ display: block;
+}
+.toggle-sidebar-button,
+.close-nav-button {
+ width: 219px;
+ position: fixed;
+ height: 48px;
+ bottom: 0;
+ padding: 0 16px;
+ background-color: #fafafa;
+ border: 0;
+ border-top: 1px solid #dbdbdb;
+ color: #666;
+ display: flex;
+ align-items: center;
+}
+.toggle-sidebar-button svg,
+.close-nav-button svg {
+ margin-right: 8px;
+}
+.toggle-sidebar-button .icon-chevron-double-lg-right,
+.close-nav-button .icon-chevron-double-lg-right {
+ display: none;
+}
+.collapse-text {
+ white-space: nowrap;
+ overflow: hidden;
+}
+.sidebar-collapsed-desktop .context-header {
+ height: 60px;
+ width: 50px;
+}
+.sidebar-collapsed-desktop .context-header a {
+ padding: 10px 4px;
+}
+.sidebar-collapsed-desktop .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) {
+ display: none;
+}
+.sidebar-collapsed-desktop .nav-icon-container {
+ margin-right: 0;
+}
+.sidebar-collapsed-desktop .toggle-sidebar-button {
+ padding: 16px;
+ width: 49px;
+}
+.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text,
+.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left {
+ display: none;
+}
+.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-right {
+ display: block;
+ margin: 0;
+}
+.fly-out-top-item > a {
+ display: flex;
+}
+.fly-out-top-item .fly-out-badge {
+ margin-left: 8px;
+}
+.fly-out-top-item-name {
+ flex: 1;
+}
+.close-nav-button {
+ display: none;
+}
+
+@media (max-width: 767.98px) {
+ .close-nav-button {
+ display: flex;
+ }
+ .toggle-sidebar-button {
+ display: none;
+ }
+}
+table.table {
+ margin-bottom: 16px;
+}
+table.table .dropdown-menu a {
+ text-decoration: none;
+}
+table.table .success,
+table.table .info {
+ color: #fff;
+}
+table.table .success a:not(.btn),
+table.table .info a:not(.btn) {
+ text-decoration: underline;
+ color: #fff;
+}
+pre {
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ display: block;
+ padding: 8px 12px;
+ margin: 0 0 8px;
+ font-size: 13px;
+ word-break: break-all;
+ word-wrap: break-word;
+ color: #303030;
+ background-color: #fafafa;
+ border: 1px solid #dbdbdb;
+ border-radius: 2px;
+}
+.monospace {
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+}
+input::-moz-placeholder,
+textarea::-moz-placeholder {
+ color: #868686;
+ opacity: 1;
+}
+input::-ms-input-placeholder,
+textarea::-ms-input-placeholder {
+ color: #868686;
+}
+input:-ms-input-placeholder,
+textarea:-ms-input-placeholder {
+ color: #868686;
+}
+svg {
+ fill: currentColor;
+}
+
+svg.s12 {
+ width: 12px;
+ height: 12px;
+}
+
+svg.s16 {
+ width: 16px;
+ height: 16px;
+}
+
+svg.s18 {
+ width: 18px;
+ height: 18px;
+}
+
+svg.s12 {
+ vertical-align: -1px;
+}
+
+svg.s16 {
+ vertical-align: -3px;
+}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
+table.code {
+ width: 100%;
+ font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ border: 0;
+ border-collapse: separate;
+ margin: 0;
+ padding: 0;
+ table-layout: fixed;
+ border-radius: 0 0 4px 4px;
+}
+.frame .badge.badge-pill {
+ position: absolute;
+ background-color: #428fdc;
+ color: #fff;
+ border: #fff 1px solid;
+ min-height: 16px;
+ padding: 5px 8px;
+ border-radius: 12px;
+}
+.frame .badge.badge-pill {
+ transform: translate(-50%, -50%);
+}
+.color-label {
+ padding: 0 0.5rem;
+ line-height: 16px;
+ border-radius: 100px;
+ color: #fff;
+}
+.label-link {
+ display: inline-flex;
+ vertical-align: text-bottom;
+}
+.label-link .label {
+ vertical-align: inherit;
+ font-size: 12px;
+}
+.login-page .container {
+ max-width: 960px;
+}
+.login-page .navbar-gitlab .container {
+ max-width: none;
+}
+.login-page .flash-container {
+ margin-bottom: 16px;
+}
+.login-page .brand-holder {
+ font-size: 18px;
+ line-height: 1.5;
+}
+.login-page .brand-holder p {
+ font-size: 16px;
+ color: #888;
+}
+.login-page .brand-holder h3 {
+ font-size: 22px;
+}
+.login-page .brand-holder img {
+ max-width: 100%;
+ margin-bottom: 30px;
+}
+.login-page .brand-holder a {
+ font-weight: 600;
+}
+.login-page p {
+ font-size: 13px;
+}
+.login-page .login-box,
+.login-page .omniauth-container {
+ box-shadow: 0 0 0 1px #dbdbdb;
+ border-bottom-right-radius: 0.25rem;
+ border-bottom-left-radius: 0.25rem;
+ padding: 15px;
+}
+.login-page .login-box .nav .active a,
+.login-page .omniauth-container .nav .active a {
+ background: transparent;
+}
+.login-page .login-box .login-body,
+.login-page .omniauth-container .login-body {
+ font-size: 13px;
+}
+.login-page .login-box .login-body input + p,
+.login-page .login-box .login-body input ~ p.field-validation,
+.login-page .omniauth-container .login-body input + p,
+.login-page .omniauth-container .login-body input ~ p.field-validation {
+ margin-top: 5px;
+}
+.login-page .login-box .login-body .username .validation-success,
+.login-page .omniauth-container .login-body .username .validation-success {
+ color: #217645;
+}
+.login-page .login-box .login-body .username .validation-error,
+.login-page .omniauth-container .login-body .username .validation-error {
+ color: #dd2b0e;
+}
+.login-page .omniauth-container {
+ border-radius: 0.25rem;
+ font-size: 13px;
+}
+.login-page .omniauth-container p {
+ margin: 0;
+}
+.login-page .omniauth-container form {
+ width: 48%;
+ padding: 0;
+ border: 0;
+ background: none;
+ margin-bottom: 16px;
+}
+
+@media (max-width: 991.98px) {
+ .login-page .omniauth-container form {
+ width: 100%;
+ }
+}
+.login-page .omniauth-container .omniauth-btn {
+ width: 100%;
+ padding: 8px;
+}
+.login-page .omniauth-container .omniauth-btn img {
+ width: 1.125rem;
+ height: 1.125rem;
+ margin-right: 16px;
+}
+.login-page .new-session-tabs {
+ display: flex;
+ box-shadow: 0 0 0 1px #dbdbdb;
+ border-top-right-radius: 4px;
+ border-top-left-radius: 4px;
+}
+.login-page .new-session-tabs li {
+ flex: 1;
+ text-align: center;
+ border-left: 1px solid #dbdbdb;
+}
+.login-page .new-session-tabs li:first-of-type {
+ border-left: 0;
+ border-top-left-radius: 4px;
+}
+.login-page .new-session-tabs li:last-of-type {
+ border-top-right-radius: 4px;
+}
+.login-page .new-session-tabs li:not(.active) {
+ background-color: #fafafa;
+}
+.login-page .new-session-tabs li a {
+ width: 100%;
+ font-size: 18px;
+}
+.login-page .new-session-tabs li.active > a {
+ cursor: default;
+}
+.login-page .submit-container {
+ margin-top: 16px;
+}
+.login-page input[type='submit'] {
+ margin-bottom: 0;
+}
+.login-page .devise-errors h2 {
+ margin-top: 0;
+ font-size: 14px;
+ color: #ae1800;
+}
+.devise-layout-html {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+}
+.devise-layout-html body {
+ height: calc(100% - 51px);
+ margin: 0;
+ padding: 0;
+}
+.devise-layout-html body.navless {
+ height: calc(100% - 11px);
+}
+.devise-layout-html body .page-wrap {
+ min-height: 100%;
+ position: relative;
+}
+.devise-layout-html body .footer-container,
+.devise-layout-html body hr.footer-fixed {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 40px;
+ background: #fff;
+}
+.devise-layout-html body .login-page-broadcast {
+ margin-top: 40px;
+}
+.devise-layout-html body .navless-container {
+ padding: 65px 15px;
+}
+
+@media (max-width: 575.98px) {
+ .devise-layout-html body .navless-container {
+ padding: 0 15px 65px;
+ }
+}
+.milestones {
+ padding: 8px;
+ margin-top: 8px;
+ border-radius: 4px;
+ background-color: #dbdbdb;
+}
+.search {
+ margin: 0 8px;
+}
+.search form {
+ margin: 0;
+ padding: 4px;
+ width: 200px;
+ line-height: 24px;
+ height: 32px;
+ border: 0;
+ border-radius: 4px;
+}
+
+@media (min-width: 1200px) {
+ .search form {
+ width: 320px;
+ }
+}
+.search .search-input {
+ border: 0;
+ font-size: 14px;
+ padding: 0 20px 0 0;
+ margin-left: 5px;
+ line-height: 25px;
+ width: 98%;
+ color: #fff;
+ background: none;
+}
+.search .search-input-container {
+ display: flex;
+ position: relative;
+}
+.search .search-input-wrap {
+ width: 100%;
+}
+.search .search-input-wrap .search-icon,
+.search .search-input-wrap .clear-icon {
+ position: absolute;
+ right: 5px;
+ top: 4px;
+}
+.search .search-input-wrap .search-icon {
+ -moz-user-select: none;
+ user-select: none;
+}
+.search .search-input-wrap .clear-icon {
+ display: none;
+}
+.search .search-input-wrap .dropdown {
+ position: static;
+}
+.search .search-input-wrap .dropdown-menu {
+ left: -5px;
+ max-height: 400px;
+ overflow: auto;
+}
+
+@media (min-width: 1200px) {
+ .search .search-input-wrap .dropdown-menu {
+ width: 320px;
+ }
+}
+.search .search-input-wrap .dropdown-content {
+ max-height: 382px;
+}
+.settings {
+ border-top: 1px solid #dbdbdb;
+}
+.settings:first-of-type {
+ margin-top: 10px;
+ border: 0;
+}
+.settings + div .settings:first-of-type {
+ margin-top: 0;
+ border-top: 1px solid #dbdbdb;
+}
+.avatar, .avatar-container {
+ float: left;
+ margin-right: 16px;
+ border-radius: 50%;
+ border: 1px solid #f5f5f5;
+}
+.s16.avatar, .s16.avatar-container {
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+}
+.s18.avatar, .s18.avatar-container {
+ width: 18px;
+ height: 18px;
+ margin-right: 8px;
+}
+.s40.avatar, .s40.avatar-container {
+ width: 40px;
+ height: 40px;
+ margin-right: 8px;
+}
+.avatar {
+ transition-property: none;
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ background: #fdfdfd;
+ overflow: hidden;
+ border-color: rgba(0, 0, 0, 0.1);
+}
+.avatar.center {
+ font-size: 14px;
+ line-height: 1.8em;
+ text-align: center;
+}
+.avatar.avatar-tile {
+ border-radius: 0;
+ border: 0;
+}
+.avatar-container {
+ overflow: hidden;
+ display: flex;
+}
+.avatar-container a {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ text-decoration: none;
+}
+.avatar-container .avatar {
+ border-radius: 0;
+ border: 0;
+ height: auto;
+ width: 100%;
+ margin: 0;
+ align-self: center;
+}
+.avatar-container.s40 {
+ min-width: 40px;
+ min-height: 40px;
+}
+.rect-avatar {
+ border-radius: 2px;
+}
+.rect-avatar.s16 {
+ border-radius: 2px;
+}
+.rect-avatar.s18 {
+ border-radius: 2px;
+}
+.rect-avatar.s40 {
+ border-radius: 4px;
+}
+.tab-width-8 {
+ -moz-tab-size: 8;
+ tab-size: 8;
+}
+.gl-sr-only {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+.gl-mt-5 {
+ margin-top: 1rem;
+}
+.gl-ml-3 {
+ margin-left: 0.5rem;
+}
+.content-wrapper > .alert-wrapper,
+#content-body, .modal-dialog {
+ display: block;
+}
+@import 'cloaking';
+@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index e2b4d6b8e7a..bfbcb8c13c6 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -11,15 +11,6 @@ $gray-800: #f2f2f2;
$gray-900: #fafafa;
$gray-950: #fff;
-$gl-gray-100: #333;
-$gl-gray-200: #555;
-$gl-gray-350: #666;
-$gl-gray-400: #777;
-$gl-gray-500: #999;
-$gl-gray-600: #aaa;
-$gl-gray-700: #ccc;
-$gl-gray-800: #ddd;
-
$green-50: #072b15;
$green-100: #0a4020;
$green-200: #0e5a2d;
@@ -94,6 +85,86 @@ $white-light: #2b2b2b;
$white-normal: #333;
$white-dark: #444;
+$border-color: #4f4f4f;
+
+body.gl-dark {
+ --gray-10: #{$gray-10};
+ --gray-50: #{$gray-50};
+ --gray-100: #{$gray-100};
+ --gray-200: #{$gray-200};
+ --gray-300: #{$gray-300};
+ --gray-400: #{$gray-400};
+ --gray-500: #{$gray-500};
+ --gray-600: #{$gray-600};
+ --gray-700: #{$gray-700};
+ --gray-800: #{$gray-800};
+ --gray-900: #{$gray-900};
+ --gray-950: #{$gray-950};
+
+ --green-50: #{$green-50};
+ --green-100: #{$green-100};
+ --green-200: #{$green-200};
+ --green-300: #{$green-300};
+ --green-400: #{$green-400};
+ --green-500: #{$green-500};
+ --green-600: #{$green-600};
+ --green-700: #{$green-700};
+ --green-800: #{$green-800};
+ --green-900: #{$green-900};
+ --green-950: #{$green-950};
+
+ --blue-50: #{$blue-50};
+ --blue-100: #{$blue-100};
+ --blue-200: #{$blue-200};
+ --blue-300: #{$blue-300};
+ --blue-400: #{$blue-400};
+ --blue-500: #{$blue-500};
+ --blue-600: #{$blue-600};
+ --blue-700: #{$blue-700};
+ --blue-800: #{$blue-800};
+ --blue-900: #{$blue-900};
+ --blue-950: #{$blue-950};
+
+ --orange-50: #{$orange-50};
+ --orange-100: #{$orange-100};
+ --orange-200: #{$orange-200};
+ --orange-300: #{$orange-300};
+ --orange-400: #{$orange-400};
+ --orange-500: #{$orange-500};
+ --orange-600: #{$orange-600};
+ --orange-700: #{$orange-700};
+ --orange-800: #{$orange-800};
+ --orange-900: #{$orange-900};
+ --orange-950: #{$orange-950};
+
+ --red-50: #{$red-50};
+ --red-100: #{$red-100};
+ --red-200: #{$red-200};
+ --red-300: #{$red-300};
+ --red-400: #{$red-400};
+ --red-500: #{$red-500};
+ --red-600: #{$red-600};
+ --red-700: #{$red-700};
+ --red-800: #{$red-800};
+ --red-900: #{$red-900};
+ --red-950: #{$red-950};
+
+ --indigo-50: #{$indigo-50};
+ --indigo-100: #{$indigo-100};
+ --indigo-200: #{$indigo-200};
+ --indigo-300: #{$indigo-300};
+ --indigo-400: #{$indigo-400};
+ --indigo-500: #{$indigo-500};
+ --indigo-600: #{$indigo-600};
+ --indigo-700: #{$indigo-700};
+ --indigo-800: #{$indigo-800};
+ --indigo-900: #{$indigo-900};
+ --indigo-950: #{$indigo-950};
+
+ --gl-text-color: #{$gray-900};
+ --border-color: #{$border-color};
+}
+
$border-white-light: $gray-900;
$border-white-normal: $gray-900;
diff --git a/app/assets/stylesheets/themes/theme_blue.scss b/app/assets/stylesheets/themes/theme_blue.scss
new file mode 100644
index 00000000000..9f9802f77f4
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_blue.scss
@@ -0,0 +1,14 @@
+@import './theme_helper';
+
+body {
+ &.ui-blue {
+ @include gitlab-theme(
+ $theme-blue-200,
+ $theme-blue-500,
+ $theme-blue-700,
+ $theme-blue-800,
+ $theme-blue-900,
+ $white
+ );
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_dark.scss b/app/assets/stylesheets/themes/theme_dark.scss
new file mode 100644
index 00000000000..e6db6cd2a5e
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_dark.scss
@@ -0,0 +1,14 @@
+@import './theme_helper';
+
+body {
+ &.ui-dark {
+ @include gitlab-theme(
+ $gray-200,
+ $gray-300,
+ $gray-500,
+ $gray-700,
+ $gray-900,
+ $white
+ );
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_green.scss b/app/assets/stylesheets/themes/theme_green.scss
new file mode 100644
index 00000000000..6dcad6e1301
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_green.scss
@@ -0,0 +1,14 @@
+@import './theme_helper';
+
+body {
+ &.ui-green {
+ @include gitlab-theme(
+ $theme-green-200,
+ $theme-green-500,
+ $theme-green-700,
+ $theme-green-800,
+ $theme-green-900,
+ $white
+ );
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
new file mode 100644
index 00000000000..85115cfd5d9
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -0,0 +1,204 @@
+@import '../page_bundles/mixins_and_variables_and_functions';
+/**
+ * Styles the GitLab application with a specific color theme
+ */
+@mixin gitlab-theme(
+ $search-and-nav-links,
+ $active-tab-border,
+ $border-and-box-shadow,
+ $sidebar-text,
+ $nav-svg-color,
+ $color-alternate
+) {
+ // Header
+
+ .navbar-gitlab {
+ background-color: $nav-svg-color;
+
+ .navbar-collapse {
+ color: $search-and-nav-links;
+ }
+
+ .container-fluid {
+ .navbar-toggler {
+ border-left: 1px solid lighten($border-and-box-shadow, 10%);
+
+ svg {
+ fill: $search-and-nav-links;
+ }
+ }
+ }
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a,
+ > button {
+ &:hover,
+ &:focus {
+ background-color: rgba($search-and-nav-links, 0.2);
+ }
+ }
+
+ &.active,
+ &.dropdown.show {
+ > a,
+ > button {
+ color: $nav-svg-color;
+ background-color: $color-alternate;
+ }
+ }
+
+ &.line-separator {
+ border-left: 1px solid rgba($search-and-nav-links, 0.2);
+ }
+ }
+ }
+
+ .navbar-sub-nav {
+ color: $search-and-nav-links;
+ }
+
+ .nav {
+ > li {
+ color: $search-and-nav-links;
+
+ > a {
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $search-and-nav-links;
+ }
+
+ .header-user-notification-dot {
+ border: 2px solid $nav-svg-color;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ @include media-breakpoint-up(sm) {
+ background-color: rgba($search-and-nav-links, 0.2);
+ }
+
+ svg {
+ fill: currentColor;
+ }
+
+ &.header-user-dropdown-toggle .header-user-notification-dot {
+ border-color: $nav-svg-color + 33;
+ }
+ }
+ }
+
+ &.active > a,
+ &.dropdown.show > a {
+ color: $nav-svg-color;
+ background-color: $color-alternate;
+
+ &:hover {
+ svg {
+ fill: $nav-svg-color;
+ }
+ }
+
+ &.header-user-dropdown-toggle .header-user-notification-dot {
+ border-color: $white;
+ }
+ }
+
+ .impersonated-user,
+ .impersonated-user:hover {
+ svg {
+ fill: $nav-svg-color;
+ }
+ }
+ }
+ }
+ }
+
+ .navbar .title {
+ > a {
+ &:hover,
+ &:focus {
+ background-color: rgba($search-and-nav-links, 0.2);
+ }
+ }
+ }
+
+ .search {
+ form {
+ background-color: rgba($search-and-nav-links, 0.2);
+
+ &:hover {
+ background-color: rgba($search-and-nav-links, 0.3);
+ }
+ }
+
+ .search-input::placeholder {
+ color: rgba($search-and-nav-links, 0.8);
+ }
+
+ .search-input-wrap {
+ .search-icon,
+ .clear-icon {
+ fill: rgba($search-and-nav-links, 0.8);
+ }
+ }
+
+ &.search-active {
+ form {
+ background-color: $white;
+ }
+
+ .search-input-wrap {
+ .search-icon {
+ fill: rgba($search-and-nav-links, 0.8);
+ }
+ }
+ }
+ }
+
+ // Sidebar
+ .nav-sidebar li.active {
+ box-shadow: inset 4px 0 0 $border-and-box-shadow;
+
+ > a {
+ color: $sidebar-text;
+ }
+
+ .nav-icon-container svg {
+ fill: $sidebar-text;
+ }
+ }
+
+ .sidebar-top-level-items > li.active .badge.badge-pill {
+ color: $sidebar-text;
+ }
+
+ .nav-links li {
+ &.active a,
+ &.md-header-tab.active button,
+ a.active {
+ border-bottom: 2px solid $active-tab-border;
+
+ .badge.badge-pill {
+ font-weight: $gl-font-weight-bold;
+ }
+ }
+ }
+
+ .branch-header-title {
+ color: $border-and-box-shadow;
+ }
+
+ .ide-sidebar-link {
+ &.active {
+ color: $border-and-box-shadow;
+ box-shadow: inset 3px 0 $border-and-box-shadow;
+
+ &.is-right {
+ box-shadow: inset -3px 0 $border-and-box-shadow;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_indigo.scss b/app/assets/stylesheets/themes/theme_indigo.scss
new file mode 100644
index 00000000000..bbf14afcca2
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_indigo.scss
@@ -0,0 +1,14 @@
+@import './theme_helper';
+
+body {
+ &.ui-indigo {
+ @include gitlab-theme(
+ $indigo-200,
+ $indigo-500,
+ $indigo-700,
+ $indigo-800,
+ $indigo-900,
+ $white
+ );
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_light.scss b/app/assets/stylesheets/themes/theme_light.scss
new file mode 100644
index 00000000000..58003db4236
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_light.scss
@@ -0,0 +1,129 @@
+@import './theme_helper';
+
+body {
+ &.ui-light {
+ @include gitlab-theme(
+ $gray-500,
+ $gray-700,
+ $gray-500,
+ $gray-500,
+ $gray-50,
+ $gray-500
+ );
+
+ .navbar-gitlab {
+ background-color: $gray-50;
+ box-shadow: 0 1px 0 0 $border-color;
+
+ .logo-text svg {
+ fill: $gray-900;
+ }
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus,
+ > button:hover {
+ color: $gray-900;
+ }
+
+ &.active > a,
+ &.active > a:hover,
+ &.active > button {
+ color: $white;
+ }
+ }
+ }
+
+ .container-fluid {
+ .navbar-toggler,
+ .navbar-toggler:hover {
+ color: $gray-500;
+ border-left: 1px solid $gray-100;
+ }
+ }
+ }
+
+ .search {
+ form {
+ background-color: $white;
+ box-shadow: inset 0 0 0 1px $border-color;
+
+ &:hover {
+ background-color: $white;
+ box-shadow: inset 0 0 0 1px $blue-200;
+ }
+ }
+
+ .search-input-wrap {
+ .search-icon {
+ fill: $gray-100;
+ }
+
+ .search-input {
+ color: $gl-text-color;
+ }
+ }
+ }
+
+ .nav-sidebar li.active {
+ > a {
+ color: $gray-900;
+ }
+
+ svg {
+ fill: $gray-900;
+ }
+ }
+
+ .sidebar-top-level-items > li.active .badge.badge-pill {
+ color: $gray-900;
+ }
+ }
+
+ &.gl-dark {
+ .logo-text svg {
+ fill: $gl-text-color;
+ }
+
+ .navbar-gitlab {
+ background-color: $gray-50;
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ li {
+ > a:hover,
+ > a:focus,
+ > button:hover,
+ > button:focus {
+ color: $gl-text-color;
+ background-color: $gray-200;
+ }
+ }
+
+ li.active,
+ li.dropdown.show {
+ > a,
+ > button {
+ color: $gl-text-color;
+ background-color: $gray-200;
+ }
+ }
+ }
+
+ .search {
+ form {
+ background-color: $gray-100;
+ box-shadow: inset 0 0 0 1px $border-color;
+
+ &:active,
+ &:hover {
+ background-color: $gray-100;
+ box-shadow: inset 0 0 0 1px $blue-200;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_light_blue.scss b/app/assets/stylesheets/themes/theme_light_blue.scss
new file mode 100644
index 00000000000..07d1c60a4c6
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_light_blue.scss
@@ -0,0 +1,14 @@
+@import './theme_helper';
+
+body {
+ &.ui-light-blue {
+ @include gitlab-theme(
+ $theme-light-blue-200,
+ $theme-light-blue-500,
+ $theme-light-blue-500,
+ $theme-light-blue-700,
+ $theme-light-blue-700,
+ $white
+ );
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss
new file mode 100644
index 00000000000..e122501b93c
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_light_green.scss
@@ -0,0 +1,14 @@
+@import './theme_helper';
+
+body {
+ &.ui-light-green {
+ @include gitlab-theme(
+ $theme-green-200,
+ $theme-green-500,
+ $theme-green-500,
+ $theme-light-green-700,
+ $theme-light-green-700,
+ $white
+ );
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_light_indigo.scss b/app/assets/stylesheets/themes/theme_light_indigo.scss
new file mode 100644
index 00000000000..5b607238ed9
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_light_indigo.scss
@@ -0,0 +1,14 @@
+@import './theme_helper';
+
+body {
+ &.ui-light-indigo {
+ @include gitlab-theme(
+ $indigo-200,
+ $indigo-500,
+ $indigo-500,
+ $indigo-700,
+ $indigo-700,
+ $white
+ );
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_light_red.scss b/app/assets/stylesheets/themes/theme_light_red.scss
new file mode 100644
index 00000000000..fd3980183f3
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_light_red.scss
@@ -0,0 +1,14 @@
+@import './theme_helper';
+
+body {
+ &.ui-light-red {
+ @include gitlab-theme(
+ $theme-light-red-200,
+ $theme-light-red-500,
+ $theme-light-red-500,
+ $theme-light-red-700,
+ $theme-light-red-700,
+ $white
+ );
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_red.scss b/app/assets/stylesheets/themes/theme_red.scss
new file mode 100644
index 00000000000..fa5ecc09f50
--- /dev/null
+++ b/app/assets/stylesheets/themes/theme_red.scss
@@ -0,0 +1,14 @@
+@import './theme_helper';
+
+body {
+ &.ui-red {
+ @include gitlab-theme(
+ $theme-red-200,
+ $theme-red-500,
+ $theme-red-700,
+ $theme-red-800,
+ $theme-red-900,
+ $white
+ );
+ }
+}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 99a13cc4e44..9c666331c4f 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -112,10 +112,47 @@
top: 66vh;
}
-// Remove when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/871
-// gets fixed on GitLab UI
-.gl-sm-w-auto\! {
+.gl-shadow-x0-y0-b3-s1-blue-500 {
+ box-shadow: inset 0 0 3px $gl-border-size-1 $blue-500;
+}
+
+
+.gl-sm-align-items-flex-end {
+ @media (min-width: $breakpoint-sm) {
+ align-items: flex-end;
+ }
+}
+
+.gl-sm-text-body {
+ @media (min-width: $breakpoint-sm) {
+ color: $body-color;
+ }
+}
+
+.gl-sm-font-weight-bold {
@media (min-width: $breakpoint-sm) {
+ font-weight: $gl-font-weight-bold;
+ }
+}
+
+.gl-min-h-6 {
+ min-height: $gl-spacing-scale-6;
+}
+
+.gl-md-justify-content-end {
+ @media (min-width: $breakpoint-md) {
width: auto !important;
}
}
+
+.gl-display-md-flex {
+ @media (min-width: $breakpoint-md) {
+ display: flex;
+ }
+}
+
+.gl-display-md-none {
+ @media (min-width: $breakpoint-md) {
+ display: none;
+ }
+}
diff --git a/app/assets/stylesheets/vendors/atwho.scss b/app/assets/stylesheets/vendors/atwho.scss
index f855c5c0d3d..f31dbbeafe8 100644
--- a/app/assets/stylesheets/vendors/atwho.scss
+++ b/app/assets/stylesheets/vendors/atwho.scss
@@ -24,8 +24,8 @@
.has-warning {
.description {
- color: $orange-700;
- background-color: $orange-100;
+ color: $gray-900;
+ background-color: $orange-50;
}
}
@@ -58,7 +58,7 @@
}
&.has-warning {
- color: $orange-700;
+ color: $orange-500;
}
}
diff --git a/app/assets/stylesheets/vendors/tribute.scss b/app/assets/stylesheets/vendors/tribute.scss
index 309cdf7245c..65f3d1b6199 100644
--- a/app/assets/stylesheets/vendors/tribute.scss
+++ b/app/assets/stylesheets/vendors/tribute.scss
@@ -1,6 +1,6 @@
.tribute-container {
background: $white;
- border: 1px solid $gl-gray-100;
+ border: 1px solid $gray-100;
border-radius: $border-radius-base;
box-shadow: 0 0 5px $issue-boards-card-shadow;
color: $black;
@@ -22,7 +22,7 @@
white-space: nowrap;
small {
- color: $gl-gray-500;
+ color: $gray-500;
}
&.highlight {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 3a5b8b2862e..73f71f7ad55 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -32,7 +32,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def integrations
- @integrations = Service.find_or_initialize_instances.sort_by(&:title)
+ @integrations = Service.find_or_initialize_all(Service.for_instance).sort_by(&:title)
end
def update
@@ -170,6 +170,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def set_application_setting
@application_setting = ApplicationSetting.current_without_cache
+ @plans = Plan.all
end
def whitelist_query_limiting
diff --git a/app/controllers/instance_statistics/cohorts_controller.rb b/app/controllers/admin/cohorts_controller.rb
index 0de62a56b01..e3df98b7917 100644
--- a/app/controllers/instance_statistics/cohorts_controller.rb
+++ b/app/controllers/admin/cohorts_controller.rb
@@ -1,10 +1,8 @@
# frozen_string_literal: true
-class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController
+class Admin::CohortsController < Admin::ApplicationController
include Analytics::UniqueVisitsHelper
- before_action :authenticate_usage_ping_enabled_or_admin!
-
track_unique_visits :index, target_id: 'i_analytics_cohorts'
def index
@@ -16,8 +14,4 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon
@cohorts = CohortsSerializer.new.represent(cohorts_results)
end
end
-
- def authenticate_usage_ping_enabled_or_admin!
- render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin?
- end
end
diff --git a/app/controllers/admin/concerns/authenticates_2fa_for_admin_mode.rb b/app/controllers/admin/concerns/authenticates_2fa_for_admin_mode.rb
index 6014ed0dd13..03783cd75a3 100644
--- a/app/controllers/admin/concerns/authenticates_2fa_for_admin_mode.rb
+++ b/app/controllers/admin/concerns/authenticates_2fa_for_admin_mode.rb
@@ -11,7 +11,13 @@ module Authenticates2FAForAdminMode
return handle_locked_user(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
- setup_u2f_authentication(user)
+ push_frontend_feature_flag(:webauthn)
+
+ if user.two_factor_webauthn_enabled?
+ setup_webauthn_authentication(user)
+ else
+ setup_u2f_authentication(user)
+ end
render 'admin/sessions/two_factor', layout: 'application'
end
@@ -24,7 +30,11 @@ module Authenticates2FAForAdminMode
if user_params[:otp_attempt].present? && session[:otp_user_id]
admin_mode_authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
- admin_mode_authenticate_with_two_factor_via_u2f(user)
+ if user.two_factor_webauthn_enabled?
+ admin_mode_authenticate_with_two_factor_via_webauthn(user)
+ else
+ admin_mode_authenticate_with_two_factor_via_u2f(user)
+ end
elsif user && user.valid_password?(user_params[:password])
admin_mode_prompt_for_two_factor(user)
else
@@ -52,18 +62,17 @@ module Authenticates2FAForAdminMode
def admin_mode_authenticate_with_two_factor_via_u2f(user)
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
- # Remove any lingering user data from login
- session.delete(:otp_user_id)
- session.delete(:challenge)
-
- # The admin user has successfully passed 2fa, enable admin mode ignoring password
- enable_admin_mode
+ admin_handle_two_factor_success
else
- user.increment_failed_attempts!
- Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=U2F")
- flash.now[:alert] = _('Authentication via U2F device failed.')
+ admin_handle_two_factor_failure(user, 'U2F')
+ end
+ end
- admin_mode_prompt_for_two_factor(user)
+ def admin_mode_authenticate_with_two_factor_via_webauthn(user)
+ if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute
+ admin_handle_two_factor_success
+ else
+ admin_handle_two_factor_failure(user, 'WebAuthn')
end
end
@@ -81,4 +90,21 @@ module Authenticates2FAForAdminMode
flash.now[:alert] = _('Invalid login or password')
render :new
end
+
+ def admin_handle_two_factor_success
+ # Remove any lingering user data from login
+ session.delete(:otp_user_id)
+ session.delete(:challenge)
+
+ # The admin user has successfully passed 2fa, enable admin mode ignoring password
+ enable_admin_mode
+ end
+
+ def admin_handle_two_factor_failure(user, method)
+ user.increment_failed_attempts!
+ Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}")
+ flash.now[:alert] = _('Authentication via %{method} device failed.') % { method: method }
+
+ admin_mode_prompt_for_two_factor(user)
+ end
end
diff --git a/app/controllers/admin/dev_ops_report_controller.rb b/app/controllers/admin/dev_ops_report_controller.rb
new file mode 100644
index 00000000000..bed0d51c331
--- /dev/null
+++ b/app/controllers/admin/dev_ops_report_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class Admin::DevOpsReportController < Admin::ApplicationController
+ include Analytics::UniqueVisitsHelper
+
+ track_unique_visits :show, target_id: 'i_analytics_dev_ops_score'
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def show
+ @metric = DevOpsReport::Metric.order(:created_at).last&.present
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 0245c00aacb..6414792dd43 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -19,7 +19,7 @@ class Admin::GroupsController < Admin::ApplicationController
# the Group with statistics).
@group = Group.with_statistics.find(group&.id)
@members = present_members(
- @group.members.order("access_level DESC").page(params[:members_page]))
+ group_members.order("access_level DESC").page(params[:members_page]))
@requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user))
@projects = @group.projects.with_statistics.page(params[:projects_page])
@@ -82,6 +82,10 @@ class Admin::GroupsController < Admin::ApplicationController
@group ||= Group.find_by_full_path(params[:id])
end
+ def group_members
+ @group.members
+ end
+
def group_params
params.require(:group).permit(allowed_group_params)
end
diff --git a/app/controllers/admin/instance_statistics_controller.rb b/app/controllers/admin/instance_statistics_controller.rb
new file mode 100644
index 00000000000..3aee26b97a2
--- /dev/null
+++ b/app/controllers/admin/instance_statistics_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Admin::InstanceStatisticsController < Admin::ApplicationController
+ include Analytics::UniqueVisitsHelper
+
+ before_action :check_feature_flag
+
+ track_unique_visits :index, target_id: 'i_analytics_instance_statistics'
+
+ def index
+ end
+
+ def check_feature_flag
+ render_404 unless Feature.enabled?(:instance_statistics)
+ end
+end
diff --git a/app/controllers/admin/integrations_controller.rb b/app/controllers/admin/integrations_controller.rb
index b2d5a2d130c..1e2a99f7078 100644
--- a/app/controllers/admin/integrations_controller.rb
+++ b/app/controllers/admin/integrations_controller.rb
@@ -6,9 +6,7 @@ class Admin::IntegrationsController < Admin::ApplicationController
private
def find_or_initialize_integration(name)
- if name.in?(Service.available_services_names)
- "#{name}_service".camelize.constantize.find_or_initialize_by(instance: true) # rubocop:disable CodeReuse/ActiveRecord
- end
+ Service.find_or_initialize_integration(name, instance: true)
end
def integrations_enabled?
diff --git a/app/controllers/admin/plan_limits_controller.rb b/app/controllers/admin/plan_limits_controller.rb
new file mode 100644
index 00000000000..2620db8aec5
--- /dev/null
+++ b/app/controllers/admin/plan_limits_controller.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class Admin::PlanLimitsController < Admin::ApplicationController
+ include InternalRedirect
+
+ before_action :set_plan_limits
+
+ def create
+ redirect_path = referer_path(request) || general_admin_application_settings_path
+
+ respond_to do |format|
+ if @plan_limits.update(plan_limits_params)
+ format.json { head :ok }
+ format.html { redirect_to redirect_path, notice: _('Application limits saved successfully') }
+ else
+ format.json { head :bad_request }
+ format.html { render_update_error }
+ end
+ end
+ end
+
+ private
+
+ def set_plan_limits
+ @plan_limits = Plan.find(plan_limits_params[:plan_id]).actual_limits
+ end
+
+ def plan_limits_params
+ params.require(:plan_limits).permit(%i[
+ plan_id
+ conan_max_file_size
+ maven_max_file_size
+ npm_max_file_size
+ nuget_max_file_size
+ pypi_max_file_size
+ generic_packages_max_file_size
+ ])
+ end
+end
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 2449fa3128c..7a377a33d41 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -17,7 +17,6 @@ class Admin::RunnersController < Admin::ApplicationController
def update
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
respond_to do |format|
- format.js
format.html { redirect_to admin_runner_path(@runner) }
end
else
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 1bc82e98ab8..1f4250639c4 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -8,7 +8,7 @@ class Admin::ServicesController < Admin::ApplicationController
def index
@services = Service.find_or_create_templates.sort_by(&:title)
- @existing_instance_types = Service.instances.pluck(:type) # rubocop: disable CodeReuse/ActiveRecord
+ @existing_instance_types = Service.for_instance.pluck(:type) # rubocop: disable CodeReuse/ActiveRecord
end
def edit
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index fc0acd8f99a..050f83edacb 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -111,10 +111,14 @@ class Admin::UsersController < Admin::ApplicationController
end
def disable_two_factor
- update_user { |user| user.disable_two_factor! }
+ result = TwoFactor::DestroyService.new(current_user, user: user).execute
- redirect_to admin_user_path(user),
- notice: _('Two-factor Authentication has been disabled for this user')
+ if result[:status] == :success
+ redirect_to admin_user_path(user),
+ notice: _('Two-factor authentication has been disabled for this user')
+ else
+ redirect_to admin_user_path(user), alert: result[:message]
+ end
end
def create
@@ -145,7 +149,7 @@ class Admin::UsersController < Admin::ApplicationController
password_confirmation: params[:user][:password_confirmation]
}
- password_params[:password_expires_at] = Time.current unless changing_own_password?
+ password_params[:password_expires_at] = Time.current if admin_making_changes_for_another_user?
user_params_with_pass.merge!(password_params)
end
@@ -153,6 +157,7 @@ class Admin::UsersController < Admin::ApplicationController
respond_to do |format|
result = Users::UpdateService.new(current_user, user_params_with_pass.merge(user: user)).execute do |user|
user.skip_reconfirmation!
+ user.send_only_admin_changed_your_password_notification! if admin_making_changes_for_another_user?
end
if result[:status] == :success
@@ -193,8 +198,8 @@ class Admin::UsersController < Admin::ApplicationController
protected
- def changing_own_password?
- user == current_user
+ def admin_making_changes_for_another_user?
+ user != current_user
end
def user
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 2595b646964..5f05337e59e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -551,13 +551,9 @@ class ApplicationController < ActionController::Base
"#{self.class.name}##{action_name}"
end
- # A user requires a role and have the setup_for_company attribute set when they are part of the experimental signup
- # flow (executed by the Growth team). Users are redirected to the welcome page when their role is required and the
- # experiment is enabled for the current user.
def required_signup_info
return unless current_user
return unless current_user.role_required?
- return unless experiment_enabled?(:signup_flow)
store_location_for :user, request.fullpath
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index cae9d098799..7006c23321c 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -237,6 +237,7 @@ class Clusters::ClustersController < Clusters::BaseController
:environment_scope,
:managed,
provider_aws_attributes: [
+ :kubernetes_version,
:key_name,
:role_arn,
:region,
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index b93c98a4790..9ff97f398f5 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -22,9 +22,15 @@ module AuthenticatesWithTwoFactor
return handle_locked_user(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
- session[:user_updated_at] = user.updated_at
+ session[:user_password_hash] = Digest::SHA256.hexdigest(user.encrypted_password)
+ push_frontend_feature_flag(:webauthn)
+
+ if user.two_factor_webauthn_enabled?
+ setup_webauthn_authentication(user)
+ else
+ setup_u2f_authentication(user)
+ end
- setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
@@ -41,12 +47,16 @@ module AuthenticatesWithTwoFactor
def authenticate_with_two_factor
user = self.resource = find_user
return handle_locked_user(user) unless user.can?(:log_in)
- return handle_changed_user(user) if user_changed?(user)
+ return handle_changed_user(user) if user_password_changed?(user)
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
- authenticate_with_two_factor_via_u2f(user)
+ if user.two_factor_webauthn_enabled?
+ authenticate_with_two_factor_via_webauthn(user)
+ else
+ authenticate_with_two_factor_via_u2f(user)
+ end
elsif user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
@@ -66,7 +76,7 @@ module AuthenticatesWithTwoFactor
def clear_two_factor_attempt!
session.delete(:otp_user_id)
- session.delete(:user_updated_at)
+ session.delete(:user_password_hash)
session.delete(:challenge)
end
@@ -89,16 +99,17 @@ module AuthenticatesWithTwoFactor
# Authenticate using the response from a U2F (universal 2nd factor) device
def authenticate_with_two_factor_via_u2f(user)
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
- # Remove any lingering user data from login
- clear_two_factor_attempt!
+ handle_two_factor_success(user)
+ else
+ handle_two_factor_failure(user, 'U2F')
+ end
+ end
- remember_me(user) if user_params[:remember_me] == '1'
- sign_in(user, message: :two_factor_authenticated, event: :authentication)
+ def authenticate_with_two_factor_via_webauthn(user)
+ if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute
+ handle_two_factor_success(user)
else
- user.increment_failed_attempts!
- Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F")
- flash.now[:alert] = _('Authentication via U2F device failed.')
- prompt_for_two_factor(user)
+ handle_two_factor_failure(user, 'WebAuthn')
end
end
@@ -116,8 +127,38 @@ module AuthenticatesWithTwoFactor
sign_requests: sign_requests })
end
end
+
+ def setup_webauthn_authentication(user)
+ if user.webauthn_registrations.present?
+
+ webauthn_registration_ids = user.webauthn_registrations.pluck(:credential_xid)
+
+ get_options = WebAuthn::Credential.options_for_get(allow: webauthn_registration_ids,
+ user_verification: 'discouraged',
+ extensions: { appid: WebAuthn.configuration.origin })
+
+ session[:credentialRequestOptions] = get_options
+ session[:challenge] = get_options.challenge
+ gon.push(webauthn: { options: get_options.to_json })
+ end
+ end
# rubocop: enable CodeReuse/ActiveRecord
+ def handle_two_factor_success(user)
+ # Remove any lingering user data from login
+ clear_two_factor_attempt!
+
+ remember_me(user) if user_params[:remember_me] == '1'
+ sign_in(user, message: :two_factor_authenticated, event: :authentication)
+ end
+
+ def handle_two_factor_failure(user, method)
+ user.increment_failed_attempts!
+ Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}")
+ flash.now[:alert] = _('Authentication via %{method} device failed.') % { method: method }
+ prompt_for_two_factor(user)
+ end
+
def handle_changed_user(user)
clear_two_factor_attempt!
@@ -126,13 +167,9 @@ module AuthenticatesWithTwoFactor
# If user has been updated since we validated the password,
# the password might have changed.
- def user_changed?(user)
- return false unless session[:user_updated_at]
-
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/244638
- # Rounding errors happen when the user is updated, as the Rails ActiveRecord
- # object has higher precision than what is stored in the database, therefore
- # using .to_i to force truncation to the timestamp
- user.updated_at.to_i != session[:user_updated_at].to_i
+ def user_password_changed?(user)
+ return false unless session[:user_password_hash]
+
+ Digest::SHA256.hexdigest(user.encrypted_password) != session[:user_password_hash]
end
end
diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb
index 9a8e5d14123..6060dc729af 100644
--- a/app/controllers/concerns/integrations_actions.rb
+++ b/app/controllers/concerns/integrations_actions.rb
@@ -16,12 +16,11 @@ module IntegrationsActions
def update
saved = integration.update(service_params[:service])
- overwrite = Gitlab::Utils.to_boolean(params[:overwrite])
respond_to do |format|
format.html do
if saved
- PropagateIntegrationWorker.perform_async(integration.id, overwrite)
+ PropagateIntegrationWorker.perform_async(integration.id, false)
redirect_to scoped_edit_integration_path(integration), notice: success_message
else
render 'shared/integrations/edit'
@@ -57,9 +56,11 @@ module IntegrationsActions
end
def success_message
- message = integration.active? ? _('activated') : _('settings saved, but not activated')
-
- _('%{service_title} %{message}.') % { service_title: integration.title, message: message }
+ if integration.active?
+ s_('Integrations|%{integration} settings saved and active.') % { integration: integration.title }
+ else
+ s_('Integrations|%{integration} settings saved, but not active.') % { integration: integration.title }
+ end
end
def serialize_as_json
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index c4dbce00593..a1a2740cde2 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -9,7 +9,7 @@ module IssuableActions
before_action :check_destroy_confirmation!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
before_action only: :show do
- push_frontend_feature_flag(:scoped_labels, default_enabled: true)
+ push_frontend_feature_flag(:scoped_labels, type: :licensed, default_enabled: true)
end
before_action do
push_frontend_feature_flag(:not_issuable_queries, @project, default_enabled: true)
diff --git a/app/controllers/concerns/issuable_links.rb b/app/controllers/concerns/issuable_links.rb
new file mode 100644
index 00000000000..2bdb190f1d5
--- /dev/null
+++ b/app/controllers/concerns/issuable_links.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module IssuableLinks
+ def index
+ render json: issuables
+ end
+
+ def create
+ result = create_service.execute
+
+ render json: { message: result[:message], issuables: issuables }, status: result[:http_status]
+ end
+
+ def destroy
+ result = destroy_service.execute
+
+ render json: { issuables: issuables }, status: result[:http_status]
+ end
+
+ private
+
+ def issuables
+ list_service.execute
+ end
+
+ def list_service
+ raise NotImplementedError
+ end
+
+ def create_params
+ params.permit(issuable_references: [])
+ end
+
+ def create_service
+ raise NotImplementedError
+ end
+
+ def destroy_service
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index f4fc7decb60..7a5b470f366 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -100,7 +100,7 @@ module NotesActions
# the finder. Here, we select between returning all notes since then, or a
# page's worth of notes.
def gather_notes
- if Feature.enabled?(:paginated_notes, project)
+ if Feature.enabled?(:paginated_notes, noteable.try(:resource_parent))
gather_some_notes
else
gather_all_notes
diff --git a/app/controllers/concerns/redis_tracking.rb b/app/controllers/concerns/redis_tracking.rb
new file mode 100644
index 00000000000..fa5eef981d1
--- /dev/null
+++ b/app/controllers/concerns/redis_tracking.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Example:
+#
+# # In controller include module
+# # Track event for index action
+#
+# include RedisTracking
+#
+# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :my_feature
+#
+# if the feature flag is enabled by default you should use
+# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :my_feature, feature_default_enabled: true
+module RedisTracking
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def track_redis_hll_event(*controller_actions, name:, feature:, feature_default_enabled: false)
+ after_action only: controller_actions, if: -> { request.format.html? && request.headers['DNT'] != '1' } do
+ track_unique_redis_hll_event(name, feature, feature_default_enabled)
+ end
+ end
+ end
+
+ private
+
+ def track_unique_redis_hll_event(event_name, feature, feature_default_enabled)
+ return unless metric_feature_enabled?(feature, feature_default_enabled)
+ return unless Gitlab::CurrentSettings.usage_ping_enabled?
+ return unless visitor_id
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(visitor_id, event_name)
+ end
+
+ def metric_feature_enabled?(feature, default_enabled)
+ Feature.enabled?(feature, default_enabled: default_enabled)
+ end
+
+ def visitor_id
+ return cookies[:visitor_id] if cookies[:visitor_id].present?
+ return unless current_user
+
+ uuid = SecureRandom.uuid
+ cookies[:visitor_id] = { value: uuid, expires: 24.months }
+ uuid
+ end
+end
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index 18015b1de88..f8e3717acee 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -5,7 +5,6 @@ module RendersNotes
def prepare_notes_for_rendering(notes, noteable = nil)
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
- preload_first_time_contribution_for_authors(noteable, notes)
preload_author_status(notes)
Notes::RenderService.new(current_user).execute(notes)
@@ -19,7 +18,8 @@ module RendersNotes
return unless project
user_ids = notes.map(&:author_id)
- project.team.max_member_access_for_user_ids(user_ids)
+ access = project.team.max_member_access_for_user_ids(user_ids).select { |k, v| v == Gitlab::Access::NO_ACCESS }.keys
+ project.team.contribution_check_for_user_ids(access)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -28,12 +28,6 @@ module RendersNotes
end
# rubocop: enable CodeReuse/ActiveRecord
- def preload_first_time_contribution_for_authors(noteable, notes)
- return unless noteable.is_a?(Issuable) && noteable.first_contribution?
-
- notes.each {|n| n.specialize_for_first_contribution!(noteable)}
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def preload_author_status(notes)
ActiveRecord::Associations::Preloader.new.preload(notes, { author: :status })
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 7cb19fc7e58..2f06cd84ee5 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -2,6 +2,8 @@
module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, proxy: false, disposition: 'attachment')
+ content_type = content_type_for(attachment)
+
if attachment
response_disposition = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: attachment)
@@ -9,7 +11,7 @@ module SendFileUpload
# Google Cloud Storage, so the metadata needs to be cleared on GCS for
# this to work. However, this override works with AWS.
redirect_params[:query] = { "response-content-disposition" => response_disposition,
- "response-content-type" => guess_content_type(attachment) }
+ "response-content-type" => content_type }
# By default, Rails will send uploads with an extension of .js with a
# content-type of text/javascript, which will trigger Rails'
# cross-origin JavaScript protection.
@@ -20,7 +22,7 @@ module SendFileUpload
if image_scaling_request?(file_upload)
location = file_upload.file_storage? ? file_upload.path : file_upload.url
- headers.store(*Gitlab::Workhorse.send_scaled_image(location, params[:width].to_i))
+ headers.store(*Gitlab::Workhorse.send_scaled_image(location, params[:width].to_i, content_type))
head :ok
elsif file_upload.file_storage?
send_file file_upload.path, send_params
@@ -32,6 +34,12 @@ module SendFileUpload
end
end
+ def content_type_for(attachment)
+ return '' unless attachment
+
+ guess_content_type(attachment)
+ end
+
def guess_content_type(filename)
types = MIME::Types.type_for(filename)
@@ -45,15 +53,33 @@ module SendFileUpload
private
def image_scaling_request?(file_upload)
- avatar_image_upload?(file_upload) && valid_image_scaling_width? && current_user &&
- Feature.enabled?(:dynamic_image_resizing, current_user)
+ avatar_safe_for_scaling?(file_upload) &&
+ scaling_allowed_by_feature_flags?(file_upload) &&
+ valid_image_scaling_width?
end
- def avatar_image_upload?(file_upload)
- file_upload.try(:image?) && file_upload.try(:mounted_as)&.to_sym == :avatar
+ def avatar_safe_for_scaling?(file_upload)
+ file_upload.try(:image_safe_for_scaling?) && mounted_as_avatar?(file_upload)
+ end
+
+ def mounted_as_avatar?(file_upload)
+ file_upload.try(:mounted_as)&.to_sym == :avatar
end
def valid_image_scaling_width?
Avatarable::ALLOWED_IMAGE_SCALER_WIDTHS.include?(params[:width]&.to_i)
end
+
+ # We use two separate feature gates to allow image resizing.
+ # The first, `:dynamic_image_resizing_requester`, based on the content requester.
+ # Enabling it for the user would allow that user to send resizing requests for any avatar.
+ # The second, `:dynamic_image_resizing_owner`, based on the content owner.
+ # Enabling it for the user would allow anyone to send resizing requests against the mentioned user avatar only.
+ # This flag allows us to operate on trusted data only, more in https://gitlab.com/gitlab-org/gitlab/-/issues/241533.
+ # Because of this, you need to enable BOTH to serve resized image,
+ # as you would need at least one allowed requester and at least one allowed avatar.
+ def scaling_allowed_by_feature_flags?(file_upload)
+ Feature.enabled?(:dynamic_image_resizing_requester, current_user) &&
+ Feature.enabled?(:dynamic_image_resizing_owner, file_upload.model)
+ end
end
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 5552fd663f7..4548595d968 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -14,8 +14,6 @@ module SnippetsActions
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
- before_action :redirect_if_binary, only: [:edit, :update]
-
respond_to :html
end
@@ -134,10 +132,4 @@ module SnippetsActions
recaptcha_check_with_fallback(errors.empty?) { render action }
end
-
- def redirect_if_binary
- return if Feature.enabled?(:snippets_binary_blob)
-
- redirect_to gitlab_snippet_path(snippet) if blob&.binary?
- end
end
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 5b953fe37d6..5a5b634da40 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -93,9 +93,10 @@ module WikiActions
def update
return render('shared/wikis/empty') unless can?(current_user, :create_wiki, container)
- @page = WikiPages::UpdateService.new(container: container, current_user: current_user, params: wiki_params).execute(page)
+ response = WikiPages::UpdateService.new(container: container, current_user: current_user, params: wiki_params).execute(page)
+ @page = response.payload[:page]
- if page.valid?
+ if response.success?
redirect_to(
wiki_page_path(wiki, page),
notice: _('Wiki was successfully updated.')
@@ -103,7 +104,7 @@ module WikiActions
else
render 'shared/wikis/edit'
end
- rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e
+ rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
@error = e
render 'shared/wikis/edit'
end
@@ -120,13 +121,8 @@ module WikiActions
notice: _('Wiki was successfully updated.')
)
else
- flash[:alert] = response.message
render 'shared/wikis/edit'
end
- rescue Gitlab::Git::Wiki::OperationError => e
- @page = build_page(wiki_params)
- @error = e
- render 'shared/wikis/edit'
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -162,14 +158,18 @@ module WikiActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def destroy
- WikiPages::DestroyService.new(container: container, current_user: current_user).execute(page)
+ return render_404 unless page
- redirect_to wiki_path(wiki),
- status: :found,
- notice: _("Page was successfully deleted")
- rescue Gitlab::Git::Wiki::OperationError => e
- @error = e
- render 'shared/wikis/edit'
+ response = WikiPages::DestroyService.new(container: container, current_user: current_user).execute(page)
+
+ if response.success?
+ redirect_to wiki_path(wiki),
+ status: :found,
+ notice: _("Page was successfully deleted")
+ else
+ @error = response
+ render 'shared/wikis/edit'
+ end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 91704f030cd..2bd6fd85381 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -63,10 +63,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def load_projects(finder_params)
- @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute
- @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute
+ @total_user_projects_count = ProjectsFinder.new(params: { non_public: true, without_deleted: true }, current_user: current_user).execute
+ @total_starred_projects_count = ProjectsFinder.new(params: { starred: true, without_deleted: true }, current_user: current_user).execute
finder_params[:use_cte] = true if use_cte_for_finder?
+ finder_params[:without_deleted] = true
projects = ProjectsFinder.new(params: finder_params, current_user: current_user).execute
@@ -89,7 +90,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def load_events
projects = ProjectsFinder
- .new(params: params.merge(non_public: true), current_user: current_user)
+ .new(params: params.merge(non_public: true, without_deleted: true), current_user: current_user)
.execute
@events = EventCollection
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index a1348e4d858..123102bf793 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -81,7 +81,7 @@ class GraphqlController < ApplicationController
end
def context
- @context ||= { current_user: current_user }
+ @context ||= { current_user: current_user, is_sessionless_user: !!sessionless_user? }
end
def build_variables(variable_info)
@@ -107,4 +107,12 @@ class GraphqlController < ApplicationController
render json: error, status: status
end
+
+ def append_info_to_payload(payload)
+ super
+
+ # Merging to :metadata will ensure these are logged as top level keys
+ payload[:metadata] ||= {}
+ payload[:metadata].merge!(graphql: { operation_name: params[:operationName] })
+ end
end
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 23d4f0d24e9..ea7e83a2caf 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
+ push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false)
push_frontend_feature_flag(:boards_with_swimlanes, group, default_enabled: false)
end
diff --git a/app/controllers/groups/settings/integrations_controller.rb b/app/controllers/groups/settings/integrations_controller.rb
index adfbe9bfa17..e8551a7f270 100644
--- a/app/controllers/groups/settings/integrations_controller.rb
+++ b/app/controllers/groups/settings/integrations_controller.rb
@@ -8,15 +8,19 @@ module Groups
before_action :authorize_admin_group!
def index
- @integrations = []
+ @integrations = Service.find_or_initialize_all(Service.for_group(group)).sort_by(&:title)
+ end
+
+ def edit
+ @default_integration = Service.default_integration(integration.type, group)
+
+ super
end
private
- # TODO: Make this compatible with group-level integration
- # https://gitlab.com/groups/gitlab-org/-/epics/2543
def find_or_initialize_integration(name)
- Project.first.find_or_initialize_service(name)
+ Service.find_or_initialize_integration(name, group_id: group.id)
end
def integrations_enabled?
diff --git a/app/controllers/instance_statistics/application_controller.rb b/app/controllers/instance_statistics/application_controller.rb
deleted file mode 100644
index a273dde105c..00000000000
--- a/app/controllers/instance_statistics/application_controller.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-class InstanceStatistics::ApplicationController < ApplicationController
- before_action :authorize_read_instance_statistics!
- layout 'instance_statistics'
-
- def authorize_read_instance_statistics!
- render_404 unless can?(current_user, :read_instance_statistics)
- end
-end
diff --git a/app/controllers/instance_statistics/dev_ops_score_controller.rb b/app/controllers/instance_statistics/dev_ops_score_controller.rb
deleted file mode 100644
index b98a1bf7f99..00000000000
--- a/app/controllers/instance_statistics/dev_ops_score_controller.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-class InstanceStatistics::DevOpsScoreController < InstanceStatistics::ApplicationController
- include Analytics::UniqueVisitsHelper
-
- track_unique_visits :index, target_id: 'i_analytics_dev_ops_score'
-
- # rubocop: disable CodeReuse/ActiveRecord
- def index
- @metric = DevOpsScore::Metric.order(:created_at).last&.present
- end
- # rubocop: enable CodeReuse/ActiveRecord
-end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 29cafbbbdb6..aa9c7d01ba3 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -12,11 +12,13 @@ class InvitesController < ApplicationController
respond_to :html
def show
+ track_experiment('opened')
accept if skip_invitation_prompt?
end
def accept
if member.accept_invite!(current_user)
+ track_experiment('accepted')
redirect_to invite_details[:path], notice: _("You have been granted %{member_human_access} access to %{title} %{name}.") %
{ member_human_access: member.human_access, title: invite_details[:title], name: invite_details[:name] }
else
@@ -74,8 +76,14 @@ class InvitesController < ApplicationController
notice << "or create an account" if Gitlab::CurrentSettings.allow_signup?
notice = notice.join(' ') + "."
+ # this is temporary finder instead of using member method due to render_404 possibility
+ # will be resolved via https://gitlab.com/gitlab-org/gitlab/-/issues/245325
+ initial_member = Member.find_by_invite_token(params[:id])
+ redirect_params = initial_member ? { invite_email: initial_member.invite_email } : {}
+
store_location_for :user, request.fullpath
- redirect_to new_user_session_path(invite_email: member.invite_email), notice: notice
+
+ redirect_to new_user_session_path(redirect_params), notice: notice
end
def invite_details
@@ -96,4 +104,17 @@ class InvitesController < ApplicationController
}
end
end
+
+ def track_experiment(action)
+ return unless params[:new_user_invite]
+
+ property = params[:new_user_invite] == 'experiment' ? 'experiment_group' : 'control_group'
+
+ Gitlab::Tracking.event(
+ Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category],
+ action,
+ property: property,
+ label: Digest::MD5.hexdigest(member.to_global_id.to_s)
+ )
+ end
end
diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb
new file mode 100644
index 00000000000..bf53c61601b
--- /dev/null
+++ b/app/controllers/jira_connect/app_descriptor_controller.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+# This returns an app descriptor for use with Jira in development mode
+# For the Atlassian Marketplace, a static copy of this JSON is uploaded to the marketplace
+# https://developer.atlassian.com/cloud/jira/platform/app-descriptor/
+
+class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
+ skip_before_action :verify_atlassian_jwt!
+
+ def show
+ render json: {
+ name: Atlassian::JiraConnect.app_name,
+ description: 'Integrate commits, branches and merge requests from GitLab into Jira',
+ key: Atlassian::JiraConnect.app_key,
+ baseUrl: jira_connect_base_url(protocol: 'https'),
+ lifecycle: {
+ installed: relative_to_base_path(jira_connect_events_installed_path),
+ uninstalled: relative_to_base_path(jira_connect_events_uninstalled_path)
+ },
+ vendor: {
+ name: 'GitLab',
+ url: 'https://gitlab.com'
+ },
+ links: {
+ documentation: help_page_url('integration/jira_development_panel', anchor: 'gitlabcom-1')
+ },
+ authentication: {
+ type: 'jwt'
+ },
+ scopes: %w(READ WRITE DELETE),
+ apiVersion: 1,
+ modules: {
+ jiraDevelopmentTool: {
+ key: 'gitlab-development-tool',
+ application: {
+ value: 'GitLab'
+ },
+ name: {
+ value: 'GitLab'
+ },
+ url: 'https://gitlab.com',
+ logoUrl: view_context.image_url('gitlab_logo.png'),
+ capabilities: %w(branch commit pull_request)
+ },
+ postInstallPage: {
+ key: 'gitlab-configuration',
+ name: {
+ value: 'GitLab Configuration'
+ },
+ url: relative_to_base_path(jira_connect_subscriptions_path)
+ }
+ },
+ apiMigrations: {
+ gdpr: true
+ }
+ }
+ end
+
+ private
+
+ def relative_to_base_path(full_path)
+ full_path.sub(/^#{jira_connect_base_path}/, '')
+ end
+end
diff --git a/app/controllers/jira_connect/application_controller.rb b/app/controllers/jira_connect/application_controller.rb
new file mode 100644
index 00000000000..a84f25998a6
--- /dev/null
+++ b/app/controllers/jira_connect/application_controller.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class JiraConnect::ApplicationController < ApplicationController
+ include Gitlab::Utils::StrongMemoize
+
+ skip_before_action :authenticate_user!
+ skip_before_action :verify_authenticity_token
+ before_action :verify_atlassian_jwt!
+
+ attr_reader :current_jira_installation
+
+ private
+
+ def verify_atlassian_jwt!
+ return render_403 unless atlassian_jwt_valid?
+
+ @current_jira_installation = installation_from_jwt
+ end
+
+ def verify_qsh_claim!
+ payload, _ = decode_auth_token!
+
+ # Make sure `qsh` claim matches the current request
+ render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url)
+ rescue
+ render_403
+ end
+
+ def atlassian_jwt_valid?
+ return false unless installation_from_jwt
+
+ # Verify JWT signature with our stored `shared_secret`
+ decode_auth_token!
+ rescue JWT::DecodeError
+ false
+ end
+
+ def installation_from_jwt
+ return unless auth_token
+
+ strong_memoize(:installation_from_jwt) do
+ # Decode without verification to get `client_key` in `iss`
+ payload, _ = Atlassian::Jwt.decode(auth_token, nil, false)
+ JiraConnectInstallation.find_by_client_key(payload['iss'])
+ end
+ end
+
+ def decode_auth_token!
+ Atlassian::Jwt.decode(auth_token, installation_from_jwt.shared_secret)
+ end
+
+ def auth_token
+ strong_memoize(:auth_token) do
+ params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last
+ end
+ end
+end
diff --git a/app/controllers/jira_connect/events_controller.rb b/app/controllers/jira_connect/events_controller.rb
new file mode 100644
index 00000000000..8f79c82d847
--- /dev/null
+++ b/app/controllers/jira_connect/events_controller.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class JiraConnect::EventsController < JiraConnect::ApplicationController
+ skip_before_action :verify_atlassian_jwt!, only: :installed
+ before_action :verify_qsh_claim!, only: :uninstalled
+
+ def installed
+ installation = JiraConnectInstallation.new(install_params)
+
+ if installation.save
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def uninstalled
+ if current_jira_installation.destroy
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ private
+
+ def install_params
+ params.permit(:clientKey, :sharedSecret, :baseUrl).transform_keys(&:underscore)
+ end
+end
diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb
new file mode 100644
index 00000000000..3ff12f29f10
--- /dev/null
+++ b/app/controllers/jira_connect/subscriptions_controller.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
+ layout 'jira_connect'
+
+ content_security_policy do |p|
+ next if p.directives.blank?
+
+ # rubocop: disable Lint/PercentStringArray
+ script_src_values = Array.wrap(p.directives['script-src']) | %w('self' https://connect-cdn.atl-paas.net https://unpkg.com/jquery@3.3.1/)
+ style_src_values = Array.wrap(p.directives['style-src']) | %w('self' 'unsafe-inline' https://unpkg.com/@atlaskit/)
+ # rubocop: enable Lint/PercentStringArray
+
+ p.frame_ancestors :self, 'https://*.atlassian.net'
+ p.script_src(*script_src_values)
+ p.style_src(*style_src_values)
+ end
+
+ before_action :allow_rendering_in_iframe, only: :index
+ before_action :verify_qsh_claim!, only: :index
+ before_action :authenticate_user!, only: :create
+
+ def index
+ @subscriptions = current_jira_installation.subscriptions.preload_namespace_route
+ end
+
+ def create
+ result = create_service.execute
+
+ if result[:status] == :success
+ render json: { success: true }
+ else
+ render json: { error: result[:message] }, status: result[:http_status]
+ end
+ end
+
+ def destroy
+ subscription = current_jira_installation.subscriptions.find(params[:id])
+
+ if subscription.destroy
+ render json: { success: true }
+ else
+ render json: { error: subscription.errors.full_messages.join(', ') }, status: :unprocessable_entity
+ end
+ end
+
+ private
+
+ def create_service
+ JiraConnectSubscriptions::CreateService.new(current_jira_installation, current_user, namespace_path: params['namespace_path'])
+ end
+
+ def allow_rendering_in_iframe
+ response.headers.delete('X-Frame-Options')
+ end
+end
diff --git a/app/controllers/oauth/jira/authorizations_controller.rb b/app/controllers/oauth/jira/authorizations_controller.rb
new file mode 100644
index 00000000000..f552b0dc10c
--- /dev/null
+++ b/app/controllers/oauth/jira/authorizations_controller.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+# This controller's role is to mimic and rewire the GitLab OAuth
+# flow routes for Jira DVCS integration.
+# See https://gitlab.com/gitlab-org/gitlab/issues/2381
+#
+class Oauth::Jira::AuthorizationsController < ApplicationController
+ skip_before_action :authenticate_user!
+ skip_before_action :verify_authenticity_token
+
+ # 1. Rewire Jira OAuth initial request to our stablished OAuth authorization URL.
+ def new
+ session[:redirect_uri] = params['redirect_uri']
+
+ redirect_to oauth_authorization_path(client_id: params['client_id'],
+ response_type: 'code',
+ scope: params['scope'],
+ redirect_uri: oauth_jira_callback_url)
+ end
+
+ # 2. Handle the callback call as we were a Github Enterprise instance client.
+ def callback
+ # Handling URI query params concatenation.
+ redirect_uri = URI.parse(session['redirect_uri'])
+ new_query = URI.decode_www_form(String(redirect_uri.query)) << ['code', params[:code]]
+ redirect_uri.query = URI.encode_www_form(new_query)
+
+ redirect_to redirect_uri.to_s
+ end
+
+ # 3. Rewire and adjust access_token request accordingly.
+ def access_token
+ # We have to modify request.parameters because Doorkeeper::Server reads params from there
+ request.parameters[:redirect_uri] = oauth_jira_callback_url
+
+ strategy = Doorkeeper::Server.new(self).token_request('authorization_code')
+ response = strategy.authorize
+
+ if response.status == :ok
+ access_token, scope, token_type = response.body.values_at('access_token', 'scope', 'token_type')
+
+ render body: "access_token=#{access_token}&scope=#{scope}&token_type=#{token_type}"
+ else
+ render status: response.status, body: response.body
+ end
+ rescue Doorkeeper::Errors::DoorkeeperError => e
+ render status: :unauthorized, body: e.type
+ end
+end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index a558b01f0c6..b798d6680bc 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -83,6 +83,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
+ def atlassian_oauth2
+ omniauth_flow(Gitlab::Auth::Atlassian)
+ end
+
private
def log_failed_login(user, provider)
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index af860297358..c27226c3f3f 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -31,8 +31,10 @@ class PasswordsController < Devise::PasswordsController
def update
super do |resource|
- if resource.valid? && resource.password_automatically_set?
- resource.update_attribute(:password_automatically_set, false)
+ if resource.valid?
+ resource.password_automatically_set = false
+ resource.password_expires_at = nil
+ resource.save(validate: false) if resource.changed?
end
end
end
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index 95e055a44db..b19285e98bb 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -7,10 +7,9 @@ class Profiles::AccountsController < Profiles::ApplicationController
render(locals: show_view_variables)
end
- # rubocop: disable CodeReuse/ActiveRecord
def unlink
provider = params[:provider]
- identity = current_user.identities.find_by(provider: provider)
+ identity = find_identity(provider)
return render_404 unless identity
@@ -22,13 +21,18 @@ class Profiles::AccountsController < Profiles::ApplicationController
redirect_to profile_account_path
end
- # rubocop: enable CodeReuse/ActiveRecord
private
def show_view_variables
{}
end
+
+ def find_identity(provider)
+ return current_user.atlassian_identity if provider == 'atlassian_oauth2'
+
+ current_user.identities.find_by(provider: provider) # rubocop: disable CodeReuse/ActiveRecord
+ end
end
Profiles::AccountsController.prepend_if_ee('EE::Profiles::AccountsController')
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 99e1b9027fa..965493955ac 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Profiles::KeysController < Profiles::ApplicationController
+ skip_before_action :authenticate_user!, only: [:get_keys]
+
def index
@keys = current_user.keys.order_id_desc
@key = Key.new
@@ -31,6 +33,25 @@ class Profiles::KeysController < Profiles::ApplicationController
end
end
+ # Get all keys of a user(params[:username]) in a text format
+ # Helpful for sysadmins to put in respective servers
+ def get_keys
+ if params[:username].present?
+ begin
+ user = UserFinder.new(params[:username]).find_by_username
+ if user.present?
+ render plain: user.all_ssh_keys.join("\n")
+ else
+ render_404
+ end
+ rescue => e
+ render html: e.message
+ end
+ else
+ render_404
+ end
+ end
+
private
def key_params
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 064b2a2cc12..bc51830c119 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -4,12 +4,9 @@ class Profiles::NotificationsController < Profiles::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def show
@user = current_user
- @group_notifications = current_user.notification_settings.preload_source_route.for_groups.order(:id)
- @group_notifications += GroupsFinder.new(
- current_user,
- all_available: false,
- exclude_group_ids: @group_notifications.select(:source_id)
- ).execute.map { |group| current_user.notification_settings_for(group, inherit: true) }
+ @user_groups = user_groups
+ @group_notifications = UserGroupNotificationSettingsFinder.new(current_user, user_groups).execute
+
@project_notifications = current_user.notification_settings.for_projects.order(:id)
.preload_source_route
.select { |notification| current_user.can?(:read_project, notification.source) }
@@ -32,4 +29,10 @@ class Profiles::NotificationsController < Profiles::ApplicationController
def user_params
params.require(:user).permit(:notification_email, :notified_of_own_activity)
end
+
+ private
+
+ def user_groups
+ GroupsFinder.new(current_user, all_available: false).execute.order_name_asc.page(params[:page])
+ end
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 8653fe3b6ed..ea4d3e861be 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -51,6 +51,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:view_diffs_file_by_file,
:tab_width,
:sourcegraph_enabled,
+ :gitpod_enabled,
:render_whitespace_in_code
]
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 50fbf8146e5..5de6d84fdd9 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -2,6 +2,9 @@
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_two_factor_requirement
+ before_action do
+ push_frontend_feature_flag(:webauthn)
+ end
def show
unless current_user.two_factor_enabled?
@@ -33,7 +36,12 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
@qr_code = build_qr_code
@account_string = account_string
- setup_u2f_registration
+
+ if Feature.enabled?(:webauthn)
+ setup_webauthn_registration
+ else
+ setup_u2f_registration
+ end
end
def create
@@ -48,7 +56,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
else
@error = _('Invalid pin code')
@qr_code = build_qr_code
- setup_u2f_registration
+
+ if Feature.enabled?(:webauthn)
+ setup_webauthn_registration
+ else
+ setup_u2f_registration
+ end
+
render 'show'
end
end
@@ -56,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
def create_u2f
- @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, u2f_registration_params, session[:challenges])
+ @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, device_registration_params, session[:challenges])
if @u2f_registration.persisted?
session.delete(:challenges)
@@ -68,6 +82,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
end
+ def create_webauthn
+ @webauthn_registration = Webauthn::RegisterService.new(current_user, device_registration_params, session[:challenge]).execute
+ if @webauthn_registration.persisted?
+ session.delete(:challenge)
+
+ redirect_to profile_two_factor_auth_path, notice: s_("Your WebAuthn device was registered!")
+ else
+ @qr_code = build_qr_code
+
+ setup_webauthn_registration
+
+ render :show
+ end
+ end
+
def codes
Users::UpdateService.new(current_user, user: current_user).execute! do |user|
@codes = user.generate_otp_backup_codes!
@@ -75,9 +104,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
def destroy
- current_user.disable_two_factor!
+ result = TwoFactor::DestroyService.new(current_user, user: current_user).execute
- redirect_to profile_account_path, status: :found
+ if result[:status] == :success
+ redirect_to profile_account_path, status: :found, notice: s_('Two-factor authentication has been disabled successfully!')
+ else
+ redirect_to profile_account_path, status: :found, alert: result[:message]
+ end
end
def skip
@@ -108,11 +141,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
- @u2f_registrations = current_user.u2f_registrations
+ @registrations = u2f_registrations
u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests
- sign_requests = u2f.authentication_requests(@u2f_registrations.map(&:key_handle))
+ sign_requests = u2f.authentication_requests(current_user.u2f_registrations.map(&:key_handle))
session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
@@ -120,8 +153,53 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
sign_requests: sign_requests })
end
- def u2f_registration_params
- params.require(:u2f_registration).permit(:device_response, :name)
+ def device_registration_params
+ params.require(:device_registration).permit(:device_response, :name)
+ end
+
+ def setup_webauthn_registration
+ @registrations = webauthn_registrations
+ @webauthn_registration ||= WebauthnRegistration.new
+
+ unless current_user.webauthn_xid
+ current_user.user_detail.update!(webauthn_xid: WebAuthn.generate_user_id)
+ end
+
+ options = webauthn_options
+ session[:challenge] = options.challenge
+
+ gon.push(webauthn: { options: options, app_id: u2f_app_id })
+ end
+
+ # Adds delete path to u2f registrations
+ # to reduce logic in view template
+ def u2f_registrations
+ current_user.u2f_registrations.map do |u2f_registration|
+ {
+ name: u2f_registration.name,
+ created_at: u2f_registration.created_at,
+ delete_path: profile_u2f_registration_path(u2f_registration)
+ }
+ end
+ end
+
+ def webauthn_registrations
+ current_user.webauthn_registrations.map do |webauthn_registration|
+ {
+ name: webauthn_registration.name,
+ created_at: webauthn_registration.created_at,
+ delete_path: profile_webauthn_registration_path(webauthn_registration)
+ }
+ end
+ end
+
+ def webauthn_options
+ WebAuthn::Credential.options_for_create(
+ user: { id: current_user.webauthn_xid, name: current_user.username },
+ exclude: current_user.webauthn_registrations.map { |c| c.credential_xid },
+ authenticator_selection: { user_verification: 'discouraged' },
+ rp: { name: 'GitLab' }
+ )
end
def groups_notification(groups)
@@ -129,6 +207,6 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
leave_group_links = groups.map { |group| view_context.link_to (s_("leave %{group_name}") % { group_name: group.full_name }), leave_group_members_path(group), remote: false, method: :delete}.to_sentence
s_(%{The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}.})
- .html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe }
+ .html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe }
end
end
diff --git a/app/controllers/profiles/webauthn_registrations_controller.rb b/app/controllers/profiles/webauthn_registrations_controller.rb
new file mode 100644
index 00000000000..81b1dd6f710
--- /dev/null
+++ b/app/controllers/profiles/webauthn_registrations_controller.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class Profiles::WebauthnRegistrationsController < Profiles::ApplicationController
+ def destroy
+ webauthn_registration = current_user.webauthn_registrations.find(params[:id])
+ webauthn_registration.destroy
+
+ redirect_to profile_two_factor_auth_path, status: :found, notice: _("Successfully deleted WebAuthn device.")
+ end
+end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index c9f46eb72c5..248d5755d92 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -6,6 +6,9 @@ class ProfilesController < Profiles::ApplicationController
before_action :user
before_action :authorize_change_username!, only: :update_username
skip_before_action :require_email, only: [:show, :update]
+ before_action do
+ push_frontend_feature_flag(:webauthn)
+ end
def show
end
@@ -101,6 +104,7 @@ class ProfilesController < Profiles::ApplicationController
:bio,
:email,
:role,
+ :gitpod_enabled,
:hide_no_password,
:hide_no_ssh_key,
:hide_project_limit,
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 518d414be1b..ca2692438e8 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -58,9 +58,9 @@ class Projects::ApplicationController < ApplicationController
def method_missing(method_sym, *arguments, &block)
case method_sym.to_s
when /\Aauthorize_(.*)!\z/
- authorize_action!($1.to_sym)
+ authorize_action!(Regexp.last_match(1).to_sym)
when /\Acheck_(.*)_available!\z/
- check_project_feature_available!($1.to_sym)
+ check_project_feature_available!(Regexp.last_match(1).to_sym)
else
super
end
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 59a7dff680c..eb47fec2b7e 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -9,6 +9,7 @@ class Projects::BadgesController < Projects::ApplicationController
def pipeline
pipeline_status = Gitlab::Badge::Pipeline::Status
.new(project, params[:ref], opts: {
+ ignore_skipped: params[:ignore_skipped],
key_text: params[:key_text],
key_width: params[:key_width]
})
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index d969e7bf771..1568d9966dd 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -10,6 +10,8 @@ class Projects::BlobController < Projects::ApplicationController
include RedirectsForMissingPathOnTree
include SourcegraphDecorator
include DiffHelper
+ include RedisTracking
+ extend ::Gitlab::Utils::Override
prepend_before_action :authenticate_user!, only: [:edit]
@@ -33,8 +35,11 @@ class Projects::BlobController < Projects::ApplicationController
before_action only: :show do
push_frontend_feature_flag(:code_navigation, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
+ push_frontend_feature_flag(:gitlab_ci_yml_preview, @project, default_enabled: false)
end
+ track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true
+
def new
commit unless @repository.empty?
end
@@ -99,8 +104,6 @@ class Projects::BlobController < Projects::ApplicationController
end
def diff
- apply_diff_view_cookie!
-
@form = Blobs::UnfoldPresenter.new(blob, diff_params)
# keep only json rendering when
@@ -256,4 +259,9 @@ class Projects::BlobController < Projects::ApplicationController
def diff_params
params.permit(:full, :since, :to, :bottom, :unfold, :offset, :indent)
end
+
+ override :visitor_id
+ def visitor_id
+ current_user&.id
+ end
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index db05da0bb7f..5ed35094476 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
+ push_frontend_feature_flag(:boards_with_swimlanes, project, default_enabled: false)
end
private
diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb
index c13baaea8c6..813a0a9ddd5 100644
--- a/app/controllers/projects/ci/lints_controller.rb
+++ b/app/controllers/projects/ci/lints_controller.rb
@@ -2,6 +2,9 @@
class Projects::Ci::LintsController < Projects::ApplicationController
before_action :authorize_create_pipeline!
+ before_action do
+ push_frontend_feature_flag(:ci_lint_vue, project)
+ end
def show
end
@@ -10,40 +13,15 @@ class Projects::Ci::LintsController < Projects::ApplicationController
@content = params[:content]
@dry_run = params[:dry_run]
- if @dry_run && Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
- pipeline = Ci::CreatePipelineService
- .new(@project, current_user, ref: @project.default_branch)
- .execute(:push, dry_run: true, content: @content)
-
- @status = pipeline.error_messages.empty?
- @stages = pipeline.stages
- @errors = pipeline.error_messages.map(&:content)
- @warnings = pipeline.warning_messages.map(&:content)
- else
- result = Gitlab::Ci::YamlProcessor.new_with_validation_errors(@content, yaml_processor_options)
+ @result = Gitlab::Ci::Lint
+ .new(project: @project, current_user: current_user)
+ .validate(@content, dry_run: @dry_run)
- @status = result.valid?
- @errors = result.errors
- @warnings = result.warnings
-
- if result.valid?
- @config_processor = result.config
- @stages = @config_processor.stages
- @builds = @config_processor.builds
- @jobs = @config_processor.jobs
+ respond_to do |format|
+ format.html { render :show }
+ format.json do
+ render json: ::Ci::Lint::ResultSerializer.new.represent(@result)
end
end
-
- render :show
- end
-
- private
-
- def yaml_processor_options
- {
- project: @project,
- user: current_user,
- sha: project.repository.commit.sha
- }
end
end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 41631aea620..c6b6b825bb7 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -43,9 +43,10 @@ class Projects::ForksController < Projects::ApplicationController
end
format.json do
- namespaces = fork_service.valid_fork_targets - [current_user.namespace, project.namespace]
+ namespaces = load_namespaces_with_associations - [project.namespace]
+
render json: {
- namespaces: ForkNamespaceSerializer.new.represent(namespaces, project: project, current_user: current_user)
+ namespaces: ForkNamespaceSerializer.new.represent(namespaces, project: project, current_user: current_user, memberships: memberships_hash)
}
end
end
@@ -100,6 +101,14 @@ class Projects::ForksController < Projects::ApplicationController
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42335')
end
+
+ def load_namespaces_with_associations
+ @load_namespaces_with_associations ||= fork_service.valid_fork_targets(only_groups: true).preload(:route)
+ end
+
+ def memberships_hash
+ current_user.members.where(source: load_namespaces_with_associations).index_by(&:source_id)
+ end
end
Projects::ForksController.prepend_if_ee('EE::Projects::ForksController')
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index deba71c9dd3..9bed12fd151 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -5,10 +5,10 @@ class Projects::ImportsController < Projects::ApplicationController
include ImportUrlParams
# Authorize
- before_action :authorize_admin_project!, only: [:new, :create]
+ before_action :authorize_admin_project!, except: :show
before_action :require_namespace_project_creation_permission, only: :show
- before_action :require_no_repo, only: [:new, :create]
- before_action :redirect_if_progress, only: [:new, :create]
+ before_action :require_no_repo, except: :show
+ before_action :redirect_if_progress, except: :show
before_action :redirect_if_no_import, only: :show
def new
diff --git a/app/controllers/projects/issue_links_controller.rb b/app/controllers/projects/issue_links_controller.rb
new file mode 100644
index 00000000000..2f7489373ed
--- /dev/null
+++ b/app/controllers/projects/issue_links_controller.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Projects
+ class IssueLinksController < Projects::ApplicationController
+ include IssuableLinks
+
+ before_action :authorize_admin_issue_link!, only: [:create, :destroy]
+ before_action :authorize_issue_link_association!, only: :destroy
+
+ private
+
+ def authorize_admin_issue_link!
+ render_403 unless can?(current_user, :admin_issue_link, @project)
+ end
+
+ def authorize_issue_link_association!
+ render_404 if link.target != issue && link.source != issue
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def issue
+ @issue ||=
+ IssuesFinder.new(current_user, project_id: @project.id)
+ .find_by!(iid: params[:issue_id])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def list_service
+ IssueLinks::ListService.new(issue, current_user)
+ end
+
+ def create_service
+ IssueLinks::CreateService.new(issue, current_user, create_params)
+ end
+
+ def destroy_service
+ IssueLinks::DestroyService.new(link, current_user)
+ end
+
+ def link
+ @link ||= IssueLink.find(params[:id])
+ end
+
+ def create_params
+ params.permit(:link_type, issuable_references: [])
+ end
+ end
+end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 2200860a184..7f0d23b79ce 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,13 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController
include SpammableActions
include RecordUserLastActivity
- def issue_except_actions
- %i[index calendar new create bulk_update import_csv export_csv service_desk]
- end
-
- def set_issuables_index_only_actions
- %i[index calendar service_desk]
- end
+ ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze
+ SET_ISSUEABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
@@ -25,9 +20,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
- before_action :issue, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) }
+ before_action :issue, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
+ after_action :log_issue_show, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
- before_action :set_issuables_index, if: ->(c) { c.set_issuables_index_only_actions.include?(c.action_name.to_sym) }
+ before_action :set_issuables_index, if: ->(c) { SET_ISSUEABLES_INDEX_ONLY_ACTIONS.include?(c.action_name.to_sym) }
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -48,6 +44,8 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:tribute_autocomplete, @project)
push_frontend_feature_flag(:vue_issuables_list, project)
+ push_frontend_feature_flag(:design_management_todo_button, project, default_enabled: true)
+ push_frontend_feature_flag(:vue_sidebar_labels, @project)
end
before_action only: :show do
@@ -95,7 +93,7 @@ class Projects::IssuesController < Projects::ApplicationController
discussion_to_resolve: params[:discussion_to_resolve],
confidential: !!Gitlab::Utils.to_boolean(params[:issue][:confidential])
)
- service = Issues::BuildService.new(project, current_user, build_params)
+ service = ::Issues::BuildService.new(project, current_user, build_params)
@issue = @noteable = service.execute
@@ -115,7 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController
discussion_to_resolve: params[:discussion_to_resolve]
)
- service = Issues::CreateService.new(project, current_user, create_params)
+ service = ::Issues::CreateService.new(project, current_user, create_params)
@issue = service.execute
if service.discussions_to_resolve.count(&:resolved?) > 0
@@ -143,7 +141,7 @@ class Projects::IssuesController < Projects::ApplicationController
new_project = Project.find(params[:move_to_project_id])
return render_404 unless issue.can_move?(current_user, new_project)
- @issue = Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue)
+ @issue = ::Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue)
end
respond_to do |format|
@@ -157,7 +155,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def reorder
- service = Issues::ReorderService.new(project, current_user, reorder_params)
+ service = ::Issues::ReorderService.new(project, current_user, reorder_params)
if service.execute(issue)
head :ok
@@ -167,7 +165,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def related_branches
- @related_branches = Issues::RelatedBranchesService
+ @related_branches = ::Issues::RelatedBranchesService
.new(project, current_user)
.execute(issue)
.map { |branch| branch.merge(link: branch_link(branch)) }
@@ -249,6 +247,13 @@ class Projects::IssuesController < Projects::ApplicationController
@issue
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def log_issue_show
+ return unless current_user && @issue
+
+ ::Gitlab::Search::RecentIssues.new(user: current_user).log_view(@issue)
+ end
+
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
alias_method :awardable, :issue
@@ -319,7 +324,7 @@ class Projects::IssuesController < Projects::ApplicationController
def update_service
update_params = issue_params.merge(spammable_params)
- Issues::UpdateService.new(project, current_user, update_params)
+ ::Issues::UpdateService.new(project, current_user, update_params)
end
def finder_type
@@ -340,10 +345,12 @@ class Projects::IssuesController < Projects::ApplicationController
def finder_options
options = super
- return options unless service_desk?
+ options[:issue_types] = Issue::TYPES_FOR_LIST
- options.reject! { |key| key == 'author_username' || key == 'author_id' }
- options[:author_id] = User.support_bot
+ if service_desk?
+ options.reject! { |key| key == 'author_username' || key == 'author_id' }
+ options[:author_id] = User.support_bot
+ end
options
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 0bb4e0fb5ee..921da788ad2 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -43,6 +43,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:discussion_locked,
label_ids: [],
assignee_ids: [],
+ reviewer_ids: [],
update_task: [:index, :checked, :line_number, :line_source]
]
end
diff --git a/app/controllers/projects/merge_requests/content_controller.rb b/app/controllers/projects/merge_requests/content_controller.rb
index eec5c1a4355..399745151b1 100644
--- a/app/controllers/projects/merge_requests/content_controller.rb
+++ b/app/controllers/projects/merge_requests/content_controller.rb
@@ -10,6 +10,9 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl
before_action :set_polling_header
around_action :allow_gitaly_ref_name_caching
+ FAST_POLLING_INTERVAL = 10.seconds.in_milliseconds
+ SLOW_POLLING_INTERVAL = 5.minutes.in_milliseconds
+
def widget
respond_to do |format|
format.json do
@@ -29,7 +32,8 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl
private
def set_polling_header
- Gitlab::PollingInterval.set_header(response, interval: 10_000)
+ interval = merge_request.open? ? FAST_POLLING_INTERVAL : SLOW_POLLING_INTERVAL
+ Gitlab::PollingInterval.set_header(response, interval: interval)
end
def serializer(entity)
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index bceccc7063b..8aacfdce094 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -4,7 +4,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
include DiffHelper
include RendersNotes
- before_action :apply_diff_view_cookie!
before_action :commit
before_action :define_diff_vars
before_action :define_diff_comment_vars, except: [:diffs_batch, :diffs_metadata]
@@ -21,15 +20,15 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
def diffs_batch
- return render_404 unless Feature.enabled?(:diffs_batch_load, @merge_request.project, default_enabled: true)
-
diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options)
positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user)
+ environment = @merge_request.environments_for(current_user, latest: true).last
diffs.unfold_diff_files(positions.unfoldable)
diffs.write_cache
options = {
+ environment: environment,
merge_request: @merge_request,
diff_view: diff_view,
pagination_data: diffs.pagination_data
@@ -65,7 +64,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
render: ->(partial, locals) { view_to_html_string(partial, locals) }
}
- options = additional_attributes.merge(diff_view: diff_view)
+ options = additional_attributes.merge(diff_view: Feature.enabled?(:unified_diff_lines, @merge_request.project) ? "inline" : diff_view)
if @merge_request.project.context_commits_enabled?
options[:context_commits] = @merge_request.recent_context_commits
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index e77d2f0f5ee..92785540172 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -10,8 +10,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableCollections
include RecordUserLastActivity
include SourcegraphDecorator
+ include DiffHelper
skip_before_action :merge_request, only: [:index, :bulk_update]
+ before_action :apply_diff_view_cookie!, only: [:show]
before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authorize_read_actual_head_pipeline!, only: [
@@ -25,9 +27,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
before_action only: [:show] do
- push_frontend_feature_flag(:diffs_batch_load, @project, default_enabled: true)
push_frontend_feature_flag(:deploy_from_footer, @project, default_enabled: true)
- push_frontend_feature_flag(:single_mr_diff_view, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
push_frontend_feature_flag(:code_navigation, @project, default_enabled: true)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
@@ -36,10 +36,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true)
push_frontend_feature_flag(:file_identifier_hash)
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
- push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project, default_enabled: true)
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
+ push_frontend_feature_flag(:unified_diff_lines, @project)
+ push_frontend_feature_flag(:highlight_current_diff_row, @project)
+ push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
end
before_action do
@@ -48,6 +50,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
+ after_action :log_merge_request_show, only: [:show]
+
feature_category :source_code_management,
unless: -> (action) { action.ends_with?("_reports") }
feature_category :code_testing,
@@ -427,7 +431,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42438')
end
- def reports_response(report_comparison)
+ def reports_response(report_comparison, pipeline = nil)
+ if pipeline&.active?
+ ::Gitlab::PollingInterval.set_header(response, interval: 3000)
+
+ render json: '', status: :no_content && return
+ end
+
case report_comparison[:status]
when :parsing
::Gitlab::PollingInterval.set_header(response, interval: 3000)
@@ -442,6 +452,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
+ def log_merge_request_show
+ return unless current_user && @merge_request
+
+ ::Gitlab::Search::RecentMergeRequests.new(user: current_user).log_view(@merge_request)
+ end
+
def authorize_read_actual_head_pipeline!
return render_404 unless can?(current_user, :read_build, merge_request.actual_head_pipeline)
end
@@ -450,6 +466,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
params = request.query_parameters
params[:view] = cookies[:diff_view] if params[:view].blank? && cookies[:diff_view].present?
+ if Feature.enabled?(:default_merge_ref_for_diffs, project)
+ params = params.merge(diff_head: true)
+ end
+
diffs_metadata_project_json_merge_request_path(project, merge_request, 'json', params)
end
end
diff --git a/app/controllers/projects/metrics_dashboard_controller.rb b/app/controllers/projects/metrics_dashboard_controller.rb
index 51307c3665c..bc0a701b9fd 100644
--- a/app/controllers/projects/metrics_dashboard_controller.rb
+++ b/app/controllers/projects/metrics_dashboard_controller.rb
@@ -6,6 +6,8 @@ module Projects
# app/controllers/projects/environments_controller.rb
# See https://gitlab.com/gitlab-org/gitlab/-/issues/226002 for more details.
+ include Gitlab::Utils::StrongMemoize
+
before_action :authorize_metrics_dashboard!
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
@@ -15,6 +17,15 @@ module Projects
def show
if environment
render 'projects/environments/metrics'
+ elsif default_environment
+ redirect_to project_metrics_dashboard_path(
+ project,
+ # Reverse merge the query parameters so that a query parameter named dashboard_path doesn't
+ # override the dashboard_path path parameter.
+ **permitted_params.to_h.symbolize_keys
+ .merge(environment: default_environment.id)
+ .reverse_merge(request.query_parameters.symbolize_keys)
+ )
else
render 'projects/environments/empty_metrics'
end
@@ -22,13 +33,21 @@ module Projects
private
+ def permitted_params
+ @permitted_params ||= params.permit(:dashboard_path, :environment, :page)
+ end
+
def environment
- @environment ||=
- if params[:environment]
- project.environments.find(params[:environment])
- else
- project.default_environment
- end
+ strong_memoize(:environment) do
+ env = permitted_params[:environment]
+ project.environments.find(env) if env
+ end
+ end
+
+ def default_environment
+ strong_memoize(:default_environment) do
+ project.default_environment
+ end
end
end
end
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index 2a8bc823931..9ad6bf4095a 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -21,7 +21,7 @@ class Projects::PagesController < Projects::ApplicationController
format.html do
redirect_to project_pages_path(@project),
status: :found,
- notice: 'Pages were removed'
+ notice: 'Pages were scheduled for removal'
end
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index bfe23eb1035..c1734d2cd8a 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -63,10 +63,27 @@ class Projects::PipelinesController < Projects::ApplicationController
.new(project, current_user, create_params)
.execute(:web, ignore_skip_ci: true, save_on_errors: false)
- if @pipeline.created_successfully?
- redirect_to project_pipeline_path(project, @pipeline)
- else
- render 'new', status: :bad_request
+ respond_to do |format|
+ format.html do
+ if @pipeline.created_successfully?
+ redirect_to project_pipeline_path(project, @pipeline)
+ else
+ render 'new', status: :bad_request
+ end
+ end
+ format.json do
+ if @pipeline.created_successfully?
+ render json: PipelineSerializer
+ .new(project: project, current_user: current_user)
+ .represent(@pipeline),
+ status: :created
+ else
+ render json: { errors: @pipeline.error_messages.map(&:content),
+ warnings: @pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content),
+ total_warnings: @pipeline.warning_messages.length },
+ status: :bad_request
+ end
+ end
end
end
@@ -247,7 +264,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def latest_pipeline
- @project.latest_pipeline_for_ref(params['ref'])
+ @project.latest_pipeline(params['ref'])
&.present(current_user: current_user)
end
diff --git a/app/controllers/projects/product_analytics_controller.rb b/app/controllers/projects/product_analytics_controller.rb
index badd7671dcf..c019dc191d6 100644
--- a/app/controllers/projects/product_analytics_controller.rb
+++ b/app/controllers/projects/product_analytics_controller.rb
@@ -27,6 +27,10 @@ class Projects::ProductAnalyticsController < Projects::ApplicationController
.new(project, { graph: graph, timerange: @timerange })
.execute
end
+
+ @activity_graph = ProductAnalytics::BuildActivityGraphService
+ .new(project, { timerange: @timerange })
+ .execute
end
private
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index c48d573edbf..bd24aae980c 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -11,6 +11,9 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_show_page, project, default_enabled: true)
push_frontend_feature_flag(:release_asset_link_editing, project, default_enabled: true)
push_frontend_feature_flag(:release_asset_link_type, project, default_enabled: true)
+ push_frontend_feature_flag(:graphql_release_data, project, default_enabled: true)
+ push_frontend_feature_flag(:graphql_milestone_stats, project, default_enabled: true)
+ push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: false)
end
before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index ca2a19e67b0..9a69ef991dd 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -20,7 +20,7 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
def edit
- @admin_integration = Service.instance_for(service.type)
+ @default_integration = Service.default_integration(service.type, project)
end
def update
@@ -65,18 +65,20 @@ class Projects::ServicesController < Projects::ApplicationController
result = ::Integrations::Test::ProjectService.new(@service, current_user, params[:event]).execute
unless result[:success]
- return { error: true, message: _('Test failed.'), service_response: result[:message].to_s, test_failed: true }
+ return { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: result[:message].to_s, test_failed: true }
end
{}
rescue Gitlab::HTTP::BlockedUrlError => e
- { error: true, message: _('Test failed.'), service_response: e.message, test_failed: true }
+ { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: e.message, test_failed: true }
end
def success_message
- message = @service.active? ? _('activated') : _('settings saved, but not activated')
-
- _('%{service_title} %{message}.') % { service_title: @service.title, message: message }
+ if @service.active?
+ s_('Integrations|%{integration} settings saved and active.') % { integration: @service.title }
+ else
+ s_('Integrations|%{integration} settings saved, but not active.') % { integration: @service.title }
+ end
end
def service
diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb
index 9ec50ff8196..e97a8db0b79 100644
--- a/app/controllers/projects/static_site_editor_controller.rb
+++ b/app/controllers/projects/static_site_editor_controller.rb
@@ -14,13 +14,27 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
end
def show
- @config = Gitlab::StaticSiteEditor::Config.new(@repository, @ref, @path, params[:return_url])
+ service_response = ::StaticSiteEditor::ConfigService.new(
+ container: project,
+ current_user: current_user,
+ params: {
+ ref: @ref,
+ path: @path,
+ return_url: params[:return_url]
+ }
+ ).execute
+
+ if service_response.success?
+ @data = service_response.payload
+ else
+ respond_422
+ end
end
private
def assign_ref_and_path
- @ref, @path = extract_ref(params[:id])
+ @ref, @path = extract_ref(params.fetch(:id))
render_404 if @ref.blank? || @path.blank?
end
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index 0b11ee9edc0..33205b93317 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -15,6 +15,9 @@ class Projects::TodosController < Projects::ApplicationController
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
when "merge_request"
MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
+ when "design"
+ issue = IssuesFinder.new(current_user, project_id: @project.id).find(params[:issue_id])
+ DesignManagement::DesignsFinder.new(issue, current_user).find(params[:issuable_id])
end
end
end
diff --git a/app/controllers/projects/web_ide_schemas_controller.rb b/app/controllers/projects/web_ide_schemas_controller.rb
new file mode 100644
index 00000000000..3d16a6fafd4
--- /dev/null
+++ b/app/controllers/projects/web_ide_schemas_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Projects::WebIdeSchemasController < Projects::ApplicationController
+ before_action :authenticate_user!
+
+ def show
+ return respond_422 unless branch_sha
+
+ result = ::Ide::SchemasConfigService.new(project, current_user, sha: branch_sha, filename: params[:filename]).execute
+
+ if result[:status] == :success
+ render json: result[:schema]
+ else
+ render json: result, status: :unprocessable_entity
+ end
+ end
+
+ private
+
+ def branch_sha
+ return unless params[:branch].present?
+
+ project.commit(params[:branch])&.id
+ end
+end
diff --git a/app/controllers/projects/web_ide_terminals_controller.rb b/app/controllers/projects/web_ide_terminals_controller.rb
index 08ea5c4bca8..76bcaa9e80c 100644
--- a/app/controllers/projects/web_ide_terminals_controller.rb
+++ b/app/controllers/projects/web_ide_terminals_controller.rb
@@ -11,7 +11,7 @@ class Projects::WebIdeTerminalsController < Projects::ApplicationController
def check_config
return respond_422 unless branch_sha
- result = ::Ci::WebIdeConfigService.new(project, current_user, sha: branch_sha).execute
+ result = ::Ide::TerminalConfigService.new(project, current_user, sha: branch_sha).execute
if result[:status] == :success
head :ok
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ba21fbddde1..848625ff6b5 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -42,10 +42,7 @@ class ProjectsController < Projects::ApplicationController
before_action only: [:edit] do
push_frontend_feature_flag(:service_desk_custom_address, @project)
- end
-
- before_action only: [:edit] do
- push_frontend_feature_flag(:approval_suggestions, @project)
+ push_frontend_feature_flag(:approval_suggestions, @project, default_enabled: true)
end
layout :determine_layout
@@ -98,6 +95,7 @@ class ProjectsController < Projects::ApplicationController
end
else
flash.now[:alert] = result[:message]
+ @project.reset
format.html { render_edit }
end
diff --git a/app/controllers/registrations/experience_levels_controller.rb b/app/controllers/registrations/experience_levels_controller.rb
index 5bb039bd9ba..38cffff91eb 100644
--- a/app/controllers/registrations/experience_levels_controller.rb
+++ b/app/controllers/registrations/experience_levels_controller.rb
@@ -12,7 +12,6 @@ module Registrations
if current_user.save
hide_advanced_issues
- flash[:message] = I18n.t('devise.registrations.signed_up')
redirect_to group_path(params[:namespace_path])
else
render :show
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 2a865aac767..a1252c68403 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -26,18 +26,18 @@ class RegistrationsController < Devise::RegistrationsController
end
def create
- track_experiment_event(:terms_opt_in, 'end')
accept_pending_invitations
super do |new_user|
persist_accepted_terms_if_required(new_user)
set_role_required(new_user)
+ track_terms_experiment(new_user)
yield new_user if block_given?
end
- # Do not show the signed_up notice message when the signup_flow experiment is enabled.
- # Instead, show it after successfully updating the role.
- flash[:notice] = nil if experiment_enabled?(:signup_flow)
+ # Devise sets a flash message on `create` for a successful signup,
+ # which we don't want to show.
+ flash[:notice] = nil
rescue Gitlab::Access::AccessDeniedError
redirect_to(new_user_session_path)
end
@@ -69,7 +69,6 @@ class RegistrationsController < Devise::RegistrationsController
return redirect_to new_users_sign_up_group_path if experiment_enabled?(:onboarding_issues) && show_onboarding_issues_experiment?
- set_flash_message! :notice, :signed_up
redirect_to path_for_signed_in_user(current_user)
else
render :welcome
@@ -89,7 +88,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def set_role_required(new_user)
- new_user.set_role_required! if new_user.persisted? && experiment_enabled?(:signup_flow)
+ new_user.set_role_required! if new_user.persisted?
end
def destroy_confirmation_valid?
@@ -115,9 +114,7 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user)
Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?))
- return users_sign_up_welcome_path if experiment_enabled?(:signup_flow)
-
- path_for_signed_in_user(user)
+ users_sign_up_welcome_path
end
def after_inactive_sign_up_path_for(resource)
@@ -154,7 +151,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def sign_up_params
- params.require(:user).permit(:username, :email, :email_confirmation, :name, :first_name, :last_name, :password)
+ params.require(:user).permit(:username, :email, :name, :first_name, :last_name, :password)
end
def resource_name
@@ -201,6 +198,13 @@ class RegistrationsController < Devise::RegistrationsController
true
end
+ def track_terms_experiment(new_user)
+ return unless new_user.persisted?
+
+ track_experiment_event(:terms_opt_in, 'end')
+ record_experiment_user(:terms_opt_in)
+ end
+
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
@@ -208,7 +212,7 @@ class RegistrationsController < Devise::RegistrationsController
# Part of an experiment to build a new sign up flow. Will be resolved
# with https://gitlab.com/gitlab-org/growth/engineering/issues/64
def choose_layout
- if experiment_enabled?(:signup_flow)
+ if %w(welcome update_registration).include?(action_name) || experiment_enabled?(:signup_flow)
'devise_experimental_separate_sign_up_flow'
else
'devise'
@@ -216,7 +220,10 @@ class RegistrationsController < Devise::RegistrationsController
end
def show_onboarding_issues_experiment?
- !helpers.in_subscription_flow? && !helpers.in_invitation_flow? && !helpers.in_oauth_flow?
+ !helpers.in_subscription_flow? &&
+ !helpers.in_invitation_flow? &&
+ !helpers.in_oauth_flow? &&
+ !helpers.in_trial_flow?
end
end
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb
index f93038f455e..35751a2578f 100644
--- a/app/controllers/repositories/lfs_api_controller.rb
+++ b/app/controllers/repositories/lfs_api_controller.rb
@@ -46,7 +46,7 @@ module Repositories
end
def download_objects!
- existing_oids = project.all_lfs_objects_oids(oids: objects_oids)
+ existing_oids = project.lfs_objects_oids(oids: objects_oids)
objects.each do |object|
if existing_oids.include?(object[:oid])
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 56b6a5201e7..dedaf0c903a 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -4,14 +4,18 @@ class SearchController < ApplicationController
include ControllerWithCrossProjectAccessCheck
include SearchHelper
include RendersCommits
+ include RedisTracking
SCOPE_PRELOAD_METHOD = {
projects: :with_web_entity_associations,
issues: :with_web_entity_associations
}.freeze
+ track_redis_hll_event :show, name: 'i_search_total', feature: :search_track_unique_users, feature_default_enabled: true
+
around_action :allow_gitaly_ref_name_caching
+ before_action :block_anonymous_global_searches
skip_before_action :authenticate_user!
requires_cross_project_access if: -> do
search_term_present = params[:search].present? || params[:term].present?
@@ -119,10 +123,22 @@ class SearchController < ApplicationController
super
# Merging to :metadata will ensure these are logged as top level keys
- payload[:metadata] || {}
+ payload[:metadata] ||= {}
payload[:metadata]['meta.search.group_id'] = params[:group_id]
payload[:metadata]['meta.search.project_id'] = params[:project_id]
payload[:metadata]['meta.search.search'] = params[:search]
payload[:metadata]['meta.search.scope'] = params[:scope]
end
+
+ def block_anonymous_global_searches
+ return if params[:project_id].present? || params[:group_id].present?
+ return if current_user
+ return unless ::Feature.enabled?(:block_anonymous_global_searches)
+
+ store_location_for(:user, request.fullpath)
+
+ redirect_to new_user_session_path, alert: _('You must be logged in to search across all of GitLab')
+ end
end
+
+SearchController.prepend_if_ee('EE::SearchController')
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 9435b9887e9..318553b5e0a 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -11,6 +11,8 @@ class SessionsController < Devise::SessionsController
include Gitlab::Utils::StrongMemoize
skip_before_action :check_two_factor_requirement, only: [:destroy]
+ skip_before_action :check_password_expiration, only: [:destroy]
+
# replaced with :require_no_authentication_without_flash
skip_before_action :require_no_authentication, only: [:new, :create]
@@ -27,6 +29,9 @@ class SessionsController < Devise::SessionsController
before_action :save_failed_login, if: :action_new_and_failed_login?
before_action :load_recaptcha
before_action :set_invite_params, only: [:new]
+ before_action do
+ push_frontend_feature_flag(:webauthn)
+ end
after_action :log_failed_login, if: :action_new_and_failed_login?
after_action :verify_known_sign_in, only: [:create]
@@ -157,13 +162,13 @@ class SessionsController < Devise::SessionsController
(options = request.env["warden.options"]) && options[:action] == "unauthenticated"
end
- # storing sessions per IP lets us check if there are associated multiple
+ # counting sessions per IP lets us check if there are associated multiple
# anonymous sessions with one IP and prevent situations when there are
# multiple attempts of logging in
def store_unauthenticated_sessions
return if current_user
- Gitlab::AnonymousSession.new(request.remote_ip, session_id: request.session.id).store_session_id_per_ip
+ Gitlab::AnonymousSession.new(request.remote_ip).count_session_ip
end
# Handle an "initial setup" state, where there's only one user, it's an admin,
@@ -285,13 +290,15 @@ class SessionsController < Devise::SessionsController
end
def exceeded_anonymous_sessions?
- Gitlab::AnonymousSession.new(request.remote_ip).stored_sessions >= MAX_FAILED_LOGIN_ATTEMPTS
+ Gitlab::AnonymousSession.new(request.remote_ip).session_count >= MAX_FAILED_LOGIN_ATTEMPTS
end
def authentication_method
if user_params[:otp_attempt]
"two-factor"
- elsif user_params[:device_response]
+ elsif user_params[:device_response] && Feature.enabled?(:webauthn)
+ "two-factor-via-webauthn-device"
+ elsif user_params[:device_response] && !Feature.enabled?(:webauthn)
"two-factor-via-u2f-device"
else
"standard"
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 95ea31fa977..75a861423ed 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -37,12 +37,6 @@ class UsersController < ApplicationController
end
end
- # Get all keys of a user(params[:username]) in a text format
- # Helpful for sysadmins to put in respective servers
- def ssh_keys
- render plain: user.all_ssh_keys.join("\n")
- end
-
def activity
respond_to do |format|
format.html { render 'show' }
diff --git a/app/finders/ci/jobs_finder.rb b/app/finders/ci/jobs_finder.rb
index 2169bf8c53e..8515b77ec0b 100644
--- a/app/finders/ci/jobs_finder.rb
+++ b/app/finders/ci/jobs_finder.rb
@@ -4,31 +4,38 @@ module Ci
class JobsFinder
include Gitlab::Allowable
- def initialize(current_user:, project: nil, params: {})
+ def initialize(current_user:, pipeline: nil, project: nil, params: {}, type: ::Ci::Build)
+ @pipeline = pipeline
@current_user = current_user
@project = project
@params = params
+ @type = type
+ raise ArgumentError 'type must be a subclass of Ci::Processable' unless type < ::Ci::Processable
end
def execute
builds = init_collection.order_id_desc
filter_by_scope(builds)
rescue Gitlab::Access::AccessDeniedError
- Ci::Build.none
+ type.none
end
private
- attr_reader :current_user, :project, :params
+ attr_reader :current_user, :pipeline, :project, :params, :type
def init_collection
- project ? project_builds : all_builds
+ if Feature.enabled?(:ci_jobs_finder_refactor)
+ pipeline_jobs || project_jobs || all_jobs
+ else
+ project ? project_builds : all_jobs
+ end
end
- def all_builds
+ def all_jobs
raise Gitlab::Access::AccessDeniedError unless current_user&.admin?
- Ci::Build.all
+ type.all
end
def project_builds
@@ -37,7 +44,25 @@ module Ci
project.builds.relevant
end
+ def project_jobs
+ return unless project
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :read_build, project)
+
+ jobs_by_type(project, type).relevant
+ end
+
+ def pipeline_jobs
+ return unless pipeline
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :read_build, pipeline)
+
+ jobs_by_type(pipeline, type).latest
+ end
+
def filter_by_scope(builds)
+ if Feature.enabled?(:ci_jobs_finder_refactor)
+ return filter_by_statuses!(params[:scope], builds) if params[:scope].is_a?(Array)
+ end
+
case params[:scope]
when 'pending'
builds.pending.reverse_order
@@ -49,5 +74,23 @@ module Ci
builds
end
end
+
+ def filter_by_statuses!(statuses, builds)
+ unknown_statuses = params[:scope] - ::CommitStatus::AVAILABLE_STATUSES
+ raise ArgumentError, 'Scope contains invalid value(s)' unless unknown_statuses.empty?
+
+ builds.where(status: params[:scope]) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def jobs_by_type(relation, type)
+ case type.name
+ when ::Ci::Build.name
+ relation.builds
+ when ::Ci::Bridge.name
+ relation.bridges
+ else
+ raise ArgumentError, "finder does not support #{type} type"
+ end
+ end
end
end
diff --git a/app/finders/concerns/finder_methods.rb b/app/finders/concerns/finder_methods.rb
index 8de3276184d..622cbcf4928 100644
--- a/app/finders/concerns/finder_methods.rb
+++ b/app/finders/concerns/finder_methods.rb
@@ -30,7 +30,7 @@ module FinderMethods
def if_authorized(result)
# Return the result if the finder does not perform authorization checks.
# this is currently the case in the `MilestoneFinder`
- return result unless respond_to?(:current_user)
+ return result unless respond_to?(:current_user, true)
if can_read_object?(result)
result
@@ -44,9 +44,14 @@ module FinderMethods
# for Todos
return true unless DeclarativePolicy.has_policy?(object)
- model_name = object&.model_name || model.model_name
+ Ability.allowed?(current_user, :"read_#{to_ability_name(object)}", object)
+ end
+
+ def to_ability_name(object)
+ return object.to_ability_name if object.respond_to?(:to_ability_name)
- Ability.allowed?(current_user, :"read_#{model_name.singular}", object)
+ # Not all objects define `#to_ability_name`, so attempt to derive it:
+ object.model_name.singular
end
# This fetches the model from the `ActiveRecord::Relation` but does not
diff --git a/app/finders/concerns/merged_at_filter.rb b/app/finders/concerns/merged_at_filter.rb
index d2858ba2f88..581bcca3c25 100644
--- a/app/finders/concerns/merged_at_filter.rb
+++ b/app/finders/concerns/merged_at_filter.rb
@@ -3,7 +3,6 @@
module MergedAtFilter
private
- # rubocop: disable CodeReuse/ActiveRecord
def by_merged_at(items)
return items unless merged_after || merged_before
@@ -11,11 +10,8 @@ module MergedAtFilter
mr_metrics_scope = mr_metrics_scope.merged_after(merged_after) if merged_after.present?
mr_metrics_scope = mr_metrics_scope.merged_before(merged_before) if merged_before.present?
- scope = items.joins(:metrics).merge(mr_metrics_scope)
- scope = target_project_id_filter_on_metrics(scope) if Feature.enabled?(:improved_mr_merged_at_queries)
- scope
+ items.join_metrics.merge(mr_metrics_scope)
end
- # rubocop: enable CodeReuse/ActiveRecord
def merged_after
params[:merged_after]
@@ -24,10 +20,4 @@ module MergedAtFilter
def merged_before
params[:merged_before]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def target_project_id_filter_on_metrics(scope)
- scope.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/design_management/designs_finder.rb b/app/finders/design_management/designs_finder.rb
index d9732f6b6f4..857b828dc47 100644
--- a/app/finders/design_management/designs_finder.rb
+++ b/app/finders/design_management/designs_finder.rb
@@ -3,6 +3,7 @@
module DesignManagement
class DesignsFinder
include Gitlab::Allowable
+ include FinderMethods
# Params:
# ids: integer[]
@@ -21,10 +22,7 @@ module DesignManagement
items = by_visible_at_version(items)
items = by_filename(items)
items = by_id(items)
-
- # TODO: We don't need to pass the project anymore after the feature flag is removed
- # https://gitlab.com/gitlab-org/gitlab/-/issues/34382
- items.ordered(issue.project)
+ items.ordered
end
private
diff --git a/app/finders/feature_flags_finder.rb b/app/finders/feature_flags_finder.rb
new file mode 100644
index 00000000000..9cb3bf7fa23
--- /dev/null
+++ b/app/finders/feature_flags_finder.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+class FeatureFlagsFinder
+ attr_reader :project, :params, :current_user
+
+ def initialize(project, current_user, params = {})
+ @project = project
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute(preload: true)
+ unless Ability.allowed?(current_user, :read_feature_flag, project)
+ return Operations::FeatureFlag.none
+ end
+
+ items = feature_flags
+ items = by_scope(items)
+
+ items = items.preload_relations if preload
+ items.ordered
+ end
+
+ private
+
+ def feature_flags
+ if Feature.enabled?(:feature_flags_new_version, project, default_enabled: true)
+ project.operations_feature_flags
+ else
+ project.operations_feature_flags.legacy_flag
+ end
+ end
+
+ def by_scope(items)
+ case params[:scope]
+ when 'enabled'
+ items.enabled
+ when 'disabled'
+ items.disabled
+ else
+ items
+ end
+ end
+end
diff --git a/app/finders/fork_targets_finder.rb b/app/finders/fork_targets_finder.rb
index 7a08273fa0d..719c244a207 100644
--- a/app/finders/fork_targets_finder.rb
+++ b/app/finders/fork_targets_finder.rb
@@ -7,8 +7,10 @@ class ForkTargetsFinder
end
# rubocop: disable CodeReuse/ActiveRecord
- def execute
- ::Namespace.where(id: user.manageable_namespaces).sort_by_type
+ def execute(options = {})
+ return ::Namespace.where(id: user.manageable_namespaces).sort_by_type unless options[:only_groups]
+
+ ::Group.where(id: user.manageable_groups)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index a4b00588368..ce0d52ad97a 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -27,7 +27,7 @@ class GroupMembersFinder < UnionFinder
relations << group_members if include_relations.include?(:direct)
if include_relations.include?(:inherited) && group.parent
- parents_members = GroupMember.non_request
+ parents_members = GroupMember.non_request.non_minimal_access
.where(source_id: group.ancestors.select(:id))
.where.not(user_id: group.users.select(:id))
@@ -35,7 +35,7 @@ class GroupMembersFinder < UnionFinder
end
if include_relations.include?(:descendants)
- descendant_members = GroupMember.non_request
+ descendant_members = GroupMember.non_request.non_minimal_access
.where(source_id: group.descendants.select(:id))
.where.not(user_id: group.users.select(:id))
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 5cdc22fd873..f13dc8c2451 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -37,12 +37,14 @@ class IssuableFinder
include FinderMethods
include CreatedAtFilter
include Gitlab::Utils::StrongMemoize
+ prepend OptimizedIssuableLabelFilter
requires_cross_project_access unless: -> { params.project? }
NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze
attr_accessor :current_user, :params
+ attr_writer :parent
delegate(*%i[assignee milestones], to: :params)
@@ -103,8 +105,8 @@ class IssuableFinder
items = filter_negated_items(items)
# This has to be last as we use a CTE as an optimization fence
- # for counts by passing the force_cte param and enabling the
- # attempt_group_search_optimizations feature flag
+ # for counts by passing the force_cte param and passing the
+ # attempt_group_search_optimizations param
# https://www.postgresql.org/docs/current/static/queries-with.html
items = by_search(items)
@@ -203,8 +205,26 @@ class IssuableFinder
end
end
+ def parent_param=(obj)
+ @parent = obj
+ params[parent_param] = parent if parent
+ end
+
+ def parent_param
+ case parent
+ when Project
+ :project_id
+ when Group
+ :group_id
+ else
+ raise "Unexpected parent: #{parent.class}"
+ end
+ end
+
private
+ attr_reader :parent
+
def not_params
strong_memoize(:not_params) do
params_class.new(params[:not].dup, current_user, klass).tap do |not_params|
@@ -229,13 +249,11 @@ class IssuableFinder
end
def attempt_group_search_optimizations?
- params[:attempt_group_search_optimizations] &&
- Feature.enabled?(:attempt_group_search_optimizations, default_enabled: true)
+ params[:attempt_group_search_optimizations]
end
def attempt_project_search_optimizations?
- params[:attempt_project_search_optimizations] &&
- Feature.enabled?(:attempt_project_search_optimizations, default_enabled: true)
+ params[:attempt_project_search_optimizations]
end
def count_key(value)
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index bbb624f543b..32be5bee0db 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -8,7 +8,7 @@
# current_user - which user use
# params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
-# state: 'open' or 'closed' or 'all'
+# state: 'opened' or 'closed' or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
@@ -41,7 +41,7 @@ class IssuesFinder < IssuableFinder
# rubocop: enable CodeReuse/ActiveRecord
def params_class
- IssuesFinder::Params
+ self.class.const_get(:Params, false)
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index e726772fba4..4358cf249f7 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -172,7 +172,14 @@ class LabelsFinder < UnionFinder
ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute # rubocop: disable CodeReuse/Finder
end
- @projects = @projects.in_namespace(group.id) if group?
+ if group?
+ @projects = if params[:include_subgroups]
+ @projects.in_namespace(group.self_and_descendants.select(:id))
+ else
+ @projects.in_namespace(group.id)
+ end
+ end
+
@projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index ce9137f91bb..013ed03a789 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -63,7 +63,7 @@ class MembersFinder
def direct_group_members(include_descendants)
requested_relations = [:inherited, :direct]
requested_relations << :descendants if include_descendants
- GroupMembersFinder.new(group).execute(include_relations: requested_relations).non_invite # rubocop: disable CodeReuse/Finder
+ GroupMembersFinder.new(group).execute(include_relations: requested_relations).non_invite.non_minimal_access # rubocop: disable CodeReuse/Finder
end
def project_invited_groups_members
@@ -73,7 +73,7 @@ class MembersFinder
.public_or_visible_to_user(current_user)
.select(:id)
- GroupMember.with_source_id(invited_groups_ids_including_ancestors)
+ GroupMember.with_source_id(invited_groups_ids_including_ancestors).non_minimal_access
end
def distinct_union_of_members(union_members)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index b70d0b7a06a..37da29b32ff 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -110,7 +110,9 @@ class MergeRequestsFinder < IssuableFinder
.or(table[:title].matches('WIP %'))
.or(table[:title].matches('[WIP]%'))
- return items unless Feature.enabled?(:merge_request_draft_filter)
+ # Let's keep this FF around until https://gitlab.com/gitlab-org/gitlab/-/issues/232999
+ # is implemented
+ return items unless Feature.enabled?(:merge_request_draft_filter, default_enabled: true)
items
.or(table[:title].matches('Draft - %'))
diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb
index ffc8c35fbcc..8b948bb056d 100644
--- a/app/finders/packages/group_packages_finder.rb
+++ b/app/finders/packages/group_packages_finder.rb
@@ -22,6 +22,9 @@ module Packages
def packages_for_group_projects
packages = ::Packages::Package
+ .including_build_info
+ .including_project_route
+ .including_tags
.for_projects(group_projects_visible_to_current_user)
.processed
.has_version
diff --git a/app/finders/packages/package_finder.rb b/app/finders/packages/package_finder.rb
index 0e911491da2..f1874b77845 100644
--- a/app/finders/packages/package_finder.rb
+++ b/app/finders/packages/package_finder.rb
@@ -9,6 +9,9 @@ module Packages
def execute
@project
.packages
+ .including_build_info
+ .including_project_route
+ .including_tags
.processed
.find(@package_id)
end
diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb
index c533cb266a2..519e8bf9c34 100644
--- a/app/finders/packages/packages_finder.rb
+++ b/app/finders/packages/packages_finder.rb
@@ -13,7 +13,12 @@ module Packages
end
def execute
- packages = project.packages.processed.has_version
+ packages = project.packages
+ .including_build_info
+ .including_project_route
+ .including_tags
+ .processed
+ .has_version
packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages)
packages = order_packages(packages)
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 7c7cd87a7c1..471029c1ef9 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -24,6 +24,7 @@
# last_activity_after: datetime
# last_activity_before: datetime
# repository_storage: string
+# without_deleted: boolean
#
class ProjectsFinder < UnionFinder
include CustomAttributesFilter
diff --git a/app/finders/user_group_notification_settings_finder.rb b/app/finders/user_group_notification_settings_finder.rb
new file mode 100644
index 00000000000..a6f6769116f
--- /dev/null
+++ b/app/finders/user_group_notification_settings_finder.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class UserGroupNotificationSettingsFinder
+ def initialize(user, groups)
+ @user = user
+ @groups = groups
+ end
+
+ def execute
+ # rubocop: disable CodeReuse/ActiveRecord
+ groups_with_ancestors = Gitlab::ObjectHierarchy.new(Group.where(id: groups.select(:id))).base_and_ancestors
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ @loaded_groups_with_ancestors = groups_with_ancestors.index_by(&:id)
+ @loaded_notification_settings = user.notification_settings_for_groups(groups_with_ancestors).preload_source_route.index_by(&:source_id)
+
+ groups.map do |group|
+ find_notification_setting_for(group)
+ end
+ end
+
+ private
+
+ attr_reader :user, :groups, :loaded_groups_with_ancestors, :loaded_notification_settings
+
+ def find_notification_setting_for(group)
+ return loaded_notification_settings[group.id] if loaded_notification_settings[group.id]
+ return user.notification_settings.build(source: group) if group.parent_id.nil?
+
+ parent_setting = loaded_notification_settings[group.parent_id]
+
+ return user.notification_settings.build(source: group) unless parent_setting
+
+ if should_copy?(parent_setting)
+ user.notification_settings.build(source: group) do |ns|
+ ns.assign_attributes(parent_setting.slice(*NotificationSetting.allowed_fields))
+ end
+ else
+ find_notification_setting_for(loaded_groups_with_ancestors[group.parent_id])
+ end
+ end
+
+ def should_copy?(parent_setting)
+ return false unless parent_setting
+
+ parent_setting.level != NotificationSetting.levels[:global] || parent_setting.notification_email.present?
+ end
+end
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index f87e0c67604..cc94536bf79 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -17,6 +17,7 @@
# without_projects: boolean
# sort: string
# id: integer
+# non_internal: boolean
#
class UsersFinder
include CreatedAtFilter
@@ -42,6 +43,7 @@ class UsersFinder
users = by_created_at(users)
users = by_without_projects(users)
users = by_custom_attributes(users)
+ users = by_non_internal(users)
order(users)
end
@@ -112,6 +114,12 @@ class UsersFinder
users.without_projects
end
+ def by_non_internal(users)
+ return users unless params[:non_internal]
+
+ users.non_internal
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def order(users)
return users unless params[:sort]
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index d8967da9f57..2f5043f9ffa 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -116,11 +116,11 @@ class GitlabSchema < GraphQL::Schema
expected_type = ctx[:expected_type]
gid = GlobalID.parse(global_id)
- raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab id." unless gid
+ raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID." unless gid
if expected_type && !gid.model_class.ancestors.include?(expected_type)
vars = { global_id: global_id, expected_type: expected_type }
- msg = _('%{global_id} is not a valid id for %{expected_type}.') % vars
+ msg = _('%{global_id} is not a valid ID for %{expected_type}.') % vars
raise Gitlab::Graphql::Errors::ArgumentError, msg
end
diff --git a/app/graphql/mutations/alert_management/alerts/set_assignees.rb b/app/graphql/mutations/alert_management/alerts/set_assignees.rb
index 1e0c9fdeeaf..517c20a85d0 100644
--- a/app/graphql/mutations/alert_management/alerts/set_assignees.rb
+++ b/app/graphql/mutations/alert_management/alerts/set_assignees.rb
@@ -20,6 +20,8 @@ module Mutations
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
result = set_assignees(alert, args[:assignee_usernames], args[:operation_mode])
+ track_usage_event(:incident_management_alert_assigned, current_user.id)
+
prepare_response(result)
end
diff --git a/app/graphql/mutations/alert_management/alerts/todo/create.rb b/app/graphql/mutations/alert_management/alerts/todo/create.rb
index 3dba96e43f1..2a1056e8f64 100644
--- a/app/graphql/mutations/alert_management/alerts/todo/create.rb
+++ b/app/graphql/mutations/alert_management/alerts/todo/create.rb
@@ -11,6 +11,8 @@ module Mutations
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
result = ::AlertManagement::Alerts::Todo::CreateService.new(alert, current_user).execute
+ track_usage_event(:incident_management_alert_todo, current_user.id)
+
prepare_response(result)
end
diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb
index 0de4b9409e4..0ccfcf34180 100644
--- a/app/graphql/mutations/alert_management/base.rb
+++ b/app/graphql/mutations/alert_management/base.rb
@@ -3,6 +3,7 @@
module Mutations
module AlertManagement
class Base < BaseMutation
+ include Gitlab::Utils::UsageData
include ResolvesProject
argument :project_path, GraphQL::ID_TYPE,
diff --git a/app/graphql/mutations/alert_management/create_alert_issue.rb b/app/graphql/mutations/alert_management/create_alert_issue.rb
index adb048a4479..2ddb94700c2 100644
--- a/app/graphql/mutations/alert_management/create_alert_issue.rb
+++ b/app/graphql/mutations/alert_management/create_alert_issue.rb
@@ -9,6 +9,8 @@ module Mutations
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
result = create_alert_issue(alert, current_user)
+ track_usage_event(:incident_management_incident_created, current_user.id)
+
prepare_response(alert, result)
end
diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb
index ed61555fbd6..1e14bae048a 100644
--- a/app/graphql/mutations/alert_management/update_alert_status.rb
+++ b/app/graphql/mutations/alert_management/update_alert_status.rb
@@ -13,6 +13,8 @@ module Mutations
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
result = update_status(alert, args[:status])
+ track_usage_event(:incident_management_alert_status_changed, current_user.id)
+
prepare_response(result)
end
diff --git a/app/graphql/mutations/award_emojis/toggle.rb b/app/graphql/mutations/award_emojis/toggle.rb
index a7714e695d2..679ec7a14ff 100644
--- a/app/graphql/mutations/award_emojis/toggle.rb
+++ b/app/graphql/mutations/award_emojis/toggle.rb
@@ -5,7 +5,7 @@ module Mutations
class Toggle < Base
graphql_name 'AwardEmojiToggle'
- field :toggledOn, GraphQL::BOOLEAN_TYPE, null: false,
+ field :toggled_on, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates the status of the emoji. ' \
'True if the toggle awarded the emoji, and false if the toggle removed the emoji.'
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index 68e7853a9b1..577f10545b3 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -17,6 +17,10 @@ module Mutations
context[:current_user]
end
+ def api_user?
+ context[:is_sessionless_user]
+ end
+
# Returns Array of errors on an ActiveRecord object
def errors_on_object(record)
record.errors.full_messages
diff --git a/app/graphql/mutations/boards/destroy.rb b/app/graphql/mutations/boards/destroy.rb
new file mode 100644
index 00000000000..7c381113d38
--- /dev/null
+++ b/app/graphql/mutations/boards/destroy.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ class Destroy < ::Mutations::BaseMutation
+ graphql_name 'DestroyBoard'
+
+ field :board,
+ Types::BoardType,
+ null: true,
+ description: 'The board after mutation'
+ argument :id,
+ ::Types::GlobalIDType[::Board],
+ required: true,
+ description: 'The global ID of the board to destroy'
+
+ authorize :admin_board
+
+ def resolve(id:)
+ board = authorized_find!(id: id)
+
+ response = ::Boards::DestroyService.new(board.resource_parent, current_user).execute(board)
+
+ {
+ board: response.success? ? nil : board,
+ errors: response.errors
+ }
+ end
+
+ private
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::Board)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb
index d4bf47af4cf..813b6d3cb2a 100644
--- a/app/graphql/mutations/boards/issues/issue_move_list.rb
+++ b/app/graphql/mutations/boards/issues/issue_move_list.rb
@@ -29,11 +29,11 @@ module Mutations
argument :move_before_id, GraphQL::ID_TYPE,
required: false,
- description: 'ID of issue before which the current issue will be positioned at'
+ description: 'ID of issue that should be placed before the current issue'
argument :move_after_id, GraphQL::ID_TYPE,
required: false,
- description: 'ID of issue after which the current issue will be positioned at'
+ description: 'ID of issue that should be placed after the current issue'
def ready?(**args)
if move_arguments(args).blank?
@@ -50,6 +50,8 @@ module Mutations
end
def resolve(board:, **args)
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/247861')
+
raise_resource_not_available_error! unless board
authorize_board!(board)
@@ -89,3 +91,5 @@ module Mutations
end
end
end
+
+Mutations::Boards::Issues::IssueMoveList.prepend_if_ee('EE::Mutations::Boards::Issues::IssueMoveList')
diff --git a/app/graphql/mutations/boards/lists/base.rb b/app/graphql/mutations/boards/lists/base.rb
index 34b271ba3b8..d244d6bf8dd 100644
--- a/app/graphql/mutations/boards/lists/base.rb
+++ b/app/graphql/mutations/boards/lists/base.rb
@@ -8,7 +8,7 @@ module Mutations
argument :board_id, ::Types::GlobalIDType[::Board],
required: true,
- description: 'The Global ID of the issue board to mutate'
+ description: 'Global ID of the issue board to mutate'
field :list,
Types::BoardListType,
diff --git a/app/graphql/mutations/boards/lists/create.rb b/app/graphql/mutations/boards/lists/create.rb
index 4f545709ee9..3fe1052315f 100644
--- a/app/graphql/mutations/boards/lists/create.rb
+++ b/app/graphql/mutations/boards/lists/create.rb
@@ -12,7 +12,7 @@ module Mutations
argument :label_id, ::Types::GlobalIDType[::Label],
required: false,
- description: 'ID of an existing label'
+ description: 'Global ID of an existing label'
def ready?(**args)
if args.slice(*mutually_exclusive_args).size != 1
@@ -39,6 +39,7 @@ module Mutations
private
+ # Overridden in EE
def authorize_list_type_resource!(board, params)
return unless params[:label_id]
@@ -57,13 +58,15 @@ module Mutations
create_list_service.execute(board)
end
+ # Overridden in EE
def create_list_params(args)
params = args.slice(*mutually_exclusive_args).with_indifferent_access
- params[:label_id] = GitlabSchema.parse_gid(params[:label_id]).model_id if params[:label_id]
+ params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id
params
end
+ # Overridden in EE
def mutually_exclusive_args
[:backlog, :label_id]
end
@@ -71,3 +74,5 @@ module Mutations
end
end
end
+
+Mutations::Boards::Lists::Create.prepend_if_ee('::EE::Mutations::Boards::Lists::Create')
diff --git a/app/graphql/mutations/ci/base.rb b/app/graphql/mutations/ci/base.rb
new file mode 100644
index 00000000000..09df4487a50
--- /dev/null
+++ b/app/graphql/mutations/ci/base.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ class Base < BaseMutation
+ argument :id, ::Types::GlobalIDType[::Ci::Pipeline],
+ required: true,
+ description: 'The id of the pipeline to mutate'
+
+ private
+
+ def find_object(id:)
+ GlobalID::Locator.locate(id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/pipeline_cancel.rb b/app/graphql/mutations/ci/pipeline_cancel.rb
new file mode 100644
index 00000000000..bc881e2ac02
--- /dev/null
+++ b/app/graphql/mutations/ci/pipeline_cancel.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ class PipelineCancel < Base
+ graphql_name 'PipelineCancel'
+
+ authorize :update_pipeline
+
+ def resolve(id:)
+ pipeline = authorized_find!(id: id)
+
+ if pipeline.cancelable?
+ pipeline.cancel_running
+ { success: true, errors: [] }
+ else
+ { success: false, errors: ['Pipeline is not cancelable'] }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/pipeline_destroy.rb b/app/graphql/mutations/ci/pipeline_destroy.rb
new file mode 100644
index 00000000000..bb24d416583
--- /dev/null
+++ b/app/graphql/mutations/ci/pipeline_destroy.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ class PipelineDestroy < Base
+ graphql_name 'PipelineDestroy'
+
+ authorize :destroy_pipeline
+
+ def resolve(id:)
+ pipeline = authorized_find!(id: id)
+ project = pipeline.project
+
+ result = ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
+ {
+ success: result.success?,
+ errors: result.errors
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/pipeline_retry.rb b/app/graphql/mutations/ci/pipeline_retry.rb
new file mode 100644
index 00000000000..0669bfc449c
--- /dev/null
+++ b/app/graphql/mutations/ci/pipeline_retry.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ class PipelineRetry < Base
+ graphql_name 'PipelineRetry'
+
+ field :pipeline,
+ Types::Ci::PipelineType,
+ null: true,
+ description: 'The pipeline after mutation'
+
+ authorize :update_pipeline
+
+ def resolve(id:)
+ pipeline = authorized_find!(id: id)
+ project = pipeline.project
+
+ ::Ci::RetryPipelineService.new(project, current_user).execute(pipeline)
+ {
+ pipeline: pipeline,
+ errors: errors_on_object(pipeline)
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/concerns/mutations/authorizes_project.rb b/app/graphql/mutations/concerns/mutations/authorizes_project.rb
new file mode 100644
index 00000000000..87341525d6c
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/authorizes_project.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AuthorizesProject
+ include ResolvesProject
+
+ def authorized_find_project!(full_path:)
+ authorized_find!(full_path: full_path)
+ end
+
+ private
+
+ def find_object(full_path:)
+ resolve_project(full_path: full_path)
+ end
+ end
+end
diff --git a/app/graphql/mutations/design_management/move.rb b/app/graphql/mutations/design_management/move.rb
index 0b654447844..6126af8b68b 100644
--- a/app/graphql/mutations/design_management/move.rb
+++ b/app/graphql/mutations/design_management/move.rb
@@ -20,10 +20,6 @@ module Mutations
null: true,
description: "The current state of the collection"
- def ready(*)
- raise ::Gitlab::Graphql::Errors::ResourceNotAvailable unless ::Feature.enabled?(:reorder_designs, default_enabled: true)
- end
-
def resolve(**args)
service = ::DesignManagement::MoveDesignsService.new(current_user, parameters(args))
diff --git a/app/graphql/mutations/issues/set_severity.rb b/app/graphql/mutations/issues/set_severity.rb
new file mode 100644
index 00000000000..bc386e07178
--- /dev/null
+++ b/app/graphql/mutations/issues/set_severity.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Issues
+ class SetSeverity < Base
+ graphql_name 'IssueSetSeverity'
+
+ argument :severity, Types::IssuableSeverityEnum, required: true,
+ description: 'Set the incident severity level.'
+
+ def resolve(project_path:, iid:, severity:)
+ issue = authorized_find!(project_path: project_path, iid: iid)
+ project = issue.project
+
+ ::Issues::UpdateService.new(project, current_user, severity: severity)
+ .execute(issue)
+
+ {
+ issue: issue,
+ errors: errors_on_object(issue)
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/set_wip.rb b/app/graphql/mutations/merge_requests/set_wip.rb
index 5d2077c12f2..0b5c20de377 100644
--- a/app/graphql/mutations/merge_requests/set_wip.rb
+++ b/app/graphql/mutations/merge_requests/set_wip.rb
@@ -9,8 +9,8 @@ module Mutations
GraphQL::BOOLEAN_TYPE,
required: true,
description: <<~DESC
- Whether or not to set the merge request as a WIP.
- DESC
+ Whether or not to set the merge request as a WIP.
+ DESC
def resolve(project_path:, iid:, wip: nil)
merge_request = authorized_find!(project_path: project_path, iid: iid)
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
index fb828ba0e2f..6e183e78d9b 100644
--- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
+++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
@@ -12,7 +12,7 @@ module Mutations
argument :id,
GraphQL::ID_TYPE,
required: true,
- description: 'The global id of the annotation to delete'
+ description: 'The global ID of the annotation to delete'
def resolve(id:)
annotation = authorized_find!(id: id)
diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb
index a068fd806f5..a8aeb15afcd 100644
--- a/app/graphql/mutations/snippets/create.rb
+++ b/app/graphql/mutations/snippets/create.rb
@@ -16,14 +16,6 @@ module Mutations
required: true,
description: 'Title of the snippet'
- argument :file_name, GraphQL::STRING_TYPE,
- required: false,
- description: 'File name of the snippet'
-
- argument :content, GraphQL::STRING_TYPE,
- required: false,
- description: 'Content of the snippet'
-
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'Description of the snippet'
@@ -59,6 +51,11 @@ module Mutations
snippet = service_response.payload[:snippet]
+ # Only when the user is not an api user and the operation was successful
+ if !api_user? && service_response.success?
+ ::Gitlab::UsageDataCounters::EditorUniqueCounter.track_snippet_editor_edit_action(author: current_user)
+ end
+
{
snippet: service_response.success? ? snippet : nil,
errors: errors_on_object(snippet)
diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb
index 6ff632ec008..d0db5fa2eb9 100644
--- a/app/graphql/mutations/snippets/update.rb
+++ b/app/graphql/mutations/snippets/update.rb
@@ -14,14 +14,6 @@ module Mutations
required: false,
description: 'Title of the snippet'
- argument :file_name, GraphQL::STRING_TYPE,
- required: false,
- description: 'File name of the snippet'
-
- argument :content, GraphQL::STRING_TYPE,
- required: false,
- description: 'Content of the snippet'
-
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'Description of the snippet'
@@ -42,6 +34,11 @@ module Mutations
update_params(args)).execute(snippet)
snippet = result.payload[:snippet]
+ # Only when the user is not an api user and the operation was successful
+ if !api_user? && result.success?
+ ::Gitlab::UsageDataCounters::EditorUniqueCounter.track_snippet_editor_edit_action(author: current_user)
+ end
+
{
snippet: result.success? ? snippet : snippet.reset,
errors: errors_on_object(snippet)
diff --git a/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb b/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb
new file mode 100644
index 00000000000..aea3afa8ec5
--- /dev/null
+++ b/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Admin
+ module Analytics
+ module InstanceStatistics
+ class MeasurementsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::Admin::Analytics::InstanceStatistics::MeasurementType, null: true
+
+ argument :identifier, Types::Admin::Analytics::InstanceStatistics::MeasurementIdentifierEnum,
+ required: true,
+ description: 'The type of measurement/statistics to retrieve'
+
+ def resolve(identifier:)
+ authorize!
+
+ ::Analytics::InstanceStatistics::Measurement
+ .with_identifier(identifier)
+ .order_by_latest
+ end
+
+ private
+
+ def authorize!
+ admin? || raise_resource_not_available_error!
+ end
+
+ def admin?
+ context[:current_user].present? && context[:current_user].admin?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/board_list_issues_resolver.rb b/app/graphql/resolvers/board_list_issues_resolver.rb
index a7cc367379d..dba9f99edeb 100644
--- a/app/graphql/resolvers/board_list_issues_resolver.rb
+++ b/app/graphql/resolvers/board_list_issues_resolver.rb
@@ -2,12 +2,20 @@
module Resolvers
class BoardListIssuesResolver < BaseResolver
+ include BoardIssueFilterable
+
+ argument :filters, Types::Boards::BoardIssueInputType,
+ required: false,
+ description: 'Filters applied when selecting issues in the board list'
+
type Types::IssueType, null: true
alias_method :list, :object
def resolve(**args)
- service = Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], { board_id: list.board.id, id: list.id })
+ filter_params = issue_filters(args[:filters]).merge(board_id: list.board.id, id: list.id)
+ service = Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params)
+
Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(service.execute)
end
diff --git a/app/graphql/resolvers/concerns/board_issue_filterable.rb b/app/graphql/resolvers/concerns/board_issue_filterable.rb
new file mode 100644
index 00000000000..1541738f46c
--- /dev/null
+++ b/app/graphql/resolvers/concerns/board_issue_filterable.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module BoardIssueFilterable
+ extend ActiveSupport::Concern
+
+ private
+
+ def issue_filters(args)
+ filters = args.to_h
+ set_filter_values(filters)
+
+ if filters[:not]
+ filters[:not] = filters[:not].to_h
+ set_filter_values(filters[:not])
+ end
+
+ filters
+ end
+
+ def set_filter_values(filters)
+ end
+end
+
+::BoardIssueFilterable.prepend_if_ee('::EE::Resolvers::BoardIssueFilterable')
diff --git a/app/graphql/resolvers/concerns/issue_resolver_fields.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index bf2f510dd89..2b14d8275d1 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_fields.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -1,9 +1,11 @@
# frozen_string_literal: true
-module IssueResolverFields
+module IssueResolverArguments
extend ActiveSupport::Concern
prepended do
+ include LooksAhead
+
argument :iid, GraphQL::STRING_TYPE,
required: false,
description: 'IID of the issue. For example, "1"'
@@ -49,7 +51,7 @@ module IssueResolverFields
required: false
end
- def resolve(**args)
+ def resolve_with_lookahead(**args)
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
# make sure it's loaded and not `nil` before continuing.
diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb
index becc6debd33..e7230287e13 100644
--- a/app/graphql/resolvers/concerns/looks_ahead.rb
+++ b/app/graphql/resolvers/concerns/looks_ahead.rb
@@ -46,7 +46,7 @@ module LooksAhead
if lookahead.selects?(:nodes)
lookahead.selection(:nodes)
elsif lookahead.selects?(:edges)
- lookahead.selection(:edges).selection(:nodes)
+ lookahead.selection(:edges).selection(:node)
end
end
end
diff --git a/app/graphql/resolvers/group_members_resolver.rb b/app/graphql/resolvers/group_members_resolver.rb
new file mode 100644
index 00000000000..f34c873a8a9
--- /dev/null
+++ b/app/graphql/resolvers/group_members_resolver.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class GroupMembersResolver < MembersResolver
+ authorize :read_group_member
+
+ private
+
+ def preloads
+ {
+ user: [:user, :source]
+ }
+ end
+
+ def finder_class
+ GroupMembersFinder
+ end
+ end
+end
diff --git a/app/graphql/resolvers/issue_status_counts_resolver.rb b/app/graphql/resolvers/issue_status_counts_resolver.rb
index 0b26b9def54..5d0d5693244 100644
--- a/app/graphql/resolvers/issue_status_counts_resolver.rb
+++ b/app/graphql/resolvers/issue_status_counts_resolver.rb
@@ -2,26 +2,13 @@
module Resolvers
class IssueStatusCountsResolver < BaseResolver
- prepend IssueResolverFields
+ prepend IssueResolverArguments
type Types::IssueStatusCountsType, null: true
def continue_issue_resolve(parent, finder, **args)
- finder.params[parent_param(parent)] = parent if parent
- Gitlab::IssuablesCountForState.new(finder, parent)
- end
-
- private
-
- def parent_param(parent)
- case parent
- when Project
- :project_id
- when Group
- :group_id
- else
- raise "Unexpected type of parent: #{parent.class}. Must be Project or Group"
- end
+ finder.parent_param = parent
+ apply_lookahead(Gitlab::IssuablesCountForState.new(finder, parent))
end
end
end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index e2874f6643c..396ae02ae13 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -2,7 +2,7 @@
module Resolvers
class IssuesResolver < BaseResolver
- prepend IssueResolverFields
+ prepend IssueResolverArguments
argument :state, Types::IssuableStateEnum,
required: false,
@@ -19,7 +19,7 @@ module Resolvers
milestone_due_asc milestone_due_desc].freeze
def continue_issue_resolve(parent, finder, **args)
- issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all
+ issues = apply_lookahead(Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all)
if non_stable_cursor_sort?(args[:sort])
# Certain complex sorts are not supported by the stable cursor pagination yet.
@@ -30,6 +30,16 @@ module Resolvers
end
end
+ private
+
+ def preloads
+ {
+ alert_management_alert: [:alert_management_alert],
+ labels: [:labels],
+ assignees: [:assignees]
+ }
+ end
+
def non_stable_cursor_sort?(sort)
NON_STABLE_CURSOR_SORTS.include?(sort)
end
diff --git a/app/graphql/resolvers/members_resolver.rb b/app/graphql/resolvers/members_resolver.rb
new file mode 100644
index 00000000000..88a1ab71c45
--- /dev/null
+++ b/app/graphql/resolvers/members_resolver.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class MembersResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include LooksAhead
+
+ argument :search, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Search query'
+
+ def resolve_with_lookahead(**args)
+ authorize!(object)
+
+ apply_lookahead(finder_class.new(object, current_user, params: args).execute)
+ end
+
+ private
+
+ def finder_class
+ # override in subclass
+ end
+ end
+end
diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
index b371f1335f8..b95e46d9cff 100644
--- a/app/graphql/resolvers/merge_request_pipelines_resolver.rb
+++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
@@ -7,6 +7,8 @@ module Resolvers
alias_method :merge_request, :object
def resolve(**args)
+ return unless project
+
resolve_pipelines(project, args)
.merge(merge_request.all_pipelines)
end
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index d15a1ede6fe..677f84e5795 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -29,11 +29,18 @@ module Resolvers
as: :label_name,
description: 'Array of label names. All resolved merge requests will have all of these labels.'
argument :merged_after, Types::TimeType,
- required: false,
- description: 'Merge requests merged after this date'
+ required: false,
+ description: 'Merge requests merged after this date'
argument :merged_before, Types::TimeType,
- required: false,
- description: 'Merge requests merged before this date'
+ required: false,
+ description: 'Merge requests merged before this date'
+ argument :milestone_title, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Title of the milestone'
+ argument :sort, Types::MergeRequestSortEnum,
+ description: 'Sort merge requests by this criteria',
+ required: false,
+ default_value: 'created_desc'
def self.single
::Resolvers::MergeRequestResolver
diff --git a/app/graphql/resolvers/metrics/dashboard_resolver.rb b/app/graphql/resolvers/metrics/dashboard_resolver.rb
index 05d82ca0f46..18a654c7dc5 100644
--- a/app/graphql/resolvers/metrics/dashboard_resolver.rb
+++ b/app/graphql/resolvers/metrics/dashboard_resolver.rb
@@ -14,7 +14,8 @@ module Resolvers
def resolve(**args)
return unless environment
- ::PerformanceMonitoring::PrometheusDashboard.find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment })
+ ::PerformanceMonitoring::PrometheusDashboard
+ .find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment })
end
end
end
diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb
index e841132eea7..c221cb9aed6 100644
--- a/app/graphql/resolvers/namespace_projects_resolver.rb
+++ b/app/graphql/resolvers/namespace_projects_resolver.rb
@@ -7,19 +7,33 @@ module Resolvers
default_value: false,
description: 'Include also subgroup projects'
+ argument :search, GraphQL::STRING_TYPE,
+ required: false,
+ default_value: nil,
+ description: 'Search project with most similar names or paths'
+
+ argument :sort, Types::Projects::NamespaceProjectSortEnum,
+ required: false,
+ default_value: nil,
+ description: 'Sort projects by this criteria'
+
type Types::ProjectType, null: true
- def resolve(include_subgroups:)
+ def resolve(include_subgroups:, sort:, search:)
# The namespace could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` or the `full_path` of the namespace
# to query for projects, so make sure it's loaded and not `nil` before continuing.
namespace = object.respond_to?(:sync) ? object.sync : object
return Project.none if namespace.nil?
- if include_subgroups
- namespace.all_projects.with_route
+ query = include_subgroups ? namespace.all_projects.with_route : namespace.projects.with_route
+
+ return query unless search.present?
+
+ if sort == :similarity
+ query.sorted_by_similarity_desc(search, include_in_select: true).merge(Project.search(search))
else
- namespace.projects.with_route
+ query.merge(Project.search(search))
end
end
diff --git a/app/graphql/resolvers/project_members_resolver.rb b/app/graphql/resolvers/project_members_resolver.rb
index 3846531762e..1ca4e81f397 100644
--- a/app/graphql/resolvers/project_members_resolver.rb
+++ b/app/graphql/resolvers/project_members_resolver.rb
@@ -1,21 +1,15 @@
# frozen_string_literal: true
module Resolvers
- class ProjectMembersResolver < BaseResolver
- argument :search, GraphQL::STRING_TYPE,
- required: false,
- description: 'Search query'
+ class ProjectMembersResolver < MembersResolver
+ type Types::MemberInterface, null: true
- type Types::ProjectMemberType, null: true
+ authorize :read_project_member
- alias_method :project, :object
-
- def resolve(**args)
- return Member.none unless project.present?
+ private
+ def finder_class
MembersFinder
- .new(project, context[:current_user], params: args)
- .execute
end
end
end
diff --git a/app/graphql/resolvers/project_merge_requests_resolver.rb b/app/graphql/resolvers/project_merge_requests_resolver.rb
new file mode 100644
index 00000000000..0526ccd315f
--- /dev/null
+++ b/app/graphql/resolvers/project_merge_requests_resolver.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class ProjectMergeRequestsResolver < MergeRequestsResolver
+ argument :assignee_username, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Username of the assignee'
+ argument :author_username, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Username of the author'
+ end
+end
diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb
index f75f591b381..3bbadf87a71 100644
--- a/app/graphql/resolvers/projects_resolver.rb
+++ b/app/graphql/resolvers/projects_resolver.rb
@@ -12,9 +12,13 @@ module Resolvers
required: false,
description: 'Search query for project name, path, or description'
+ argument :ids, [GraphQL::ID_TYPE],
+ required: false,
+ description: 'Filter projects by IDs'
+
def resolve(**args)
ProjectsFinder
- .new(current_user: current_user, params: project_finder_params(args))
+ .new(current_user: current_user, params: project_finder_params(args), project_ids_relation: parse_gids(args[:ids]))
.execute
end
@@ -27,5 +31,9 @@ module Resolvers
search: params[:search]
}.compact
end
+
+ def parse_gids(gids)
+ gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Project).model_id }
+ end
end
end
diff --git a/app/graphql/resolvers/user_starred_projects_resolver.rb b/app/graphql/resolvers/user_starred_projects_resolver.rb
new file mode 100644
index 00000000000..cc3bb90decf
--- /dev/null
+++ b/app/graphql/resolvers/user_starred_projects_resolver.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class UserStarredProjectsResolver < BaseResolver
+ type Types::ProjectType, null: true
+
+ argument :search, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Search query'
+
+ alias_method :user, :object
+
+ def resolve(**args)
+ StarredProjectsFinder.new(user, params: args, current_user: current_user).execute
+ end
+ end
+end
diff --git a/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb b/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb
new file mode 100644
index 00000000000..13c67442c2e
--- /dev/null
+++ b/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+ module Admin
+ module Analytics
+ module InstanceStatistics
+ class MeasurementIdentifierEnum < BaseEnum
+ graphql_name 'MeasurementIdentifier'
+ description 'Possible identifier types for a measurement'
+
+ value 'PROJECTS', 'Project count', value: :projects
+ value 'USERS', 'User count', value: :users
+ value 'ISSUES', 'Issue count', value: :issues
+ value 'MERGE_REQUESTS', 'Merge request count', value: :merge_requests
+ value 'GROUPS', 'Group count', value: :groups
+ value 'PIPELINES', 'Pipeline count', value: :pipelines
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb b/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb
new file mode 100644
index 00000000000..d45341077a4
--- /dev/null
+++ b/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+# rubocop:disable Graphql/AuthorizeTypes
+
+module Types
+ module Admin
+ module Analytics
+ module InstanceStatistics
+ class MeasurementType < BaseObject
+ graphql_name 'InstanceStatisticsMeasurement'
+ description 'Represents a recorded measurement (object count) for the Admins'
+
+ field :recorded_at, Types::TimeType, null: true,
+ description: 'The time the measurement was recorded'
+
+ field :count, GraphQL::INT_TYPE, null: false,
+ description: 'Object count'
+
+ field :identifier, Types::Admin::Analytics::InstanceStatistics::MeasurementIdentifierEnum, null: false,
+ description: 'The type of objects being measured'
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb b/app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb
index 69af9d463bb..93dd49b3c38 100644
--- a/app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb
+++ b/app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb
@@ -7,7 +7,7 @@ module Types
# a plain hash.
class DeleteJobsResponseType < BaseObject # rubocop:disable Graphql/AuthorizeTypes
graphql_name 'DeleteJobsResponse'
- description 'The response from the AdminSidekiqQueuesDeleteJobs mutation.'
+ description 'The response from the AdminSidekiqQueuesDeleteJobs mutation'
field :completed,
GraphQL::BOOLEAN_TYPE,
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
index 1a0b0685ffe..2da97030b88 100644
--- a/app/graphql/types/alert_management/alert_type.rb
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -6,6 +6,8 @@ module Types
graphql_name 'AlertManagementAlert'
description "Describes an alert from the project's Alert Management"
+ present_using ::AlertManagement::AlertPresenter
+
implements(Types::Notes::NoteableType)
authorize :read_alert_management_alert
diff --git a/app/graphql/types/award_emojis/award_emoji_type.rb b/app/graphql/types/award_emojis/award_emoji_type.rb
index 0247ec767c8..fe7affa50cc 100644
--- a/app/graphql/types/award_emojis/award_emoji_type.rb
+++ b/app/graphql/types/award_emojis/award_emoji_type.rb
@@ -4,7 +4,7 @@ module Types
module AwardEmojis
class AwardEmojiType < BaseObject
graphql_name 'AwardEmoji'
- description 'An emoji awarded by a user.'
+ description 'An emoji awarded by a user'
authorize :read_emoji
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
index 94f6c47e876..159443641bc 100644
--- a/app/graphql/types/base_enum.rb
+++ b/app/graphql/types/base_enum.rb
@@ -2,9 +2,12 @@
module Types
class BaseEnum < GraphQL::Schema::Enum
+ extend GitlabStyleDeprecations
+
class << self
def value(*args, **kwargs, &block)
enum[args[0].downcase] = kwargs[:value] || args[0]
+ kwargs = gitlab_deprecation(kwargs)
super(*args, **kwargs, &block)
end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index c8ec4b3f897..1e72a4cddf5 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -3,6 +3,7 @@
module Types
class BaseField < GraphQL::Schema::Field
prepend Gitlab::Graphql::Authorize
+ include GitlabStyleDeprecations
DEFAULT_COMPLEXITY = 1
@@ -12,11 +13,39 @@ module Types
kwargs[:complexity] = field_complexity(kwargs[:resolver_class], kwargs[:complexity])
@feature_flag = kwargs[:feature_flag]
kwargs = check_feature_flag(kwargs)
- kwargs = handle_deprecated(kwargs)
+ kwargs = gitlab_deprecation(kwargs)
super(*args, **kwargs, &block)
end
+ # Based on https://github.com/rmosolgo/graphql-ruby/blob/v1.11.4/lib/graphql/schema/field.rb#L538-L563
+ # Modified to fix https://github.com/rmosolgo/graphql-ruby/issues/3113
+ def resolve_field(obj, args, ctx)
+ ctx.schema.after_lazy(obj) do |after_obj|
+ query_ctx = ctx.query.context
+ inner_obj = after_obj && after_obj.object
+
+ ctx.schema.after_lazy(to_ruby_args(after_obj, args, ctx)) do |ruby_args|
+ if authorized?(inner_obj, ruby_args, query_ctx)
+ if @resolve_proc
+ # We pass `after_obj` here instead of `inner_obj` because extensions expect a GraphQL::Schema::Object
+ with_extensions(after_obj, ruby_args, query_ctx) do |extended_obj, extended_args|
+ # Since `extended_obj` is now a GraphQL::Schema::Object, we need to get the inner object and pass that to `@resolve_proc`
+ extended_obj = extended_obj.object if extended_obj.is_a?(GraphQL::Schema::Object)
+
+ @resolve_proc.call(extended_obj, args, ctx)
+ end
+ else
+ public_send_field(after_obj, ruby_args, query_ctx)
+ end
+ else
+ err = GraphQL::UnauthorizedFieldError.new(object: inner_obj, type: obj.class, context: ctx, field: self)
+ query_ctx.schema.unauthorized_field(err)
+ end
+ end
+ end
+ end
+
def base_complexity
complexity = DEFAULT_COMPLEXITY
complexity += 1 if calls_gitaly?
@@ -52,28 +81,6 @@ module Types
args
end
- def handle_deprecated(kwargs)
- if kwargs[:deprecation_reason].present?
- raise ArgumentError, 'Use `deprecated` property instead of `deprecation_reason`. ' \
- 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields'
- end
-
- deprecation = kwargs.delete(:deprecated)
- return kwargs unless deprecation
-
- milestone, reason = deprecation.values_at(:milestone, :reason).map(&:presence)
-
- raise ArgumentError, 'Please provide a `milestone` within `deprecated`' unless milestone
- raise ArgumentError, 'Please provide a `reason` within `deprecated`' unless reason
- raise ArgumentError, '`milestone` must be a `String`' unless milestone.is_a?(String)
-
- deprecated_in = "Deprecated in #{milestone}"
- kwargs[:deprecation_reason] = "#{reason}. #{deprecated_in}"
- kwargs[:description] += ". #{deprecated_in}: #{reason}" if kwargs[:description]
-
- kwargs
- end
-
def field_complexity(resolver_class, current)
return current if current.present? && current > 0
diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb
index 70c0794fc90..24faf1fe8bc 100644
--- a/app/graphql/types/board_list_type.rb
+++ b/app/graphql/types/board_list_type.rb
@@ -41,7 +41,7 @@ module Types
list = self.object
user = context[:current_user]
- Boards::Issues::ListService
+ ::Boards::Issues::ListService
.new(list.board.resource_parent, user, board_id: list.board_id, id: list.id)
.metadata
end
diff --git a/app/graphql/types/boards/board_issue_input_base_type.rb b/app/graphql/types/boards/board_issue_input_base_type.rb
new file mode 100644
index 00000000000..1187b3352cd
--- /dev/null
+++ b/app/graphql/types/boards/board_issue_input_base_type.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Types
+ module Boards
+ # rubocop: disable Graphql/AuthorizeTypes
+ class BoardIssueInputBaseType < BaseInputObject
+ argument :label_name, GraphQL::STRING_TYPE.to_list_type,
+ required: false,
+ description: 'Filter by label name'
+
+ argument :milestone_title, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Filter by milestone title'
+
+ argument :assignee_username, GraphQL::STRING_TYPE.to_list_type,
+ required: false,
+ description: 'Filter by assignee username'
+
+ argument :author_username, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Filter by author username'
+
+ argument :release_tag, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Filter by release tag'
+
+ argument :my_reaction_emoji, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Filter by reaction emoji'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
+
+Types::Boards::BoardIssueInputBaseType.prepend_if_ee('::EE::Types::Boards::BoardIssueInputBaseType')
diff --git a/app/graphql/types/boards/board_issue_input_type.rb b/app/graphql/types/boards/board_issue_input_type.rb
new file mode 100644
index 00000000000..40d065d8ea9
--- /dev/null
+++ b/app/graphql/types/boards/board_issue_input_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+ module Boards
+ # rubocop: disable Graphql/AuthorizeTypes
+ class NegatedBoardIssueInputType < BoardIssueInputBaseType
+ end
+
+ class BoardIssueInputType < BoardIssueInputBaseType
+ graphql_name 'BoardIssueInput'
+
+ argument :not, NegatedBoardIssueInputType,
+ required: false,
+ description: 'List of negated params. Warning: this argument is experimental and a subject to change in future'
+
+ argument :search, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Search query for issue title or description'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
+
+Types::Boards::BoardIssueInputType.prepend_if_ee('::EE::Types::Boards::BoardIssueInputType')
diff --git a/app/graphql/types/ci/pipeline_config_source_enum.rb b/app/graphql/types/ci/pipeline_config_source_enum.rb
index 48f88c133b4..e1575cb2f99 100644
--- a/app/graphql/types/ci/pipeline_config_source_enum.rb
+++ b/app/graphql/types/ci/pipeline_config_source_enum.rb
@@ -3,7 +3,7 @@
module Types
module Ci
class PipelineConfigSourceEnum < BaseEnum
- ::Ci::PipelineEnums.config_sources.keys.each do |state_symbol|
+ ::Enums::Ci::Pipeline.config_sources.keys.each do |state_symbol|
value state_symbol.to_s.upcase, value: state_symbol.to_s
end
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 82a9f8495ce..c508b746317 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -26,7 +26,7 @@ module Types
description: 'Detailed status of the pipeline',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
field :config_source, PipelineConfigSourceEnum, null: true,
- description: "Config source of the pipeline (#{::Ci::PipelineEnums.config_sources.keys.join(', ').upcase})"
+ description: "Config source of the pipeline (#{::Enums::Ci::Pipeline.config_sources.keys.join(', ').upcase})"
field :duration, GraphQL::INT_TYPE, null: true,
description: 'Duration of the pipeline in seconds'
field :coverage, GraphQL::FLOAT_TYPE, null: true,
@@ -48,6 +48,14 @@ module Types
field :user, Types::UserType, null: true,
description: 'Pipeline user',
resolve: -> (pipeline, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, pipeline.user_id).find }
+ field :retryable, GraphQL::BOOLEAN_TYPE,
+ description: 'Specifies if a pipeline can be retried',
+ method: :retryable?,
+ null: false
+ field :cancelable, GraphQL::BOOLEAN_TYPE,
+ description: 'Specifies if a pipeline can be canceled',
+ method: :cancelable?,
+ null: false
end
end
end
diff --git a/app/graphql/types/concerns/gitlab_style_deprecations.rb b/app/graphql/types/concerns/gitlab_style_deprecations.rb
new file mode 100644
index 00000000000..2c932f4214b
--- /dev/null
+++ b/app/graphql/types/concerns/gitlab_style_deprecations.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+# Concern for handling deprecation arguments.
+# https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-and-enum-values
+module GitlabStyleDeprecations
+ extend ActiveSupport::Concern
+
+ private
+
+ def gitlab_deprecation(kwargs)
+ if kwargs[:deprecation_reason].present?
+ raise ArgumentError, 'Use `deprecated` property instead of `deprecation_reason`. ' \
+ 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-and-enum-values'
+ end
+
+ deprecation = kwargs.delete(:deprecated)
+ return kwargs unless deprecation
+
+ milestone, reason = deprecation.values_at(:milestone, :reason).map(&:presence)
+
+ raise ArgumentError, 'Please provide a `milestone` within `deprecated`' unless milestone
+ raise ArgumentError, 'Please provide a `reason` within `deprecated`' unless reason
+ raise ArgumentError, '`milestone` must be a `String`' unless milestone.is_a?(String)
+
+ deprecated_in = "Deprecated in #{milestone}"
+ kwargs[:deprecation_reason] = "#{reason}. #{deprecated_in}"
+ kwargs[:description] += ". #{deprecated_in}: #{reason}" if kwargs[:description]
+
+ kwargs
+ end
+end
diff --git a/app/graphql/types/current_user_todos.rb b/app/graphql/types/current_user_todos.rb
new file mode 100644
index 00000000000..e610286c1a9
--- /dev/null
+++ b/app/graphql/types/current_user_todos.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Interface to expose todos for the current_user on the `object`
+module Types
+ module CurrentUserTodos
+ include BaseInterface
+
+ field_class Types::BaseField
+
+ field :current_user_todos, Types::TodoType.connection_type,
+ description: 'Todos for the current user',
+ null: false do
+ argument :state, Types::TodoStateEnum,
+ description: 'State of the todos',
+ required: false
+ end
+
+ def current_user_todos(state: nil)
+ state ||= %i(done pending) # TodosFinder treats a `nil` state param as `pending`
+
+ TodosFinder.new(current_user, state: state, type: object.class.name, target_id: object.id).execute
+ end
+ end
+end
diff --git a/app/graphql/types/design_management/design_at_version_type.rb b/app/graphql/types/design_management/design_at_version_type.rb
index 343d4cf4ff4..e10a0de1715 100644
--- a/app/graphql/types/design_management/design_at_version_type.rb
+++ b/app/graphql/types/design_management/design_at_version_type.rb
@@ -6,7 +6,7 @@ module Types
graphql_name 'DesignAtVersion'
description 'A design pinned to a specific version. ' \
- 'The image field reflects the design as of the associated version.'
+ 'The image field reflects the design as of the associated version'
authorize :read_design
@@ -23,7 +23,7 @@ module Types
field :design,
Types::DesignManagement::DesignType,
null: false,
- description: 'The underlying design.'
+ description: 'The underlying design'
def cached_stateful_version(_parent)
version
diff --git a/app/graphql/types/design_management/design_collection_type.rb b/app/graphql/types/design_management/design_collection_type.rb
index 194910831c6..904fb270e11 100644
--- a/app/graphql/types/design_management/design_collection_type.rb
+++ b/app/graphql/types/design_management/design_collection_type.rb
@@ -4,7 +4,7 @@ module Types
module DesignManagement
class DesignCollectionType < BaseObject
graphql_name 'DesignCollection'
- description 'A collection of designs.'
+ description 'A collection of designs'
authorize :read_design
diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb
index 3c84dc151bd..4e11a7aaf09 100644
--- a/app/graphql/types/design_management/design_type.rb
+++ b/app/graphql/types/design_management/design_type.rb
@@ -12,6 +12,7 @@ module Types
implements(Types::Notes::NoteableType)
implements(Types::DesignManagement::DesignFields)
+ implements(Types::CurrentUserTodos)
field :versions,
Types::DesignManagement::VersionType.connection_type,
diff --git a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
index 8bdd8afcbff..cfde9fa0d6a 100644
--- a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
+++ b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
@@ -4,7 +4,7 @@ module Types
module ErrorTracking
class SentryDetailedErrorType < ::Types::BaseObject
graphql_name 'SentryDetailedError'
- description 'A Sentry error.'
+ description 'A Sentry error'
present_using SentryErrorPresenter
diff --git a/app/graphql/types/error_tracking/sentry_error_collection_type.rb b/app/graphql/types/error_tracking/sentry_error_collection_type.rb
index f423fcb1b9f..798e0433d06 100644
--- a/app/graphql/types/error_tracking/sentry_error_collection_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_collection_type.rb
@@ -4,7 +4,7 @@ module Types
module ErrorTracking
class SentryErrorCollectionType < ::Types::BaseObject
graphql_name 'SentryErrorCollection'
- description 'An object containing a collection of Sentry errors, and a detailed error.'
+ description 'An object containing a collection of Sentry errors, and a detailed error'
authorize :read_sentry_issue
@@ -21,7 +21,7 @@ module Types
required: false
argument :sort,
String,
- description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.',
+ description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default',
required: false
end
field :detailed_error, Types::ErrorTracking::SentryDetailedErrorType,
diff --git a/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb b/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb
index 0747e41e9fb..2e6c40b233b 100644
--- a/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb
@@ -5,7 +5,7 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes
class SentryErrorStackTraceEntryType < ::Types::BaseObject
graphql_name 'SentryErrorStackTraceEntry'
- description 'An object containing a stack trace entry for a Sentry error.'
+ description 'An object containing a stack trace entry for a Sentry error'
field :function, GraphQL::STRING_TYPE,
null: true,
diff --git a/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb b/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb
index 0e6105d1ff2..1bbe7e0c77b 100644
--- a/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb
@@ -4,7 +4,7 @@ module Types
module ErrorTracking
class SentryErrorStackTraceType < ::Types::BaseObject
graphql_name 'SentryErrorStackTrace'
- description 'An object containing a stack trace entry for a Sentry error.'
+ description 'An object containing a stack trace entry for a Sentry error'
authorize :read_sentry_issue
diff --git a/app/graphql/types/error_tracking/sentry_error_type.rb b/app/graphql/types/error_tracking/sentry_error_type.rb
index 7a842025e45..693ab0c4f8f 100644
--- a/app/graphql/types/error_tracking/sentry_error_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_type.rb
@@ -5,7 +5,7 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes
class SentryErrorType < ::Types::BaseObject
graphql_name 'SentryError'
- description 'A Sentry error. A simplified version of SentryDetailedError.'
+ description 'A Sentry error. A simplified version of SentryDetailedError'
present_using SentryErrorPresenter
diff --git a/app/graphql/types/group_member_type.rb b/app/graphql/types/group_member_type.rb
index ffffa3247db..6cca0a50647 100644
--- a/app/graphql/types/group_member_type.rb
+++ b/app/graphql/types/group_member_type.rb
@@ -8,7 +8,7 @@ module Types
implements MemberInterface
graphql_name 'GroupMember'
- description 'Represents a Group Member'
+ description 'Represents a Group Membership'
field :group, Types::GroupType, null: true,
description: 'Group that a User is a member of',
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index cc8cd7c01f9..60b2e3c7b6e 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -75,6 +75,12 @@ module Types
description: 'Title of the label'
end
+ field :group_members,
+ Types::GroupMemberType.connection_type,
+ description: 'A membership of a user within this group',
+ extras: [:lookahead],
+ resolver: Resolvers::GroupMembersResolver
+
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: group) do |titles, loader, args|
LabelsFinder
diff --git a/app/graphql/types/issuable_severity_enum.rb b/app/graphql/types/issuable_severity_enum.rb
new file mode 100644
index 00000000000..563965d991e
--- /dev/null
+++ b/app/graphql/types/issuable_severity_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ class IssuableSeverityEnum < BaseEnum
+ graphql_name 'IssuableSeverity'
+ description 'Incident severity'
+
+ ::IssuableSeverity.severities.keys.each do |severity|
+ value severity.upcase, value: severity, description: "#{severity.titleize} severity"
+ end
+ end
+end
diff --git a/app/graphql/types/issue_status_counts_type.rb b/app/graphql/types/issue_status_counts_type.rb
index f2b1ba8e655..77429f9ea12 100644
--- a/app/graphql/types/issue_status_counts_type.rb
+++ b/app/graphql/types/issue_status_counts_type.rb
@@ -3,7 +3,7 @@
module Types
class IssueStatusCountsType < BaseObject
graphql_name 'IssueStatusCountsType'
- description "Represents total number of issues for the represented statuses."
+ description 'Represents total number of issues for the represented statuses'
authorize :read_issue
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 0a73ce95424..d6253f74ce5 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
+ implements(Types::CurrentUserTodos)
authorize :read_issue
@@ -38,12 +39,10 @@ module Types
description: 'User that created the issue',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
- # Remove complexity when BatchLoader is used
- field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
+ field :assignees, Types::UserType.connection_type, null: true,
description: 'Assignees of the issue'
- # Remove complexity when BatchLoader is used
- field :labels, Types::LabelType.connection_type, null: true, complexity: 5,
+ field :labels, Types::LabelType.connection_type, null: true,
description: 'Labels of the issue'
field :milestone, Types::MilestoneType, null: true,
description: 'Milestone of the issue',
@@ -101,6 +100,14 @@ module Types
field :type, Types::IssueTypeEnum, null: true,
method: :issue_type,
description: 'Type of the issue'
+
+ field :alert_management_alert,
+ Types::AlertManagement::AlertType,
+ null: true,
+ description: 'Alert associated to this issue'
+
+ field :severity, Types::IssuableSeverityEnum, null: true,
+ description: 'Severity level of the incident'
end
end
diff --git a/app/graphql/types/jira_user_type.rb b/app/graphql/types/jira_user_type.rb
index 999526a920e..394820d23be 100644
--- a/app/graphql/types/jira_user_type.rb
+++ b/app/graphql/types/jira_user_type.rb
@@ -7,7 +7,7 @@ module Types
graphql_name 'JiraUser'
field :jira_account_id, GraphQL::STRING_TYPE, null: false,
- description: 'Account id of the Jira user'
+ description: 'Account ID of the Jira user'
field :jira_display_name, GraphQL::STRING_TYPE, null: false,
description: 'Display name of the Jira user'
field :jira_email, GraphQL::STRING_TYPE, null: true,
diff --git a/app/graphql/types/member_interface.rb b/app/graphql/types/member_interface.rb
index 976836221bc..615a45413cb 100644
--- a/app/graphql/types/member_interface.rb
+++ b/app/graphql/types/member_interface.rb
@@ -4,6 +4,9 @@ module Types
module MemberInterface
include BaseInterface
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the member'
+
field :access_level, Types::AccessLevelType, null: true,
description: 'GitLab::Access level'
@@ -18,5 +21,21 @@ module Types
field :expires_at, Types::TimeType, null: true,
description: 'Date and time the membership expires'
+
+ field :user, Types::UserType, null: false,
+ description: 'User that is associated with the member object'
+
+ definition_methods do
+ def resolve_type(object, context)
+ case object
+ when GroupMember
+ Types::GroupMemberType
+ when ProjectMember
+ Types::ProjectMemberType
+ else
+ raise ::Gitlab::Graphql::Errors::BaseError, "Unknown member type #{object.class.name}"
+ end
+ end
+ end
end
end
diff --git a/app/graphql/types/merge_request_sort_enum.rb b/app/graphql/types/merge_request_sort_enum.rb
new file mode 100644
index 00000000000..c64ae367a76
--- /dev/null
+++ b/app/graphql/types/merge_request_sort_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class MergeRequestSortEnum < IssuableSortEnum
+ graphql_name 'MergeRequestSort'
+ description 'Values for sorting merge requests'
+
+ value 'MERGED_AT_ASC', 'Merge time by ascending order', value: :merged_at_asc
+ value 'MERGED_AT_DESC', 'Merge time by descending order', value: :merged_at_desc
+ end
+end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 01b02b7976f..56c88491684 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
+ implements(Types::CurrentUserTodos)
authorize :read_merge_request
@@ -79,7 +80,7 @@ module Types
description: 'Error message due to a merge error'
field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if members of the target project can push to the fork'
- field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false,
+ field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false, calls_gitaly: true,
description: 'Indicates if the merge request will be rebased'
field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true,
description: 'Rebase commit SHA of the merge request'
@@ -112,6 +113,7 @@ module Types
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline,
description: 'The pipeline running on the branch HEAD of the merge request'
field :pipelines, Types::Ci::PipelineType.connection_type,
+ null: true,
description: 'Pipelines for the merge request',
resolver: Resolvers::MergeRequestPipelinesResolver
@@ -145,6 +147,10 @@ module Types
description: Types::TaskCompletionStatus.description
field :commit_count, GraphQL::INT_TYPE, null: true,
description: 'Number of commits in the merge request'
+ field :conflicts, GraphQL::BOOLEAN_TYPE, null: false, method: :cannot_be_merged?,
+ description: 'Indicates if the merge request has conflicts'
+ field :auto_merge_enabled, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates if auto merge is enabled for the merge request'
def diff_stats(path: nil)
stats = Array.wrap(object.diff_stats&.to_a)
diff --git a/app/graphql/types/metrics/dashboard_type.rb b/app/graphql/types/metrics/dashboard_type.rb
index bbcce2d9596..47502356773 100644
--- a/app/graphql/types/metrics/dashboard_type.rb
+++ b/app/graphql/types/metrics/dashboard_type.rb
@@ -16,6 +16,13 @@ module Types
field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true,
description: 'Annotations added to the dashboard',
resolver: Resolvers::Metrics::Dashboards::AnnotationResolver
+
+ # In order to maintain backward compatibility we need to return NULL when there are no warnings
+ # and dashboard validation returns an empty array when there are no issues.
+ def schema_validation_warnings
+ warnings = object.schema_validation_warnings
+ warnings unless warnings.empty?
+ end
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb
index ca606c9da44..8603043804e 100644
--- a/app/graphql/types/milestone_type.rb
+++ b/app/graphql/types/milestone_type.rb
@@ -3,7 +3,7 @@
module Types
class MilestoneType < BaseObject
graphql_name 'Milestone'
- description 'Represents a milestone.'
+ description 'Represents a milestone'
present_using MilestonePresenter
@@ -60,3 +60,5 @@ module Types
end
end
end
+
+Types::MilestoneType.prepend_if_ee('::EE::Types::MilestoneType')
diff --git a/app/graphql/types/mutation_operation_mode_enum.rb b/app/graphql/types/mutation_operation_mode_enum.rb
index 90a29d2b0e5..37e83e7a2e1 100644
--- a/app/graphql/types/mutation_operation_mode_enum.rb
+++ b/app/graphql/types/mutation_operation_mode_enum.rb
@@ -3,7 +3,7 @@
module Types
class MutationOperationModeEnum < BaseEnum
graphql_name 'MutationOperationMode'
- description 'Different toggles for changing mutator behavior.'
+ description 'Different toggles for changing mutator behavior'
# Suggested param name for the enum: `operation_mode`
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index e143d14676e..b2732d83aac 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -14,6 +14,7 @@ module Types
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
+ mount_mutation Mutations::Boards::Destroy
mount_mutation Mutations::Boards::Issues::IssueMoveList
mount_mutation Mutations::Boards::Lists::Create
mount_mutation Mutations::Boards::Lists::Update
@@ -24,6 +25,7 @@ module Types
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetLocked
mount_mutation Mutations::Issues::SetDueDate
+ mount_mutation Mutations::Issues::SetSeverity
mount_mutation Mutations::Issues::SetSubscription
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::MergeRequests::Create
@@ -62,6 +64,9 @@ module Types
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Move
mount_mutation Mutations::ContainerExpirationPolicies::Update
+ mount_mutation Mutations::Ci::PipelineCancel
+ mount_mutation Mutations::Ci::PipelineDestroy
+ mount_mutation Mutations::Ci::PipelineRetry
end
end
diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb
index 28b7ebd2af6..e9c89b0c92e 100644
--- a/app/graphql/types/permission_types/merge_request.rb
+++ b/app/graphql/types/permission_types/merge_request.rb
@@ -18,6 +18,10 @@ module Types
PERMISSION_FIELDS.each do |field_name|
permission_field field_name, method: :"can_#{field_name}?", calls_gitaly: true
end
+
+ permission_field :can_merge, calls_gitaly: true, resolve: -> (object, args, context) do
+ object.can_be_merged_by?(context[:current_user])
+ end
end
end
end
diff --git a/app/graphql/types/project_member_type.rb b/app/graphql/types/project_member_type.rb
index e9ccb51886b..f08781238d0 100644
--- a/app/graphql/types/project_member_type.rb
+++ b/app/graphql/types/project_member_type.rb
@@ -3,7 +3,7 @@
module Types
class ProjectMemberType < BaseObject
graphql_name 'ProjectMember'
- description 'Represents a Project Member'
+ description 'Represents a Project Membership'
expose_permissions Types::PermissionTypes::Project
@@ -11,13 +11,6 @@ module Types
authorize :read_project
- field :id, GraphQL::ID_TYPE, null: false,
- description: 'ID of the member'
-
- field :user, Types::UserType, null: false,
- description: 'User that is associated with the member object',
- resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.user_id).find }
-
field :project, Types::ProjectType, null: true,
description: 'Project that User is a member of',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, obj.source_id).find }
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 5562db69de6..0fd54af1538 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -54,7 +54,7 @@ module Types
field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if the project stores Docker container images in a container registry'
field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true,
- description: 'Indicates if Shared Runners are enabled for the project'
+ description: 'Indicates if shared runners are enabled for the project'
field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if the project has Large File Storage (LFS) enabled'
field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true,
@@ -134,7 +134,7 @@ module Types
null: true,
description: 'Merge requests of the project',
extras: [:lookahead],
- resolver: Resolvers::MergeRequestsResolver
+ resolver: Resolvers::ProjectMergeRequestsResolver
field :merge_request,
Types::MergeRequestType,
@@ -146,12 +146,14 @@ module Types
Types::IssueType.connection_type,
null: true,
description: 'Issues of the project',
+ extras: [:lookahead],
resolver: Resolvers::IssuesResolver
field :issue_status_counts,
Types::IssueStatusCountsType,
null: true,
description: 'Counts of issues by status for the project',
+ extras: [:lookahead],
resolver: Resolvers::IssueStatusCountsResolver
field :milestones, Types::MilestoneType.connection_type, null: true,
@@ -159,7 +161,7 @@ module Types
resolver: Resolvers::ProjectMilestonesResolver
field :project_members,
- Types::ProjectMemberType.connection_type,
+ Types::MemberInterface.connection_type,
description: 'Members of the project',
resolver: Resolvers::ProjectMembersResolver
diff --git a/app/graphql/types/projects/namespace_project_sort_enum.rb b/app/graphql/types/projects/namespace_project_sort_enum.rb
new file mode 100644
index 00000000000..1e13deb6508
--- /dev/null
+++ b/app/graphql/types/projects/namespace_project_sort_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ module Projects
+ class NamespaceProjectSortEnum < BaseEnum
+ graphql_name 'NamespaceProjectSort'
+ description 'Values for sorting projects'
+
+ value 'SIMILARITY', 'Most similar to the search query', value: :similarity
+ end
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index c04f4da70cf..447ac63a294 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -70,9 +70,24 @@ module Types
description: 'Text to echo back',
resolver: Resolvers::EchoResolver
+ field :issue, Types::IssueType,
+ null: true,
+ description: 'Find an issue' do
+ argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue'
+ end
+
+ field :instance_statistics_measurements, Types::Admin::Analytics::InstanceStatistics::MeasurementType.connection_type,
+ null: true,
+ description: 'Get statistics on the instance',
+ resolver: Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsResolver
+
def design_management
DesignManagementObject.new(nil)
end
+
+ def issue(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::Issue)
+ end
end
end
diff --git a/app/graphql/types/release_asset_link_type.rb b/app/graphql/types/release_asset_link_type.rb
index 21f1bd50cff..0e519ece791 100644
--- a/app/graphql/types/release_asset_link_type.rb
+++ b/app/graphql/types/release_asset_link_type.rb
@@ -17,5 +17,17 @@ module Types
description: 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`'
field :external, GraphQL::BOOLEAN_TYPE, null: true, method: :external?,
description: 'Indicates the link points to an external resource'
+
+ field :direct_asset_url, GraphQL::STRING_TYPE, null: true,
+ description: 'Direct asset URL of the link'
+
+ def direct_asset_url
+ return object.url unless object.filepath
+
+ release = object.release
+ project = release.project
+
+ Gitlab::Routing.url_helpers.project_release_url(project, release) << object.filepath
+ end
end
end
diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb
index a0703b96a36..b715b981483 100644
--- a/app/graphql/types/release_type.rb
+++ b/app/graphql/types/release_type.rb
@@ -5,6 +5,8 @@ module Types
graphql_name 'Release'
description 'Represents a release'
+ connection_type_class(Types::CountableConnectionType)
+
authorize :read_release
alias_method :release, :object
@@ -26,6 +28,8 @@ module Types
description: 'Timestamp of when the release was created'
field :released_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was released'
+ field :upcoming_release, GraphQL::BOOLEAN_TYPE, null: true, method: :upcoming_release?,
+ description: 'Indicates the release is an upcoming release'
field :assets, Types::ReleaseAssetsType, null: true, method: :itself,
description: 'Assets of the release'
field :links, Types::ReleaseLinksType, null: true, method: :itself,
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
index 08e7fabeb74..4f21da3d897 100644
--- a/app/graphql/types/todo_type.rb
+++ b/app/graphql/types/todo_type.rb
@@ -26,7 +26,7 @@ module Types
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find }
field :author, Types::UserType,
- description: 'The owner of this todo',
+ description: 'The author of this todo',
null: false,
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find }
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index cb3575b41d1..8047708776d 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -37,6 +37,9 @@ module Types
field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
description: 'Project memberships of the user',
method: :project_members
+ field :starred_projects, Types::ProjectType.connection_type, null: true,
+ description: 'Projects starred by the user',
+ resolver: Resolvers::UserStarredProjectsResolver
# Merge request field: MRs can be either authored or assigned:
field :authored_merge_requests, Types::MergeRequestType.connection_type, null: true,
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 41b20a1d9a0..a81225c8954 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -4,6 +4,8 @@ require 'digest/md5'
require 'uri'
module ApplicationHelper
+ include StartupCssHelper
+
# See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views
# rubocop: disable CodeReuse/ActiveRecord
def render_if_exists(partial, locals = {})
@@ -235,13 +237,9 @@ module ApplicationHelper
"#{request.path}?#{options.compact.to_param}"
end
- def use_startup_css?
- Feature.enabled?(:startup_css) && !Rails.env.test?
- end
-
def stylesheet_link_tag_defer(path)
if use_startup_css?
- stylesheet_link_tag(path, media: "print")
+ stylesheet_link_tag(path, media: "print", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil)
else
stylesheet_link_tag(path, media: "all")
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 404700bb25e..9245cc1cb1c 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -222,6 +222,8 @@ module ApplicationSettingsHelper
:gitaly_timeout_default,
:gitaly_timeout_medium,
:gitaly_timeout_fast,
+ :gitpod_enabled,
+ :gitpod_url,
:grafana_enabled,
:grafana_url,
:gravatar_enabled,
@@ -298,7 +300,6 @@ module ApplicationSettingsHelper
:unique_ips_limit_per_user,
:unique_ips_limit_time_window,
:usage_ping_enabled,
- :instance_statistics_visibility_private,
:user_default_external,
:user_show_add_ssh_key_message,
:user_default_internal_regex,
@@ -313,7 +314,6 @@ module ApplicationSettingsHelper
:snowplow_cookie_domain,
:snowplow_enabled,
:snowplow_app_id,
- :snowplow_iglu_registry_url,
:push_event_hooks_limit,
:push_event_activities_limit,
:custom_http_clone_url_root,
@@ -328,7 +328,8 @@ module ApplicationSettingsHelper
:group_import_limit,
:group_export_limit,
:group_download_export_limit,
- :wiki_page_max_content_bytes
+ :wiki_page_max_content_bytes,
+ :container_registry_delete_tags_service_timeout
]
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index a57e27d23c8..7f8cb66a84f 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce).freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce atlassian_oauth2).freeze
LDAP_PROVIDER = /\Aldap/.freeze
def ldap_enabled?
@@ -133,6 +133,8 @@ module AuthHelper
# rubocop: disable CodeReuse/ActiveRecord
def auth_active?(provider)
+ return current_user.atlassian_identity.present? if provider == :atlassian_oauth2
+
current_user.identities.exists?(provider: provider.to_s)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 615c834c529..2eff87ae0ec 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -41,7 +41,7 @@ module BlobHelper
end
def encode_ide_path(path)
- url_encode(path).gsub('%2F', '/')
+ ERB::Util.url_encode(path).gsub('%2F', '/')
end
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
@@ -375,4 +375,9 @@ module BlobHelper
def human_access
@project.team.human_max_access(current_user&.id).try(:downcase)
end
+
+ def editing_ci_config?
+ @path.to_s.end_with?(Ci::Pipeline::CONFIG_EXTENSION) ||
+ @path.to_s == @project.ci_config_path_or_default
+ end
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index f8c00f3a4cd..6a4a7a8dfb2 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -11,13 +11,13 @@ module BoardsHelper
lists_endpoint: board_lists_path(board),
board_id: board.id,
disabled: (!can?(current_user, :create_non_backlog_issues, board)).to_s,
- issue_link_base: build_issue_link_base,
root_path: root_path,
full_path: full_path,
bulk_update_path: @bulk_issues_path,
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s,
recent_boards_endpoint: recent_boards_path,
- parent: current_board_parent.model_name.param_key
+ parent: current_board_parent.model_name.param_key,
+ group_id: @group&.id
}
end
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index 0344413b849..e876eb64029 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -6,6 +6,7 @@ module Ci
{
"endpoint" => project_job_path(@project, @build, format: :json),
"project_path" => @project.full_path,
+ "artifact_help_url" => help_page_path('user/gitlab_com/index.html', anchor: 'gitlab-cicd'),
"deployment_help_url" => help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting'),
"runner_help_url" => help_page_path('ci/runners/README.html', anchor: 'set-maximum-job-timeout-for-a-runner'),
"runner_settings_url" => project_runners_path(@build.project, anchor: 'js-runners-settings'),
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index 749726e0e33..309aa477108 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -2,16 +2,35 @@
module Ci
module PipelinesHelper
+ include Gitlab::Ci::Warnings
+
def pipeline_warnings(pipeline)
return unless pipeline.warning_messages.any?
- content_tag(:div, class: 'alert alert-warning') do
- content_tag(:h4, 'Warning:') <<
- content_tag(:div) do
- pipeline.warning_messages.each do |warning|
- concat(markdown(warning.content))
- end
- end
+ total_warnings = pipeline.warning_messages.length
+ message = warning_header(total_warnings)
+
+ content_tag(:div, class: 'bs-callout bs-callout-warning') do
+ content_tag(:details) do
+ concat content_tag(:summary, message, class: 'gl-mb-2')
+ warning_markdown(pipeline) { |markdown| concat markdown }
+ end
+ end
+ end
+
+ def warning_header(count)
+ message = _("%{total_warnings} warning(s) found:") % { total_warnings: count }
+
+ return message unless count > MAX_LIMIT
+
+ _("%{message} showing first %{warnings_displayed}") % { message: message, warnings_displayed: MAX_LIMIT }
+ end
+
+ private
+
+ def warning_markdown(pipeline)
+ pipeline.warning_messages(limit: MAX_LIMIT).each do |warning|
+ yield markdown(warning.content)
end
end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index b97e847c397..caad215e996 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -36,6 +36,12 @@ module ClustersHelper
}
end
+ def js_cluster_new
+ {
+ cluster_connect_help_path: help_page_path('user/project/clusters/add_remove_clusters', anchor: 'add-existing-cluster')
+ }
+ end
+
# This method is depreciated and will be removed when associated HAML files are moved to JavaScript
def provider_icon(provider = nil)
img_data = js_clusters_list_data.dig(:img_tags, provider&.to_sym) ||
diff --git a/app/helpers/container_registry_helper.rb b/app/helpers/container_registry_helper.rb
new file mode 100644
index 00000000000..9a5d84a90dd
--- /dev/null
+++ b/app/helpers/container_registry_helper.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module ContainerRegistryHelper
+ def limit_delete_tags_service?
+ Feature.enabled?(:container_registry_expiration_policies_throttling) &&
+ ContainerRegistry::Client.supports_tag_delete?
+ end
+end
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index 0ba03cd90ea..195b3162039 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -58,10 +58,6 @@ module DashboardHelper
links += [:activity, :milestones]
end
- if can?(current_user, :read_instance_statistics)
- links << :analytics
- end
-
links
end
end
diff --git a/app/helpers/deploy_tokens_helper.rb b/app/helpers/deploy_tokens_helper.rb
index 80a5bb44c69..d6fbe0b6b45 100644
--- a/app/helpers/deploy_tokens_helper.rb
+++ b/app/helpers/deploy_tokens_helper.rb
@@ -7,8 +7,13 @@ module DeployTokensHelper
Rails.env.test?
end
- def container_registry_enabled?(project)
+ def container_registry_enabled?(group_or_project)
Gitlab.config.registry.enabled &&
- can?(current_user, :read_container_image, project)
+ can?(current_user, :read_container_image, group_or_project)
+ end
+
+ def packages_registry_enabled?(group_or_project)
+ Gitlab.config.packages.enabled &&
+ can?(current_user, :read_package, group_or_project)
end
end
diff --git a/app/helpers/dev_ops_score_helper.rb b/app/helpers/dev_ops_report_helper.rb
index 9a673998149..ab7e56fc1a2 100644
--- a/app/helpers/dev_ops_score_helper.rb
+++ b/app/helpers/dev_ops_report_helper.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module DevOpsScoreHelper
+module DevOpsReportHelper
def score_level(score)
if score < 33.33
'low'
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 3b25de521d0..7c254e069f6 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -100,20 +100,43 @@ module DiffHelper
end
def submodule_link(blob, ref, repository = @repository)
- project_url, tree_url = submodule_links(blob, ref, repository)
- commit_id = if tree_url.nil?
- Commit.truncate_sha(blob.id)
- else
- link_to Commit.truncate_sha(blob.id), tree_url
- end
+ urls = submodule_links(blob, ref, repository)
+
+ folder_name = truncate(blob.name, length: 40)
+ folder_name = link_to(folder_name, urls.web) if urls&.web
+
+ commit_id = Commit.truncate_sha(blob.id)
+ commit_id = link_to(commit_id, urls.tree) if urls&.tree
[
- content_tag(:span, link_to(truncate(blob.name, length: 40), project_url)),
+ content_tag(:span, folder_name),
'@',
content_tag(:span, commit_id, class: 'commit-sha')
].join(' ').html_safe
end
+ def submodule_diff_compare_link(diff_file)
+ compare_url = submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository, diff_file)&.compare
+
+ link = ""
+
+ if compare_url
+
+ link_text = [
+ _('Compare'),
+ ' ',
+ content_tag(:span, Commit.truncate_sha(diff_file.old_blob.id), class: 'commit-sha'),
+ '...',
+ content_tag(:span, Commit.truncate_sha(diff_file.blob.id), class: 'commit-sha')
+ ].join('').html_safe
+
+ tooltip = _('Compare submodule commit revisions')
+ link = content_tag(:span, link_to(link_text, compare_url, class: 'btn has-tooltip', title: tooltip), class: 'submodule-compare')
+ end
+
+ link
+ end
+
def diff_file_blob_raw_url(diff_file, only_path: false)
project_raw_url(@project, tree_join(diff_file.content_sha, diff_file.file_path), only_path: only_path)
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 772a5f79a4d..84aa08281f6 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -62,20 +62,37 @@ module DropdownsHelper
end
def dropdown_title(title, options: {})
- content_tag :div, class: "dropdown-title" do
+ has_back = options.fetch(:back, false)
+ has_close = options.fetch(:close, true)
+
+ container_class = %w[dropdown-title gl-display-flex]
+ margin_class = []
+
+ if has_back && has_close
+ container_class << 'gl-justify-content-space-between'
+ elsif has_back
+ margin_class << 'gl-mr-auto'
+ elsif has_close
+ margin_class << 'gl-ml-auto'
+ end
+
+ container_class = container_class.join(' ')
+ margin_class = margin_class.join(' ')
+
+ content_tag :div, class: container_class do
title_output = []
- if options.fetch(:back, false)
- title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
- icon('arrow-left')
+ if has_back
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back " + margin_class, aria: { label: "Go back" }, type: "button") do
+ sprite_icon('arrow-left')
end
end
- title_output << content_tag(:span, title)
+ title_output << content_tag(:span, title, class: margin_class)
- if options.fetch(:close, true)
- title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
- icon('times', class: 'dropdown-menu-close-icon')
+ if has_close
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close " + margin_class, aria: { label: "Close" }, type: "button") do
+ sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
end
end
@@ -83,20 +100,11 @@ module DropdownsHelper
end
end
- def dropdown_input(placeholder, input_id: nil)
- content_tag :div, class: "dropdown-input" do
- filter_output = text_field_tag input_id, nil, class: "dropdown-input-field dropdown-no-filter", placeholder: placeholder, autocomplete: 'off'
- filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
-
- filter_output.html_safe
- end
- end
-
def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag search_id, nil, class: "dropdown-input-field qa-dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
filter_output << icon('search', class: "dropdown-input-search")
- filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
+ filter_output << sprite_icon('close', size: 16, css_class: 'dropdown-input-clear js-dropdown-input-clear')
filter_output.html_safe
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index ba2330dfc9a..d5c22927991 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -177,8 +177,53 @@ module EmailsHelper
strip_tags(render_message(:footer_message, style: ''))
end
+ def say_hi(user)
+ _('Hi %{username}!') % { username: sanitize_name(user.name) }
+ end
+
+ def say_hello(user)
+ _('Hello, %{username}!') % { username: sanitize_name(user.name) }
+ end
+
+ def two_factor_authentication_disabled_text
+ _('Two-factor authentication has been disabled for your GitLab account.')
+ end
+
+ def re_enable_two_factor_authentication_text(format: nil)
+ url = profile_two_factor_auth_url
+
+ case format
+ when :html
+ settings_link_to = generate_link(_('two-factor authentication settings'), url).html_safe
+ _("If you want to re-enable two-factor authentication, visit the %{settings_link_to} page.").html_safe % { settings_link_to: settings_link_to }
+ else
+ _('If you want to re-enable two-factor authentication, visit %{two_factor_link}') %
+ { two_factor_link: url }
+ end
+ end
+
+ def admin_changed_password_text(format: nil)
+ url = Gitlab.config.gitlab.url
+
+ case format
+ when :html
+ link_to = generate_link(url, url).html_safe
+ _('An administrator changed the password for your GitLab account on %{link_to}.').html_safe % { link_to: link_to }
+ else
+ _('An administrator changed the password for your GitLab account on %{link_to}.') % { link_to: url }
+ end
+ end
+
+ def contact_your_administrator_text
+ _('Please contact your administrator with any questions.')
+ end
+
private
+ def generate_link(text, url)
+ link_to(text, url, target: :_blank, rel: 'noopener noreferrer')
+ end
+
def show_footer?
email_header_and_footer_enabled? && current_appearance&.show_footer?
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 39be8ae9f60..7f0c59f65a0 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -12,8 +12,8 @@ module EnvironmentsHelper
def environments_folder_list_view_data
{
"endpoint" => folder_project_environments_path(@project, @folder, format: :json),
- "folder-name" => @folder,
- "can-read-environment" => can?(current_user, :read_environment, @project).to_s
+ "folder_name" => @folder,
+ "can_read_environment" => can?(current_user, :read_environment, @project).to_s
}
end
@@ -33,11 +33,11 @@ module EnvironmentsHelper
def environment_logs_data(project, environment)
{
- "environment-name": environment.name,
- "environments-path": project_environments_path(project, format: :json),
- "environment-id": environment.id,
- "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
- "clusters-path": project_clusters_path(project, format: :json)
+ "environment_name": environment.name,
+ "environments_path": project_environments_path(project, format: :json),
+ "environment_id": environment.id,
+ "cluster_applications_documentation_path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
+ "clusters_path": project_clusters_path(project, format: :json)
}
end
@@ -51,18 +51,18 @@ module EnvironmentsHelper
return {} unless project
{
- 'settings-path' => edit_project_service_path(project, 'prometheus'),
- 'clusters-path' => project_clusters_path(project),
- 'dashboards-endpoint' => project_performance_monitoring_dashboards_path(project, format: :json),
- 'default-branch' => project.default_branch,
- 'project-path' => project_path(project),
- 'tags-path' => project_tags_path(project),
- 'external-dashboard-url' => project.metrics_setting_external_dashboard_url,
- 'custom-metrics-path' => project_prometheus_metrics_path(project),
- 'validate-query-path' => validate_query_project_prometheus_metrics_path(project),
- 'custom-metrics-available' => "#{custom_metrics_available?(project)}",
- 'prometheus-alerts-available' => "#{can?(current_user, :read_prometheus_alerts, project)}",
- 'dashboard-timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase
+ 'settings_path' => edit_project_service_path(project, 'prometheus'),
+ 'clusters_path' => project_clusters_path(project),
+ 'dashboards_endpoint' => project_performance_monitoring_dashboards_path(project, format: :json),
+ 'default_branch' => project.default_branch,
+ 'project_path' => project_path(project),
+ 'tags_path' => project_tags_path(project),
+ 'external_dashboard_url' => project.metrics_setting_external_dashboard_url,
+ 'custom_metrics_path' => project_prometheus_metrics_path(project),
+ 'validate_query_path' => validate_query_project_prometheus_metrics_path(project),
+ 'custom_metrics_available' => "#{custom_metrics_available?(project)}",
+ 'prometheus_alerts_available' => "#{can?(current_user, :read_prometheus_alerts, project)}",
+ 'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase
}
end
@@ -70,11 +70,11 @@ module EnvironmentsHelper
return {} unless environment
{
- 'metrics-dashboard-base-path' => metrics_dashboard_base_path(environment, project),
- 'current-environment-name' => environment.name,
- 'has-metrics' => "#{environment.has_metrics?}",
- 'prometheus-status' => "#{environment.prometheus_status}",
- 'environment-state' => "#{environment.state}"
+ 'metrics_dashboard_base_path' => metrics_dashboard_base_path(environment, project),
+ 'current_environment_name' => environment.name,
+ 'has_metrics' => "#{environment.has_metrics?}",
+ 'prometheus_status' => "#{environment.prometheus_status}",
+ 'environment_state' => "#{environment.state}"
}
end
@@ -93,26 +93,26 @@ module EnvironmentsHelper
return {} unless project && environment
{
- 'metrics-endpoint' => additional_metrics_project_environment_path(project, environment, format: :json),
- 'dashboard-endpoint' => metrics_dashboard_project_environment_path(project, environment, format: :json),
- 'deployments-endpoint' => project_environment_deployments_path(project, environment, format: :json),
- 'alerts-endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json),
- 'operations-settings-path' => project_settings_operations_path(project),
- 'can-access-operations-settings' => can?(current_user, :admin_operations, project).to_s,
- 'panel-preview-endpoint' => project_metrics_dashboards_builder_path(project, format: :json)
+ 'metrics_endpoint' => additional_metrics_project_environment_path(project, environment, format: :json),
+ 'dashboard_endpoint' => metrics_dashboard_project_environment_path(project, environment, format: :json),
+ 'deployments_endpoint' => project_environment_deployments_path(project, environment, format: :json),
+ 'alerts_endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json),
+ 'operations_settings_path' => project_settings_operations_path(project),
+ 'can_access_operations_settings' => can?(current_user, :admin_operations, project).to_s,
+ 'panel_preview_endpoint' => project_metrics_dashboards_builder_path(project, format: :json)
}
end
def static_metrics_data
{
- 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'),
- 'add-dashboard-documentation-path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
- 'empty-getting-started-svg-path' => image_path('illustrations/monitoring/getting_started.svg'),
- 'empty-loading-svg-path' => image_path('illustrations/monitoring/loading.svg'),
- 'empty-no-data-svg-path' => image_path('illustrations/monitoring/no_data.svg'),
- 'empty-no-data-small-svg-path' => image_path('illustrations/chart-empty-state-small.svg'),
- 'empty-unable-to-connect-svg-path' => image_path('illustrations/monitoring/unable_to_connect.svg'),
- 'custom-dashboard-base-path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT
+ 'documentation_path' => help_page_path('administration/monitoring/prometheus/index.md'),
+ 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'empty_getting_started_svg_path' => image_path('illustrations/monitoring/getting_started.svg'),
+ 'empty_loading_svg_path' => image_path('illustrations/monitoring/loading.svg'),
+ 'empty_no_data_svg_path' => image_path('illustrations/monitoring/no_data.svg'),
+ 'empty_no_data_small_svg_path' => image_path('illustrations/chart-empty-state-small.svg'),
+ 'empty_unable_to_connect_svg_path' => image_path('illustrations/monitoring/unable_to_connect.svg'),
+ 'custom_dashboard_base_path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT
}
end
end
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 67bfeb22d92..3dde5afcb92 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -55,6 +55,29 @@ module FormHelper
dropdown_data
end
+ def reviewers_dropdown_options(issuable_type)
+ {
+ toggle_class: 'js-reviewer-search js-multiselect js-save-user-data',
+ title: 'Request review from',
+ filter: true,
+ dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-reviewer',
+ placeholder: _('Search users'),
+ data: {
+ first_user: current_user&.username,
+ null_user: true,
+ current_user: true,
+ project_id: (@target_project || @project)&.id,
+ field_name: "#{issuable_type}[reviewer_ids][]",
+ default_label: 'Unassigned',
+ 'dropdown-header': 'Reviewer(s)',
+ multi_select: true,
+ 'input-meta': 'name',
+ 'always-show-selectbox': true,
+ current_user_info: UserSerializer.new.represent(current_user)
+ }
+ }
+ end
+
# Overwritten
def issue_supports_multiple_assignees?
false
diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb
index 1952325c504..dcff2be34da 100644
--- a/app/helpers/groups/group_members_helper.rb
+++ b/app/helpers/groups/group_members_helper.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
module Groups::GroupMembersHelper
+ include AvatarsHelper
+
+ AVATAR_SIZE = 40
+
def group_member_select_options
{ multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true }
end
@@ -8,6 +12,81 @@ module Groups::GroupMembersHelper
def render_invite_member_for_group(group, default_access_level)
render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: GroupMember.access_level_roles, default_access_level: default_access_level
end
+
+ def linked_groups_data_json(group_links)
+ GroupGroupLinkSerializer.new.represent(group_links).to_json
+ end
+
+ def members_data_json(group, members)
+ members_data(group, members).to_json
+ end
+
+ private
+
+ def members_data(group, members)
+ members.map do |member|
+ user = member.user
+ source = member.source
+
+ data = {
+ id: member.id,
+ created_at: member.created_at,
+ expires_at: member.expires_at&.to_time,
+ requested_at: member.requested_at,
+ can_update: member.can_update?,
+ can_remove: member.can_remove?,
+ can_override: member.can_override?,
+ access_level: {
+ string_value: member.human_access,
+ integer_value: member.access_level
+ },
+ source: {
+ id: source.id,
+ name: source.full_name,
+ web_url: Gitlab::UrlBuilder.build(source)
+ }
+ }.merge(member_created_by_data(member.created_by))
+
+ if user.present?
+ data[:user] = member_user_data(user)
+ else
+ data[:invite] = member_invite_data(member)
+ end
+
+ data
+ end
+ end
+
+ def member_created_by_data(created_by)
+ return {} unless created_by.present?
+
+ {
+ created_by: {
+ name: created_by.name,
+ web_url: Gitlab::UrlBuilder.build(created_by)
+ }
+ }
+ end
+
+ def member_user_data(user)
+ {
+ id: user.id,
+ name: user.name,
+ username: user.username,
+ web_url: Gitlab::UrlBuilder.build(user),
+ avatar_url: avatar_icon_for_user(user, AVATAR_SIZE),
+ blocked: user.blocked?,
+ two_factor_enabled: user.two_factor_enabled?
+ }
+ end
+
+ def member_invite_data(member)
+ {
+ email: member.invite_email,
+ avatar_url: avatar_icon_for_email(member.invite_email, AVATAR_SIZE),
+ can_resend: member.can_resend_invite?
+ }
+ end
end
Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper')
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index eb80acd869f..06a52457fd6 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -167,8 +167,23 @@ module GroupsHelper
@group.packages_feature_enabled?
end
+ def show_invite_banner?(group)
+ Feature.enabled?(:invite_your_teammates_banner_a, group) &&
+ can?(current_user, :admin_group, group) &&
+ !just_created? &&
+ !multiple_members?(group)
+ end
+
private
+ def just_created?
+ flash[:notice] =~ /successfully created/
+ end
+
+ def multiple_members?(group)
+ group.member_count > 1
+ end
+
def get_group_sidebar_links
links = [:overview, :group_members]
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 9957d5c6330..0352b0ddf28 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -100,7 +100,7 @@ module IconsHelper
def boolean_to_icon(value)
if value
- icon('circle', class: 'cgreen')
+ sprite_icon('check', css_class: 'cgreen')
else
sprite_icon('power', css_class: 'clgray')
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 0b859a39c4f..b255597b18d 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -205,6 +205,12 @@ module IssuablesHelper
author_output
end
+ if access = project.team.human_max_access(issuable.author_id)
+ output << content_tag(:span, access, class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3 ", title: _("This user is a %{access} of the %{name} project.") % { access: access.downcase, name: project.name })
+ elsif project.team.contributor?(issuable.author_id)
+ output << content_tag(:span, _("Contributor"), class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3", title: _("This user has previously committed to the %{name} project.") % { name: project.name })
+ end
+
output << content_tag(:span, (sprite_icon('first-contribution', css_class: 'gl-icon gl-vertical-align-middle') if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!'))
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block gl-ml-3")
@@ -292,8 +298,10 @@ module IssuablesHelper
{
hasClosingMergeRequest: issuable.merge_requests_count(current_user) != 0,
+ issueType: issuable.issue_type,
zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
- sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier # rubocop:disable CodeReuse/ActiveRecord
+ sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier, # rubocop:disable CodeReuse/ActiveRecord
+ iid: issuable.iid.to_s
}
end
@@ -301,8 +309,8 @@ module IssuablesHelper
return { groupPath: parent.path } if parent.is_a?(Group)
{
- projectPath: ref_project.path,
- projectNamespace: ref_project.namespace.full_path
+ projectPath: ref_project.path,
+ projectNamespace: ref_project.namespace.full_path
}
end
@@ -464,6 +472,7 @@ module IssuablesHelper
rootPath: root_path,
fullPath: issuable[:project_full_path],
iid: issuable[:iid],
+ severity: issuable[:severity],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours
}
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 55170cbfa6b..e8ea39d7ffc 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -4,7 +4,7 @@ module IssuesHelper
def issue_css_classes(issue)
classes = ["issue"]
classes << "closed" if issue.closed?
- classes << "today" if issue.today?
+ classes << "today" if issue.new?
classes << "user-can-drag" if @sort == 'relative_position'
classes.join(' ')
end
diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb
index ac987a04895..0c5744b46ae 100644
--- a/app/helpers/lazy_image_tag_helper.rb
+++ b/app/helpers/lazy_image_tag_helper.rb
@@ -25,5 +25,5 @@ module LazyImageTagHelper
end
# Required for Banzai::Filter::ImageLazyLoadFilter
- module_function :placeholder_image
+ module_function :placeholder_image # rubocop: disable Style/AccessModifierDeclarations
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index d849ed9d076..578c7ae7923 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -47,7 +47,7 @@ module NavHelper
end
def has_extra_nav_icons?
- Gitlab::Sherlock.enabled? || can?(current_user, :read_instance_statistics) || current_user.admin?
+ Gitlab::Sherlock.enabled? || current_user.admin?
end
def page_has_markdown?
@@ -62,6 +62,10 @@ module NavHelper
%w(system_info background_jobs health_check requests_profiles)
end
+ def admin_analytics_nav_links
+ %w(dev_ops_report cohorts)
+ end
+
def group_issues_sub_menu_items
%w(groups#issues labels#index milestones#index boards#index boards#show)
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 2ba1d841c2e..c02adfcf4c6 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -85,6 +85,10 @@ module NotesHelper
note.project.team.max_member_access(note.author_id)
end
+ def note_human_max_access(note)
+ note.project.team.human_max_access(note.author_id)
+ end
+
def discussion_path(discussion)
if discussion.for_merge_request?
return unless discussion.diff_discussion?
@@ -181,7 +185,7 @@ module NotesHelper
reopenPath: reopen_issuable_path(issuable),
notesPath: notes_url,
prerenderedNotesCount: issuable.capped_notes_count(MAX_PRERENDERED_NOTES),
- lastFetchedAt: Time.now.to_i
+ lastFetchedAt: Time.now.to_i * ::Gitlab::UpdatedNotesPaginator::MICROSECOND
}
if issuable.is_a?(MergeRequest)
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 9a64fe98f86..784b242e2b5 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -6,15 +6,15 @@ module NotificationsHelper
def notification_icon_class(level)
case level.to_sym
when :disabled, :owner_disabled
- 'microphone-slash'
+ 'notifications-off'
when :participating
- 'volume-up'
+ 'notifications'
when :watch
'eye'
when :mention
'at'
when :global
- 'globe'
+ 'earth'
end
end
@@ -28,8 +28,8 @@ module NotificationsHelper
end
end
- def notification_icon(level, text = nil)
- icon("#{notification_icon_class(level)} fw", text: text)
+ def notification_icon(level)
+ sprite_icon("#{notification_icon_class(level)}")
end
def notification_title(level)
diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb
index 37e91153710..521f394a920 100644
--- a/app/helpers/operations_helper.rb
+++ b/app/helpers/operations_helper.rb
@@ -43,6 +43,7 @@ module OperationsHelper
create_issue: setting.create_issue.to_s,
issue_template_key: setting.issue_template_key.to_s,
send_email: setting.send_email.to_s,
+ auto_close_incident: setting.auto_close_incident.to_s,
pagerduty_active: setting.pagerduty_active.to_s,
pagerduty_token: setting.pagerduty_token.to_s,
pagerduty_webhook_url: project_incidents_integrations_pagerduty_url(@project, token: setting.pagerduty_token),
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 5a917a02d51..2c406641882 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -61,6 +61,10 @@ module PreferencesHelper
@user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
end
+ def user_application_theme_name
+ @user_application_theme_name ||= Gitlab::Themes.for_user(current_user).name.downcase.tr(' ', '_')
+ end
+
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end
@@ -76,6 +80,13 @@ module PreferencesHelper
)
end
+ def integration_views
+ [].tap do |views|
+ views << 'gitpod' if Gitlab::Gitpod.feature_and_settings_enabled?
+ views << 'sourcegraph' if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
+ end
+ end
+
private
# Ensure that anyone adding new options updates `DASHBOARD_CHOICES` too
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 1ce4903f8df..72cc07b13a5 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -7,7 +7,7 @@ module ProjectsHelper
end
def link_to_project(project)
- link_to namespace_project_path(namespace_id: project.namespace, id: project), title: h(project.name) do
+ link_to namespace_project_path(namespace_id: project.namespace, id: project), title: h(project.name), class: 'gl-link' do
title = content_tag(:span, project.name, class: 'project-name')
if project.namespace
@@ -109,7 +109,7 @@ module ProjectsHelper
end
def transfer_project_message(project)
- _("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") %
+ _("You are going to transfer %{project_full_name} to another namespace. Are you ABSOLUTELY sure?") %
{ project_full_name: project.full_name }
end
@@ -640,7 +640,7 @@ module ProjectsHelper
pagesAvailable: Gitlab.config.pages.enabled,
pagesAccessControlEnabled: Gitlab.config.pages.access_control,
pagesAccessControlForced: ::Gitlab::Pages.access_control_is_forced?,
- pagesHelpPath: help_page_path('user/project/pages/introduction', anchor: 'gitlab-pages-access-control-core')
+ pagesHelpPath: help_page_path('user/project/pages/introduction', anchor: 'gitlab-pages-access-control')
}
end
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index f1dff18523f..979a68ecb7b 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -15,6 +15,7 @@ module ReleasesHelper
def data_for_releases_page
{
project_id: @project.id,
+ project_path: @project.full_path,
illustration_path: illustration,
documentation_path: help_page
}.tap do |data|
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 377aee1ae9e..d55ad878b92 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
module SearchHelper
- SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze
+ SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets, :state].freeze
def search_autocomplete_opts(term)
return unless current_user
resources_results = [
+ recent_merge_requests_autocomplete(term),
+ recent_issues_autocomplete(term),
groups_autocomplete(term),
projects_autocomplete(term)
].flatten
@@ -178,6 +180,34 @@ module SearchHelper
}
end
end
+
+ def recent_merge_requests_autocomplete(term, limit = 5)
+ return [] unless current_user
+
+ ::Gitlab::Search::RecentMergeRequests.new(user: current_user).search(term).limit(limit).map do |mr|
+ {
+ category: "Recent merge requests",
+ id: mr.id,
+ label: search_result_sanitize(mr.title),
+ url: merge_request_path(mr),
+ avatar_url: mr.project.avatar_url || ''
+ }
+ end
+ end
+
+ def recent_issues_autocomplete(term, limit = 5)
+ return [] unless current_user
+
+ ::Gitlab::Search::RecentIssues.new(user: current_user).search(term).limit(limit).map do |i|
+ {
+ category: "Recent issues",
+ id: i.id,
+ label: search_result_sanitize(i.title),
+ url: issue_path(i),
+ avatar_url: i.project.avatar_url || ''
+ }
+ end
+ end
# rubocop: enable CodeReuse/ActiveRecord
def search_result_sanitize(str)
@@ -250,15 +280,16 @@ module SearchHelper
# Sanitize a HTML field for search display. Most tags are stripped out and the
# maximum length is set to 200 characters.
- def search_md_sanitize(object, field)
- html = markdown_field(object, field)
- html = Truncato.truncate(
- html,
+ def search_md_sanitize(source)
+ source = Truncato.truncate(
+ source,
count_tags: false,
count_tail: false,
max_length: 200
)
+ html = markdown(source)
+
# Truncato's filtered_tags and filtered_attributes are not quite the same
sanitize(html, tags: %w(a p ol ul li pre code))
end
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index e9d39cc8175..6b5de73a831 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -92,9 +92,15 @@ module ServicesHelper
commit_events: integration.commit_events.to_s,
enable_comments: integration.comment_on_event_enabled.to_s,
comment_detail: integration.comment_detail,
+ learn_more_path: integrations_help_page_path,
trigger_events: trigger_events_for_service(integration),
fields: fields_for_service(integration),
- inherit_from_id: integration.inherit_from_id
+ inherit_from_id: integration.inherit_from_id,
+ integration_level: integration_level(integration),
+ editable: integration.editable?.to_s,
+ cancel_path: scoped_integrations_path,
+ can_test: integration.can_test?.to_s,
+ test_path: scoped_test_integration_path(integration)
}
end
@@ -106,11 +112,31 @@ module ServicesHelper
ServiceFieldSerializer.new(service: integration).represent(integration.global_fields).to_json
end
+ def integrations_help_page_path
+ help_page_path('user/admin_area/settings/project_integration_management')
+ end
+
def project_jira_issues_integration?
false
end
+ def group_level_integrations?
+ @group.present? && Feature.enabled?(:group_level_integrations, @group)
+ end
+
extend self
+
+ private
+
+ def integration_level(integration)
+ if integration.instance
+ 'instance'
+ elsif integration.group_id
+ 'group'
+ else
+ 'project'
+ end
+ end
end
ServicesHelper.prepend_if_ee('EE::ServicesHelper')
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 10c95da394f..94c46feb8ae 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -12,7 +12,7 @@ module SnippetsHelper
end
def download_raw_snippet_button(snippet)
- link_to(icon('download'),
+ link_to(sprite_icon('download'),
gitlab_raw_snippet_path(snippet, inline: false),
target: '_blank',
rel: 'noopener noreferrer',
diff --git a/app/helpers/startup_css_helper.rb b/app/helpers/startup_css_helper.rb
new file mode 100644
index 00000000000..b54e19bfc0d
--- /dev/null
+++ b/app/helpers/startup_css_helper.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module StartupCssHelper
+ def use_startup_css?
+ (Feature.enabled?(:startup_css) || params[:startup_css] == 'true' || cookies['startup_css'] == 'true') && !Rails.env.test?
+ end
+end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 06ea3dd8a81..959178c47d7 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -6,19 +6,19 @@ module SubmoduleHelper
VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze
# links to files listing for submodule if submodule is a project on this server
- def submodule_links(submodule_item, ref = nil, repository = @repository)
- repository.submodule_links.for(submodule_item, ref)
+ def submodule_links(submodule_item, ref = nil, repository = @repository, diff_file = nil)
+ repository.submodule_links.for(submodule_item, ref, diff_file)
end
- def submodule_links_for_url(submodule_item_id, url, repository)
- return [nil, nil] unless url
+ def submodule_links_for_url(submodule_item_id, url, repository, old_submodule_item_id = nil)
+ return [nil, nil, nil] unless url
if url == '.' || url == './'
url = File.join(Gitlab.config.gitlab.url, repository.project.full_path)
end
if url =~ %r{([^/:]+)/([^/]+(?:\.git)?)\Z}
- namespace, project = $1, $2
+ namespace, project = Regexp.last_match(1), Regexp.last_match(2)
gitlab_hosts = [Gitlab.config.gitlab.url,
Gitlab.config.gitlab_shell.ssh_path_prefix]
@@ -34,21 +34,24 @@ module SubmoduleHelper
project.sub!(/\.git\z/, '')
if self_url?(url, namespace, project)
- [url_helpers.namespace_project_path(namespace, project),
- url_helpers.namespace_project_tree_path(namespace, project, submodule_item_id)]
+ [
+ url_helpers.namespace_project_path(namespace, project),
+ url_helpers.namespace_project_tree_path(namespace, project, submodule_item_id),
+ (url_helpers.namespace_project_compare_path(namespace, project, to: submodule_item_id, from: old_submodule_item_id) if old_submodule_item_id)
+ ]
elsif relative_self_url?(url)
- relative_self_links(url, submodule_item_id, repository.project)
+ relative_self_links(url, submodule_item_id, old_submodule_item_id, repository.project)
elsif gist_github_dot_com_url?(url)
gist_github_com_tree_links(namespace, project, submodule_item_id)
elsif github_dot_com_url?(url)
- github_com_tree_links(namespace, project, submodule_item_id)
+ github_com_tree_links(namespace, project, submodule_item_id, old_submodule_item_id)
elsif gitlab_dot_com_url?(url)
- gitlab_com_tree_links(namespace, project, submodule_item_id)
+ gitlab_com_tree_links(namespace, project, submodule_item_id, old_submodule_item_id)
else
- [sanitize_submodule_url(url), nil]
+ [sanitize_submodule_url(url), nil, nil]
end
else
- [sanitize_submodule_url(url), nil]
+ [sanitize_submodule_url(url), nil, nil]
end
end
@@ -79,22 +82,30 @@ module SubmoduleHelper
url.start_with?('../', './')
end
- def gitlab_com_tree_links(namespace, project, commit)
+ def gitlab_com_tree_links(namespace, project, commit, old_commit)
base = ['https://gitlab.com/', namespace, '/', project].join('')
- [base, [base, '/-/tree/', commit].join('')]
+ [
+ base,
+ [base, '/-/tree/', commit].join(''),
+ ([base, '/-/compare/', old_commit, '...', commit].join('') if old_commit)
+ ]
end
def gist_github_com_tree_links(namespace, project, commit)
base = ['https://gist.github.com/', namespace, '/', project].join('')
- [base, [base, commit].join('/')]
+ [base, [base, commit].join('/'), nil]
end
- def github_com_tree_links(namespace, project, commit)
+ def github_com_tree_links(namespace, project, commit, old_commit)
base = ['https://github.com/', namespace, '/', project].join('')
- [base, [base, '/tree/', commit].join('')]
+ [
+ base,
+ [base, '/tree/', commit].join(''),
+ ([base, '/compare/', old_commit, '...', commit].join('') if old_commit)
+ ]
end
- def relative_self_links(relative_path, commit, project)
+ def relative_self_links(relative_path, commit, old_commit, project)
relative_path = relative_path.rstrip
absolute_project_path = "/" + project.full_path
@@ -107,7 +118,7 @@ module SubmoduleHelper
target_namespace_path = File.dirname(submodule_project_path)
if target_namespace_path == '/' || target_namespace_path.start_with?(absolute_project_path)
- return [nil, nil]
+ return [nil, nil, nil]
end
target_namespace_path.sub!(%r{^/}, '')
@@ -116,10 +127,11 @@ module SubmoduleHelper
begin
[
url_helpers.namespace_project_path(target_namespace_path, submodule_base),
- url_helpers.namespace_project_tree_path(target_namespace_path, submodule_base, commit)
+ url_helpers.namespace_project_tree_path(target_namespace_path, submodule_base, commit),
+ (url_helpers.namespace_project_compare_path(target_namespace_path, submodule_base, to: commit, from: old_commit) if old_commit)
]
rescue ActionController::UrlGenerationError
- [nil, nil]
+ [nil, nil, nil]
end
end
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index 6ea6a33ba5e..0227ad1092d 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -7,7 +7,7 @@ module SystemNoteHelper
'description' => 'pencil-square',
'merge' => 'git-merge',
'merged' => 'git-merge',
- 'opened' => 'issue-open',
+ 'opened' => 'issues',
'closed' => 'issue-close',
'time_tracking' => 'timer',
'assignee' => 'user',
@@ -33,7 +33,8 @@ module SystemNoteHelper
'designs_removed' => 'doc-image',
'designs_discussion_added' => 'doc-image',
'status' => 'status',
- 'alert_issue_added' => 'issues'
+ 'alert_issue_added' => 'issues',
+ 'new_alert_added' => 'warning'
}.freeze
def system_note_icon_name(note)
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 0f156003a01..6fbe2642056 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -127,5 +127,3 @@ module TabHelper
end
end
end
-
-TabHelper.prepend_if_ee('EE::TabHelper')
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 90a5b6da4c7..7644ed783eb 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -191,16 +191,46 @@ module TreeHelper
def vue_file_list_data(project, ref)
{
- can_push_code: current_user&.can?(:push_code, project) && "true",
project_path: project.full_path,
project_short_path: project.path,
- fork_path: current_user&.fork_of(project)&.full_path,
ref: ref,
escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref),
full_name: project.name_with_namespace
}
end
+ def ide_base_path(project)
+ can_push_code = current_user&.can?(:push_code, project)
+ fork_path = current_user&.fork_of(project)&.full_path
+
+ if can_push_code
+ project.full_path
+ else
+ fork_path || project.full_path
+ end
+ end
+
+ def vue_ide_link_data(project, ref)
+ can_collaborate = can_collaborate_with_project?(project)
+ can_create_mr_from_fork = can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
+ show_web_ide_button = (can_collaborate || current_user&.already_forked?(project) || can_create_mr_from_fork)
+
+ {
+ ide_base_path: ide_base_path(project),
+ needs_to_fork: !can_collaborate && !current_user&.already_forked?(project),
+ show_web_ide_button: show_web_ide_button,
+ show_gitpod_button: show_web_ide_button && Gitlab::Gitpod.feature_and_settings_enabled?(project),
+ gitpod_url: full_gitpod_url(project, ref),
+ gitpod_enabled: current_user&.gitpod_enabled
+ }
+ end
+
+ def full_gitpod_url(project, ref)
+ return "" unless Gitlab::Gitpod.feature_and_settings_enabled?(project)
+
+ "#{Gitlab::CurrentSettings.gitpod_url}##{project_tree_url(project, tree_join(ref, @path || ''))}"
+ end
+
def directory_download_links(project, ref, archive_prefix)
Gitlab::Workhorse::ARCHIVE_FORMATS.map do |fmt|
{
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 98159369fb1..967271a8431 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -5,9 +5,11 @@ module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
GCP_SIGNUP_OFFER = 'gcp_signup_offer'
SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
+ SERVICE_TEMPLATES_DEPRECATED = 'service_templates_deprecated'
TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
WEBHOOKS_MOVED = 'webhooks_moved'
CUSTOMIZE_HOMEPAGE = 'customize_homepage'
+ WEB_IDE_ALERT_DISMISSED = 'web_ide_alert_dismissed'
def show_admin_integrations_moved?
!user_dismissed?(ADMIN_INTEGRATIONS_MOVED)
@@ -37,6 +39,10 @@ module UserCalloutsHelper
!user_dismissed?(SUGGEST_POPOVER_DISMISSED)
end
+ def show_service_templates_deprecated?
+ !user_dismissed?(SERVICE_TEMPLATES_DEPRECATED)
+ end
+
def show_webhooks_moved_alert?
!user_dismissed?(WEBHOOKS_MOVED)
end
@@ -45,6 +51,10 @@ module UserCalloutsHelper
customize_homepage && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
end
+ def show_web_ide_alert?
+ !user_dismissed?(WEB_IDE_ALERT_DISMISSED)
+ end
+
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb
new file mode 100644
index 00000000000..f0044daa645
--- /dev/null
+++ b/app/helpers/whats_new_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module WhatsNewHelper
+ EMPTY_JSON = ''.to_json
+
+ def whats_new_most_recent_release_items
+ YAML.load_file(most_recent_release_file_path).to_json
+
+ rescue => e
+ Gitlab::ErrorTracking.track_exception(e, yaml_file_path: most_recent_release_file_path)
+
+ EMPTY_JSON
+ end
+
+ private
+
+ def most_recent_release_file_path
+ Dir.glob(files_path).max
+ end
+
+ def files_path
+ Rails.root.join('data', 'whats_new', '*.yml')
+ end
+end
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index ad33ac66f38..8c756b9370b 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -69,7 +69,12 @@ module WikiHelper
end
def wiki_attachment_upload_url
- expose_url(api_v4_projects_wikis_attachments_path(id: @wiki.container.id))
+ case @wiki.container
+ when Project
+ expose_url(api_v4_projects_wikis_attachments_path(id: @wiki.container.id))
+ else
+ raise TypeError, "Unsupported wiki container #{@wiki.container.class}"
+ end
end
def wiki_sort_controls(wiki, sort, direction)
@@ -147,3 +152,5 @@ module WikiHelper
!container.has_confluence?
end
end
+
+WikiHelper.prepend_if_ee('EE::WikiHelper')
diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb
index cbaf53fced1..a02670aed90 100644
--- a/app/mailers/devise_mailer.rb
+++ b/app/mailers/devise_mailer.rb
@@ -9,6 +9,10 @@ class DeviseMailer < Devise::Mailer
helper EmailsHelper
helper ApplicationHelper
+ def password_change_by_admin(record, opts = {})
+ devise_mail(record, :password_change_by_admin, opts)
+ end
+
protected
def subject_for(key)
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 07ce9bba784..3a13c5949bd 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -51,9 +51,32 @@ module Emails
return unless member_exists?
- member_email_with_layout(
- to: member.invite_email,
- subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}"))
+ subject_line = subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
+
+ if member.invite_to_unknown_user? && Feature.enabled?(:invite_email_experiment)
+ subject_line = subject("#{member.created_by.name} invited you to join GitLab") if member.created_by
+ @invite_url_params = { new_user_invite: 'experiment' }
+
+ member_email_with_layout(
+ to: member.invite_email,
+ subject: subject_line,
+ template: 'member_invited_email_experiment',
+ layout: 'experiment_mailer'
+ )
+
+ Gitlab::Tracking.event(Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category], 'sent', property: 'experiment_group')
+ else
+ @invite_url_params = member.invite_to_unknown_user? ? { new_user_invite: 'control' } : {}
+
+ member_email_with_layout(
+ to: member.invite_email,
+ subject: subject_line
+ )
+
+ if member.invite_to_unknown_user?
+ Gitlab::Tracking.event(Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category], 'sent', property: 'control_group')
+ end
+ end
end
def member_invite_accepted_email(member_source_type, member_id)
@@ -107,10 +130,15 @@ module Emails
@member_source_type.classify.constantize
end
- def member_email_with_layout(to:, subject:)
+ def member_email_with_layout(to:, subject:, template: nil, layout: 'mailer')
mail(to: to, subject: subject) do |format|
- format.html { render layout: 'mailer' }
- format.text { render layout: 'mailer' }
+ if template
+ format.html { render template, layout: layout }
+ format.text { render template, layout: layout }
+ else
+ format.html { render layout: layout }
+ format.text { render layout: layout }
+ end
end
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index b45755788b8..96cf3571968 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -72,6 +72,16 @@ module Emails
end
end
end
+
+ def disabled_two_factor_email(user)
+ return unless user
+
+ @user = user
+
+ Gitlab::I18n.with_locale(@user.preferred_language) do
+ mail(to: @user.notification_email, subject: subject(_("Two-factor authentication disabled")))
+ end
+ end
end
end
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 75581805b49..e9b89af45c6 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -11,6 +11,8 @@ module AlertManagement
include Noteable
include Gitlab::SQL::Pattern
include Presentable
+ include Gitlab::Utils::StrongMemoize
+ include Referable
STATUSES = {
triggered: 0,
@@ -31,8 +33,6 @@ module AlertManagement
:acknowledged
].freeze
- DETAILS_IGNORED_PARAMS = %w(start_time).freeze
-
belongs_to :project
belongs_to :issue, optional: true
belongs_to :prometheus_alert, optional: true
@@ -118,7 +118,7 @@ module AlertManagement
end
delegate :iid, to: :issue, prefix: true, allow_nil: true
- delegate :metrics_dashboard_url, :runbook, :details_url, to: :present
+ delegate :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_status, -> (status) { where(status: status) }
@@ -171,10 +171,23 @@ module AlertManagement
with_prometheus_alert.where(id: ids)
end
- def details
- details_payload = payload.except(*attributes.keys, *DETAILS_IGNORED_PARAMS)
+ def self.reference_prefix
+ '^alert#'
+ end
- Gitlab::Utils::InlineHash.merge_keys(details_payload)
+ def self.reference_pattern
+ @reference_pattern ||= %r{
+ (#{Project.reference_pattern})?
+ #{Regexp.escape(reference_prefix)}(?<alert>\d+)
+ }x
+ end
+
+ def self.link_reference_pattern
+ @link_reference_pattern ||= super("alert_management", /(?<alert>\d+)\/details(\#)?/)
+ end
+
+ def self.reference_valid?(reference)
+ reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
def prometheus?
@@ -185,10 +198,10 @@ module AlertManagement
increment!(:events)
end
- # required for todos (typically contains an identifier like issue iid)
- # no-op; we could use iid, but we don't have a reference prefix
- def to_reference(_from = nil, full: false)
- ''
+ def to_reference(from = nil, full: false)
+ reference = "#{self.class.reference_prefix}#{iid}"
+
+ "#{project.to_reference_base(from, full: full)}#{reference}"
end
def execute_services
@@ -197,10 +210,12 @@ module AlertManagement
project.execute_services(hook_data, :alert_hooks)
end
- def present
- return super(presenter_class: AlertManagement::PrometheusAlertPresenter) if prometheus?
-
- super
+ # Representation of the alert's payload. Avoid accessing
+ # #payload attribute directly.
+ def parsed_payload
+ strong_memoize(:parsed_payload) do
+ Gitlab::AlertManagement::Payload.parse(project, payload, monitoring_tool: monitoring_tool)
+ end
end
private
diff --git a/app/models/analytics/instance_statistics.rb b/app/models/analytics/instance_statistics.rb
new file mode 100644
index 00000000000..df7b26e4fa6
--- /dev/null
+++ b/app/models/analytics/instance_statistics.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Analytics
+ module InstanceStatistics
+ def self.table_name_prefix
+ 'analytics_instance_statistics_'
+ end
+ end
+end
diff --git a/app/models/analytics/instance_statistics/measurement.rb b/app/models/analytics/instance_statistics/measurement.rb
new file mode 100644
index 00000000000..eaaf9e999b3
--- /dev/null
+++ b/app/models/analytics/instance_statistics/measurement.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Analytics
+ module InstanceStatistics
+ class Measurement < ApplicationRecord
+ enum identifier: {
+ projects: 1,
+ users: 2,
+ issues: 3,
+ merge_requests: 4,
+ groups: 5,
+ pipelines: 6
+ }
+
+ IDENTIFIER_QUERY_MAPPING = {
+ identifiers[:projects] => -> { Project },
+ identifiers[:users] => -> { User },
+ identifiers[:issues] => -> { Issue },
+ identifiers[:merge_requests] => -> { MergeRequest },
+ identifiers[:groups] => -> { Group },
+ identifiers[:pipelines] => -> { Ci::Pipeline }
+ }.freeze
+
+ validates :recorded_at, :identifier, :count, presence: true
+ validates :recorded_at, uniqueness: { scope: :identifier }
+
+ scope :order_by_latest, -> { order(recorded_at: :desc) }
+ scope :with_identifier, -> (identifier) { where(identifier: identifier) }
+ end
+ end
+end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 91b8bfedcbb..6ffb9b7642a 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -13,6 +13,10 @@ class ApplicationRecord < ActiveRecord::Base
where(id: ids)
end
+ def self.primary_key_in(values)
+ where(primary_key => values)
+ end
+
def self.iid_in(iids)
where(iid: iids)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index eb46be65858..e9a3dcf39df 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -8,6 +8,8 @@ class ApplicationSetting < ApplicationRecord
include IgnorableColumns
ignore_column :namespace_storage_size_limit, remove_with: '13.5', remove_after: '2020-09-22'
+ ignore_column :instance_statistics_visibility_private, remove_with: '13.6', remove_after: '2020-10-22'
+ ignore_column :snowplow_iglu_registry_url, remove_with: '13.6', remove_after: '2020-11-22'
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
@@ -20,7 +22,9 @@ class ApplicationSetting < ApplicationRecord
belongs_to :push_rule
alias_attribute :self_monitoring_project_id, :instance_administration_project_id
- belongs_to :instance_administrators_group, class_name: "Group"
+ belongs_to :instance_group, class_name: "Group", foreign_key: 'instance_administrators_group_id'
+ alias_attribute :instance_group_id, :instance_administrators_group_id
+ alias_attribute :instance_administrators_group, :instance_group
def self.repository_storages_weighted_attributes
@repository_storages_weighted_atributes ||= Gitlab.config.repositories.storages.keys.map { |k| "repository_storages_weighted_#{k}".to_sym }.freeze
@@ -128,16 +132,16 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :sourcegraph_enabled
+ validates :gitpod_url,
+ presence: true,
+ addressable_url: { enforce_sanitization: true },
+ if: :gitpod_enabled
+
validates :snowplow_collector_hostname,
presence: true,
hostname: true,
if: :snowplow_enabled
- validates :snowplow_iglu_registry_url,
- addressable_url: true,
- allow_blank: true,
- if: :snowplow_enabled
-
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -281,6 +285,9 @@ class ApplicationSetting < ApplicationRecord
validates :hashed_storage_enabled, inclusion: { in: [true], message: _("Hashed storage can't be disabled anymore for new projects") }
+ validates :container_registry_delete_tags_service_timeout,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 8bdb80a65b1..7a869d16a31 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -74,6 +74,8 @@ module ApplicationSettingImplementation
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
+ gitpod_enabled: false,
+ gitpod_url: 'https://gitpod.io/',
gravatar_enabled: Settings.gravatar['enabled'],
group_download_export_limit: 1,
group_export_limit: 6,
@@ -87,7 +89,6 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'],
- instance_statistics_visibility_private: false,
issues_create_limit: 300,
local_markdown_version: 0,
login_recaptcha_protection_enabled: false,
@@ -132,7 +133,6 @@ module ApplicationSettingImplementation
snowplow_collector_hostname: nil,
snowplow_cookie_domain: nil,
snowplow_enabled: false,
- snowplow_iglu_registry_url: nil,
sourcegraph_enabled: false,
sourcegraph_public_only: true,
sourcegraph_url: nil,
@@ -164,7 +164,8 @@ module ApplicationSettingImplementation
user_default_external: false,
user_default_internal_regex: nil,
user_show_add_ssh_key_message: true,
- wiki_page_max_content_bytes: 50.megabytes
+ wiki_page_max_content_bytes: 50.megabytes,
+ container_registry_delete_tags_service_timeout: 100
}
end
diff --git a/app/models/atlassian/identity.rb b/app/models/atlassian/identity.rb
new file mode 100644
index 00000000000..906f2be0fbf
--- /dev/null
+++ b/app/models/atlassian/identity.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Atlassian
+ class Identity < ApplicationRecord
+ self.table_name = 'atlassian_identities'
+
+ belongs_to :user
+
+ validates :extern_uid, presence: true, uniqueness: true
+ validates :user, presence: true, uniqueness: true
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: false,
+ encode_iv: false
+
+ attr_encrypted :refresh_token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: false,
+ encode_iv: false
+ end
+end
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index e7cfa30a892..f46803be057 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -5,9 +5,9 @@ class AuditEvent < ApplicationRecord
include IgnorableColumns
include BulkInsertSafe
- PARALLEL_PERSISTENCE_COLUMNS = [:author_name, :entity_path, :target_details].freeze
+ PARALLEL_PERSISTENCE_COLUMNS = [:author_name, :entity_path, :target_details, :target_type].freeze
- ignore_column :updated_at, remove_with: '13.4', remove_after: '2020-09-22'
+ ignore_column :type, remove_with: '13.6', remove_after: '2020-11-22'
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize
@@ -29,6 +29,14 @@ class AuditEvent < ApplicationRecord
# https://gitlab.com/groups/gitlab-org/-/epics/2765
after_validation :parallel_persist
+ # Note: After loading records, do not attempt to type cast objects it finds.
+ # We are in the process of deprecating STI (i.e. SecurityEvent) out of AuditEvent.
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/216845
+ def self.inheritance_column
+ :_type_disabled
+ end
+
def self.order_by(method)
case method.to_s
when 'created_asc'
diff --git a/app/models/authentication_event.rb b/app/models/authentication_event.rb
new file mode 100644
index 00000000000..1ac3c5fbd9c
--- /dev/null
+++ b/app/models/authentication_event.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AuthenticationEvent < ApplicationRecord
+ belongs_to :user, optional: true
+
+ validates :provider, :user_name, :result, presence: true
+
+ enum result: {
+ failed: 0,
+ success: 1
+ }
+end
diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb
index a851f22bfcd..1be7120a955 100644
--- a/app/models/blob_viewer/dependency_manager.rb
+++ b/app/models/blob_viewer/dependency_manager.rb
@@ -33,8 +33,8 @@ module BlobViewer
@json_data ||= begin
prepare!
Gitlab::Json.parse(blob.data)
- rescue
- {}
+ rescue
+ {}
end
end
diff --git a/app/models/blob_viewer/metrics_dashboard_yml.rb b/app/models/blob_viewer/metrics_dashboard_yml.rb
index c05fb5d88d6..88643253d3d 100644
--- a/app/models/blob_viewer/metrics_dashboard_yml.rb
+++ b/app/models/blob_viewer/metrics_dashboard_yml.rb
@@ -25,20 +25,30 @@ module BlobViewer
private
def parse_blob_data
- yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
+ if Feature.enabled?(:metrics_dashboard_exhaustive_validations, project)
+ exhaustive_metrics_dashboard_validation
+ else
+ old_metrics_dashboard_validation
+ end
+ end
+ def old_metrics_dashboard_validation
+ yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
::PerformanceMonitoring::PrometheusDashboard.from_json(yaml)
- nil
+ []
rescue Gitlab::Config::Loader::FormatError => error
- wrap_yml_syntax_error(error)
+ ["YAML syntax: #{error.message}"]
rescue ActiveModel::ValidationError => invalid
- invalid.model.errors
+ invalid.model.errors.messages.map { |messages| messages.join(': ') }
end
- def wrap_yml_syntax_error(error)
- ::PerformanceMonitoring::PrometheusDashboard.new.errors.tap do |errors|
- errors.add(:'YAML syntax', error.message)
- end
+ def exhaustive_metrics_dashboard_validation
+ yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
+ Gitlab::Metrics::Dashboard::Validator
+ .errors(yaml, dashboard_path: blob.path, project: project)
+ .map(&:message)
+ rescue Gitlab::Config::Loader::FormatError => error
+ [error.message]
end
end
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 58c26e8c806..1697067f633 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -35,6 +35,10 @@ module Ci
end
end
+ event :pending do
+ transition all => :pending
+ end
+
event :manual do
transition all => :manual
end
@@ -48,6 +52,14 @@ module Ci
raise NotImplementedError
end
+ def self.with_preloads
+ preload(
+ :metadata,
+ downstream_pipeline: [project: [:route, { namespace: :route }]],
+ project: [:namespace]
+ )
+ end
+
def schedule_downstream_pipeline!
raise InvalidBridgeTypeError unless downstream_project
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index af4e6bb0494..99580a52e96 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -38,14 +38,17 @@ module Ci
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :resource, class_name: 'Ci::Resource', inverse_of: :build
+ has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
- has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
+ has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id
+ has_many :pages_deployments, inverse_of: :ci_build
+
Ci::JobArtifact.file_types.each do |key, value|
has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
end
@@ -90,9 +93,9 @@ module Ci
Ci::BuildMetadata.scoped_build.with_interruptible.select(:id))
end
- scope :unstarted, ->() { where(runner_id: nil) }
- scope :ignore_failures, ->() { where(allow_failure: false) }
- scope :with_downloadable_artifacts, ->() do
+ scope :unstarted, -> { where(runner_id: nil) }
+ scope :ignore_failures, -> { where(allow_failure: false) }
+ scope :with_downloadable_artifacts, -> do
where('EXISTS (?)',
Ci::JobArtifact.select(1)
.where('ci_builds.id = ci_job_artifacts.job_id')
@@ -104,11 +107,11 @@ module Ci
where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query))
end
- scope :with_archived_trace, ->() do
+ scope :with_archived_trace, -> do
with_existing_job_artifacts(Ci::JobArtifact.trace)
end
- scope :without_archived_trace, ->() do
+ scope :without_archived_trace, -> do
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
@@ -139,11 +142,11 @@ module Ci
.includes(:metadata, :job_artifacts_metadata)
end
- scope :with_artifacts_not_expired, ->() { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) }
- scope :with_expired_artifacts, ->() { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) }
- scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
- scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
- scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
+ scope :with_artifacts_not_expired, -> { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) }
+ scope :with_expired_artifacts, -> { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) }
+ scope :last_month, -> { where('created_at > ?', Date.today - 1.month) }
+ scope :manual_actions, -> { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
+ scope :scheduled_actions, -> { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
scope :ref_protected, -> { where(protected: true) }
scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) }
@@ -175,7 +178,6 @@ module Ci
end
scope :queued_before, ->(time) { where(arel_table[:queued_at].lt(time)) }
- scope :order_id_desc, -> { order('ci_builds.id DESC') }
scope :preload_project_and_pipeline_project, -> do
preload(Ci::Pipeline::PROJECT_ROUTE_AND_NAMESPACE_ROUTE,
@@ -213,6 +215,10 @@ module Ci
.execute(build)
# rubocop: enable CodeReuse/ServiceClass
end
+
+ def with_preloads
+ preload(:job_artifacts_archive, :job_artifacts, project: [:namespace])
+ end
end
state_machine :status do
@@ -647,6 +653,10 @@ module Ci
!artifacts_expired? && artifacts_file&.exists?
end
+ def locked_artifacts?
+ pipeline.artifacts_locked? && artifacts_file&.exists?
+ end
+
# This method is similar to #artifacts? but it includes the artifacts
# locking mechanics. A new method was created to prevent breaking existing
# behavior and avoid introducing N+1s.
@@ -867,13 +877,17 @@ module Ci
options.dig(:release)&.any?
end
- def hide_secrets(trace)
+ def hide_secrets(data, metrics = ::Gitlab::Ci::Trace::Metrics.new)
return unless trace
- trace = trace.dup
- Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
- Gitlab::Ci::MaskSecret.mask!(trace, token) if token
- trace
+ data.dup.tap do |trace|
+ Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
+ Gitlab::Ci::MaskSecret.mask!(trace, token) if token
+
+ if trace != data
+ metrics.increment_trace_operation(operation: :mutated)
+ end
+ end
end
def serializable_hash(options = {})
@@ -945,6 +959,10 @@ module Ci
var[:value]&.to_i if var
end
+ def remove_pending_state!
+ pending_state.try(:delete)
+ end
+
private
def auto_retry
diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb
new file mode 100644
index 00000000000..45f323adec2
--- /dev/null
+++ b/app/models/ci/build_pending_state.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class Ci::BuildPendingState < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id
+
+ enum state: Ci::Stage.statuses
+ enum failure_reason: CommitStatus.failure_reasons
+
+ validates :build, presence: true
+end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 407802baf09..444742062d9 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -2,14 +2,17 @@
module Ci
class BuildTraceChunk < ApplicationRecord
- include FastDestroyAll
+ extend ::Gitlab::Ci::Model
+ include ::FastDestroyAll
+ include ::Checksummable
include ::Gitlab::ExclusiveLeaseHelpers
- extend Gitlab::Ci::Model
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
default_value_for :data_store, :redis
+ after_create { metrics.increment_trace_operation(operation: :chunked) }
+
CHUNK_SIZE = 128.kilobytes
WRITE_LOCK_RETRY = 10
WRITE_LOCK_SLEEP = 0.01.seconds
@@ -25,6 +28,8 @@ module Ci
fog: 3
}
+ scope :live, -> { redis }
+
class << self
def all_stores
@all_stores ||= self.data_stores.keys
@@ -60,8 +65,6 @@ module Ci
end
end
- ##
- # Data is memoized for optimizing #size and #end_offset
def data
@data ||= get_data.to_s
end
@@ -80,11 +83,11 @@ module Ci
in_lock(*lock_params) { unsafe_append_data!(new_data, offset) }
- schedule_to_persist if full?
+ schedule_to_persist! if full?
end
def size
- @size ||= current_store.size(self) || data&.bytesize
+ @size ||= @data&.bytesize || current_store.size(self) || data&.bytesize
end
def start_offset
@@ -100,35 +103,68 @@ module Ci
end
def persist_data!
- in_lock(*lock_params) do # Write operation is atomic
- unsafe_persist_to!(self.class.persistable_store)
- end
+ in_lock(*lock_params) { unsafe_persist_data! }
+ end
+
+ def schedule_to_persist!
+ return if persisted?
+
+ Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ end
+
+ def persisted?
+ !redis?
+ end
+
+ def live?
+ redis?
+ end
+
+ ##
+ # Build trace chunk is final (the last one that we do not expect to ever
+ # become full) when a runner submitted a build pending state and there is
+ # no chunk with higher index in the database.
+ #
+ def final?
+ build.pending_state.present? &&
+ build.trace_chunks.maximum(:chunk_index).to_i == chunk_index
end
private
- def unsafe_persist_to!(new_store)
+ def get_data
+ # Redis / database return UTF-8 encoded string by default
+ current_store.data(self)&.force_encoding(Encoding::BINARY)
+ end
+
+ def unsafe_persist_data!(new_store = self.class.persistable_store)
return if data_store == new_store.to_s
- current_data = get_data
+ current_data = data
+ old_store_class = current_store
+ current_size = current_data&.bytesize.to_i
- unless current_data&.bytesize.to_i == CHUNK_SIZE
+ unless current_size == CHUNK_SIZE || final?
raise FailedToPersistDataError, 'Data is not fulfilled in a bucket'
end
- old_store_class = current_store
-
self.raw_data = nil
self.data_store = new_store
+ self.checksum = crc32(current_data)
+
+ ##
+ # We need to so persist data then save a new store identifier before we
+ # remove data from the previous store to make this operation
+ # trasnaction-safe. `unsafe_set_data! calls `save!` because of this
+ # reason.
+ #
+ # TODO consider using callbacks and state machine to remove old data
+ #
unsafe_set_data!(current_data)
old_store_class.delete_data(self)
end
- def get_data
- current_store.data(self)&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
- end
-
def unsafe_set_data!(value)
raise ArgumentError, 'New data size exceeds chunk size' if value.bytesize > CHUNK_SIZE
@@ -148,6 +184,8 @@ module Ci
end
current_store.append_data(self, value, offset).then do |stored|
+ metrics.increment_trace_operation(operation: :appended)
+
raise ArgumentError, 'Trace appended incorrectly' if stored != new_size
end
@@ -157,16 +195,6 @@ module Ci
save! if changed?
end
- def schedule_to_persist
- return if data_persisted?
-
- Ci::BuildTraceChunkFlushWorker.perform_async(id)
- end
-
- def data_persisted?
- !redis?
- end
-
def full?
size == CHUNK_SIZE
end
@@ -181,5 +209,9 @@ module Ci
retries: WRITE_LOCK_RETRY,
sleep_sec: WRITE_LOCK_SLEEP }]
end
+
+ def metrics
+ @metrics ||= ::Gitlab::Ci::Trace::Metrics.new
+ end
end
end
diff --git a/app/models/ci/build_trace_chunks/database.rb b/app/models/ci/build_trace_chunks/database.rb
index 3b8e23510d9..ea8072099c6 100644
--- a/app/models/ci/build_trace_chunks/database.rb
+++ b/app/models/ci/build_trace_chunks/database.rb
@@ -29,7 +29,7 @@ module Ci
new_data = truncated_data + new_data
end
- model.raw_data = new_data
+ set_data(model, new_data)
model.raw_data.to_s.bytesize
end
diff --git a/app/models/ci/build_trace_chunks/redis.rb b/app/models/ci/build_trace_chunks/redis.rb
index 0ae563f6ce8..58d50b39c11 100644
--- a/app/models/ci/build_trace_chunks/redis.rb
+++ b/app/models/ci/build_trace_chunks/redis.rb
@@ -41,9 +41,9 @@ module Ci
end
end
- def set_data(model, data)
+ def set_data(model, new_data)
Gitlab::Redis::SharedState.with do |redis|
- redis.set(key(model), data, ex: CHUNK_REDIS_TTL)
+ redis.set(key(model), new_data, ex: CHUNK_REDIS_TTL)
end
end
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
index d6617b8c2eb..e6f02f2e4f3 100644
--- a/app/models/ci/daily_build_group_report_result.rb
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -11,6 +11,8 @@ module Ci
validates :data, json_schema: { filename: "daily_build_group_report_result_data" }
+ scope :with_included_projects, -> { includes(:project) }
+
def self.upsert_reports(data)
upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 75c3ce98c95..8bbb92e319f 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -12,8 +12,6 @@ module Ci
include FileStoreMounter
extend Gitlab::Ci::Model
- NotSupportedAdapterError = Class.new(StandardError)
-
ignore_columns :locked, remove_after: '2020-07-22', remove_with: '13.4'
TEST_REPORT_FILE_TYPES = %w[junit].freeze
@@ -163,7 +161,6 @@ module Ci
where(file_type: types)
end
- scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) }
scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) }
scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked).order(expire_at: :desc) }
@@ -271,16 +268,6 @@ module Ci
end
end
- def each_blob(&blk)
- unless file_format_adapter_class
- raise NotSupportedAdapterError, 'This file format requires a dedicated adapter'
- end
-
- file.open do |stream|
- file_format_adapter_class.new(stream).each_blob(&blk)
- end
- end
-
def self.archived_trace_exists_for?(job_id)
where(job_id: job_id).trace.take&.file&.file&.exists?
end
@@ -298,10 +285,6 @@ module Ci
private
- def file_format_adapter_class
- FILE_FORMAT_ADAPTERS[file_format.to_sym]
- end
-
def set_size
self.size = file.size
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 7762328d274..47eba685afe 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -19,6 +19,8 @@ module Ci
PROJECT_ROUTE_AND_NAMESPACE_ROUTE = {
project: [:project_feature, :route, { namespace: :route }]
}.freeze
+ CONFIG_EXTENSION = '.gitlab-ci.yml'
+ DEFAULT_CONFIG_PATH = CONFIG_EXTENSION
BridgeStatusError = Class.new(StandardError)
@@ -104,15 +106,15 @@ module Ci
after_create :keep_around_commits, unless: :importing?
- # We use `Ci::PipelineEnums.sources` here so that EE can more easily extend
+ # We use `Enums::Ci::Pipeline.sources` here so that EE can more easily extend
# this `Hash` with new values.
- enum_with_nil source: ::Ci::PipelineEnums.sources
+ enum_with_nil source: Enums::Ci::Pipeline.sources
- enum_with_nil config_source: ::Ci::PipelineEnums.config_sources
+ enum_with_nil config_source: Enums::Ci::Pipeline.config_sources
- # We use `Ci::PipelineEnums.failure_reasons` here so that EE can more easily
+ # We use `Enums::Ci::Pipeline.failure_reasons` here so that EE can more easily
# extend this `Hash` with new values.
- enum failure_reason: ::Ci::PipelineEnums.failure_reasons
+ enum failure_reason: Enums::Ci::Pipeline.failure_reasons
enum locked: { unlocked: 0, artifacts_locked: 1 }
@@ -229,7 +231,12 @@ module Ci
end
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
- next unless pipeline.bridge_triggered?
+ pipeline.run_after_commit do
+ ::Ci::Pipelines::CreateArtifactWorker.perform_async(pipeline.id)
+ end
+ end
+
+ after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
next unless pipeline.bridge_waiting?
pipeline.run_after_commit do
@@ -254,7 +261,7 @@ module Ci
scope :internal, -> { where(source: internal_sources) }
scope :no_child, -> { where.not(source: :parent_pipeline) }
- scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
+ scope :ci_sources, -> { where(source: Enums::Ci::Pipeline.ci_sources.values) }
scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) }
scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
@@ -483,6 +490,12 @@ module Ci
end
end
+ def git_commit_timestamp
+ strong_memoize(:git_commit_timestamp) do
+ commit.try(:timestamp)
+ end
+ end
+
def before_sha
super || Gitlab::Git::BLANK_SHA
end
@@ -539,12 +552,6 @@ module Ci
end
# rubocop: enable CodeReuse/ServiceClass
- def mark_as_processable_after_stage(stage_idx)
- builds.skipped.after_stage(stage_idx).find_each do |build|
- Gitlab::OptimisticLocking.retry_lock(build, &:process)
- end
- end
-
def lazy_ref_commit
return unless ::Gitlab::Ci::Features.pipeline_latest?
@@ -647,7 +654,7 @@ module Ci
def config_path
return unless repository_source? || unknown_source?
- project.ci_config_path.presence || '.gitlab-ci.yml'
+ project.ci_config_path_or_default
end
def has_yaml_errors?
@@ -669,8 +676,10 @@ module Ci
messages.select(&:error?)
end
- def warning_messages
- messages.select(&:warning?)
+ def warning_messages(limit: nil)
+ messages.select(&:warning?).tap do |warnings|
+ break warnings.take(limit) if limit
+ end
end
# Manually set the notes for a Ci::Pipeline
@@ -766,6 +775,7 @@ module Ci
variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s)
+ variables.append(key: 'CI_COMMIT_TIMESTAMP', value: git_commit_timestamp.to_s)
# legacy variables
variables.append(key: 'CI_BUILD_REF', value: sha)
@@ -810,11 +820,17 @@ module Ci
all_merge_requests.order(id: :desc)
end
- # If pipeline is a child of another pipeline, include the parent
- # and the siblings, otherwise return only itself and children.
def same_family_pipeline_ids
- parent = parent_pipeline || self
- [parent.id] + parent.child_pipelines.pluck(:id)
+ if ::Gitlab::Ci::Features.child_of_child_pipeline_enabled?(project)
+ ::Gitlab::Ci::PipelineObjectHierarchy.new(
+ base_and_ancestors(same_project: true), options: { same_project: true }
+ ).base_and_descendants.select(:id)
+ else
+ # If pipeline is a child of another pipeline, include the parent
+ # and the siblings, otherwise return only itself and children.
+ parent = parent_pipeline || self
+ [parent.id] + parent.child_pipelines.pluck(:id)
+ end
end
def bridge_triggered?
@@ -858,12 +874,26 @@ module Ci
builds.latest.with_reports(reports_scope)
end
+ def builds_with_coverage
+ builds.with_coverage
+ end
+
def has_reports?(reports_scope)
complete? && latest_report_builds(reports_scope).exists?
end
+ def has_coverage_reports?
+ pipeline_artifacts&.has_code_coverage?
+ end
+
+ def can_generate_coverage_reports?
+ has_reports?(Ci::JobArtifact.coverage_reports)
+ end
+
def test_report_summary
- Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results)
+ strong_memoize(:test_report_summary) do
+ Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results)
+ end
end
def test_reports
@@ -1008,7 +1038,11 @@ module Ci
end
def cacheable?
- Ci::PipelineEnums.ci_config_sources.key?(config_source.to_sym)
+ !dangling?
+ end
+
+ def dangling?
+ Enums::Ci::Pipeline.dangling_sources.key?(source.to_sym)
end
def source_ref_path
@@ -1029,6 +1063,26 @@ module Ci
self.ci_ref = Ci::Ref.ensure_for(self)
end
+ def base_and_ancestors(same_project: false)
+ # Without using `unscoped`, caller scope is also included into the query.
+ # Using `unscoped` here will be redundant after Rails 6.1
+ ::Gitlab::Ci::PipelineObjectHierarchy
+ .new(self.class.unscoped.where(id: id), options: { same_project: same_project })
+ .base_and_ancestors
+ end
+
+ # We need `base_and_ancestors` in a specific order to "break" when needed.
+ # If we use `find_each`, then the order is broken.
+ # rubocop:disable Rails/FindEach
+ def reset_ancestor_bridges!
+ base_and_ancestors.includes(:source_bridge).each do |pipeline|
+ break unless pipeline.bridge_waiting?
+
+ pipeline.source_bridge.pending!
+ end
+ end
+ # rubocop:enable Rails/FindEach
+
private
def add_message(severity, content)
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index e7f51977ccd..b6db8cad667 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -5,33 +5,44 @@
module Ci
class PipelineArtifact < ApplicationRecord
extend Gitlab::Ci::Model
+ include UpdateProjectStatistics
include Artifactable
include FileStoreMounter
-
- FILE_STORE_SUPPORTED = [
- ObjectStorage::Store::LOCAL,
- ObjectStorage::Store::REMOTE
- ].freeze
+ include Presentable
FILE_SIZE_LIMIT = 10.megabytes.freeze
+ EXPIRATION_DATE = 1.week.freeze
+
+ DEFAULT_FILE_NAMES = {
+ code_coverage: 'code_coverage.json'
+ }.freeze
belongs_to :project, class_name: "Project", inverse_of: :pipeline_artifacts
belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :pipeline_artifacts
validates :pipeline, :project, :file_format, :file, presence: true
- validates :file_store, presence: true, inclusion: { in: FILE_STORE_SUPPORTED }
+ validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
validates :size, presence: true, numericality: { less_than_or_equal_to: FILE_SIZE_LIMIT }
validates :file_type, presence: true
mount_file_store_uploader Ci::PipelineArtifactUploader
- before_save :set_size, if: :file_changed?
+
+ update_project_statistics project_statistics_name: :pipeline_artifacts_size
enum file_type: {
code_coverage: 1
}
- def set_size
- self.size = file.size
+ def self.has_code_coverage?
+ where(file_type: :code_coverage).exists?
+ end
+
+ def self.find_with_code_coverage
+ find_by(file_type: :code_coverage)
+ end
+
+ def present
+ super(presenter_class: "Ci::PipelineArtifacts::#{self.file_type.camelize}Presenter".constantize)
end
end
end
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
deleted file mode 100644
index 9d108ff0fa4..00000000000
--- a/app/models/ci/pipeline_enums.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- module PipelineEnums
- # Returns the `Hash` to use for creating the `failure_reason` enum for
- # `Ci::Pipeline`.
- def self.failure_reasons
- {
- unknown_failure: 0,
- config_error: 1,
- external_validation_failure: 2
- }
- end
-
- # Returns the `Hash` to use for creating the `sources` enum for
- # `Ci::Pipeline`.
- def self.sources
- {
- unknown: nil,
- push: 1,
- web: 2,
- trigger: 3,
- schedule: 4,
- api: 5,
- external: 6,
- # TODO: Rename `pipeline` to `cross_project_pipeline` in 13.0
- # https://gitlab.com/gitlab-org/gitlab/issues/195991
- pipeline: 7,
- chat: 8,
- webide: 9,
- merge_request_event: 10,
- external_pull_request_event: 11,
- parent_pipeline: 12,
- ondemand_dast_scan: 13
- }
- end
-
- # Returns the `Hash` to use for creating the `config_sources` enum for
- # `Ci::Pipeline`.
- def self.config_sources
- {
- unknown_source: nil,
- repository_source: 1,
- auto_devops_source: 2,
- webide_source: 3,
- remote_source: 4,
- external_project_source: 5,
- bridge_source: 6,
- parameter_source: 7
- }
- end
-
- def self.ci_config_sources
- config_sources.slice(
- :unknown_source,
- :repository_source,
- :auto_devops_source,
- :remote_source,
- :external_project_source
- )
- end
-
- def self.ci_config_sources_values
- ci_config_sources.values
- end
-
- def self.non_ci_config_source_values
- config_sources.values - ci_config_sources.values
- end
- end
-end
-
-Ci::PipelineEnums.prepend_if_ee('EE::Ci::PipelineEnums')
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 3d8823728e7..6e9b8416c10 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -33,8 +33,6 @@ module Ci
state :still_failing, value: 5
after_transition any => [:fixed, :success] do |ci_ref|
- next unless ::Gitlab::Ci::Features.keep_latest_artifacts_for_ref_enabled?(ci_ref.project)
-
ci_ref.run_after_commit do
Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id)
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 00ee45740bd..86879b9dc68 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -52,7 +52,7 @@ module Ci
has_many :runner_namespaces, inverse_of: :runner
has_many :groups, through: :runner_namespaces
- has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
+ has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build'
before_save :ensure_token
diff --git a/app/models/ci_platform_metric.rb b/app/models/ci_platform_metric.rb
new file mode 100644
index 00000000000..5e6e3eddce9
--- /dev/null
+++ b/app/models/ci_platform_metric.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class CiPlatformMetric < ApplicationRecord
+ include BulkInsertSafe
+
+ PLATFORM_TARGET_MAX_LENGTH = 255
+
+ validates :recorded_at, presence: true
+ validates :platform_target,
+ exclusion: [nil], # allow '' (the empty string), but not nil
+ length: { maximum: PLATFORM_TARGET_MAX_LENGTH }
+ validates :count,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ CI_VARIABLE_KEY = 'AUTO_DEVOPS_PLATFORM_TARGET'
+ ALLOWED_TARGETS = %w[ECS FARGATE].freeze
+
+ def self.insert_auto_devops_platform_targets!
+ recorded_at = Time.zone.now
+
+ # This work can NOT be done in-database because value is encrypted.
+ # However, for 'AUTO_DEVOPS_PLATFORM_TARGET', these values are only
+ # encrypted as a matter of course, rather than as a need for secrecy.
+ # So this is not a security risk, but exposing other keys possibly could be.
+ variables = Ci::Variable.by_key(CI_VARIABLE_KEY)
+
+ counts = variables.group_by(&:value).map do |value, variables|
+ # While this value is, in theory, not secret. A user could accidentally
+ # put a secret in here so we need to make sure we filter invalid values.
+ next unless ALLOWED_TARGETS.include?(value)
+
+ count = variables.count
+ self.new(recorded_at: recorded_at, platform_target: value, count: count)
+ end.compact
+
+ bulk_insert!(counts, validate: true)
+ end
+end
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index c21759a3c3b..874415e7bf4 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -8,6 +8,8 @@ module Clusters
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
+ scope :with_name, -> (name) { where(name: name) }
+
validates :name,
presence: true,
length: { maximum: 63 },
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 216bbbc1c5a..dd6a4144608 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -5,7 +5,7 @@ module Clusters
class Prometheus < ApplicationRecord
include PrometheusAdapter
- VERSION = '9.5.2'
+ VERSION = '10.4.1'
self.table_name = 'clusters_applications_prometheus'
@@ -106,7 +106,9 @@ module Clusters
proxy_url = kube_client.proxy_url('service', service_name, service_port, Gitlab::Kubernetes::Helm::NAMESPACE)
# ensures headers containing auth data are appended to original k8s client options
- options = kube_client.rest_client.options.merge(headers: kube_client.headers)
+ options = kube_client.rest_client.options
+ .merge(prometheus_client_default_options)
+ .merge(headers: kube_client.headers)
Gitlab::PrometheusClient.new(proxy_url, options)
rescue Kubeclient::HttpError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ENETUNREACH
# If users have mistakenly set parameters or removed the depended clusters,
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index e99ed03852a..4983de83800 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.19.3'
+ VERSION = '0.20.1'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 63aebdf1bdb..b94ec3c6dea 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -99,6 +99,7 @@ module Clusters
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
delegate :available?, to: :application_knative, prefix: true, allow_nil: true
+ delegate :available?, to: :application_elastic_stack, prefix: true, allow_nil: true
delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true
delegate :external_hostname, to: :application_ingress, prefix: true, allow_nil: true
diff --git a/app/models/clusters/instance.rb b/app/models/clusters/instance.rb
index 8c9d9ab9ab1..94fadace01c 100644
--- a/app/models/clusters/instance.rb
+++ b/app/models/clusters/instance.rb
@@ -7,7 +7,7 @@ module Clusters
end
def feature_available?(feature)
- ::Feature.enabled?(feature, default_enabled: true)
+ ::Feature.enabled?(feature, type: :licensed, default_enabled: true)
end
def flipper_id
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
index 86869361ed8..35e8b751b3d 100644
--- a/app/models/clusters/providers/aws.rb
+++ b/app/models/clusters/providers/aws.rb
@@ -37,7 +37,7 @@ module Clusters
greater_than: 0
}
- validates :key_name, :region, :instance_type, :security_group_id, length: { in: 1..255 }
+ validates :kubernetes_version, :key_name, :region, :instance_type, :security_group_id, length: { in: 1..255 }
validates :subnet_ids, presence: true
def nullify_credentials
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 4f18ece9e50..5e0fceb23a4 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -221,12 +221,16 @@ class Commit
description.present?
end
+ def timestamp
+ committed_date.xmlschema
+ end
+
def hook_attrs(with_changed_files: false)
data = {
id: id,
message: safe_message,
title: title,
- timestamp: committed_date.xmlschema,
+ timestamp: timestamp,
url: Gitlab::UrlBuilder.build(self),
author: {
name: author_name,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 8aba74bedbc..2f0596c93cc 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -32,6 +32,8 @@ class CommitStatus < ApplicationRecord
where(allow_failure: true, status: [:failed, :canceled])
end
+ scope :order_id_desc, -> { order('ci_builds.id DESC') }
+
scope :exclude_ignored, -> do
# We want to ignore failed but allowed to fail jobs.
#
@@ -77,9 +79,9 @@ class CommitStatus < ApplicationRecord
merge(or_conditions)
end
- # We use `CommitStatusEnums.failure_reasons` here so that EE can more easily
+ # We use `Enums::CommitStatus.failure_reasons` here so that EE can more easily
# extend this `Hash` with new values.
- enum_with_nil failure_reason: ::CommitStatusEnums.failure_reasons
+ enum_with_nil failure_reason: Enums::CommitStatus.failure_reasons
##
# We still create some CommitStatuses outside of CreatePipelineService.
@@ -199,7 +201,14 @@ class CommitStatus < ApplicationRecord
end
def group_name
- name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip
+ # 'rspec:linux: 1/10' => 'rspec:linux'
+ common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
+
+ # 'rspec:linux: [aws, max memory]' => 'rspec:linux'
+ common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
+
+ common_name.strip!
+ common_name
end
def failed_but_allowed?
diff --git a/app/models/commit_status_enums.rb b/app/models/commit_status_enums.rb
deleted file mode 100644
index ad90929b8fa..00000000000
--- a/app/models/commit_status_enums.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module CommitStatusEnums
- # Returns the Hash to use for creating the `failure_reason` enum for
- # `CommitStatus`.
- def self.failure_reasons
- {
- unknown_failure: nil,
- script_failure: 1,
- api_failure: 2,
- stuck_or_timeout_failure: 3,
- runner_system_failure: 4,
- missing_dependency_failure: 5,
- runner_unsupported: 6,
- stale_schedule: 7,
- job_execution_timeout: 8,
- archived_failure: 9,
- unmet_prerequisites: 10,
- scheduler_failure: 11,
- data_integrity_failure: 12,
- forward_deployment_failure: 13,
- insufficient_bridge_permissions: 1_001,
- downstream_bridge_project_not_found: 1_002,
- invalid_bridge_trigger: 1_003,
- bridge_pipeline_is_child_pipeline: 1_006,
- downstream_pipeline_creation_failed: 1_007,
- secrets_provider_not_found: 1_008
- }
- end
-end
-
-CommitStatusEnums.prepend_if_ee('EE::CommitStatusEnums')
diff --git a/app/models/concerns/admin_changed_password_notifier.rb b/app/models/concerns/admin_changed_password_notifier.rb
new file mode 100644
index 00000000000..f6c2abc7e0f
--- /dev/null
+++ b/app/models/concerns/admin_changed_password_notifier.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module AdminChangedPasswordNotifier
+ # This module is responsible for triggering the `Password changed by administrator` emails
+ # when a GitLab administrator changes the password of another user.
+
+ # Usage
+ # These emails are disabled by default and are never trigerred after updating the password, unless
+ # explicitly specified.
+
+ # To explicitly trigger this email, the `send_only_admin_changed_your_password_notification!`
+ # method should be called, so like:
+
+ # user = User.find_by(email: 'hello@example.com')
+ # user.send_only_admin_changed_your_password_notification!
+ # user.password = user.password_confirmation = 'new_password'
+ # user.save!
+
+ # The `send_only_admin_changed_your_password_notification` has 2 responsibilities.
+ # It prevents triggering Devise's default `Password changed` email.
+ # It trigggers the `Password changed by administrator` email.
+
+ # It is important to skip sending the default Devise email when sending out `Password changed by administrator`
+ # email because we should not be sending 2 emails for the same event,
+ # hence the only public API made available from this module is `send_only_admin_changed_your_password_notification!`
+
+ # There is no public API made available to send the `Password changed by administrator` email,
+ # *without* skipping the default `Password changed` email, to prevent the problem mentioned above.
+
+ extend ActiveSupport::Concern
+
+ included do
+ after_update :send_admin_changed_your_password_notification, if: :send_admin_changed_your_password_notification?
+ end
+
+ def initialize(*args, &block)
+ @allow_admin_changed_your_password_notification = false # These emails are off by default
+ super
+ end
+
+ def send_only_admin_changed_your_password_notification!
+ skip_password_change_notification! # skip sending the default Devise 'password changed' notification
+ allow_admin_changed_your_password_notification!
+ end
+
+ private
+
+ def send_admin_changed_your_password_notification
+ send_devise_notification(:password_change_by_admin)
+ end
+
+ def allow_admin_changed_your_password_notification!
+ @allow_admin_changed_your_password_notification = true # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ def send_admin_changed_your_password_notification?
+ self.class.send_password_change_notification && saved_change_to_encrypted_password? &&
+ @allow_admin_changed_your_password_notification # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+end
diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb
index 041ed3755e0..f44ad474cd5 100644
--- a/app/models/concerns/bulk_member_access_load.rb
+++ b/app/models/concerns/bulk_member_access_load.rb
@@ -22,7 +22,7 @@ module BulkMemberAccessLoad
end
# Look up only the IDs we need
- resource_ids = resource_ids - access.keys
+ resource_ids -= access.keys
return access if resource_ids.empty?
diff --git a/app/models/concerns/checksummable.rb b/app/models/concerns/checksummable.rb
index 1f76eb87aa5..d6d17bfc604 100644
--- a/app/models/concerns/checksummable.rb
+++ b/app/models/concerns/checksummable.rb
@@ -3,9 +3,13 @@
module Checksummable
extend ActiveSupport::Concern
+ def crc32(data)
+ Zlib.crc32(data)
+ end
+
class_methods do
def hexdigest(path)
- Digest::SHA256.file(path).hexdigest
+ ::Digest::SHA256.file(path).hexdigest
end
end
end
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index 54fb9021f2f..24df86dbc3c 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -4,6 +4,8 @@ module Ci
module Artifactable
extend ActiveSupport::Concern
+ NotSupportedAdapterError = Class.new(StandardError)
+
FILE_FORMAT_ADAPTERS = {
gzip: Gitlab::Ci::Build::Artifacts::Adapters::GzipStream,
raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream
@@ -15,6 +17,24 @@ module Ci
zip: 2,
gzip: 3
}, _suffix: true
+
+ scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) }
+ end
+
+ def each_blob(&blk)
+ unless file_format_adapter_class
+ raise NotSupportedAdapterError, 'This file format requires a dedicated adapter'
+ end
+
+ file.open do |stream|
+ file_format_adapter_class.new(stream).each_blob(&blk)
+ end
+ end
+
+ private
+
+ def file_format_adapter_class
+ FILE_FORMAT_ADAPTERS[file_format.to_sym]
end
end
end
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index 8542c48f366..40891073738 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -13,14 +13,12 @@ module DiscussionOnDiff
:diff_line,
:active?,
:created_at_diff?,
-
to: :first_note
delegate :file_path,
:blob,
:highlighted_diff_lines,
:diff_lines,
-
to: :diff_file,
allow_nil: true
end
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
new file mode 100644
index 00000000000..f1bc43a12d8
--- /dev/null
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Enums
+ module Ci
+ module Pipeline
+ # Returns the `Hash` to use for creating the `failure_reason` enum for
+ # `Ci::Pipeline`.
+ def self.failure_reasons
+ {
+ unknown_failure: 0,
+ config_error: 1,
+ external_validation_failure: 2
+ }
+ end
+
+ # Returns the `Hash` to use for creating the `sources` enum for
+ # `Ci::Pipeline`.
+ def self.sources
+ {
+ unknown: nil,
+ push: 1,
+ web: 2,
+ trigger: 3,
+ schedule: 4,
+ api: 5,
+ external: 6,
+ # TODO: Rename `pipeline` to `cross_project_pipeline` in 13.0
+ # https://gitlab.com/gitlab-org/gitlab/issues/195991
+ pipeline: 7,
+ chat: 8,
+ webide: 9,
+ merge_request_event: 10,
+ external_pull_request_event: 11,
+ parent_pipeline: 12,
+ ondemand_dast_scan: 13
+ }
+ end
+
+ # Dangling sources are those events that generate pipelines for which
+ # we don't want to directly affect the ref CI status.
+ # - when a webide pipeline fails it does not change the ref CI status to failed
+ # - when a child pipeline (from parent_pipeline source) fails it affects its
+ # parent pipeline. It's up to the parent to affect the ref CI status
+ # - when an ondemand_dast_scan pipeline runs it is for testing purpose and should
+ # not affect the ref CI status.
+ def self.dangling_sources
+ sources.slice(:webide, :parent_pipeline, :ondemand_dast_scan)
+ end
+
+ # CI sources are those pipeline events that affect the CI status of the ref
+ # they run for. By definition it excludes dangling pipelines.
+ def self.ci_sources
+ sources.except(*dangling_sources.keys)
+ end
+
+ # Returns the `Hash` to use for creating the `config_sources` enum for
+ # `Ci::Pipeline`.
+ def self.config_sources
+ {
+ unknown_source: nil,
+ repository_source: 1,
+ auto_devops_source: 2,
+ webide_source: 3,
+ remote_source: 4,
+ external_project_source: 5,
+ bridge_source: 6,
+ parameter_source: 7
+ }
+ end
+ end
+ end
+end
+
+Enums::Ci::Pipeline.prepend_if_ee('EE::Enums::Ci::Pipeline')
diff --git a/app/models/concerns/enums/commit_status.rb b/app/models/concerns/enums/commit_status.rb
new file mode 100644
index 00000000000..faeed7276ab
--- /dev/null
+++ b/app/models/concerns/enums/commit_status.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Enums
+ module CommitStatus
+ # Returns the Hash to use for creating the `failure_reason` enum for
+ # `CommitStatus`.
+ def self.failure_reasons
+ {
+ unknown_failure: nil,
+ script_failure: 1,
+ api_failure: 2,
+ stuck_or_timeout_failure: 3,
+ runner_system_failure: 4,
+ missing_dependency_failure: 5,
+ runner_unsupported: 6,
+ stale_schedule: 7,
+ job_execution_timeout: 8,
+ archived_failure: 9,
+ unmet_prerequisites: 10,
+ scheduler_failure: 11,
+ data_integrity_failure: 12,
+ forward_deployment_failure: 13,
+ insufficient_bridge_permissions: 1_001,
+ downstream_bridge_project_not_found: 1_002,
+ invalid_bridge_trigger: 1_003,
+ bridge_pipeline_is_child_pipeline: 1_006, # not used anymore, but cannot be deleted because of old data
+ downstream_pipeline_creation_failed: 1_007,
+ secrets_provider_not_found: 1_008,
+ reached_max_descendant_pipelines_depth: 1_009
+ }
+ end
+ end
+end
+
+Enums::CommitStatus.prepend_if_ee('EE::Enums::CommitStatus')
diff --git a/app/models/internal_id_enums.rb b/app/models/concerns/enums/internal_id.rb
index 125ae7573b6..2d51d232e93 100644
--- a/app/models/internal_id_enums.rb
+++ b/app/models/concerns/enums/internal_id.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
-module InternalIdEnums
- def self.usage_resources
- # when adding new resource, make sure it doesn't conflict with EE usage_resources
- {
+module Enums
+ module InternalId
+ def self.usage_resources
+ # when adding new resource, make sure it doesn't conflict with EE usage_resources
+ {
issues: 0,
merge_requests: 1,
deployments: 2,
@@ -14,8 +15,9 @@ module InternalIdEnums
operations_user_lists: 7,
alert_management_alerts: 8,
sprints: 9 # iterations
- }
+ }
+ end
end
end
-InternalIdEnums.prepend_if_ee('EE::InternalIdEnums')
+Enums::InternalId.prepend_if_ee('EE::Enums::InternalId')
diff --git a/app/models/concerns/enums/prometheus_metric.rb b/app/models/concerns/enums/prometheus_metric.rb
new file mode 100644
index 00000000000..e65a01990a3
--- /dev/null
+++ b/app/models/concerns/enums/prometheus_metric.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Enums
+ module PrometheusMetric
+ def self.groups
+ {
+ # built-in groups
+ nginx_ingress_vts: -1,
+ ha_proxy: -2,
+ aws_elb: -3,
+ nginx: -4,
+ kubernetes: -5,
+ nginx_ingress: -6,
+ cluster_health: -100
+ }.merge(custom_groups).freeze
+ end
+
+ # custom/user groups
+ def self.custom_groups
+ {
+ business: 0,
+ response: 1,
+ system: 2,
+ custom: 3
+ }.freeze
+ end
+
+ def self.group_details
+ {
+ # built-in groups
+ nginx_ingress_vts: {
+ group_title: _('Response metrics (NGINX Ingress VTS)'),
+ required_metrics: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
+ priority: 10
+ }.freeze,
+ nginx_ingress: {
+ group_title: _('Response metrics (NGINX Ingress)'),
+ required_metrics: %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum),
+ priority: 10
+ }.freeze,
+ ha_proxy: {
+ group_title: _('Response metrics (HA Proxy)'),
+ required_metrics: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
+ priority: 10
+ }.freeze,
+ aws_elb: {
+ group_title: _('Response metrics (AWS ELB)'),
+ required_metrics: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
+ priority: 10
+ }.freeze,
+ nginx: {
+ group_title: _('Response metrics (NGINX)'),
+ required_metrics: %w(nginx_server_requests nginx_server_requestMsec),
+ priority: 10
+ }.freeze,
+ kubernetes: {
+ group_title: _('System metrics (Kubernetes)'),
+ required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
+ priority: 5
+ }.freeze,
+ cluster_health: {
+ group_title: _('Cluster Health'),
+ required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
+ priority: 10
+ }.freeze
+ }.merge(custom_group_details).freeze
+ end
+
+ # custom/user groups
+ def self.custom_group_details
+ {
+ business: {
+ group_title: _('Business metrics (Custom)'),
+ priority: 0
+ }.freeze,
+ response: {
+ group_title: _('Response metrics (Custom)'),
+ priority: -5
+ }.freeze,
+ system: {
+ group_title: _('System metrics (Custom)'),
+ priority: -10
+ }.freeze,
+ custom: {
+ group_title: _('Custom metrics'),
+ priority: 0
+ }
+ }.freeze
+ end
+ end
+end
diff --git a/app/models/concerns/from_except.rb b/app/models/concerns/from_except.rb
new file mode 100644
index 00000000000..b9ca9dda4b0
--- /dev/null
+++ b/app/models/concerns/from_except.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module FromExcept
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Produces a query that uses a FROM to select data using an EXCEPT.
+ #
+ # Example:
+ # groups = Group.from_except([group1.self_and_hierarchy, group2.self_and_hierarchy])
+ #
+ # This would produce the following SQL query:
+ #
+ # SELECT *
+ # FROM (
+ # SELECT "namespaces". *
+ # ...
+ #
+ # EXCEPT
+ #
+ # SELECT "namespaces". *
+ # ...
+ # ) groups;
+ #
+ # members - An Array of ActiveRecord::Relation objects to use in the except.
+ #
+ # remove_duplicates - A boolean indicating if duplicate entries should be
+ # removed. Defaults to true.
+ #
+ # alias_as - The alias to use for the sub query. Defaults to the name of the
+ # table of the current model.
+ # rubocop: disable Gitlab/Except
+ extend FromSetOperator
+ define_set_operator Gitlab::SQL::Except
+ # rubocop: enable Gitlab/Except
+ end
+end
diff --git a/app/models/concerns/from_intersect.rb b/app/models/concerns/from_intersect.rb
new file mode 100644
index 00000000000..428e63eb45e
--- /dev/null
+++ b/app/models/concerns/from_intersect.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module FromIntersect
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Produces a query that uses a FROM to select data using an INTERSECT.
+ #
+ # Example:
+ # groups = Group.from_intersect([group1.self_and_hierarchy, group2.self_and_hierarchy])
+ #
+ # This would produce the following SQL query:
+ #
+ # SELECT *
+ # FROM (
+ # SELECT "namespaces". *
+ # ...
+ #
+ # INTERSECT
+ #
+ # SELECT "namespaces". *
+ # ...
+ # ) groups;
+ #
+ # members - An Array of ActiveRecord::Relation objects to use in the intersect.
+ #
+ # remove_duplicates - A boolean indicating if duplicate entries should be
+ # removed. Defaults to true.
+ #
+ # alias_as - The alias to use for the sub query. Defaults to the name of the
+ # table of the current model.
+ # rubocop: disable Gitlab/Intersect
+ extend FromSetOperator
+ define_set_operator Gitlab::SQL::Intersect
+ # rubocop: enable Gitlab/Intersect
+ end
+end
diff --git a/app/models/concerns/from_set_operator.rb b/app/models/concerns/from_set_operator.rb
new file mode 100644
index 00000000000..593fd251c5c
--- /dev/null
+++ b/app/models/concerns/from_set_operator.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module FromSetOperator
+ # Define a high level method to more easily work with the SQL set operations
+ # of UNION, INTERSECT, and EXCEPT as defined by Gitlab::SQL::Union,
+ # Gitlab::SQL::Intersect, and Gitlab::SQL::Except respectively.
+ def define_set_operator(operator)
+ method_name = 'from_' + operator.name.demodulize.downcase
+ method_name = method_name.to_sym
+
+ raise "Trying to redefine method '#{method(method_name)}'" if methods.include?(method_name)
+
+ define_method(method_name) do |members, remove_duplicates: true, alias_as: table_name|
+ operator_sql = operator.new(members, remove_duplicates: remove_duplicates).to_sql
+
+ from(Arel.sql("(#{operator_sql}) #{alias_as}"))
+ end
+ end
+end
diff --git a/app/models/concerns/from_union.rb b/app/models/concerns/from_union.rb
index e28dee34815..e25d603b802 100644
--- a/app/models/concerns/from_union.rb
+++ b/app/models/concerns/from_union.rb
@@ -35,13 +35,29 @@ module FromUnion
# alias_as - The alias to use for the sub query. Defaults to the name of the
# table of the current model.
# rubocop: disable Gitlab/Union
+ extend FromSetOperator
+ define_set_operator Gitlab::SQL::Union
+
+ alias_method :from_union_set_operator, :from_union
def from_union(members, remove_duplicates: true, alias_as: table_name)
+ if Feature.enabled?(:sql_set_operators)
+ from_union_set_operator(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
+ else
+ # The original from_union method.
+ standard_from_union(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
+ end
+ end
+
+ private
+
+ def standard_from_union(members, remove_duplicates: true, alias_as: table_name)
union = Gitlab::SQL::Union
.new(members, remove_duplicates: remove_duplicates)
.to_sql
from(Arel.sql("(#{union}) #{alias_as}"))
end
+
# rubocop: enable Gitlab/Union
end
end
diff --git a/app/models/concerns/has_wiki.rb b/app/models/concerns/has_wiki.rb
index 3e7cb940a62..df7bbe4dc08 100644
--- a/app/models/concerns/has_wiki.rb
+++ b/app/models/concerns/has_wiki.rb
@@ -25,10 +25,6 @@ module HasWiki
wiki.repository_exists?
end
- def after_wiki_activity
- true
- end
-
private
def check_wiki_path_conflict
diff --git a/app/models/concerns/id_in_ordered.rb b/app/models/concerns/id_in_ordered.rb
new file mode 100644
index 00000000000..b89409e6841
--- /dev/null
+++ b/app/models/concerns/id_in_ordered.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module IdInOrdered
+ extend ActiveSupport::Concern
+
+ included do
+ scope :id_in_ordered, -> (ids) do
+ raise ArgumentError, "ids must be an array of integers" unless ids.is_a?(Enumerable) && ids.all? { |id| id.is_a?(Integer) }
+
+ # No need to sort if no more than 1 and the sorting code doesn't work
+ # with an empty array
+ return id_in(ids) unless ids.count > 1
+
+ id_attribute = arel_table[:id]
+ id_in(ids)
+ .order(
+ Arel.sql("array_position(ARRAY[#{ids.join(',')}], #{id_attribute.relation.name}.#{id_attribute.name})"))
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index dd5aedbb760..888e1b384a2 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -177,10 +177,41 @@ module Issuable
assignees.count > 1
end
- def supports_weight?
+ def allows_reviewers?
false
end
+ def supports_time_tracking?
+ is_a?(TimeTrackable) && !incident?
+ end
+
+ def supports_severity?
+ incident?
+ end
+
+ def incident?
+ is_a?(Issue) && super
+ end
+
+ def supports_issue_type?
+ is_a?(Issue)
+ end
+
+ def severity
+ return IssuableSeverity::DEFAULT unless incident?
+
+ issuable_severity&.severity || IssuableSeverity::DEFAULT
+ end
+
+ def update_severity(severity)
+ return unless incident?
+
+ severity = severity.to_s.downcase
+ severity = IssuableSeverity::DEFAULT unless IssuableSeverity.severities.key?(severity)
+
+ (issuable_severity || build_issuable_severity(issue_id: id)).update(severity: severity)
+ end
+
private
def description_max_length_for_new_records_is_valid
@@ -377,8 +408,12 @@ module Issuable
Date.today == created_at.to_date
end
+ def created_hours_ago
+ (Time.now.utc.to_i - created_at.utc.to_i) / 3600
+ end
+
def new?
- today? && created_at == updated_at
+ created_hours_ago < 24
end
def open?
diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb
index 79ff82d9f99..e624b9aa356 100644
--- a/app/models/concerns/loaded_in_group_list.rb
+++ b/app/models/concerns/loaded_in_group_list.rb
@@ -54,6 +54,7 @@ module LoadedInGroupList
.where(members[:source_type].eq(Namespace.name))
.where(members[:source_id].eq(namespaces[:id]))
.where(members[:requested_at].eq(nil))
+ .where(members[:access_level].gt(Gitlab::Access::MINIMAL_ACCESS))
end
end
@@ -70,7 +71,7 @@ module LoadedInGroupList
end
def member_count
- @member_count ||= try(:preloaded_member_count) || users.count
+ @member_count ||= try(:preloaded_member_count) || members.count
end
end
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index f44a674b3c9..307d58a3a3c 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -30,7 +30,7 @@ module Mentionable
def self.external_pattern
strong_memoize(:external_pattern) do
issue_pattern = IssueTrackerService.reference_pattern
- link_patterns = URI.regexp(%w(http https))
+ link_patterns = URI::DEFAULT_PARSER.make_regexp(%w(http https))
reference_pattern(link_patterns, issue_pattern)
end
end
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index ccb334343ff..b1698bc2ee3 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -51,7 +51,7 @@ module Milestoneable
# Overridden on EE module
#
def supports_milestone?
- respond_to?(:milestone_id)
+ respond_to?(:milestone_id) && !incident?
end
end
diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb
new file mode 100644
index 00000000000..7be4a26d4fa
--- /dev/null
+++ b/app/models/concerns/optimized_issuable_label_filter.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+module OptimizedIssuableLabelFilter
+ def by_label(items)
+ return items unless params.labels?
+
+ return super if Feature.disabled?(:optimized_issuable_label_filter)
+
+ target_model = items.model
+
+ if params.filter_by_no_label?
+ items.where('NOT EXISTS (?)', optimized_any_label_query(target_model))
+ elsif params.filter_by_any_label?
+ items.where('EXISTS (?)', optimized_any_label_query(target_model))
+ else
+ issuables_with_selected_labels(items, target_model)
+ end
+ end
+
+ # Taken from IssuableFinder
+ def count_by_state
+ return super if root_namespace.nil?
+ return super if Feature.disabled?(:optimized_issuable_label_filter)
+
+ count_params = params.merge(state: nil, sort: nil, force_cte: true)
+ finder = self.class.new(current_user, count_params)
+
+ state_counts = finder
+ .execute
+ .reorder(nil)
+ .group(:state_id)
+ .count
+
+ counts = state_counts.transform_keys { |key| count_key(key) }
+
+ counts[:all] = counts.values.sum
+ counts.with_indifferent_access
+ end
+
+ private
+
+ def issuables_with_selected_labels(items, target_model)
+ if root_namespace
+ all_label_ids = find_label_ids(root_namespace)
+ # Found less labels in the DB than we were searching for. Return nothing.
+ return items.none if all_label_ids.size != params.label_names.size
+
+ all_label_ids.each do |label_ids|
+ items = items.where('EXISTS (?)', optimized_label_query_by_label_ids(target_model, label_ids))
+ end
+ else
+ params.label_names.each do |label_name|
+ items = items.where('EXISTS (?)', optimized_label_query_by_label_name(target_model, label_name))
+ end
+ end
+
+ items
+ end
+
+ def find_label_ids(root_namespace)
+ finder_params = {
+ include_subgroups: true,
+ include_ancestor_groups: true,
+ include_descendant_groups: true,
+ group: root_namespace,
+ title: params.label_names
+ }
+
+ LabelsFinder
+ .new(nil, finder_params)
+ .execute(skip_authorization: true)
+ .pluck(:title, :id)
+ .group_by(&:first)
+ .values
+ .map { |labels| labels.map(&:last) }
+ end
+
+ def root_namespace
+ strong_memoize(:root_namespace) do
+ (params.project || params.group)&.root_ancestor
+ end
+ end
+
+ def optimized_any_label_query(target_model)
+ LabelLink
+ .where(target_type: target_model.name)
+ .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
+ .limit(1)
+ end
+
+ def optimized_label_query_by_label_ids(target_model, label_ids)
+ LabelLink
+ .where(target_type: target_model.name)
+ .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
+ .where(label_id: label_ids)
+ .limit(1)
+ end
+
+ def optimized_label_query_by_label_name(target_model, label_name)
+ LabelLink
+ .joins(:label)
+ .where(target_type: target_model.name)
+ .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
+ .where(labels: { name: label_name })
+ .limit(1)
+ end
+end
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index adb6a59e11c..55c2bf96a94 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -3,6 +3,11 @@
module PrometheusAdapter
extend ActiveSupport::Concern
+ # We should choose more conservative timeouts, but some queries we run are now busting our
+ # default timeouts, which are stricter. We should make those queries faster instead.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/232786
+ DEFAULT_PROMETHEUS_REQUEST_TIMEOUT_SEC = 60.seconds
+
included do
include ReactiveCaching
@@ -15,6 +20,12 @@ module PrometheusAdapter
raise NotImplementedError
end
+ def prometheus_client_default_options
+ {
+ timeout: DEFAULT_PROMETHEUS_REQUEST_TIMEOUT_SEC
+ }
+ end
+
# This is a light-weight check if a prometheus client is properly configured.
def configured?
raise NotImplemented
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index d1f04609693..3cbc174536c 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -27,18 +27,7 @@
#
module RelativePositioning
extend ActiveSupport::Concern
-
- STEPS = 10
- IDEAL_DISTANCE = 2**(STEPS - 1) + 1
-
- MIN_POSITION = Gitlab::Database::MIN_INT_VALUE
- START_POSITION = 0
- MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
-
- MAX_GAP = IDEAL_DISTANCE * 2
- MIN_GAP = 2
-
- NoSpaceLeft = Class.new(StandardError)
+ include ::Gitlab::RelativePositioning
class_methods do
def move_nulls_to_end(objects)
@@ -49,56 +38,10 @@ module RelativePositioning
move_nulls(objects, at_end: false)
end
- # This method takes two integer values (positions) and
- # calculates the position between them. The range is huge as
- # the maximum integer value is 2147483647.
- #
- # We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
- #
- # Then we handle one of three cases:
- # - If the gap is too small, we raise NoSpaceLeft
- # - If the gap is larger than MAX_GAP, we place the new position at most
- # IDEAL_DISTANCE from the edge of the gap.
- # - otherwise we place the new position at the midpoint.
- #
- # The new position will always satisfy: pos_before <= midpoint <= pos_after
- #
- # As a precondition, the gap between pos_before and pos_after MUST be >= 2.
- # If the gap is too small, NoSpaceLeft is raised.
- #
- # This class method should only be called by instance methods of this module, which
- # include handling for minimum gap size.
- #
- # @raises NoSpaceLeft
- # @api private
- def position_between(pos_before, pos_after)
- pos_before ||= MIN_POSITION
- pos_after ||= MAX_POSITION
-
- pos_before, pos_after = [pos_before, pos_after].sort
-
- gap_width = pos_after - pos_before
- midpoint = [pos_after - 1, pos_before + (gap_width / 2)].min
-
- if gap_width < MIN_GAP
- raise NoSpaceLeft
- elsif gap_width > MAX_GAP
- if pos_before == MIN_POSITION
- pos_after - IDEAL_DISTANCE
- elsif pos_after == MAX_POSITION
- pos_before + IDEAL_DISTANCE
- else
- midpoint
- end
- else
- midpoint
- end
- end
-
private
# @api private
- def gap_size(object, gaps:, at_end:, starting_from:)
+ def gap_size(context, gaps:, at_end:, starting_from:)
total_width = IDEAL_DISTANCE * gaps
size = if at_end && starting_from + total_width >= MAX_POSITION
(MAX_POSITION - starting_from) / gaps
@@ -108,23 +51,17 @@ module RelativePositioning
IDEAL_DISTANCE
end
- # Shift max elements leftwards if there isn't enough space
return [size, starting_from] if size >= MIN_GAP
- order = at_end ? :desc : :asc
- terminus = object
- .send(:relative_siblings) # rubocop:disable GitlabSecurity/PublicSend
- .where('relative_position IS NOT NULL')
- .order(relative_position: order)
- .first
-
if at_end
- terminus.move_sequence_before(true)
- max_relative_position = terminus.reset.relative_position
+ terminus = context.max_sibling
+ terminus.shift_left
+ max_relative_position = terminus.relative_position
[[(MAX_POSITION - max_relative_position) / gaps, IDEAL_DISTANCE].min, max_relative_position]
else
- terminus.move_sequence_after(true)
- min_relative_position = terminus.reset.relative_position
+ terminus = context.min_sibling
+ terminus.shift_right
+ min_relative_position = terminus.relative_position
[[(min_relative_position - MIN_POSITION) / gaps, IDEAL_DISTANCE].min, min_relative_position]
end
end
@@ -142,8 +79,9 @@ module RelativePositioning
objects = objects.reject(&:relative_position)
return 0 if objects.empty?
- representative = objects.first
- number_of_gaps = objects.size + 1 # 1 at left, one between each, and one at right
+ number_of_gaps = objects.size # 1 to the nearest neighbour, and one between each
+ representative = RelativePositioning.mover.context(objects.first)
+
position = if at_end
representative.max_relative_position
else
@@ -152,16 +90,21 @@ module RelativePositioning
position ||= START_POSITION # If there are no positioned siblings, start from START_POSITION
- gap, position = gap_size(representative, gaps: number_of_gaps, at_end: at_end, starting_from: position)
-
- # Raise if we could not make enough space
- raise NoSpaceLeft if gap < MIN_GAP
+ gap = 0
+ attempts = 10 # consolidate up to 10 gaps to find enough space
+ while gap < 1 && attempts > 0
+ gap, position = gap_size(representative, gaps: number_of_gaps, at_end: at_end, starting_from: position)
+ attempts -= 1
+ end
- indexed = objects.each_with_index.to_a
- starting_from = at_end ? position : position - (gap * number_of_gaps)
+ # Allow placing items next to each other, if we have to.
+ gap = 1 if gap < MIN_GAP
+ delta = at_end ? gap : -gap
+ indexed = (at_end ? objects : objects.reverse).each_with_index
# Some classes are polymorphic, and not all siblings are in the same table.
by_model = indexed.group_by { |pair| pair.first.class }
+ lower_bound, upper_bound = at_end ? [position, MAX_POSITION] : [MIN_POSITION, position]
by_model.each do |model, pairs|
model.transaction do
@@ -169,7 +112,8 @@ module RelativePositioning
# These are known to be integers, one from the DB, and the other
# calculated by us, and thus safe to interpolate
values = batch.map do |obj, i|
- pos = starting_from + gap * (i + 1)
+ desired_pos = position + delta * (i + 1)
+ pos = desired_pos.clamp(lower_bound, upper_bound)
obj.relative_position = pos
"(#{obj.id}, #{pos})"
end.join(', ')
@@ -192,306 +136,68 @@ module RelativePositioning
end
end
- def min_relative_position(&block)
- calculate_relative_position('MIN', &block)
- end
-
- def max_relative_position(&block)
- calculate_relative_position('MAX', &block)
- end
-
- def prev_relative_position(ignoring: nil)
- prev_pos = nil
-
- if self.relative_position
- prev_pos = max_relative_position do |relation|
- relation = relation.id_not_in(ignoring.id) if ignoring.present?
- relation.where('relative_position < ?', self.relative_position)
- end
- end
-
- prev_pos
- end
-
- def next_relative_position(ignoring: nil)
- next_pos = nil
-
- if self.relative_position
- next_pos = min_relative_position do |relation|
- relation = relation.id_not_in(ignoring.id) if ignoring.present?
- relation.where('relative_position > ?', self.relative_position)
- end
- end
-
- next_pos
+ def self.mover
+ ::Gitlab::RelativePositioning::Mover.new(START_POSITION, (MIN_POSITION..MAX_POSITION))
end
def move_between(before, after)
- return move_after(before) unless after
- return move_before(after) unless before
-
- before, after = after, before if after.relative_position < before.relative_position
-
- pos_left = before.relative_position
- pos_right = after.relative_position
+ before, after = [before, after].sort_by(&:relative_position) if before && after
- if pos_right - pos_left < MIN_GAP
- # Not enough room! Make space by shifting all previous elements to the left
- # if there is enough space, else to the right
- gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
-
- if gap.present?
- after.move_sequence_before(next_gap: gap)
- pos_left -= optimum_delta_for_gap(gap)
- else
- before.move_sequence_after
- pos_right = after.reset.relative_position
- end
- end
-
- new_position = self.class.position_between(pos_left, pos_right)
-
- self.relative_position = new_position
+ RelativePositioning.mover.move(self, before, after)
+ rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
+ could_not_move(e)
+ raise e
end
def move_after(before = self)
- pos_before = before.relative_position
- pos_after = before.next_relative_position(ignoring: self)
-
- if pos_before == MAX_POSITION || gap_too_small?(pos_after, pos_before)
- gap = before.send(:find_next_gap_after) # rubocop:disable GitlabSecurity/PublicSend
-
- if gap.nil?
- before.move_sequence_before(true)
- pos_before = before.reset.relative_position
- else
- before.move_sequence_after(next_gap: gap)
- pos_after += optimum_delta_for_gap(gap)
- end
- end
-
- self.relative_position = self.class.position_between(pos_before, pos_after)
+ RelativePositioning.mover.move(self, before, nil)
+ rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
+ could_not_move(e)
+ raise e
end
def move_before(after = self)
- pos_after = after.relative_position
- pos_before = after.prev_relative_position(ignoring: self)
-
- if pos_after == MIN_POSITION || gap_too_small?(pos_before, pos_after)
- gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
-
- if gap.nil?
- after.move_sequence_after(true)
- pos_after = after.reset.relative_position
- else
- after.move_sequence_before(next_gap: gap)
- pos_before -= optimum_delta_for_gap(gap)
- end
- end
-
- self.relative_position = self.class.position_between(pos_before, pos_after)
+ RelativePositioning.mover.move(self, nil, after)
+ rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
+ could_not_move(e)
+ raise e
end
def move_to_end
- max_pos = max_relative_position
-
- if max_pos.nil?
- self.relative_position = START_POSITION
- elsif gap_too_small?(max_pos, MAX_POSITION)
- max = relative_siblings.order(Gitlab::Database.nulls_last_order('relative_position', 'DESC')).first
- max.move_sequence_before(true)
- max.reset
- self.relative_position = self.class.position_between(max.relative_position, MAX_POSITION)
- else
- self.relative_position = self.class.position_between(max_pos, MAX_POSITION)
- end
+ RelativePositioning.mover.move_to_end(self)
+ rescue NoSpaceLeft => e
+ could_not_move(e)
+ self.relative_position = MAX_POSITION
+ rescue ActiveRecord::QueryCanceled => e
+ could_not_move(e)
+ raise e
end
def move_to_start
- min_pos = min_relative_position
-
- if min_pos.nil?
- self.relative_position = START_POSITION
- elsif gap_too_small?(min_pos, MIN_POSITION)
- min = relative_siblings.order(Gitlab::Database.nulls_last_order('relative_position', 'ASC')).first
- min.move_sequence_after(true)
- min.reset
- self.relative_position = self.class.position_between(MIN_POSITION, min.relative_position)
- else
- self.relative_position = self.class.position_between(MIN_POSITION, min_pos)
- end
- end
-
- # Moves the sequence before the current item to the middle of the next gap
- # For example, we have
- #
- # 5 . . . . . 11 12 13 14 [15] 16 . 17
- # -----------
- #
- # This moves the sequence [11 12 13 14] to [8 9 10 11], so we have:
- #
- # 5 . . 8 9 10 11 . . . [15] 16 . 17
- # ---------
- #
- # Creating a gap to the left of the current item. We can understand this as
- # dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3.
- #
- # If `include_self` is true, the current item will also be moved, creating a
- # gap to the right of the current item:
- #
- # 5 . . 8 9 10 11 [14] . . . 16 . 17
- # --------------
- #
- # As an optimization, the gap can be precalculated and passed to this method.
- #
- # @api private
- # @raises NoSpaceLeft if the sequence cannot be moved
- def move_sequence_before(include_self = false, next_gap: find_next_gap_before)
- raise NoSpaceLeft unless next_gap.present?
-
- delta = optimum_delta_for_gap(next_gap)
-
- move_sequence(next_gap[:start], relative_position, -delta, include_self)
- end
-
- # Moves the sequence after the current item to the middle of the next gap
- # For example, we have:
- #
- # 8 . 10 [11] 12 13 14 15 . . . . . 21
- # -----------
- #
- # This moves the sequence [12 13 14 15] to [15 16 17 18], so we have:
- #
- # 8 . 10 [11] . . . 15 16 17 18 . . 21
- # -----------
- #
- # Creating a gap to the right of the current item. We can understand this as
- # dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2.
- #
- # If `include_self` is true, the current item will also be moved, creating a
- # gap to the left of the current item:
- #
- # 8 . 10 . . . [14] 15 16 17 18 . . 21
- # ----------------
- #
- # As an optimization, the gap can be precalculated and passed to this method.
- #
- # @api private
- # @raises NoSpaceLeft if the sequence cannot be moved
- def move_sequence_after(include_self = false, next_gap: find_next_gap_after)
- raise NoSpaceLeft unless next_gap.present?
-
- delta = optimum_delta_for_gap(next_gap)
-
- move_sequence(relative_position, next_gap[:start], delta, include_self)
- end
-
- private
-
- def gap_too_small?(pos_a, pos_b)
- return false unless pos_a && pos_b
-
- (pos_a - pos_b).abs < MIN_GAP
- end
-
- # Find the first suitable gap to the left of the current position.
- #
- # Satisfies the relations:
- # - gap[:start] <= relative_position
- # - abs(gap[:start] - gap[:end]) >= MIN_GAP
- # - MIN_POSITION <= gap[:start] <= MAX_POSITION
- # - MIN_POSITION <= gap[:end] <= MAX_POSITION
- #
- # Supposing that the current item is 13, and we have a sequence of items:
- #
- # 1 . . . 5 . . . . 11 12 [13] 14 . . 17
- # ^---------^
- #
- # Then we return: `{ start: 11, end: 5 }`
- #
- # Here start refers to the end of the gap closest to the current item.
- def find_next_gap_before
- items_with_next_pos = scoped_items
- .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
- .where('relative_position <= ?', relative_position)
- .order(relative_position: :desc)
-
- find_next_gap(items_with_next_pos, MIN_POSITION)
- end
-
- # Find the first suitable gap to the right of the current position.
- #
- # Satisfies the relations:
- # - gap[:start] >= relative_position
- # - abs(gap[:start] - gap[:end]) >= MIN_GAP
- # - MIN_POSITION <= gap[:start] <= MAX_POSITION
- # - MIN_POSITION <= gap[:end] <= MAX_POSITION
- #
- # Supposing the current item is 13, and that we have a sequence of items:
- #
- # 9 . . . [13] 14 15 . . . . 20 . . . 24
- # ^---------^
- #
- # Then we return: `{ start: 15, end: 20 }`
- #
- # Here start refers to the end of the gap closest to the current item.
- def find_next_gap_after
- items_with_next_pos = scoped_items
- .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
- .where('relative_position >= ?', relative_position)
- .order(:relative_position)
-
- find_next_gap(items_with_next_pos, MAX_POSITION)
- end
-
- def find_next_gap(items_with_next_pos, end_is_nil)
- gap = self.class
- .from(items_with_next_pos, :items)
- .where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP)
- .limit(1)
- .pluck(:pos, :next_pos)
- .first
-
- return if gap.nil? || gap.first == end_is_nil
-
- { start: gap.first, end: gap.second || end_is_nil }
- end
-
- def optimum_delta_for_gap(gap)
- delta = ((gap[:start] - gap[:end]) / 2.0).abs.ceil
-
- [delta, IDEAL_DISTANCE].min
- end
-
- def move_sequence(start_pos, end_pos, delta, include_self = false)
- relation = include_self ? scoped_items : relative_siblings
-
+ RelativePositioning.mover.move_to_start(self)
+ rescue NoSpaceLeft => e
+ could_not_move(e)
+ self.relative_position = MIN_POSITION
+ rescue ActiveRecord::QueryCanceled => e
+ could_not_move(e)
+ raise e
+ end
+
+ # This method is used during rebalancing - override it to customise the update
+ # logic:
+ def update_relative_siblings(relation, range, delta)
relation
- .where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
+ .where(relative_position: range)
.update_all("relative_position = relative_position + #{delta}")
end
- def calculate_relative_position(calculation)
- # When calculating across projects, this is much more efficient than
- # MAX(relative_position) without the GROUP BY, due to index usage:
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/54276#note_119340977
- relation = scoped_items
- .order(Gitlab::Database.nulls_last_order('position', 'DESC'))
- .group(self.class.relative_positioning_parent_column)
- .limit(1)
-
- relation = yield relation if block_given?
-
- relation
- .pluck(self.class.relative_positioning_parent_column, Arel.sql("#{calculation}(relative_position) AS position"))
- .first&.last
- end
-
- def relative_siblings(relation = scoped_items)
- relation.id_not_in(id)
+ # This method is used to exclude the current self (or another object)
+ # from a relation. Customize this if `id <> :id` is not sufficient
+ def exclude_self(relation, excluded: self)
+ relation.id_not_in(excluded.id)
end
- def scoped_items
- self.class.relative_positioning_query_base(self)
+ # Override if you want to be notified of failures to move
+ def could_not_move(exception)
end
end
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index 5174ae05d15..3e1e5faee54 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -31,7 +31,6 @@ module ResolvableDiscussion
delegate :resolved_at,
:resolved_by,
:resolved_by_push?,
-
to: :last_resolved_note,
allow_nil: true
end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 250889fdf8b..71b976c6f11 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -23,10 +23,22 @@ module Storage
former_parent_full_path = parent_was&.full_path
parent_full_path = parent&.full_path
Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
- Gitlab::PagesTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
+
+ if any_project_with_pages_deployed?
+ run_after_commit do
+ Gitlab::PagesTransfer.new.async.move_namespace(path, former_parent_full_path, parent_full_path)
+ end
+ end
else
Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path)
- Gitlab::PagesTransfer.new.rename_namespace(full_path_before_last_save, full_path)
+
+ if any_project_with_pages_deployed?
+ full_path_was = full_path_before_last_save
+
+ run_after_commit do
+ Gitlab::PagesTransfer.new.async.rename_namespace(full_path_was, full_path)
+ end
+ end
end
# If repositories moved successfully we need to
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 8927e42dd97..3e2cf9031d0 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -75,8 +75,8 @@ module Timebox
scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL')
- .where('start_date is NULL or start_date <= ?', end_date)
- .where('due_date is NULL or due_date >= ?', start_date)
+ .where('start_date is NULL or start_date <= ?', end_date)
+ .where('due_date is NULL or due_date >= ?', start_date)
end
strip_attributes :title
@@ -195,6 +195,10 @@ module Timebox
end
end
+ def weight_available?
+ resource_parent&.feature_available?(:issue_weights)
+ end
+
private
def timebox_format_reference(format = :iid)
diff --git a/app/models/cycle_analytics/level_base.rb b/app/models/cycle_analytics/level_base.rb
index 543349ebf8f..967de9a22b4 100644
--- a/app/models/cycle_analytics/level_base.rb
+++ b/app/models/cycle_analytics/level_base.rb
@@ -2,7 +2,7 @@
module CycleAnalytics
module LevelBase
- STAGES = %i[issue plan code test review staging production].freeze
+ STAGES = %i[issue plan code test review staging].freeze
def all_medians_by_stage
STAGES.each_with_object({}) do |stage_name, medians_per_stage|
diff --git a/app/models/data_list.rb b/app/models/data_list.rb
index 12011cb17f7..2cee3447886 100644
--- a/app/models/data_list.rb
+++ b/app/models/data_list.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class DataList
- def initialize(batch, data_fields_hash, klass)
- @batch = batch
+ def initialize(batch_ids, data_fields_hash, klass)
+ @batch_ids = batch_ids
@data_fields_hash = data_fields_hash
@klass = klass
end
@@ -13,13 +13,15 @@ class DataList
private
- attr_reader :batch, :data_fields_hash, :klass
+ attr_reader :batch_ids, :data_fields_hash, :klass
def columns
data_fields_hash.keys << 'service_id'
end
def values
- batch.map { |row| data_fields_hash.values << row['id'] }
+ batch_ids.map do |row|
+ data_fields_hash.values << row['id']
+ end
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index d6508ffceba..3978620c74d 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -148,7 +148,7 @@ class Deployment < ApplicationRecord
def execute_hooks
deployment_data = Gitlab::DataBuilder::Deployment.build(self)
- project.execute_hooks(deployment_data, :deployment_hooks) if Feature.enabled?(:deployment_webhooks, project, default_enabled: true)
+ project.execute_hooks(deployment_data, :deployment_hooks)
project.execute_services(deployment_data, :deployment_hooks)
end
diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb
index deda814d689..57bb250829d 100644
--- a/app/models/design_management/design.rb
+++ b/app/models/design_management/design.rb
@@ -79,16 +79,10 @@ module DesignManagement
joins(join.join_sources).where(actions[:event].not_eq(deletion))
end
- scope :ordered, -> (project) do
- # TODO: Always order by relative position after the feature flag is removed
- # https://gitlab.com/gitlab-org/gitlab/-/issues/34382
- if Feature.enabled?(:reorder_designs, project, default_enabled: true)
- # We need to additionally sort by `id` to support keyset pagination.
- # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17788/diffs#note_230875678
- order(:relative_position, :id)
- else
- in_creation_order
- end
+ scope :ordered, -> do
+ # We need to additionally sort by `id` to support keyset pagination.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17788/diffs#note_230875678
+ order(:relative_position, :id)
end
scope :in_creation_order, -> { reorder(:id) }
diff --git a/app/models/design_management/design_collection.rb b/app/models/design_management/design_collection.rb
index 96d5f4c2419..c48b36588c9 100644
--- a/app/models/design_management/design_collection.rb
+++ b/app/models/design_management/design_collection.rb
@@ -6,8 +6,34 @@ module DesignManagement
delegate :designs, :project, to: :issue
+ state_machine :copy_state, initial: :ready, namespace: :copy do
+ after_transition any => any, do: :update_stored_copy_state!
+
+ event :start do
+ transition ready: :in_progress
+ end
+
+ event :end do
+ transition in_progress: :ready
+ end
+
+ event :error do
+ transition in_progress: :error
+ end
+
+ event :reset do
+ transition any => :ready
+ end
+ end
+
def initialize(issue)
+ super() # Necessary to initialize state_machine
+
@issue = issue
+
+ if stored_copy_state = get_stored_copy_state
+ @copy_state = stored_copy_state
+ end
end
def ==(other)
@@ -30,5 +56,39 @@ module DesignManagement
def designs_by_filename(filenames)
designs.current.where(filename: filenames)
end
+
+ private
+
+ def update_stored_copy_state!
+ # As "ready" is the initial copy state we can clear the cached value
+ # rather than persist it.
+ if copy_ready?
+ unset_store_copy_state!
+ else
+ set_stored_copy_state!
+ end
+ end
+
+ def copy_state_cache_key
+ "DesignCollection/copy_state/issue=#{issue.id}"
+ end
+
+ def get_stored_copy_state
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(copy_state_cache_key)
+ end
+ end
+
+ def set_stored_copy_state!
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(copy_state_cache_key, copy_state)
+ end
+ end
+
+ def unset_store_copy_state!
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.del(copy_state_cache_key)
+ end
+ end
end
end
diff --git a/app/models/dev_ops_score/card.rb b/app/models/dev_ops_report/card.rb
index b1894cf4138..4060cb1e5b6 100644
--- a/app/models/dev_ops_score/card.rb
+++ b/app/models/dev_ops_report/card.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module DevOpsScore
+module DevOpsReport
class Card
attr_accessor :metric, :title, :description, :feature, :blog, :docs
diff --git a/app/models/dev_ops_score/idea_to_production_step.rb b/app/models/dev_ops_report/idea_to_production_step.rb
index d892793cf97..2503d5949e5 100644
--- a/app/models/dev_ops_score/idea_to_production_step.rb
+++ b/app/models/dev_ops_report/idea_to_production_step.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module DevOpsScore
+module DevOpsReport
class IdeaToProductionStep
attr_accessor :metric, :title, :features
diff --git a/app/models/dev_ops_score/metric.rb b/app/models/dev_ops_report/metric.rb
index a9133128ce9..14eff725433 100644
--- a/app/models/dev_ops_score/metric.rb
+++ b/app/models/dev_ops_report/metric.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module DevOpsScore
+module DevOpsReport
class Metric < ApplicationRecord
include Presentable
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index f9e2f00b9f3..6806008d676 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -16,7 +16,6 @@ class DiffDiscussion < Discussion
:diff_note_positions,
:on_text?,
:on_image?,
-
to: :first_note
def legacy_diff_discussion?
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index adcb2217d85..793cdb5dece 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -24,7 +24,6 @@ class Discussion
:system_note_with_references_visible_for?,
:resource_parent,
:save,
-
to: :first_note
def declarative_policy_delegate
diff --git a/app/models/environment.rb b/app/models/environment.rb
index c6a08c996da..cfdcb0499e6 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -371,7 +371,7 @@ class Environment < ApplicationRecord
end
def elastic_stack_available?
- !!deployment_platform&.cluster&.application_elastic_stack&.available?
+ !!deployment_platform&.cluster&.application_elastic_stack_available?
end
private
diff --git a/app/models/group.rb b/app/models/group.rb
index f8cbaa2495c..c0f145997cc 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -20,8 +20,10 @@ class Group < Namespace
UpdateSharedRunnersError = Class.new(StandardError)
- has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
+ has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
+ has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
+
has_many :users, through: :group_members
has_many :owners,
-> { where(members: { access_level: Gitlab::Access::OWNER }) },
@@ -33,6 +35,7 @@ class Group < Namespace
has_many :milestones
has_many :iterations
+ has_many :services
has_many :shared_group_links, foreign_key: :shared_with_group_id, class_name: 'GroupGroupLink'
has_many :shared_with_group_links, foreign_key: :shared_group_id, class_name: 'GroupGroupLink'
has_many :shared_groups, through: :shared_group_links, source: :shared_group
@@ -395,6 +398,10 @@ class Group < Namespace
])
end
+ def users_count
+ members.count
+ end
+
# Returns all users that are members of projects
# belonging to the current group or sub-groups
def project_users_with_descendants
@@ -403,10 +410,17 @@ class Group < Namespace
.where(namespaces: { id: self_and_descendants.select(:id) })
end
- def max_member_access_for_user(user)
+ # Return the highest access level for a user
+ #
+ # A special case is handled here when the user is a GitLab admin
+ # which implies it has "OWNER" access everywhere, but should not
+ # officially appear as a member of a group unless specifically added to it
+ #
+ # @param user [User]
+ # @param only_concrete_membership [Bool] whether require admin concrete membership status
+ def max_member_access_for_user(user, only_concrete_membership: false)
return GroupMember::NO_ACCESS unless user
-
- return GroupMember::OWNER if user.admin?
+ return GroupMember::OWNER if user.admin? && !only_concrete_membership
max_member_access = members_with_parents.where(user_id: user)
.reorder(access_level: :desc)
@@ -630,6 +644,7 @@ class Group < Namespace
.where(group_member_table[:requested_at].eq(nil))
.where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
.where(group_member_table[:source_type].eq('Namespace'))
+ .non_minimal_access
end
def smallest_value_arel(args, column_alias)
diff --git a/app/models/group_deploy_key.rb b/app/models/group_deploy_key.rb
index 160ac28b33b..c65b00a6de0 100644
--- a/app/models/group_deploy_key.rb
+++ b/app/models/group_deploy_key.rb
@@ -8,6 +8,10 @@ class GroupDeployKey < Key
validates :user, presence: true
+ scope :for_groups, ->(group_ids) do
+ joins(:group_deploy_keys_groups).where(group_deploy_keys_groups: { group_id: group_ids }).uniq
+ end
+
def type
'DeployKey'
end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index 21cf6bfa414..4c0469d849a 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -21,7 +21,7 @@ class InternalId < ApplicationRecord
belongs_to :project
belongs_to :namespace
- enum usage: ::InternalIdEnums.usage_resources
+ enum usage: Enums::InternalId.usage_resources
validates :usage, presence: true
diff --git a/app/models/issuable_severity.rb b/app/models/issuable_severity.rb
new file mode 100644
index 00000000000..d68b3dc48ee
--- /dev/null
+++ b/app/models/issuable_severity.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class IssuableSeverity < ApplicationRecord
+ DEFAULT = 'unknown'
+
+ belongs_to :issue
+
+ validates :issue, presence: true, uniqueness: true
+ validates :severity, presence: true
+
+ enum severity: {
+ unknown: 0,
+ low: 1,
+ medium: 2,
+ high: 3,
+ critical: 4
+ }
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index a0003df87e1..5a5de371301 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -18,6 +18,7 @@ class Issue < ApplicationRecord
include MilestoneEventable
include WhereComposite
include StateEventable
+ include IdInOrdered
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -29,6 +30,11 @@ class Issue < ApplicationRecord
SORTING_PREFERENCE_FIELD = :issues_sort
+ # Types of issues that should be displayed on lists across the app
+ # for example, project issues list, group issues list and issue boards.
+ # Some issue types, like test cases, should be hidden by default.
+ TYPES_FOR_LIST = %w(issue incident).freeze
+
belongs_to :project
has_one :namespace, through: :project
@@ -59,6 +65,7 @@ class Issue < ApplicationRecord
end
end
+ has_one :issuable_severity
has_one :sentry_issue
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
@@ -72,7 +79,8 @@ class Issue < ApplicationRecord
enum issue_type: {
issue: 0,
- incident: 1
+ incident: 1,
+ test_case: 2 ## EE-only
}
alias_attribute :parent_ids, :project_id
@@ -305,6 +313,24 @@ class Issue < ApplicationRecord
end
end
+ def related_issues(current_user, preload: nil)
+ related_issues = ::Issue
+ .select(['issues.*', 'issue_links.id AS issue_link_id',
+ 'issue_links.link_type as issue_link_type_value',
+ 'issue_links.target_id as issue_link_source_id'])
+ .joins("INNER JOIN issue_links ON
+ (issue_links.source_id = issues.id AND issue_links.target_id = #{id})
+ OR
+ (issue_links.target_id = issues.id AND issue_links.source_id = #{id})")
+ .preload(preload)
+ .reorder('issue_link_id')
+
+ cross_project_filter = -> (issues) { issues.where(project: project) }
+ Ability.issues_readable_by_user(related_issues,
+ current_user,
+ filters: { read_cross_project: cross_project_filter })
+ end
+
def can_be_worked_on?
!self.closed? && !self.project.forked?
end
@@ -378,6 +404,15 @@ class Issue < ApplicationRecord
author.id == User.support_bot.id
end
+ def issue_link_type
+ return unless respond_to?(:issue_link_type_value) && respond_to?(:issue_link_source_id)
+
+ type = IssueLink.link_types.key(issue_link_type_value) || IssueLink::TYPE_RELATES_TO
+ return type if issue_link_source_id == id
+
+ IssueLink.inverse_link_type(type)
+ end
+
private
def ensure_metrics
@@ -413,6 +448,11 @@ class Issue < ApplicationRecord
key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
Gitlab::EtagCaching::Store.new.touch(key)
end
+
+ def could_not_move(exception)
+ # Symptom of running out of space - schedule rebalancing
+ IssueRebalancingWorker.perform_async(nil, project_id)
+ end
end
Issue.prepend_if_ee('EE::Issue')
diff --git a/app/models/issue_link.rb b/app/models/issue_link.rb
new file mode 100644
index 00000000000..9740b009396
--- /dev/null
+++ b/app/models/issue_link.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class IssueLink < ApplicationRecord
+ include FromUnion
+
+ belongs_to :source, class_name: 'Issue'
+ belongs_to :target, class_name: 'Issue'
+
+ validates :source, presence: true
+ validates :target, presence: true
+ validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
+ validate :check_self_relation
+
+ scope :for_source_issue, ->(issue) { where(source_id: issue.id) }
+ scope :for_target_issue, ->(issue) { where(target_id: issue.id) }
+
+ TYPE_RELATES_TO = 'relates_to'
+ TYPE_BLOCKS = 'blocks'
+ TYPE_IS_BLOCKED_BY = 'is_blocked_by'
+
+ enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1, TYPE_IS_BLOCKED_BY => 2 }
+
+ def self.inverse_link_type(type)
+ type
+ end
+
+ private
+
+ def check_self_relation
+ return unless source && target
+
+ if source == target
+ errors.add(:source, 'cannot be related to itself')
+ end
+ end
+end
+
+IssueLink.prepend_if_ee('EE::IssueLink')
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index 3495f099064..d223c80fca0 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -29,6 +29,7 @@ class Iteration < ApplicationRecord
scope :upcoming, -> { with_state(:upcoming) }
scope :started, -> { with_state(:started) }
+ scope :closed, -> { with_state(:closed) }
scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL')
@@ -36,8 +37,8 @@ class Iteration < ApplicationRecord
.where('due_date is NULL or due_date >= ?', start_date)
end
- scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date > ?', Date.current) }
- scope :due_date_passed, -> { where('due_date <= ?', Date.current) }
+ scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date >= ?', Date.current) }
+ scope :due_date_passed, -> { where('due_date < ?', Date.current) }
state_machine :state_enum, initial: :upcoming do
event :start do
@@ -63,9 +64,10 @@ class Iteration < ApplicationRecord
case state
when 'closed' then iterations.closed
when 'started' then iterations.started
+ when 'upcoming' then iterations.upcoming
when 'opened' then iterations.started.or(iterations.upcoming)
when 'all' then iterations
- else iterations.upcoming
+ else raise ArgumentError, "Unknown state filter: #{state}"
end
end
diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb
new file mode 100644
index 00000000000..7480800abc3
--- /dev/null
+++ b/app/models/jira_connect_installation.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class JiraConnectInstallation < ApplicationRecord
+ attr_encrypted :shared_secret,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm',
+ key: Settings.attr_encrypted_db_key_base_32
+
+ has_many :subscriptions, class_name: 'JiraConnectSubscription'
+
+ validates :client_key, presence: true, uniqueness: true
+ validates :shared_secret, presence: true
+ validates :base_url, presence: true, public_url: true
+
+ scope :for_project, -> (project) {
+ distinct
+ .joins(:subscriptions)
+ .where(jira_connect_subscriptions: {
+ id: JiraConnectSubscription.for_project(project)
+ })
+ }
+end
diff --git a/app/models/jira_connect_subscription.rb b/app/models/jira_connect_subscription.rb
new file mode 100644
index 00000000000..c74f75b2d8e
--- /dev/null
+++ b/app/models/jira_connect_subscription.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class JiraConnectSubscription < ApplicationRecord
+ belongs_to :installation, class_name: 'JiraConnectInstallation', foreign_key: 'jira_connect_installation_id'
+ belongs_to :namespace
+
+ validates :installation, presence: true
+ validates :namespace, presence: true, uniqueness: { scope: :jira_connect_installation_id, message: 'has already been added' }
+
+ scope :preload_namespace_route, -> { preload(namespace: :route) }
+ scope :for_project, -> (project) { where(namespace_id: project.namespace.self_and_ancestors) }
+end
diff --git a/app/models/jira_import_state.rb b/app/models/jira_import_state.rb
index 2d952c552a8..76b5f1def6a 100644
--- a/app/models/jira_import_state.rb
+++ b/app/models/jira_import_state.rb
@@ -110,10 +110,6 @@ class JiraImportState < ApplicationRecord
)
end
- def self.finished_imports_count
- finished.sum(:imported_issues_count)
- end
-
def mark_as_failed(error_message)
sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb
index 674294f0916..e5632ff2842 100644
--- a/app/models/lfs_objects_project.rb
+++ b/app/models/lfs_objects_project.rb
@@ -19,6 +19,7 @@ class LfsObjectsProject < ApplicationRecord
}
scope :project_id_in, ->(ids) { where(project_id: ids) }
+ scope :lfs_object_in, -> (lfs_objects) { where(lfs_object: lfs_objects) }
private
diff --git a/app/models/member.rb b/app/models/member.rb
index bbc5d638637..5a084a3a2e6 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -25,7 +25,6 @@ class Member < ApplicationRecord
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
allow_nil: true }
- validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validate :higher_access_level_than_group, unless: :importing?
validates :invite_email,
presence: {
@@ -60,6 +59,7 @@ class Member < ApplicationRecord
left_join_users
.where(user_ok)
.where(requested_at: nil)
+ .non_minimal_access
.reorder(nil)
end
@@ -68,6 +68,8 @@ class Member < ApplicationRecord
left_join_users
.where(users: { state: 'active' })
.non_request
+ .non_invite
+ .non_minimal_access
.reorder(nil)
end
@@ -85,6 +87,7 @@ class Member < ApplicationRecord
scope :developers, -> { active.where(access_level: DEVELOPER) }
scope :maintainers, -> { active.where(access_level: MAINTAINER) }
scope :non_guests, -> { where('members.access_level > ?', GUEST) }
+ scope :non_minimal_access, -> { where('members.access_level > ?', MINIMAL_ACCESS) }
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :with_user, -> (user) { where(user: user) }
@@ -161,8 +164,8 @@ class Member < ApplicationRecord
where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
end
- def find_by_invite_token(invite_token)
- invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
+ def find_by_invite_token(raw_invite_token)
+ invite_token = Devise.token_generator.digest(self, :invite_token, raw_invite_token)
find_by(invite_token: invite_token)
end
@@ -397,6 +400,10 @@ class Member < ApplicationRecord
end
end
+ def invite_to_unknown_user?
+ invite? && user_id.nil?
+ end
+
private
def send_invite
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 8c224dea88f..34958936c9f 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -13,6 +13,9 @@ class GroupMember < Member
# Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE
validates :source_type, format: { with: /\ANamespace\z/ }
+ validates :access_level, presence: true
+ validate :access_level_inclusion
+
default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
@@ -45,6 +48,12 @@ class GroupMember < Member
private
+ def access_level_inclusion
+ return if access_level.in?(Gitlab::Access.all_values)
+
+ errors.add(:access_level, "is not included in the list")
+ end
+
def send_invite
run_after_commit_or_now { notification_service.invite_group_member(self, @raw_invite_token) }
diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb
index 6da8d5f3161..88db7f63bd9 100644
--- a/app/models/members_preloader.rb
+++ b/app/models/members_preloader.rb
@@ -12,6 +12,7 @@ class MembersPreloader
ActiveRecord::Associations::Preloader.new.preload(members, :source)
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
+ ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :webauthn_registrations)
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index f4c2d568b4d..3fdc501644d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -21,10 +21,12 @@ class MergeRequest < ApplicationRecord
include MilestoneEventable
include StateEventable
include ApprovableBase
+ include IdInOrdered
extend ::Gitlab::Utils::Override
sha_attribute :squash_commit_sha
+ sha_attribute :merge_ref_sha
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
@@ -80,6 +82,8 @@ class MergeRequest < ApplicationRecord
has_many :merge_request_assignees
has_many :assignees, class_name: "User", through: :merge_request_assignees
+ has_many :merge_request_reviewers
+ has_many :reviewers, class_name: "User", through: :merge_request_reviewers
has_many :user_mentions, class_name: "MergeRequestUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :deployment_merge_requests
@@ -105,7 +109,6 @@ class MergeRequest < ApplicationRecord
after_create :ensure_merge_request_diff
after_update :clear_memoized_shas
- after_update :clear_memoized_source_branch_exists
after_update :reload_diff_if_branch_changed
after_commit :ensure_metrics, on: [:create, :update], unless: :importing?
after_commit :expire_etag_cache, unless: :importing?
@@ -250,6 +253,15 @@ class MergeRequest < ApplicationRecord
joins(:notes).where(notes: { commit_id: sha })
end
scope :join_project, -> { joins(:target_project) }
+ scope :join_metrics, -> do
+ query = joins(:metrics)
+
+ if Feature.enabled?(:improved_mr_merged_at_queries, default_enabled: true)
+ query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
+ end
+
+ query
+ end
scope :references_project, -> { references(:target_project) }
scope :with_api_entity_associations, -> {
preload_routables
@@ -263,6 +275,14 @@ class MergeRequest < ApplicationRecord
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
end
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
+ scope :order_merged_at, ->(direction) do
+ query = join_metrics.order(Gitlab::Database.nulls_last_order('merge_request_metrics.merged_at', direction))
+
+ # Add `merge_request_metrics.merged_at` to the `SELECT` in order to make the keyset pagination work.
+ query.select(*query.arel.projections, MergeRequest::Metrics.arel_table[:merged_at].as('"merge_request_metrics.merged_at"'))
+ end
+ scope :order_merged_at_asc, -> { order_merged_at('ASC') }
+ scope :order_merged_at_desc, -> { order_merged_at('DESC') }
scope :preload_source_project, -> { preload(:source_project) }
scope :preload_target_project, -> { preload(:target_project) }
scope :preload_routables, -> do
@@ -294,7 +314,7 @@ class MergeRequest < ApplicationRecord
alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds
alias_method :issuing_parent, :target_project
- delegate :active?, to: :head_pipeline, prefix: true, allow_nil: true
+ delegate :active?, :builds_with_coverage, to: :head_pipeline, prefix: true, allow_nil: true
delegate :success?, :active?, to: :actual_head_pipeline, prefix: true, allow_nil: true
RebaseLockTimeout = Class.new(StandardError)
@@ -319,6 +339,15 @@ class MergeRequest < ApplicationRecord
.pluck(:target_branch)
end
+ def self.sort_by_attribute(method, excluded_labels: [])
+ case method.to_s
+ when 'merged_at', 'merged_at_asc' then order_merged_at_asc.with_order_id_desc
+ when 'merged_at_desc' then order_merged_at_desc.with_order_id_desc
+ else
+ super
+ end
+ end
+
def rebase_in_progress?
rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)
end
@@ -333,7 +362,11 @@ class MergeRequest < ApplicationRecord
def merge_pipeline
return unless merged?
- target_project.pipeline_for(target_branch, merge_commit_sha)
+ # When the merge_method is :merge there will be a merge_commit_sha, however
+ # when it is fast-forward there is no merge commit, so we must fall back to
+ # either the squash commit (if the MR was squashed) or the diff head commit.
+ sha = merge_commit_sha || squash_commit_sha || diff_head_sha
+ target_project.latest_pipeline(target_branch, sha)
end
# Pattern used to extract `!123` merge request references from text
@@ -867,10 +900,6 @@ class MergeRequest < ApplicationRecord
clear_memoization(:target_branch_head)
end
- def clear_memoized_source_branch_exists
- clear_memoization(:source_branch_exists)
- end
-
def reload_diff_if_branch_changed
if (saved_change_to_source_branch? || saved_change_to_target_branch?) &&
(source_branch_head && target_branch_head)
@@ -928,8 +957,9 @@ class MergeRequest < ApplicationRecord
self.class.wip_title(self.title)
end
- def mergeable?(skip_ci_check: false)
- return false unless mergeable_state?(skip_ci_check: skip_ci_check)
+ def mergeable?(skip_ci_check: false, skip_discussions_check: false)
+ return false unless mergeable_state?(skip_ci_check: skip_ci_check,
+ skip_discussions_check: skip_discussions_check)
check_mergeability
@@ -1122,11 +1152,9 @@ class MergeRequest < ApplicationRecord
end
def source_branch_exists?
- strong_memoize(:source_branch_exists) do
- next false unless self.source_project
+ return false unless self.source_project
- self.source_project.repository.branch_exists?(self.source_branch)
- end
+ self.source_project.repository.branch_exists?(self.source_branch)
end
def target_branch_exists?
@@ -1232,6 +1260,8 @@ class MergeRequest < ApplicationRecord
# Returns the current merge-ref HEAD commit.
#
def merge_ref_head
+ return project.repository.commit(merge_ref_sha) if merge_ref_sha
+
project.repository.commit(merge_ref_path)
end
@@ -1345,9 +1375,9 @@ class MergeRequest < ApplicationRecord
end
def has_coverage_reports?
- return false unless Feature.enabled?(:coverage_report_view, project)
+ return false unless Feature.enabled?(:coverage_report_view, project, default_enabled: true)
- actual_head_pipeline&.has_reports?(Ci::JobArtifact.coverage_reports)
+ actual_head_pipeline&.has_coverage_reports?
end
def has_terraform_reports?
@@ -1447,6 +1477,19 @@ class MergeRequest < ApplicationRecord
Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
end
+ def merged_commit_sha
+ return unless merged?
+
+ sha = merge_commit_sha || squash_commit_sha || diff_head_sha
+ sha.presence
+ end
+
+ def short_merged_commit_sha
+ if sha = merged_commit_sha
+ Commit.truncate_sha(sha)
+ end
+ end
+
def can_be_reverted?(current_user)
return false unless merge_commit
return false unless merged_at
@@ -1561,7 +1604,7 @@ class MergeRequest < ApplicationRecord
def first_contribution?
return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
- project.merge_requests.merged.where(author_id: author_id).empty?
+ !project.merge_requests.merged.exists?(author_id: author_id)
end
# TODO: remove once production database rename completes
@@ -1633,6 +1676,10 @@ class MergeRequest < ApplicationRecord
end
end
+ def allows_reviewers?
+ Feature.enabled?(:merge_request_reviewers, project)
+ end
+
private
def with_rebase_lock
diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb
index 2ac1de4321a..73f8fe77b04 100644
--- a/app/models/merge_request_assignee.rb
+++ b/app/models/merge_request_assignee.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class MergeRequestAssignee < ApplicationRecord
- belongs_to :merge_request
+ belongs_to :merge_request, touch: true
belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees
validates :assignee, uniqueness: { scope: :merge_request_id }
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index b70340a98cd..880e3cc1ba5 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -17,6 +17,10 @@ class MergeRequestDiff < ApplicationRecord
# diffs to external storage
EXTERNAL_DIFF_CUTOFF = 7.days.freeze
+ # The files_count column is a 2-byte signed integer. Look up the true value
+ # from the database if this sentinel is seen
+ FILES_COUNT_SENTINEL = 2**15 - 1
+
belongs_to :merge_request
manual_inverse_association :merge_request, :merge_request_diff
@@ -150,10 +154,10 @@ class MergeRequestDiff < ApplicationRecord
# 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?
- after_create :set_count_columns
after_create_commit :set_as_latest_diff, unless: :importing?
after_save :update_external_diff_store
+ after_save :set_count_columns
def self.find_by_diff_refs(diff_refs)
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
@@ -202,6 +206,17 @@ class MergeRequestDiff < ApplicationRecord
end
end
+ def files_count
+ db_value = read_attribute(:files_count)
+
+ case db_value
+ when nil, FILES_COUNT_SENTINEL
+ merge_request_diff_files.count
+ else
+ db_value
+ end
+ end
+
# This method will rely on repository branch sha
# in case start_commit_sha is nil. Its necesarry for old merge request diff
# created before version 8.4 to work
@@ -423,7 +438,7 @@ class MergeRequestDiff < ApplicationRecord
# external storage. If external storage isn't an option for this diff, the
# method is a no-op.
def migrate_files_to_external_storage!
- return if stored_externally? || !use_external_diff? || merge_request_diff_files.count == 0
+ return if stored_externally? || !use_external_diff? || files_count == 0
rows = build_merge_request_diff_files(merge_request_diff_files)
rows = build_external_merge_request_diff_files(rows)
@@ -449,7 +464,7 @@ class MergeRequestDiff < ApplicationRecord
# If this diff isn't in external storage, the method is a no-op.
def migrate_files_to_database!
return unless stored_externally?
- return if merge_request_diff_files.count == 0
+ return if files_count == 0
rows = convert_external_diffs_to_database
@@ -666,7 +681,7 @@ class MergeRequestDiff < ApplicationRecord
def set_count_columns
update_columns(
commits_count: merge_request_diff_commits.size,
- files_count: merge_request_diff_files.size
+ files_count: [FILES_COUNT_SENTINEL, merge_request_diff_files.size].min
)
end
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index 23319445a38..55ff4250c2d 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -25,6 +25,16 @@ class MergeRequestDiffFile < ApplicationRecord
super
end
- binary? ? content.unpack1('m0') : content
+ return content unless binary?
+
+ # If the data isn't valid base64, return it as-is, since it's almost certain
+ # to be a valid diff. Parsing it as a diff will fail if it's something else.
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/240921
+ begin
+ content.unpack1('m0')
+ rescue ArgumentError
+ content
+ end
end
end
diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb
new file mode 100644
index 00000000000..1cb49c0cd76
--- /dev/null
+++ b/app/models/merge_request_reviewer.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class MergeRequestReviewer < ApplicationRecord
+ belongs_to :merge_request
+ belongs_to :reviewer, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index e529ba6b486..527fa9d52d0 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -135,6 +135,10 @@ class Namespace < ApplicationRecord
uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) }
end
+ def clean_name(value)
+ value.scan(Gitlab::Regex.group_name_regex_chars).join(' ')
+ end
+
def find_by_pages_host(host)
gitlab_host = "." + Settings.pages.host.downcase
host = host.downcase
@@ -349,6 +353,10 @@ class Namespace < ApplicationRecord
)
end
+ def any_project_with_pages_deployed?
+ all_projects.with_pages_deployed.any?
+ end
+
def closest_setting(name)
self_and_ancestors(hierarchy_order: :asc)
.find { |n| !n.read_attribute(name).nil? }
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
index 2ad6ea59588..5723a823e98 100644
--- a/app/models/namespace/root_storage_statistics.rb
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -2,7 +2,16 @@
class Namespace::RootStorageStatistics < ApplicationRecord
SNIPPETS_SIZE_STAT_NAME = 'snippets_size'.freeze
- STATISTICS_ATTRIBUTES = %W(storage_size repository_size wiki_size lfs_objects_size build_artifacts_size packages_size #{SNIPPETS_SIZE_STAT_NAME}).freeze
+ STATISTICS_ATTRIBUTES = %W(
+ storage_size
+ repository_size
+ wiki_size
+ lfs_objects_size
+ build_artifacts_size
+ packages_size
+ #{SNIPPETS_SIZE_STAT_NAME}
+ pipeline_artifacts_size
+ ).freeze
self.primary_key = :namespace_id
@@ -40,7 +49,8 @@ class Namespace::RootStorageStatistics < ApplicationRecord
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
'COALESCE(SUM(ps.packages_size), 0) AS packages_size',
- "COALESCE(SUM(ps.snippets_size), 0) AS #{SNIPPETS_SIZE_STAT_NAME}"
+ "COALESCE(SUM(ps.snippets_size), 0) AS #{SNIPPETS_SIZE_STAT_NAME}",
+ 'COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size'
)
end
diff --git a/app/models/note.rb b/app/models/note.rb
index e1fc16818b3..812d77d5f86 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -20,20 +20,6 @@ class Note < ApplicationRecord
include ThrottledTouch
include FromUnion
- module SpecialRole
- FIRST_TIME_CONTRIBUTOR = :first_time_contributor
-
- class << self
- def values
- constants.map {|const| self.const_get(const, false)}
- end
-
- def value?(val)
- values.include?(val)
- end
- end
- end
-
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
redact_field :note
@@ -60,9 +46,6 @@ class Note < ApplicationRecord
# Attribute used to store the attributes that have been changed by quick actions.
attr_accessor :commands_changes
- # A special role that may be displayed on issuable's discussions
- attr_reader :special_role
-
default_value_for :system, false
attr_mentionable :note, pipeline: :note
@@ -220,10 +203,6 @@ class Note < ApplicationRecord
.where(noteable_type: type, noteable_id: ids)
end
- def has_special_role?(role, note)
- note.special_role == role
- end
-
def search(query)
fuzzy_search(query, [:note])
end
@@ -342,20 +321,20 @@ class Note < ApplicationRecord
noteable.author_id == user.id
end
- def special_role=(role)
- raise "Role is undefined, #{role} not found in #{SpecialRole.values}" unless SpecialRole.value?(role)
+ def contributor?
+ return false unless ::Feature.enabled?(:show_contributor_on_note, project)
- @special_role = role
+ project&.team&.contributor?(self.author_id)
end
- def has_special_role?(role)
- self.class.has_special_role?(role, self)
- end
+ def noteable_author?(noteable)
+ return false unless ::Feature.enabled?(:show_author_on_note, project)
- def specialize_for_first_contribution!(noteable)
- return unless noteable.author_id == self.author_id
+ noteable.author == self.author
+ end
- self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
+ def project_name
+ project&.name
end
def confidential?(include_noteable: false)
@@ -556,6 +535,8 @@ class Note < ApplicationRecord
end
def system_note_with_references_visible_for?(user)
+ return true unless system?
+
(!system_note_with_references? || all_referenced_mentionables_allowed?(user)) && system_note_viewable_by?(user)
end
@@ -563,6 +544,10 @@ class Note < ApplicationRecord
noteable.author if for_personal_snippet?
end
+ def skip_notification?
+ review.present?
+ end
+
private
# Using this method followed by a call to `save` may result in ActiveRecord::RecordNotUnique exception
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
new file mode 100644
index 00000000000..586e9d689a1
--- /dev/null
+++ b/app/models/operations/feature_flag.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module Operations
+ class FeatureFlag < ApplicationRecord
+ include AtomicInternalId
+ include IidRoutes
+
+ self.table_name = 'operations_feature_flags'
+
+ belongs_to :project
+
+ has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.operations_feature_flags&.maximum(:iid) }
+
+ default_value_for :active, true
+
+ # scopes exists only for the first version
+ has_many :scopes, class_name: 'Operations::FeatureFlagScope'
+ # strategies exists only for the second version
+ has_many :strategies, class_name: 'Operations::FeatureFlags::Strategy'
+ has_many :feature_flag_issues
+ has_many :issues, through: :feature_flag_issues
+ has_one :default_scope, -> { where(environment_scope: '*') }, class_name: 'Operations::FeatureFlagScope'
+
+ validates :project, presence: true
+ validates :name,
+ presence: true,
+ length: 2..63,
+ format: {
+ with: Gitlab::Regex.feature_flag_regex,
+ message: Gitlab::Regex.feature_flag_regex_message
+ }
+ validates :name, uniqueness: { scope: :project_id }
+ validates :description, allow_blank: true, length: 0..255
+ validate :first_default_scope, on: :create, if: :has_scopes?
+ validate :version_associations
+
+ before_create :build_default_scope, if: -> { legacy_flag? && scopes.none? }
+
+ accepts_nested_attributes_for :scopes, allow_destroy: true
+ accepts_nested_attributes_for :strategies, allow_destroy: true
+
+ scope :ordered, -> { order(:name) }
+
+ scope :enabled, -> { where(active: true) }
+ scope :disabled, -> { where(active: false) }
+
+ enum version: {
+ legacy_flag: 1,
+ new_version_flag: 2
+ }
+
+ class << self
+ def preload_relations
+ preload(:scopes, strategies: :scopes)
+ end
+
+ def for_unleash_client(project, environment)
+ includes(strategies: [:scopes, :user_list])
+ .where(project: project)
+ .merge(Operations::FeatureFlags::Scope.on_environment(environment))
+ .reorder(:id)
+ .references(:operations_scopes)
+ end
+ end
+
+ def related_issues(current_user, preload:)
+ issues = ::Issue
+ .select('issues.*, operations_feature_flags_issues.id AS link_id')
+ .joins(:feature_flag_issues)
+ .where('operations_feature_flags_issues.feature_flag_id = ?', id)
+ .order('operations_feature_flags_issues.id ASC')
+ .includes(preload)
+
+ Ability.issues_readable_by_user(issues, current_user)
+ end
+
+ private
+
+ def version_associations
+ if new_version_flag? && scopes.any?
+ errors.add(:version_associations, 'version 2 feature flags may not have scopes')
+ elsif legacy_flag? && strategies.any?
+ errors.add(:version_associations, 'version 1 feature flags may not have strategies')
+ end
+ end
+
+ def first_default_scope
+ unless scopes.first.environment_scope == '*'
+ errors.add(:default_scope, 'has to be the first element')
+ end
+ end
+
+ def build_default_scope
+ scopes.build(environment_scope: '*', active: self.active)
+ end
+
+ def has_scopes?
+ scopes.any?
+ end
+ end
+end
diff --git a/app/models/operations/feature_flag_scope.rb b/app/models/operations/feature_flag_scope.rb
new file mode 100644
index 00000000000..78be29f2531
--- /dev/null
+++ b/app/models/operations/feature_flag_scope.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Operations
+ class FeatureFlagScope < ApplicationRecord
+ prepend HasEnvironmentScope
+ include Gitlab::Utils::StrongMemoize
+
+ self.table_name = 'operations_feature_flag_scopes'
+
+ belongs_to :feature_flag
+
+ validates :environment_scope, uniqueness: {
+ scope: :feature_flag,
+ message: "(%{value}) has already been taken"
+ }
+
+ validates :environment_scope,
+ if: :default_scope?, on: :update,
+ inclusion: { in: %w(*), message: 'cannot be changed from default scope' }
+
+ validates :strategies, feature_flag_strategies: true
+
+ before_destroy :prevent_destroy_default_scope, if: :default_scope?
+
+ scope :ordered, -> { order(:id) }
+ scope :enabled, -> { where(active: true) }
+ scope :disabled, -> { where(active: false) }
+
+ def self.with_name_and_description
+ joins(:feature_flag)
+ .select(FeatureFlag.arel_table[:name], FeatureFlag.arel_table[:description])
+ end
+
+ def self.for_unleash_client(project, environment)
+ select_columns = [
+ 'DISTINCT ON (operations_feature_flag_scopes.feature_flag_id) operations_feature_flag_scopes.id',
+ '(operations_feature_flags.active AND operations_feature_flag_scopes.active) AS active',
+ 'operations_feature_flag_scopes.strategies',
+ 'operations_feature_flag_scopes.environment_scope',
+ 'operations_feature_flag_scopes.created_at',
+ 'operations_feature_flag_scopes.updated_at'
+ ]
+
+ select(select_columns)
+ .with_name_and_description
+ .where(feature_flag_id: project.operations_feature_flags.select(:id))
+ .order(:feature_flag_id)
+ .on_environment(environment)
+ .reverse_order
+ end
+
+ private
+
+ def default_scope?
+ environment_scope_was == '*'
+ end
+
+ def prevent_destroy_default_scope
+ raise ActiveRecord::ReadOnlyRecord, "default scope cannot be destroyed"
+ end
+ end
+end
diff --git a/app/models/operations/feature_flags/scope.rb b/app/models/operations/feature_flags/scope.rb
new file mode 100644
index 00000000000..d70101b5e0d
--- /dev/null
+++ b/app/models/operations/feature_flags/scope.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Operations
+ module FeatureFlags
+ class Scope < ApplicationRecord
+ prepend HasEnvironmentScope
+
+ self.table_name = 'operations_scopes'
+
+ belongs_to :strategy, class_name: 'Operations::FeatureFlags::Strategy'
+ end
+ end
+end
diff --git a/app/models/operations/feature_flags/strategy.rb b/app/models/operations/feature_flags/strategy.rb
new file mode 100644
index 00000000000..ff68af9741e
--- /dev/null
+++ b/app/models/operations/feature_flags/strategy.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Operations
+ module FeatureFlags
+ class Strategy < ApplicationRecord
+ STRATEGY_DEFAULT = 'default'
+ STRATEGY_GITLABUSERLIST = 'gitlabUserList'
+ STRATEGY_GRADUALROLLOUTUSERID = 'gradualRolloutUserId'
+ STRATEGY_USERWITHID = 'userWithId'
+ STRATEGIES = {
+ STRATEGY_DEFAULT => [].freeze,
+ STRATEGY_GITLABUSERLIST => [].freeze,
+ STRATEGY_GRADUALROLLOUTUSERID => %w[groupId percentage].freeze,
+ STRATEGY_USERWITHID => ['userIds'].freeze
+ }.freeze
+ USERID_MAX_LENGTH = 256
+
+ self.table_name = 'operations_strategies'
+
+ belongs_to :feature_flag
+ has_many :scopes, class_name: 'Operations::FeatureFlags::Scope'
+ has_one :strategy_user_list
+ has_one :user_list, through: :strategy_user_list
+
+ validates :name,
+ inclusion: {
+ in: STRATEGIES.keys,
+ message: 'strategy name is invalid'
+ }
+
+ validate :parameters_validations, if: -> { errors[:name].blank? }
+ validates :user_list, presence: true, if: -> { name == STRATEGY_GITLABUSERLIST }
+ validates :user_list, absence: true, if: -> { name != STRATEGY_GITLABUSERLIST }
+ validate :same_project_validation, if: -> { user_list.present? }
+
+ accepts_nested_attributes_for :scopes, allow_destroy: true
+
+ def user_list_id=(user_list_id)
+ self.user_list = ::Operations::FeatureFlags::UserList.find(user_list_id)
+ end
+
+ private
+
+ def same_project_validation
+ unless user_list.project_id == feature_flag.project_id
+ errors.add(:user_list, 'must belong to the same project')
+ end
+ end
+
+ def parameters_validations
+ validate_parameters_type &&
+ validate_parameters_keys &&
+ validate_parameters_values
+ end
+
+ def validate_parameters_type
+ parameters.is_a?(Hash) || parameters_error('parameters are invalid')
+ end
+
+ def validate_parameters_keys
+ actual_keys = parameters.keys.sort
+ expected_keys = STRATEGIES[name].sort
+ expected_keys == actual_keys || parameters_error('parameters are invalid')
+ end
+
+ def validate_parameters_values
+ case name
+ when STRATEGY_GRADUALROLLOUTUSERID
+ gradual_rollout_user_id_parameters_validation
+ when STRATEGY_USERWITHID
+ FeatureFlagUserXidsValidator.validate_user_xids(self, :parameters, parameters['userIds'], 'userIds')
+ end
+ end
+
+ def gradual_rollout_user_id_parameters_validation
+ percentage = parameters['percentage']
+ group_id = parameters['groupId']
+
+ unless percentage.is_a?(String) && percentage.match(/\A[1-9]?[0-9]\z|\A100\z/)
+ parameters_error('percentage must be a string between 0 and 100 inclusive')
+ end
+
+ unless group_id.is_a?(String) && group_id.match(/\A[a-z]{1,32}\z/)
+ parameters_error('groupId parameter is invalid')
+ end
+ end
+
+ def parameters_error(message)
+ errors.add(:parameters, message)
+ false
+ end
+ end
+ end
+end
diff --git a/app/models/operations/feature_flags/strategy_user_list.rb b/app/models/operations/feature_flags/strategy_user_list.rb
new file mode 100644
index 00000000000..813b632dd67
--- /dev/null
+++ b/app/models/operations/feature_flags/strategy_user_list.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Operations
+ module FeatureFlags
+ class StrategyUserList < ApplicationRecord
+ self.table_name = 'operations_strategies_user_lists'
+
+ belongs_to :strategy
+ belongs_to :user_list
+ end
+ end
+end
diff --git a/app/models/operations/feature_flags/user_list.rb b/app/models/operations/feature_flags/user_list.rb
new file mode 100644
index 00000000000..b9bdcb59d5f
--- /dev/null
+++ b/app/models/operations/feature_flags/user_list.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Operations
+ module FeatureFlags
+ class UserList < ApplicationRecord
+ include AtomicInternalId
+ include IidRoutes
+
+ self.table_name = 'operations_user_lists'
+
+ belongs_to :project
+ has_many :strategy_user_lists
+ has_many :strategies, through: :strategy_user_lists
+
+ has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.operations_feature_flags_user_lists&.maximum(:iid) }, presence: true
+
+ validates :project, presence: true
+ validates :name,
+ presence: true,
+ uniqueness: { scope: :project_id },
+ length: 1..255
+ validates :user_xids, feature_flag_user_xids: true
+
+ before_destroy :ensure_no_associated_strategies
+
+ private
+
+ def ensure_no_associated_strategies
+ if strategies.present?
+ errors.add(:base, 'User list is associated with a strategy')
+ throw :abort # rubocop: disable Cop/BanCatchThrow
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/operations/feature_flags_client.rb b/app/models/operations/feature_flags_client.rb
new file mode 100644
index 00000000000..1c65c3f096e
--- /dev/null
+++ b/app/models/operations/feature_flags_client.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Operations
+ class FeatureFlagsClient < ApplicationRecord
+ include TokenAuthenticatable
+
+ self.table_name = 'operations_feature_flags_clients'
+
+ belongs_to :project
+
+ validates :project, presence: true
+ validates :token, presence: true
+
+ add_authentication_token_field :token, encrypted: :required
+
+ before_validation :ensure_token!
+
+ def self.find_for_project_and_token(project, token)
+ return unless project
+ return unless token
+
+ where(project_id: project).find_by_token(token)
+ end
+ end
+end
diff --git a/app/models/packages/conan/file_metadatum.rb b/app/models/packages/conan/file_metadatum.rb
index e1ef62b3959..de54580e948 100644
--- a/app/models/packages/conan/file_metadatum.rb
+++ b/app/models/packages/conan/file_metadatum.rb
@@ -3,6 +3,9 @@
class Packages::Conan::FileMetadatum < ApplicationRecord
belongs_to :package_file, inverse_of: :conan_file_metadatum
+ DEFAULT_PACKAGE_REVISION = '0'.freeze
+ DEFAULT_RECIPE_REVISION = '0'.freeze
+
validates :package_file, presence: true
validates :recipe_revision,
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index d6633456de4..bda11160957 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -5,6 +5,8 @@ class Packages::Package < ApplicationRecord
include UsageStatistics
belongs_to :project
+ belongs_to :creator, class_name: 'User'
+
# package_files must be destroyed by ruby code in order to properly remove carrierwave uploads and update project statistics
has_many :package_files, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :dependency_links, inverse_of: :package, class_name: 'Packages::DependencyLink'
@@ -37,8 +39,13 @@ class Packages::Package < ApplicationRecord
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
+ validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
+ validates :version,
+ presence: true,
+ format: { with: Gitlab::Regex.generic_package_version_regex },
+ if: :generic?
- enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6 }
+ enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7 }
scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
@@ -46,6 +53,9 @@ class Packages::Package < ApplicationRecord
scope :with_version, ->(version) { where(version: version) }
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
+ scope :including_build_info, -> { includes(build_info: { pipeline: :user }) }
+ scope :including_project_route, -> { includes(project: { namespace: :route }) }
+ scope :including_tags, -> { includes(:tags) }
scope :with_conan_channel, ->(package_channel) do
joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel })
@@ -138,6 +148,8 @@ class Packages::Package < ApplicationRecord
def versions
project.packages
+ .including_build_info
+ .including_tags
.with_name(name)
.where.not(version: version)
.with_package_type(package_type)
diff --git a/app/models/packages/pypi/metadatum.rb b/app/models/packages/pypi/metadatum.rb
index 7e6456ad964..2e4d61eaf53 100644
--- a/app/models/packages/pypi/metadatum.rb
+++ b/app/models/packages/pypi/metadatum.rb
@@ -6,6 +6,7 @@ class Packages::Pypi::Metadatum < ApplicationRecord
belongs_to :package, -> { where(package_type: :pypi) }, inverse_of: :pypi_metadatum
validates :package, presence: true
+ validates :required_python, length: { maximum: 255 }, allow_blank: true
validate :pypi_package_type
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 51c496c77d3..84d820e539c 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -22,10 +22,11 @@ module Pages
end
def source
- {
- type: 'file',
- path: File.join(project.full_path, 'public/')
- }
+ if artifacts_archive && !artifacts_archive.file_storage?
+ zip_source
+ else
+ file_source
+ end
end
def prefix
@@ -39,5 +40,28 @@ module Pages
private
attr_reader :project, :trim_prefix, :domain
+
+ def artifacts_archive
+ return unless Feature.enabled?(:pages_artifacts_archive, project)
+
+ # Using build artifacts is temporary solution for quick test
+ # in production environment, we'll replace this with proper
+ # `pages_deployments` later
+ project.pages_metadatum.artifacts_archive&.file
+ end
+
+ def zip_source
+ {
+ type: 'zip',
+ path: artifacts_archive.url(expire_at: 1.day.from_now)
+ }
+ end
+
+ def file_source
+ {
+ type: 'file',
+ path: File.join(project.full_path, 'public/')
+ }
+ end
end
end
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
new file mode 100644
index 00000000000..78e0f185a11
--- /dev/null
+++ b/app/models/pages_deployment.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# PagesDeployment stores a zip archive containing GitLab Pages web-site
+class PagesDeployment < ApplicationRecord
+ belongs_to :project, optional: false
+ belongs_to :ci_build, class_name: 'Ci::Build', optional: true
+
+ validates :file, presence: true
+ validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
+ validates :size, presence: true, numericality: { greater_than: 0, only_integer: true }
+end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index d071d2d3c89..98db47deaa3 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -249,11 +249,7 @@ class PagesDomain < ApplicationRecord
return if usage_serverless?
return unless pages_deployed?
- if Feature.enabled?(:async_update_pages_config, project)
- run_after_commit { PagesUpdateConfigurationWorker.perform_async(project_id) }
- else
- Projects::UpdatePagesConfigurationService.new(project).execute
- end
+ run_after_commit { PagesUpdateConfigurationWorker.perform_async(project_id) }
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb
index bf87d2c3916..40d14aaa1de 100644
--- a/app/models/performance_monitoring/prometheus_dashboard.rb
+++ b/app/models/performance_monitoring/prometheus_dashboard.rb
@@ -53,14 +53,23 @@ module PerformanceMonitoring
# This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398
# implementation. For new existing logic was reused to faster deliver MVC
def schema_validation_warnings
+ return run_custom_validation.map(&:message) if Feature.enabled?(:metrics_dashboard_exhaustive_validations, environment&.project)
+
self.class.from_json(reload_schema)
- nil
+ []
+ rescue Gitlab::Metrics::Dashboard::Errors::LayoutError => error
+ [error.message]
rescue ActiveModel::ValidationError => exception
exception.model.errors.map { |attr, error| "#{attr}: #{error}" }
end
private
+ def run_custom_validation
+ Gitlab::Metrics::Dashboard::Validator
+ .errors(reload_schema, dashboard_path: path, project: environment&.project)
+ end
+
# dashboard finder methods are somehow limited, #find includes checking if
# user is authorised to view selected dashboard, but modifies schema, which in some cases may
# cause false positives returned from validation, and #find_raw does not authorise users
diff --git a/app/models/product_analytics_event.rb b/app/models/product_analytics_event.rb
index 579ea88c272..d2026d3b333 100644
--- a/app/models/product_analytics_event.rb
+++ b/app/models/product_analytics_event.rb
@@ -20,10 +20,19 @@ class ProductAnalyticsEvent < ApplicationRecord
where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1)
}
+ scope :by_category_and_action, ->(category, action) { where(se_category: category, se_action: action) }
+
def self.count_by_graph(graph, days)
group(graph).timerange(days).count
end
+ def self.count_collector_tstamp_by_day(days)
+ group("DATE_TRUNC('day', collector_tstamp)")
+ .reorder('date_trunc_day_collector_tstamp')
+ .timerange(days)
+ .count
+ end
+
def as_json_wo_empty
as_json.compact
end
diff --git a/app/models/project.rb b/app/models/project.rb
index e1b6a9c41dd..4db0eaa0442 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -3,7 +3,6 @@
require 'carrierwave/orm/activerecord'
class Project < ApplicationRecord
- extend ::Gitlab::Utils::Override
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
include AccessRequestable
@@ -147,6 +146,7 @@ class Project < ApplicationRecord
has_one :discord_service
has_one :drone_ci_service
has_one :emails_on_push_service
+ has_one :ewm_service
has_one :pipelines_email_service
has_one :irker_service
has_one :pivotaltracker_service
@@ -245,7 +245,6 @@ class Project < ApplicationRecord
has_many :lfs_file_locks
has_many :project_group_links
has_many :invited_groups, through: :project_group_links, source: :group
- has_many :pages_domains
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
@@ -254,6 +253,7 @@ class Project < ApplicationRecord
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
+ has_one :feature_usage, class_name: 'ProjectFeatureUsage'
has_one :cluster_project, class_name: 'Clusters::Project'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
@@ -279,10 +279,9 @@ class Project < ApplicationRecord
# The relation :all_pipelines is intended to be used when we want to get the
# whole list of pipelines associated to the project
has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
- # The relation :ci_pipelines is intended to be used when we want to get only
- # those pipeline which are directly related to CI. There are
- # other pipelines, like webide ones, that we won't retrieve
- # if we use this relation.
+ # The relation :ci_pipelines includes all those that directly contribute to the
+ # latest status of a ref. This does not include dangling pipelines such as those
+ # from webide, child pipelines, etc.
has_many :ci_pipelines,
-> { ci_sources },
class_name: 'Ci::Pipeline',
@@ -327,8 +326,6 @@ class Project < ApplicationRecord
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id
- has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
-
has_many :import_failures, inverse_of: :project
has_many :jira_imports, -> { order 'jira_imports.created_at' }, class_name: 'JiraImportState', inverse_of: :project
@@ -339,10 +336,19 @@ class Project < ApplicationRecord
has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
has_many :reviews, inverse_of: :project
+ # GitLab Pages
+ has_many :pages_domains
+ has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
+ has_many :pages_deployments
+
# Can be too many records. We need to implement delete_all in batches.
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637
has_many :product_analytics_events, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :operations_feature_flags, class_name: 'Operations::FeatureFlag'
+ has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
+ has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :project_setting, update_only: true
@@ -393,6 +399,8 @@ class Project < ApplicationRecord
to: :project_setting
delegate :active?, to: :prometheus_service, allow_nil: true, prefix: true
+ delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
+
# Validations
validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true
@@ -454,14 +462,17 @@ class Project < ApplicationRecord
# Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name
scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
- scope :sorted_by_similarity_desc, -> (search) do
+ scope :sorted_by_similarity_desc, -> (search, include_in_select: false) do
order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
{ column: arel_table["path"], multiplier: 1 },
{ column: arel_table["name"], multiplier: 0.7 },
{ column: arel_table["description"], multiplier: 0.2 }
])
- reorder(order_expression.desc, arel_table['id'].desc)
+ query = reorder(order_expression.desc, arel_table['id'].desc)
+
+ query = query.select(*query.arel.projections, order_expression.as('similarity')) if include_in_select
+ query
end
scope :with_packages, -> { joins(:packages) }
@@ -476,6 +487,9 @@ class Project < ApplicationRecord
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
+ scope :with_active_jira_services, -> { joins(:services).merge(::JiraService.active) } # rubocop:disable CodeReuse/ServiceClass
+ scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) }
+ scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
scope :inc_routes, -> { includes(:route, namespace: :route) }
scope :with_statistics, -> { includes(:statistics) }
scope :with_namespace, -> { includes(:namespace) }
@@ -545,6 +559,8 @@ class Project < ApplicationRecord
preload(:project_feature, :route, namespace: [:route, :owner])
}
+ scope :imported_from, -> (type) { where(import_type: type) }
+
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
@@ -882,12 +898,12 @@ class Project < ApplicationRecord
end
def repository
- @repository ||= Repository.new(full_path, self, shard: repository_storage, disk_path: disk_path)
+ @repository ||= Gitlab::GlRepository::PROJECT.repository_for(self)
end
def design_repository
strong_memoize(:design_repository) do
- DesignManagement::Repository.new(self)
+ Gitlab::GlRepository::DESIGN.repository_for(self)
end
end
@@ -942,13 +958,12 @@ class Project < ApplicationRecord
latest_successful_build_for_ref(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}"))
end
- def latest_pipeline_for_ref(ref = default_branch)
+ def latest_pipeline(ref = default_branch, sha = nil)
ref = ref.presence || default_branch
- sha = commit(ref)&.sha
-
+ sha ||= commit(ref)&.sha
return unless sha
- ci_pipelines.newest_first(ref: ref, sha: sha).first
+ ci_pipelines.newest_first(ref: ref, sha: sha).take
end
def merge_base_commit(first_commit_id, second_commit_id)
@@ -1442,6 +1457,10 @@ class Project < ApplicationRecord
http_url_to_repo
end
+ def feature_usage
+ super.presence || build_feature_usage
+ end
+
def forked?
fork_network && fork_network.root_project != self
end
@@ -1452,44 +1471,10 @@ class Project < ApplicationRecord
forked_from_project || fork_network&.root_project
end
- # TODO: Remove this method once all LfsObjectsProject records are backfilled
- # for forks.
- #
- # See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info.
- def lfs_storage_project
- @lfs_storage_project ||= begin
- result = self
-
- # TODO: Make this go to the fork_network root immediately
- # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-foss/issues/39769
- result = result.fork_source while result&.forked?
-
- result || self
- end
- end
-
- # This will return all `lfs_objects` that are accessible to the project and
- # the fork source. This is needed since older forks won't have access to some
- # LFS objects directly and have to get it from the fork source.
- #
- # TODO: Remove this method once all LfsObjectsProject records are backfilled
- # for forks. At that point, projects can look at their own `lfs_objects`.
- #
- # See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info.
- def all_lfs_objects
+ def lfs_objects_for_repository_types(*types)
LfsObject
- .distinct
.joins(:lfs_objects_projects)
- .where(lfs_objects_projects: { project_id: [self, lfs_storage_project] })
- end
-
- # TODO: Remove this method once all LfsObjectsProject records are backfilled
- # for forks. At that point, projects can look at their own `lfs_objects` so
- # `lfs_objects_oids` can be used instead.
- #
- # See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info.
- def all_lfs_objects_oids(oids: [])
- oids(all_lfs_objects, oids: oids)
+ .where(lfs_objects_projects: { project: self, repository_type: types })
end
def lfs_objects_oids(oids: [])
@@ -1653,21 +1638,6 @@ class Project < ApplicationRecord
!namespace.share_with_group_lock
end
- def pipeline_for(ref, sha = nil, id = nil)
- sha ||= commit(ref).try(:sha)
- return unless sha
-
- if id.present?
- pipelines_for(ref, sha).find_by(id: id)
- else
- pipelines_for(ref, sha).take
- end
- end
-
- def pipelines_for(ref, sha)
- ci_pipelines.order(id: :desc).where(sha: sha, ref: ref)
- end
-
def latest_successful_pipeline_for_default_branch
if defined?(@latest_successful_pipeline_for_default_branch)
return @latest_successful_pipeline_for_default_branch
@@ -1826,12 +1796,12 @@ class Project < ApplicationRecord
end
# rubocop: enable CodeReuse/ServiceClass
- def mark_pages_as_deployed
- ensure_pages_metadatum.update!(deployed: true)
+ def mark_pages_as_deployed(artifacts_archive: nil)
+ ensure_pages_metadatum.update!(deployed: true, artifacts_archive: artifacts_archive)
end
def mark_pages_as_not_deployed
- ensure_pages_metadatum.update!(deployed: false)
+ ensure_pages_metadatum.update!(deployed: false, artifacts_archive: nil)
end
def write_repository_config(gl_full_path: full_path)
@@ -2140,8 +2110,8 @@ class Project < ApplicationRecord
data = repository.route_map_for(sha)
Gitlab::RouteMap.new(data) if data
- rescue Gitlab::RouteMap::FormatError
- nil
+ rescue Gitlab::RouteMap::FormatError
+ nil
end
end
@@ -2424,6 +2394,10 @@ class Project < ApplicationRecord
false
end
+ def jira_subscription_exists?
+ JiraConnectSubscription.for_project(self).exists?
+ end
+
def uses_default_ci_config?
ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci]
end
@@ -2464,11 +2438,6 @@ class Project < ApplicationRecord
jira_imports.last
end
- override :after_wiki_activity
- def after_wiki_activity
- touch(:last_activity_at, :last_repository_updated_at)
- end
-
def metrics_setting
super || build_metrics_setting
end
@@ -2518,6 +2487,20 @@ class Project < ApplicationRecord
.exists?
end
+ def default_branch_or_master
+ default_branch || 'master'
+ end
+
+ def ci_config_path_or_default
+ ci_config_path.presence || Ci::Pipeline::DEFAULT_CONFIG_PATH
+ end
+
+ def enabled_group_deploy_keys
+ return GroupDeployKey.none unless group
+
+ GroupDeployKey.for_groups(group.self_and_ancestors_ids)
+ end
+
private
def find_service(services, name)
@@ -2533,11 +2516,11 @@ class Project < ApplicationRecord
end
def services_templates
- @services_templates ||= Service.templates
+ @services_templates ||= Service.for_template
end
def services_instances
- @services_instances ||= Service.instances
+ @services_instances ||= Service.for_instance
end
def closest_namespace_setting(name)
@@ -2678,9 +2661,11 @@ class Project < ApplicationRecord
end
def oids(objects, oids: [])
- collection = oids.any? ? objects.where(oid: oids) : objects
+ objects = objects.where(oid: oids) if oids.any?
- collection.pluck(:oid)
+ [].tap do |out|
+ objects.each_batch { |relation| out.concat(relation.pluck(:oid)) }
+ end
end
end
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
new file mode 100644
index 00000000000..b167c2e371b
--- /dev/null
+++ b/app/models/project_feature_usage.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class ProjectFeatureUsage < ApplicationRecord
+ self.primary_key = :project_id
+
+ JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at'.freeze
+ JIRA_DVCS_SERVER_FIELD = 'jira_dvcs_server_last_sync_at'.freeze
+
+ belongs_to :project
+ validates :project, presence: true
+
+ scope :with_jira_dvcs_integration_enabled, -> (cloud: true) do
+ where.not(jira_dvcs_integration_field(cloud: cloud) => nil)
+ end
+
+ class << self
+ def jira_dvcs_integration_field(cloud: true)
+ cloud ? JIRA_DVCS_CLOUD_FIELD : JIRA_DVCS_SERVER_FIELD
+ end
+ end
+
+ def log_jira_dvcs_integration_usage(cloud: true)
+ transaction(requires_new: true) do
+ save unless persisted?
+ touch(self.class.jira_dvcs_integration_field(cloud: cloud))
+ end
+ rescue ActiveRecord::RecordNotUnique
+ reset
+ retry
+ end
+end
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index 1fda388b1ae..8a1db4a9acf 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -4,6 +4,7 @@ class ProjectPagesMetadatum < ApplicationRecord
self.primary_key = :project_id
belongs_to :project, inverse_of: :pages_metadatum
+ belongs_to :artifacts_archive, class_name: 'Ci::JobArtifact'
scope :deployed, -> { where(deployed: true) }
end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index c4fcdff8386..b9916a54d75 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -5,6 +5,7 @@ module ChatMessage
attr_reader :merge_request_iid
attr_reader :source_branch
attr_reader :target_branch
+ attr_reader :action
attr_reader :state
attr_reader :title
@@ -16,6 +17,7 @@ module ChatMessage
@merge_request_iid = obj_attr[:iid]
@source_branch = obj_attr[:source_branch]
@target_branch = obj_attr[:target_branch]
+ @action = obj_attr[:action]
@state = obj_attr[:state]
@title = format_title(obj_attr[:title])
end
@@ -63,11 +65,17 @@ module ChatMessage
"#{project_url}/-/merge_requests/#{merge_request_iid}"
end
- # overridden in EE
def state_or_action_text
- state
+ case action
+ when 'approved', 'unapproved'
+ action
+ when 'approval'
+ 'added their approval to'
+ when 'unapproval'
+ 'removed their approval from'
+ else
+ state
+ end
end
end
end
-
-ChatMessage::MergeMessage.prepend_if_ee('::EE::ChatMessage::MergeMessage')
diff --git a/app/models/project_services/ewm_service.rb b/app/models/project_services/ewm_service.rb
new file mode 100644
index 00000000000..af402e50292
--- /dev/null
+++ b/app/models/project_services/ewm_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class EwmService < IssueTrackerService
+ validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
+
+ def self.reference_pattern(only_long: true)
+ @reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i
+ end
+
+ def title
+ 'EWM'
+ end
+
+ def description
+ s_('IssueTracker|EWM work items tracker')
+ end
+
+ def self.to_param
+ 'ewm'
+ end
+
+ def can_test?
+ false
+ end
+
+ def issue_url(iid)
+ issues_url.gsub(':id', iid.to_s.split(' ')[-1])
+ end
+end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 36d7026de30..732da62863f 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -5,6 +5,7 @@ class JiraService < IssueTrackerService
include Gitlab::Routing
include ApplicationHelper
include ActionView::Helpers::AssetUrlHelper
+ include Gitlab::Utils::StrongMemoize
PROJECTS_PER_PAGE = 50
@@ -32,6 +33,7 @@ class JiraService < IssueTrackerService
data_field :username, :password, :url, :api_url, :jira_issue_transition_id, :project_key, :issues_enabled
before_update :reset_password
+ after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
enum comment_detail: {
standard: 1,
@@ -212,7 +214,7 @@ class JiraService < IssueTrackerService
end
def test(_)
- result = test_settings
+ result = server_info
success = result.present?
result = @error&.message unless success
@@ -231,10 +233,10 @@ class JiraService < IssueTrackerService
private
- def test_settings
- return unless client_url.present?
-
- jira_request { client.ServerInfo.all.attrs }
+ def server_info
+ strong_memoize(:server_info) do
+ client_url.present? ? jira_request { client.ServerInfo.all.attrs } : nil
+ end
end
def can_cross_reference?(noteable)
@@ -436,6 +438,26 @@ class JiraService < IssueTrackerService
url_changed?
end
+ def update_deployment_type?
+ (api_url_changed? || url_changed? || username_changed? || password_changed?) &&
+ can_test?
+ end
+
+ def update_deployment_type
+ clear_memoization(:server_info) # ensure we run the request when we try to update deployment type
+ results = server_info
+ return data_fields.deployment_unknown! unless results.present?
+
+ case results['deploymentType']
+ when 'Server'
+ data_fields.deployment_server!
+ when 'Cloud'
+ data_fields.deployment_cloud!
+ else
+ data_fields.deployment_unknown!
+ end
+ end
+
def self.event_description(event)
case event
when "merge_request", "merge_request_events"
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 950cd4f6859..d0e62a1afba 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -97,13 +97,9 @@ class PrometheusService < MonitoringService
def prometheus_client
return unless should_return_client?
- options = {
- allow_local_requests: allow_local_api_url?,
- # We should choose more conservative timeouts, but some queries we run are now busting our
- # default timeouts, which are stricter. We should make those queries faster instead.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/233109
- timeout: 60
- }
+ options = prometheus_client_default_options.merge(
+ allow_local_requests: allow_local_api_url?
+ )
if behind_iap?
# Adds the Authorization header
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index f153bfe3f5b..67ab2c0ce8a 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -12,13 +12,17 @@ class ProjectStatistics < ApplicationRecord
before_save :update_storage_size
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze
- INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size], snippets_size: %i[storage_size] }.freeze
+ INCREMENTABLE_COLUMNS = {
+ build_artifacts_size: %i[storage_size],
+ packages_size: %i[storage_size],
+ pipeline_artifacts_size: %i[storage_size],
+ snippets_size: %i[storage_size]
+ }.freeze
NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size].freeze
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) }
- scope :with_any_ci_minutes_used, -> { where.not(shared_runners_seconds: 0) }
def total_repository_size
repository_size + lfs_objects_size
@@ -80,6 +84,10 @@ class ProjectStatistics < ApplicationRecord
# might try to update project statistics before the `snippets_size` column has been created.
storage_size += snippets_size if self.class.column_names.include?('snippets_size')
+ # The `pipeline_artifacts_size` column was added on 20200817142800 but db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
+ # might try to update project statistics before the `pipeline_artifacts_size` column has been created.
+ storage_size += pipeline_artifacts_size if self.class.column_names.include?('pipeline_artifacts_size')
+
self.storage_size = storage_size
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 072d281e5f8..5b7eded00cd 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -178,6 +178,40 @@ class ProjectTeam
max_member_access_for_user_ids([user_id])[user_id]
end
+ def contribution_check_for_user_ids(user_ids)
+ user_ids = user_ids.uniq
+ key = "contribution_check_for_users:#{project.id}"
+
+ Gitlab::SafeRequestStore[key] ||= {}
+ contributors = Gitlab::SafeRequestStore[key] || {}
+
+ user_ids -= contributors.keys
+
+ return contributors if user_ids.empty?
+
+ resource_contributors = project.merge_requests
+ .merged
+ .where(author_id: user_ids, target_branch: project.default_branch.to_s)
+ .pluck(:author_id)
+ .product([true]).to_h
+
+ contributors.merge!(resource_contributors)
+
+ missing_resource_ids = user_ids - resource_contributors.keys
+
+ missing_resource_ids.each do |resource_id|
+ contributors[resource_id] = false
+ end
+
+ contributors
+ end
+
+ def contributor?(user_id)
+ return false if max_member_access(user_id) >= Gitlab::Access::GUEST
+
+ contribution_check_for_user_ids([user_id])[user_id]
+ end
+
private
def fetch_members(level = nil)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 5df0a33dc9a..bd570cf7ead 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -10,6 +10,23 @@ class ProjectWiki < Wiki
def disk_path(*args, &block)
container.disk_path + '.wiki'
end
+
+ override :after_wiki_activity
+ def after_wiki_activity
+ # Update activity columns, this is done synchronously to avoid
+ # replication delays in Geo.
+ project.touch(:last_activity_at, :last_repository_updated_at)
+ end
+
+ override :after_post_receive
+ def after_post_receive
+ # Update storage statistics
+ ProjectCacheWorker.perform_async(project.id, [], [:wiki_size])
+
+ # This call is repeated for post-receive, to make sure we're updating
+ # the activity columns for Git pushes as well.
+ after_wiki_activity
+ end
end
# TODO: Remove this once we implement ES support for group wikis.
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
index bfd23d2a334..9ddf66cd388 100644
--- a/app/models/prometheus_metric.rb
+++ b/app/models/prometheus_metric.rb
@@ -4,7 +4,7 @@ class PrometheusMetric < ApplicationRecord
belongs_to :project, validate: true, inverse_of: :prometheus_metrics
has_many :prometheus_alerts, inverse_of: :prometheus_metric
- enum group: PrometheusMetricEnums.groups
+ enum group: Enums::PrometheusMetric.groups
validates :title, presence: true
validates :query, presence: true
@@ -16,11 +16,13 @@ class PrometheusMetric < ApplicationRecord
validates :project, presence: true, unless: :common?
validates :project, absence: true, if: :common?
+ scope :for_dashboard_path, -> (dashboard_path) { where(dashboard_path: dashboard_path) }
scope :for_project, -> (project) { where(project: project) }
scope :for_group, -> (group) { where(group: group) }
scope :for_title, -> (title) { where(title: title) }
scope :for_y_label, -> (y_label) { where(y_label: y_label) }
scope :for_identifier, -> (identifier) { where(identifier: identifier) }
+ scope :not_identifier, -> (identifier) { where.not(identifier: identifier) }
scope :common, -> { where(common: true) }
scope :ordered, -> { reorder(created_at: :asc) }
@@ -72,6 +74,6 @@ class PrometheusMetric < ApplicationRecord
private
def group_details(group)
- PrometheusMetricEnums.group_details.fetch(group.to_sym)
+ Enums::PrometheusMetric.group_details.fetch(group.to_sym)
end
end
diff --git a/app/models/prometheus_metric_enums.rb b/app/models/prometheus_metric_enums.rb
deleted file mode 100644
index 75a34618e2c..00000000000
--- a/app/models/prometheus_metric_enums.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module PrometheusMetricEnums
- def self.groups
- {
- # built-in groups
- nginx_ingress_vts: -1,
- ha_proxy: -2,
- aws_elb: -3,
- nginx: -4,
- kubernetes: -5,
- nginx_ingress: -6,
- cluster_health: -100
- }.merge(custom_groups).freeze
- end
-
- # custom/user groups
- def self.custom_groups
- {
- business: 0,
- response: 1,
- system: 2
- }.freeze
- end
-
- def self.group_details
- {
- # built-in groups
- nginx_ingress_vts: {
- group_title: _('Response metrics (NGINX Ingress VTS)'),
- required_metrics: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
- priority: 10
- }.freeze,
- nginx_ingress: {
- group_title: _('Response metrics (NGINX Ingress)'),
- required_metrics: %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum),
- priority: 10
- }.freeze,
- ha_proxy: {
- group_title: _('Response metrics (HA Proxy)'),
- required_metrics: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
- priority: 10
- }.freeze,
- aws_elb: {
- group_title: _('Response metrics (AWS ELB)'),
- required_metrics: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
- priority: 10
- }.freeze,
- nginx: {
- group_title: _('Response metrics (NGINX)'),
- required_metrics: %w(nginx_server_requests nginx_server_requestMsec),
- priority: 10
- }.freeze,
- kubernetes: {
- group_title: _('System metrics (Kubernetes)'),
- required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
- priority: 5
- }.freeze,
- cluster_health: {
- group_title: _('Cluster Health'),
- required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
- priority: 10
- }.freeze
- }.merge(custom_group_details).freeze
- end
-
- # custom/user groups
- def self.custom_group_details
- {
- business: {
- group_title: _('Business metrics (Custom)'),
- priority: 0
- }.freeze,
- response: {
- group_title: _('Response metrics (Custom)'),
- priority: -5
- }.freeze,
- system: {
- group_title: _('System metrics (Custom)'),
- priority: -10
- }.freeze
- }.freeze
- end
-end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 594c822c18f..599c174ddd7 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -38,9 +38,9 @@ class ProtectedBranch < ApplicationRecord
project.protected_branches
end
+ # overridden in EE
def self.branch_requires_code_owner_approval?(project, branch_name)
- # NOOP
- #
+ false
end
def self.by_name(query)
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 8b15d481c1b..6b8b34ce4d2 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -210,6 +210,10 @@ class RemoteMirror < ApplicationRecord
super(usernames_whitelist: %w[git])
end
+ def bare_url
+ Gitlab::UrlSanitizer.new(read_attribute(:url)).full_url
+ end
+
def ensure_remote!
return unless project
return unless remote_name && remote_url
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 07122db36b3..ef17e010ba8 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -214,7 +214,7 @@ class Repository
return false if with_slash.empty?
prefixes = no_slash.map { |ref| Regexp.escape(ref) }.join('|')
- prefix_regex = %r{^#{prefixes}/}
+ prefix_regex = %r{^(#{prefixes})/}
with_slash.any? do |ref|
prefix_regex.match?(ref)
diff --git a/app/models/resource_iteration_event.rb b/app/models/resource_iteration_event.rb
deleted file mode 100644
index 78d85ea8b95..00000000000
--- a/app/models/resource_iteration_event.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-class ResourceIterationEvent < ResourceTimeboxEvent
- belongs_to :iteration
-end
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
index 766b4d7a865..1ce4e14d289 100644
--- a/app/models/resource_state_event.rb
+++ b/app/models/resource_state_event.rb
@@ -19,3 +19,5 @@ class ResourceStateEvent < ResourceEvent
issue || merge_request
end
end
+
+ResourceStateEvent.prepend_if_ee('EE::ResourceStateEvent')
diff --git a/app/models/security_event.rb b/app/models/security_event.rb
deleted file mode 100644
index 3fe4cc99c9b..00000000000
--- a/app/models/security_event.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-class SecurityEvent < AuditEvent
-end
diff --git a/app/models/service.rb b/app/models/service.rb
index 40e7e5552d1..e63e06bf46f 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -9,12 +9,11 @@ class Service < ApplicationRecord
include DataFields
include IgnorableColumns
- ignore_columns %i[title description], remove_with: '13.4', remove_after: '2020-09-22'
ignore_columns %i[default], remove_with: '13.5', remove_after: '2020-10-22'
SERVICE_NAMES = %w[
alerts asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
- drone_ci emails_on_push external_wiki flowdock hangouts_chat hipchat irker jira
+ drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat hipchat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
].freeze
@@ -27,16 +26,17 @@ class Service < ApplicationRecord
default_value_for :active, false
default_value_for :alert_events, true
- default_value_for :push_events, true
- default_value_for :issues_events, true
- default_value_for :confidential_issues_events, true
+ default_value_for :category, 'common'
default_value_for :commit_events, true
- default_value_for :merge_requests_events, true
- default_value_for :tag_push_events, true
- default_value_for :note_events, true
+ default_value_for :confidential_issues_events, true
default_value_for :confidential_note_events, true
+ default_value_for :issues_events, true
default_value_for :job_events, true
+ default_value_for :merge_requests_events, true
+ default_value_for :note_events, true
default_value_for :pipeline_events, true
+ default_value_for :push_events, true
+ default_value_for :tag_push_events, true
default_value_for :wiki_page_events, true
after_initialize :initialize_properties
@@ -46,6 +46,7 @@ class Service < ApplicationRecord
after_commit :cache_project_has_external_wiki
belongs_to :project, inverse_of: :services
+ belongs_to :group, inverse_of: :services
has_one :service_hook
validates :project_id, presence: true, unless: -> { template? || instance? || group_id }
@@ -64,8 +65,9 @@ class Service < ApplicationRecord
scope :active, -> { where(active: true) }
scope :by_type, -> (type) { where(type: type) }
scope :by_active_flag, -> (flag) { where(active: flag) }
- scope :templates, -> { where(template: true, type: available_services_types) }
- scope :instances, -> { where(instance: true, type: available_services_types) }
+ scope :for_group, -> (group) { where(group_id: group, type: available_services_types) }
+ scope :for_template, -> { where(template: true, type: available_services_types) }
+ scope :for_instance, -> { where(instance: true, type: available_services_types) }
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
@@ -81,7 +83,178 @@ class Service < ApplicationRecord
scope :alert_hooks, -> { where(alert_events: true, active: true) }
scope :deployment, -> { where(category: 'deployment') }
- default_value_for :category, 'common'
+ # Provide convenient accessor methods for each serialized property.
+ # Also keep track of updated properties in a similar way as ActiveModel::Dirty
+ def self.prop_accessor(*args)
+ args.each do |arg|
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
+ unless method_defined?(arg)
+ def #{arg}
+ properties['#{arg}']
+ end
+ end
+
+ def #{arg}=(value)
+ self.properties ||= {}
+ updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
+ self.properties['#{arg}'] = value
+ end
+
+ def #{arg}_changed?
+ #{arg}_touched? && #{arg} != #{arg}_was
+ end
+
+ def #{arg}_touched?
+ updated_properties.include?('#{arg}')
+ end
+
+ def #{arg}_was
+ updated_properties['#{arg}']
+ end
+ RUBY
+ end
+ end
+
+ # Provide convenient boolean accessor methods for each serialized property.
+ # Also keep track of updated properties in a similar way as ActiveModel::Dirty
+ def self.boolean_accessor(*args)
+ self.prop_accessor(*args)
+
+ args.each do |arg|
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
+ def #{arg}?
+ # '!!' is used because nil or empty string is converted to nil
+ !!ActiveRecord::Type::Boolean.new.cast(#{arg})
+ end
+ RUBY
+ end
+ end
+
+ def self.to_param
+ raise NotImplementedError
+ end
+
+ def self.event_names
+ self.supported_events.map { |event| ServicesHelper.service_event_field_name(event) }
+ end
+
+ def self.supported_event_actions
+ %w[]
+ end
+
+ def self.supported_events
+ %w[commit push tag_push issue confidential_issue merge_request wiki_page]
+ end
+
+ def self.event_description(event)
+ ServicesHelper.service_event_description(event)
+ end
+
+ def self.find_or_create_templates
+ create_nonexistent_templates
+ for_template
+ end
+
+ def self.create_nonexistent_templates
+ nonexistent_services = list_nonexistent_services_for(for_template)
+ return if nonexistent_services.empty?
+
+ # Create within a transaction to perform the lowest possible SQL queries.
+ transaction do
+ nonexistent_services.each do |service_type|
+ service_type.constantize.create(template: true)
+ end
+ end
+ end
+ private_class_method :create_nonexistent_templates
+
+ def self.find_or_initialize_integration(name, instance: false, group_id: nil)
+ if name.in?(available_services_names)
+ "#{name}_service".camelize.constantize.find_or_initialize_by(instance: instance, group_id: group_id)
+ end
+ end
+
+ def self.find_or_initialize_all(scope)
+ scope + build_nonexistent_services_for(scope)
+ end
+
+ def self.build_nonexistent_services_for(scope)
+ list_nonexistent_services_for(scope).map do |service_type|
+ service_type.constantize.new
+ end
+ end
+ private_class_method :build_nonexistent_services_for
+
+ def self.list_nonexistent_services_for(scope)
+ # Using #map instead of #pluck to save one query count. This is because
+ # ActiveRecord loaded the object here, so we don't need to query again later.
+ available_services_types - scope.map(&:type)
+ end
+ private_class_method :list_nonexistent_services_for
+
+ def self.available_services_names
+ service_names = services_names
+ service_names += dev_services_names
+
+ service_names.sort_by(&:downcase)
+ end
+
+ def self.services_names
+ SERVICE_NAMES
+ end
+
+ def self.dev_services_names
+ return [] unless Rails.env.development?
+
+ DEV_SERVICE_NAMES
+ end
+
+ def self.available_services_types
+ available_services_names.map { |service_name| "#{service_name}_service".camelize }
+ end
+
+ def self.services_types
+ services_names.map { |service_name| "#{service_name}_service".camelize }
+ end
+
+ def self.build_from_integration(project_id, integration)
+ service = integration.dup
+
+ if integration.supports_data_fields?
+ data_fields = integration.data_fields.dup
+ data_fields.service = service
+ end
+
+ service.template = false
+ service.instance = false
+ service.inherit_from_id = integration.id if integration.instance?
+ service.project_id = project_id
+ service.active = false if service.invalid?
+ service
+ end
+
+ def self.instance_exists_for?(type)
+ exists?(instance: true, type: type)
+ end
+
+ def self.default_integration(type, scope)
+ closest_group_integration(type, scope) || instance_level_integration(type)
+ end
+
+ def self.closest_group_integration(type, scope)
+ group_ids = scope.ancestors.select(:id)
+ array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
+
+ where(type: type, group_id: group_ids)
+ .order(Arel.sql("array_position(#{array}::bigint[], services.group_id)"))
+ .first
+ end
+ private_class_method :closest_group_integration
+
+ def self.instance_level_integration(type)
+ find_by(type: type, instance: true)
+ end
+ private_class_method :instance_level_integration
def activated?
active
@@ -124,10 +297,6 @@ class Service < ApplicationRecord
self.class.to_param
end
- def self.to_param
- raise NotImplementedError
- end
-
def fields
# implement inside child
[]
@@ -137,11 +306,11 @@ class Service < ApplicationRecord
#
# This list is used in `Service#as_json(only: json_fields)`.
def json_fields
- %w(active)
+ %w[active]
end
def to_service_hash
- as_json(methods: :type, except: %w[id template instance project_id])
+ as_json(methods: :type, except: %w[id template instance project_id group_id])
end
def to_data_fields_hash
@@ -156,10 +325,6 @@ class Service < ApplicationRecord
self.class.event_names
end
- def self.event_names
- self.supported_events.map { |event| ServicesHelper.service_event_field_name(event) }
- end
-
def event_field(event)
nil
end
@@ -188,18 +353,10 @@ class Service < ApplicationRecord
self.class.supported_event_actions
end
- def self.supported_event_actions
- %w()
- end
-
def supported_events
self.class.supported_events
end
- def self.supported_events
- %w(commit push tag_push issue confidential_issue merge_request wiki_page)
- end
-
def execute(data)
# implement inside child
end
@@ -210,59 +367,10 @@ class Service < ApplicationRecord
{ success: result.present?, result: result }
end
- # Disable test for instance-level services.
+ # Disable test for instance-level and group-level services.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def can_test?
- !instance?
- end
-
- # Provide convenient accessor methods
- # for each serialized property.
- # Also keep track of updated properties in a similar way as ActiveModel::Dirty
- def self.prop_accessor(*args)
- args.each do |arg|
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
- unless method_defined?(arg)
- def #{arg}
- properties['#{arg}']
- end
- end
-
- def #{arg}=(value)
- self.properties ||= {}
- updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
- self.properties['#{arg}'] = value
- end
-
- def #{arg}_changed?
- #{arg}_touched? && #{arg} != #{arg}_was
- end
-
- def #{arg}_touched?
- updated_properties.include?('#{arg}')
- end
-
- def #{arg}_was
- updated_properties['#{arg}']
- end
- RUBY
- end
- end
-
- # Provide convenient boolean accessor methods
- # for each serialized property.
- # Also keep track of updated properties in a similar way as ActiveModel::Dirty
- def self.boolean_accessor(*args)
- self.prop_accessor(*args)
-
- args.each do |arg|
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
- def #{arg}?
- # '!!' is used because nil or empty string is converted to nil
- !!ActiveRecord::Type::Boolean.new.cast(#{arg})
- end
- RUBY
- end
+ !instance? && !group_id
end
# Returns a hash of the properties that have been assigned a new value since last save,
@@ -289,86 +397,6 @@ class Service < ApplicationRecord
self.category == :issue_tracker
end
- def self.find_or_create_templates
- create_nonexistent_templates
- templates
- end
-
- private_class_method def self.create_nonexistent_templates
- nonexistent_services = list_nonexistent_services_for(templates)
- return if nonexistent_services.empty?
-
- # Create within a transaction to perform the lowest possible SQL queries.
- transaction do
- nonexistent_services.each do |service_type|
- service_type.constantize.create(template: true)
- end
- end
- end
-
- def self.find_or_initialize_instances
- instances + build_nonexistent_instances
- end
-
- private_class_method def self.build_nonexistent_instances
- list_nonexistent_services_for(instances).map do |service_type|
- service_type.constantize.new
- end
- end
-
- private_class_method def self.list_nonexistent_services_for(scope)
- available_services_types - scope.map(&:type)
- end
-
- def self.available_services_names
- service_names = services_names
- service_names += dev_services_names
-
- service_names.sort_by(&:downcase)
- end
-
- def self.services_names
- SERVICE_NAMES
- end
-
- def self.dev_services_names
- return [] unless Rails.env.development?
-
- DEV_SERVICE_NAMES
- end
-
- def self.available_services_types
- available_services_names.map { |service_name| "#{service_name}_service".camelize }
- end
-
- def self.services_types
- services_names.map { |service_name| "#{service_name}_service".camelize }
- end
-
- def self.build_from_integration(project_id, integration)
- service = integration.dup
-
- if integration.supports_data_fields?
- data_fields = integration.data_fields.dup
- data_fields.service = service
- end
-
- service.template = false
- service.instance = false
- service.inherit_from_id = integration.id if integration.instance?
- service.project_id = project_id
- service.active = false if service.invalid?
- service
- end
-
- def self.instance_exists_for?(type)
- exists?(instance: true, type: type)
- end
-
- def self.instance_for(type)
- find_by(instance: true, type: type)
- end
-
# override if needed
def supports_data_fields?
false
@@ -396,10 +424,6 @@ class Service < ApplicationRecord
end
end
- def self.event_description(event)
- ServicesHelper.service_event_description(event)
- end
-
def valid_recipients?
activated? && !importing?
end
diff --git a/app/models/service_list.rb b/app/models/service_list.rb
index fa3760f0c56..9cbc5e68059 100644
--- a/app/models/service_list.rb
+++ b/app/models/service_list.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
class ServiceList
- def initialize(batch, service_hash, extra_hash = {})
- @batch = batch
+ def initialize(batch_ids, service_hash, association)
+ @batch_ids = batch_ids
@service_hash = service_hash
- @extra_hash = extra_hash
+ @association = association
end
def to_array
@@ -13,15 +13,15 @@ class ServiceList
private
- attr_reader :batch, :service_hash, :extra_hash
+ attr_reader :batch_ids, :service_hash, :association
def columns
- (service_hash.keys << 'project_id') + extra_hash.keys
+ (service_hash.keys << "#{association}_id")
end
def values
- batch.map do |project_id|
- (service_hash.values << project_id) + extra_hash.values
+ batch_ids.map do |id|
+ (service_hash.values << id)
end
end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index eb3960ff12b..1cf3097861c 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -214,7 +214,7 @@ class Snippet < ApplicationRecord
def blobs
return [] unless repository_exists?
- repository.ls_files(repository.root_ref).map { |file| Blob.lazy(repository, repository.root_ref, file) }
+ repository.ls_files(default_branch).map { |file| Blob.lazy(repository, default_branch, file) }
end
def hook_attrs
@@ -275,7 +275,7 @@ class Snippet < ApplicationRecord
override :repository
def repository
- @repository ||= Repository.new(full_path, self, shard: repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::SNIPPET)
+ @repository ||= Gitlab::GlRepository::SNIPPET.repository_for(self)
end
override :repository_size_checker
@@ -309,6 +309,11 @@ class Snippet < ApplicationRecord
end
end
+ override :default_branch
+ def default_branch
+ super || 'master'
+ end
+
def repository_storage
snippet_repository&.shard_name || self.class.pick_repository_storage
end
@@ -336,13 +341,17 @@ class Snippet < ApplicationRecord
def file_name_on_repo
return if repository.empty?
- list_files(repository.root_ref).first
+ list_files(default_branch).first
end
def list_files(ref = nil)
return [] if repository.empty?
- repository.ls_files(ref)
+ repository.ls_files(ref || default_branch)
+ end
+
+ def multiple_files?
+ list_files.size > 1
end
class << self
diff --git a/app/models/snippet_input_action.rb b/app/models/snippet_input_action.rb
index cc6373264cc..b5362b5c14e 100644
--- a/app/models/snippet_input_action.rb
+++ b/app/models/snippet_input_action.rb
@@ -15,7 +15,7 @@ class SnippetInputAction
validates :action, inclusion: { in: ACTIONS, message: "%{value} is not a valid action" }
validates :previous_path, presence: true, if: :move_action?
- validates :file_path, presence: true, unless: :create_action?
+ validates :file_path, presence: true, if: -> (action) { action.update_action? || action.delete_action? }
validates :content, presence: true, if: -> (action) { action.create_action? || action.update_action? }
validate :ensure_same_file_path_and_previous_path, if: :update_action?
validate :ensure_different_file_path_and_previous_path, if: :move_action?
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index 8151308125a..2cfb201191d 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -93,7 +93,7 @@ class SnippetRepository < ApplicationRecord
end
def get_last_empty_file_index
- repository.ls_files(nil).inject(0) do |max, file|
+ repository.ls_files(snippet.default_branch).inject(0) do |max, file|
idx = file[EMPTY_FILE_PATTERN, 1].to_i
[idx, max].max
end
@@ -131,3 +131,5 @@ class SnippetRepository < ApplicationRecord
action[:action] == :update && action[:content].nil?
end
end
+
+SnippetRepository.prepend_if_ee('EE::SnippetRepository')
diff --git a/app/models/snippet_statistics.rb b/app/models/snippet_statistics.rb
index 7439f98d114..8545296d076 100644
--- a/app/models/snippet_statistics.rb
+++ b/app/models/snippet_statistics.rb
@@ -25,7 +25,7 @@ class SnippetStatistics < ApplicationRecord
def update_file_count
count = if snippet.repository_exists?
- repository.ls_files(repository.root_ref).size
+ repository.ls_files(snippet.default_branch).size
else
0
end
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index b6ba96c768e..961212d0295 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -11,6 +11,7 @@ class SystemNoteMetadata < ApplicationRecord
close duplicate
moved merge
label milestone
+ relate unrelate
].freeze
ICON_TYPES = %w[
@@ -19,7 +20,7 @@ class SystemNoteMetadata < ApplicationRecord
title time_tracking branch milestone discussion task moved
opened closed merged duplicate locked unlocked outdated
tag due_date pinned_embed cherry_pick health_status approved unapproved
- status alert_issue_added
+ status alert_issue_added relate unrelate new_alert_added
].freeze
validates :note, presence: true
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index c50b9da1310..419fffcb666 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -5,27 +5,34 @@ module Terraform
include UsageStatistics
include FileStoreMounter
- DEFAULT = '{"version":1}'.freeze
HEX_REGEXP = %r{\A\h+\z}.freeze
UUID_LENGTH = 32
belongs_to :project
belongs_to :locked_by_user, class_name: 'User'
+ has_many :versions, class_name: 'Terraform::StateVersion', foreign_key: :terraform_state_id
+ has_one :latest_version, -> { ordered_by_version_desc }, class_name: 'Terraform::StateVersion', foreign_key: :terraform_state_id
+
+ scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
+
validates :project_id, presence: true
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
format: { with: HEX_REGEXP, message: 'only allows hex characters' }
default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
+ default_value_for(:versioning_enabled, true)
mount_file_store_uploader StateUploader
- default_value_for(:file) { CarrierWaveStringFile.new(DEFAULT) }
-
def file_store
super || StateUploader.default_store
end
+ def latest_file
+ versioning_enabled ? latest_version&.file : file
+ end
+
def local?
file_store == ObjectStorage::Store::LOCAL
end
@@ -33,6 +40,17 @@ module Terraform
def locked?
self.lock_xid.present?
end
+
+ def update_file!(data, version:)
+ if versioning_enabled?
+ new_version = versions.build(version: version)
+ new_version.assign_attributes(created_by_user: locked_by_user, file: data)
+ new_version.save!
+ else
+ self.file = data
+ save!
+ end
+ end
end
end
diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb
new file mode 100644
index 00000000000..d5e315d18a1
--- /dev/null
+++ b/app/models/terraform/state_version.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Terraform
+ class StateVersion < ApplicationRecord
+ include FileStoreMounter
+
+ belongs_to :terraform_state, class_name: 'Terraform::State', optional: false
+ belongs_to :created_by_user, class_name: 'User', optional: true
+
+ scope :ordered_by_version_desc, -> { order(version: :desc) }
+
+ default_value_for(:file_store) { VersionedStateUploader.default_store }
+
+ mount_file_store_uploader VersionedStateUploader
+
+ delegate :project_id, :uuid, to: :terraform_state, allow_nil: true
+ end
+end
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index c0aac6f27aa..60aaaaef831 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -7,6 +7,7 @@ class Timelog < ApplicationRecord
belongs_to :issue, touch: true
belongs_to :merge_request, touch: true
belongs_to :user
+ belongs_to :note
scope :for_issues_in_group, -> (group) do
joins(:issue).where(
diff --git a/app/models/todo.rb b/app/models/todo.rb
index f973c1ff1d4..6c8e085762d 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -17,9 +17,11 @@ class Todo < ApplicationRecord
UNMERGEABLE = 6
DIRECTLY_ADDRESSED = 7
MERGE_TRAIN_REMOVED = 8 # This is an EE-only feature
+ REVIEW_REQUESTED = 9
ACTION_NAMES = {
ASSIGNED => :assigned,
+ REVIEW_REQUESTED => :review_requested,
MENTIONED => :mentioned,
BUILD_FAILED => :build_failed,
MARKED => :marked,
@@ -167,6 +169,10 @@ class Todo < ApplicationRecord
action == ASSIGNED
end
+ def review_requested?
+ action == REVIEW_REQUESTED
+ end
+
def merge_train_removed?
action == MERGE_TRAIN_REMOVED
end
diff --git a/app/models/user.rb b/app/models/user.rb
index f31a6823657..0a784b30d8f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -58,6 +58,8 @@ class User < ApplicationRecord
devise :lockable, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable
+ include AdminChangedPasswordNotifier
+
# This module adds async behaviour to Devise emails
# and should be added after Devise modules are initialized.
include AsyncDeviseEmail
@@ -107,17 +109,18 @@ class User < ApplicationRecord
has_many :group_deploy_keys
has_many :gpg_keys
- has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :emails
has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :identities, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :webauthn_registrations
has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :user_synced_attributes_metadata, autosave: true
has_one :aws_role, class_name: 'Aws::Role'
# Groups
has_many :members
- has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
+ has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, source: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group
@@ -181,6 +184,7 @@ class User < ApplicationRecord
has_one :user_detail
has_one :user_highest_role
has_one :user_canonical_email
+ has_one :atlassian_identity, class_name: 'Atlassian::Identity'
has_many :reviews, foreign_key: :author_id, inverse_of: :author
@@ -275,6 +279,7 @@ class User < ApplicationRecord
:view_diffs_file_by_file, :view_diffs_file_by_file=,
:tab_width, :tab_width=,
:sourcegraph_enabled, :sourcegraph_enabled=,
+ :gitpod_enabled, :gitpod_enabled=,
:setup_for_company, :setup_for_company=,
:render_whitespace_in_code, :render_whitespace_in_code=,
:experience_level, :experience_level=,
@@ -283,6 +288,7 @@ class User < ApplicationRecord
delegate :path, to: :namespace, allow_nil: true, prefix: true
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
+ delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
@@ -431,14 +437,21 @@ class User < ApplicationRecord
FROM u2f_registrations AS u2f
WHERE u2f.user_id = users.id
) OR users.otp_required_for_login = ?
+ OR
+ EXISTS (
+ SELECT *
+ FROM webauthn_registrations AS webauthn
+ WHERE webauthn.user_id = users.id
+ )
SQL
where(with_u2f_registrations, true)
end
def self.without_two_factor
- joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
- .where("u2f.id IS NULL AND users.otp_required_for_login = ?", false)
+ joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id
+ LEFT OUTER JOIN webauthn_registrations AS webauthn ON webauthn.user_id = users.id")
+ .where("u2f.id IS NULL AND webauthn.id IS NULL AND users.otp_required_for_login = ?", false)
end
#
@@ -751,11 +764,12 @@ class User < ApplicationRecord
otp_backup_codes: nil
)
self.u2f_registrations.destroy_all # rubocop: disable Cop/DestroyAll
+ self.webauthn_registrations.destroy_all # rubocop: disable Cop/DestroyAll
end
end
def two_factor_enabled?
- two_factor_otp_enabled? || two_factor_u2f_enabled?
+ two_factor_otp_enabled? || two_factor_webauthn_u2f_enabled?
end
def two_factor_otp_enabled?
@@ -770,6 +784,16 @@ class User < ApplicationRecord
end
end
+ def two_factor_webauthn_u2f_enabled?
+ two_factor_u2f_enabled? || two_factor_webauthn_enabled?
+ end
+
+ def two_factor_webauthn_enabled?
+ return false unless Feature.enabled?(:webauthn)
+
+ (webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?)
+ end
+
def namespace_move_dir_allowed
if namespace&.any_project_has_container_registry_tags?
errors.add(:username, _('cannot be changed if a personal project has container registry tags.'))
@@ -1460,6 +1484,11 @@ class User < ApplicationRecord
end
end
+ def notification_settings_for_groups(groups)
+ ids = groups.is_a?(ActiveRecord::Relation) ? groups.select(:id) : groups.map(&:id)
+ notification_settings.for_groups.where(source_id: ids)
+ end
+
# Lazy load global notification setting
# Initializes User setting with Participating level if setting not persisted
def global_notification_setting
@@ -1687,9 +1716,6 @@ class User < ApplicationRecord
[last_activity, last_sign_in].compact.max
end
- # Below is used for the signup_flow experiment. Should be removed
- # when experiment finishes.
- # See https://gitlab.com/gitlab-org/growth/engineering/issues/64
REQUIRES_ROLE_VALUE = 99
def role_required?
@@ -1699,7 +1725,6 @@ class User < ApplicationRecord
def set_role_required!
update_column(:role, REQUIRES_ROLE_VALUE)
end
- # End of signup_flow experiment methods
def dismissed_callout?(feature_name:, ignore_dismissal_earlier_than: nil)
callouts = self.callouts.with_feature_name(feature_name)
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 82f82356cb4..0ba319aa444 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -3,9 +3,30 @@
class UserCallout < ApplicationRecord
belongs_to :user
- # We use `UserCalloutEnums.feature_names` here so that EE can more easily
- # extend this `Hash` with new values.
- enum feature_name: ::UserCalloutEnums.feature_names
+ enum feature_name: {
+ gke_cluster_integration: 1,
+ gcp_signup_offer: 2,
+ cluster_security_warning: 3,
+ gold_trial: 4, # EE-only
+ geo_enable_hashed_storage: 5, # EE-only
+ geo_migrate_hashed_storage: 6, # EE-only
+ canary_deployment: 7, # EE-only
+ gold_trial_billings: 8, # EE-only
+ suggest_popover_dismissed: 9,
+ tabs_position_highlight: 10,
+ threat_monitoring_info: 11, # EE-only
+ account_recovery_regular_check: 12, # EE-only
+ webhooks_moved: 13,
+ service_templates_deprecated: 14,
+ admin_integrations_moved: 15,
+ web_ide_alert_dismissed: 16,
+ active_user_count_threshold: 18, # EE-only
+ buy_pipeline_minutes_notification_dot: 19, # EE-only
+ personal_access_token_expiry: 21, # EE-only
+ suggest_pipeline: 22,
+ customize_homepage: 23,
+ feature_flags_new_version: 24
+ }
validates :user, presence: true
validates :feature_name,
diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb
deleted file mode 100644
index 5b64befd284..00000000000
--- a/app/models/user_callout_enums.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module UserCalloutEnums
- # Returns the `Hash` to use for the `feature_name` enum in the `UserCallout`
- # model.
- #
- # This method is separate from the `UserCallout` model so that it can be
- # extended by EE.
- #
- # If you are going to add new items to this hash, check that you're not going
- # to conflict with EE-only values: https://gitlab.com/gitlab-org/gitlab/blob/master/ee/app/models/ee/user_callout_enums.rb
- def self.feature_names
- {
- gke_cluster_integration: 1,
- gcp_signup_offer: 2,
- cluster_security_warning: 3,
- suggest_popover_dismissed: 9,
- tabs_position_highlight: 10,
- webhooks_moved: 13,
- admin_integrations_moved: 15,
- personal_access_token_expiry: 21, # EE-only
- suggest_pipeline: 22,
- customize_homepage: 23
- }
- end
-end
-
-UserCalloutEnums.prepend_if_ee('EE::UserCalloutEnums')
diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb
new file mode 100644
index 00000000000..71d0b1db410
--- /dev/null
+++ b/app/models/vulnerability.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# Placeholder class for model that is implemented in EE
+# It reserves '+' as a reference prefix, but the table does not exist in FOSS
+class Vulnerability < ApplicationRecord
+ include IgnorableColumns
+
+ def self.reference_prefix
+ '+'
+ end
+
+ def self.reference_prefix_escaped
+ '&plus;'
+ end
+end
+
+Vulnerability.prepend_if_ee('EE::Vulnerability')
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 30273d646cf..9462f7401c4 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -133,8 +133,9 @@ class Wiki
commit = commit_details(:created, message, title)
wiki.write_page(title, format.to_sym, content, commit)
+ after_wiki_activity
- update_container_activity
+ true
rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
false
@@ -144,16 +145,18 @@ class Wiki
commit = commit_details(:updated, message, page.title)
wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
+ after_wiki_activity
- update_container_activity
+ true
end
def delete_page(page, message = nil)
return unless page
wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
+ after_wiki_activity
- update_container_activity
+ true
end
def page_title_and_dir(title)
@@ -180,7 +183,7 @@ class Wiki
override :repository
def repository
- @repository ||= Repository.new(full_path, container, shard: repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::WIKI)
+ @repository ||= Gitlab::GlRepository::WIKI.repository_for(container)
end
def repository_storage
@@ -209,6 +212,17 @@ class Wiki
web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '')
end
+ # Callbacks for synchronous processing after wiki changes.
+ # These will be executed after any change made through GitLab itself (web UI and API),
+ # but not for Git pushes.
+ def after_wiki_activity
+ end
+
+ # Callbacks for background processing after wiki changes.
+ # These will be executed after any change to the wiki repository.
+ def after_post_receive
+ end
+
private
def commit_details(action, message = nil, title = nil)
@@ -225,10 +239,6 @@ class Wiki
def default_message(action, title)
"#{user.username} #{action} page: #{title}"
end
-
- def update_container_activity
- container.after_wiki_activity
- end
end
Wiki.prepend_if_ee('EE::Wiki')
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index cc66ad0577d..b3950c6a0e3 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -20,6 +20,11 @@ module Ci
end
end
+ # overridden in EE
+ condition(:protected_environment_access) do
+ false
+ end
+
condition(:owner_of_job) do
@subject.triggered_by?(@user)
end
@@ -40,7 +45,7 @@ module Ci
@subject.pipeline.webide?
end
- rule { protected_ref | archived }.policy do
+ rule { ~protected_environment_access & (protected_ref | archived) }.policy do
prevent :update_build
prevent :update_commit_status
prevent :erase_build
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index c66f0d199b0..de69636b078 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -15,14 +15,9 @@ class GlobalPolicy < BasePolicy
@user&.required_terms_not_accepted?
end
- condition(:private_instance_statistics, score: 0) { Gitlab::CurrentSettings.instance_statistics_visibility_private? }
-
condition(:project_bot, scope: :user) { @user&.project_bot? }
condition(:migration_bot, scope: :user) { @user&.migration_bot? }
- rule { admin | (~private_instance_statistics & ~anonymous) }
- .enable :read_instance_statistics
-
rule { anonymous }.policy do
prevent :log_in
prevent :receive_notifications
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 3cc1be9dfb7..c98e82efef7 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -80,6 +80,7 @@ class GroupPolicy < BasePolicy
enable :read_list
enable :read_label
enable :read_board
+ enable :read_group_member
end
rule { ~can?(:read_group) }.policy do
@@ -116,6 +117,7 @@ class GroupPolicy < BasePolicy
enable :update_cluster
enable :admin_cluster
enable :read_deploy_token
+ enable :create_jira_connect_subscription
end
rule { owner }.policy do
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 537319addc2..5cfbcfec5c0 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -24,5 +24,6 @@ class IssuablePolicy < BasePolicy
prevent :create_note
prevent :admin_note
prevent :resolve_note
+ prevent :award_emoji
end
end
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
index 350dd208499..aa87442cadd 100644
--- a/app/policies/namespace_policy.rb
+++ b/app/policies/namespace_policy.rb
@@ -12,6 +12,7 @@ class NamespacePolicy < BasePolicy
enable :admin_namespace
enable :read_namespace
enable :read_statistics
+ enable :create_jira_connect_subscription
end
rule { personal_project & ~can_create_personal_project }.prevent :create_projects
diff --git a/app/policies/operations/feature_flag_policy.rb b/app/policies/operations/feature_flag_policy.rb
new file mode 100644
index 00000000000..e2f4781d07c
--- /dev/null
+++ b/app/policies/operations/feature_flag_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Operations
+ class FeatureFlagPolicy < BasePolicy
+ delegate { @subject.project }
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index b2432bfa608..87ee7d201e4 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -102,11 +102,6 @@ class ProjectPolicy < BasePolicy
end
with_scope :subject
- condition(:moving_designs_disabled) do
- !::Feature.enabled?(:reorder_designs, @subject, default_enabled: true)
- end
-
- with_scope :subject
condition(:service_desk_enabled) { @subject.service_desk_enabled? }
# We aren't checking `:read_issue` or `:read_merge_request` in this case
@@ -330,6 +325,12 @@ class ProjectPolicy < BasePolicy
enable :destroy_design
enable :read_terraform_state
enable :read_pod_logs
+ enable :read_feature_flag
+ enable :create_feature_flag
+ enable :update_feature_flag
+ enable :destroy_feature_flag
+ enable :admin_feature_flag
+ enable :admin_feature_flags_user_lists
end
rule { can?(:developer_access) & user_confirmed? }.policy do
@@ -376,6 +377,7 @@ class ProjectPolicy < BasePolicy
enable :read_freeze_period
enable :update_freeze_period
enable :destroy_freeze_period
+ enable :admin_feature_flags_client
end
rule { public_project & metrics_dashboard_allowed }.policy do
@@ -452,6 +454,8 @@ class ProjectPolicy < BasePolicy
prevent :read_pipeline
prevent :read_pipeline_schedule
prevent(*create_read_update_admin_destroy(:release))
+ prevent(*create_read_update_admin_destroy(:feature_flag))
+ prevent(:admin_feature_flags_user_lists)
end
rule { container_registry_disabled }.policy do
@@ -557,10 +561,6 @@ class ProjectPolicy < BasePolicy
prevent :move_design
end
- rule { moving_designs_disabled }.policy do
- prevent :move_design
- end
-
rule { read_package_registry_deploy_token }.policy do
enable :read_package
enable :read_project
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 6ebafca9885..c9dfa98b285 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -25,6 +25,7 @@ class UserPolicy < BasePolicy
rule { default }.enable :read_user_profile
rule { (private_profile | blocked_user) & ~(user_is_self | admin) }.prevent :read_user_profile
+ rule { user_is_self | admin }.enable :disable_two_factor
end
UserPolicy.prepend_if_ee('EE::UserPolicy')
diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb
index 5bfa6dee18b..5debe6d5dbd 100644
--- a/app/presenters/alert_management/alert_presenter.rb
+++ b/app/presenters/alert_management/alert_presenter.rb
@@ -6,7 +6,10 @@ module AlertManagement
include IncidentManagement::Settings
include ActionView::Helpers::UrlHelper
- MARKDOWN_LINE_BREAK = " \n".freeze
+ MARKDOWN_LINE_BREAK = " \n"
+ HORIZONTAL_LINE = "\n\n---\n\n"
+
+ delegate :metrics_dashboard_url, :runbook, to: :parsed_payload
def initialize(alert, _attributes = {})
super
@@ -16,52 +19,37 @@ module AlertManagement
end
def issue_description
- horizontal_line = "\n\n---\n\n"
-
[
issue_summary_markdown,
alert_markdown,
incident_management_setting.issue_template_content
- ].compact.join(horizontal_line)
+ ].compact.join(HORIZONTAL_LINE)
end
def start_time
started_at&.strftime('%d %B %Y, %-l:%M%p (%Z)')
end
- def issue_summary_markdown
- <<~MARKDOWN.chomp
- #### Summary
-
- #{metadata_list}
- #{alert_details}#{metric_embed_for_alert}
- MARKDOWN
- end
-
- def runbook
- strong_memoize(:runbook) do
- payload&.dig('runbook')
- end
- end
-
- def metrics_dashboard_url; end
-
def details_url
details_project_alert_management_url(project, alert.iid)
end
+ def details
+ Gitlab::Utils::InlineHash.merge_keys(payload)
+ end
+
private
attr_reader :alert, :project
+ delegate :alert_markdown, :full_query, to: :parsed_payload
- def alerting_alert
- strong_memoize(:alerting_alert) do
- Gitlab::Alerting::Alert.new(project: project, payload: alert.payload).present
- end
+ def issue_summary_markdown
+ <<~MARKDOWN.chomp
+ #{metadata_list}
+ #{metric_embed_for_alert}
+ MARKDOWN
end
- def alert_markdown; end
-
def metadata_list
metadata = []
@@ -77,27 +65,10 @@ module AlertManagement
metadata.join(MARKDOWN_LINE_BREAK)
end
- def alert_details
- if details.present?
- <<~MARKDOWN.chomp
-
- #### Alert Details
-
- #{details_list}
- MARKDOWN
- end
- end
-
- def details_list
- alert.details
- .map { |label, value| list_item(label, value) }
- .join(MARKDOWN_LINE_BREAK)
+ def metric_embed_for_alert
+ "\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
end
- def metric_embed_for_alert; end
-
- def full_query; end
-
def list_item(key, value)
"**#{key}:** #{value}".strip
end
diff --git a/app/presenters/alert_management/prometheus_alert_presenter.rb b/app/presenters/alert_management/prometheus_alert_presenter.rb
deleted file mode 100644
index 6b8c8183f08..00000000000
--- a/app/presenters/alert_management/prometheus_alert_presenter.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module AlertManagement
- class PrometheusAlertPresenter < AlertManagement::AlertPresenter
- def runbook
- strong_memoize(:runbook) do
- payload&.dig('annotations', 'runbook')
- end
- end
-
- def metrics_dashboard_url
- alerting_alert.metrics_dashboard_url
- end
-
- private
-
- def alert_markdown
- alerting_alert.alert_markdown
- end
-
- def details_list
- alerting_alert.annotation_list
- end
-
- def metric_embed_for_alert
- alerting_alert.metric_embed_for_alert
- end
-
- def full_query
- alerting_alert.full_query
- end
- end
-end
diff --git a/app/presenters/ci/pipeline_artifacts/code_coverage_presenter.rb b/app/presenters/ci/pipeline_artifacts/code_coverage_presenter.rb
new file mode 100644
index 00000000000..098e839132c
--- /dev/null
+++ b/app/presenters/ci/pipeline_artifacts/code_coverage_presenter.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ci
+ module PipelineArtifacts
+ class CodeCoveragePresenter < ProcessablePresenter
+ include Gitlab::Utils::StrongMemoize
+
+ def for_files(filenames)
+ coverage_files = raw_report["files"].select { |key| filenames.include?(key) }
+
+ { files: coverage_files }
+ end
+
+ private
+
+ def raw_report
+ strong_memoize(:raw_report) do
+ self.each_blob do |blob|
+ Gitlab::Json.parse(blob)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 25693af4881..541a6363edd 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -79,7 +79,7 @@ module Clusters
{
'clusters-path': clusterable.index_path,
'dashboard-endpoint': clusterable.metrics_dashboard_path(cluster),
- 'documentation-path': help_page_path('user/project/clusters/index', anchor: 'monitoring-your-kubernetes-cluster-ultimate'),
+ 'documentation-path': help_page_path('user/project/clusters/index', anchor: 'monitoring-your-kubernetes-cluster'),
'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
'empty-getting-started-svg-path': image_path('illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path': image_path('illustrations/monitoring/loading.svg'),
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index eaa7cf848cd..714dd232efb 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -20,7 +20,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline',
bridge_pipeline_is_child_pipeline: 'This job belongs to a child pipeline and cannot create further child pipelines',
downstream_pipeline_creation_failed: 'The downstream pipeline could not be created',
- secrets_provider_not_found: 'The secrets provider can not be found'
+ secrets_provider_not_found: 'The secrets provider can not be found',
+ reached_max_descendant_pipelines_depth: 'Maximum child pipeline depth has been reached'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
diff --git a/app/presenters/dev_ops_score/metric_presenter.rb b/app/presenters/dev_ops_score/metric_presenter.rb
index d22beefee54..e7363293435 100644
--- a/app/presenters/dev_ops_score/metric_presenter.rb
+++ b/app/presenters/dev_ops_score/metric_presenter.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module DevOpsScore
+module DevOpsReport
class MetricPresenter < Gitlab::View::Presenter::Simple
def cards
[
diff --git a/app/presenters/packages/conan/package_presenter.rb b/app/presenters/packages/conan/package_presenter.rb
index 5141c450412..df770777ad1 100644
--- a/app/presenters/packages/conan/package_presenter.rb
+++ b/app/presenters/packages/conan/package_presenter.rb
@@ -3,13 +3,14 @@
module Packages
module Conan
class PackagePresenter
+ include API::Helpers::Packages::Conan::ApiHelpers
include API::Helpers::RelatedResourcesHelpers
include Gitlab::Utils::StrongMemoize
attr_reader :params
- def initialize(recipe, user, project, params = {})
- @recipe = recipe
+ def initialize(package, user, project, params = {})
+ @package = package
@user = user
@project = project
@params = params
@@ -17,7 +18,10 @@ module Packages
def recipe_urls
map_package_files do |package_file|
- build_recipe_file_url(package_file) if package_file.conan_file_metadatum.recipe_file?
+ next unless package_file.conan_file_metadatum.recipe_file?
+
+ options = url_options(package_file)
+ recipe_file_url(options)
end
end
@@ -31,7 +35,12 @@ module Packages
map_package_files do |package_file|
next unless package_file.conan_file_metadatum.package_file? && matching_reference?(package_file)
- build_package_file_url(package_file)
+ options = url_options(package_file).merge(
+ conan_package_reference: package_file.conan_file_metadatum.conan_package_reference,
+ package_revision: package_file.conan_file_metadatum.package_revision
+ )
+
+ package_file_url(options)
end
end
@@ -45,36 +54,21 @@ module Packages
private
- def build_recipe_file_url(package_file)
- expose_url(
- api_v4_packages_conan_v1_files_export_path(
- package_name: package.name,
- package_version: package.version,
- package_username: package.conan_metadatum.package_username,
- package_channel: package.conan_metadatum.package_channel,
- recipe_revision: package_file.conan_file_metadatum.recipe_revision,
- file_name: package_file.file_name
- )
- )
- end
-
- def build_package_file_url(package_file)
- expose_url(
- api_v4_packages_conan_v1_files_package_path(
- package_name: package.name,
- package_version: package.version,
- package_username: package.conan_metadatum.package_username,
- package_channel: package.conan_metadatum.package_channel,
- recipe_revision: package_file.conan_file_metadatum.recipe_revision,
- conan_package_reference: package_file.conan_file_metadatum.conan_package_reference,
- package_revision: package_file.conan_file_metadatum.package_revision,
- file_name: package_file.file_name
- )
- )
+ def url_options(package_file)
+ {
+ package_name: @package.name,
+ package_version: @package.version,
+ package_username: @package.conan_metadatum.package_username,
+ package_channel: @package.conan_metadatum.package_channel,
+ file_name: package_file.file_name,
+ recipe_revision: package_file.conan_file_metadatum.recipe_revision.presence || ::Packages::Conan::FileMetadatum::DEFAULT_RECIPE_REVISION
+ }
end
def map_package_files
package_files.to_a.map do |package_file|
+ next unless package_file.conan_file_metadatum
+
key = package_file.file_name
value = yield(package_file)
next unless key && value
@@ -84,22 +78,9 @@ module Packages
end
def package_files
- return unless package
+ return unless @package
- @package_files ||= package.package_files.preload_conan_file_metadata
- end
-
- def package
- strong_memoize(:package) do
- name, version = @recipe.split('@')[0].split('/')
-
- @project.packages
- .conan
- .with_name(name)
- .with_version(version)
- .order_created
- .last
- end
+ @package_files ||= @package.package_files.preload_conan_file_metadata
end
def matching_reference?(package_file)
diff --git a/app/presenters/packages/nuget/search_results_presenter.rb b/app/presenters/packages/nuget/search_results_presenter.rb
index 96c8fe7dd2a..dc391c380f3 100644
--- a/app/presenters/packages/nuget/search_results_presenter.rb
+++ b/app/presenters/packages/nuget/search_results_presenter.rb
@@ -49,7 +49,7 @@ module Packages
def latest_version(packages)
versions = packages.map(&:version).compact
- VersionSorter.sort(versions).last # rubocop: disable Style/UnneededSort
+ VersionSorter.sort(versions).last # rubocop: disable Style/RedundantSort
end
end
end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 86fd405812e..ef75c160b2d 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -7,6 +7,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include StorageHelper
include TreeHelper
include IconsHelper
+ include BlobHelper
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
@@ -114,7 +115,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_ci_yml_path
- add_special_file_path(file_name: '.gitlab-ci.yml')
+ add_special_file_path(file_name: ci_config_path_or_default)
+ end
+
+ def add_ci_yml_ide_path
+ ide_edit_path(project, default_branch_or_master, ci_config_path_or_default)
end
def add_readme_path
@@ -219,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can_current_user_push_to_default_branch?
AnchorData.new(false,
statistic_icon + _('New file'),
- project_new_blob_path(project, default_branch || 'master'),
+ project_new_blob_path(project, default_branch_or_master),
'missing')
end
end
@@ -325,7 +330,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if cicd_missing?
AnchorData.new(false,
statistic_icon + _('Set up CI/CD'),
- add_ci_yml_path)
+ add_ci_yml_ide_path)
elsif repository.gitlab_ci_yml.present?
AnchorData.new(false,
statistic_icon('doc-text') + _('CI/CD configuration'),
@@ -397,7 +402,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path(
project,
- project.default_branch || 'master',
+ default_branch_or_master,
file_name: file_name,
commit_message: commit_message,
branch_name: branch_name
diff --git a/app/presenters/projects/prometheus/alert_presenter.rb b/app/presenters/projects/prometheus/alert_presenter.rb
index 49859f27edd..783b2b2b1e0 100644
--- a/app/presenters/projects/prometheus/alert_presenter.rb
+++ b/app/presenters/projects/prometheus/alert_presenter.rb
@@ -3,7 +3,6 @@
module Projects
module Prometheus
class AlertPresenter < Gitlab::View::Presenter::Delegated
- RESERVED_ANNOTATIONS = %w(gitlab_incident_markdown gitlab_y_label title).freeze
GENERIC_ALERT_SUMMARY_ANNOTATIONS = %w(monitoring_tool service hosts).freeze
MARKDOWN_LINE_BREAK = " \n".freeze
INCIDENT_LABEL_NAME = ::IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title].freeze
@@ -51,22 +50,11 @@ module Projects
def issue_summary_markdown
<<~MARKDOWN.chomp
- #### Summary
-
#{metadata_list}
- #{alert_details}#{metric_embed_for_alert}
+ #{metric_embed_for_alert}
MARKDOWN
end
- def annotation_list
- strong_memoize(:annotation_list) do
- annotations
- .reject { |annotation| annotation.label.in?(RESERVED_ANNOTATIONS | GENERIC_ALERT_SUMMARY_ANNOTATIONS) }
- .map { |annotation| list_item(annotation.label, annotation.value) }
- .join(MARKDOWN_LINE_BREAK)
- end
- end
-
def metric_embed_for_alert
"\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
end
@@ -111,15 +99,8 @@ module Projects
metadata.join(MARKDOWN_LINE_BREAK)
end
- def alert_details
- if annotation_list.present?
- <<~MARKDOWN.chomp
-
- #### Alert Details
-
- #{annotation_list}
- MARKDOWN
- end
+ def details
+ Gitlab::Utils::InlineHash.merge_keys(payload)
end
def list_item(key, value)
diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb
index 4744a7c1cc8..90bd4730f1e 100644
--- a/app/serializers/base_serializer.rb
+++ b/app/serializers/base_serializer.rb
@@ -9,7 +9,7 @@ class BaseSerializer
end
def represent(resource, opts = {}, entity_class = nil)
- entity_class = entity_class || self.class.entity_class
+ entity_class ||= self.class.entity_class
entity_class
.represent(resource, opts.merge(request: @request))
diff --git a/app/serializers/build_coverage_entity.rb b/app/serializers/build_coverage_entity.rb
new file mode 100644
index 00000000000..47e0c30ba1e
--- /dev/null
+++ b/app/serializers/build_coverage_entity.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class BuildCoverageEntity < Grape::Entity
+ expose :name, :coverage
+end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 523f1a0f8c6..2b8522539b4 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -27,15 +27,15 @@ class BuildDetailsEntity < JobEntity
end
expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do
- expose :download_path, if: -> (*) { build.pipeline.artifacts_locked? || build.artifacts? } do |build|
+ expose :download_path, if: -> (*) { build.locked_artifacts? || build.artifacts? } do |build|
download_project_job_artifacts_path(project, build)
end
- expose :browse_path, if: -> (*) { build.pipeline.artifacts_locked? || build.browsable_artifacts? } do |build|
+ expose :browse_path, if: -> (*) { build.locked_artifacts? || build.browsable_artifacts? } do |build|
browse_project_job_artifacts_path(project, build)
end
- expose :keep_path, if: -> (*) { build.has_expiring_archive_artifacts? && can?(current_user, :update_build, build) } do |build|
+ expose :keep_path, if: -> (*) { (build.locked_artifacts? || build.has_expiring_archive_artifacts?) && can?(current_user, :update_build, build) } do |build|
keep_project_job_artifacts_path(project, build)
end
diff --git a/app/serializers/ci/lint/job_entity.rb b/app/serializers/ci/lint/job_entity.rb
new file mode 100644
index 00000000000..2393f9ba44c
--- /dev/null
+++ b/app/serializers/ci/lint/job_entity.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Ci::Lint::JobEntity < Grape::Entity
+ expose :name
+ expose :stage
+ expose :before_script
+ expose :script
+ expose :after_script
+ expose :tag_list
+ expose :environment
+ expose :when
+ expose :allow_failure
+ expose :only
+ expose :except
+end
diff --git a/app/serializers/ci/lint/result_entity.rb b/app/serializers/ci/lint/result_entity.rb
new file mode 100644
index 00000000000..f9306d1dc70
--- /dev/null
+++ b/app/serializers/ci/lint/result_entity.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class Ci::Lint::ResultEntity < Grape::Entity
+ expose :valid?, as: :valid
+ expose :errors
+ expose :warnings
+ expose :jobs, using: Ci::Lint::JobEntity do |result, options|
+ next [] unless result.valid?
+
+ result.jobs
+ end
+end
diff --git a/app/serializers/ci/lint/result_serializer.rb b/app/serializers/ci/lint/result_serializer.rb
new file mode 100644
index 00000000000..4b55b415129
--- /dev/null
+++ b/app/serializers/ci/lint/result_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class Ci::Lint::ResultSerializer < BaseSerializer
+ entity ::Ci::Lint::ResultEntity
+end
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
index 06e14179238..eea0acdc11b 100644
--- a/app/serializers/cluster_entity.rb
+++ b/app/serializers/cluster_entity.rb
@@ -24,4 +24,8 @@ class ClusterEntity < Grape::Entity
expose :kubernetes_errors do |cluster|
ClusterErrorEntity.new(cluster)
end
+
+ expose :enable_advanced_logs_querying do |cluster|
+ cluster.application_elastic_stack_available?
+ end
end
diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb
index a70458d2bcb..700a46040e3 100644
--- a/app/serializers/cluster_serializer.rb
+++ b/app/serializers/cluster_serializer.rb
@@ -11,6 +11,7 @@ class ClusterSerializer < BaseSerializer
:enabled,
:environment_scope,
:gitlab_managed_apps_logs_path,
+ :enable_advanced_logs_querying,
:kubernetes_errors,
:name,
:nodes,
diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb
index 2af14f1eb82..9f27191c3c8 100644
--- a/app/serializers/diff_file_base_entity.rb
+++ b/app/serializers/diff_file_base_entity.rb
@@ -12,11 +12,23 @@ class DiffFileBaseEntity < Grape::Entity
expose :submodule?, as: :submodule
expose :submodule_link do |diff_file, options|
- memoized_submodule_links(diff_file, options).first
+ memoized_submodule_links(diff_file, options)&.web
end
expose :submodule_tree_url do |diff_file|
- memoized_submodule_links(diff_file, options).last
+ memoized_submodule_links(diff_file, options)&.tree
+ end
+
+ expose :submodule_compare do |diff_file|
+ url = memoized_submodule_links(diff_file, options)&.compare
+
+ next unless url
+
+ {
+ url: url,
+ old_sha: diff_file.old_blob&.id,
+ new_sha: diff_file.blob&.id
+ }
end
expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
@@ -96,11 +108,9 @@ class DiffFileBaseEntity < Grape::Entity
def memoized_submodule_links(diff_file, options)
strong_memoize(:submodule_links) do
- if diff_file.submodule?
- options[:submodule_links].for(diff_file.blob, diff_file.content_sha)
- else
- []
- end
+ next unless diff_file.submodule?
+
+ options[:submodule_links].for(diff_file.blob, diff_file.content_sha, diff_file)
end
end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index 26a42d2e9eb..e3fefbb46b6 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -71,15 +71,11 @@ class DiffFileEntity < DiffFileBaseEntity
private
def parallel_diff_view?(options, diff_file)
- return true unless Feature.enabled?(:single_mr_diff_view, diff_file.repository.project, default_enabled: true)
-
# If we're not rendering inline, we must be rendering parallel
!inline_diff_view?(options, diff_file)
end
def inline_diff_view?(options, diff_file)
- return true unless Feature.enabled?(:single_mr_diff_view, diff_file.repository.project, default_enabled: true)
-
# If nothing is present, inline will be the default.
options.fetch(:diff_view, :inline).to_sym == :inline
end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index a2bf9716f8f..7da5910a75b 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -71,8 +71,6 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :destroy_environment, environment)
end
- expose :has_opened_alert?, if: -> (*) { can_read_alert_management_alert? }, expose_nil: false, as: :has_opened_alert
-
private
alias_method :environment, :object
@@ -93,10 +91,6 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :read_pod_logs, environment.project)
end
- def can_read_alert_management_alert?
- can?(current_user, :read_alert_management_alert, environment.project)
- end
-
def cluster_platform_kubernetes?
deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
end
diff --git a/app/serializers/fork_namespace_entity.rb b/app/serializers/fork_namespace_entity.rb
index 068862e0951..abfaf4be811 100644
--- a/app/serializers/fork_namespace_entity.rb
+++ b/app/serializers/fork_namespace_entity.rb
@@ -19,7 +19,7 @@ class ForkNamespaceEntity < Grape::Entity
end
expose :permission do |namespace, options|
- membership(options[:current_user], namespace)&.human_access
+ membership(options[:current_user], namespace, options[:memberships])&.human_access
end
expose :relative_path do |namespace|
@@ -37,10 +37,10 @@ class ForkNamespaceEntity < Grape::Entity
private
# rubocop: disable CodeReuse/ActiveRecord
- def membership(user, object)
+ def membership(user, object, memberships)
return unless user
- @membership ||= user.members.find_by(source: object)
+ memberships[object.id]
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/serializers/group_group_link_entity.rb b/app/serializers/group_group_link_entity.rb
new file mode 100644
index 00000000000..7a51e1a9316
--- /dev/null
+++ b/app/serializers/group_group_link_entity.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class GroupGroupLinkEntity < Grape::Entity
+ expose :id
+ expose :created_at
+ expose :expires_at do |group_link|
+ group_link.expires_at&.to_time
+ end
+
+ expose :access_level do
+ expose :human_access, as: :string_value
+ expose :group_access, as: :integer_value
+ end
+
+ expose :shared_with_group do
+ expose :avatar_url do |group_link|
+ group_link.shared_with_group.avatar_url(only_path: false)
+ end
+
+ expose :web_url do |group_link|
+ group_link.shared_with_group.web_url
+ end
+
+ expose :shared_with_group, merge: true, using: GroupBasicEntity
+ end
+end
diff --git a/app/serializers/group_group_link_serializer.rb b/app/serializers/group_group_link_serializer.rb
new file mode 100644
index 00000000000..6ae8daf9207
--- /dev/null
+++ b/app/serializers/group_group_link_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class GroupGroupLinkSerializer < BaseSerializer
+ entity GroupGroupLinkEntity
+end
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
index bbec107544e..53c123c06fd 100644
--- a/app/serializers/issuable_sidebar_basic_entity.rb
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -103,6 +103,10 @@ class IssuableSidebarBasicEntity < Grape::Entity
issuable.project.emails_disabled?
end
+ expose :supports_time_tracking?, as: :supports_time_tracking
+ expose :supports_milestone?, as: :supports_milestone
+ expose :supports_severity?, as: :supports_severity
+
private
def current_user
diff --git a/app/serializers/issue_sidebar_basic_entity.rb b/app/serializers/issue_sidebar_basic_entity.rb
index 165dc462cfe..e9e05718af9 100644
--- a/app/serializers/issue_sidebar_basic_entity.rb
+++ b/app/serializers/issue_sidebar_basic_entity.rb
@@ -3,6 +3,7 @@
class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
expose :due_date
expose :confidential
+ expose :severity
end
IssueSidebarBasicEntity.prepend_if_ee('EE::IssueSidebarBasicEntity')
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index d0099ae77f2..d05b500b140 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -9,7 +9,8 @@ class JobEntity < Grape::Entity
expose :started?, as: :started
expose :archived?, as: :archived
- expose :build_path do |build|
+ # bridge jobs don't have build detail pages
+ expose :build_path, if: ->(build) { !build.is_a?(Ci::Bridge) } do |build|
build_path(build)
end
diff --git a/app/serializers/linked_issue_entity.rb b/app/serializers/linked_issue_entity.rb
new file mode 100644
index 00000000000..6ae3f4044db
--- /dev/null
+++ b/app/serializers/linked_issue_entity.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class LinkedIssueEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id, :confidential, :title
+
+ expose :assignees, using: UserEntity
+
+ expose :state
+
+ expose :milestone, using: API::Entities::Milestone
+
+ expose :weight
+
+ expose :reference do |link|
+ link.to_reference(issuable.project)
+ end
+
+ expose :path do |link|
+ project_issue_path(link.project, link.iid)
+ end
+
+ expose :relation_path
+
+ expose :due_date, :created_at, :closed_at
+
+ private
+
+ def current_user
+ request.current_user
+ end
+
+ def issuable
+ request.issuable
+ end
+end
diff --git a/app/serializers/linked_project_issue_entity.rb b/app/serializers/linked_project_issue_entity.rb
new file mode 100644
index 00000000000..c95f68f58a3
--- /dev/null
+++ b/app/serializers/linked_project_issue_entity.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class LinkedProjectIssueEntity < LinkedIssueEntity
+ include Gitlab::Utils::StrongMemoize
+
+ expose :relation_path, override: true do |issue|
+ # Make sure the user can admin both the current issue AND the
+ # referenced issue projects in order to return the removal link.
+ if can_admin_issue_link_on_current_project? && can_admin_issue_link?(issue.project)
+ project_issue_link_path(issuable.project, issuable.iid, issue.issue_link_id)
+ end
+ end
+
+ expose :link_type do |issue|
+ issue.issue_link_type
+ end
+
+ private
+
+ def can_admin_issue_link_on_current_project?
+ strong_memoize(:can_admin_on_current_project) do
+ can_admin_issue_link?(issuable.project)
+ end
+ end
+
+ def can_admin_issue_link?(project)
+ Ability.allowed?(current_user, :admin_issue_link, project)
+ end
+end
diff --git a/app/serializers/linked_project_issue_serializer.rb b/app/serializers/linked_project_issue_serializer.rb
new file mode 100644
index 00000000000..3015e593e34
--- /dev/null
+++ b/app/serializers/linked_project_issue_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class LinkedProjectIssueSerializer < BaseSerializer
+ entity LinkedProjectIssueEntity
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index 82baf4a4a78..ef1177e9967 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -9,6 +9,7 @@ class MergeRequestBasicEntity < Grape::Entity
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
expose :assignees, using: API::Entities::UserBasic
+ expose :reviewers, if: -> (m) { m.allows_reviewers? }, using: API::Entities::UserBasic
expose :task_status, :task_status_short
expose :lock_version, :lock_version
end
diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb
index c51c08ab646..002be8be729 100644
--- a/app/serializers/merge_request_poll_cached_widget_entity.rb
+++ b/app/serializers/merge_request_poll_cached_widget_entity.rb
@@ -3,8 +3,8 @@
class MergeRequestPollCachedWidgetEntity < IssuableEntity
expose :auto_merge_enabled
expose :state
- expose :merge_commit_sha
- expose :short_merge_commit_sha
+ expose :merged_commit_sha
+ expose :short_merged_commit_sha
expose :merge_error
expose :public_merge_status, as: :merge_status
expose :merge_user_id
@@ -56,9 +56,9 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
presenter(merge_request).target_branch_tree_path
end
- expose :merge_commit_path do |merge_request|
- if merge_request.merge_commit_sha
- project_commit_path(merge_request.project, merge_request.merge_commit_sha)
+ expose :merged_commit_path do |merge_request|
+ if sha = merge_request.merged_commit_sha
+ project_commit_path(merge_request.project, sha)
end
end
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 99d6211b487..41ab5005091 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -73,6 +73,8 @@ class MergeRequestPollWidgetEntity < Grape::Entity
presenter(merge_request).pipeline_coverage_delta
end
+ expose :head_pipeline_builds_with_coverage, as: :builds_with_coverage, using: BuildCoverageEntity
+
expose :cancel_auto_merge_path do |merge_request|
presenter(merge_request).cancel_auto_merge_path
end
diff --git a/app/serializers/merge_request_reviewer_entity.rb b/app/serializers/merge_request_reviewer_entity.rb
new file mode 100644
index 00000000000..fefd116014f
--- /dev/null
+++ b/app/serializers/merge_request_reviewer_entity.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class MergeRequestReviewerEntity < ::API::Entities::UserBasic
+ expose :can_merge do |reviewer, options|
+ options[:merge_request]&.can_be_merged_by?(reviewer)
+ end
+end
+
+MergeRequestReviewerEntity.prepend_if_ee('EE::MergeRequestReviewerEntity')
diff --git a/app/serializers/merge_request_sidebar_extras_entity.rb b/app/serializers/merge_request_sidebar_extras_entity.rb
index 7276509c363..9db8e52abef 100644
--- a/app/serializers/merge_request_sidebar_extras_entity.rb
+++ b/app/serializers/merge_request_sidebar_extras_entity.rb
@@ -4,4 +4,8 @@ class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees do |merge_request|
MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request)
end
+
+ expose :reviewers, if: -> (m) { m.allows_reviewers? } do |merge_request|
+ MergeRequestReviewerEntity.represent(merge_request.reviewers, merge_request: merge_request)
+ end
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index b7b9e7d1036..494192c8dbb 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -62,6 +62,7 @@ class MergeRequestWidgetEntity < Grape::Entity
merge_request.source_branch,
file_name: '.gitlab-ci.yml',
commit_message: s_("CommitMessage|Add %{file_name}") % { file_name: Gitlab::FileDetector::PATTERNS[:gitlab_ci] },
+ mr_path: merge_request_path(merge_request),
suggest_gitlab_ci_yml: true
)
end
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index c49dec2a93c..ef305195e22 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -46,6 +46,10 @@ class NoteEntity < API::Entities::Note
SystemNoteHelper.system_note_icon_name(note)
end
+ expose :is_noteable_author do |note|
+ note.noteable_author?(request.noteable)
+ end
+
expose :discussion_id do |note|
note.discussion_id(request.noteable)
end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index de1e07139ad..64958b06f1d 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -85,10 +85,6 @@ class PipelineEntity < Grape::Entity
pipeline.failed_builds
end
- expose :tests_total_count do |pipeline|
- pipeline.test_report_summary.total[:count]
- end
-
private
alias_method :pipeline, :object
diff --git a/app/serializers/project_note_entity.rb b/app/serializers/project_note_entity.rb
index f6cdea1d8b5..3cd34f6af0d 100644
--- a/app/serializers/project_note_entity.rb
+++ b/app/serializers/project_note_entity.rb
@@ -5,6 +5,14 @@ class ProjectNoteEntity < NoteEntity
note.project.team.human_max_access(note.author_id)
end
+ expose :is_contributor, if: -> (note, _) { note.project.present? } do |note|
+ note.contributor?
+ end
+
+ expose :project_name, if: -> (note, _) { note.project.present? } do |note|
+ note.project.name
+ end
+
expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
toggle_award_emoji_project_note_path(note.project, note.id)
end
diff --git a/app/serializers/test_suite_entity.rb b/app/serializers/test_suite_entity.rb
index d04fd5f6a84..15eb2891b22 100644
--- a/app/serializers/test_suite_entity.rb
+++ b/app/serializers/test_suite_entity.rb
@@ -13,7 +13,7 @@ class TestSuiteEntity < Grape::Entity
with_options if: -> (_, opts) { opts[:details] } do |test_suite|
expose :suite_error
expose :test_cases, using: TestCaseEntity do |test_suite|
- test_suite.suite_error ? [] : test_suite.test_cases.values.flat_map(&:values)
+ test_suite.suite_error ? [] : test_suite.sorted.test_cases.values.flat_map(&:values)
end
end
end
diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb
index 9a5ce58ee2c..34d6008cb6a 100644
--- a/app/services/admin/propagate_integration_service.rb
+++ b/app/services/admin/propagate_integration_service.rb
@@ -2,46 +2,24 @@
module Admin
class PropagateIntegrationService
- BATCH_SIZE = 100
-
- delegate :data_fields_present?, to: :integration
-
- def self.propagate(integration:, overwrite:)
- new(integration, overwrite).propagate
- end
-
- def initialize(integration, overwrite)
- @integration = integration
- @overwrite = overwrite
- end
+ include PropagateService
def propagate
- if overwrite
- update_integration_for_all_projects
- else
- update_integration_for_inherited_projects
- end
+ update_inherited_integrations
+ create_integration_for_groups_without_integration if Feature.enabled?(:group_level_integrations)
create_integration_for_projects_without_integration
end
private
- attr_reader :integration, :overwrite
-
# rubocop: disable Cop/InBatches
# rubocop: disable CodeReuse/ActiveRecord
- def update_integration_for_inherited_projects
+ def update_inherited_integrations
Service.where(type: integration.type, inherit_from_id: integration.id).in_batches(of: BATCH_SIZE) do |batch|
bulk_update_from_integration(batch)
end
end
-
- def update_integration_for_all_projects
- Service.where(type: integration.type).in_batches(of: BATCH_SIZE) do |batch|
- bulk_update_from_integration(batch)
- end
- end
# rubocop: enable Cop/InBatches
# rubocop: enable CodeReuse/ActiveRecord
@@ -62,61 +40,33 @@ module Admin
end
# rubocop: enable CodeReuse/ActiveRecord
- def create_integration_for_projects_without_integration
+ def create_integration_for_groups_without_integration
loop do
- batch = Project.uncached { Project.ids_without_integration(integration, BATCH_SIZE) }
+ batch = Group.uncached { group_ids_without_integration(integration, BATCH_SIZE) }
- bulk_create_from_integration(batch) unless batch.empty?
+ bulk_create_from_integration(batch, 'group') unless batch.empty?
break if batch.size < BATCH_SIZE
end
end
- def bulk_create_from_integration(batch)
- service_list = ServiceList.new(batch, service_hash, { 'inherit_from_id' => integration.id }).to_array
-
- Project.transaction do
- results = bulk_insert(*service_list)
-
- if data_fields_present?
- data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array
-
- bulk_insert(*data_list)
- end
-
- run_callbacks(batch)
- end
- end
-
- def bulk_insert(klass, columns, values_array)
- items_to_insert = values_array.map { |array| Hash[columns.zip(array)] }
-
- klass.insert_all(items_to_insert, returning: [:id])
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def run_callbacks(batch)
- if integration.issue_tracker?
- Project.where(id: batch).update_all(has_external_issue_tracker: true)
- end
-
- if active_external_wiki?
- Project.where(id: batch).update_all(has_external_wiki: true)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def active_external_wiki?
- integration.type == 'ExternalWikiService'
- end
-
def service_hash
@service_hash ||= integration.to_service_hash
.tap { |json| json['inherit_from_id'] = integration.id }
end
- def data_fields_hash
- @data_fields_hash ||= integration.to_data_fields_hash
+ # rubocop:disable CodeReuse/ActiveRecord
+ def group_ids_without_integration(integration, limit)
+ services = Service
+ .select('1')
+ .where('services.group_id = namespaces.id')
+ .where(type: integration.type)
+
+ Group
+ .where('NOT EXISTS (?)', services)
+ .limit(limit)
+ .pluck(:id)
end
+ # rubocop:enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/admin/propagate_service_template.rb b/app/services/admin/propagate_service_template.rb
new file mode 100644
index 00000000000..cd0d2d5d03f
--- /dev/null
+++ b/app/services/admin/propagate_service_template.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Admin
+ class PropagateServiceTemplate
+ include PropagateService
+
+ def propagate
+ return unless integration.active?
+
+ create_integration_for_projects_without_integration
+ end
+
+ private
+
+ def service_hash
+ @service_hash ||= integration.to_service_hash
+ end
+ end
+end
diff --git a/app/services/alert_management/create_alert_issue_service.rb b/app/services/alert_management/create_alert_issue_service.rb
index f16b106b748..58c7402c6c1 100644
--- a/app/services/alert_management/create_alert_issue_service.rb
+++ b/app/services/alert_management/create_alert_issue_service.rb
@@ -41,7 +41,8 @@ module AlertManagement
project,
user,
title: alert_presenter.title,
- description: alert_presenter.issue_description
+ description: alert_presenter.issue_description,
+ severity: alert.severity
).execute
end
diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb
index c233ea4e2e5..95ae84a85a4 100644
--- a/app/services/alert_management/process_prometheus_alert_service.rb
+++ b/app/services/alert_management/process_prometheus_alert_service.rb
@@ -3,9 +3,10 @@
module AlertManagement
class ProcessPrometheusAlertService < BaseService
include Gitlab::Utils::StrongMemoize
+ include ::IncidentManagement::Settings
def execute
- return bad_request unless parsed_alert.valid?
+ return bad_request unless incoming_payload.has_required_attributes?
process_alert_management_alert
@@ -14,71 +15,62 @@ module AlertManagement
private
- delegate :firing?, :resolved?, :gitlab_fingerprint, :ends_at, to: :parsed_alert
-
- def parsed_alert
- strong_memoize(:parsed_alert) do
- Gitlab::Alerting::Alert.new(project: project, payload: params)
- end
- end
-
def process_alert_management_alert
- process_firing_alert_management_alert if firing?
- process_resolved_alert_management_alert if resolved?
+ if incoming_payload.resolved?
+ process_resolved_alert_management_alert
+ else
+ process_firing_alert_management_alert
+ end
end
def process_firing_alert_management_alert
- if am_alert.present?
- am_alert.register_new_event!
+ if alert.persisted?
+ alert.register_new_event!
reset_alert_management_alert_status
else
create_alert_management_alert
end
- process_incident_alert
+ process_incident_issues if process_issues?
end
def reset_alert_management_alert_status
- return if am_alert.trigger
+ return if alert.trigger
logger.warn(
message: 'Unable to update AlertManagement::Alert status to triggered',
project_id: project.id,
- alert_id: am_alert.id
+ alert_id: alert.id
)
end
def create_alert_management_alert
- new_alert = AlertManagement::Alert.new(am_alert_params.merge(ended_at: nil))
- if new_alert.save
- new_alert.execute_services
- @am_alert = new_alert
+ if alert.save
+ alert.execute_services
+ SystemNoteService.create_new_alert(alert, Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus])
return
end
logger.warn(
message: 'Unable to create AlertManagement::Alert',
project_id: project.id,
- alert_errors: new_alert.errors.messages
+ alert_errors: alert.errors.messages
)
end
- def am_alert_params
- Gitlab::AlertManagement::AlertParams.from_prometheus_alert(project: project, parsed_alert: parsed_alert)
- end
-
def process_resolved_alert_management_alert
- return if am_alert.blank?
+ return unless alert.persisted?
+ return unless auto_close_incident?
- if am_alert.resolve(ends_at)
- close_issue(am_alert.issue)
+ if alert.resolve(incoming_payload.ends_at)
+ close_issue(alert.issue)
return
end
logger.warn(
message: 'Unable to update AlertManagement::Alert status to resolved',
project_id: project.id,
- alert_id: am_alert.id
+ alert_id: alert.id
)
end
@@ -92,20 +84,45 @@ module AlertManagement
SystemNoteService.auto_resolve_prometheus_alert(issue, project, User.alert_bot) if issue.reset.closed?
end
- def process_incident_alert
- return unless am_alert
- return if am_alert.issue
+ def process_incident_issues
+ return unless alert.persisted?
+ return if alert.issue
- IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, am_alert.id)
+ IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
end
def logger
@logger ||= Gitlab::AppLogger
end
- def am_alert
- strong_memoize(:am_alert) do
- AlertManagement::Alert.not_resolved.for_fingerprint(project, gitlab_fingerprint).first
+ def alert
+ strong_memoize(:alert) do
+ existing_alert || new_alert
+ end
+ end
+
+ def existing_alert
+ strong_memoize(:existing_alert) do
+ AlertManagement::Alert.not_resolved.for_fingerprint(project, incoming_payload.gitlab_fingerprint).first
+ end
+ end
+
+ def new_alert
+ strong_memoize(:new_alert) do
+ AlertManagement::Alert.new(
+ **incoming_payload.alert_params,
+ ended_at: nil
+ )
+ end
+ end
+
+ def incoming_payload
+ strong_memoize(:incoming_payload) do
+ Gitlab::AlertManagement::Payload.parse(
+ project,
+ params,
+ monitoring_tool: Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
+ )
end
end
diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb
index fef733a7d09..d7630dbdac9 100644
--- a/app/services/audit_event_service.rb
+++ b/app/services/audit_event_service.rb
@@ -25,6 +25,8 @@ class AuditEventService
#
# @return [AuditEventService]
def for_authentication
+ mark_as_authentication_event!
+
@details = {
with: @details[:with],
target_id: @author.id,
@@ -37,9 +39,10 @@ class AuditEventService
# Writes event to a file and creates an event record in DB
#
- # @return [SecurityEvent] persited if saves and non-persisted if fails
+ # @return [AuditEvent] persited if saves and non-persisted if fails
def security_event
log_security_event_to_file
+ log_authentication_event_to_database
log_security_event_to_database
end
@@ -50,6 +53,7 @@ class AuditEventService
private
+ attr_accessor :authentication_event
attr_reader :ip_address
def build_author(author)
@@ -70,6 +74,22 @@ class AuditEventService
}
end
+ def authentication_event_payload
+ {
+ # @author can be a User or various Gitlab::Audit authors.
+ # Only capture real users for successful authentication events.
+ user: author_if_user,
+ user_name: @author.name,
+ ip_address: ip_address,
+ result: AuthenticationEvent.results[:success],
+ provider: @details[:with]
+ }
+ end
+
+ def author_if_user
+ @author if @author.is_a?(User)
+ end
+
def file_logger
@file_logger ||= Gitlab::AuditJsonLogger.build
end
@@ -78,10 +98,24 @@ class AuditEventService
@details.merge(@details.slice(:from, :to).transform_values(&:to_s))
end
+ def mark_as_authentication_event!
+ self.authentication_event = true
+ end
+
+ def authentication_event?
+ authentication_event
+ end
+
def log_security_event_to_database
return if Gitlab::Database.read_only?
- SecurityEvent.create(base_payload.merge(details: @details))
+ AuditEvent.create(base_payload.merge(details: @details))
+ end
+
+ def log_authentication_event_to_database
+ return unless Gitlab::Database.read_write? && authentication_event?
+
+ AuthenticationEvent.create(authentication_event_payload)
end
end
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index 5c63dc34cb1..41236286d23 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -60,6 +60,21 @@ module AutoMerge
end
end
+ ##
+ # NOTE: This method is to be removed when `disallow_to_create_merge_request_pipelines_in_target_project`
+ # feature flag is removed.
+ def self.can_add_to_merge_train?(merge_request)
+ if Gitlab::Ci::Features.disallow_to_create_merge_request_pipelines_in_target_project?(merge_request.target_project)
+ merge_request.for_same_project?
+ else
+ true
+ end
+ end
+
+ def can_add_to_merge_train?(merge_request)
+ self.class.can_add_to_merge_train?(merge_request)
+ end
+
private
# Overridden in child classes
diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
index 7e0298432ac..d18f2935d92 100644
--- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
+++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
@@ -38,8 +38,6 @@ module AutoMerge
private
def notify(merge_request)
- return unless Feature.enabled?(:mwps_notification, project)
-
notification_service.async.merge_when_pipeline_succeeds(merge_request, current_user) if merge_request.saved_change_to_auto_merge_enabled?
end
end
diff --git a/app/services/boards/destroy_service.rb b/app/services/boards/destroy_service.rb
index ea0c1394aa3..8f3d4b58b7b 100644
--- a/app/services/boards/destroy_service.rb
+++ b/app/services/boards/destroy_service.rb
@@ -3,9 +3,13 @@
module Boards
class DestroyService < Boards::BaseService
def execute(board)
- return false if parent.boards.size == 1
+ if parent.boards.size == 1
+ return ServiceResponse.error(message: "The board could not be deleted, because the parent doesn't have any other boards.")
+ end
- board.destroy
+ board.destroy!
+
+ ServiceResponse.success
end
end
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 14e8683ebdf..56a7e228b10 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -71,12 +71,16 @@ module Boards
# rubocop: disable CodeReuse/ActiveRecord
def moving_from_list
+ return unless params[:from_list_id].present?
+
@moving_from_list ||= board.lists.find_by(id: params[:from_list_id])
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def moving_to_list
+ return unless params[:to_list_id].present?
+
@moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/branches/delete_service.rb b/app/services/branches/delete_service.rb
index 9bd5b343448..6efbdd161a1 100644
--- a/app/services/branches/delete_service.rb
+++ b/app/services/branches/delete_service.rb
@@ -26,7 +26,7 @@ module Branches
message: 'Failed to remove branch',
http_status: 400)
end
- rescue Gitlab::Git::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError, Gitlab::Git::CommandError => ex
ServiceResponse.error(message: ex.message, http_status: 400)
end
diff --git a/app/services/ci/archive_trace_service.rb b/app/services/ci/archive_trace_service.rb
index f143736ddc1..073ef465e13 100644
--- a/app/services/ci/archive_trace_service.rb
+++ b/app/services/ci/archive_trace_service.rb
@@ -13,6 +13,7 @@ module Ci
end
job.trace.archive!
+ job.remove_pending_state!
# TODO: Remove this logging once we confirmed new live trace architecture is functional.
# See https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/4667.
diff --git a/app/services/ci/cancel_user_pipelines_service.rb b/app/services/ci/cancel_user_pipelines_service.rb
index bcafb6b4a35..3a8b5e91088 100644
--- a/app/services/ci/cancel_user_pipelines_service.rb
+++ b/app/services/ci/cancel_user_pipelines_service.rb
@@ -7,6 +7,10 @@ module Ci
# https://gitlab.com/gitlab-org/gitlab/issues/32332
def execute(user)
user.pipelines.cancelable.find_each(&:cancel_running)
+
+ ServiceResponse.success(message: 'Pipeline canceled')
+ rescue ActiveRecord::StaleObjectError
+ ServiceResponse.error(message: 'Error canceling pipeline')
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/services/ci/create_cross_project_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb
index 23207d809d4..0394cfb6119 100644
--- a/app/services/ci/create_cross_project_pipeline_service.rb
+++ b/app/services/ci/create_downstream_pipeline_service.rb
@@ -1,12 +1,16 @@
# frozen_string_literal: true
module Ci
- # TODO: rename this (and worker) to CreateDownstreamPipelineService
- class CreateCrossProjectPipelineService < ::BaseService
+ # Takes in input a Ci::Bridge job and creates a downstream pipeline
+ # (either multi-project or child pipeline) according to the Ci::Bridge
+ # specifications.
+ class CreateDownstreamPipelineService < ::BaseService
include Gitlab::Utils::StrongMemoize
DuplicateDownstreamPipelineError = Class.new(StandardError)
+ MAX_DESCENDANTS_DEPTH = 2
+
def execute(bridge)
@bridge = bridge
@@ -73,9 +77,16 @@ module Ci
# TODO: Remove this condition if favour of model validation
# https://gitlab.com/gitlab-org/gitlab/issues/38338
- if @bridge.triggers_child_pipeline? && @bridge.pipeline.parent_pipeline.present?
- @bridge.drop!(:bridge_pipeline_is_child_pipeline)
- return false
+ if ::Gitlab::Ci::Features.child_of_child_pipeline_enabled?(project)
+ if has_max_descendants_depth?
+ @bridge.drop!(:reached_max_descendant_pipelines_depth)
+ return false
+ end
+ else
+ if @bridge.triggers_child_pipeline? && @bridge.pipeline.parent_pipeline.present?
+ @bridge.drop!(:bridge_pipeline_is_child_pipeline)
+ return false
+ end
end
unless can_create_downstream_pipeline?(target_ref)
@@ -106,5 +117,12 @@ module Ci
@bridge.downstream_project
end
end
+
+ def has_max_descendants_depth?
+ return false unless @bridge.triggers_child_pipeline?
+
+ ancestors_of_new_child = @bridge.pipeline.base_and_ancestors(same_project: true)
+ ancestors_of_new_child.count > MAX_DESCENDANTS_DEPTH
+ end
end
end
diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb
index cd3807e0495..1fe65898d55 100644
--- a/app/services/ci/create_job_artifacts_service.rb
+++ b/app/services/ci/create_job_artifacts_service.rb
@@ -2,6 +2,8 @@
module Ci
class CreateJobArtifactsService < ::BaseService
+ include Gitlab::Utils::UsageData
+
ArtifactsExistError = Class.new(StandardError)
LSIF_ARTIFACT_TYPE = 'lsif'
@@ -25,7 +27,7 @@ module Ci
if lsif?(artifact_type)
headers[:ProcessLsif] = true
- headers[:ProcessLsifReferences] = Feature.enabled?(:code_navigation_references, project, default_enabled: true)
+ track_usage_event('i_source_code_code_intelligence', project.id)
end
success(headers: headers)
diff --git a/app/services/ci/create_web_ide_terminal_service.rb b/app/services/ci/create_web_ide_terminal_service.rb
index 4f1bf0447d2..a78281aed16 100644
--- a/app/services/ci/create_web_ide_terminal_service.rb
+++ b/app/services/ci/create_web_ide_terminal_service.rb
@@ -70,7 +70,7 @@ module Ci
end
def load_terminal_config!
- result = ::Ci::WebIdeConfigService.new(project, current_user, sha: sha).execute
+ result = ::Ide::TerminalConfigService.new(project, current_user, sha: sha).execute
raise TerminalCreationError, result[:message] if result[:status] != :success
@terminal = result[:terminal]
diff --git a/app/services/ci/destroy_expired_job_artifacts_service.rb b/app/services/ci/destroy_expired_job_artifacts_service.rb
index 1fa8926faa1..ca6e60f819a 100644
--- a/app/services/ci/destroy_expired_job_artifacts_service.rb
+++ b/app/services/ci/destroy_expired_job_artifacts_service.rb
@@ -20,18 +20,18 @@ module Ci
def execute
in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
- destroy_batch
+ destroy_batch(Ci::JobArtifact) || destroy_batch(Ci::PipelineArtifact)
end
end
end
private
- def destroy_batch
- artifact_batch = if Gitlab::Ci::Features.destroy_only_unlocked_expired_artifacts_enabled?
- Ci::JobArtifact.expired(BATCH_SIZE).unlocked
+ def destroy_batch(klass)
+ artifact_batch = if klass == Ci::JobArtifact
+ klass.expired(BATCH_SIZE).unlocked
else
- Ci::JobArtifact.expired(BATCH_SIZE)
+ klass.expired(BATCH_SIZE)
end
artifacts = artifact_batch.to_a
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
index 9aea20c45f7..1d9533ed76f 100644
--- a/app/services/ci/destroy_pipeline_service.rb
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -8,6 +8,10 @@ module Ci
Ci::ExpirePipelineCacheService.new.execute(pipeline, delete: true)
pipeline.destroy!
+
+ ServiceResponse.success(message: 'Pipeline not found')
+ rescue ActiveRecord::RecordNotFound
+ ServiceResponse.error(message: 'Pipeline not found')
end
end
end
diff --git a/app/services/ci/generate_coverage_reports_service.rb b/app/services/ci/generate_coverage_reports_service.rb
index ebd1eaf0bad..063fb966183 100644
--- a/app/services/ci/generate_coverage_reports_service.rb
+++ b/app/services/ci/generate_coverage_reports_service.rb
@@ -12,7 +12,7 @@ module Ci
{
status: :parsed,
key: key(base_pipeline, head_pipeline),
- data: head_pipeline.coverage_reports.pick(merge_request.new_paths)
+ data: head_pipeline.pipeline_artifacts.find_with_code_coverage.present.for_files(merge_request.new_paths)
}
rescue => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
diff --git a/app/services/ci/parse_dotenv_artifact_service.rb b/app/services/ci/parse_dotenv_artifact_service.rb
index fcbdc94c097..71b306864b2 100644
--- a/app/services/ci/parse_dotenv_artifact_service.rb
+++ b/app/services/ci/parse_dotenv_artifact_service.rb
@@ -54,7 +54,7 @@ module Ci
end
def scan_line!(line)
- result = line.scan(/^(.*)=(.*)$/).last
+ result = line.scan(/^(.*?)=(.*)$/).last
raise ParserError, 'Invalid Format' if result.nil?
diff --git a/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb b/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb
index d0aa8b04775..aeabbb99468 100644
--- a/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb
+++ b/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb
@@ -77,15 +77,8 @@ module Ci
private
def status_for_array(statuses, dag:)
- # TODO: This is hack to support
- # the same exact behaviour for Atomic and Legacy processing
- # that DAG is blocked from executing if dependent is not "complete"
- if dag && statuses.any? { |status| Ci::HasStatus::COMPLETED_STATUSES.exclude?(status[:status]) }
- return 'pending'
- end
-
result = Gitlab::Ci::Status::Composite
- .new(statuses)
+ .new(statuses, dag: dag)
.status
result || 'success'
end
diff --git a/app/services/ci/pipelines/create_artifact_service.rb b/app/services/ci/pipelines/create_artifact_service.rb
new file mode 100644
index 00000000000..b7d334e436d
--- /dev/null
+++ b/app/services/ci/pipelines/create_artifact_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+module Ci
+ module Pipelines
+ class CreateArtifactService
+ def execute(pipeline)
+ return unless ::Gitlab::Ci::Features.coverage_report_view?(pipeline.project)
+ return unless pipeline.can_generate_coverage_reports?
+ return if pipeline.has_coverage_reports?
+
+ file = build_carrierwave_file(pipeline)
+
+ pipeline.pipeline_artifacts.create!(
+ project_id: pipeline.project_id,
+ file_type: :code_coverage,
+ file_format: :raw,
+ size: file["tempfile"].size,
+ file: file,
+ expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now
+ )
+ end
+
+ private
+
+ def build_carrierwave_file(pipeline)
+ CarrierWaveStringFile.new_file(
+ file_content: pipeline.coverage_reports.to_json,
+ filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_coverage),
+ content_type: 'application/json'
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index d84ef5fbb93..18bae26613f 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -37,10 +37,12 @@ module Ci
.pluck(Arel.sql('MAX(id)'), 'name')
# mark builds that are retried
- pipeline.statuses.latest
- .where(name: latest_statuses.map(&:second))
- .where.not(id: latest_statuses.map(&:first))
- .update_all(retried: true) if latest_statuses.any?
+ if latest_statuses.any?
+ pipeline.statuses.latest
+ .where(name: latest_statuses.map(&:second))
+ .where.not(id: latest_statuses.map(&:first))
+ .update_all(retried: true)
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 60b3d28b0c5..6b2e6c245f3 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -2,17 +2,20 @@
module Ci
class RetryBuildService < ::BaseService
- CLONE_ACCESSORS = %i[pipeline project ref tag options name
- allow_failure stage stage_id stage_idx trigger_request
- yaml_variables when environment coverage_regex
- description tag_list protected needs_attributes
- resource_group scheduling_type].freeze
+ def self.clone_accessors
+ %i[pipeline project ref tag options name
+ allow_failure stage stage_id stage_idx trigger_request
+ yaml_variables when environment coverage_regex
+ description tag_list protected needs_attributes
+ resource_group scheduling_type].freeze
+ end
def execute(build)
build.ensure_scheduling_type!
reprocess!(build).tap do |new_build|
- build.pipeline.mark_as_processable_after_stage(build.stage_idx)
+ mark_subsequent_stages_as_processable(build)
+ build.pipeline.reset_ancestor_bridges!
Gitlab::OptimisticLocking.retry_lock(new_build, &:enqueue)
@@ -28,7 +31,7 @@ module Ci
raise Gitlab::Access::AccessDeniedError
end
- attributes = CLONE_ACCESSORS.map do |attribute|
+ attributes = self.class.clone_accessors.map do |attribute|
[attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
end.to_h
@@ -60,5 +63,13 @@ module Ci
end
build
end
+
+ def mark_subsequent_stages_as_processable(build)
+ build.pipeline.processables.skipped.after_stage(build.stage_idx).find_each do |processable|
+ Gitlab::OptimisticLocking.retry_lock(processable, &:process)
+ end
+ end
end
end
+
+Ci::RetryBuildService.prepend_if_ee('EE::Ci::RetryBuildService')
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 2f52f0a39c1..45244d16393 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -26,6 +26,8 @@ module Ci
retry_optimistic_lock(skipped) { |build| build.process }
end
+ pipeline.reset_ancestor_bridges!
+
MergeRequests::AddTodoWhenBuildFailsService
.new(project, current_user)
.close_all(pipeline)
diff --git a/app/services/ci/update_build_state_service.rb b/app/services/ci/update_build_state_service.rb
new file mode 100644
index 00000000000..61e4c77c1e5
--- /dev/null
+++ b/app/services/ci/update_build_state_service.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+module Ci
+ class UpdateBuildStateService
+ Result = Struct.new(:status, keyword_init: true)
+
+ ACCEPT_TIMEOUT = 5.minutes.freeze
+
+ attr_reader :build, :params, :metrics
+
+ def initialize(build, params, metrics = ::Gitlab::Ci::Trace::Metrics.new)
+ @build = build
+ @params = params
+ @metrics = metrics
+ end
+
+ def execute
+ overwrite_trace! if has_trace?
+
+ if accept_request?
+ accept_build_state!
+ else
+ check_migration_state
+ update_build_state!
+ end
+ end
+
+ private
+
+ def accept_build_state!
+ if Time.current - ensure_pending_state.created_at > ACCEPT_TIMEOUT
+ metrics.increment_trace_operation(operation: :discarded)
+
+ return update_build_state!
+ end
+
+ build.trace_chunks.live.find_each do |chunk|
+ chunk.schedule_to_persist!
+ end
+
+ metrics.increment_trace_operation(operation: :accepted)
+
+ Result.new(status: 202)
+ end
+
+ def overwrite_trace!
+ metrics.increment_trace_operation(operation: :overwrite)
+
+ build.trace.set(params[:trace]) if Gitlab::Ci::Features.trace_overwrite?
+ end
+
+ def check_migration_state
+ return unless accept_available?
+
+ if has_chunks? && !live_chunks_pending?
+ metrics.increment_trace_operation(operation: :finalized)
+ end
+ end
+
+ def update_build_state!
+ case build_state
+ when 'running'
+ build.touch if build.needs_touch?
+
+ Result.new(status: 200)
+ when 'success'
+ build.success!
+
+ Result.new(status: 200)
+ when 'failed'
+ build.drop!(params[:failure_reason] || :unknown_failure)
+
+ Result.new(status: 200)
+ else
+ Result.new(status: 400)
+ end
+ end
+
+ def accept_available?
+ !build_running? && has_checksum? && chunks_migration_enabled?
+ end
+
+ def accept_request?
+ accept_available? && live_chunks_pending?
+ end
+
+ def build_state
+ params.dig(:state).to_s
+ end
+
+ def has_trace?
+ params.dig(:trace).present?
+ end
+
+ def has_checksum?
+ params.dig(:checksum).present?
+ end
+
+ def has_chunks?
+ build.trace_chunks.any?
+ end
+
+ def live_chunks_pending?
+ build.trace_chunks.live.any?
+ end
+
+ def build_running?
+ build_state == 'running'
+ end
+
+ def ensure_pending_state
+ Ci::BuildPendingState.create_or_find_by!(
+ build_id: build.id,
+ state: params.fetch(:state),
+ trace_checksum: params.fetch(:checksum),
+ failure_reason: params.dig(:failure_reason)
+ )
+ rescue ActiveRecord::RecordNotFound
+ metrics.increment_trace_operation(operation: :conflict)
+
+ build.pending_state
+ end
+
+ def chunks_migration_enabled?
+ ::Gitlab::Ci::Features.accept_trace?(build.project)
+ end
+ end
+end
diff --git a/app/services/ci/update_ci_ref_status_service.rb b/app/services/ci/update_ci_ref_status_service.rb
deleted file mode 100644
index 22cc43232cc..00000000000
--- a/app/services/ci/update_ci_ref_status_service.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This class is unused and to be removed in 13.1~
-module Ci
- class UpdateCiRefStatusService
- include Gitlab::OptimisticLocking
-
- attr_reader :pipeline
-
- def initialize(pipeline)
- @pipeline = pipeline
- end
-
- def call
- save.tap { |success| after_save if success }
- end
-
- private
-
- def save
- might_insert = ref.new_record?
-
- begin
- retry_optimistic_lock(ref) do
- next false if ref.persisted? &&
- (ref.last_updated_by_pipeline_id || 0) > pipeline.id
-
- ref.update(status: next_status(ref.status, pipeline.status),
- last_updated_by_pipeline: pipeline)
- end
- rescue ActiveRecord::RecordNotUnique
- if might_insert
- @ref = pipeline.reset.ref_status
- might_insert = false
- retry
- else
- raise
- end
- end
- end
-
- def next_status(ref_status, pipeline_status)
- if ref_status == 'failed' && pipeline_status == 'success'
- 'fixed'
- else
- pipeline_status
- end
- end
-
- def after_save
- enqueue_pipeline_notification
- end
-
- def enqueue_pipeline_notification
- PipelineNotificationWorker.perform_async(pipeline.id, ref_status: ref.status)
- end
-
- def ref
- @ref ||= pipeline.ref_status || build_ref
- end
-
- def build_ref
- Ci::Ref.new(ref: pipeline.ref, project: pipeline.project, tag: pipeline.tag)
- end
- end
-end
diff --git a/app/services/clusters/aws/provision_service.rb b/app/services/clusters/aws/provision_service.rb
index 109e4c04a9c..b454a7a5f59 100644
--- a/app/services/clusters/aws/provision_service.rb
+++ b/app/services/clusters/aws/provision_service.rb
@@ -63,6 +63,7 @@ module Clusters
[
parameter('ClusterName', provider.cluster.name),
parameter('ClusterRole', provider.role_arn),
+ parameter('KubernetesVersion', provider.kubernetes_version),
parameter('ClusterControlPlaneSecurityGroup', provider.security_group_id),
parameter('VpcId', provider.vpc_id),
parameter('Subnets', provider.subnet_ids.join(',')),
diff --git a/app/services/concerns/admin/propagate_service.rb b/app/services/concerns/admin/propagate_service.rb
new file mode 100644
index 00000000000..974408f678c
--- /dev/null
+++ b/app/services/concerns/admin/propagate_service.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Admin
+ module PropagateService
+ extend ActiveSupport::Concern
+
+ BATCH_SIZE = 100
+
+ delegate :data_fields_present?, to: :integration
+
+ class_methods do
+ def propagate(integration)
+ new(integration).propagate
+ end
+ end
+
+ def initialize(integration)
+ @integration = integration
+ end
+
+ private
+
+ attr_reader :integration
+
+ def create_integration_for_projects_without_integration
+ loop do
+ batch_ids = Project.uncached { Project.ids_without_integration(integration, BATCH_SIZE) }
+
+ bulk_create_from_integration(batch_ids, 'project') unless batch_ids.empty?
+
+ break if batch_ids.size < BATCH_SIZE
+ end
+ end
+
+ def bulk_create_from_integration(batch_ids, association)
+ service_list = ServiceList.new(batch_ids, service_hash, association).to_array
+
+ Service.transaction do
+ results = bulk_insert(*service_list)
+
+ if data_fields_present?
+ data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array
+
+ bulk_insert(*data_list)
+ end
+
+ run_callbacks(batch_ids) if association == 'project'
+ end
+ end
+
+ def bulk_insert(klass, columns, values_array)
+ items_to_insert = values_array.map { |array| Hash[columns.zip(array)] }
+
+ klass.insert_all(items_to_insert, returning: [:id])
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def run_callbacks(batch_ids)
+ if integration.issue_tracker?
+ Project.where(id: batch_ids).update_all(has_external_issue_tracker: true)
+ end
+
+ if integration.type == 'ExternalWikiService'
+ Project.where(id: batch_ids).update_all(has_external_wiki: true)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def data_fields_hash
+ @data_fields_hash ||= integration.to_data_fields_hash
+ end
+ end
+end
diff --git a/app/services/concerns/incident_management/settings.rb b/app/services/concerns/incident_management/settings.rb
index 13a047ec106..93dfd6a306d 100644
--- a/app/services/concerns/incident_management/settings.rb
+++ b/app/services/concerns/incident_management/settings.rb
@@ -16,5 +16,9 @@ module IncidentManagement
def process_issues?
incident_management_setting.create_issue?
end
+
+ def auto_close_incident?
+ incident_management_setting.auto_close_incident?
+ end
end
end
diff --git a/app/services/concerns/incident_management/usage_data.rb b/app/services/concerns/incident_management/usage_data.rb
new file mode 100644
index 00000000000..b91aa59099d
--- /dev/null
+++ b/app/services/concerns/incident_management/usage_data.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module UsageData
+ include Gitlab::Utils::UsageData
+
+ def track_incident_action(current_user, target, action)
+ return unless target.incident?
+
+ track_usage_event(:"incident_management_#{action}", current_user.id)
+ end
+
+ # No-op as optionally overridden in implementing classes.
+ # For use to provide checks before calling #track_incident_action.
+ def track_event
+ end
+ end
+end
diff --git a/app/services/concerns/measurable.rb b/app/services/concerns/measurable.rb
index b099a58a9ae..fcb3022a1dc 100644
--- a/app/services/concerns/measurable.rb
+++ b/app/services/concerns/measurable.rb
@@ -45,7 +45,7 @@ module Measurable
private
def measuring?
- Feature.enabled?("gitlab_service_measuring_#{service_class}")
+ Feature.enabled?("gitlab_service_measuring_#{service_class}", type: :ops)
end
# These attributes are always present in log.
diff --git a/app/services/concerns/merge_requests/removes_refs.rb b/app/services/concerns/merge_requests/removes_refs.rb
new file mode 100644
index 00000000000..87c15746548
--- /dev/null
+++ b/app/services/concerns/merge_requests/removes_refs.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ module RemovesRefs
+ def cleanup_refs(merge_request)
+ CleanupRefsService.schedule(merge_request)
+ end
+ end
+end
diff --git a/app/services/design_management/move_designs_service.rb b/app/services/design_management/move_designs_service.rb
index de763caba2f..ca715b10351 100644
--- a/app/services/design_management/move_designs_service.rb
+++ b/app/services/design_management/move_designs_service.rb
@@ -13,7 +13,6 @@ module DesignManagement
def execute
return error(:no_focus) unless current_design.present?
- return error(:cannot_move) unless ::Feature.enabled?(:reorder_designs, project, default_enabled: true)
return error(:cannot_move) unless current_user.can?(:move_design, current_design)
return error(:no_neighbors) unless neighbors.present?
return error(:not_distinct) unless all_distinct?
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 3921dbefd06..85658598afc 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -23,11 +23,15 @@ class EventCreateService
end
def open_mr(merge_request, current_user)
- create_record_event(merge_request, current_user, :created)
+ create_record_event(merge_request, current_user, :created).tap do
+ track_event(event_action: :created, event_target: MergeRequest, author_id: current_user.id)
+ end
end
def close_mr(merge_request, current_user)
- create_record_event(merge_request, current_user, :closed)
+ create_record_event(merge_request, current_user, :closed).tap do
+ track_event(event_action: :closed, event_target: MergeRequest, author_id: current_user.id)
+ end
end
def reopen_mr(merge_request, current_user)
@@ -35,7 +39,9 @@ class EventCreateService
end
def merge_mr(merge_request, current_user)
- create_record_event(merge_request, current_user, :merged)
+ create_record_event(merge_request, current_user, :merged).tap do
+ track_event(event_action: :merged, event_target: MergeRequest, author_id: current_user.id)
+ end
end
def open_milestone(milestone, current_user)
@@ -55,7 +61,11 @@ class EventCreateService
end
def leave_note(note, current_user)
- create_record_event(note, current_user, :commented)
+ create_record_event(note, current_user, :commented).tap do
+ if note.is_a?(DiffNote) && note.for_merge_request?
+ track_event(event_action: :commented, event_target: MergeRequest, author_id: current_user.id)
+ end
+ end
end
def join_project(project, current_user)
@@ -109,7 +119,7 @@ class EventCreateService
def wiki_event(wiki_page_meta, author, action, fingerprint)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
- Gitlab::UsageDataCounters::TrackUniqueActions.track_event(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
+ track_event(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
duplicate = Event.for_wiki_meta(wiki_page_meta).for_fingerprint(fingerprint).first
return duplicate if duplicate.present?
@@ -154,7 +164,7 @@ class EventCreateService
result = Event.insert_all(attribute_sets, returning: %w[id])
tuples.each do |record, status, _|
- Gitlab::UsageDataCounters::TrackUniqueActions.track_event(event_action: status, event_target: record.class, author_id: current_user.id)
+ track_event(event_action: status, event_target: record.class, author_id: current_user.id)
end
result
@@ -172,7 +182,7 @@ class EventCreateService
new_event
end
- Gitlab::UsageDataCounters::TrackUniqueActions.track_event(event_action: :pushed, event_target: Project, author_id: current_user.id)
+ track_event(event_action: :pushed, event_target: Project, author_id: current_user.id)
Users::LastPushEventService.new(current_user)
.cache_last_push_event(event)
@@ -206,6 +216,10 @@ class EventCreateService
{ resource_parent_attr => resource_parent.id }
end
+
+ def track_event(**params)
+ Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(**params)
+ end
end
EventCreateService.prepend_if_ee('EE::EventCreateService')
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index 92e7702727c..dcb32b4c84b 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -75,6 +75,7 @@ module Git
def branch_change_hooks
enqueue_process_commit_messages
+ enqueue_jira_connect_sync_messages
end
def branch_remove_hooks
@@ -103,6 +104,17 @@ module Git
end
end
+ def enqueue_jira_connect_sync_messages
+ return unless project.jira_subscription_exists?
+
+ branch_to_sync = branch_name if Atlassian::JiraIssueKeyExtractor.has_keys?(branch_name)
+ commits_to_sync = limited_commits.select { |commit| Atlassian::JiraIssueKeyExtractor.has_keys?(commit.safe_message) }.map(&:sha)
+
+ if branch_to_sync || commits_to_sync.any?
+ JiraConnect::SyncBranchWorker.perform_async(project.id, branch_to_sync, commits_to_sync)
+ end
+ end
+
def unsigned_x509_shas(commits)
X509CommitSignature.unsigned_commit_shas(commits.map(&:sha))
end
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index f9de72f2d5f..fa3019ee9d6 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -5,7 +5,16 @@ module Git
# Maximum number of change events we will process on any single push
MAX_CHANGES = 100
+ attr_reader :wiki
+
+ def initialize(wiki, current_user, params)
+ @wiki, @current_user, @params = wiki, current_user, params.dup
+ end
+
def execute
+ # Execute model-specific callbacks
+ wiki.after_post_receive
+
process_changes
end
@@ -23,7 +32,11 @@ module Git
end
def can_process_wiki_events?
- Feature.enabled?(:wiki_events_on_git_push, project)
+ # TODO: Support activity events for group wikis
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/209306
+ return false unless wiki.is_a?(ProjectWiki)
+
+ Feature.enabled?(:wiki_events_on_git_push, wiki.container)
end
def push_changes
@@ -36,10 +49,6 @@ module Git
wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev])
end
- def wiki
- project.wiki
- end
-
def create_event_for(change)
event_service.execute(
change.last_known_slug,
@@ -54,7 +63,7 @@ module Git
end
def on_default_branch?(change)
- project.wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
+ wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
end
# See: [Gitlab::GitPostReceive#changes]
diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb
index 562c43487e9..3d1d0fe8c4e 100644
--- a/app/services/git/wiki_push_service/change.rb
+++ b/app/services/git/wiki_push_service/change.rb
@@ -5,11 +5,11 @@ module Git
class Change
include Gitlab::Utils::StrongMemoize
- # @param [ProjectWiki] wiki
+ # @param [Wiki] wiki
# @param [Hash] change - must have keys `:oldrev` and `:newrev`
# @param [Gitlab::Git::RawDiffChange] raw_change
- def initialize(project_wiki, change, raw_change)
- @wiki, @raw_change, @change = project_wiki, raw_change, change
+ def initialize(wiki, change, raw_change)
+ @wiki, @raw_change, @change = wiki, raw_change, change
end
def page
diff --git a/app/services/ci/web_ide_config_service.rb b/app/services/ide/base_config_service.rb
index ade9132f419..1f8d5c17584 100644
--- a/app/services/ci/web_ide_config_service.rb
+++ b/app/services/ide/base_config_service.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
-module Ci
- class WebIdeConfigService < ::BaseService
- include ::Gitlab::Utils::StrongMemoize
-
+module Ide
+ class BaseConfigService < ::BaseService
ValidationError = Class.new(StandardError)
WEBIDE_CONFIG_FILE = '.gitlab/.gitlab-webide.yml'.freeze
@@ -11,15 +9,21 @@ module Ci
attr_reader :config, :config_content
def execute
- check_access!
- load_config_content!
- load_config!
+ check_access_and_load_config!
- success(terminal: config.terminal_value)
+ success
rescue ValidationError => e
error(e.message)
end
+ protected
+
+ def check_access_and_load_config!
+ check_access!
+ load_config_content!
+ load_config!
+ end
+
private
def check_access!
diff --git a/app/services/ide/schemas_config_service.rb b/app/services/ide/schemas_config_service.rb
new file mode 100644
index 00000000000..8d2ce97103d
--- /dev/null
+++ b/app/services/ide/schemas_config_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Ide
+ class SchemasConfigService < ::Ide::BaseConfigService
+ PREDEFINED_SCHEMAS = [{
+ uri: 'https://json.schemastore.org/gitlab-ci',
+ match: ['*.gitlab-ci.yml']
+ }].freeze
+
+ def execute
+ schema = predefined_schema_for(params[:filename]) || {}
+ success(schema: schema)
+ rescue => e
+ error(e.message)
+ end
+
+ private
+
+ def find_schema(filename, schemas)
+ match_flags = ::File::FNM_DOTMATCH | ::File::FNM_PATHNAME
+
+ schemas.each do |schema|
+ match = schema[:match].any? { |pattern| ::File.fnmatch?(pattern, filename, match_flags) }
+
+ return Gitlab::Json.parse(get_cached(schema[:uri])) if match
+ end
+
+ nil
+ end
+
+ def predefined_schema_for(filename)
+ find_schema(filename, predefined_schemas)
+ end
+
+ def predefined_schemas
+ return PREDEFINED_SCHEMAS if Feature.enabled?(:schema_linting)
+
+ []
+ end
+
+ def get_cached(url)
+ Rails.cache.fetch("services:ide:schema:#{url}", expires_in: 1.day) do
+ Gitlab::HTTP.get(url).body
+ end
+ end
+ end
+end
+
+Ide::SchemasConfigService.prepend_if_ee('::EE::Ide::SchemasConfigService')
diff --git a/app/services/ide/terminal_config_service.rb b/app/services/ide/terminal_config_service.rb
new file mode 100644
index 00000000000..318df3436c4
--- /dev/null
+++ b/app/services/ide/terminal_config_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ide
+ class TerminalConfigService < ::Ide::BaseConfigService
+ private
+
+ def success(pass_back = {})
+ result = super(pass_back)
+ result[:terminal] = config.terminal_value
+ result
+ end
+ end
+end
diff --git a/app/services/incident_management/incidents/create_service.rb b/app/services/incident_management/incidents/create_service.rb
index 7206eaf51b2..5b925e0f440 100644
--- a/app/services/incident_management/incidents/create_service.rb
+++ b/app/services/incident_management/incidents/create_service.rb
@@ -5,11 +5,12 @@ module IncidentManagement
class CreateService < BaseService
ISSUE_TYPE = 'incident'
- def initialize(project, current_user, title:, description:)
+ def initialize(project, current_user, title:, description:, severity: IssuableSeverity::DEFAULT)
super(project, current_user)
@title = title
@description = description
+ @severity = severity
end
def execute
@@ -18,25 +19,19 @@ module IncidentManagement
current_user,
title: title,
description: description,
- label_ids: [find_or_create_incident_label.id],
issue_type: ISSUE_TYPE
).execute
return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
+ issue.update_severity(severity)
+
success(issue)
end
private
- attr_reader :title, :description
-
- def find_or_create_incident_label
- IncidentManagement::CreateIncidentLabelService
- .new(project, current_user)
- .execute
- .payload[:label]
- end
+ attr_reader :title, :description, :severity
def success(issue)
ServiceResponse.success(payload: { issue: issue })
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index 84024cca68c..fbc72dc867a 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -22,7 +22,7 @@ module Issuable
end
create_due_date_note if issuable.previous_changes.include?('due_date')
- create_milestone_note(old_milestone) if issuable.previous_changes.include?('milestone_id')
+ create_milestone_change_event(old_milestone) if issuable.previous_changes.include?('milestone_id')
create_labels_note(old_labels) if old_labels && issuable.labels != old_labels
end
end
@@ -94,23 +94,11 @@ module Issuable
SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user)
end
- def create_milestone_note(old_milestone)
- if milestone_changes_tracking_enabled?
- create_milestone_change_event(old_milestone)
- else
- SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
- end
- end
-
def create_milestone_change_event(old_milestone)
ResourceEvents::ChangeMilestoneService.new(issuable, current_user, old_milestone: old_milestone)
.execute
end
- def milestone_changes_tracking_enabled?
- ::Feature.enabled?(:track_resource_milestone_change_events, issuable.project, default_enabled: true)
- end
-
def create_due_date_note
SystemNoteService.change_due_date(issuable, issuable.project, current_user, issuable.due_date)
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 65a73dadc2e..56bcef0c562 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -46,7 +46,7 @@ class IssuableBaseService < BaseService
params[:assignee_ids] = params[:assignee_ids].first(1)
end
- assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
+ assignee_ids = params[:assignee_ids].select { |assignee_id| user_can_read?(issuable, assignee_id) }
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::Params::NONE]
params[:assignee_ids] = []
@@ -57,15 +57,15 @@ class IssuableBaseService < BaseService
end
end
- def assignee_can_read?(issuable, assignee_id)
- new_assignee = User.find_by_id(assignee_id)
+ def user_can_read?(issuable, user_id)
+ user = User.find_by_id(user_id)
- return false unless new_assignee
+ return false unless user
ability_name = :"read_#{issuable.to_ability_name}"
resource = issuable.persisted? ? issuable : project
- can?(new_assignee, ability_name, resource)
+ can?(user, ability_name, resource)
end
def filter_milestone
@@ -136,7 +136,7 @@ class IssuableBaseService < BaseService
{}
end
- def create(issuable)
+ def create(issuable, skip_system_notes: false)
handle_quick_actions(issuable)
filter_params(issuable)
@@ -153,7 +153,7 @@ class IssuableBaseService < BaseService
end
if issuable_saved
- Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, is_update: false)
+ create_system_notes(issuable, is_update: false) unless skip_system_notes
after_create(issuable)
execute_hooks(issuable)
@@ -184,10 +184,7 @@ class IssuableBaseService < BaseService
handle_quick_actions(issuable)
filter_params(issuable)
- change_state(issuable)
- change_subscription(issuable)
- change_todo(issuable)
- toggle_award(issuable)
+ change_additional_attributes(issuable)
old_associations = associations_before_update(issuable)
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
@@ -220,8 +217,9 @@ class IssuableBaseService < BaseService
end
if issuable_saved
- Issuable::CommonSystemNotesService.new(project, current_user).execute(
- issuable, old_labels: old_associations[:labels], old_milestone: old_associations[:milestone])
+ create_system_notes(
+ issuable, old_labels: old_associations[:labels], old_milestone: old_associations[:milestone]
+ )
handle_changes(issuable, old_associations: old_associations)
@@ -255,7 +253,7 @@ class IssuableBaseService < BaseService
before_update(issuable, skip_spam_check: true)
if issuable.with_transaction_returning_status { issuable.save }
- Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: nil)
+ create_system_notes(issuable, old_labels: nil)
handle_task_changes(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees.to_a)
@@ -304,6 +302,14 @@ class IssuableBaseService < BaseService
issuable.title_changed? || issuable.description_changed?
end
+ def change_additional_attributes(issuable)
+ change_state(issuable)
+ change_severity(issuable)
+ change_subscription(issuable)
+ change_todo(issuable)
+ toggle_award(issuable)
+ end
+
def change_state(issuable)
case params.delete(:state_event)
when 'reopen'
@@ -313,6 +319,12 @@ class IssuableBaseService < BaseService
end
end
+ def change_severity(issuable)
+ if severity = params.delete(:severity)
+ issuable.update_severity(severity)
+ end
+ end
+
def change_subscription(issuable)
case params.delete(:subscription_event)
when 'subscribe'
@@ -339,6 +351,10 @@ class IssuableBaseService < BaseService
AwardEmojis::ToggleService.new(issuable, award, current_user).execute if award
end
+ def create_system_notes(issuable, **options)
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, **options)
+ end
+
def associations_before_update(issuable)
associations =
{
@@ -353,8 +369,8 @@ class IssuableBaseService < BaseService
associations
end
- def has_changes?(issuable, old_labels: [], old_assignees: [])
- valid_attrs = [:title, :description, :assignee_ids, :milestone_id, :target_branch]
+ def has_changes?(issuable, old_labels: [], old_assignees: [], old_reviewers: [])
+ valid_attrs = [:title, :description, :assignee_ids, :reviewer_ids, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s)
@@ -364,7 +380,9 @@ class IssuableBaseService < BaseService
assignees_changed = issuable.assignees != old_assignees
- attrs_changed || labels_changed || assignees_changed
+ reviewers_changed = issuable.reviewers != old_reviewers if issuable.allows_reviewers?
+
+ attrs_changed || labels_changed || assignees_changed || reviewers_changed
end
def invalidate_cache_counts(issuable, users: [])
diff --git a/app/services/issuable_links/create_service.rb b/app/services/issuable_links/create_service.rb
new file mode 100644
index 00000000000..f148c503dcf
--- /dev/null
+++ b/app/services/issuable_links/create_service.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+module IssuableLinks
+ class CreateService < BaseService
+ include IncidentManagement::UsageData
+
+ attr_reader :issuable, :current_user, :params
+
+ def initialize(issuable, user, params)
+ @issuable, @current_user, @params = issuable, user, params.dup
+ end
+
+ def execute
+ # If ALL referenced issues are already assigned to the given epic it renders a conflict status,
+ # otherwise create issue links for the issues which
+ # are still not assigned and return success message.
+ if render_conflict_error?
+ return error(issuables_assigned_message, 409)
+ end
+
+ if render_not_found_error?
+ return error(issuables_not_found_message, 404)
+ end
+
+ @errors = []
+ create_links
+
+ if @errors.present?
+ return error(@errors.join('. '), 422)
+ end
+
+ track_event
+
+ success
+ end
+
+ private
+
+ def render_conflict_error?
+ referenced_issuables.present? && (referenced_issuables - previous_related_issuables).empty?
+ end
+
+ def render_not_found_error?
+ linkable_issuables(referenced_issuables).empty?
+ end
+
+ def create_links
+ objects = linkable_issuables(referenced_issuables)
+ link_issuables(objects)
+ end
+
+ def link_issuables(target_issuables)
+ target_issuables.each do |referenced_object|
+ link = relate_issuables(referenced_object)
+
+ unless link.valid?
+ @errors << _("%{ref} cannot be added: %{error}") % {
+ ref: referenced_object.to_reference,
+ error: link.errors.messages.values.flatten.to_sentence
+ }
+ end
+ end
+ end
+
+ def referenced_issuables
+ @referenced_issuables ||= begin
+ target_issuable = params[:target_issuable]
+
+ if params[:issuable_references].present?
+ extract_references
+ elsif target_issuable
+ [target_issuable]
+ else
+ []
+ end
+ end
+ end
+
+ def extract_references
+ issuable_references = params[:issuable_references]
+ text = issuable_references.join(' ')
+
+ extractor = Gitlab::ReferenceExtractor.new(issuable.project, current_user)
+ extractor.analyze(text, extractor_context)
+
+ references(extractor)
+ end
+
+ def references(extractor)
+ extractor.issues
+ end
+
+ def extractor_context
+ {}
+ end
+
+ def linkable_issuables(objects)
+ raise NotImplementedError
+ end
+
+ def previous_related_issuables
+ raise NotImplementedError
+ end
+
+ def relate_issuables(referenced_object)
+ raise NotImplementedError
+ end
+
+ def issuables_assigned_message
+ 'Issue(s) already assigned'
+ end
+
+ def issuables_not_found_message
+ 'No Issue found for given params'
+ end
+ end
+end
+
+IssuableLinks::CreateService.prepend_if_ee('EE::IssuableLinks::CreateService')
diff --git a/app/services/issuable_links/destroy_service.rb b/app/services/issuable_links/destroy_service.rb
new file mode 100644
index 00000000000..57e1314e0da
--- /dev/null
+++ b/app/services/issuable_links/destroy_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module IssuableLinks
+ class DestroyService < BaseService
+ include IncidentManagement::UsageData
+
+ attr_reader :link, :current_user
+
+ def initialize(link, user)
+ @link = link
+ @current_user = user
+ end
+
+ def execute
+ return error(not_found_message, 404) unless permission_to_remove_relation?
+
+ remove_relation
+ create_notes
+ track_event
+
+ success(message: 'Relation was removed')
+ end
+
+ private
+
+ def remove_relation
+ link.destroy!
+ end
+
+ def not_found_message
+ 'No Issue Link found'
+ end
+ end
+end
diff --git a/app/services/issuable_links/list_service.rb b/app/services/issuable_links/list_service.rb
new file mode 100644
index 00000000000..10a2da7eb03
--- /dev/null
+++ b/app/services/issuable_links/list_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module IssuableLinks
+ class ListService
+ include Gitlab::Routing
+
+ attr_reader :issuable, :current_user
+
+ def initialize(issuable, user)
+ @issuable, @current_user = issuable, user
+ end
+
+ def execute
+ serializer.new(current_user: current_user, issuable: issuable).represent(child_issuables)
+ end
+
+ private
+
+ def serializer
+ raise NotImplementedError
+ end
+
+ def preload_for_collection
+ [{ project: :namespace }, :assignees]
+ end
+ end
+end
diff --git a/app/services/issue_links/create_service.rb b/app/services/issue_links/create_service.rb
new file mode 100644
index 00000000000..63762b1af79
--- /dev/null
+++ b/app/services/issue_links/create_service.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module IssueLinks
+ class CreateService < IssuableLinks::CreateService
+ # rubocop: disable CodeReuse/ActiveRecord
+ def relate_issuables(referenced_issue)
+ link = IssueLink.find_or_initialize_by(source: issuable, target: referenced_issue)
+
+ set_link_type(link)
+
+ if link.changed? && link.save
+ create_notes(referenced_issue)
+ end
+
+ link
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def linkable_issuables(issues)
+ @linkable_issuables ||= begin
+ issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
+ end
+ end
+
+ def create_notes(referenced_issue)
+ SystemNoteService.relate_issue(issuable, referenced_issue, current_user)
+ SystemNoteService.relate_issue(referenced_issue, issuable, current_user)
+ end
+
+ def previous_related_issuables
+ @related_issues ||= issuable.related_issues(current_user).to_a
+ end
+
+ private
+
+ def set_link_type(_link)
+ # EE only
+ end
+
+ def track_event
+ track_incident_action(current_user, issuable, :incident_relate)
+ end
+ end
+end
+
+IssueLinks::CreateService.prepend_if_ee('EE::IssueLinks::CreateService')
diff --git a/app/services/issue_links/destroy_service.rb b/app/services/issue_links/destroy_service.rb
new file mode 100644
index 00000000000..25a45fc697b
--- /dev/null
+++ b/app/services/issue_links/destroy_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module IssueLinks
+ class DestroyService < IssuableLinks::DestroyService
+ private
+
+ def source
+ @source ||= link.source
+ end
+
+ def target
+ @target ||= link.target
+ end
+
+ def permission_to_remove_relation?
+ can?(current_user, :admin_issue_link, source) && can?(current_user, :admin_issue_link, target)
+ end
+
+ def create_notes
+ SystemNoteService.unrelate_issue(source, target, current_user)
+ SystemNoteService.unrelate_issue(target, source, current_user)
+ end
+
+ def track_event
+ track_incident_action(current_user, target, :incident_unrelate)
+ end
+ end
+end
diff --git a/app/services/issue_links/list_service.rb b/app/services/issue_links/list_service.rb
new file mode 100644
index 00000000000..5ac731d3fbc
--- /dev/null
+++ b/app/services/issue_links/list_service.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module IssueLinks
+ class ListService < IssuableLinks::ListService
+ extend ::Gitlab::Utils::Override
+
+ private
+
+ def child_issuables
+ issuable.related_issues(current_user, preload: preload_for_collection)
+ end
+
+ override :serializer
+ def serializer
+ LinkedProjectIssueSerializer
+ end
+ end
+end
diff --git a/app/services/issue_rebalancing_service.rb b/app/services/issue_rebalancing_service.rb
new file mode 100644
index 00000000000..4138c6441c8
--- /dev/null
+++ b/app/services/issue_rebalancing_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+class IssueRebalancingService
+ MAX_ISSUE_COUNT = 10_000
+ TooManyIssues = Class.new(StandardError)
+
+ def initialize(issue)
+ @issue = issue
+ @base = Issue.relative_positioning_query_base(issue)
+ end
+
+ def execute
+ gates = [issue.project, issue.project.group].compact
+ return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) }
+
+ raise TooManyIssues, "#{issue_count} issues" if issue_count > MAX_ISSUE_COUNT
+
+ start = RelativePositioning::START_POSITION - (gaps / 2) * gap_size
+
+ Issue.transaction do
+ indexed_ids.each_slice(100) { |pairs| assign_positions(start, pairs) }
+ end
+ end
+
+ private
+
+ attr_reader :issue, :base
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def indexed_ids
+ base.reorder(:relative_position, :id).pluck(:id).each_with_index
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def assign_positions(start, positions)
+ values = positions.map do |id, index|
+ "(#{id}, #{start + (index * gap_size)})"
+ end.join(', ')
+
+ Issue.connection.exec_query(<<~SQL, "rebalance issue positions")
+ WITH cte(cte_id, new_pos) AS (
+ SELECT *
+ FROM (VALUES #{values}) as t (id, pos)
+ )
+ UPDATE #{Issue.table_name}
+ SET relative_position = cte.new_pos
+ FROM cte
+ WHERE cte_id = id
+ SQL
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def issue_count
+ @issue_count ||= base.count
+ end
+
+ def gaps
+ issue_count - 1
+ end
+
+ def gap_size
+ # We could try to split the available range over the number of gaps we need,
+ # but IDEAL_DISTANCE * MAX_ISSUE_COUNT is only 0.1% of the available range,
+ # so we are guaranteed not to exhaust it by using this static value.
+ #
+ # If we raise MAX_ISSUE_COUNT or IDEAL_DISTANCE significantly, this may
+ # change!
+ RelativePositioning::IDEAL_DISTANCE
+ end
+end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 9e72f6dad8d..0ed2b08b7b1 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -2,6 +2,8 @@
module Issues
class BaseService < ::IssuableBaseService
+ include IncidentManagement::UsageData
+
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
@@ -17,6 +19,19 @@ module Issues
Issues::CloseService
end
+ NO_REBALANCING_NEEDED = ((RelativePositioning::MIN_POSITION * 0.9999)..(RelativePositioning::MAX_POSITION * 0.9999)).freeze
+
+ def rebalance_if_needed(issue)
+ return unless issue
+ return if issue.relative_position.nil?
+ return if NO_REBALANCING_NEEDED.cover?(issue.relative_position)
+
+ gates = [issue.project, issue.project.group].compact
+ return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) }
+
+ IssueRebalancingWorker.perform_async(nil, issue.project_id)
+ end
+
private
def create_assignee_note(issue, old_assignees)
@@ -46,6 +61,22 @@ module Issues
Milestones::IssuesCountService.new(milestone).delete_cache
end
+
+ # Applies label "incident" (creates it if missing) to incident issues.
+ # Please use in "after" hooks only to ensure we are not appyling
+ # labels prematurely.
+ def add_incident_label(issue)
+ return unless issue.incident?
+
+ label = ::IncidentManagement::CreateIncidentLabelService
+ .new(project, current_user)
+ .execute
+ .payload[:label]
+
+ return if issue.label_ids.include?(label.id)
+
+ issue.labels << label
+ end
end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index e431c766df8..c3677de015f 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -37,6 +37,7 @@ module Issues
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
+ track_incident_action(current_user, issue, :incident_closed)
store_first_mentioned_in_commit_at(issue, closed_via) if closed_via.is_a?(MergeRequest)
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index c0194f5b847..fb7683f940d 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -5,31 +5,33 @@ module Issues
include SpamCheckMethods
include ResolveDiscussions
- def execute
+ def execute(skip_system_notes: false)
@issue = BuildService.new(project, current_user, params).execute
filter_spam_check_params
filter_resolve_discussion_params
- create(@issue)
+ create(@issue, skip_system_notes: skip_system_notes)
end
def before_create(issue)
spam_check(issue, current_user, action: :create)
- issue.move_to_end
# current_user (defined in BaseService) is not available within run_after_commit block
user = current_user
issue.run_after_commit do
NewIssueWorker.perform_async(issue.id, user.id)
+ IssuePlacementWorker.perform_async(nil, issue.project_id)
end
end
- def after_create(issuable)
- todo_service.new_issue(issuable, current_user)
+ def after_create(issue)
+ add_incident_label(issue)
+ todo_service.new_issue(issue, current_user)
user_agent_detail_service.create
- resolve_discussions_with_issue(issuable)
- delete_milestone_total_issue_counter_cache(issuable.milestone)
+ resolve_discussions_with_issue(issue)
+ delete_milestone_total_issue_counter_cache(issue.milestone)
+ track_incident_action(current_user, issue, :incident_created)
super
end
diff --git a/app/services/issues/duplicate_service.rb b/app/services/issues/duplicate_service.rb
index c936d75e277..feb496542c8 100644
--- a/app/services/issues/duplicate_service.rb
+++ b/app/services/issues/duplicate_service.rb
@@ -12,6 +12,8 @@ module Issues
close_service.new(project, current_user, {}).execute(duplicate_issue)
duplicate_issue.update(duplicated_to: canonical_issue)
+
+ relate_two_issues(duplicate_issue, canonical_issue)
end
private
@@ -23,7 +25,10 @@ module Issues
def create_issue_canonical_note(canonical_issue, duplicate_issue)
SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue)
end
+
+ def relate_two_issues(duplicate_issue, canonical_issue)
+ params = { target_issuable: canonical_issue }
+ IssueLinks::CreateService.new(duplicate_issue, current_user, params).execute
+ end
end
end
-
-Issues::DuplicateService.prepend_if_ee('EE::Issues::DuplicateService')
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index ce1466307e1..60e0d1eec3d 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -38,6 +38,7 @@ module Issues
def update_old_entity
super
+ rewrite_related_issues
mark_as_moved
end
@@ -51,13 +52,24 @@ module Issues
}
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
- CreateService.new(@target_project, @current_user, new_params).execute
+
+ # Skip creation of system notes for existing attributes of the issue. The system notes of the old
+ # issue are copied over so we don't want to end up with duplicate notes.
+ CreateService.new(@target_project, @current_user, new_params).execute(skip_system_notes: true)
end
def mark_as_moved
original_entity.update(moved_to: new_entity)
end
+ def rewrite_related_issues
+ source_issue_links = IssueLink.for_source_issue(original_entity)
+ source_issue_links.update_all(source_id: new_entity.id)
+
+ target_issue_links = IssueLink.for_target_issue(original_entity)
+ target_issue_links.update_all(target_id: new_entity.id)
+ end
+
def notify_participants
notification_service.async.issue_moved(original_entity, new_entity, @current_user)
end
diff --git a/app/services/issues/related_branches_service.rb b/app/services/issues/related_branches_service.rb
index 46076218857..98d8412102f 100644
--- a/app/services/issues/related_branches_service.rb
+++ b/app/services/issues/related_branches_service.rb
@@ -24,7 +24,7 @@ module Issues
return unless target
- pipeline = project.pipeline_for(branch_name, target.sha)
+ pipeline = project.latest_pipeline(branch_name, target.sha)
pipeline.detailed_status(current_user) if can?(current_user, :read_pipeline, pipeline)
end
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 0ffe33dd317..e2b1b5400c7 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -13,6 +13,7 @@ module Issues
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
delete_milestone_closed_issue_counter_cache(issue.milestone)
+ track_incident_action(current_user, issue, :incident_reopened)
end
issue
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index ac7baba3b7c..ce21b2e0275 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -22,6 +22,7 @@ module Issues
end
def after_update(issue)
+ add_incident_label(issue)
IssuesChannel.broadcast_to(issue, event: 'updated') if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
end
@@ -44,12 +45,14 @@ module Issues
create_assignee_note(issue, old_assignees)
notification_service.async.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_assignable(issue, current_user, old_assignees)
+ track_incident_action(current_user, issue, :incident_assigned)
end
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
create_confidentiality_note(issue)
+ track_usage_event(:incident_management_incident_change_confidential, current_user.id)
end
added_labels = issue.labels - old_labels
@@ -83,6 +86,7 @@ module Issues
raise ActiveRecord::RecordNotFound unless issue_before || issue_after
issue.move_between(issue_before, issue_after)
+ rebalance_if_needed(issue)
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb
index 9572cf50564..1384e2f83b2 100644
--- a/app/services/issues/zoom_link_service.rb
+++ b/app/services/issues/zoom_link_service.rb
@@ -60,6 +60,7 @@ module Issues
if @issue.persisted?
# Save the meeting directly since we only want to update one meeting, not all
zoom_meeting.save
+ track_incident_action(current_user, issue, :incident_zoom_meeting)
success(message: _('Zoom meeting added'))
else
success(message: _('Zoom meeting added'), payload: { zoom_meetings: [zoom_meeting] })
diff --git a/app/services/jira_connect/sync_service.rb b/app/services/jira_connect/sync_service.rb
new file mode 100644
index 00000000000..07a648bb8c9
--- /dev/null
+++ b/app/services/jira_connect/sync_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class SyncService
+ def initialize(project)
+ self.project = project
+ end
+
+ def execute(commits: nil, branches: nil, merge_requests: nil)
+ JiraConnectInstallation.for_project(project).each do |installation|
+ client = Atlassian::JiraConnect::Client.new(installation.base_url, installation.shared_secret)
+
+ response = client.store_dev_info(project: project, commits: commits, branches: branches, merge_requests: merge_requests)
+
+ log_response(response)
+ end
+ end
+
+ private
+
+ attr_accessor :project
+
+ def log_response(response)
+ message = {
+ message: 'response from jira dev_info api',
+ integration: 'JiraConnect',
+ project_id: project.id,
+ project_path: project.full_path,
+ jira_response: response&.to_json
+ }
+
+ if response && response['errorMessages']
+ logger.error(message)
+ else
+ logger.info(message)
+ end
+ end
+
+ def logger
+ Gitlab::ProjectServiceLogger
+ end
+ end
+end
diff --git a/app/services/jira_connect_subscriptions/base_service.rb b/app/services/jira_connect_subscriptions/base_service.rb
new file mode 100644
index 00000000000..0e5bb91660e
--- /dev/null
+++ b/app/services/jira_connect_subscriptions/base_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module JiraConnectSubscriptions
+ class BaseService < ::BaseService
+ attr_accessor :jira_connect_installation, :current_user, :params
+
+ def initialize(jira_connect_installation, user = nil, params = {})
+ @jira_connect_installation, @current_user, @params = jira_connect_installation, user, params.dup
+ end
+ end
+end
diff --git a/app/services/jira_connect_subscriptions/create_service.rb b/app/services/jira_connect_subscriptions/create_service.rb
new file mode 100644
index 00000000000..8e794d3acf7
--- /dev/null
+++ b/app/services/jira_connect_subscriptions/create_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module JiraConnectSubscriptions
+ class CreateService < ::JiraConnectSubscriptions::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute
+ unless namespace && can?(current_user, :create_jira_connect_subscription, namespace)
+ return error('Invalid namespace. Please make sure you have sufficient permissions', 401)
+ end
+
+ create_subscription
+ end
+
+ private
+
+ def create_subscription
+ subscription = JiraConnectSubscription.new(installation: jira_connect_installation, namespace: namespace)
+
+ if subscription.save
+ success
+ else
+ error(subscription.errors.full_messages.join(', '), 422)
+ end
+ end
+
+ def namespace
+ strong_memoize(:namespace) do
+ Namespace.find_by_full_path(params[:namespace_path])
+ end
+ end
+ end
+end
diff --git a/app/services/lfs/push_service.rb b/app/services/lfs/push_service.rb
new file mode 100644
index 00000000000..6e1a11ebff8
--- /dev/null
+++ b/app/services/lfs/push_service.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Lfs
+ # Lfs::PushService pushes the LFS objects associated with a project to a
+ # remote URL
+ class PushService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ # Match the canonical LFS client's batch size:
+ # https://github.com/git-lfs/git-lfs/blob/master/tq/transfer_queue.go#L19
+ BATCH_SIZE = 100
+
+ def execute
+ lfs_objects_relation.each_batch(of: BATCH_SIZE) do |objects|
+ push_objects(objects)
+ end
+
+ success
+ rescue => err
+ error(err.message)
+ end
+
+ private
+
+ # Currently we only set repository_type for design repository objects, so
+ # push mirroring must send objects with a `nil` repository type - but if the
+ # wiki repository uses LFS, its objects will also be sent. This will be
+ # addressed by https://gitlab.com/gitlab-org/gitlab/-/issues/250346
+ def lfs_objects_relation
+ project.lfs_objects_for_repository_types(nil, :project)
+ end
+
+ def push_objects(objects)
+ rsp = lfs_client.batch('upload', objects)
+ objects = objects.index_by(&:oid)
+
+ rsp.fetch('objects', []).each do |spec|
+ actions = spec['actions']
+ object = objects[spec['oid']]
+
+ upload_object!(object, spec) if actions&.key?('upload')
+ verify_object!(object, spec) if actions&.key?('verify')
+ end
+ end
+
+ def upload_object!(object, spec)
+ authenticated = spec['authenticated']
+ upload = spec.dig('actions', 'upload')
+
+ # The server wants us to upload the object but something is wrong
+ unless object && object.size == spec['size'].to_i
+ log_error("Couldn't match object #{spec['oid']}/#{spec['size']}")
+ return
+ end
+
+ lfs_client.upload(object, upload, authenticated: authenticated)
+ end
+
+ def verify_object!(object, spec)
+ # TODO: the remote has requested that we make another call to verify that
+ # the object has been sent correctly.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/250654
+ log_error("LFS upload verification requested, but not supported for #{object.oid}")
+ end
+
+ def url
+ params.fetch(:url)
+ end
+
+ def credentials
+ params.fetch(:credentials)
+ end
+
+ def lfs_client
+ strong_memoize(:lfs_client) do
+ Gitlab::Lfs::Client.new(url, credentials: credentials)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 7e301f311e9..abc3f99797d 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -23,6 +23,8 @@ module MergeRequests
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)
+
+ enqueue_jira_connect_messages_for(merge_request)
end
def cleanup_environments(merge_request)
@@ -52,6 +54,14 @@ module MergeRequests
private
+ def enqueue_jira_connect_messages_for(merge_request)
+ return unless project.jira_subscription_exists?
+
+ if Atlassian::JiraIssueKeyExtractor.has_keys?(merge_request.title, merge_request.description)
+ JiraConnect::SyncMergeRequestWorker.perform_async(merge_request.id)
+ end
+ end
+
def create(merge_request)
self.params = assign_allowed_merge_params(merge_request, params)
@@ -87,6 +97,28 @@ module MergeRequests
unless merge_request.can_allow_collaboration?(current_user)
params.delete(:allow_collaboration)
end
+
+ filter_reviewer(merge_request)
+ end
+
+ def filter_reviewer(merge_request)
+ return if params[:reviewer_ids].blank?
+
+ unless can_admin_issuable?(merge_request) && merge_request.allows_reviewers?
+ params.delete(:reviewer_ids)
+
+ return
+ end
+
+ reviewer_ids = params[:reviewer_ids].select { |reviewer_id| user_can_read?(merge_request, reviewer_id) }
+
+ if params[:reviewer_ids].map(&:to_s) == [IssuableFinder::Params::NONE]
+ params[:reviewer_ids] = []
+ elsif reviewer_ids.any?
+ params[:reviewer_ids] = reviewer_ids
+ else
+ params.delete(:reviewer_ids)
+ end
end
def merge_request_metrics_service(merge_request)
diff --git a/app/services/merge_requests/cleanup_refs_service.rb b/app/services/merge_requests/cleanup_refs_service.rb
new file mode 100644
index 00000000000..0f03f5f09b4
--- /dev/null
+++ b/app/services/merge_requests/cleanup_refs_service.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class CleanupRefsService
+ include BaseServiceUtility
+
+ TIME_THRESHOLD = 14.days
+
+ attr_reader :merge_request
+
+ def self.schedule(merge_request)
+ MergeRequestCleanupRefsWorker.perform_in(TIME_THRESHOLD, merge_request.id)
+ end
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ @repository = merge_request.project.repository
+ @ref_path = merge_request.ref_path
+ @merge_ref_path = merge_request.merge_ref_path
+ @ref_head_sha = @repository.commit(merge_request.ref_path).id
+ @merge_ref_sha = merge_request.merge_ref_head&.id
+ end
+
+ def execute
+ return error("Merge request has not been closed nor merged for #{TIME_THRESHOLD.inspect}.") unless eligible?
+
+ # Ensure that commit shas of refs are kept around so we won't lose them when GC runs.
+ keep_around
+
+ return error('Failed to create keep around refs.') unless kept_around?
+ return error('Failed to cache merge ref sha.') unless cache_merge_ref_sha
+
+ delete_refs
+ success
+ end
+
+ private
+
+ attr_reader :repository, :ref_path, :merge_ref_path, :ref_head_sha, :merge_ref_sha
+
+ def eligible?
+ return met_time_threshold?(merge_request.metrics&.latest_closed_at) if merge_request.closed?
+
+ merge_request.merged? && met_time_threshold?(merge_request.metrics&.merged_at)
+ end
+
+ def met_time_threshold?(attr)
+ attr.nil? || attr.to_i <= TIME_THRESHOLD.ago.to_i
+ end
+
+ def kept_around?
+ service = Gitlab::Git::KeepAround.new(repository)
+
+ [ref_head_sha, merge_ref_sha].compact.all? do |sha|
+ service.kept_around?(sha)
+ end
+ end
+
+ def keep_around
+ repository.keep_around(ref_head_sha, merge_ref_sha)
+ end
+
+ def cache_merge_ref_sha
+ return true if merge_ref_sha.nil?
+
+ # Caching the merge ref sha is needed before we delete the merge ref so
+ # we can still show the merge ref diff (via `MergeRequest#merge_ref_head`)
+ merge_request.update_column(:merge_ref_sha, merge_ref_sha)
+ end
+
+ def delete_refs
+ repository.delete_refs(ref_path, merge_ref_path)
+ end
+ end
+end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index c2174d2a130..b0a7face594 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -2,6 +2,8 @@
module MergeRequests
class CloseService < MergeRequests::BaseService
+ include RemovesRefs
+
def execute(merge_request, commit = nil)
return merge_request unless can?(current_user, :update_merge_request, merge_request)
@@ -19,6 +21,7 @@ module MergeRequests
merge_request.update_project_counter_caches
cleanup_environments(merge_request)
abort_auto_merge(merge_request, 'merge request was closed')
+ cleanup_refs(merge_request)
end
merge_request
diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb
index f9352f10fea..46c4c102091 100644
--- a/app/services/merge_requests/create_pipeline_service.rb
+++ b/app/services/merge_requests/create_pipeline_service.rb
@@ -48,12 +48,18 @@ module MergeRequests
end
def can_create_pipeline_in_target_project?(merge_request)
- if Gitlab::Ci::Features.allow_to_create_merge_request_pipelines_in_target_project?(merge_request.target_project)
- can?(current_user, :create_pipeline, merge_request.target_project)
- else
+ if Gitlab::Ci::Features.disallow_to_create_merge_request_pipelines_in_target_project?(merge_request.target_project)
merge_request.for_same_project?
+ else
+ can?(current_user, :create_pipeline, merge_request.target_project) &&
+ can_update_source_branch_in_target_project?(merge_request)
end
end
+
+ def can_update_source_branch_in_target_project?(merge_request)
+ ::Gitlab::UserAccess.new(current_user, container: merge_request.target_project)
+ .can_update_branch?(merge_request.source_branch_ref)
+ end
end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 961a7cb1ef6..437e87dadf7 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -10,13 +10,14 @@ module MergeRequests
class MergeService < MergeRequests::MergeBaseService
delegate :merge_jid, :state, to: :@merge_request
- def execute(merge_request)
+ def execute(merge_request, options = {})
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project, current_user, params).execute(merge_request)
return
end
@merge_request = merge_request
+ @options = options
validate!
@@ -55,7 +56,7 @@ module MergeRequests
error =
if @merge_request.should_be_rebased?
'Only fast-forward merge is allowed for your project. Please update your source branch'
- elsif !@merge_request.mergeable?
+ elsif !@merge_request.mergeable?(skip_discussions_check: @options[:skip_discussions_check])
'Merge request is not mergeable'
elsif !@merge_request.squash && project.squash_always?
'This project requires squashing commits when merge requests are accepted.'
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index fdf8f442297..1c78fca3c26 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
# and execute all hooks and notifications
#
class PostMergeService < MergeRequests::BaseService
+ include RemovesRefs
+
def execute(merge_request)
merge_request.mark_as_merged
close_issues(merge_request)
@@ -20,6 +22,7 @@ module MergeRequests
delete_non_latest_diffs(merge_request)
cancel_review_app_jobs!(merge_request)
cleanup_environments(merge_request)
+ cleanup_refs(merge_request)
end
private
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 56a91fa0305..405b8fe9c9e 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -2,6 +2,7 @@
module MergeRequests
class RefreshService < MergeRequests::BaseService
+ include Gitlab::Utils::StrongMemoize
attr_reader :push
def execute(oldrev, newrev, ref)
@@ -23,25 +24,37 @@ module MergeRequests
post_merge_manually_merged
link_forks_lfs_objects
reload_merge_requests
- outdate_suggestions
- refresh_pipelines_on_merge_requests
- abort_auto_merges
+
+ merge_requests_for_source_branch.each do |mr|
+ outdate_suggestions(mr)
+ refresh_pipelines_on_merge_requests(mr)
+ abort_auto_merges(mr)
+ mark_pending_todos_done(mr)
+ end
+
abort_ff_merge_requests_with_when_pipeline_succeeds
- mark_pending_todos_done
cache_merge_requests_closing_issues
- # Leave a system note if a branch was deleted/added
- if @push.branch_added? || @push.branch_removed?
- comment_mr_branch_presence_changed
- end
+ merge_requests_for_source_branch.each do |mr|
+ # Leave a system note if a branch was deleted/added
+ if branch_added_or_removed?
+ comment_mr_branch_presence_changed(mr)
+ end
- notify_about_push
- mark_mr_as_wip_from_commits
- execute_mr_web_hooks
+ notify_about_push(mr)
+ mark_mr_as_wip_from_commits(mr)
+ execute_mr_web_hooks(mr)
+ end
true
end
+ def branch_added_or_removed?
+ strong_memoize(:branch_added_or_removed) do
+ @push.branch_added? || @push.branch_removed?
+ end
+ end
+
def close_upon_missing_source_branch_ref
# MergeRequest#reload_diff ignores not opened MRs. This means it won't
# create an `empty` diff for `closed` MRs without a source branch, keeping
@@ -140,25 +153,22 @@ module MergeRequests
merge_request.source_branch == @push.branch_name
end
- def outdate_suggestions
- outdate_service = Suggestions::OutdateService.new
+ def outdate_suggestions(merge_request)
+ outdate_service.execute(merge_request)
+ end
- merge_requests_for_source_branch.each do |merge_request|
- outdate_service.execute(merge_request)
- end
+ def outdate_service
+ @outdate_service ||= Suggestions::OutdateService.new
end
- def refresh_pipelines_on_merge_requests
- merge_requests_for_source_branch.each do |merge_request|
- create_pipeline_for(merge_request, current_user)
- UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
- end
+ def refresh_pipelines_on_merge_requests(merge_request)
+ create_pipeline_for(merge_request, current_user)
+
+ UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
- def abort_auto_merges
- merge_requests_for_source_branch.each do |merge_request|
- abort_auto_merge(merge_request, 'source branch was updated')
- end
+ def abort_auto_merges(merge_request)
+ abort_auto_merge(merge_request, 'source branch was updated')
end
def abort_ff_merge_requests_with_when_pipeline_succeeds
@@ -187,10 +197,8 @@ module MergeRequests
.with_auto_merge_enabled
end
- def mark_pending_todos_done
- merge_requests_for_source_branch.each do |merge_request|
- todo_service.merge_request_push(merge_request, @current_user)
- end
+ def mark_pending_todos_done(merge_request)
+ todo_service.merge_request_push(merge_request, @current_user)
end
def find_new_commits
@@ -218,62 +226,54 @@ module MergeRequests
end
# Add comment about branches being deleted or added to merge requests
- def comment_mr_branch_presence_changed
+ def comment_mr_branch_presence_changed(merge_request)
presence = @push.branch_added? ? :add : :delete
- merge_requests_for_source_branch.each do |merge_request|
- SystemNoteService.change_branch_presence(
- merge_request, merge_request.project, @current_user,
- :source, @push.branch_name, presence)
- end
+ SystemNoteService.change_branch_presence(
+ merge_request, merge_request.project, @current_user,
+ :source, @push.branch_name, presence)
end
# Add comment about pushing new commits to merge requests and send nofitication emails
- def notify_about_push
+ def notify_about_push(merge_request)
return unless @commits.present?
- merge_requests_for_source_branch.each do |merge_request|
- mr_commit_ids = Set.new(merge_request.commit_shas)
+ mr_commit_ids = Set.new(merge_request.commit_shas)
- new_commits, existing_commits = @commits.partition do |commit|
- mr_commit_ids.include?(commit.id)
- end
+ new_commits, existing_commits = @commits.partition do |commit|
+ mr_commit_ids.include?(commit.id)
+ end
- SystemNoteService.add_commits(merge_request, merge_request.project,
- @current_user, new_commits,
- existing_commits, @push.oldrev)
+ SystemNoteService.add_commits(merge_request, merge_request.project,
+ @current_user, new_commits,
+ existing_commits, @push.oldrev)
- notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
- end
+ notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
end
- def mark_mr_as_wip_from_commits
+ def mark_mr_as_wip_from_commits(merge_request)
return unless @commits.present?
- merge_requests_for_source_branch.each do |merge_request|
- commit_shas = merge_request.commit_shas
+ commit_shas = merge_request.commit_shas
- wip_commit = @commits.detect do |commit|
- commit.work_in_progress? && commit_shas.include?(commit.sha)
- end
+ wip_commit = @commits.detect do |commit|
+ commit.work_in_progress? && commit_shas.include?(commit.sha)
+ end
- if wip_commit && !merge_request.work_in_progress?
- merge_request.update(title: merge_request.wip_title)
- SystemNoteService.add_merge_request_wip_from_commit(
- merge_request,
- merge_request.project,
- @current_user,
- wip_commit
- )
- end
+ if wip_commit && !merge_request.work_in_progress?
+ merge_request.update(title: merge_request.wip_title)
+ SystemNoteService.add_merge_request_wip_from_commit(
+ merge_request,
+ merge_request.project,
+ @current_user,
+ wip_commit
+ )
end
end
# Call merge request webhook with update branches
- def execute_mr_web_hooks
- merge_requests_for_source_branch.each do |merge_request|
- execute_hooks(merge_request, 'update', old_rev: @push.oldrev)
- end
+ def execute_mr_web_hooks(merge_request)
+ execute_hooks(merge_request, 'update', old_rev: @push.oldrev)
end
# If the merge requests closes any issues, save this information in the
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index cf02158b629..1468bfd6bb6 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -24,8 +24,9 @@ module MergeRequests
old_labels = old_associations.fetch(:labels, [])
old_mentioned_users = old_associations.fetch(:mentioned_users, [])
old_assignees = old_associations.fetch(:assignees, [])
+ old_reviewers = old_associations.fetch(:reviewers, [])
- if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees)
+ if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees, old_reviewers: old_reviewers)
todo_service.resolve_todos_for_target(merge_request, current_user)
end
@@ -44,6 +45,8 @@ module MergeRequests
handle_assignees_change(merge_request, old_assignees) if merge_request.assignees != old_assignees
+ handle_reviewers_change(merge_request, old_reviewers) if merge_request.reviewers != old_reviewers
+
if merge_request.previous_changes.include?('target_branch') ||
merge_request.previous_changes.include?('source_branch')
merge_request.mark_as_unchecked
@@ -108,6 +111,10 @@ module MergeRequests
todo_service.reassigned_assignable(merge_request, current_user, old_assignees)
end
+ def handle_reviewers_change(merge_request, old_reviewers)
+ todo_service.reassigned_reviewable(merge_request, current_user, old_reviewers)
+ end
+
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
diff --git a/app/services/metrics/dashboard/custom_metric_embed_service.rb b/app/services/metrics/dashboard/custom_metric_embed_service.rb
index 229bd17f5cf..eff1db21aff 100644
--- a/app/services/metrics/dashboard/custom_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/custom_metric_embed_service.rb
@@ -31,7 +31,7 @@ module Metrics
# A group title is valid if it is one of the limited
# options the user can select in the UI.
def valid_group_title?(group)
- PrometheusMetricEnums
+ Enums::PrometheusMetric
.custom_group_details
.map { |_, details| details[:group_title] }
.include?(group)
@@ -99,12 +99,12 @@ module Metrics
# Returns a symbol representing the group that
# the dashboard's group title belongs to.
# It will be one of the keys found under
- # PrometheusMetricEnums.custom_groups.
+ # Enums::PrometheusMetric.custom_groups.
#
# @return [String]
def group_key
strong_memoize(:group_key) do
- PrometheusMetricEnums
+ Enums::PrometheusMetric
.group_details
.find { |_, details| details[:group_title] == group }
.first
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 4f2329a42f2..e26f662a697 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -2,6 +2,8 @@
module Notes
class CreateService < ::Notes::BaseService
+ include IncidentManagement::UsageData
+
def execute
note = Notes::BuildService.new(project, current_user, params.except(:merge_request_diff_head_sha)).execute
@@ -62,6 +64,7 @@ module Notes
clear_noteable_diffs_cache(note)
Suggestions::CreateService.new(note).execute
increment_usage_counter(note)
+ track_event(note, current_user)
if Feature.enabled?(:notes_create_service_tracking, project)
Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note))
@@ -104,5 +107,11 @@ module Notes
value: note.id
}
end
+
+ def track_event(note, user)
+ return unless note.noteable.is_a?(Issue) && note.noteable.incident?
+
+ track_usage_event(:incident_management_incident_comment, user.id)
+ end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 909a0033d12..731d72c41d4 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -35,6 +35,12 @@ class NotificationService
@async ||= Async.new(self)
end
+ def disabled_two_factor(user)
+ return unless user.can?(:receive_notifications)
+
+ mailer.disabled_two_factor_email(user).deliver_later
+ end
+
# Always notify user about ssh key added
# only if ssh key is not deploy key
#
diff --git a/app/services/packages/composer/create_package_service.rb b/app/services/packages/composer/create_package_service.rb
index ad5d267698b..7e16fc78599 100644
--- a/app/services/packages/composer/create_package_service.rb
+++ b/app/services/packages/composer/create_package_service.rb
@@ -2,7 +2,7 @@
module Packages
module Composer
- class CreatePackageService < BaseService
+ class CreatePackageService < ::Packages::CreatePackageService
include ::Gitlab::Utils::StrongMemoize
def execute
@@ -21,10 +21,7 @@ module Packages
private
def created_package
- project
- .packages
- .composer
- .safe_find_or_create_by!(name: package_name, version: package_version)
+ find_or_create_package!(:composer, name: package_name, version: package_version)
end
def composer_json
diff --git a/app/services/packages/conan/create_package_service.rb b/app/services/packages/conan/create_package_service.rb
index 22a0436c5fb..35046d8776e 100644
--- a/app/services/packages/conan/create_package_service.rb
+++ b/app/services/packages/conan/create_package_service.rb
@@ -2,12 +2,11 @@
module Packages
module Conan
- class CreatePackageService < BaseService
+ class CreatePackageService < ::Packages::CreatePackageService
def execute
- project.packages.create!(
+ create_package!(:conan,
name: params[:package_name],
version: params[:package_version],
- package_type: :conan,
conan_metadatum_attributes: {
package_username: params[:package_username],
package_channel: params[:package_channel]
diff --git a/app/services/packages/create_package_service.rb b/app/services/packages/create_package_service.rb
new file mode 100644
index 00000000000..397a5f74e0a
--- /dev/null
+++ b/app/services/packages/create_package_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Packages
+ class CreatePackageService < BaseService
+ protected
+
+ def find_or_create_package!(package_type, name: params[:name], version: params[:version])
+ project
+ .packages
+ .with_package_type(package_type)
+ .safe_find_or_create_by!(name: name, version: version) do |pkg|
+ pkg.creator = package_creator
+ end
+ end
+
+ def create_package!(package_type, attrs = {})
+ project
+ .packages
+ .with_package_type(package_type)
+ .create!(package_attrs(attrs))
+ end
+
+ private
+
+ def package_attrs(attrs)
+ {
+ creator: package_creator,
+ name: params[:name],
+ version: params[:version]
+ }.merge(attrs)
+ end
+
+ def package_creator
+ current_user if current_user.is_a?(User)
+ end
+ end
+end
diff --git a/app/services/packages/maven/create_package_service.rb b/app/services/packages/maven/create_package_service.rb
index aca5d28ca98..3df17021499 100644
--- a/app/services/packages/maven/create_package_service.rb
+++ b/app/services/packages/maven/create_package_service.rb
@@ -1,15 +1,12 @@
# frozen_string_literal: true
module Packages
module Maven
- class CreatePackageService < BaseService
+ class CreatePackageService < ::Packages::CreatePackageService
def execute
app_group, _, app_name = params[:name].rpartition('/')
app_group.tr!('/', '.')
- package = project.packages.create!(
- name: params[:name],
- version: params[:version],
- package_type: :maven,
+ package = create_package!(:maven,
maven_metadatum_attributes: {
path: params[:path],
app_group: app_group,
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index cf927683ce9..7f868b71734 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -1,24 +1,21 @@
# frozen_string_literal: true
module Packages
module Npm
- class CreatePackageService < BaseService
+ class CreatePackageService < ::Packages::CreatePackageService
include Gitlab::Utils::StrongMemoize
def execute
return error('Version is empty.', 400) if version.blank?
return error('Package already exists.', 403) if current_package_exists?
+ return error('File is too large.', 400) if file_size_exceeded?
- ActiveRecord::Base.transaction { create_package! }
+ ActiveRecord::Base.transaction { create_npm_package! }
end
private
- def create_package!
- package = project.packages.create!(
- name: name,
- version: version,
- package_type: 'npm'
- )
+ def create_npm_package!
+ package = create_package!(:npm, name: name, version: version)
if build.present?
package.create_build_info!(pipeline: build.pipeline)
@@ -86,6 +83,10 @@ module Packages
_version, versions_data = params[:versions].first
versions_data
end
+
+ def file_size_exceeded?
+ project.actual_limits.exceeded?(:npm_max_file_size, attachment['length'].to_i)
+ end
end
end
end
diff --git a/app/services/packages/nuget/create_dependency_service.rb b/app/services/packages/nuget/create_dependency_service.rb
index 2be5db732f6..19143fe3778 100644
--- a/app/services/packages/nuget/create_dependency_service.rb
+++ b/app/services/packages/nuget/create_dependency_service.rb
@@ -41,7 +41,7 @@ module Packages
}
end
- ::Gitlab::Database.bulk_insert(::Packages::Nuget::DependencyLinkMetadatum.table_name, rows.compact)
+ ::Gitlab::Database.bulk_insert(::Packages::Nuget::DependencyLinkMetadatum.table_name, rows.compact) # rubocop:disable Gitlab/BulkInsert
end
def raw_dependency_for(dependency)
diff --git a/app/services/packages/nuget/create_package_service.rb b/app/services/packages/nuget/create_package_service.rb
index 68ad7f028e4..3999ccd3347 100644
--- a/app/services/packages/nuget/create_package_service.rb
+++ b/app/services/packages/nuget/create_package_service.rb
@@ -2,12 +2,12 @@
module Packages
module Nuget
- class CreatePackageService < BaseService
+ class CreatePackageService < ::Packages::CreatePackageService
TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package'
PACKAGE_VERSION = '0.0.0'
def execute
- project.packages.nuget.create!(
+ create_package!(:nuget,
name: TEMPORARY_PACKAGE_NAME,
version: "#{PACKAGE_VERSION}-#{uuid}"
)
diff --git a/app/services/packages/pypi/create_package_service.rb b/app/services/packages/pypi/create_package_service.rb
index 1313fc80e33..c49efca0fc5 100644
--- a/app/services/packages/pypi/create_package_service.rb
+++ b/app/services/packages/pypi/create_package_service.rb
@@ -2,16 +2,22 @@
module Packages
module Pypi
- class CreatePackageService < BaseService
+ class CreatePackageService < ::Packages::CreatePackageService
include ::Gitlab::Utils::StrongMemoize
def execute
::Packages::Package.transaction do
- Packages::Pypi::Metadatum.upsert(
- package_id: created_package.id,
+ meta = Packages::Pypi::Metadatum.new(
+ package: created_package,
required_python: params[:requires_python]
)
+ unless meta.valid?
+ raise ActiveRecord::RecordInvalid.new(meta)
+ end
+
+ Packages::Pypi::Metadatum.upsert(meta.attributes)
+
::Packages::CreatePackageFileService.new(created_package, file_params).execute
end
end
@@ -20,10 +26,7 @@ module Packages
def created_package
strong_memoize(:created_package) do
- project
- .packages
- .pypi
- .safe_find_or_create_by!(name: params[:name], version: params[:version])
+ find_or_create_package!(:pypi)
end
end
diff --git a/app/services/packages/update_tags_service.rb b/app/services/packages/update_tags_service.rb
index da50cd3479e..adfad52910e 100644
--- a/app/services/packages/update_tags_service.rb
+++ b/app/services/packages/update_tags_service.rb
@@ -15,7 +15,7 @@ module Packages
tags_to_create = @tags - existing_tags
@package.tags.with_name(tags_to_destroy).delete_all if tags_to_destroy.any?
- ::Gitlab::Database.bulk_insert(Packages::Tag.table_name, rows(tags_to_create)) if tags_to_create.any?
+ ::Gitlab::Database.bulk_insert(Packages::Tag.table_name, rows(tags_to_create)) if tags_to_create.any? # rubocop:disable Gitlab/BulkInsert
end
private
diff --git a/app/services/pages/delete_service.rb b/app/services/pages/delete_service.rb
index 7408943e78c..fc5d01a93a1 100644
--- a/app/services/pages/delete_service.rb
+++ b/app/services/pages/delete_service.rb
@@ -3,8 +3,7 @@
module Pages
class DeleteService < BaseService
def execute
- project.remove_pages
- project.pages_domains.destroy_all # rubocop: disable Cop/DestroyAll
+ PagesRemoveWorker.perform_async(project.id)
end
end
end
diff --git a/app/services/product_analytics/build_activity_graph_service.rb b/app/services/product_analytics/build_activity_graph_service.rb
new file mode 100644
index 00000000000..63108d76afd
--- /dev/null
+++ b/app/services/product_analytics/build_activity_graph_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ProductAnalytics
+ class BuildActivityGraphService < BuildGraphService
+ def execute
+ timerange = @params[:timerange].days
+
+ results = product_analytics_events.count_collector_tstamp_by_day(timerange)
+
+ format_results('collector_tstamp', results.transform_keys(&:to_date))
+ end
+ end
+end
diff --git a/app/services/product_analytics/build_graph_service.rb b/app/services/product_analytics/build_graph_service.rb
index 31f9f093bb9..da54ad4de0e 100644
--- a/app/services/product_analytics/build_graph_service.rb
+++ b/app/services/product_analytics/build_graph_service.rb
@@ -13,15 +13,19 @@ module ProductAnalytics
results = product_analytics_events.count_by_graph(graph, timerange)
+ format_results(graph, results)
+ end
+
+ private
+
+ def format_results(name, results)
{
- id: graph,
+ id: name,
keys: results.keys,
values: results.values
}
end
- private
-
def product_analytics_events
@project.product_analytics_events
end
diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb
index 2a35a07d555..a2cdb87e631 100644
--- a/app/services/projects/after_rename_service.rb
+++ b/app/services/projects/after_rename_service.rb
@@ -96,9 +96,19 @@ module Projects
.rename_project(path_before, project_path, namespace_full_path)
end
- Gitlab::PagesTransfer
- .new
- .rename_project(path_before, project_path, namespace_full_path)
+ if project.pages_deployed?
+ # Block will be evaluated in the context of project so we need
+ # to bind to a local variable to capture it, as the instance
+ # variable and method aren't available on Project
+ path_before_local = @path_before
+
+ project.run_after_commit_or_now do
+ Gitlab::PagesTransfer
+ .new
+ .async
+ .rename_project(path_before_local, path, namespace.full_path)
+ end
+ end
end
def log_completion
@@ -110,8 +120,7 @@ module Projects
def migrate_to_hashed_storage?
Gitlab::CurrentSettings.hashed_storage_enabled? &&
- project.storage_upgradable? &&
- Feature.disabled?(:skip_hashed_storage_upgrade)
+ project.storage_upgradable?
end
def send_move_instructions?
diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb
index f883c8c7bd8..bfce5f1ad63 100644
--- a/app/services/projects/alerting/notify_service.rb
+++ b/app/services/projects/alerting/notify_service.rb
@@ -42,12 +42,39 @@ module Projects
end
def process_existing_alert(alert)
- alert.register_new_event!
+ if am_alert_params[:ended_at].present?
+ process_resolved_alert(alert)
+ else
+ alert.register_new_event!
+ end
+
+ alert
+ end
+
+ def process_resolved_alert(alert)
+ return unless auto_close_incident?
+
+ if alert.resolve(am_alert_params[:ended_at])
+ close_issue(alert.issue)
+ end
+
+ alert
+ end
+
+ def close_issue(issue)
+ return if issue.blank? || issue.closed?
+
+ ::Issues::CloseService
+ .new(project, User.alert_bot)
+ .execute(issue, system_note: false)
+
+ SystemNoteService.auto_resolve_prometheus_alert(issue, project, User.alert_bot) if issue.reset.closed?
end
def create_alert
- alert = AlertManagement::Alert.create(am_alert_params)
+ alert = AlertManagement::Alert.create(am_alert_params.except(:ended_at))
alert.execute_services if alert.persisted?
+ SystemNoteService.create_new_alert(alert, 'Generic Alert Endpoint')
alert
end
diff --git a/app/services/projects/container_repository/gitlab/delete_tags_service.rb b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
index 18049648e26..cee94b994a3 100644
--- a/app/services/projects/container_repository/gitlab/delete_tags_service.rb
+++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
@@ -5,6 +5,11 @@ module Projects
module Gitlab
class DeleteTagsService
include BaseServiceUtility
+ include ::Gitlab::Utils::StrongMemoize
+
+ DISABLED_TIMEOUTS = [nil, 0].freeze
+
+ TimeoutError = Class.new(StandardError)
def initialize(container_repository, tag_names)
@container_repository = container_repository
@@ -17,12 +22,42 @@ module Projects
def execute
return success(deleted: []) if @tag_names.empty?
+ delete_tags
+ rescue TimeoutError => e
+ ::Gitlab::ErrorTracking.track_exception(e, tags_count: @tag_names&.size, container_repository_id: @container_repository&.id)
+ error('timeout while deleting tags')
+ end
+
+ private
+
+ def delete_tags
+ start_time = Time.zone.now
+
deleted_tags = @tag_names.select do |name|
+ raise TimeoutError if timeout?(start_time)
+
@container_repository.delete_tag_by_name(name)
end
deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags')
end
+
+ def timeout?(start_time)
+ return false unless throttling_enabled?
+ return false if service_timeout.in?(DISABLED_TIMEOUTS)
+
+ (Time.zone.now - start_time) > service_timeout
+ end
+
+ def throttling_enabled?
+ strong_memoize(:feature_flag) do
+ Feature.enabled?(:container_registry_expiration_policies_throttling)
+ end
+ end
+
+ def service_timeout
+ ::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout
+ end
end
end
end
diff --git a/app/services/projects/container_repository/third_party/delete_tags_service.rb b/app/services/projects/container_repository/third_party/delete_tags_service.rb
index 6504172109e..404642acf72 100644
--- a/app/services/projects/container_repository/third_party/delete_tags_service.rb
+++ b/app/services/projects/container_repository/third_party/delete_tags_service.rb
@@ -15,7 +15,7 @@ module Projects
# This is a hack as the registry doesn't support deleting individual
# tags. This code effectively pushes a dummy image and assigns the tag to it.
# This way when the tag is deleted only the dummy image is affected.
- # This is used to preverse compatibility with third-party registries that
+ # This is used to preserve compatibility with third-party registries that
# don't support fast delete.
# See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
def execute
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 33ed1151407..68b40fdd8f1 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -114,8 +114,13 @@ module Projects
# completes), and any other affected users in the background
def setup_authorizations
if @project.group
- current_user.project_authorizations.create!(project: @project,
- access_level: @project.group.max_member_access_for_user(current_user))
+ group_access_level = @project.group.max_member_access_for_user(current_user,
+ only_concrete_membership: true)
+
+ if group_access_level > GroupMember::NO_ACCESS
+ current_user.project_authorizations.create!(project: @project,
+ access_level: group_access_level)
+ end
if Feature.enabled?(:specialized_project_authorization_workers)
AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 37487261f2c..bec75657530 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -28,7 +28,7 @@ module Projects
Projects::UnlinkForkService.new(project, current_user).execute
- attempt_destroy_transaction(project)
+ attempt_destroy(project)
system_hook_service.execute_hooks_for(project, :destroy)
log_info("Project \"#{project.full_path}\" was deleted")
@@ -98,29 +98,35 @@ module Projects
log_error("Deletion failed on #{project.full_path} with the following message: #{message}")
end
- def attempt_destroy_transaction(project)
+ def attempt_destroy(project)
unless remove_registry_tags
raise_error(s_('DeleteProject|Failed to remove some tags in project container registry. Please try again or contact administrator.'))
end
project.leave_pool_repository
- Project.transaction do
- log_destroy_event
- trash_relation_repositories!
- trash_project_repositories!
-
- # Rails attempts to load all related records into memory before
- # destroying: https://github.com/rails/rails/issues/22510
- # This ensures we delete records in batches.
- #
- # Exclude container repositories because its before_destroy would be
- # called multiple times, and it doesn't destroy any database records.
- project.destroy_dependent_associations_in_batches(exclude: [:container_repositories, :snippets])
- project.destroy!
+ if Gitlab::Ci::Features.project_transactionless_destroy?(project)
+ destroy_project_related_records(project)
+ else
+ Project.transaction { destroy_project_related_records(project) }
end
end
+ def destroy_project_related_records(project)
+ log_destroy_event
+ trash_relation_repositories!
+ trash_project_repositories!
+
+ # Rails attempts to load all related records into memory before
+ # destroying: https://github.com/rails/rails/issues/22510
+ # This ensures we delete records in batches.
+ #
+ # Exclude container repositories because its before_destroy would be
+ # called multiple times, and it doesn't destroy any database records.
+ project.destroy_dependent_associations_in_batches(exclude: [:container_repositories, :snippets])
+ project.destroy!
+ end
+
def log_destroy_event
log_info("Attempting to destroy #{project.full_path} (#{project.id})")
end
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index aba175eb79b..9810db84605 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -27,7 +27,7 @@ module Projects
end
def http?(url)
- url =~ /\A#{URI.regexp(%w(http https))}\z/
+ url =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w(http https))}\z/
end
def valid_domain?(url)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index bb660d47887..0b4963e356a 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -10,8 +10,8 @@ module Projects
forked_project
end
- def valid_fork_targets
- @valid_fork_targets ||= ForkTargetsFinder.new(@project, current_user).execute
+ def valid_fork_targets(options = {})
+ @valid_fork_targets ||= ForkTargetsFinder.new(@project, current_user).execute(options)
end
def valid_fork_target?(namespace = target_namespace)
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index 52c73bcff03..d6e5b825e13 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -6,6 +6,9 @@ module Projects
class LfsDownloadService < BaseService
SizeError = Class.new(StandardError)
OidError = Class.new(StandardError)
+ ResponseError = Class.new(StandardError)
+
+ LARGE_FILE_SIZE = 1.megabytes
attr_reader :lfs_download_object
delegate :oid, :size, :credentials, :sanitized_url, to: :lfs_download_object, prefix: :lfs
@@ -19,6 +22,7 @@ module Projects
def execute
return unless project&.lfs_enabled? && lfs_download_object
return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid?
+ return link_existing_lfs_object! if Feature.enabled?(:lfs_link_existing_object, project, default_enabled: true) && lfs_size > LARGE_FILE_SIZE && lfs_object
wrap_download_errors do
download_lfs_file!
@@ -29,7 +33,7 @@ module Projects
def wrap_download_errors(&block)
yield
- rescue SizeError, OidError, StandardError => e
+ rescue SizeError, OidError, ResponseError, StandardError => e
error("LFS file with oid #{lfs_oid} could't be downloaded from #{lfs_sanitized_url}: #{e.message}")
end
@@ -56,15 +60,13 @@ module Projects
def download_and_save_file!(file)
digester = Digest::SHA256.new
- response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment|
+ fetch_file do |fragment|
digester << fragment
file.write(fragment)
raise_size_error! if file.size > lfs_size
end
- raise StandardError, "Received error code #{response.code}" unless response.success?
-
raise_size_error! if file.size != lfs_size
raise_oid_error! if digester.hexdigest != lfs_oid
end
@@ -78,6 +80,12 @@ module Projects
end
end
+ def fetch_file(&block)
+ response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers, &block)
+
+ raise ResponseError, "Received error code #{response.code}" unless response.success?
+ end
+
def with_tmp_file
create_tmp_storage_dir
@@ -123,6 +131,29 @@ module Projects
super
end
+
+ def lfs_object
+ @lfs_object ||= LfsObject.find_by_oid(lfs_oid)
+ end
+
+ def link_existing_lfs_object!
+ existing_file = lfs_object.file.open
+ buffer_size = 0
+ result = fetch_file do |fragment|
+ unless fragment == existing_file.read(fragment.size)
+ break error("LFS file with oid #{lfs_oid} cannot be linked with an existing LFS object")
+ end
+
+ buffer_size += fragment.size
+ break success if buffer_size > LARGE_FILE_SIZE
+ end
+
+ project.lfs_objects << lfs_object
+
+ result
+ ensure
+ existing_file&.close
+ end
end
end
end
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index 82632d63e5b..dc450311db2 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -68,10 +68,12 @@ module Projects
# Check https://gitlab.com/gitlab-org/gitlab-foss/issues/38418 description.
# rubocop: disable CodeReuse/ActiveRecord
def self.query(projects, public_only: true)
+ issues_filtered_by_type = Issue.opened.with_issue_type(Issue::TYPES_FOR_LIST)
+
if public_only
- Issue.opened.public_only.where(project: projects)
+ issues_filtered_by_type.public_only.where(project: projects)
else
- Issue.opened.where(project: projects)
+ issues_filtered_by_type.where(project: projects)
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb
deleted file mode 100644
index 54d09b354a1..00000000000
--- a/app/services/projects/propagate_service_template.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- class PropagateServiceTemplate
- BATCH_SIZE = 100
-
- delegate :data_fields_present?, to: :template
-
- def self.propagate(template)
- new(template).propagate
- end
-
- def initialize(template)
- @template = template
- end
-
- def propagate
- return unless template.active?
-
- propagate_projects_with_template
- end
-
- private
-
- attr_reader :template
-
- def propagate_projects_with_template
- loop do
- batch = Project.uncached { Project.ids_without_integration(template, BATCH_SIZE) }
-
- bulk_create_from_template(batch) unless batch.empty?
-
- break if batch.size < BATCH_SIZE
- end
- end
-
- def bulk_create_from_template(batch)
- service_list = ServiceList.new(batch, service_hash).to_array
-
- Project.transaction do
- results = bulk_insert(*service_list)
-
- if data_fields_present?
- data_list = DataList.new(results, data_fields_hash, template.data_fields.class).to_array
-
- bulk_insert(*data_list)
- end
-
- run_callbacks(batch)
- end
- end
-
- def bulk_insert(klass, columns, values_array)
- items_to_insert = values_array.map { |array| Hash[columns.zip(array)] }
-
- klass.insert_all(items_to_insert, returning: [:id])
- end
-
- def service_hash
- @service_hash ||= template.to_service_hash
- end
-
- def data_fields_hash
- @data_fields_hash ||= template.to_data_fields_hash
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def run_callbacks(batch)
- if template.issue_tracker?
- Project.where(id: batch).update_all(has_external_issue_tracker: true)
- end
-
- if active_external_wiki?
- Project.where(id: batch).update_all(has_external_wiki: true)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def active_external_wiki?
- template.type == 'ExternalWikiService'
- end
- end
-end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 0fb70feec86..dba5177718d 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -88,15 +88,14 @@ module Projects
# Move uploads
move_project_uploads(project)
- # Move pages
- Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
-
project.old_path_with_namespace = @old_path
update_repository_configuration(@new_path)
execute_system_hooks
end
+
+ move_pages(project)
rescue Exception # rubocop:disable Lint/RescueException
rollback_side_effects
raise
@@ -181,6 +180,13 @@ module Projects
)
end
+ def move_pages(project)
+ return unless project.pages_deployed?
+
+ transfer = Gitlab::PagesTransfer.new.async
+ transfer.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
+ end
+
def old_wiki_repo_path
"#{old_path}#{::Gitlab::GlRepository::WIKI.path_suffix}"
end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index b3cf27373cd..52aea8c51a5 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -2,21 +2,13 @@
module Projects
class UnlinkForkService < BaseService
- # If a fork is given, it:
- #
- # - Saves LFS objects to the root project
- # - Close existing MRs coming from it
- # - Is removed from the fork network
- #
- # If a root of fork(s) is given, it does the same,
- # but not updating LFS objects (there'll be no related root to cache it).
+ # Close existing MRs coming from the project and remove it from the fork network
def execute
fork_network = @project.fork_network
+ forked_from = @project.forked_from_project
return unless fork_network
- save_lfs_objects
-
merge_requests = fork_network
.merge_requests
.opened
@@ -41,7 +33,7 @@ module Projects
# When the project getting out of the network is a node with parent
# and children, both the parent and the node needs a cache refresh.
- [@project.forked_from_project, @project].compact.each do |project|
+ [forked_from, @project].compact.each do |project|
refresh_forks_count(project)
end
end
@@ -51,22 +43,5 @@ module Projects
def refresh_forks_count(project)
Projects::ForksCountService.new(project).refresh_cache
end
-
- # TODO: Remove this method once all LfsObjectsProject records are backfilled
- # for forks.
- #
- # See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info.
- def save_lfs_objects
- return unless @project.forked?
-
- lfs_storage_project = @project.lfs_storage_project
-
- return unless lfs_storage_project
- return if lfs_storage_project == @project # that project is being unlinked
-
- lfs_storage_project.lfs_objects.find_each do |lfs_object|
- lfs_object.projects << @project unless lfs_object.projects.include?(@project)
- end
- end
end
end
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
index 88c17d502df..67d388dc8a3 100644
--- a/app/services/projects/update_pages_configuration_service.rb
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -22,9 +22,6 @@ module Projects
end
success
- rescue => e
- Gitlab::ErrorTracking.track_exception(e)
- error(e.message, pass_back: { exception: e })
end
private
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 334f5993d15..ea37f2e4ec0 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -52,7 +52,7 @@ module Projects
def success
@status.success
- @project.mark_pages_as_deployed
+ @project.mark_pages_as_deployed(artifacts_archive: build.job_artifacts_archive)
super
end
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index 7961f689259..5c41f00aac2 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -31,6 +31,9 @@ module Projects
remote_mirror.update_start!
remote_mirror.ensure_remote!
+ # LFS objects must be sent first, or the push has dangling pointers
+ send_lfs_objects!(remote_mirror)
+
response = remote_mirror.update_repository
if response.divergent_refs.any?
@@ -43,6 +46,23 @@ module Projects
end
end
+ def send_lfs_objects!(remote_mirror)
+ return unless Feature.enabled?(:push_mirror_syncs_lfs, project)
+ return unless project.lfs_enabled?
+
+ # TODO: Support LFS sync over SSH
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/249587
+ return unless remote_mirror.url =~ /\Ahttps?:\/\//i
+ return unless remote_mirror.password_auth?
+
+ Lfs::PushService.new(
+ project,
+ current_user,
+ url: remote_mirror.bare_url,
+ credentials: remote_mirror.credentials
+ ).execute
+ end
+
def retry_or_fail(mirror, message, tries)
if tries < MAX_TRIES
mirror.mark_for_retry!(message)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index c9ba7cde199..bb430811497 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -144,11 +144,7 @@ module Projects
def update_pages_config
return unless project.pages_deployed?
- if Feature.enabled?(:async_update_pages_config, project)
- PagesUpdateConfigurationWorker.perform_async(project.id)
- else
- Projects::UpdatePagesConfigurationService.new(project).execute
- end
+ PagesUpdateConfigurationWorker.perform_async(project.id)
end
def changing_pages_https_only?
diff --git a/app/services/prometheus/proxy_service.rb b/app/services/prometheus/proxy_service.rb
index 33635796771..c1bafd03b48 100644
--- a/app/services/prometheus/proxy_service.rb
+++ b/app/services/prometheus/proxy_service.rb
@@ -44,8 +44,8 @@ module Prometheus
def self.from_cache(proxyable_class_name, proxyable_id, method, path, params)
proxyable_class = begin
proxyable_class_name.constantize
- rescue NameError
- nil
+ rescue NameError
+ nil
end
return unless proxyable_class
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index a781eacc40e..de1cd7cd981 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -10,6 +10,7 @@ module QuickActions
include Gitlab::QuickActions::MergeRequestActions
include Gitlab::QuickActions::CommitActions
include Gitlab::QuickActions::CommonActions
+ include Gitlab::QuickActions::RelateActions
attr_reader :quick_action_target
diff --git a/app/services/resource_events/change_state_service.rb b/app/services/resource_events/change_state_service.rb
index 202972c1efd..cd6d82df46f 100644
--- a/app/services/resource_events/change_state_service.rb
+++ b/app/services/resource_events/change_state_service.rb
@@ -13,14 +13,14 @@ module ResourceEvents
ResourceStateEvent.create(
user: user,
- issue: issue,
- merge_request: merge_request,
+ resource.class.underscore => resource,
source_commit: commit_id_of(mentionable_source),
source_merge_request_id: merge_request_id_of(mentionable_source),
state: ResourceStateEvent.states[state],
close_after_error_tracking_resolve: close_after_error_tracking_resolve,
close_auto_resolve_prometheus_alert: close_auto_resolve_prometheus_alert,
- created_at: Time.zone.now)
+ created_at: Time.zone.now
+ )
resource.expire_note_etag_cache
end
@@ -56,17 +56,5 @@ module ResourceEvents
mentionable_source.id
end
-
- def issue
- return unless resource.is_a?(Issue)
-
- resource
- end
-
- def merge_request
- return unless resource.is_a?(MergeRequest)
-
- resource
- end
end
end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index c841cbfaa00..fab02697cf0 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -5,16 +5,16 @@ module Search
include Gitlab::Utils::StrongMemoize
attr_accessor :current_user, :params
- attr_reader :default_project_filter
def initialize(user, params)
@current_user, @params = user, params.dup
- @default_project_filter = true
end
def execute
- Gitlab::SearchResults.new(current_user, projects, params[:search],
- default_project_filter: default_project_filter)
+ Gitlab::SearchResults.new(current_user,
+ params[:search],
+ projects,
+ filters: { state: params[:state] })
end
def projects
diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb
index 4dbd9eb14bb..68778aa2768 100644
--- a/app/services/search/group_service.rb
+++ b/app/services/search/group_service.rb
@@ -7,13 +7,16 @@ module Search
def initialize(user, group, params)
super(user, params)
- @default_project_filter = false
@group = group
end
def execute
Gitlab::GroupSearchResults.new(
- current_user, projects, group, params[:search], default_project_filter: default_project_filter
+ current_user,
+ params[:search],
+ projects,
+ group: group,
+ filters: { state: params[:state] }
)
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index 17a322c2665..5eba909c23b 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -10,9 +10,10 @@ module Search
def execute
Gitlab::ProjectSearchResults.new(current_user,
- project,
params[:search],
- params[:repository_ref])
+ project: project,
+ repository_ref: params[:repository_ref],
+ filters: { state: params[:state] })
end
def scope
diff --git a/app/services/snippets/base_service.rb b/app/services/snippets/base_service.rb
index d9e8326f159..53a04e5a398 100644
--- a/app/services/snippets/base_service.rb
+++ b/app/services/snippets/base_service.rb
@@ -46,7 +46,7 @@ module Snippets
snippet.errors.add(:snippet_actions, 'have invalid data')
end
- snippet_error_response(snippet, 403)
+ snippet_error_response(snippet, 422)
end
def snippet_error_response(snippet, http_status)
diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb
index dab47de8a36..5c9b2eb1aea 100644
--- a/app/services/snippets/create_service.rb
+++ b/app/services/snippets/create_service.rb
@@ -82,7 +82,7 @@ module Snippets
def create_commit
commit_attrs = {
- branch_name: 'master',
+ branch_name: @snippet.default_branch,
message: 'Initial commit'
}
diff --git a/app/services/snippets/destroy_service.rb b/app/services/snippets/destroy_service.rb
index 977626fcf17..f1f80dbaf86 100644
--- a/app/services/snippets/destroy_service.rb
+++ b/app/services/snippets/destroy_service.rb
@@ -58,3 +58,5 @@ module Snippets
end
end
end
+
+Snippets::DestroyService.prepend_if_ee('EE::Snippets::DestroyService')
diff --git a/app/services/snippets/repository_validation_service.rb b/app/services/snippets/repository_validation_service.rb
index c8197795383..5bf5e692ef4 100644
--- a/app/services/snippets/repository_validation_service.rb
+++ b/app/services/snippets/repository_validation_service.rb
@@ -39,7 +39,7 @@ module Snippets
def check_branch_name_default!
branches = repository.branch_names
- return if branches.first == Gitlab::Checks::SnippetCheck::DEFAULT_BRANCH
+ return if branches.first == snippet.default_branch
raise RepositoryValidationError, _('Repository has an invalid default branch name.')
end
@@ -51,7 +51,7 @@ module Snippets
end
def check_file_count!
- file_count = repository.ls_files(nil).size
+ file_count = repository.ls_files(snippet.default_branch).size
limit = Snippet.max_file_limit(current_user)
if file_count > limit
diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb
index 00146389e22..a0e9ab6ffda 100644
--- a/app/services/snippets/update_service.rb
+++ b/app/services/snippets/update_service.rb
@@ -93,7 +93,7 @@ module Snippets
raise UpdateError unless snippet.snippet_repository
commit_attrs = {
- branch_name: 'master',
+ branch_name: snippet.default_branch,
message: 'Update snippet'
}
diff --git a/app/services/static_site_editor/config_service.rb b/app/services/static_site_editor/config_service.rb
new file mode 100644
index 00000000000..987ee071976
--- /dev/null
+++ b/app/services/static_site_editor/config_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module StaticSiteEditor
+ class ConfigService < ::BaseContainerService
+ ValidationError = Class.new(StandardError)
+
+ def execute
+ @project = container
+ check_access!
+
+ ServiceResponse.success(payload: data)
+ rescue ValidationError => e
+ ServiceResponse.error(message: e.message)
+ end
+
+ private
+
+ attr_reader :project
+
+ def check_access!
+ unless can?(current_user, :download_code, project)
+ raise ValidationError, 'Insufficient permissions to read configuration'
+ end
+ end
+
+ def data
+ check_for_duplicate_keys!
+ generated_data.merge(file_data)
+ end
+
+ def generated_data
+ @generated_data ||= Gitlab::StaticSiteEditor::Config::GeneratedConfig.new(
+ project.repository,
+ params.fetch(:ref),
+ params.fetch(:path),
+ params[:return_url]
+ ).data
+ end
+
+ def file_data
+ @file_data ||= Gitlab::StaticSiteEditor::Config::FileConfig.new.data
+ end
+
+ def check_for_duplicate_keys!
+ duplicate_keys = generated_data.keys & file_data.keys
+ raise ValidationError.new("Duplicate key(s) '#{duplicate_keys}' found.") if duplicate_keys.present?
+ end
+ end
+end
diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index 9191943caa7..2fbeaf4405c 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -51,11 +51,11 @@ class SubmitUsagePingService
end
def store_metrics(response)
- metrics = response['conv_index'] || response['dev_ops_score']
+ metrics = response['conv_index'] || response['dev_ops_score'] # leaving dev_ops_score here, as the response data comes from the gitlab-version-com
return unless metrics.present?
- DevOpsScore::Metric.create!(
+ DevOpsReport::Metric.create!(
metrics.slice(*METRICS)
)
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 6702596f17c..df042fdc393 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -41,8 +41,12 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_assignees(old_assignees)
end
- def change_milestone(noteable, project, author, milestone)
- ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_milestone(milestone)
+ def relate_issue(noteable, noteable_ref, user)
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref)
+ end
+
+ def unrelate_issue(noteable, noteable_ref, user)
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issue(noteable_ref)
end
# Called when the due_date of a Noteable is changed
@@ -300,6 +304,10 @@ module SystemNoteService
::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project, author: author).new_alert_issue(issue)
end
+ def create_new_alert(alert, monitoring_tool)
+ ::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project).create_new_alert(monitoring_tool)
+ end
+
private
def merge_requests_service(noteable, project, author)
diff --git a/app/services/system_notes/alert_management_service.rb b/app/services/system_notes/alert_management_service.rb
index f835376727a..376f2c1cfbf 100644
--- a/app/services/system_notes/alert_management_service.rb
+++ b/app/services/system_notes/alert_management_service.rb
@@ -2,6 +2,21 @@
module SystemNotes
class AlertManagementService < ::SystemNotes::BaseService
+ # Called when the a new AlertManagement::Alert has been created
+ #
+ # alert - AlertManagement::Alert object.
+ #
+ # Example Note text:
+ #
+ # "GitLab Alert Bot logged an alert from Prometheus"
+ #
+ # Returns the created Note object
+ def create_new_alert(monitoring_tool)
+ body = "logged an alert from **#{monitoring_tool}**"
+
+ create_note(NoteSummary.new(noteable, project, User.alert_bot, body, action: 'new_alert_added'))
+ end
+
# Called when the status of an AlertManagement::Alert has changed
#
# alert - AlertManagement::Alert object.
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 7535db54130..2252503d97e 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -2,6 +2,34 @@
module SystemNotes
class IssuablesService < ::SystemNotes::BaseService
+ #
+ # noteable_ref - Referenced noteable object
+ #
+ # Example Note text:
+ #
+ # "marked this issue as related to gitlab-foss#9001"
+ #
+ # Returns the created Note object
+ def relate_issue(noteable_ref)
+ body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'relate'))
+ end
+
+ #
+ # noteable_ref - Referenced noteable object
+ #
+ # Example Note text:
+ #
+ # "removed the relation with gitlab-foss#9001"
+ #
+ # Returns the created Note object
+ def unrelate_issue(noteable_ref)
+ body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'unrelate'))
+ end
+
# Called when the assignee of a Noteable is changed or removed
#
# assignee - User being assigned, or nil
@@ -16,6 +44,8 @@ module SystemNotes
def change_assignee(assignee)
body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"
+ issue_activity_counter.track_issue_assignee_changed_action(author: author) if noteable.is_a?(Issue)
+
create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
end
@@ -46,25 +76,9 @@ module SystemNotes
body = text_parts.join(' and ')
- create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
- end
-
- # Called when the milestone of a Noteable is changed
- #
- # milestone - Milestone being assigned, or nil
- #
- # Example Note text:
- #
- # "removed milestone"
- #
- # "changed milestone to 7.11"
- #
- # Returns the created Note object
- def change_milestone(milestone)
- format = milestone&.group_milestone? ? :name : :iid
- body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
+ issue_activity_counter.track_issue_assignee_changed_action(author: author) if noteable.is_a?(Issue)
- create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
end
# Called when the title of a Noteable is changed
@@ -86,6 +100,8 @@ module SystemNotes
body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
+ issue_activity_counter.track_issue_title_changed_action(author: author) if noteable.is_a?(Issue)
+
create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
@@ -103,6 +119,8 @@ module SystemNotes
def change_description
body = 'changed the description'
+ issue_activity_counter.track_issue_description_changed_action(author: author) if noteable.is_a?(Issue)
+
create_note(NoteSummary.new(noteable, project, author, body, action: 'description'))
end
@@ -199,9 +217,13 @@ module SystemNotes
if noteable.confidential
body = 'made the issue confidential'
action = 'confidential'
+
+ issue_activity_counter.track_issue_made_confidential_action(author: author) if noteable.is_a?(Issue)
else
body = 'made the issue visible to everyone'
action = 'visible'
+
+ issue_activity_counter.track_issue_made_visible_action(author: author) if noteable.is_a?(Issue)
end
create_note(NoteSummary.new(noteable, project, author, body, action: action))
@@ -343,6 +365,10 @@ module SystemNotes
noteable.respond_to?(:resource_state_events) &&
::Feature.enabled?(:track_resource_state_change_events, noteable.project, default_enabled: true)
end
+
+ def issue_activity_counter
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter
+ end
end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index a3db2ae7947..0eb099753cb 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -8,6 +8,7 @@
# TodoService.new.new_issue(issue, current_user)
#
class TodoService
+ include Gitlab::Utils::UsageData
# When create an issue we should:
#
# * create a todo for assignee if issue is assigned
@@ -57,6 +58,14 @@ class TodoService
create_assignment_todo(issuable, current_user, old_assignees)
end
+ # When we reassign an reviewable object (merge request) we should:
+ #
+ # * create a pending todo for new reviewer if object is assigned
+ #
+ def reassigned_reviewable(issuable, current_user, old_reviewers = [])
+ create_reviewer_todo(issuable, current_user, old_reviewers)
+ end
+
# When create a merge request we should:
#
# * creates a pending todo for assignee if merge request is assigned
@@ -209,6 +218,9 @@ class TodoService
Array(users).map do |user|
next if pending_todos(user, attributes).exists?
+ issue_type = attributes.delete(:issue_type)
+ track_todo_creation(user, issue_type)
+
todo = Todo.create(attributes.merge(user_id: user.id))
user.update_todos_count_cache
todo
@@ -217,6 +229,7 @@ class TodoService
def new_issuable(issuable, author)
create_assignment_todo(issuable, author)
+ create_reviewer_todo(issuable, author) if issuable.allows_reviewers?
create_mention_todos(issuable.project, issuable, author)
end
@@ -250,6 +263,14 @@ class TodoService
end
end
+ def create_reviewer_todo(target, author, old_reviewers = [])
+ if target.reviewers.any?
+ reviewers = target.reviewers - old_reviewers
+ attributes = attributes_for_todo(target.project, target, author, Todo::REVIEW_REQUESTED)
+ create_todos(reviewers, attributes)
+ end
+ end
+
def create_mention_todos(parent, target, author, note = nil, skip_users = [])
# Create Todos for directly addressed users
directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
@@ -282,6 +303,8 @@ class TodoService
if target.is_a?(Commit)
attributes.merge!(target_id: nil, commit_id: target.id)
+ elsif target.is_a?(Issue)
+ attributes[:issue_type] = target.issue_type
end
attributes
@@ -329,6 +352,12 @@ class TodoService
def pending_todos(user, criteria = {})
PendingTodosFinder.new(user, criteria).execute
end
+
+ def track_todo_creation(user, issue_type)
+ return unless issue_type == 'incident'
+
+ track_usage_event(:incident_management_incident_todo, user.id)
+ end
end
TodoService.prepend_if_ee('EE::TodoService')
diff --git a/app/services/two_factor/base_service.rb b/app/services/two_factor/base_service.rb
new file mode 100644
index 00000000000..7d3f63f3442
--- /dev/null
+++ b/app/services/two_factor/base_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module TwoFactor
+ class BaseService
+ include BaseServiceUtility
+
+ attr_reader :current_user, :params, :user
+
+ def initialize(current_user, params = {})
+ @current_user, @params = current_user, params
+ @user = params.delete(:user)
+ end
+ end
+end
diff --git a/app/services/two_factor/destroy_service.rb b/app/services/two_factor/destroy_service.rb
new file mode 100644
index 00000000000..b8bbe215d6e
--- /dev/null
+++ b/app/services/two_factor/destroy_service.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module TwoFactor
+ class DestroyService < ::TwoFactor::BaseService
+ def execute
+ return error(_('You are not authorized to perform this action')) unless can?(current_user, :disable_two_factor, user)
+ return error(_('Two-factor authentication is not enabled for this user')) unless user.two_factor_enabled?
+
+ result = disable_two_factor
+
+ notification_service.disabled_two_factor(user) if result[:status] == :success
+
+ result
+ end
+
+ private
+
+ def disable_two_factor
+ ::Users::UpdateService.new(current_user, user: user).execute do |user|
+ user.disable_two_factor!
+ end
+ end
+ end
+end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index f06f00a5c3f..2fc46f033dd 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -91,7 +91,6 @@ module Users
def signup_params
[
:email,
- :email_confirmation,
:password_automatically_set,
:name,
:first_name,
diff --git a/app/services/users/signup_service.rb b/app/services/users/signup_service.rb
index 1031cec44cb..1087ae76216 100644
--- a/app/services/users/signup_service.rb
+++ b/app/services/users/signup_service.rb
@@ -27,7 +27,7 @@ module Users
def inject_validators
class << @user
validates :role, presence: true
- validates :setup_for_company, inclusion: { in: [true, false], message: :blank }
+ validates :setup_for_company, inclusion: { in: [true, false], message: :blank } if Gitlab.com?
end
end
end
diff --git a/app/services/webauthn/authenticate_service.rb b/app/services/webauthn/authenticate_service.rb
new file mode 100644
index 00000000000..a4513c62c2d
--- /dev/null
+++ b/app/services/webauthn/authenticate_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Webauthn
+ class AuthenticateService < BaseService
+ def initialize(user, device_response, challenge)
+ @user = user
+ @device_response = device_response
+ @challenge = challenge
+ end
+
+ def execute
+ parsed_device_response = Gitlab::Json.parse(@device_response)
+
+ # appid is set for legacy U2F devices, will be used in a future iteration
+ # rp_id = @app_id
+ # unless parsed_device_response['clientExtensionResults'] && parsed_device_response['clientExtensionResults']['appid']
+ # rp_id = URI(@app_id).host
+ # end
+
+ webauthn_credential = WebAuthn::Credential.from_get(parsed_device_response)
+ encoded_raw_id = Base64.strict_encode64(webauthn_credential.raw_id)
+ stored_webauthn_credential = @user.webauthn_registrations.find_by_credential_xid(encoded_raw_id)
+
+ encoder = WebAuthn.configuration.encoder
+
+ if stored_webauthn_credential &&
+ validate_webauthn_credential(webauthn_credential) &&
+ verify_webauthn_credential(webauthn_credential, stored_webauthn_credential, @challenge, encoder)
+
+ stored_webauthn_credential.update!(counter: webauthn_credential.sign_count)
+ return true
+ end
+
+ false
+ rescue JSON::ParserError, WebAuthn::SignCountVerificationError, WebAuthn::Error
+ false
+ end
+
+ ##
+ # Validates that webauthn_credential is syntactically valid
+ #
+ # duplicated from WebAuthn::PublicKeyCredential#verify
+ # which can't be used here as we need to call WebAuthn::AuthenticatorAssertionResponse#verify instead
+ # (which is done in #verify_webauthn_credential)
+ def validate_webauthn_credential(webauthn_credential)
+ webauthn_credential.type == WebAuthn::TYPE_PUBLIC_KEY &&
+ webauthn_credential.raw_id && webauthn_credential.id &&
+ webauthn_credential.raw_id == WebAuthn.standard_encoder.decode(webauthn_credential.id)
+ end
+
+ ##
+ # Verifies that webauthn_credential matches stored_credential with the given challenge
+ #
+ def verify_webauthn_credential(webauthn_credential, stored_credential, challenge, encoder)
+ webauthn_credential.response.verify(
+ encoder.decode(challenge),
+ public_key: encoder.decode(stored_credential.public_key),
+ sign_count: stored_credential.counter)
+ end
+ end
+end
diff --git a/app/services/webauthn/register_service.rb b/app/services/webauthn/register_service.rb
new file mode 100644
index 00000000000..21be22027a8
--- /dev/null
+++ b/app/services/webauthn/register_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Webauthn
+ class RegisterService < BaseService
+ def initialize(user, params, challenge)
+ @user = user
+ @params = params
+ @challenge = challenge
+ end
+
+ def execute
+ registration = WebauthnRegistration.new
+
+ begin
+ webauthn_credential = WebAuthn::Credential.from_create(Gitlab::Json.parse(@params[:device_response]))
+ webauthn_credential.verify(@challenge)
+
+ registration.update(
+ credential_xid: Base64.strict_encode64(webauthn_credential.raw_id),
+ public_key: webauthn_credential.public_key,
+ counter: webauthn_credential.sign_count,
+ name: @params[:name],
+ user: @user
+ )
+ rescue JSON::ParserError
+ registration.errors.add(:base, _('Your WebAuthn device did not send a valid JSON response.'))
+ rescue WebAuthn::Error => e
+ registration.errors.add(:base, e.message)
+ end
+
+ registration
+ end
+ end
+end
diff --git a/app/services/wiki_pages/destroy_service.rb b/app/services/wiki_pages/destroy_service.rb
index ab5abe1c82b..1d566f98760 100644
--- a/app/services/wiki_pages/destroy_service.rb
+++ b/app/services/wiki_pages/destroy_service.rb
@@ -3,11 +3,14 @@
module WikiPages
class DestroyService < WikiPages::BaseService
def execute(page)
- if page&.delete
+ if page.delete
execute_hooks(page)
+ ServiceResponse.success(payload: { page: page })
+ else
+ ServiceResponse.error(
+ message: _('Could not delete wiki page'), payload: { page: page }
+ )
end
-
- page
end
def usage_counter_action
diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb
index 5ac6902e0b0..f2fc6b37c34 100644
--- a/app/services/wiki_pages/update_service.rb
+++ b/app/services/wiki_pages/update_service.rb
@@ -8,9 +8,13 @@ module WikiPages
if page.update(@params)
execute_hooks(page)
+ ServiceResponse.success(payload: { page: page })
+ else
+ ServiceResponse.error(
+ message: _('Could not update wiki page'),
+ payload: { page: page }
+ )
end
-
- page
end
def usage_counter_action
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index ac1f022c63f..d4c74ce277f 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -30,6 +30,8 @@ module ObjectStorage
REMOTE = 2
end
+ SUPPORTED_STORES = [Store::LOCAL, Store::REMOTE].freeze
+
module Extension
# this extension is the glue between the ObjectStorage::Concern and RecordsUploads::Concern
module RecordsUploads
@@ -178,10 +180,14 @@ module ObjectStorage
end
def workhorse_authorize(has_length:, maximum_size: nil)
- if self.object_store_enabled? && self.direct_upload_enabled?
- { RemoteObject: workhorse_remote_upload_options(has_length: has_length, maximum_size: maximum_size) }
- else
- { TempPath: workhorse_local_upload_path }
+ {}.tap do |hash|
+ if self.object_store_enabled? && self.direct_upload_enabled?
+ hash[:RemoteObject] = workhorse_remote_upload_options(has_length: has_length, maximum_size: maximum_size)
+ else
+ hash[:TempPath] = workhorse_local_upload_path
+ end
+
+ hash[:MaximumSize] = maximum_size if maximum_size.present?
end
end
@@ -206,6 +212,20 @@ module ObjectStorage
end
end
+ class OpenFile
+ extend Forwardable
+
+ # Explicitly exclude :path, because rubyzip uses that to detect "real" files.
+ def_delegators :@file, *(Zip::File::IO_METHODS - [:path])
+
+ # Even though :size is not in IO_METHODS, we do need it.
+ def_delegators :@file, :size
+
+ def initialize(file)
+ @file = file
+ end
+ end
+
# allow to configure and overwrite the filename
def filename
@filename || super || file&.filename # rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -255,6 +275,24 @@ module ObjectStorage
end
end
+ def use_open_file(&blk)
+ Tempfile.open(path) do |file|
+ file.unlink
+ file.binmode
+
+ if file_storage?
+ IO.copy_stream(path, file)
+ else
+ streamer = lambda { |chunk, _, _| file.write(chunk) }
+ Excon.get(url, response_block: streamer)
+ end
+
+ file.seek(0, IO::SEEK_SET)
+
+ yield OpenFile.new(file)
+ end
+ end
+
#
# Move the file to another store
#
diff --git a/app/uploaders/terraform/versioned_state_uploader.rb b/app/uploaders/terraform/versioned_state_uploader.rb
new file mode 100644
index 00000000000..be07993da0f
--- /dev/null
+++ b/app/uploaders/terraform/versioned_state_uploader.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Terraform
+ class VersionedStateUploader < StateUploader
+ def filename
+ "#{model.version}.tfstate"
+ end
+
+ def store_dir
+ Gitlab::HashedPath.new(model.uuid, root_hash: project_id)
+ end
+ end
+end
diff --git a/app/validators/feature_flag_strategies_validator.rb b/app/validators/feature_flag_strategies_validator.rb
new file mode 100644
index 00000000000..e542d52c50a
--- /dev/null
+++ b/app/validators/feature_flag_strategies_validator.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+class FeatureFlagStrategiesValidator < ActiveModel::EachValidator
+ STRATEGY_DEFAULT = 'default'.freeze
+ STRATEGY_GRADUALROLLOUTUSERID = 'gradualRolloutUserId'.freeze
+ STRATEGY_USERWITHID = 'userWithId'.freeze
+ # Order key names alphabetically
+ STRATEGIES = {
+ STRATEGY_DEFAULT => [].freeze,
+ STRATEGY_GRADUALROLLOUTUSERID => %w[groupId percentage].freeze,
+ STRATEGY_USERWITHID => ['userIds'].freeze
+ }.freeze
+ USERID_MAX_LENGTH = 256
+
+ def validate_each(record, attribute, value)
+ return unless value
+
+ if value.is_a?(Array) && value.all? { |s| s.is_a?(Hash) }
+ value.each do |strategy|
+ strategy_validations(record, attribute, strategy)
+ end
+ else
+ error(record, attribute, 'must be an array of strategy hashes')
+ end
+ end
+
+ private
+
+ def strategy_validations(record, attribute, strategy)
+ validate_name(record, attribute, strategy) &&
+ validate_parameters_type(record, attribute, strategy) &&
+ validate_parameters_keys(record, attribute, strategy) &&
+ validate_parameters_values(record, attribute, strategy)
+ end
+
+ def validate_name(record, attribute, strategy)
+ STRATEGIES.key?(strategy['name']) || error(record, attribute, 'strategy name is invalid')
+ end
+
+ def validate_parameters_type(record, attribute, strategy)
+ strategy['parameters'].is_a?(Hash) || error(record, attribute, 'parameters are invalid')
+ end
+
+ def validate_parameters_keys(record, attribute, strategy)
+ name, parameters = strategy.values_at('name', 'parameters')
+ actual_keys = parameters.keys.sort
+ expected_keys = STRATEGIES[name]
+ expected_keys == actual_keys || error(record, attribute, 'parameters are invalid')
+ end
+
+ def validate_parameters_values(record, attribute, strategy)
+ case strategy['name']
+ when STRATEGY_GRADUALROLLOUTUSERID
+ gradual_rollout_user_id_parameters_validation(record, attribute, strategy)
+ when STRATEGY_USERWITHID
+ user_with_id_parameters_validation(record, attribute, strategy)
+ end
+ end
+
+ def gradual_rollout_user_id_parameters_validation(record, attribute, strategy)
+ percentage = strategy.dig('parameters', 'percentage')
+ group_id = strategy.dig('parameters', 'groupId')
+
+ unless percentage.is_a?(String) && percentage.match(/\A[1-9]?[0-9]\z|\A100\z/)
+ error(record, attribute, 'percentage must be a string between 0 and 100 inclusive')
+ end
+
+ unless group_id.is_a?(String) && group_id.match(/\A[a-z]{1,32}\z/)
+ error(record, attribute, 'groupId parameter is invalid')
+ end
+ end
+
+ def user_with_id_parameters_validation(record, attribute, strategy)
+ user_ids = strategy.dig('parameters', 'userIds')
+ unless user_ids.is_a?(String) && !user_ids.match(/[\n\r\t]|,,/) && valid_ids?(user_ids.split(","))
+ error(record, attribute, "userIds must be a string of unique comma separated values each #{USERID_MAX_LENGTH} characters or less")
+ end
+ end
+
+ def valid_ids?(user_ids)
+ user_ids.uniq.length == user_ids.length &&
+ user_ids.all? { |id| valid_id?(id) }
+ end
+
+ def valid_id?(user_id)
+ user_id.present? &&
+ user_id.strip == user_id &&
+ user_id.length <= USERID_MAX_LENGTH
+ end
+
+ def error(record, attribute, msg)
+ record.errors.add(attribute, msg)
+ false
+ end
+end
diff --git a/app/validators/feature_flag_user_xids_validator.rb b/app/validators/feature_flag_user_xids_validator.rb
new file mode 100644
index 00000000000..a840993a94b
--- /dev/null
+++ b/app/validators/feature_flag_user_xids_validator.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class FeatureFlagUserXidsValidator < ActiveModel::EachValidator
+ USERXID_MAX_LENGTH = 256
+
+ def validate_each(record, attribute, value)
+ self.class.validate_user_xids(record, attribute, value, attribute)
+ end
+
+ class << self
+ def validate_user_xids(record, attribute, user_xids, error_message_attribute_name)
+ unless user_xids.is_a?(String) && !user_xids.match(/[\n\r\t]|,,/) && valid_xids?(user_xids.split(","))
+ record.errors.add(attribute,
+ "#{error_message_attribute_name} must be a string of unique comma separated values each #{USERXID_MAX_LENGTH} characters or less")
+ end
+ end
+
+ private
+
+ def valid_xids?(user_xids)
+ user_xids.uniq.length == user_xids.length &&
+ user_xids.all? { |xid| valid_xid?(xid) }
+ end
+
+ def valid_xid?(user_xid)
+ user_xid.present? &&
+ user_xid.strip == user_xid &&
+ user_xid.length <= USERXID_MAX_LENGTH
+ end
+ end
+end
diff --git a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
index 995f2ad6616..8fde92d6312 100644
--- a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
+++ b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
@@ -52,67 +52,126 @@
{
"name": "brakeman",
"label": "Brakeman",
- "enabled" : true
+ "enabled" : true,
+ "description": "Ruby on Rails",
+ "variables": [
+ {
+ "field" : "SAST_BRAKEMAN_LEVEL",
+ "label" : "Brakeman confidence level.",
+ "type": "string",
+ "default_value": "1",
+ "value": "",
+ "size": "SMALL",
+ "description": "Ignore Brakeman vulnerabilities under given confidence level. Integer, 1=Low, 2=Medium, 3=High."
+ }
+ ]
},
{
"name": "bandit",
"label": "Bandit",
- "enabled" : true
+ "enabled" : true,
+ "description": "Python",
+ "variables": [
+ {
+ "field" : "SAST_BANDIT_EXCLUDED_PATHS",
+ "label" : "Paths to exclude from scan.",
+ "type": "string",
+ "default_value": "",
+ "value": "",
+ "size": "SMALL",
+ "description": "Comma-separated list of paths to exclude from scan. Uses Python’s 'fnmatch' syntax; For example: '*/tests/*, */venv/*'"
+ }
+ ]
},
{
"name": "eslint",
"label": "ESLint",
- "enabled" : true
+ "enabled" : true,
+ "description": "JavaScript, TypeScript, React",
+ "variables": []
},
{
"name": "flawfinder",
"label": "Flawfinder",
- "enabled" : true
+ "enabled" : true,
+ "description": "C, C++",
+ "variables": [
+ {
+ "field" : "SAST_FLAWFINDER_LEVEL",
+ "label" : "Flawfinder risk level",
+ "type": "string",
+ "default_value": "1",
+ "value": "",
+ "size": "SMALL",
+ "description": "Ignore Flawfinder vulnerabilities under given risk level. Integer, 0=No risk, 5=High risk."
+ }
+ ]
},
{
"name": "kubesec",
"label": "kubesec",
- "enabled" : true
+ "enabled" : true,
+ "description": "Kubernetes manifests, Helm Charts",
+ "variables": []
},
{
- "name": "nodejsscan",
+ "name": "nodejs-scan",
"label": "Node.js Scan",
- "enabled" : true
+ "enabled" : true,
+ "description": "Node.js",
+ "variables": []
},
{
"name": "gosec",
"label": "Golang Security Checker",
- "enabled" : true
+ "enabled" : true,
+ "description": "Go",
+ "variables": [
+ {
+ "field" : "SAST_GOSEC_LEVEL",
+ "label" : "Gosec confidence level",
+ "type": "string",
+ "default_value": "0",
+ "value": "",
+ "size": "SMALL",
+ "description": "Ignore Gosec vulnerabilities under given confidence level. Integer, 0=Undefined, 1=Low, 2=Medium, 3=High."
+ }
+ ]
},
{
"name": "phpcs-security-audit",
"label": "PHP Security Audit",
- "enabled" : true
+ "enabled" : true,
+ "description": "PHP",
+ "variables": []
},
{
"name": "pmd-apex",
"label": "PMD APEX",
- "enabled" : true
+ "enabled" : true,
+ "description": "Apex (Salesforce)",
+ "variables": []
},
{
"name": "security-code-scan",
"label": "Security Code Scan",
- "enabled" : true
+ "enabled" : true,
+ "description": ".NET Core, .NET Framework",
+ "variables": []
},
{
"name": "sobelow",
"label": "Sobelow",
- "enabled" : true
+ "enabled" : true,
+ "description": "Elixir (Phoenix)",
+ "variables": []
},
{
"name": "spotbugs",
"label": "Spotbugs",
- "enabled" : true
- },
- {
- "name": "secrets",
- "label": "Secrets",
- "enabled" : true
+ "enabled" : true,
+ "description": "Groovy, Java, Scala",
+ "variables": []
}
]
}
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index 89a87f38968..5ed9a0d1adb 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -2,33 +2,30 @@
- user = abuse_report.user
%tr
%th.d-block.d-sm-none.d-md-none
- %strong User
+ %strong= _('User')
%td
- if user
= link_to user.name, user
.light.small
- Joined #{time_ago_with_tooltip(user.created_at)}
+ = _('Joined %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(user.created_at) }
- else
- (removed)
+ = _('(removed)')
%td
- %strong.subheading.d-block.d-sm-none.d-md-none Reported by
- - if reporter
- = link_to reporter.name, reporter
- - else
- (removed)
+ %strong.subheading.d-block.d-sm-none.d-md-none
+ = _('Reported by %{reporter}') % { reporter: reporter ? link_to(reporter.name, reporter) : _('(removed)') }
.light.small
= time_ago_with_tooltip(abuse_report.created_at)
%td
- %strong.subheading.d-block.d-sm-none.d-md-none Message
+ %strong.subheading.d-block.d-sm-none.d-md-none= _('Message')
.message
= markdown_field(abuse_report, :message)
%td
- if user
- = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
- data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-sm btn-block btn-remove js-remove-tr"
+ = link_to _('Remove user & report'), admin_abuse_report_path(abuse_report, remove_user: true),
+ data: { confirm: _("USER %{user} WILL BE REMOVED! Are you sure?") % { user: user.name } }, remote: true, method: :delete, class: "btn btn-sm btn-block btn-remove js-remove-tr"
- if user && !user.blocked?
- = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-block"
+ = link_to _('Block user'), block_admin_user_path(user), data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')}, method: :put, class: "btn btn-sm btn-block"
- else
.btn.btn-sm.disabled.btn-block
- Already blocked
- = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr"
+ = _('Already blocked')
+ = link_to _('Remove report'), [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr"
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index fcb1c1a6f3e..ad3795445d1 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -16,7 +16,7 @@
= image_tag @appearance.header_logo_path, 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-inverted btn-remove btn-sm remove-logo"
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm"
%hr
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: ""
@@ -35,7 +35,7 @@
= image_tag @appearance.favicon_path, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
+ = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm"
%hr
= f.hidden_field :favicon_cache
= f.file_field :favicon, class: ''
@@ -67,7 +67,7 @@
= image_tag @appearance.logo_path, class: 'appearance-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm remove-logo"
%hr
= f.hidden_field :logo_cache
= f.file_field :logo, class: ""
@@ -101,7 +101,7 @@
= parsed_with_gfm
.gl-mt-3.gl-mb-3
- = f.submit 'Update appearance settings', class: 'btn btn-success'
+ = f.submit 'Update appearance settings', class: 'btn gl-button btn-success'
- if @appearance.persisted? || @appearance.updated_at
.mt-4
- if @appearance.persisted?
diff --git a/app/views/admin/appearances/_system_header_footer_form.html.haml b/app/views/admin/appearances/_system_header_footer_form.html.haml
index 7f53b2baa32..b50778a1076 100644
--- a/app/views/admin/appearances/_system_header_footer_form.html.haml
+++ b/app/views/admin/appearances/_system_header_footer_form.html.haml
@@ -23,7 +23,7 @@
= _('Add header and footer to emails. Please note that color settings will only be applied within the application interface')
.form-group.js-toggle-colors-container
- %button.btn.btn-link.js-toggle-colors-link{ type: 'button' }
+ %button.btn.gl-button.btn-link.js-toggle-colors-link{ type: 'button' }
= _('Customize colors')
.form-group.js-toggle-colors-container.hide
= form.label :message_background_color, _('Background Color'), class: 'col-form-label label-bold'
diff --git a/app/views/admin/appearances/preview_sign_in.html.haml b/app/views/admin/appearances/preview_sign_in.html.haml
index 2cd95071c73..eec4719c13c 100644
--- a/app/views/admin/appearances/preview_sign_in.html.haml
+++ b/app/views/admin/appearances/preview_sign_in.html.haml
@@ -8,5 +8,5 @@
= label_tag :password
= password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.'
.form-group
- = button_tag "Sign in", class: "btn-success btn"
+ = button_tag "Sign in", class: "btn gl-button btn-success"
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 7051b790fb7..b9cce6c8085 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -34,13 +34,13 @@
= f.number_field :max_artifacts_size, class: 'form-control'
.form-text.text-muted
= _("Set the maximum file size for each job's artifacts")
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size-core-only')
+ = link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
.form-group
= f.label :default_artifacts_expire_in, _('Default artifacts expiration'), class: 'label-bold'
= f.text_field :default_artifacts_expire_in, class: 'form-control'
.form-text.text-muted
= html_escape(_("Set the default expiration time for each job's artifacts. 0 for unlimited. The default unit is in seconds, but you can define an alternative. For example: %{code_open}4 mins 2 sec%{code_close}, %{code_open}2h42min%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration-core-only')
+ = link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
.form-group
= f.label :archive_builds_in_human_readable, _('Archive jobs'), class: 'label-bold'
= f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never'
@@ -58,6 +58,6 @@
= f.text_field :default_ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted
= _("The default CI configuration path for new projects.").html_safe
- = link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'custom-ci-configuration-path'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'custom-ci-configuration-path'), target: '_blank'
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_diff_limits.html.haml b/app/views/admin/application_settings/_diff_limits.html.haml
index 16b7fbe1ab6..1bf25b6a558 100644
--- a/app/views/admin/application_settings/_diff_limits.html.haml
+++ b/app/views/admin/application_settings/_diff_limits.html.haml
@@ -9,7 +9,7 @@
Diff files surpassing this limit will be presented as 'too large'
and won't be expandable.
- = link_to icon('question-circle'),
+ = link_to sprite_icon('question-o'),
help_page_path('user/admin_area/diff_limits',
anchor: 'maximum-diff-patch-size')
.gl-display-flex.gl-justify-content-end
diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
index e82ed0db851..179eb2d5f2e 100644
--- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml
+++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
@@ -19,7 +19,7 @@
= _('Enable classification control using an external service')
%span.form-text.text-muted
= external_authorization_description
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/external_authorization')
+ = link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/external_authorization')
.form-group
= f.label :external_authorization_service_url, _('Service URL'), class: 'label-bold'
= f.text_field :external_authorization_service_url, class: 'form-control'
diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml
new file mode 100644
index 00000000000..bbad5155ada
--- /dev/null
+++ b/app/views/admin/application_settings/_gitpod.html.haml
@@ -0,0 +1,30 @@
+- return unless Gitlab::Gitpod.feature_available?
+- expanded = integration_expanded?('gitpod_')
+- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer')
+
+%section.settings.no-animate#js-gitpod-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Gitpod')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link }
+ = link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
+
+
+ .settings-content
+ = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-gitpod-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :gitpod_enabled, class: 'form-check-input'
+ = f.label :gitpod_enabled, s_('Gitpod|Enable Gitpod integration'), class: 'form-check-label'
+ .form-group
+ = f.label :gitpod_url, s_('Gitpod|Gitpod URL'), class: 'label-bold'
+ = f.text_field :gitpod_url, class: 'form-control', placeholder: s_('Gitpod|e.g. https://gitpod.example.com')
+ .form-text.text-muted
+ = s_('Gitpod|Add the URL to your Gitpod instance configured to read your GitLab projects.')
+ = f.submit s_('Save changes'), class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_grafana.html.haml b/app/views/admin/application_settings/_grafana.html.haml
index 700be7db54f..80ff5a298b4 100644
--- a/app/views/admin/application_settings/_grafana.html.haml
+++ b/app/views/admin/application_settings/_grafana.html.haml
@@ -4,7 +4,7 @@
%fieldset
%p
= _("Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab.")
- = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/grafana_configuration.md')
+ = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/performance/grafana_configuration.md')
.form-group
.form-check
= f.check_box :grafana_enabled, class: 'form-check-input'
diff --git a/app/views/admin/application_settings/_package_registry.html.haml b/app/views/admin/application_settings/_package_registry.html.haml
new file mode 100644
index 00000000000..257a90252cc
--- /dev/null
+++ b/app/views/admin/application_settings/_package_registry.html.haml
@@ -0,0 +1,50 @@
+- if Gitlab.config.packages.enabled
+ %section.settings.as-package.no-animate#js-package-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Package Registry')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _("Settings related to the use and experience of using GitLab's Package Registry.")
+
+ = render_if_exists 'admin/application_settings/ee_package_registry'
+
+ .settings-content
+ %h4
+ = _('Package file size limits')
+ %p
+ = _('Set limit to 0 to allow any file size.')
+ .scrolling-tabs-container.inner-page-scroll-tabs
+ - if @plans.size > 1
+ %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs.mb-3
+ - @plans.each_with_index do |plan, index|
+ %li
+ = link_to admin_plan_limits_path(anchor: 'js-package-settings'), data: { target: "div#plan#{index}", action: "plan#{index}", toggle: 'tab'}, class: index == 0 ? 'active': '' do
+ = plan.name.capitalize
+ .tab-content
+ - @plans.each_with_index do |plan, index|
+ .tab-pane{ :id => "plan#{index}", class: index == 0 ? 'active': '' }
+ = form_for plan.actual_limits, url: admin_plan_limits_path(anchor: 'js-package-settings'), html: { class: 'fieldset-form' }, method: :post do |f|
+ = form_errors(plan)
+ %fieldset
+ = f.hidden_field(:plan_id, value: plan.id)
+ .form-group
+ = f.label :conan_max_file_size, _('Maximum Conan package file size in bytes'), class: 'label-bold'
+ = f.number_field :conan_max_file_size, class: 'form-control'
+ .form-group
+ = f.label :maven_max_file_size, _('Maximum Maven package file size in bytes'), class: 'label-bold'
+ = f.number_field :maven_max_file_size, class: 'form-control'
+ .form-group
+ = f.label :npm_max_file_size, _('Maximum NPM package file size in bytes'), class: 'label-bold'
+ = f.number_field :npm_max_file_size, class: 'form-control'
+ .form-group
+ = f.label :nuget_max_file_size, _('Maximum NuGet package file size in bytes'), class: 'label-bold'
+ = f.number_field :nuget_max_file_size, class: 'form-control'
+ .form-group
+ = f.label :pypi_max_file_size, _('Maximum PyPI package file size in bytes'), class: 'label-bold'
+ = f.number_field :pypi_max_file_size, class: 'form-control'
+ .form-group
+ = f.label :generic_packages_max_file_size, _('Generic package file size in bytes'), class: 'label-bold'
+ = f.number_field :generic_packages_max_file_size, class: 'form-control'
+ = f.submit _('Save %{name} size limits').html_safe % { name: plan.name.capitalize }, class: 'btn gl-button btn-success'
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index 8214cf8ce9f..2ee7f3edc97 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -14,7 +14,7 @@
= _("Require users to prove ownership of custom domains")
.form-text.text-muted
= _("Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled")
- = link_to icon('question-circle'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership')
+ = link_to sprite_icon('question-o'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership')
- if Gitlab.config.pages.access_control
.form-group
.form-check
@@ -23,7 +23,7 @@
= _("Disable public access to Pages sites")
.form-text.text-muted
= _("Access to Pages websites are controlled based on the user's membership to a given project. By checking this box, users will be required to be logged in to have access to all Pages websites in your instance.")
- = link_to icon('question-circle'), help_page_path('administration/pages/index.md', anchor: 'disabling-public-access-to-all-pages-websites')
+ = link_to sprite_icon('question-o'), help_page_path('administration/pages/index.md', anchor: 'disabling-public-access-to-all-pages-websites')
%h5
= _("Configure Let's Encrypt")
%p
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index 6b02521a0f0..3473c185dbe 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -13,7 +13,7 @@
to authenticate SSH keys via the database file. Only uncheck this
if you have configured your OpenSSH server to use the
AuthorizedKeysCommand. Click on the help icon for more details.
- = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
+ = link_to sprite_icon('question-o'), help_page_path('administration/operations/fast_ssh_key_lookup')
.form-group
= f.label :raw_blob_request_limit, _('Raw blob request rate limit per minute'), class: 'label-bold'
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
index b2ec25cdf8d..49f58449d29 100644
--- a/app/views/admin/application_settings/_prometheus.html.haml
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -10,7 +10,7 @@
\. This setting requires a
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index')
+ = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/prometheus/index')
.form-group
.form-check
= f.check_box :prometheus_metrics_enabled, class: 'form-check-input'
@@ -22,7 +22,7 @@
Environment variable
%code prometheus_multiproc_dir
does not exist or is not pointing to a valid directory.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
+ = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
.form-group
= f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'label-bold'
= f.number_field :metrics_method_call_threshold, class: 'form-control'
diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml
index 8f6946534ea..0e9731b1c70 100644
--- a/app/views/admin/application_settings/_realtime.html.haml
+++ b/app/views/admin/application_settings/_realtime.html.haml
@@ -12,7 +12,6 @@
The multiplier can also have a decimal value.
The default value (1) is a reasonable choice for the majority of GitLab
installations. Set to 0 to completely disable polling.
- = link_to icon('question-circle'), help_page_path('administration/polling')
+ = link_to sprite_icon('question-o'), help_page_path('administration/polling')
= f.submit 'Save changes', class: "btn btn-success"
-
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
index fea3ff4c3ba..7ff2b6e841d 100644
--- a/app/views/admin/application_settings/_registry.html.haml
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -10,9 +10,15 @@
= f.check_box :container_expiration_policies_enable_historic_entries, class: 'form-check-input'
= f.label :container_expiration_policies_enable_historic_entries, class: 'form-check-label' do
= _("Enable container expiration and retention policies for projects created earlier than GitLab 12.7.")
- = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy')
+ = link_to sprite_icon('question-o'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy')
.form-text.text-muted
= _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.")
- = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries')
+ = link_to sprite_icon('question-o'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries')
+ - if limit_delete_tags_service?
+ .form-group
+ = f.label :container_registry_delete_tags_service_timeout, _('Cleanup policy maximum processing time (seconds)'), class: 'label-bold'
+ = f.number_field :container_registry_delete_tags_service_timeout, min: 0, class: 'form-control'
+ .form-text.text-muted
+ = _("Tags are deleted until the timeout is reached. Any remaining tags are included the next time the policy runs. To remove the time limit, set it to 0.")
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
index 3b1d1eceb9c..d598f173ff3 100644
--- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -10,7 +10,7 @@
= _('Allow repository mirroring to be configured by project maintainers')
%span.form-text.text-muted
= _('If disabled, only admins will be able to configure repository mirroring.')
- = link_to icon('question-circle'), help_page_path('user/project/repository/repository_mirroring.md')
+ = link_to sprite_icon('question-o'), help_page_path('user/project/repository/repository_mirroring.md')
= render_if_exists 'admin/application_settings/mirror_settings', form: f
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index ee55529621b..0dc8dc0740e 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -16,7 +16,7 @@
.form-text
%p.text-secondary
= _('Enter weights for storages for new repositories.')
- = link_to icon('question-circle'), help_page_path('administration/repository_storage_paths')
+ = link_to sprite_icon('question-o'), help_page_path('administration/repository_storage_paths')
.form-check
- storage_weights.each do |attribute|
= f.text_field attribute[:name], class: 'form-text-input', value: attribute[:value]
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 505be869620..2a26a0909fd 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -38,7 +38,7 @@
= f.check_box :notify_on_unknown_sign_in, class: 'form-check-input'
= f.label :notify_on_unknown_sign_in, class: 'form-check-label' do
= _('Notify users by email when sign-in location is not recognized')
- = link_to icon('question-circle'),
+ = link_to sprite_icon('question-o'),
'https://docs.gitlab.com/ee/user/profile/unknown_sign_in_notification.html',
target: '_blank'
.form-group
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
index 3216d7b7a9a..c339d6df363 100644
--- a/app/views/admin/application_settings/_snowplow.html.haml
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -25,8 +25,5 @@
.form-group
= f.label :snowplow_cookie_domain, _('Cookie domain'), class: 'label-light'
= f.text_field :snowplow_cookie_domain, class: 'form-control'
- .form-group
- = f.label :snowplow_iglu_registry_url, _('Iglu registry URL (optional)'), class: 'label-light'
- = f.text_field :snowplow_iglu_registry_url, class: 'form-control'
= f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index d8a4c601b77..4e45db1e10a 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -1,3 +1,5 @@
+- payload_class = 'js-usage-ping-payload'
+
= form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
@@ -9,7 +11,7 @@
Enable version check
.form-text.text-muted
GitLab will inform you if a new version is available.
- = link_to 'Learn more', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'version-check-core-only')
+ = link_to 'Learn more', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'version-check')
about what information is shared with GitLab Inc.
.form-group
- can_be_configured = @application_setting.usage_ping_can_be_configured?
@@ -21,21 +23,18 @@
- if can_be_configured
%p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.')
- - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping-core-only')
+ - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
- usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
%p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
- %button.btn.js-usage-ping-payload-trigger{ type: 'button' }
+ %button.btn.js-payload-preview-trigger{ type: 'button', data: { payload_selector: ".#{payload_class}" } }
.spinner.js-spinner.d-none
.js-text.d-inline= _('Preview payload')
- %pre.usage-data.js-usage-ping-payload.js-syntax-highlight.code.highlight.mt-2.d-none{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
+ %pre.usage-data.js-syntax-highlight.code.highlight.mt-2.d-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else
= _('The usage ping is disabled, and cannot be configured through this form.')
- deactivating_usage_ping_path = help_page_path('development/telemetry/usage_ping', anchor: 'disable-usage-ping')
- deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path }
= s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe }
- .form-group.mt-3
- = f.label :instance_statistics_visibility_private, _('Instance Statistics visibility')
- = f.select :instance_statistics_visibility_private, options_for_select({_('All users') => false, _('Only admins') => true}, Gitlab::CurrentSettings.instance_statistics_visibility_private?), {}, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/ci/_header.html.haml b/app/views/admin/application_settings/ci/_header.html.haml
index aa1a6256986..6ff35a42efd 100644
--- a/app/views/admin/application_settings/ci/_header.html.haml
+++ b/app/views/admin/application_settings/ci/_header.html.haml
@@ -2,7 +2,7 @@
%h4
= _('Variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to sprite_icon('question-o', css_class: 'gl-vertical-align-baseline!'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index 788dc0b0f1b..823cee09d4b 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -117,6 +117,7 @@
#js-maintenance-mode-settings
= render_if_exists 'admin/application_settings/elasticsearch_form'
+= render 'admin/application_settings/gitpod'
= render 'admin/application_settings/plantuml'
= render 'admin/application_settings/sourcegraph'
= render_if_exists 'admin/application_settings/slack'
diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml
index b5dae424b46..ed4f63d0b82 100644
--- a/app/views/admin/application_settings/integrations.html.haml
+++ b/app/views/admin/application_settings/integrations.html.haml
@@ -16,5 +16,5 @@
%h4= s_('AdminSettings|Apply integration settings to all Projects')
%p
= s_('AdminSettings|Integrations configured here will automatically apply to all projects on this instance.')
- = link_to _('Learn more'), '#'
+ = link_to _('Learn more'), integrations_help_page_path, target: '_blank', rel: 'noopener noreferrer'
= render 'shared/integrations/index', integrations: @integrations
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index 181c54c2716..9f1b7195ab7 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -32,7 +32,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable access to the Performance Bar for a given group.')
- = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
+ = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/performance/performance_bar')
.settings-content
= render 'performance_bar'
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 8338401bea5..0d01f1c57e0 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -16,11 +16,6 @@
= doorkeeper_errors_for application, :redirect_uri
%span.form-text.text-muted
Use one line per URI
- - if Doorkeeper.configuration.native_redirect_uri
- %span.form-text.text-muted
- Use
- %code= Doorkeeper.configuration.native_redirect_uri
- for local tests
= content_tag :div, class: 'form-group row' do
.col-sm-2.col-form-label.pt-0
diff --git a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml b/app/views/admin/cohorts/_cohorts_table.html.haml
index d4defd3f849..bb6266b38f6 100644
--- a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml
+++ b/app/views/admin/cohorts/_cohorts_table.html.haml
@@ -2,7 +2,7 @@
.bs-callout.clearfix
%p
= s_("Cohorts|User cohorts are shown for the last %{months_included} months. Only users with activity are counted in the 'New users' column; inactive users are counted separately.") % { months_included: @cohorts[:months_included] }
- = link_to icon('question-circle'), help_page_path('user/instance_statistics/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('user/admin_area/analytics/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
.table-holder.d-xl-table
%table.table
diff --git a/app/views/admin/cohorts/index.html.haml b/app/views/admin/cohorts/index.html.haml
new file mode 100644
index 00000000000..03cd392d370
--- /dev/null
+++ b/app/views/admin/cohorts/index.html.haml
@@ -0,0 +1,7 @@
+- breadcrumb_title _("Cohorts")
+- page_title _("Cohorts")
+
+- if @cohorts
+ = render 'cohorts_table'
+- else
+ #js-cohorts-empty-state{ data: { empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('user/admin_area/analytics/user_cohorts') } }
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 271ab12037e..4acfc96caf2 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -7,47 +7,44 @@
dismissible: true.to_s } }
= notice[:message].html_safe
-- if show_license_breakdown?
- = render_if_exists 'admin/licenses/breakdown', license: @license
+- if @license.present? && show_license_breakdown?
+ = render_if_exists 'admin/licenses/breakdown'
.admin-dashboard.gl-mt-3
.row
.col-sm-4
- .info-well.dark-well
+ .info-well.dark-well.flex-fill
.well-segment.well-centered
= link_to admin_projects_path do
%h3.text-center
- Projects:
- = approximate_count_with_delimiters(@counts, Project)
+ = s_('AdminArea|Projects: %{number_of_projects}') % { number_of_projects: approximate_count_with_delimiters(@counts, Project) }
%hr
- = link_to('New project', new_project_path, class: "btn btn-success")
+ = link_to(s_('AdminArea|New project'), new_project_path, class: "btn btn-success gl-w-full")
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
= link_to admin_users_path do
%h3.text-center
- Users:
- = approximate_count_with_delimiters(@counts, User)
+ = s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) }
%hr
.btn-group.d-flex{ role: 'group' }
- = link_to 'New user', new_admin_user_path, class: "btn btn-success"
- = link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn btn-primary'
+ = link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn btn-success gl-w-full"
+ = link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn btn-primary gl-w-full'
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
= link_to admin_groups_path do
%h3.text-center
- Groups:
- = approximate_count_with_delimiters(@counts, Group)
+ = s_('AdminArea|Groups: %{number_of_groups}') % { number_of_groups: approximate_count_with_delimiters(@counts, Group) }
%hr
- = link_to 'New group', new_admin_group_path, class: "btn btn-success"
+ = link_to s_('AdminArea|New group'), new_admin_group_path, class: "btn btn-success gl-w-full"
.row
.col-md-4
#js-admin-statistics-container
.col-md-4
.info-well
.well-segment.admin-well.admin-well-features
- %h4 Features
+ %h4= s_('AdminArea|Features')
= feature_entry(_('Sign up'),
href: general_admin_application_settings_path(anchor: 'js-signup-settings'),
enabled: allow_signup?)
@@ -87,42 +84,41 @@
.info-well
.well-segment.admin-well
%h4
- Components
+ = s_('AdminArea|Components')
- if Gitlab::CurrentSettings.version_check_enabled
.float-right
= version_status_badge
%p
- %a{ href: general_admin_application_settings_path }
- GitLab
+ = link_to _('GitLab'), general_admin_application_settings_path
%span.float-right
= Gitlab::VERSION
= "(#{Gitlab.revision})"
%p
- GitLab Shell
+ = _('GitLab Shell')
%span.float-right
= Gitlab::Shell.version
%p
- GitLab Workhorse
+ = _('GitLab Workhorse')
%span.float-right
= gitlab_workhorse_version
%p
- GitLab API
+ = _('GitLab API')
%span.float-right
= API::API::version
- if Gitlab.config.pages.enabled
%p
- GitLab Pages
+ = _('GitLab Pages')
%span.float-right
= Gitlab::Pages::VERSION
= render_if_exists 'admin/dashboard/geo'
%p
- Ruby
+ = _('Ruby')
%span.float-right
#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}
%p
- Rails
+ = _('Rails')
%span.float-right
#{Rails::VERSION::STRING}
%p
@@ -130,12 +126,12 @@
%span.float-right
= Gitlab::Database.version
%p
- = link_to "Gitaly Servers", admin_gitaly_servers_path
+ = link_to _("Gitaly Servers"), admin_gitaly_servers_path
.row
.col-md-4
.info-well
.well-segment.admin-well
- %h4 Latest projects
+ %h4= s_('AdminArea|Latest projects')
- @projects.each do |project|
%p
= link_to project.full_name, admin_project_path(project), class: 'str-truncated-60'
@@ -144,7 +140,7 @@
.col-md-4
.info-well
.well-segment.admin-well
- %h4 Latest users
+ %h4= s_('AdminArea|Latest users')
- @users.each do |user|
%p
= link_to [:admin, user], class: 'str-truncated-60' do
@@ -154,7 +150,7 @@
.col-md-4
.info-well
.well-segment.admin-well
- %h4 Latest groups
+ %h4= s_('AdminArea|Latest groups')
- @groups.each do |group|
%p
= link_to [:admin, group], class: 'str-truncated-60' do
diff --git a/app/views/admin/dev_ops_report/_callout.html.haml b/app/views/admin/dev_ops_report/_callout.html.haml
new file mode 100644
index 00000000000..7507f433af8
--- /dev/null
+++ b/app/views/admin/dev_ops_report/_callout.html.haml
@@ -0,0 +1,13 @@
+.gl-mt-3
+.user-callout{ data: { uid: 'dev_ops_report_intro_callout_dismissed' } }
+ .bordered-box.landing.content-block
+ %button.btn.btn-default.close.js-close-callout{ type: 'button',
+ 'aria-label' => _('Dismiss DevOps Report introduction') }
+ = sprite_icon('close', size: 16, css_class: 'dismiss-icon')
+ .user-callout-copy
+ %h4
+ = _('Introducing Your DevOps Report')
+ %p
+ = _('Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers.')
+ .svg-container.devops
+ = custom_icon('dev_ops_report_overview')
diff --git a/app/views/instance_statistics/dev_ops_score/_card.html.haml b/app/views/admin/dev_ops_report/_card.html.haml
index dd6e5c0f108..dd6e5c0f108 100644
--- a/app/views/instance_statistics/dev_ops_score/_card.html.haml
+++ b/app/views/admin/dev_ops_report/_card.html.haml
diff --git a/app/views/admin/dev_ops_report/_no_data.html.haml b/app/views/admin/dev_ops_report/_no_data.html.haml
new file mode 100644
index 00000000000..e540a4e2bce
--- /dev/null
+++ b/app/views/admin/dev_ops_report/_no_data.html.haml
@@ -0,0 +1,7 @@
+.container.devops-empty
+ .col-sm-12.justify-content-center.text-center
+ = custom_icon('dev_ops_report_no_data')
+ %h4= _('Data is still calculating...')
+ %p
+ = _('It may be several days before you see feature usage data.')
+ = link_to _('Our documentation includes an example DevOps Score report.'), help_page_path('user/admin_area/analytics/dev_ops_report'), target: '_blank'
diff --git a/app/views/instance_statistics/dev_ops_score/index.html.haml b/app/views/admin/dev_ops_report/show.html.haml
index 215624d27ce..1892557d0d6 100644
--- a/app/views/instance_statistics/dev_ops_score/index.html.haml
+++ b/app/views/admin/dev_ops_report/show.html.haml
@@ -1,13 +1,13 @@
-- page_title _('DevOps Score')
+- page_title _('DevOps Report')
- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
.container
- - if usage_ping_enabled && show_callout?('dev_ops_score_intro_callout_dismissed')
+ - if usage_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
= render 'callout'
.gl-mt-3
- if !usage_ping_enabled
- = render 'disabled'
+ #js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/telemetry/usage_ping') } }
- elsif @metric.blank?
= render 'no_data'
- else
@@ -16,10 +16,10 @@
%h2.devops-header-title{ class: "devops-#{score_level(@metric.average_percentage_score)}-score" }
= number_to_percentage(@metric.average_percentage_score, precision: 1)
.devops-header-subtitle
- = _('index')
+ = _('DevOps')
%br
- = _('score')
- = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/instance_statistics/dev_ops_score')
+ = _('Score')
+ = link_to sprite_icon('question-o', css_class: 'devops-header-icon'), help_page_path('user/admin_area/analytics/dev_ops_report')
.devops-cards.board-card-container
- @metric.cards.each do |card|
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 22cf722d117..041b0661d37 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -24,8 +24,10 @@
- if @group.new_record?
.form-group.row
.offset-sm-2.col-sm-10
- .alert.alert-info
- = render 'shared/group_tips'
+ .gl-alert.gl-alert-info
+ = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ = render 'shared/group_tips'
.form-actions
= f.submit _('Create group'), class: "btn btn-success"
= link_to _('Cancel'), admin_groups_path, class: "btn btn-cancel"
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index ab817b2ef6e..3a82f3803bd 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -27,7 +27,7 @@
%span.gl-ml-5
= sprite_icon('users', css_class: 'gl-vertical-align-text-bottom')
- = number_with_delimiter(group.users.count)
+ = number_with_delimiter(group.users_count)
%span.gl-ml-5.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
= visibility_level_icon(group.visibility_level)
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 6dd73698848..6c2c0b3a488 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -57,7 +57,7 @@
%span.light= _('Group Git LFS status:')
%strong
= group_lfs_status(@group)
- = link_to icon('question-circle'), help_page_path('topics/git/lfs/index')
+ = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index')
= render_if_exists 'namespaces/shared_runner_status', namespace: @group
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index fbe37f6c509..65d3c78ec11 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -27,7 +27,7 @@
.card-header
Current Status:
- if no_errors
- = icon('circle', class: 'cgreen')
+ = sprite_icon('check', css_class: 'cgreen')
#{ s_('HealthCheck|Healthy') }
- else
= icon('warning', class: 'cred')
diff --git a/app/views/admin/instance_statistics/index.html.haml b/app/views/admin/instance_statistics/index.html.haml
new file mode 100644
index 00000000000..ab28a2471e6
--- /dev/null
+++ b/app/views/admin/instance_statistics/index.html.haml
@@ -0,0 +1,4 @@
+- breadcrumb_title _("Instance Statistics")
+- page_title _("Instance Statistics")
+
+#js-instance-statistics-app
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index f842ab2d009..44317eb7f6e 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -5,10 +5,7 @@
%li.project-row{ class: ('no-description' if project.description.blank?) }
.controls
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
- %button.delete-project-button.btn.btn-danger{ data: { toggle: 'modal',
- target: '#delete-project-modal',
- delete_project_url: admin_project_path(project),
- project_name: project.name }, type: 'button' }
+ %button.delete-project-button.btn.btn-danger{ data: { delete_project_url: admin_project_path(project), project_name: project.name } }
= s_('AdminProjects|Delete')
.stats
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index bd3b2f40059..d5af12fcd09 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -100,7 +100,7 @@
= _('Git LFS status:')
%strong
= project_lfs_status(@project)
- = link_to icon('question-circle'), help_page_path('topics/git/lfs/index')
+ = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index')
- else
%li
%span.light
@@ -165,7 +165,7 @@
- else
= _("This repository was last checked %{last_check_timestamp}. The check passed.") % { last_check_timestamp: @project.last_repository_check_at.to_s(:medium) }
- = link_to icon('question-circle'), help_page_path('administration/repository_checks')
+ = link_to sprite_icon('question-o'), help_page_path('administration/repository_checks')
.form-group
= f.submit _('Trigger repository check'), class: 'btn btn-primary'
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 0bbe73d6f7e..a2b736c332c 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -76,4 +76,4 @@
= sprite_icon('play')
.btn-group
= link_to [:admin, runner], method: :delete, class: 'btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
- = icon('remove')
+ = sprite_icon('close')
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 08d65819476..cc218aefdb7 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -108,7 +108,7 @@
{{name}}
= button_tag class: %w[clear-search hidden] do
- = icon('times')
+ = sprite_icon('close', size: 16, css_class: 'clear-search-icon')
.filter-dropdown-container
= render 'sort_dropdown'
diff --git a/app/views/admin/runners/update.js.haml b/app/views/admin/runners/update.js.haml
deleted file mode 100644
index 2b7d3067e20..00000000000
--- a/app/views/admin/runners/update.js.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-:plain
- $("#runner_#{@runner.id}").replaceWith("#{escape_javascript(render(@runner))}")
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index f2153e503af..c17ab5e08a7 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -3,8 +3,5 @@
%p #{@service.description} template.
-= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form|
+= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form js-integration-settings-form' } do |form|
= render 'shared/service_settings', form: form, integration: @service
-
- .footer-block.row-content-block
- = form.submit 'Save', class: 'btn btn-success'
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index 19a0b7466a2..3517beac976 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -1,9 +1,28 @@
- page_title _("Service Templates")
+- @content_class = 'limit-container-width' unless fluid_layout
+
+- if show_service_templates_deprecated?
+ .gl-alert.gl-alert-tip.js-service-templates-deprecated.gl-mt-5{ role: 'alert', data: { feature_id: UserCalloutsHelper::SERVICE_TEMPLATES_DEPRECATED, dismiss_endpoint: user_callouts_path } }
+ = sprite_icon('bulb', css_class: 'gl-alert-icon gl-alert-icon-no-title')
+ %button.js-close.gl-alert-dismiss{ type: 'button', aria: { label: _('Dismiss') } }
+ = sprite_icon('close')
+ %h4.gl-alert-title= s_('AdminSettings|Service Templates will soon be deprecated.')
+ .gl-alert-body
+ = s_('AdminSettings|Try using the latest version of Integrations instead.')
+ .gl-alert-actions
+ = link_to _('Go to Integrations'), integrations_admin_application_settings_path, class: 'btn btn-info gl-alert-action gl-button'
+ = link_to _('Learn more'), help_page_path('user/admin_area/settings/project_integration_management'), class: 'btn btn-default gl-alert-action btn-secondary gl-button', target: '_blank', rel: 'noopener noreferrer'
+
%h3.page-title Service templates
%p.light= s_('AdminSettings|Service template allows you to set default values for integrations')
.table-holder
%table.table
+ %colgroup
+ %col
+ %col
+ %col
+ %col{ width: 135 }
%thead
%tr
%th
diff --git a/app/views/admin/sessions/_two_factor_otp.html.haml b/app/views/admin/sessions/_two_factor_otp.html.haml
index 9d4acbf1b99..8d5588de06e 100644
--- a/app/views/admin/sessions/_two_factor_otp.html.haml
+++ b/app/views/admin/sessions/_two_factor_otp.html.haml
@@ -1,4 +1,4 @@
-= form_tag(admin_session_path, { method: :post, class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if current_user.two_factor_u2f_enabled?}" }) do
+= form_tag(admin_session_path, { method: :post, class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if current_user.two_factor_webauthn_u2f_enabled?}" }) do
.form-group
= label_tag :user_otp_attempt, _('Two-Factor Authentication code')
= text_field_tag 'user[otp_attempt]', nil, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: _('This field is required.')
diff --git a/app/views/admin/sessions/two_factor.html.haml b/app/views/admin/sessions/two_factor.html.haml
index 746d57dbad1..531ab206157 100644
--- a/app/views/admin/sessions/two_factor.html.haml
+++ b/app/views/admin/sessions/two_factor.html.haml
@@ -11,5 +11,5 @@
.login-body
- if current_user.two_factor_otp_enabled?
= render 'admin/sessions/two_factor_otp'
- - if current_user.two_factor_u2f_enabled?
- = render 'u2f/authenticate', render_remember_me: false, target_path: admin_session_path
+ - if current_user.two_factor_webauthn_u2f_enabled?
+ = render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 38c6c8b2a62..9e31c8d2852 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -87,8 +87,8 @@
.form-actions
- if @user.new_record?
- = f.submit 'Create user', class: "btn btn-success"
+ = f.submit 'Create user', class: "btn gl-button btn-success"
= link_to 'Cancel', admin_users_path, class: "btn btn-cancel"
- else
- = f.submit 'Save changes', class: "btn btn-success"
- = link_to 'Cancel', admin_user_path(@user), class: "btn btn-cancel"
+ = f.submit 'Save changes', class: "btn gl-button btn-success"
+ = link_to 'Cancel', admin_user_path(@user), class: "btn gl-button btn-cancel"
diff --git a/app/views/admin/users/_modals.html.haml b/app/views/admin/users/_modals.html.haml
index eaec6d69f5a..6cf6dc116e3 100644
--- a/app/views/admin/users/_modals.html.haml
+++ b/app/views/admin/users/_modals.html.haml
@@ -16,7 +16,7 @@
'secondary-action': s_('AdminUsers|Block user') } }
= s_('AdminUsers|You are about to permanently delete the user %{username}. Issues, merge requests,
and groups linked to them will be transferred to a system-wide "Ghost-user". To avoid data loss,
- consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end},
+ consider using the %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd},
it cannot be undone or recovered.')
%div{ data: { modal: "delete-with-contributions",
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 440eaac1917..160303890f5 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -17,9 +17,9 @@
- unless user.internal?
.table-section.section-20.table-button-footer
.table-action-buttons
- = link_to _('Edit'), edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn btn-default'
+ = link_to _('Edit'), edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn gl-button btn-default'
- unless user == current_user
- %button.dropdown-new.btn.btn-default{ type: 'button', data: { toggle: 'dropdown' } }
+ %button.dropdown-new.btn.gl-button.btn-default{ type: 'button', data: { toggle: 'dropdown' } }
= sprite_icon('settings')
= sprite_icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right
@@ -32,13 +32,13 @@
- elsif user.blocked?
= link_to _('Unblock'), unblock_admin_user_path(user), method: :put
- else
- %button.btn{ data: { 'gl-modal-action': 'block',
+ %button.btn.gl-button.btn-default-tertiary{ data: { 'gl-modal-action': 'block',
url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Block')
- if user.can_be_deactivated?
%li
- %button.btn{ data: { 'gl-modal-action': 'deactivate',
+ %button.btn.gl-button.btn-default-tertiary{ data: { 'gl-modal-action': 'deactivate',
url: deactivate_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Deactivate')
@@ -52,13 +52,13 @@
%li.divider
- if user.can_be_removed?
%li
- %button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete',
+ %button.delete-user-button.btn.gl-button.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete',
delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Delete user')
%li
- %button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
+ %button.delete-user-button.btn.gl-button.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 05988c17412..118bdf7bb17 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -40,7 +40,7 @@
%small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
.nav-controls
= render_if_exists 'admin/users/admin_email_users'
- = link_to s_('AdminUsers|New user'), new_admin_user_path, class: 'btn btn-success btn-search float-right'
+ = link_to s_('AdminUsers|New user'), new_admin_user_path, class: 'btn gl-button btn-success btn-search float-right'
.filtered-search-block.row-content-block.border-top-0
= form_tag admin_users_path, method: :get do
diff --git a/app/views/admin/users/new.html.haml b/app/views/admin/users/new.html.haml
index e5e6790b789..08aa7c3c9d2 100644
--- a/app/views/admin/users/new.html.haml
+++ b/app/views/admin/users/new.html.haml
@@ -1,5 +1,5 @@
- page_title _("New User")
%h3.page-title
- New user
+ = s_('AdminUsers|New user')
%hr
= render 'form'
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index f66d9b76afc..70a497f14ff 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -16,8 +16,8 @@
.float-right
%span.light.vertical-align-middle= group_member.human_access
- unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-sm btn btn-remove gl-ml-3", title: 'Remove user from group' do
- %i.fa.fa-times.fa-inverse
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member), testid: 'remove-user' }, method: :delete, remote: true, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from group') do
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
.row
.col-md-6
@@ -46,5 +46,5 @@
%span.light.vertical-align-middle= member.human_access
- if member.respond_to? :project
- = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-sm btn btn-remove gl-ml-3", title: 'Remove user from project' do
- %i.fa.fa-times
+ = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: 'Remove user from project' do
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 2bc39a23b2d..a08c29714e0 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -38,19 +38,23 @@
%span.light Secondary email:
%strong
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
- = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
- %i.fa.fa-times
+ = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
%li
%span.light ID:
%strong
= @user.id
+ %li
+ %span.light= _('Namespace ID:')
+ %strong
+ = @user.namespace_id
%li.two-factor-status
%span.light Two-factor Authentication:
%strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
- if @user.two_factor_enabled?
Enabled
- = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-sm btn-remove float-right', title: 'Disable Two-factor Authentication'
+ = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn gl-button btn-sm btn-remove float-right', title: 'Disable Two-factor Authentication'
- else
Disabled
@@ -142,7 +146,7 @@
- email = " (#{@user.unconfirmed_email})"
%p This user has an unconfirmed email address#{email}. You may force a confirmation.
%br
- = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?', qa_selector: 'confirm_user_button' }
+ = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?', qa_selector: 'confirm_user_button' }
= render 'admin/users/user_detail_note'
@@ -153,7 +157,7 @@
.card-body
= render partial: 'admin/users/user_activation_effects'
%br
- = link_to 'Activate user', activate_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
+ = link_to 'Activate user', activate_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
- elsif @user.can_be_deactivated?
.card.border-warning
.card-header.bg-warning.text-white
@@ -161,7 +165,7 @@
.card-body
= render partial: 'admin/users/user_deactivation_effects'
%br
- %button.btn.btn-warning{ data: { 'gl-modal-action': 'deactivate',
+ %button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'deactivate',
content: 'You can always re-activate their account, their data will remain intact.',
url: deactivate_admin_user_path(@user),
username: sanitize_name(@user.name) } }
@@ -177,7 +181,7 @@
%li Log in
%li Access Git repositories
%br
- = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
+ = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
- else
.card.border-warning
.card-header.bg-warning.text-white
@@ -185,7 +189,7 @@
.card-body
= render partial: 'admin/users/user_block_effects'
%br
- %button.btn.btn-warning{ data: { 'gl-modal-action': 'block',
+ %button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'block',
content: 'You can always unblock their account, their data will remain intact.',
url: block_admin_user_path(@user),
username: sanitize_name(@user.name) } }
@@ -197,7 +201,7 @@
.card-body
%p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.
%br
- = link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
+ = link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
.card.border-danger
.card-header.bg-danger.text-white
@@ -207,7 +211,7 @@
%p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user
%br
- %button.delete-user-button.btn.btn-danger{ data: { 'gl-modal-action': 'delete',
+ %button.delete-user-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete',
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
username: sanitize_name(@user.name) } }
@@ -237,7 +241,7 @@
the user, and projects in them, will also be removed. Commits
to other projects are unaffected.
%br
- %button.delete-user-button.btn.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
+ %button.delete-user-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user),
username: @user.name } }
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/authentication/_authenticate.html.haml
index 6658d70df8d..17e855dbddd 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/authentication/_authenticate.html.haml
@@ -6,7 +6,7 @@
%script#js-authenticate-token-2fa-error{ type: "text/template" }
%div
- %p <%= error_message %> (#{_("error code:")} <%= error_code %>)
+ %p <%= error_message %> (<%= error_name %>)
%a.btn.btn-block.btn-warning#js-token-2fa-try-again= _("Try again?")
%script#js-authenticate-token-2fa-authenticated{ type: "text/template" }
diff --git a/app/views/authentication/_register.html.haml b/app/views/authentication/_register.html.haml
new file mode 100644
index 00000000000..f1aa76d115a
--- /dev/null
+++ b/app/views/authentication/_register.html.haml
@@ -0,0 +1,36 @@
+#js-register-token-2fa
+
+%script#js-register-2fa-message{ type: "text/template" }
+ %p <%= message %>
+
+%script#js-register-token-2fa-setup{ type: "text/template" }
+ - if current_user.two_factor_otp_enabled?
+ .row.gl-mb-3
+ .col-md-5
+ %button#js-setup-token-2fa-device.btn.btn-info= _("Set up new device")
+ .col-md-7
+ %p= _("Your device needs to be set up. Plug it in (if needed) and click the button on the left.")
+ - else
+ .row.gl-mb-3
+ .col-md-4
+ %button#js-setup-token-2fa-device.btn.btn-info.btn-block{ disabled: true }= _("Set up new device")
+ .col-md-8
+ %p= _("You need to register a two-factor authentication app before you can set up a device.")
+
+%script#js-register-token-2fa-error{ type: "text/template" }
+ %div
+ %p
+ %span <%= error_message %> (<%= error_name %>)
+ %a.btn.btn-warning#js-token-2fa-try-again= _("Try again?")
+
+%script#js-register-token-2fa-registered{ type: "text/template" }
+ .row.gl-mb-3
+ .col-md-12
+ %p= _("Your device was successfully set up! Give it a name and register it with the GitLab server.")
+ = form_tag(target_path, method: :post) do
+ .row.gl-mb-3
+ .col-md-3
+ = text_field_tag 'device_registration[name]', nil, class: 'form-control', placeholder: _("Pick a name")
+ .col-md-3
+ = hidden_field_tag 'device_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
+ = submit_tag _("Register device"), class: "btn btn-success"
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index 69cb41b1713..4c8bb84c9ef 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -8,12 +8,12 @@
- if status.has_details?
= link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item d-flex', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
%span{ class: klass }= sprite_icon(status.icon)
- %span.ci-build-text.text-truncate.mw-70p.gl-pl-1-deprecated-no-really-do-not-use-me= subject.name
+ %span.ci-build-text.text-truncate.mw-70p.gl-pl-2= subject.name
- else
.menu-item.mini-pipeline-graph-dropdown-item.d-flex{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
%span{ class: klass }= sprite_icon(status.icon)
- %span.ci-build-text.text-truncate.mw-70p.gl-pl-1-deprecated-no-really-do-not-use-me= subject.name
+ %span.ci-build-text.text-truncate.mw-70p.gl-pl-2= subject.name
- if status.has_action?
= link_to status.action_path, class: "gl-button ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
diff --git a/app/views/ci/variables/_header.html.haml b/app/views/ci/variables/_header.html.haml
index d0148e455de..f4e2a8584d8 100644
--- a/app/views/ci/variables/_header.html.haml
+++ b/app/views/ci/variables/_header.html.haml
@@ -2,7 +2,7 @@
%h4
= _('Variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to sprite_icon('question-o', css_class: 'gl-vertical-align-baseline!'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 542a41c2f7d..193ec8abf04 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -39,7 +39,7 @@
= value
%p.masking-validation-error.gl-field-error.hide
= s_("CiVariables|Cannot use Masked Variable with current value")
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'mask-a-custom-variable'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to sprite_icon('question-o'), help_page_path('ci/variables/README', anchor: 'mask-a-custom-variable'), target: '_blank', rel: 'noopener noreferrer'
- unless only_key_value
.ci-variable-body-item.ci-variable-protected-item.table-section.section-20.mr-0.border-top-0
.gl-mr-3
diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index 3461831eda2..4a84745cf98 100644
--- a/app/views/clusters/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
@@ -8,14 +8,14 @@
.hidden.row.js-cluster-api-unreachable.gl-alert.gl-alert-warning{ role: 'alert' }
= sprite_icon('warning', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- %button.js-close-banner.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
+ %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
= sprite_icon('close', css_class: 'gl-icon')
.gl-alert-body
= s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.')
.hidden.js-cluster-authentication-failure.js-cluster-api-unreachable.gl-alert.gl-alert-warning{ role: 'alert' }
= sprite_icon('warning', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- %button.js-close-banner.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
+ %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
= sprite_icon('close', css_class: 'gl-icon')
.gl-alert-body
= s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.')
diff --git a/app/views/clusters/clusters/aws/_new.html.haml b/app/views/clusters/clusters/aws/_new.html.haml
index ec604ca83e5..3eab9b46fb3 100644
--- a/app/views/clusters/clusters/aws/_new.html.haml
+++ b/app/views/clusters/clusters/aws/_new.html.haml
@@ -1,6 +1,6 @@
- if !Gitlab::CurrentSettings.eks_integration_enabled?
- documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/clusters/add_eks_clusters.md',
- anchor: 'additional-requirements-for-self-managed-instances-core-only') }
+ anchor: 'additional-requirements-for-self-managed-instances') }
= s_('Amazon authentication is not %{link_start}correctly configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: '<a/>'.html_safe }
- else
.js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'),
diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index 0a51d4b2e93..ff33fb46db8 100644
--- a/app/views/clusters/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -16,8 +16,8 @@
%span
= create_new_cluster_label(provider: params[:provider])
%li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#add-cluster-pane', id: 'add-cluster-tab', class: active_when(active_tab == 'add'), data: { toggle: 'tab' }, role: 'tab' }
- %span Add existing cluster
+ %a.nav-link{ href: '#add-cluster-pane', id: 'add-cluster-tab', class: active_when(active_tab == 'add'), data: { toggle: 'tab', qa_selector: 'add_existing_cluster_tab' }, role: 'tab' }
+ %span= s_('ClusterIntegration|Connect existing cluster')
.tab-content.gitlab-tab-content
.tab-pane.p-0{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
@@ -28,5 +28,5 @@
= render "clusters/clusters/#{provider}/new"
.tab-pane{ id: 'add-cluster-pane', class: active_when(active_tab == 'add'), role: 'tabpanel' }
- = render 'clusters/clusters/user/header'
+ #js-cluster-new{ data: js_cluster_new }
= render 'clusters/clusters/user/form'
diff --git a/app/views/clusters/clusters/user/_header.html.haml b/app/views/clusters/clusters/user/_header.html.haml
deleted file mode 100644
index b0a24ee464f..00000000000
--- a/app/views/clusters/clusters/user/_header.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%h4
- = s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
-%p
- - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/add_remove_clusters', anchor: 'add-existing-cluster'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes').html_safe % { link_to_help_page: link_to_help_page }
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 54a5624c6dd..d458f1d8eba 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -5,11 +5,12 @@
- if show_customize_homepage_banner?(@customize_homepage)
= content_for :customize_homepage_banner do
- .d-none.d-md-block{ class: "gl-pt-6! gl-pb-2! #{(container_class unless @no_container)} #{@content_class}" }
+ .gl-display-none.gl-display-md-block{ class: "gl-pt-6! gl-pb-2! #{(container_class unless @no_container)} #{@content_class}" }
.js-customize-homepage-banner{ data: { svg_path: image_path('illustrations/monitoring/getting_started.svg'),
preferences_behavior_path: profile_preferences_path(anchor: 'behavior'),
callouts_path: user_callouts_path,
- callouts_feature_id: UserCalloutsHelper::CUSTOMIZE_HOMEPAGE } }
+ callouts_feature_id: UserCalloutsHelper::CUSTOMIZE_HOMEPAGE,
+ track_label: 'home_page' } }
= render_dashboard_gold_trial(current_user)
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 9b6150c4be2..9c6a6be1bc3 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -3,6 +3,7 @@
- header_title _("To-Do List"), dashboard_todos_path
= render_dashboard_gold_trial(current_user)
+= stylesheet_link_tag 'page_bundles/todos'
.page-title-holder.d-flex.align-items-center
%h1.page-title= _('To-Do List')
@@ -26,10 +27,10 @@
.nav-controls
- if @todos.any?(&:pending?)
.gl-mr-3
- = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading d-flex align-items-center js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading align-items-center js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
Mark all as done
%span.spinner.ml-1
- = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading d-flex align-items-center js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
+ = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading align-items-center js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
Undo mark all as done
%span.spinner.ml-1
@@ -80,7 +81,7 @@
= link_to todos_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
-.js-todos-all
+.todos-list-container.js-todos-all
- if @todos.any?
.js-todos-list-container
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
diff --git a/app/views/devise/mailer/password_change_by_admin.html.haml b/app/views/devise/mailer/password_change_by_admin.html.haml
new file mode 100644
index 00000000000..d72f36838c5
--- /dev/null
+++ b/app/views/devise/mailer/password_change_by_admin.html.haml
@@ -0,0 +1,6 @@
+= email_default_heading(say_hello(@resource))
+
+%p
+ = admin_changed_password_text(format: :html)
+%p
+ = contact_your_administrator_text
diff --git a/app/views/devise/mailer/password_change_by_admin.text.erb b/app/views/devise/mailer/password_change_by_admin.text.erb
new file mode 100644
index 00000000000..d0e7ab98641
--- /dev/null
+++ b/app/views/devise/mailer/password_change_by_admin.text.erb
@@ -0,0 +1,5 @@
+<%= say_hello(@resource) %>
+
+<%= admin_changed_password_text %>
+
+<%= contact_your_administrator_text %>
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index afdf3c38567..2f75203ac62 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -8,7 +8,7 @@
= _("GitLab is a single application for the entire software development lifecycle. From project planning and source code management to CI/CD, monitoring, and security.")
.col-lg-5.order-12
.text-center.mb-3
- %h2.font-weight-bold.gl-font-size-20-deprecated-no-really-do-not-use-me= _('Register for GitLab')
+ %h2.font-weight-bold= _('Register for GitLab')
= render 'devise/shared/experimental_separate_sign_up_flow_box'
= render 'devise/shared/sign_in_link'
- else
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 115ebc94238..8e05488c091 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -3,7 +3,7 @@
.login-box
.login-body
- if @user.two_factor_otp_enabled?
- = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_u2f_enabled?}" }) do |f|
+ = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_u2f_enabled?}" }) do |f|
- resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
@@ -12,6 +12,5 @@
%p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-success", data: { qa_selector: 'verify_code_button' }
-
- - if @user.two_factor_u2f_enabled?
- = render "u2f/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path
+ - if @user.two_factor_webauthn_u2f_enabled?
+ = render "authentication/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 0da51d460e3..07ef9a7914a 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -20,9 +20,6 @@
.form-group
= f.label :email, class: 'label-bold'
= f.email_field :email, value: @invite_email, class: "form-control middle", data: { qa_selector: 'new_user_email_field' }, required: true, title: _("Please provide a valid email address.")
- .form-group
- = f.label :email_confirmation, class: 'label-bold'
- = f.email_field :email_confirmation, class: "form-control middle", data: { qa_selector: 'new_user_email_confirmation_field' }, required: true, title: _("Please retype the email address.")
.form-group.append-bottom-20#password-strength
= f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", data: { qa_selector: 'new_user_password_field' }, required: true, pattern: ".{#{@minimum_password_length},}", title: _("Minimum length is %{minimum_password_length} characters.") % { minimum_password_length: @minimum_password_length }
diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml
index ac5cac50699..77b7a50338c 100644
--- a/app/views/doorkeeper/applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/applications/_delete_form.html.haml
@@ -5,6 +5,6 @@
= button_tag type: "submit", class: "btn btn-transparent", data: { confirm: _("Are you sure?") } do
%span.sr-only
= _('Destroy')
- = icon('trash')
+ = sprite_icon('remove')
- else
= submit_tag _('Destroy'), data: { confirm: _("Are you sure?") }, class: submit_btn_css
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 7fbaa35d1d5..f99db696fd6 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -11,9 +11,6 @@
%span.form-text.text-muted
= _('Use one line per URI')
- - if Doorkeeper.configuration.native_redirect_uri
- %span.form-text.text-muted
- = html_escape(_('Use %{native_redirect_uri} for local tests')) % { native_redirect_uri: tag.code(Doorkeeper.configuration.native_redirect_uri) }
.form-group.form-check
= f.check_box :confidential, class: 'form-check-input'
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index dc16c46476e..a38d6dd3836 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -11,7 +11,7 @@
= link_to event.target_link_options, class: 'has-tooltip event-target-link gl-mr-2', title: event.target_title do
= event.target.reference_link_text
- unless event.milestone?
- %span.event-target-title.gl-mr-2{ dir: "auto" }
+ %span.event-target-title.gl-text-overflow-ellipsis.gl-overflow-hidden.gl-mr-2{ dir: "auto" }
= "&quot;".html_safe + event.target.title + "&quot".html_safe
- else
%span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index a81b999acba..4e936025c74 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -9,7 +9,7 @@
= event_note_title_html(event)
- title = note_target_title(event.target)
- if title.present?
- %span.event-target-title.gl-mr-2{ dir: "auto" }
+ %span.event-target-title.gl-text-overflow-ellipsis.gl-overflow-hidden.gl-mr-2{ dir: "auto" }
= "&quot;".html_safe + title + "&quot".html_safe
= render "events/event_scope", event: event
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index 75a0706ee84..393ab8013e7 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -7,7 +7,7 @@
= f.label :lfs_enabled, class: 'form-check-label' do
%strong
Allow projects within this group to use Git LFS
- = link_to icon('question-circle'), help_page_path('topics/git/lfs/index')
+ = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index')
%br/
%span This setting can be overridden in each project.
.form-group.row
@@ -31,7 +31,7 @@
= f.label :require_two_factor_authentication, class: 'form-check-label' do
%strong
Require all users in this group to set up Two-factor authentication
- = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
+ = link_to sprite_icon('question-o'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
.form-group.row
.offset-sm-2.col-sm-10
.form-check
diff --git a/app/views/groups/_import_group_pane.html.haml b/app/views/groups/_import_group_pane.html.haml
index adfac7d59a5..9ad8ebbb37d 100644
--- a/app/views/groups/_import_group_pane.html.haml
+++ b/app/views/groups/_import_group_pane.html.haml
@@ -41,7 +41,7 @@
= s_('GroupsNew|To copy a GitLab group between installations, navigate to the group settings page for the original installation, generate an export file, and upload it here.')
.row
.form-group.col-sm-12
- = f.label :file, s_('GroupsNew|GitLab group export'), class: 'label-bold'
+ = f.label :file, s_('GroupsNew|Import a GitLab group export file'), class: 'label-bold'
%div
= render 'shared/file_picker_button', f: f, field: :file, help_text: nil
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index c8e58a50b18..ed7b201323a 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -3,6 +3,8 @@
- show_invited_members = can_manage_members && @invited_members.exists?
- show_access_requests = can_manage_members && @requesters.exists?
- invited_active = params[:search_invited].present? || params[:invited_members_page].present?
+- vue_members_list_enabled = Feature.enabled?(:vue_group_members_list, @group)
+- data_attributes = { group_id: @group.id }
- form_item_label_css_class = 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center'
@@ -66,18 +68,24 @@
= render 'groups/group_members/tab_pane/form_item' do
= label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
= render 'shared/members/sort_dropdown'
- %ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
- = render partial: 'shared/members/member', collection: @members, as: :member
- = paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil }
+ - if vue_members_list_enabled
+ .js-group-members-list{ data: { members: members_data_json(@group, @members), **data_attributes } }
+ - else
+ %ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
+ = render partial: 'shared/members/member', collection: @members, as: :member
+ = paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil }
- if @group.shared_with_group_links.any?
#tab-groups.tab-pane
.card.card-without-border
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' do
= html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- %ul.content-list.members-list{ data: { qa_selector: 'groups_list' } }
- - @group.shared_with_group_links.each do |group_link|
- = render 'shared/members/group', group_link: group_link, can_admin_member: can_manage_members, group_link_path: group_group_link_path(@group, group_link)
+ - if vue_members_list_enabled
+ .js-group-linked-list{ data: { members: linked_groups_data_json(@group.shared_with_group_links), **data_attributes } }
+ - else
+ %ul.content-list.members-list{ data: { qa_selector: 'groups_list' } }
+ - @group.shared_with_group_links.each do |group_link|
+ = render 'shared/members/group', group_link: group_link, can_admin_member: can_manage_members, group_link_path: group_group_link_path(@group, group_link)
- if show_invited_members
#tab-invited-members.tab-pane{ class: ('active' if invited_active) }
.card.card-without-border
@@ -86,14 +94,20 @@
= html_escape(_('Members invited to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do
= render 'shared/members/search_field', name: 'search_invited'
- %ul.content-list.members-list
- = render partial: 'shared/members/member', collection: @invited_members, as: :member
- = paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab', params: { page: nil }
+ - if vue_members_list_enabled
+ .js-group-invited-members-list{ data: { members: members_data_json(@group, @invited_members), **data_attributes } }
+ - else
+ %ul.content-list.members-list
+ = render partial: 'shared/members/member', collection: @invited_members, as: :member
+ = paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab', params: { page: nil }
- if show_access_requests
#tab-access-requests.tab-pane
.card.card-without-border
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' do
= html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- %ul.content-list.members-list
- = render partial: 'shared/members/member', collection: @requesters, as: :member
+ - if vue_members_list_enabled
+ .js-group-access-requests-list{ data: { members: members_data_json(@group, @requesters), **data_attributes } }
+ - else
+ %ul.content-list.members-list
+ = render partial: 'shared/members/member', collection: @requesters, as: :member
diff --git a/app/views/groups/packages/_legacy_package_list.haml b/app/views/groups/packages/_legacy_package_list.haml
deleted file mode 100644
index 481a0dbb6e8..00000000000
--- a/app/views/groups/packages/_legacy_package_list.haml
+++ /dev/null
@@ -1,59 +0,0 @@
-- sort_value = @sort
-- sort_title = packages_sort_option_title(sort_value)
-
-- if @packages.any?
- .d-flex.justify-content-end
- .dropdown.inline.gl-mt-3.gl-mb-3.package-sort-dropdown
- .btn-group{ role: 'group' }
- .btn-group{ role: 'group' }
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static', 'qa-selector': 'sort-dropdown-button' }, class: 'btn btn-default' }
- = sort_title
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
- %li
- = sortable_item(sort_title_created_date, package_sort_path(sort: sort_value_recently_created), sort_title)
- = sortable_item(sort_title_name, package_sort_path(sort: sort_value_name_desc), sort_title)
- = sortable_item(sort_title_project_name, package_sort_path(sort: sort_value_project_name_desc), sort_title)
- = sortable_item(sort_title_version, package_sort_path(sort: sort_value_version_desc), sort_title)
- = sortable_item(sort_title_type, package_sort_path(sort: sort_value_type_desc), sort_title)
- = packages_sort_direction_button(sort_value)
-
- .table-holder
- .gl-responsive-table-row.table-row-header.bg-secondary-50.px-2.border-top{ role: 'row' }
- .table-section.section-30{ role: 'rowheader' }
- = _('Name')
- .table-section.section-20{ role: 'rowheader' }
- = _('Project')
- .table-section.section-20{ role: 'rowheader' }
- = _('Version')
- .table-section.section-10{ role: 'rowheader' }
- = _('Type')
- .table-section.section-20{ role: 'rowheader' }
- = _('Created')
- - @packages.each do |package|
- .gl-responsive-table-row{ data: { 'qa-selector': 'package-row' } }
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= _("Name")
- .table-mobile-content.flex-truncate-parent
- = link_to package.name, project_package_path(package.project, package), class: 'flex-truncate-child'
- .table-section.section-20
- .table-mobile-header{ role: "rowheader" }= _("Project")
- .table-mobile-content
- = link_to_project(package.project)
- .table-section.section-20
- .table-mobile-header{ role: "rowheader" }= _("Version")
- .table-mobile-content
- = package.version
- .table-section.section-10
- .table-mobile-header{ role: "rowheader" }= _("Type")
- .table-mobile-content
- = package.package_type
- .table-section.section-20
- .table-mobile-header{ role: "rowheader" }= _("Created")
- .table-mobile-content
- = time_ago_with_tooltip(package.created_at)
- = paginate @packages, theme: "gitlab"
-- else
- .row.empty-state
- .col-12
- = render 'shared/packages/no_packages'
diff --git a/app/views/groups/packages/index.html.haml b/app/views/groups/packages/index.html.haml
index b07c08f50ca..7910217c939 100644
--- a/app/views/groups/packages/index.html.haml
+++ b/app/views/groups/packages/index.html.haml
@@ -1,4 +1,5 @@
-- page_title _("Packages")
+- page_title _("Package Registry")
+- @content_class = "limit-container-width" unless fluid_layout
.row
.col-12
diff --git a/app/views/groups/runners/_index.html.haml b/app/views/groups/runners/_index.html.haml
index 51375f50659..e885fcc08eb 100644
--- a/app/views/groups/runners/_index.html.haml
+++ b/app/views/groups/runners/_index.html.haml
@@ -73,7 +73,7 @@
{{name}}
= button_tag class: 'clear-search hidden' do
- = icon('times')
+ = sprite_icon('close', size: 16, css_class: 'clear-search-icon')
.filter-dropdown-container
= render 'admin/runners/sort_dropdown'
diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml
index 07cbcd8401e..3fc50cc86d2 100644
--- a/app/views/groups/runners/_runner.html.haml
+++ b/app/views/groups/runners/_runner.html.haml
@@ -79,8 +79,8 @@
- if runner.belongs_to_more_than_one_project?
.btn-group
.btn.btn-danger.has-tooltip{ 'aria-label' => 'Remove', 'data-container' => 'body', 'data-original-title' => _('Multi-project Runners cannot be removed'), 'data-placement' => 'top', disabled: 'disabled' }
- = icon('remove')
+ = sprite_icon('close')
- else
.btn-group
= link_to group_runner_path(@group, runner), method: :delete, class: 'btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
- = icon('remove')
+ = sprite_icon('close')
diff --git a/app/views/groups/settings/ci_cd/_form.html.haml b/app/views/groups/settings/ci_cd/_form.html.haml
index 139c710fac0..8fad73f1249 100644
--- a/app/views/groups/settings/ci_cd/_form.html.haml
+++ b/app/views/groups/settings/ci_cd/_form.html.haml
@@ -8,6 +8,6 @@
= f.number_field :max_artifacts_size, class: 'form-control'
%p.form-text.text-muted
= _("Set the maximum file size for each job's artifacts")
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size-core-only'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size'), target: '_blank'
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 366d7dd5afe..5b5f357dbec 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -11,7 +11,7 @@
.settings-header
%h4
= _("General pipelines")
- %button.btn.js-settings-toggle{ type: 'button' }
+ %button.btn.gl-button.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _("Customize your pipeline configuration.")
@@ -28,7 +28,7 @@
.settings-header
%h4
= _('Runners')
- %button.btn.btn-default.js-settings-toggle{ type: "button" }
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
%p
= _("Runners are processes that pick up and execute jobs for GitLab. Here you can register and see your Runners for this project.")
@@ -40,7 +40,7 @@
.settings-header
%h4
= _('Auto DevOps')
- %button.btn.btn-default.js-settings-toggle{ type: "button" }
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
%p
- auto_devops_url = help_page_path('topics/autodevops/index')
diff --git a/app/views/groups/settings/integrations/index.html.haml b/app/views/groups/settings/integrations/index.html.haml
index 96bd6d69a96..f62eb17d236 100644
--- a/app/views/groups/settings/integrations/index.html.haml
+++ b/app/views/groups/settings/integrations/index.html.haml
@@ -5,5 +5,5 @@
%h4= s_('GroupSettings|Apply integration settings to all Projects')
%p
= s_('GroupSettings|Integrations configured here will automatically apply to all projects in this group.')
- = link_to _('Learn more'), '#'
+ = link_to _('Learn more'), integrations_help_page_path, target: '_blank', rel: 'noopener noreferrer'
= render 'shared/integrations/index', integrations: @integrations
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 6ad864121d7..ec4ab603d22 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -2,48 +2,55 @@
- page_title _("Groups")
- @content_class = "limit-container-width" unless fluid_layout
+- if show_invite_banner?(@group)
+ = content_for :group_invite_members_banner do
+ .container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
+ .js-group-invite-members-banner{ data: { svg_path: image_path('illustrations/merge_requests.svg'),
+ is_dismissed_key: "invite_#{@group.id}_#{current_user.id}",
+ track_label: 'invite_members_banner',
+ invite_members_path: group_group_members_path(@group) } }
+
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
= render partial: 'flash_messages'
-%div{ class: [("limit-container-width" unless fluid_layout)] }
- = render_if_exists 'trials/banner', namespace: @group
+= render_if_exists 'trials/banner', namespace: @group
- = render 'groups/home_panel'
+= render 'groups/home_panel'
- = render_if_exists 'groups/self_or_ancestor_marked_for_deletion_notice', group: @group
+= render_if_exists 'groups/self_or_ancestor_marked_for_deletion_notice', group: @group
- = render_if_exists 'groups/group_activity_analytics', group: @group
+= render_if_exists 'groups/group_activity_analytics', group: @group
- .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
- .top-area.group-nav-container.justify-content-between
- .scrolling-tabs-container.inner-page-scroll-tabs
- .fade-left= sprite_icon('chevron-lg-left', size: 12)
- .fade-right= sprite_icon('chevron-lg-right', size: 12)
- %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
- %li.js-subgroups_and_projects-tab
- = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do
- = _("Subgroups and projects")
- %li.js-shared-tab
- = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do
- = _("Shared projects")
- %li.js-archived-tab
- = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do
- = _("Archived projects")
+.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
+ .top-area.group-nav-container.justify-content-between
+ .scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= sprite_icon('chevron-lg-left', size: 12)
+ .fade-right= sprite_icon('chevron-lg-right', size: 12)
+ %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
+ %li.js-subgroups_and_projects-tab
+ = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do
+ = _("Subgroups and projects")
+ %li.js-shared-tab
+ = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do
+ = _("Shared projects")
+ %li.js-archived-tab
+ = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do
+ = _("Archived projects")
- .nav-controls.d-block.d-md-flex
- .group-search
- = render "shared/groups/search_form"
+ .nav-controls.d-block.d-md-flex
+ .group-search
+ = render "shared/groups/search_form"
- = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
+ = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
- .tab-content
- #subgroups_and_projects.tab-pane
- = render "subgroups_and_projects", group: @group
+ .tab-content
+ #subgroups_and_projects.tab-pane
+ = render "subgroups_and_projects", group: @group
- #shared.tab-pane
- = render "shared_projects", group: @group
+ #shared.tab-pane
+ = render "shared_projects", group: @group
- #archived.tab-pane
- = render "archived_projects", group: @group
+ #archived.tab-pane
+ = render "archived_projects", group: @group
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 80df8581a9b..6f917e81fb0 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -40,6 +40,8 @@
%tr
%td.shortcut
%kbd s
+ \/
+ %kbd /
%td= _('Start search')
%tr
%td.shortcut
@@ -72,7 +74,7 @@
%tr
%td.shortcut
%kbd
- %i.fa.fa-arrow-up
+ = sprite_icon('arrow-up', size: 12)
%td= _('Edit your most recent comment in a thread (from an empty textarea)')
%tbody
%tr
@@ -89,28 +91,28 @@
%tr
%td.shortcut
%kbd
- %i.fa.fa-arrow-left
+ = sprite_icon('arrow-left', size: 12)
\/
%kbd h
%td= _('Scroll left')
%tr
%td.shortcut
%kbd
- %i.fa.fa-arrow-right
+ = sprite_icon('arrow-right', size: 12)
\/
%kbd l
%td= _('Scroll right')
%tr
%td.shortcut
%kbd
- %i.fa.fa-arrow-up
+ = sprite_icon('arrow-up', size: 12)
\/
%kbd k
%td= _('Scroll up')
%tr
%td.shortcut
%kbd
- %i.fa.fa-arrow-down
+ = sprite_icon('arrow-down', size: 12)
\/
%kbd j
%td= _('Scroll down')
@@ -118,14 +120,14 @@
%td.shortcut
%kbd
shift
- %i.fa.fa-arrow-up
+ = sprite_icon('arrow-up', size: 12)
\/ k
%td= _('Scroll to top')
%tr
%td.shortcut
%kbd
shift
- %i.fa.fa-arrow-down
+ = sprite_icon('arrow-down', size: 12)
\/ j
%td= _('Scroll to bottom')
.col-lg-4
@@ -229,12 +231,12 @@
%tr
%td.shortcut
%kbd
- %i.fa.fa-arrow-up
+ = sprite_icon('arrow-up', size: 12)
%td= _('Move selection up')
%tr
%td.shortcut
%kbd
- %i.fa.fa-arrow-down
+ = sprite_icon('arrow-down', size: 12)
%td= _('Move selection down')
%tr
%td.shortcut
diff --git a/app/views/import/_project_status.html.haml b/app/views/import/_project_status.html.haml
index 280bcbc1e63..b968db58d38 100644
--- a/app/views/import/_project_status.html.haml
+++ b/app/views/import/_project_status.html.haml
@@ -3,7 +3,7 @@
= icon('check')
= _('Done')
- when 'started'
- = icon("spinner spin")
+ = loading_icon
= _('Started')
- when 'failed'
= _('Failed')
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index 626080c284b..4daa769215f 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -1,7 +1,8 @@
- page_title _("FogBugz Import")
- header_title _("Projects"), root_path
-%h3.page-title
- %i.fa.fa-bug
+%h3.page-title.d-flex
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('bug', css_class: 'gl-mr-2')
= _('Import projects from FogBugz')
%hr
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index cdc53520e93..fb93a3eca0d 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -1,7 +1,8 @@
- page_title _('User map'), _('FogBugz import')
- header_title _("Projects"), root_path
-%h3.page-title
- %i.fa.fa-bug
+%h3.page-title.d-flex
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('bug', css_class: 'gl-mr-2')
= _('Import projects from FogBugz')
%hr
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index f201c0e83fe..e04a412e3bc 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -1,7 +1,8 @@
- page_title _("FogBugz import")
- header_title _("Projects"), root_path
-%h3.page-title
- %i.fa.fa-bug
+%h3.page-title.d-flex
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('bug', css_class: 'gl-mr-2')
= _('Import projects from FogBugz')
%p.light
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index 8d8754e1069..22984d59afe 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -15,11 +15,11 @@
- if @incompatible_repos.any?
= button_tag class: "btn btn-import btn-success js-import-all" do
= _("Import all compatible projects")
- = icon("spinner spin", class: "loading-icon")
+ = loading_icon(css_class: 'loading-icon')
- else
= button_tag class: "btn btn-import btn-success js-import-all" do
= _("Import all projects")
- = icon("spinner spin", class: "loading-icon")
+ = loading_icon(css_class: 'loading-icon')
.table-responsive
%table.table.import-jobs
@@ -45,7 +45,7 @@
%i.fa.fa-check
= _("done")
- when 'started'
- %i.fa.fa-spinner.fa-spin
+ = loading_icon
= _("started")
- else
= project.human_import_status_name
@@ -59,7 +59,7 @@
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
= _("Import")
- = icon("spinner spin", class: "loading-icon")
+ = loading_icon(css_class: 'loading-icon')
- @incompatible_repos.each do |repo|
%tr{ id: "repo_#{repo.id}" }
%td
diff --git a/app/views/import/manifest/_form.html.haml b/app/views/import/manifest/_form.html.haml
index 3a5b5924c6a..2ee964974c3 100644
--- a/app/views/import/manifest/_form.html.haml
+++ b/app/views/import/manifest/_form.html.haml
@@ -16,7 +16,7 @@
= file_field_tag :manifest, class: 'form-control-file w-auto', required: true
.form-text.text-muted
= _('Import multiple repositories by uploading a manifest file.')
- = link_to icon('question-circle'), help_page_path('user/project/import/manifest')
+ = link_to sprite_icon('question-o'), help_page_path('user/project/import/manifest')
.gl-mb-3
= submit_tag _('List available repositories'), class: 'btn btn-success'
diff --git a/app/views/instance_statistics/cohorts/index.html.haml b/app/views/instance_statistics/cohorts/index.html.haml
deleted file mode 100644
index a038246bd53..00000000000
--- a/app/views/instance_statistics/cohorts/index.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-- breadcrumb_title _("Cohorts")
-- page_title _("Cohorts")
-
-- if @cohorts
- = render 'cohorts_table'
-- else
- .bs-callout.bs-callout-warning.clearfix
- %p
- - usage_ping_path = help_page_path('development/telemetry/usage_ping')
- - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
- = s_('User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
- - if current_user.admin?
- - application_settings_path = metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings')
- - application_settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: application_settings_path }
- = s_('To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}.').html_safe % { application_settings_link_start: application_settings_link_start, application_settings_link_end: '</a>'.html_safe }
diff --git a/app/views/instance_statistics/dev_ops_score/_callout.html.haml b/app/views/instance_statistics/dev_ops_score/_callout.html.haml
deleted file mode 100644
index 31ae7721f5f..00000000000
--- a/app/views/instance_statistics/dev_ops_score/_callout.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-.gl-mt-3
-.user-callout{ data: { uid: 'dev_ops_score_intro_callout_dismissed' } }
- .bordered-box.landing.content-block
- %button.btn.btn-default.close.js-close-callout{ type: 'button',
- 'aria-label' => _('Dismiss DevOps Score introduction') }
- = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
- .user-callout-copy
- %h4
- = _('Introducing Your DevOps Score')
- %p
- = _('Your DevOps Score gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers.')
- .svg-container.devops
- = custom_icon('dev_ops_score_overview')
diff --git a/app/views/instance_statistics/dev_ops_score/_disabled.html.haml b/app/views/instance_statistics/dev_ops_score/_disabled.html.haml
deleted file mode 100644
index bd808218f75..00000000000
--- a/app/views/instance_statistics/dev_ops_score/_disabled.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-.container.devops-empty
- .col-sm-12.justify-content-center.text-center
- = custom_icon('dev_ops_score_no_index')
- %h4= _('Usage ping is not enabled')
- - if !current_user.admin?
- %p
- - usage_ping_path = help_page_path('development/telemetry/usage_ping')
- - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
- = s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
- - if current_user.admin?
- %p
- = _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.')
- - if current_user.admin?
- = link_to _('Enable usage ping'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'btn btn-primary'
diff --git a/app/views/instance_statistics/dev_ops_score/_no_data.html.haml b/app/views/instance_statistics/dev_ops_score/_no_data.html.haml
deleted file mode 100644
index 54598244039..00000000000
--- a/app/views/instance_statistics/dev_ops_score/_no_data.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.container.devops-empty
- .col-sm-12.justify-content-center.text-center
- = custom_icon('dev_ops_score_no_data')
- %h4= _('Data is still calculating...')
- %p
- = _('In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index.')
- = link_to _('Learn more'), help_page_path('user/instance_statistics/dev_ops_score'), target: '_blank'
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index 283683511d7..6b3996bee76 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -25,5 +25,5 @@
- if !member?
.actions
- = link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn btn-success"
+ = link_to _("Accept invitation"), accept_invite_url(@token, new_user_invite: params[:new_user_invite]), method: :post, class: "btn btn-success"
= link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn btn-danger gl-ml-3"
diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml
new file mode 100644
index 00000000000..f7ecfd09209
--- /dev/null
+++ b/app/views/jira_connect/subscriptions/index.html.haml
@@ -0,0 +1,28 @@
+%h1
+ GitLab for Jira Configuration
+
+%form#add-subscription-form.subscription-form{ action: jira_connect_subscriptions_path }
+ .ak-field-group
+ %label
+ Namespace
+
+ .ak-field-group.field-group-input
+ %input#namespace-input.ak-field-text{ type: 'text', required: true }
+ %button.ak-button.ak-button__appearance-primary{ type: 'submit' }
+ Link namespace to Jira
+
+%table.subscriptions
+ %thead
+ %tr
+ %th Namespace
+ %th Added
+ %th
+ %tbody
+ - @subscriptions.each do |subscription|
+ %tr
+ %td= subscription.namespace.full_path
+ %td= subscription.created_at
+ %td= link_to 'Remove', jira_connect_subscription_path(subscription), class: 'remove-subscription'
+
+= page_specific_javascript_tag('jira_connect.js')
+= stylesheet_link_tag 'page_bundles/jira_connect'
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index be3f2fd74e4..a0b57f8dd52 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -10,4 +10,4 @@
%span= value
- if %w(alert notice success).include?(key)
%div{ class: "close-icon-wrapper js-close-icon" }
- = sprite_icon('close', css_class: 'close-icon')
+ = sprite_icon('close', css_class: 'close-icon gl-vertical-align-baseline!')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index b869298e99d..1c87452f0a3 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,6 +1,6 @@
- page_description brand_title unless page_description
--# Needs a redirect on the client side since it's using an anchor to distuingish
+-# Needs a redirect on the client side since it's using an anchor to distinguish
-# between sign in and registration. We need to inline the JS to not render
-# anything from this page beforehand.
-# Part of an experiment to build a new sign up flow. Will be removed again with
@@ -16,12 +16,7 @@
%head{ prefix: "og: http://ogp.me/ns#" }
%meta{ charset: "utf-8" }
- - if ActionController::Base.asset_host
- %link{ rel: 'dns-prefetch', href: ActionController::Base.asset_host }
- %link{ rel: 'preconnect', href: ActionController::Base.asset_host, crossorigin: '' }
-
- - if Gitlab::CurrentSettings.snowplow_enabled? && Gitlab::CurrentSettings.snowplow_collector_hostname
- %link{ rel: 'preconnect', href: Gitlab::CurrentSettings.snowplow_collector_hostname, crossorigin: '' }
+ = render 'layouts/loading_hints'
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
@@ -54,6 +49,8 @@
= stylesheet_link_tag_defer "application_dark"
- else
= stylesheet_link_tag_defer "application"
+ - unless use_startup_css?
+ = stylesheet_link_tag_defer "themes/theme_#{user_application_theme_name}"
= stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations']
= stylesheet_link_tag_defer 'performance_bar' if performance_bar_enabled?
@@ -73,7 +70,8 @@
= yield :page_specific_javascripts
= webpack_controller_bundle_tags
- = webpack_bundle_tag "chrome_84_icon_fix" if browser.chrome?([">=84", "<85"]) || browser.edge?([">=84", "<85"])
+ = webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
+ = webpack_bundle_tag "chrome_84_icon_fix" if browser.chrome?([">=84", "<84.0.4147.125"]) || browser.edge?([">=84", "<84.0.522.59"])
= yield :project_javascripts
diff --git a/app/views/layouts/_loading_hints.html.haml b/app/views/layouts/_loading_hints.html.haml
new file mode 100644
index 00000000000..0ef50d1b122
--- /dev/null
+++ b/app/views/layouts/_loading_hints.html.haml
@@ -0,0 +1,10 @@
+- if ActionController::Base.asset_host
+ %link{ rel: 'dns-prefetch', href: ActionController::Base.asset_host }
+ %link{ rel: 'preconnect', href: ActionController::Base.asset_host, crossorigin: '' }
+- if user_application_theme == 'gl-dark'
+ %link{ { rel: 'preload', href: stylesheet_url('application_dark'), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
+- else
+ %link{ { rel: 'preload', href: stylesheet_url('application'), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
+%link{ { rel: 'preload', href: stylesheet_url("highlight/themes/#{user_color_scheme}"), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
+- if Gitlab::CurrentSettings.snowplow_enabled? && Gitlab::CurrentSettings.snowplow_collector_hostname
+ %link{ rel: 'preconnect', href: Gitlab::CurrentSettings.snowplow_collector_hostname, crossorigin: '' }
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 3a543fef292..5184bc93a81 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -3,9 +3,9 @@
= render "layouts/nav/sidebar/#{nav}"
.content-wrapper{ class: "#{@content_wrapper_class}" }
.mobile-overlay
+ = yield :group_invite_members_banner
.alert-wrapper
= render 'shared/outdated_browser'
- = render_if_exists 'layouts/header/users_over_license_banner'
= render_if_exists "layouts/header/licensed_user_count_threshold"
= render_if_exists "layouts/header/token_expiry_notification"
= render "layouts/broadcast"
diff --git a/app/views/layouts/_startup_css.haml b/app/views/layouts/_startup_css.haml
index 094038d39b0..ea05157ed19 100644
--- a/app/views/layouts/_startup_css.haml
+++ b/app/views/layouts/_startup_css.haml
@@ -1,4 +1,7 @@
- return unless use_startup_css?
+- startup_filename = current_path?("sessions#new") ? 'signin' : user_application_theme == 'gl-dark' ? 'dark' : 'general'
+
%style{ type: "text/css" }
- = Rails.application.assets_manifest.find_sources('startup/startup-general.css').first.to_s.html_safe
+ = Rails.application.assets_manifest.find_sources("themes/theme_#{user_application_theme_name}.css").first.to_s.html_safe
+ = Rails.application.assets_manifest.find_sources("startup/startup-#{startup_filename}.css").first.to_s.html_safe
diff --git a/app/views/layouts/_startup_css_activation.haml b/app/views/layouts/_startup_css_activation.haml
index 0b1cce06f47..022b9a695bc 100644
--- a/app/views/layouts/_startup_css_activation.haml
+++ b/app/views/layouts/_startup_css_activation.haml
@@ -3,5 +3,8 @@
= javascript_tag nonce: true do
:plain
document.querySelectorAll('link[media="print"]').forEach(linkTag => {
- linkTag.addEventListener('load', function() {this.media='all'}, {once: true});
+ linkTag.setAttribute('data-startupcss', 'loading');
+ const startupLinkLoadedEvent = new CustomEvent('CSSStartupLinkLoaded');
+ linkTag.addEventListener('load',function(){this.media='all';this.setAttribute('data-startupcss', 'loaded');document.dispatchEvent(startupLinkLoadedEvent);},{once: true});
})
+- return unless use_startup_css?
diff --git a/app/views/layouts/_startup_js.html.haml b/app/views/layouts/_startup_js.html.haml
index 3eb68df07c6..33c759b7a7c 100644
--- a/app/views/layouts/_startup_js.html.haml
+++ b/app/views/layouts/_startup_js.html.haml
@@ -6,8 +6,11 @@
gl.startup_calls = #{page_startup_api_calls.to_json};
if (gl.startup_calls && window.fetch) {
Object.keys(gl.startup_calls).forEach(apiCall => {
+ // fetch won’t send cookies in older browsers, unless you set the credentials init option.
+ // We set to `same-origin` which is default value in modern browsers.
+ // See https://github.com/whatwg/fetch/pull/585 for more information.
gl.startup_calls[apiCall] = {
- fetchCall: fetch(apiCall)
+ fetchCall: fetch(apiCall, { credentials: 'same-origin' })
};
});
}
diff --git a/app/views/layouts/experiment_mailer.html.haml b/app/views/layouts/experiment_mailer.html.haml
new file mode 100644
index 00000000000..5a342c400d6
--- /dev/null
+++ b/app/views/layouts/experiment_mailer.html.haml
@@ -0,0 +1,48 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%html{ lang: "en" }
+ %head
+ %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
+ %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
+ %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
+ %title= message.subject
+
+ -# Avoid premailer processing of client-specific styles (@media tag not supported)
+ -# We need to inline the contents here because mail clients (e.g. iOS Mail, Outlook)
+ -# do not support linked stylesheets.
+ %style{ type: 'text/css', 'data-premailer': 'ignore' }
+ = asset_to_string('mailer_client_specific.css').html_safe
+
+ = stylesheet_link_tag 'mailer.css'
+ %body
+ %table#body{ border: "0", cellpadding: "0", cellspacing: "0" }
+ %tbody
+ %tr.line
+ %td
+ %tr.header
+ %td
+ = html_header_message
+ = header_logo
+ %tr
+ %td
+ %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0" }
+ %tbody
+ %tr
+ %td.wrapper-cell{ style: "padding: 0" }
+ %table.content{ border: "0", cellpadding: "0", cellspacing: "0" }
+ %tbody
+ = yield
+
+ = render_if_exists 'layouts/mailer/additional_text'
+
+ %tr.footer
+ %td{ style: "padding: 24px 0" }
+ %img{ alt: "GitLab", height: "33", width: "90", src: image_url('mailers/gitlab_footer_logo.gif') }
+ %p{ style: "color: #949ba5; max-width: 640px; margin: 0 auto; text-align: left; font-size: 12px;" }
+ GitLab is a complete DevOps platform, delivered as a single application, fundamentally changing the way
+ %br
+ Development, Security, and Ops teams collaborate.
+
+ = yield :additional_footer
+ %tr
+ %td.footer-message
+ = html_footer_message
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 4c659241f99..dcc6cba8444 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -14,7 +14,11 @@
%li.divider
- if can?(current_user, :update_user_status, current_user)
%li
- .js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } }
+ %button.btn.menu-item.js-set-status-modal-trigger{ type: 'button' }
+ - if current_user.status.present?
+ = s_('SetStatusModal|Edit status')
+ - else
+ = s_('SetStatusModal|Set status')
- if current_user_menu?(:profile)
%li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
@@ -35,6 +39,8 @@
= link_to _("Help"), help_path
%li.d-md-none
= link_to _("Support"), support_url
+ %li.d-md-none
+ = render 'shared/help_dropdown_forum_link'
%li.d-md-none
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 56b70c463d0..845231238f6 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -66,7 +66,7 @@
track_property: 'navigation',
container: 'body' } do
= sprite_icon('todo-done')
- %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count == 0) }
+ %span.badge.badge-pill.todos-count.js-todos-count{ class: ('hidden' if todos_pending_count == 0) }
= todos_count_format(todos_pending_count)
%li.nav-item.header-help.dropdown.d-none.d-md-block{ **tracking_attrs('main_navigation', 'click_question_mark_link', 'navigation') }
= link_to help_path, class: 'header-help-dropdown-toggle', data: { toggle: "dropdown" } do
@@ -87,7 +87,7 @@
- if has_impersonation_link
%li.nav-item.impersonation.ml-0
= link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body', qa_selector: 'stop_impersonation_link' } do
- = icon('user-secret')
+ = sprite_icon('incognito', size: 18)
- if header_link?(:sign_in)
%li.nav-item
%div
@@ -100,7 +100,7 @@
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if ::Feature.enabled?(:whats_new_drawer)
- #whats-new-app
+ #whats-new-app{ data: { features: whats_new_most_recent_release_items } }
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index ad4e0f1f4b2..2b6cbc1c0ef 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -6,6 +6,8 @@
%li
= link_to _("Support"), support_url
%li
+ = render 'shared/help_dropdown_forum_link'
+ %li
%button.js-shortcuts-modal-trigger{ type: "button" }
= _("Keyboard shortcuts")
%span.text-secondary.float-right{ "aria-hidden": true }= '?'.html_safe
diff --git a/app/views/layouts/instance_statistics.html.haml b/app/views/layouts/instance_statistics.html.haml
deleted file mode 100644
index 1de6b385c86..00000000000
--- a/app/views/layouts/instance_statistics.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- page_title _('Analytics')
-- header_title _('Analytics'), instance_statistics_root_path
-- nav 'instance_statistics'
-- @left_sidebar = true
-
-= render template: 'layouts/application'
diff --git a/app/views/layouts/jira_connect.html.haml b/app/views/layouts/jira_connect.html.haml
new file mode 100644
index 00000000000..fdeb3d3c9ac
--- /dev/null
+++ b/app/views/layouts/jira_connect.html.haml
@@ -0,0 +1,13 @@
+%html{ lang: "en" }
+ %head
+ %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
+ %title
+ GitLab
+ = stylesheet_link_tag 'https://unpkg.com/@atlaskit/css-reset@3.0.6/dist/bundle.css'
+ = stylesheet_link_tag 'https://unpkg.com/@atlaskit/reduced-ui-pack@10.5.5/dist/bundle.css'
+ = javascript_include_tag 'https://connect-cdn.atl-paas.net/all.js'
+ = javascript_include_tag 'https://unpkg.com/jquery@3.3.1/dist/jquery.min.js'
+ = yield :head
+ %body
+ .ac-content
+ = yield
diff --git a/app/views/layouts/nav/_analytics_link.html.haml b/app/views/layouts/nav/_analytics_link.html.haml
deleted file mode 100644
index f481aeecc1b..00000000000
--- a/app/views/layouts/nav/_analytics_link.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- return unless dashboard_nav_link?(:analytics)
-= nav_link(controller: [:dev_ops_score, :cohorts], html_options: { class: "d-none d-xl-block"}) do
- = link_to instance_statistics_root_path, class: 'chart-icon', title: _('Analytics'), aria: { label: _('Analytics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = sprite_icon('chart', size: 18)
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 29cacbe4aff..40ea42091bd 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -43,8 +43,6 @@
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
= _('Snippets')
- = render_if_exists 'layouts/nav/sidebar/analytics_link'
-
%li.dropdown
= render_if_exists 'dashboard/nav_link_list'
@@ -66,8 +64,6 @@
= link_to sherlock_transactions_path, class: 'admin-icon' do
= _('Sherlock Transactions')
- = render_if_exists 'layouts/nav/analytics_link'
-
- if current_user.admin?
= nav_link(controller: 'admin/dashboard', html_options: { class: "d-none d-xl-block"}) do
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 7fb5fff1e05..cb5277c02f0 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -48,6 +48,33 @@
%span
= _('Gitaly Servers')
+ = nav_link(controller: admin_analytics_nav_links) do
+ = link_to admin_dev_ops_report_path, data: { qa_selector: 'admin_analytics_link' } do
+ .nav-icon-container
+ = sprite_icon('chart')
+ %span.nav-item-name
+ = _('Analytics')
+
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_analytics_submenu_content' } }
+ = nav_link(controller: admin_analytics_nav_links, html_options: { class: "fly-out-top-item" }) do
+ = link_to admin_dev_ops_report_path do
+ %strong.fly-out-top-item-name
+ = _('Analytics')
+ %li.divider.fly-out-top-item
+ = nav_link(controller: :dev_ops_report) do
+ = link_to admin_dev_ops_report_path, title: _('DevOps Report') do
+ %span
+ = _('DevOps Report')
+ = nav_link(controller: :cohorts) do
+ = link_to admin_cohorts_path, title: _('Cohorts') do
+ %span
+ = _('Cohorts')
+ - if Feature.enabled?(:instance_statistics)
+ = nav_link(controller: :instance_statistics) do
+ = link_to admin_instance_statistics_path, title: _('Instance Statistics') do
+ %span
+ = _('Instance Statistics')
+
= nav_link(controller: admin_monitoring_nav_links) do
= link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_link' } do
.nav-icon-container
diff --git a/app/views/layouts/nav/sidebar/_analytics_link.html.haml b/app/views/layouts/nav/sidebar/_analytics_link.html.haml
deleted file mode 100644
index 9e5ae422e2d..00000000000
--- a/app/views/layouts/nav/sidebar/_analytics_link.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- return unless dashboard_nav_link?(:analytics)
-= nav_link(controller: [:dev_ops_score, :cohorts]) do
- = link_to instance_statistics_root_path, class: 'd-xl-none' do
- = _('Analytics')
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 47dad21edd7..9e9e6493e5b 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -157,6 +157,12 @@
%span
= _('General')
+ - if group_level_integrations?
+ = nav_link(controller: :integrations) do
+ = link_to group_settings_integrations_path(@group), title: _('Integrations') do
+ %span
+ = _('Integrations')
+
= nav_link(path: 'groups#projects') do
= link_to projects_group_path(@group), title: _('Projects') do
%span
@@ -171,11 +177,6 @@
= link_to group_settings_ci_cd_path(@group), title: _('CI / CD') do
%span
= _('CI / CD')
- - if Feature.enabled?(:group_level_integrations, @group)
- = nav_link(controller: :integrations) do
- = link_to group_settings_integrations_path(@group), title: _('Integrations') do
- %span
- = _('Integrations')
= render_if_exists "groups/ee/settings_nav"
diff --git a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
deleted file mode 100644
index 979d98ec382..00000000000
--- a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
- .nav-sidebar-inner-scroll
- .context-header
- = link_to instance_statistics_root_path, title: _('Analytics') do
- .avatar-container.s40.settings-avatar
- = sprite_icon('chart', size: 24)
- .sidebar-context-title= _('Analytics')
- %ul.sidebar-top-level-items
- = render 'layouts/nav/sidebar/instance_statistics_links'
-
- = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_instance_statistics_links.html.haml b/app/views/layouts/nav/sidebar/_instance_statistics_links.html.haml
deleted file mode 100644
index ee2c83dc31e..00000000000
--- a/app/views/layouts/nav/sidebar/_instance_statistics_links.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-- return unless can?(current_user, :read_instance_statistics)
-= nav_link(controller: :dev_ops_score) do
- = link_to instance_statistics_dev_ops_score_index_path do
- .nav-icon-container
- = sprite_icon('comment')
- %span.nav-item-name
- = _('DevOps Score')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :dev_ops_score, html_options: { class: "fly-out-top-item" } ) do
- = link_to instance_statistics_dev_ops_score_index_path do
- %strong.fly-out-top-item-name
- = _('DevOps Score')
-
-- if Gitlab::CurrentSettings.usage_ping_enabled
- = nav_link(controller: :cohorts) do
- = link_to instance_statistics_cohorts_path do
- .nav-icon-container
- = sprite_icon('users')
- %span.nav-item-name
- = _('Cohorts')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do
- = link_to instance_statistics_cohorts_path do
- %strong.fly-out-top-item-name
- = _('Cohorts')
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 054311214ab..0eef587d7c7 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -167,7 +167,7 @@
= render_if_exists "layouts/nav/requirements_link", project: @project
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], unless: -> { current_path?('projects/pipelines#charts') }) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases], unless: -> { current_path?('projects/pipelines#charts') }) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
.nav-icon-container
= sprite_icon('rocket')
@@ -175,7 +175,7 @@
= _('CI / CD')
%ul.sidebar-sub-level-items
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], html_options: { class: "fly-out-top-item" }) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases], html_options: { class: "fly-out-top-item" }) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
= _('CI / CD')
@@ -204,6 +204,8 @@
%span
= _('Schedules')
+ = render_if_exists "layouts/nav/test_cases_link", project: @project
+
= render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
- if project_nav_tab? :operations
diff --git a/app/views/notify/_failed_builds.html.haml b/app/views/notify/_failed_builds.html.haml
index 1711c34a842..cde0ac21d6d 100644
--- a/app/views/notify/_failed_builds.html.haml
+++ b/app/views/notify/_failed_builds.html.haml
@@ -23,10 +23,3 @@
= build.stage
%td{ align: "right", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
= render "notify/links/#{build.to_partial_path}", pipeline: pipeline, build: build
- %tr.build-log
- - if build.has_trace?
- %td{ colspan: "2", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 0 16px;" }
- %pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" }
- = build.trace.html(last_lines: 30).html_safe
- - else
- %td{ colspan: "2" }
diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb
index 91092060e74..f849c017265 100644
--- a/app/views/notify/autodevops_disabled_email.text.erb
+++ b/app/views/notify/autodevops_disabled_email.text.erb
@@ -14,7 +14,4 @@ had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
Stage: <%= build.stage %>
Name: <%= build.name %>
- <% if build.has_trace? -%>
- Trace: <%= build.trace.raw(last_lines: 30) %>
- <% end -%>
<% end -%>
diff --git a/app/views/notify/disabled_two_factor_email.html.haml b/app/views/notify/disabled_two_factor_email.html.haml
new file mode 100644
index 00000000000..8c64a43fc07
--- /dev/null
+++ b/app/views/notify/disabled_two_factor_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ = say_hi(@user)
+%p
+ = two_factor_authentication_disabled_text
+%p
+ = re_enable_two_factor_authentication_text(format: :html)
diff --git a/app/views/notify/disabled_two_factor_email.text.erb b/app/views/notify/disabled_two_factor_email.text.erb
new file mode 100644
index 00000000000..46eeab4414f
--- /dev/null
+++ b/app/views/notify/disabled_two_factor_email.text.erb
@@ -0,0 +1,5 @@
+<%= say_hi(@user) %>
+
+<%= two_factor_authentication_disabled_text %>
+
+<%= re_enable_two_factor_authentication_text %>
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
index ae3fecf404a..4fcd2936d25 100644
--- a/app/views/notify/member_invited_email.html.haml
+++ b/app/views/notify/member_invited_email.html.haml
@@ -10,7 +10,7 @@
#{member_source.model_name.singular} as #{content_tag :span, member.human_access, class: :highlight}.
%p
- = link_to 'Accept invitation', invite_url(@token)
+ = link_to 'Accept invitation', invite_url(@token, @invite_url_params)
or
= link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb
index d944c3b4a50..e6e6a685f92 100644
--- a/app/views/notify/member_invited_email.text.erb
+++ b/app/views/notify/member_invited_email.text.erb
@@ -1,4 +1,4 @@
You have been invited <%= "by #{sanitize_name(member.created_by.name)} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
-Accept invitation: <%= invite_url(@token) %>
+Accept invitation: <%= invite_url(@token, @invite_url_params) %>
Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/member_invited_email_experiment.html.haml b/app/views/notify/member_invited_email_experiment.html.haml
new file mode 100644
index 00000000000..5cfb6acee05
--- /dev/null
+++ b/app/views/notify/member_invited_email_experiment.html.haml
@@ -0,0 +1,12 @@
+%tr
+ %td.text-content
+ %h2.invite-header
+ = s_('InviteEmail|You are invited!')
+ %p
+ - if member.created_by
+ = html_escape(s_("InviteEmail|%{inviter} invited you")) % { inviter: (link_to member.created_by.name, user_url(member.created_by)).html_safe }
+ = html_escape(s_("InviteEmail|to join the %{strong_start}%{project_or_group_name}%{strong_end}")) % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_or_group_name: member_source.human_name }
+ %br
+ = s_("InviteEmail|%{project_or_group} as a %{role}") % { project_or_group: member_source.model_name.singular, role: member.human_access.downcase }
+ %p.invite-actions
+ = link_to s_('InviteEmail|Join now'), invite_url(@token, @invite_url_params), class: 'invite-btn-join'
diff --git a/app/views/notify/member_invited_email_experiment.text.erb b/app/views/notify/member_invited_email_experiment.text.erb
new file mode 100644
index 00000000000..6843cea4df7
--- /dev/null
+++ b/app/views/notify/member_invited_email_experiment.text.erb
@@ -0,0 +1,10 @@
+<% project_and_role = s_('InviteEmail|to join the %{project_or_group_name} %{project_or_group} as a %{role}') \
+ % { project_or_group_name: member_source.human_name, project_or_group: member_source.model_name.singular, role: member.human_access.downcase } %>
+
+<% if member.created_by %>
+<%= s_('InviteEmail|%{inviter} invited you') % { inviter: sanitize_name(member.created_by.name) } %> <%= project_and_role %>
+<% else %>
+<%= s_('InviteEmail|You have been invited') %> <%= project_and_role %>
+<% end %>
+
+Join now: <%= invite_url(@token, @invite_url_params) %>
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index 41b26842dbc..b388aad7048 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -34,8 +34,4 @@ had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
Stage: <%= build.stage %>
Name: <%= build.name %>
-<% if build.has_trace? -%>
-Trace: <%= build.trace.raw(last_lines: 30) %>
-<% end -%>
-
<% end -%>
diff --git a/app/views/profiles/accounts/_providers.html.haml b/app/views/profiles/accounts/_providers.html.haml
index a87191d0fa4..f7368c5e921 100644
--- a/app/views/profiles/accounts/_providers.html.haml
+++ b/app/views/profiles/accounts/_providers.html.haml
@@ -11,11 +11,11 @@
- if auth_active?(provider)
- if unlink_allowed
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
- = s_('Profiles|Disconnect')
+ = s_('Profiles|Disconnect %{provider}') % { provider: label_for_provider(provider) }
- else
%a.provider-btn
- = s_('Profiles|Active')
+ = s_('Profiles|%{provider} Active') % { provider: label_for_provider(provider) }
- elsif link_allowed
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn gl-text-blue-500' do
- = s_('Profiles|Connect')
+ = s_('Profiles|Connect %{provider}') % { provider: label_for_provider(provider) }
= render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 20660e61f38..c875caca94a 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -55,8 +55,8 @@
= s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user
- %button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal',
- target: '#delete-account-modal', qa_selector: 'delete_account_button' } }
+ -# Delete button here
+ %button#delete-account-button.btn.btn-danger.disabled{ data: { qa_selector: 'delete_account_button' } }
= s_('Profiles|Delete account')
#delete-account-modal{ data: { action_url: user_registration_path,
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
index e05f121c5d9..f1abafa4149 100644
--- a/app/views/profiles/gpg_keys/_key.html.haml
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -21,7 +21,7 @@
= s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)}
= link_to profile_gpg_key_path(key), data: { confirm: _('Are you sure? Removing this GPG key does not affect already signed commits.') }, method: :delete, class: "btn btn-danger gl-ml-3" do
%span.sr-only= _('Remove')
- = icon('trash')
+ = sprite_icon('remove')
= link_to revoke_profile_gpg_key_path(key), data: { confirm: _('Are you sure? All commits that were signed with this GPG key will be unverified.') }, method: :put, class: "btn btn-danger gl-ml-3" do
%span.sr-only= _('Revoke')
= _('Revoke')
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index 404bb224655..ea698a296fb 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -2,7 +2,7 @@
.gl-responsive-table-row.notification-list-item
.table-section.section-40
- %span.notification.fa.fa-holder.gl-mr-2
+ %span.notification.gl-mr-2
= notification_icon(notification_icon_level(setting, emails_disabled))
%span.str-truncated
diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml
index f9172ae87aa..6e81d585f24 100644
--- a/app/views/profiles/notifications/_project_settings.html.haml
+++ b/app/views/profiles/notifications/_project_settings.html.haml
@@ -1,7 +1,7 @@
- emails_disabled = project.emails_disabled?
%li.notification-list-item
- %span.notification.fa.fa-holder.gl-mr-2
+ %span.notification.gl-mr-2
= notification_icon(notification_icon_level(setting, emails_disabled))
%span.str-truncated
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index ab04d977a4d..da684c29372 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -39,10 +39,11 @@
%hr
%h5
- = _('Groups (%{count})') % { count: @group_notifications.size }
+ = _('Groups (%{count})') % { count: @user_groups.total_count }
%div
- @group_notifications.each do |setting|
= render 'group_settings', setting: setting, group: setting.source
+ = paginate @user_groups, theme: 'gitlab'
%h5
= _('Projects (%{count})') % { count: @project_notifications.size }
%p.account-well
diff --git a/app/views/profiles/preferences/_gitpod.html.haml b/app/views/profiles/preferences/_gitpod.html.haml
new file mode 100644
index 00000000000..69c9443ebbb
--- /dev/null
+++ b/app/views/profiles/preferences/_gitpod.html.haml
@@ -0,0 +1,11 @@
+- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer')
+
+%label.label-bold#gitpod
+ = s_('Gitpod')
+= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
+.form-group.form-check
+ = f.check_box :gitpod_enabled, class: 'form-check-input'
+ = f.label :gitpod_enabled, class: 'form-check-label' do
+ = s_('Gitpod|Enable Gitpod integration').html_safe
+ .form-text.text-muted
+ = s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link }
diff --git a/app/views/profiles/preferences/_integrations.html.haml b/app/views/profiles/preferences/_integrations.html.haml
new file mode 100644
index 00000000000..037fe5df263
--- /dev/null
+++ b/app/views/profiles/preferences/_integrations.html.haml
@@ -0,0 +1,18 @@
+- views = integration_views
+- return unless views.any?
+
+.col-sm-12
+ %hr
+
+.col-lg-4.profile-settings-sidebar#integrations
+ %h4.gl-mt-0
+ = s_('Preferences|Integrations')
+ %p
+ = s_('Preferences|Customize integrations with third party services.')
+ = succeed '.' do
+ = link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'integrations'), target: '_blank'
+
+.col-lg-8
+ - views.each do |view|
+ = render view, f: f
+
diff --git a/app/views/profiles/preferences/_sourcegraph.html.haml b/app/views/profiles/preferences/_sourcegraph.html.haml
index f3530da9a5f..fdd0be22664 100644
--- a/app/views/profiles/preferences/_sourcegraph.html.haml
+++ b/app/views/profiles/preferences/_sourcegraph.html.haml
@@ -1,26 +1,10 @@
-- return unless Gitlab::Sourcegraph::feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
-- sourcegraph_url = Gitlab::CurrentSettings.sourcegraph_url
-
-.col-sm-12
- %hr
-
-.col-lg-4.profile-settings-sidebar#integrations
- %h4.gl-mt-0
- = s_('Preferences|Integrations')
- %p
- = s_('Preferences|Customize integrations with third party services.')
- = succeed '.' do
- = link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'integrations'), target: '_blank'
-.col-lg-8
- %label.label-bold
- = s_('Preferences|Sourcegraph')
- = link_to icon('question-circle'), help_page_path('user/profile/preferences.md', anchor: 'sourcegraph'), target: '_blank', class: 'has-tooltip', title: _('More information')
- .form-group.form-check
- = f.check_box :sourcegraph_enabled, class: 'form-check-input'
- = f.label :sourcegraph_enabled, class: 'form-check-label' do
- - link_start = '<a href="%{url}">'.html_safe % { url: sourcegraph_url }
- - link_end = '</a>'.html_safe
- = s_('Preferences|Enable integrated code intelligence on code views').html_safe % { link_start: link_start, link_end: link_end }
- .form-text.text-muted
- = sourcegraph_url_message
- = sourcegraph_experimental_message
+%label.label-bold
+ = s_('Preferences|Sourcegraph')
+= link_to sprite_icon('question-o'), help_page_path('user/profile/preferences.md', anchor: 'sourcegraph'), target: '_blank', class: 'has-tooltip', title: _('More information')
+.form-group.form-check
+ = f.check_box :sourcegraph_enabled, class: 'form-check-input'
+ = f.label :sourcegraph_enabled, class: 'form-check-label' do
+ = s_('Preferences|Enable integrated code intelligence on code views').html_safe
+ .form-text.text-muted
+ = sourcegraph_url_message
+ = sourcegraph_experimental_message
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 54ca8788864..2c705886f47 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,6 +1,9 @@
- page_title _('Preferences')
- @content_class = "limit-container-width" unless fluid_layout
+- Gitlab::Themes.each do |theme|
+ = stylesheet_link_tag "themes/theme_#{theme.css_class.gsub('ui-', '')}"
+
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row gl-mt-3 js-preferences-form' } do |f|
.col-lg-4.application-theme#navigation-theme
%h4.gl-mt-0
@@ -135,7 +138,7 @@
.form-text.text-muted
= s_('Preferences|For example: 30 mins ago.')
- = render 'sourcegraph', f: f
+ = render 'integrations', f: f
.col-lg-4.profile-settings-sidebar
.col-lg-8
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 8397acbf1b3..241262880c1 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -14,7 +14,7 @@ $('input[type=submit]').enable()
// Show flash messages
<% if flash.notice %>
- new Flash('<%= flash.discard(:notice) %>', 'notice')
+ new Flash({ message: '<%= flash.discard(:notice) %>', type: 'notice'})
<% elsif flash.alert %>
- new Flash('<%= flash.discard(:alert) %>', 'alert')
+ new Flash({ message: '<%= flash.discard(:alert) %>', type: 'alert'})
<% end %>
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 672f9c9a0c0..1eb3a14525f 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -113,7 +113,7 @@
- private_profile_label = capture do
= s_("Profiles|Don't display activity-related personal information on your profiles")
= f.check_box :private_profile, label: private_profile_label, inline: true, wrapper_class: 'mr-0'
- = link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile')
+ = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'private-profile')
%h5= s_("Profiles|Private contributions")
= f.check_box :include_private_contributions, label: s_('Profiles|Include private contributions on my profile'), wrapper_class: 'mb-2', inline: true
.help-block
@@ -135,10 +135,12 @@
%img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") }
.crop-controls
.btn-group
- %button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } }
- %span.fa.fa-search-plus
%button.btn.btn-primary{ data: { method: 'zoom', option: '-0.1' } }
- %span.fa.fa-search-minus
+ %span
+ = sprite_icon('search-minus')
+ %button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } }
+ %span
+ = sprite_icon('search-plus')
.modal-footer
%button.btn.btn-primary.js-upload-user-avatar{ type: 'button' }
= s_("Profiles|Set new profile picture")
diff --git a/app/views/profiles/two_factor_auths/create.html.haml b/app/views/profiles/two_factor_auths/create.html.haml
index 973eb8136c4..5a756cca0ab 100644
--- a/app/views/profiles/two_factor_auths/create.html.haml
+++ b/app/views/profiles/two_factor_auths/create.html.haml
@@ -1,6 +1,6 @@
- page_title _('Two-factor Authentication'), _('Account')
-.alert.alert-success
+.gl-alert.gl-alert-success.gl-mb-5
= _('Congratulations! You have enabled Two-factor Authentication!')
= render 'codes'
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index bce43b16d27..82265938180 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -1,6 +1,7 @@
- page_title _('Two-Factor Authentication'), _('Account')
- add_to_breadcrumbs(_('Two-Factor Authentication'), profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout
+- webauthn_enabled = Feature.enabled?(:webauthn)
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
.row.gl-mt-3
@@ -18,7 +19,7 @@
%div
= link_to _('Disable two-factor authentication'), profile_two_factor_auth_path,
method: :delete,
- data: { confirm: _('Are you sure? This will invalidate your registered applications and U2F devices.') },
+ data: { confirm: webauthn_enabled ? _('Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.') : _('Are you sure? This will invalidate your registered applications and U2F devices.') },
class: 'btn btn-danger gl-mr-3'
= form_tag codes_profile_two_factor_auth_path, {style: 'display: inline-block', method: :post} do |f|
= submit_tag _('Regenerate recovery codes'), class: 'btn'
@@ -45,7 +46,7 @@
= _('Time based: Yes')
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error
- .alert.alert-danger
+ .gl-alert.gl-alert-danger.gl-mb-5
= @error
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
@@ -58,22 +59,35 @@
.row.gl-mt-3
.col-lg-4
%h4.gl-mt-0
- = _('Register Universal Two-Factor (U2F) Device')
+ - if webauthn_enabled
+ = _('Register WebAuthn Device')
+ - else
+ = _('Register Universal Two-Factor (U2F) Device')
%p
= _('Use a hardware device to add the second factor of authentication.')
%p
- = _("As U2F devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a U2F device. That way you'll always be able to log in - even when you're using an unsupported browser.")
+ - if webauthn_enabled
+ = _("As WebAuthn devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a WebAuthn device. That way you'll always be able to log in - even when you're using an unsupported browser.")
+ - else
+ = _("As U2F devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a U2F device. That way you'll always be able to log in - even when you're using an unsupported browser.")
.col-lg-8
- - if @u2f_registration.errors.present?
- = form_errors(@u2f_registration)
- = render "u2f/register"
+ - registration = webauthn_enabled ? @webauthn_registration : @u2f_registration
+ - if registration.errors.present?
+ = form_errors(registration)
+ - if webauthn_enabled
+ = render "authentication/register", target_path: create_webauthn_profile_two_factor_auth_path
+ - else
+ = render "authentication/register", target_path: create_u2f_profile_two_factor_auth_path
%hr
%h5
- = _('U2F Devices (%{length})') % { length: @u2f_registrations.length }
+ - if webauthn_enabled
+ = _('WebAuthn Devices (%{length})') % { length: @registrations.length }
+ - else
+ = _('U2F Devices (%{length})') % { length: @registrations.length }
- - if @u2f_registrations.present?
+ - if @registrations.present?
.table-responsive
%table.table.table-bordered.u2f-registrations
%colgroup
@@ -86,12 +100,15 @@
%th= s_('2FADevice|Registered On')
%th
%tbody
- - @u2f_registrations.each do |registration|
+ - @registrations.each do |registration|
%tr
- %td= registration.name.presence || html_escape_once(_("&lt;no name set&gt;")).html_safe
- %td= registration.created_at.to_date.to_s(:medium)
- %td= link_to _('Delete'), profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.') }
+ %td= registration[:name].presence || html_escape_once(_("&lt;no name set&gt;")).html_safe
+ %td= registration[:created_at].to_date.to_s(:medium)
+ %td= link_to _('Delete'), registration[:delete_path], method: :delete, class: "btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.') }
- else
.settings-message.text-center
- = _("You don't have any U2F devices registered yet.")
+ - if webauthn_enabled
+ = _("You don't have any WebAuthn devices registered yet.")
+ - else
+ = _("You don't have any U2F devices registered yet.")
diff --git a/app/views/projects/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml
index 4f3698f91e6..7e65f2f1cef 100644
--- a/app/views/projects/_deletion_failed.html.haml
+++ b/app/views/projects/_deletion_failed.html.haml
@@ -1,6 +1,8 @@
- project = local_assigns.fetch(:project)
- return unless project.delete_error.present?
-.project-deletion-failed-message.alert.alert-warning
- This project was scheduled for deletion, but failed with the following message:
- = project.delete_error
+.project-deletion-failed-message.gl-alert.gl-alert-warning
+ = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ This project was scheduled for deletion, but failed with the following message:
+ = project.delete_error
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index dd7971f6db0..fe3354aefbb 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -46,7 +46,8 @@
- if fogbugz_import_enabled?
%div
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz', **tracking_attrs(track_label, 'click_button', 'fogbugz') do
- = icon('bug', text: 'FogBugz')
+ = sprite_icon('bug')
+ FogBugz
- if gitea_import_enabled?
%div
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
index 0b2d179456d..c11ee765cca 100644
--- a/app/views/projects/_issuable_by_email.html.haml
+++ b/app/views/projects/_issuable_by_email.html.haml
@@ -37,9 +37,9 @@
= render 'by_email_description'
%p
This is a private email address
-
- %a{ href: 'https://docs.gitlab.com/ee/development/emails.html#email-namespace', target: "_blank", rel: "noopener" }
- %i.fa.fa-question-circle{ 'aria-label': "Learn more about incoming email addresses" }
+ %span<
+ = link_to help_page_path('development/emails', anchor: 'email-namespace'), target: '_blank', rel: 'noopener', aria: { label: 'Learn more about incoming email addresses' } do
+ = sprite_icon('question-o')
generated just for you.
diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml
index 9cebb191346..7a5997bbcfd 100644
--- a/app/views/projects/_merge_request_merge_checks_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_checks_settings.html.haml
@@ -9,7 +9,7 @@
= s_('ProjectSettings|Pipelines must succeed')
.text-secondary
= s_('ProjectSettings|Pipelines need to be configured to enable this feature.')
- = link_to icon('question-circle'),
+ = link_to sprite_icon('question-o'),
help_page_path('ci/merge_request_pipelines/index.md',
anchor: 'pipelines-for-merge-requests'),
target: '_blank'
@@ -21,6 +21,6 @@
.text-secondary
= s_('ProjectSettings|This introduces the risk of merging changes that will not pass the pipeline.')
.form-check.mb-2
- = form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input'
+ = form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input', data: { qa_selector: 'allow_merge_if_all_discussions_are_resolved_checkbox' }
= form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do
= s_('ProjectSettings|All discussions must be resolved')
diff --git a/app/views/projects/_merge_request_merge_suggestions_settings.html.haml b/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
index 12f26a7e315..258cf86ab05 100644
--- a/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
@@ -4,7 +4,7 @@
%b= s_('ProjectSettings|Merge suggestions')
%p.text-secondary
= s_('ProjectSettings|The commit message used to apply merge request suggestions')
- = link_to icon('question-circle'),
+ = link_to sprite_icon('question-o'),
help_page_path('user/discussions/index.md',
anchor: 'configure-the-commit-message-for-applied-suggestions'),
target: '_blank'
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index e0a426607d4..ee35f734e3e 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -47,7 +47,7 @@
= f.label :visibility_level, class: 'label-bold' do
= s_('ProjectsNew|Visibility Level')
- = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer'
+ = link_to sprite_icon('question-o'), help_page_path('public_access/public_access'), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer'
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
- if !hide_init_with_readme
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index 7c08955983a..fceef0624d7 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -10,7 +10,7 @@
- if ::Gitlab::ServiceDesk.supported?
.js-service-desk-setting-root{ data: { endpoint: project_service_desk_path(@project),
enabled: "#{@project.service_desk_enabled}",
- incoming_email: (@project.service_desk_incoming_address if @project.service_desk_enabled),
+ incoming_email: (@project.service_desk_address if @project.service_desk_enabled),
custom_email: (@project.service_desk_custom_address if @project.service_desk_enabled),
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
index 1a9ce8d0508..c68cc19f6c1 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, class: 'str-truncated' do
+ = link_to path_to_directory, class: 'str-truncated', data: { qa_selector: 'directory_name_link', qa_directory_name: directory.name } do
%span= directory.name
%td
diff --git a/app/views/projects/blob/_content.html.haml b/app/views/projects/blob/_content.html.haml
index 7afbd85cd6d..11946f22811 100644
--- a/app/views/projects/blob/_content.html.haml
+++ b/app/views/projects/blob/_content.html.haml
@@ -1,6 +1,10 @@
- simple_viewer = blob.simple_viewer
- rich_viewer = blob.rich_viewer
- rich_viewer_active = rich_viewer && params[:viewer] != 'simple'
+- blob_data = defined?(@blob) ? @blob.data : {}
+- filename = defined?(@blob) ? @blob.name : ''
+
+#js-blob-toggle-graph-preview{ data: { blob_data: blob_data, filename: filename } }
= render 'projects/blob/viewer', viewer: simple_viewer, hidden: rich_viewer_active
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 787dc3b030f..cea65bf9b4e 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -23,6 +23,7 @@
.js-suggest-gitlab-ci-yml{ data: { toggle: 'popover',
target: '#gitlab-ci-yml-selector',
track_label: 'suggest_gitlab_ci_yml',
+ merge_request_path: params[:mr_path],
dismiss_key: @project.id,
human_access: human_access } }
diff --git a/app/views/projects/blob/_pipeline_tour_success.html.haml b/app/views/projects/blob/_pipeline_tour_success.html.haml
index cf1427df044..3ea2defb2b3 100644
--- a/app/views/projects/blob/_pipeline_tour_success.html.haml
+++ b/app/views/projects/blob/_pipeline_tour_success.html.haml
@@ -1,3 +1,4 @@
.js-success-pipeline-modal{ data: { 'commit-cookie': suggest_pipeline_commit_cookie_name,
'go-to-pipelines-path': project_pipelines_path(@project),
+ 'project-merge-requests-path': project_merge_requests_path(@project),
'human-access': @project.team.human_max_access(current_user&.id) } }
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index 2aefcdc5762..b4962f4e78e 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -1,4 +1,4 @@
-.template-selectors-menu.gl-pl-2-deprecated-no-really-do-not-use-me
+.template-selectors-menu.gl-pl-3
.template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden
- toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type'
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 7d072ba5899..9bb4342ffb4 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,10 +2,16 @@
- page_title _("Edit"), @blob.path, @ref
- if @conflict
- .alert.alert-danger
- Someone edited the file the same time you did. Please check out
- = link_to "the file", project_blob_path(@project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer'
- and make sure your changes will not unintentionally remove theirs.
+ .gl-alert.gl-alert-danger.gl-mb-5.gl-mt-5
+ = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ Someone edited the file the same time you did. Please check out
+ = link_to "the file", project_blob_path(@project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer', class: 'gl-link'
+ and make sure your changes will not unintentionally remove theirs.
+
+- if editing_ci_config? && show_web_ide_alert?
+ #js-suggest-web-ide-ci{ data: { dismiss_endpoint: user_callouts_path, feature_id: UserCalloutsHelper::WEB_IDE_ALERT_DISMISSED, edit_path: ide_edit_path } }
+
.editor-title-row
%h3.page-title.blob-edit-page-title
Edit file
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 48ffd80aa9c..a939f43d5e2 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -15,6 +15,7 @@
- if should_suggest_gitlab_ci_yml?
.js-suggest-gitlab-ci-yml-commit-changes{ data: { toggle: 'popover',
target: '#commit-changes',
+ merge_request_path: params[:mr_path],
track_label: 'suggest_commit_first_project_gitlab_ci_yml',
dismiss_key: @project.id,
human_access: human_access } }
diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml
index 10cbf6a2f7a..379a6c3084a 100644
--- a/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml
+++ b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml
@@ -1,4 +1,4 @@
-= icon('spinner spin fw')
+= loading_icon(css_class: "gl-vertical-align-text-bottom mr-1")
Validating GitLab CI configuration…
= link_to 'Learn more', help_page_path('ci/yaml/README')
diff --git a/app/views/projects/blob/viewers/_loading.html.haml b/app/views/projects/blob/viewers/_loading.html.haml
index 5fbe9b0df0c..18fd0d87ce6 100644
--- a/app/views/projects/blob/viewers/_loading.html.haml
+++ b/app/views/projects/blob/viewers/_loading.html.haml
@@ -1,2 +1,2 @@
-.text-center.gl-mt-3.gl-mb-3
- = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…', class: 'qa-spinner')
+.text-center.gl-mt-4.gl-mb-3
+ = loading_icon(size: "md", css_class: "qa-spinner")
diff --git a/app/views/projects/blob/viewers/_loading_auxiliary.html.haml b/app/views/projects/blob/viewers/_loading_auxiliary.html.haml
index c7dc9e3250a..5a6c1a493a5 100644
--- a/app/views/projects/blob/viewers/_loading_auxiliary.html.haml
+++ b/app/views/projects/blob/viewers/_loading_auxiliary.html.haml
@@ -1,2 +1,2 @@
-= icon('spinner spin fw')
+= loading_icon(css_class: "gl-vertical-align-text-bottom")
Analyzing file…
diff --git a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml
index 9ec1d7d0d67..de9c6c5320f 100644
--- a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml
+++ b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml
@@ -5,7 +5,7 @@
= icon('warning fw')
= _('Metrics Dashboard YAML definition is invalid:')
%ul
- - viewer.errors.messages.each do |error|
- %li= error.join(': ')
+ - viewer.errors.each do |error|
+ %li= error
= link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md')
diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml
index 1d768bd1ca4..8610847fbc9 100644
--- a/app/views/projects/blob/viewers/_route_map_loading.html.haml
+++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml
@@ -1,4 +1,4 @@
-= icon('spinner spin fw')
+= loading_icon(css_class: "gl-vertical-align-text-bottom gl-mr-1")
Validating Route Map…
= link_to 'Learn more', help_page_path('ci/environments/index.md', anchor: 'going-from-source-files-to-public-pages')
diff --git a/app/views/projects/blob/viewers/_sketch.html.haml b/app/views/projects/blob/viewers/_sketch.html.haml
index aa8d1dd326f..08c21258d3f 100644
--- a/app/views/projects/blob/viewers/_sketch.html.haml
+++ b/app/views/projects/blob/viewers/_sketch.html.haml
@@ -1,3 +1,3 @@
.file-content#js-sketch-viewer{ data: { endpoint: blob_raw_path } }
- .js-loading-icon.text-center.gl-mt-3.gl-mb-3.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
- = icon('spinner spin 2x', 'aria-hidden' => 'true');
+ .text-center.gl-mt-4.gl-mb-3.js-loading-icon
+ = loading_icon(size: "md")
diff --git a/app/views/projects/blob/viewers/_stl.html.haml b/app/views/projects/blob/viewers/_stl.html.haml
index 6983c3cc81b..44c986595df 100644
--- a/app/views/projects/blob/viewers/_stl.html.haml
+++ b/app/views/projects/blob/viewers/_stl.html.haml
@@ -1,6 +1,6 @@
.file-content.is-stl-loading
.text-center#js-stl-viewer{ data: { endpoint: blob_raw_path } }
- = icon('spinner spin 2x', class: 'gl-mt-3 gl-mb-3', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
+ = loading_icon(size: "md", css_class: "gl-mt-4 gl-mb-3")
.text-center.gl-mt-3.gl-mb-3.stl-controls
.btn-group
%button.btn.btn-default.btn-sm.js-material-changer{ data: { type: 'wireframe' } }
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index ed7dbdeae93..020a4361203 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -50,25 +50,25 @@
- if can?(current_user, :push_code, @project)
- if branch.name == @project.repository.root_ref
- %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
+ %button{ class: "btn btn-remove remove-row has-tooltip disabled",
disabled: true,
title: s_('Branches|The default branch cannot be deleted') }
- = icon("trash-o")
+ = sprite_icon("remove")
- elsif protected_branch?(@project, branch)
- if can?(current_user, :push_to_delete_protected_branch, @project)
- %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
+ %button{ class: "btn btn-remove remove-row has-tooltip",
title: s_('Branches|Delete protected branch'),
data: { toggle: "modal",
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name),
branch_name: branch.name,
is_merged: ("true" if merged) } }
- = icon("trash-o")
+ = sprite_icon("remove")
- else
- %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
+ %button{ class: "btn btn-remove remove-row has-tooltip disabled",
disabled: true,
title: s_('Branches|Only a project maintainer or owner can delete a protected branch') }
- = icon("trash-o")
+ = sprite_icon("remove")
- else
= link_to project_branch_path(@project, branch.name),
class: "btn btn-remove remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip",
@@ -77,4 +77,4 @@
data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
remote: true,
'aria-label' => s_('Branches|Delete branch') do
- = icon("trash-o")
+ = sprite_icon("remove")
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 97e46aaa710..7a8bc45a272 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -28,5 +28,4 @@
.form-actions
= button_tag 'Create branch', class: 'btn btn-success', tabindex: 3
= link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel'
--# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 1d0ad6dcde6..c04687bd846 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -17,7 +17,7 @@
%section.border-top.pt-1.mt-1
%h5.m-0.dropdown-bold-header= _('Download artifacts')
- unless pipeline.latest?
- %span.unclickable= ci_status_for_statuseable(project.pipeline_for(ref))
+ %span.unclickable= ci_status_for_statuseable(project.latest_pipeline(ref))
%h6.m-0.dropdown-header= _('Previous Artifacts')
%ul
- pipeline.latest_builds_with_artifacts.each do |job|
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 23f9a6a8f6c..c7ab01a4ef7 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -102,7 +102,7 @@
- if can?(current_user, :update_build, job)
- if job.active?
= link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'btn btn-build' do
- = icon('remove', class: 'cred')
+ = sprite_icon('close')
- elsif job.scheduled?
.btn-group
.btn.btn-default{ disabled: true }
diff --git a/app/views/projects/ci/lints/_create.html.haml b/app/views/projects/ci/lints/_create.html.haml
index 5cc89343ba3..4b7cda0ef57 100644
--- a/app/views/projects/ci/lints/_create.html.haml
+++ b/app/views/projects/ci/lints/_create.html.haml
@@ -1,10 +1,10 @@
-- if @status
+- if @result.valid?
.bs-callout.bs-callout-success
%p
%b= _("Status:")
= _("syntax is correct")
- = render "projects/ci/lints/lint_warnings", warnings: @warnings
+ = render "projects/ci/lints/lint_warnings", warnings: @result.warnings
.table-holder
%table.table.table-bordered
@@ -13,54 +13,31 @@
%th= _("Parameter")
%th= _("Value")
%tbody
- - if @dry_run
- - @stages.each do |stage|
- - stage.statuses.each do |job|
- %tr
- %td #{stage.name.capitalize} Job - #{job.name}
- %td
- %pre= job.options[:before_script].to_a.join('\n')
- %pre= job.options[:script].to_a.join('\n')
- %pre= job.options[:after_script].to_a.join('\n')
- %br
- %b= _("Tag list:")
- = job.tag_list.to_a.join(", ") if job.is_a?(Ci::Build)
- %br
- %b= _("Environment:")
- = job.options.dig(:environment, :name)
- %br
- %b= _("When:")
- = job.when
- - if job.allow_failure
- %b= _("Allowed to fail")
-
- - else
- - @stages.each do |stage|
- - @builds.select { |build| build[:stage] == stage }.each do |build|
- - job = @jobs[build[:name].to_sym]
- %tr
- %td #{stage.capitalize} Job - #{build[:name]}
- %td
- %pre= job[:before_script].to_a.join('\n')
- %pre= job[:script].to_a.join('\n')
- %pre= job[:after_script].to_a.join('\n')
- %br
- %b= _("Tag list:")
- = build[:tag_list].to_a.join(", ")
- %br
- %b= _("Only policy:")
- = job[:only].to_a.join(", ")
- %br
- %b= _("Except policy:")
- = job[:except].to_a.join(", ")
- %br
- %b= _("Environment:")
- = build[:environment]
- %br
- %b= _("When:")
- = build[:when]
- - if build[:allow_failure]
- %b= _("Allowed to fail")
+ - @result.jobs.each do |job|
+ %tr
+ %td #{job[:stage].capitalize} Job - #{job[:name]}
+ %td
+ %pre= job[:before_script].to_a.join('\n')
+ %pre= job[:script].to_a.join('\n')
+ %pre= job[:after_script].to_a.join('\n')
+ %br
+ %b= _("Tag list:")
+ = job[:tag_list].to_a.join(", ")
+ - unless @dry_run
+ %br
+ %b= _("Only policy:")
+ = job[:only].to_a.join(", ")
+ %br
+ %b= _("Except policy:")
+ = job[:except].to_a.join(", ")
+ %br
+ %b= _("Environment:")
+ = job[:environment]
+ %br
+ %b= _("When:")
+ = job[:when]
+ - if job[:allow_failure]
+ %b= _("Allowed to fail")
- else
.bs-callout.bs-callout-danger
@@ -68,7 +45,7 @@
%b= _("Status:")
= _("syntax is incorrect")
%pre
- - @errors.each do |message|
+ - @result.errors.each do |message|
%p= message
- = render "projects/ci/lints/lint_warnings", warnings: @warnings
+ = render "projects/ci/lints/lint_warnings", warnings: @result.warnings
diff --git a/app/views/projects/ci/lints/_lint_warnings.html.haml b/app/views/projects/ci/lints/_lint_warnings.html.haml
index 0a5bb8f76ef..90db65e6c27 100644
--- a/app/views/projects/ci/lints/_lint_warnings.html.haml
+++ b/app/views/projects/ci/lints/_lint_warnings.html.haml
@@ -1,6 +1,10 @@
- if warnings
- - warnings.each do |warning|
+ - total_warnings = warnings.length
+ - message = warning_header(total_warnings)
+
+ - if warnings.any?
.bs-callout.bs-callout-warning
- %p
- %b= _("Warning:")
- = markdown(warning)
+ %details
+ %summary.gl-mb-2= message
+ - warnings.each do |warning|
+ = markdown(warning)
diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml
index 0c51c978bfe..2e79852f4c9 100644
--- a/app/views/projects/ci/lints/show.html.haml
+++ b/app/views/projects/ci/lints/show.html.haml
@@ -1,30 +1,40 @@
- page_title _("CI Lint")
- page_description _("Validate your GitLab CI configuration file")
-- content_for :library_javascripts do
- = page_specific_javascript_tag('lib/ace.js')
+- unless Feature.enabled?(:monaco_ci)
+ - content_for :library_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
%h2.pt-3.pb-3= _("Validate your GitLab CI configuration")
-.project-ci-linter
- = form_tag project_ci_lint_path(@project), method: :post do
- .row
- .col-sm-12
- .file-holder
- .js-file-title.file-title.clearfix
- = _("Contents of .gitlab-ci.yml")
- #ci-editor.ci-editor= @content
- = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
- .col-sm-12
- .float-left.gl-mt-3
- = submit_tag(_('Validate'), class: 'btn btn-success submit-yml')
- - if Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
- = check_box_tag(:dry_run, 'true', params[:dry_run])
- = label_tag(:dry_run, _('Simulate a pipeline created for the default branch'))
- = link_to icon('question-circle'), help_page_path('ci/lint', anchor: 'pipeline-simulation'), target: '_blank', rel: 'noopener noreferrer'
- .float-right.prepend-top-10
- = button_tag(_('Clear'), type: 'button', class: 'btn btn-default clear-yml')
+- if Feature.enabled?(:ci_lint_vue, @project)
+ #js-ci-lint{ data: { endpoint: project_ci_lint_path(@project) } }
- .row.prepend-top-20
- .col-sm-12
- .results.project-ci-template
- = render partial: 'create' if defined?(@status)
+- else
+ .project-ci-linter
+ = form_tag project_ci_lint_path(@project), method: :post, class: 'js-ci-lint-form' do
+ .row
+ .col-sm-12
+ .file-holder
+ .js-file-title.file-title.clearfix
+ = _("Contents of .gitlab-ci.yml")
+ - if Feature.enabled?(:monaco_ci)
+ .file-editor.code
+ .js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }<
+ %pre.editor-loading-content= params[:content]
+ - else
+ #ci-editor.ci-editor= @content
+ = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
+ .col-sm-12
+ .float-left.gl-mt-3
+ = submit_tag(_('Validate'), class: 'btn btn-success submit-yml')
+ - if Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
+ = check_box_tag(:dry_run, 'true', params[:dry_run])
+ = label_tag(:dry_run, _('Simulate a pipeline created for the default branch'))
+ = link_to sprite_icon('question-o'), help_page_path('ci/lint', anchor: 'pipeline-simulation'), target: '_blank', rel: 'noopener noreferrer'
+ .float-right.prepend-top-10
+ = button_tag(_('Clear'), type: 'button', class: 'btn btn-default clear-yml')
+
+ .row.prepend-top-20
+ .col-sm-12
+ .results.project-ci-template
+ = render partial: 'create' if defined?(@result)
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index f560127fefd..019894ddbb4 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
= _("Clean up after running %{filter_repo} on the repository" % { filter_repo: link_to_filter_repo }).html_safe
- = link_to icon('question-circle'),
+ = link_to sprite_icon('question-o'),
help_page_path('user/project/repository/reducing_the_repo_size_using_git.md'),
target: '_blank', rel: 'noopener noreferrer'
@@ -28,4 +28,3 @@
.gl-display-flex.gl-justify-content-end
= f.submit _('Start cleanup'), class: 'btn btn-success'
-
diff --git a/app/views/projects/commit/_limit_exceeded_message.html.haml b/app/views/projects/commit/_limit_exceeded_message.html.haml
index ace1be787fb..236418ecd0e 100644
--- a/app/views/projects/commit/_limit_exceeded_message.html.haml
+++ b/app/views/projects/commit/_limit_exceeded_message.html.haml
@@ -3,6 +3,6 @@
- if objects == :branch
= sprite_icon('fork', size: 12)
- else
- = icon('tag')
+ = sprite_icon('tag')
.limit-message
%span= _('%{label_for_message} unavailable') % { label_for_message: label_for_message.capitalize }
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 33fedde0cd1..cd61576a96a 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -11,6 +11,7 @@
- commit = commit.present(current_user: current_user)
- commit_status = commit.status_for(ref)
- collapsible = local_assigns.fetch(:collapsible, true)
+- link_data_attrs = local_assigns.fetch(:link_data_attrs, {})
- link = commit_path(project, commit, merge_request: merge_request)
@@ -26,7 +27,7 @@
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: ["commit-row-message item-title js-onboarding-commit-item", ("font-italic" if commit.message.empty?)]
- else
- = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title js-onboarding-commit-item #{"font-italic" if commit.message.empty?}")
+ = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title js-onboarding-commit-item #{"font-italic" if commit.message.empty?}", data: link_data_attrs)
%span.commit-row-message.d-inline.d-sm-none
&middot;
= commit.short_id
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
deleted file mode 100644
index 2ca72b141be..00000000000
--- a/app/views/projects/cycle_analytics/_overview.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-.cycle-analytics-overview
- .container
- .row
- .col-md-10.offset-md-1
- .row.overview-details
- .col-md-6.overview-text
- %h4 Introducing Value Stream Analytics
- %p
- Value Stream Analytics (VSA) gives an overview of how much time it takes to go from idea to production in your project.
- To set up VSA, you must first define a production environment by setting up your CI and then deploy to production.
- %p
- %a.btn{ href: help_page_path('user/analytics/value_stream_analytics.md'), target: '_blank' } Read more
- .col-md-6.overview-image
- %span.overview-icon
- = custom_icon ('icon_cycle_analytics_overview')
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 090fc602ebb..d7e10efc3b1 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -40,19 +40,23 @@
%li.stage-header.pl-5
%span.stage-name.font-weight-bold
{{ s__('ProjectLifecycle|Stage') }}
- %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" }
+ %span.has-tooltip{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" }
+ = sprite_icon('question-o', css_class: 'gl-text-gray-500')
%li.median-header
%span.stage-name.font-weight-bold
{{ __('Median') }}
- %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" }
+ %span.has-tooltip{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" }
+ = sprite_icon('question-o', css_class: 'gl-text-gray-500')
%li.event-header.pl-3
%span.stage-name.font-weight-bold
{{ currentStage ? __(currentStage.legend) : __('Related Issues') }}
- %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" }
+ %span.has-tooltip{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" }
+ = sprite_icon('question-o', css_class: 'gl-text-gray-500')
%li.total-time-header.pr-5.text-right
%span.stage-name.font-weight-bold
{{ __('Time') }}
- %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" }
+ %span.has-tooltip{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" }
+ = sprite_icon('question-o', css_class: 'gl-text-gray-500')
.stage-panel-body
%nav.stage-nav
%ul
diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml
index b78535bbe2f..46ee60949db 100644
--- a/app/views/projects/default_branch/_show.html.haml
+++ b/app/views/projects/default_branch/_show.html.haml
@@ -26,7 +26,7 @@
%strong= _("Auto-close referenced issues on default branch")
.form-text.text-muted
= _("Issues referenced by merge requests and commits within the default branch will be closed automatically")
- = link_to icon('question-circle'), help_page_path('user/project/issues/managing_issues.md', anchor: 'disabling-automatic-issue-closing'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('user/project/issues/managing_issues.md', anchor: 'disabling-automatic-issue-closing'), target: '_blank'
.gl-display-flex.gl-justify-content-end
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index bd023e0442c..187ebcb739c 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -9,6 +9,10 @@
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}"
+ - if diff_file.submodule?
+ .file-actions.d-none.d-sm-block
+ = submodule_diff_compare_link(diff_file)
+
- unless diff_file.submodule?
- blob = diff_file.blob
.file-actions.d-none.d-sm-block
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index bf978b01652..e5c4cfcbd72 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -39,7 +39,7 @@
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
= render 'projects/merge_request_settings', form: f
.gl-display-flex.gl-justify-content-end
- = f.submit _('Save changes'), class: "btn btn-succes qa-save-merge-request-changes rspec-save-merge-request-changes"
+ = f.submit _('Save changes'), class: "btn btn-success qa-save-merge-request-changes rspec-save-merge-request-changes"
= render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index bfb22aa8025..c9edc3c12ec 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -5,72 +5,71 @@
= render partial: 'flash_messages', locals: { project: @project }
-%div{ class: [("limit-container-width" unless fluid_layout)] }
- = render "home_panel"
+= render "home_panel"
- %h4.gl-mt-0.gl-mb-3
- = _('The repository for this project is empty')
+%h4.gl-mt-0.gl-mb-3
+ = _('The repository for this project is empty')
- - if @project.can_current_user_push_code?
- %p.gl-mb-0
- = _('You can get started by cloning the repository or start adding files to it with one of the following options.')
+- if @project.can_current_user_push_code?
+ %p.gl-mb-0
+ = _('You can get started by cloning the repository or start adding files to it with one of the following options.')
- .project-buttons.qa-quick-actions
- .project-clone-holder.d-block.d-md-none.mt-2.mr-2
- = render "shared/mobile_clone_panel"
+.project-buttons.qa-quick-actions
+ .project-clone-holder.d-block.d-md-none.mt-2.mr-2
+ = render "shared/mobile_clone_panel"
- .project-clone-holder.d-none.d-md-inline-block.mt-2.mr-2.float-left
- = render "projects/buttons/clone"
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
+ .project-clone-holder.d-none.d-md-inline-block.mt-2.mr-2.float-left
+ = render "projects/buttons/clone"
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
- - if can?(current_user, :push_code, @project)
- .empty-wrapper.gl-mt-7
- %h3#repo-command-line-instructions.page-title-empty
- = _('Command line instructions')
- %p
- = _('You can also upload existing files from your computer using the instructions below.')
- .git-empty.js-git-empty
- %fieldset
- %h5= _('Git global setup')
- %pre.bg-light
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
+- if can?(current_user, :push_code, @project)
+ .empty-wrapper.gl-mt-7
+ %h3#repo-command-line-instructions.page-title-empty
+ = _('Command line instructions')
+ %p
+ = _('You can also upload existing files from your computer using the instructions below.')
+ .git-empty.js-git-empty
+ %fieldset
+ %h5= _('Git global setup')
+ %pre.bg-light
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5= _('Create a new repository')
- %pre.bg-light
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin #{ default_branch_name }
+ %fieldset
+ %h5= _('Create a new repository')
+ %pre.bg-light
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin #{ default_branch_name }
- %fieldset
- %h5= _('Push an existing folder')
- %pre.bg-light
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- git add .
- git commit -m "Initial commit"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin #{ default_branch_name }
+ %fieldset
+ %h5= _('Push an existing folder')
+ %pre.bg-light
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ git add .
+ git commit -m "Initial commit"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin #{ default_branch_name }
- %fieldset
- %h5= _('Push an existing Git repository')
- %pre.bg-light
- :preserve
- cd existing_repo
- git remote rename origin old-origin
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin --all
- git push -u origin --tags
+ %fieldset
+ %h5= _('Push an existing Git repository')
+ %pre.bg-light
+ :preserve
+ cd existing_repo
+ git remote rename origin old-origin
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin --all
+ git push -u origin --tags
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index cd24c30e46f..554cb4323f7 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -2,4 +2,4 @@
- breadcrumb_title _("Folder/%{name}") % { name: @folder }
- page_title _("Environments in %{name}") % { name: @folder }
-#environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data } }
+#environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data, project_path: @project.full_path } }
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 445196ed449..9abc1a5a925 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -5,4 +5,5 @@
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
"new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments/index.md"),
- "deploy-boards-help-path" => help_page_path("user/project/deploy_boards", anchor: "enabling-deploy-boards") } }
+ "deploy-boards-help-path" => help_page_path("user/project/deploy_boards", anchor: "enabling-deploy-boards"),
+ "project-path" => @project.full_path } }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 2e665a12a0a..929015023d2 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -69,7 +69,7 @@
.text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
- .table-holder
+ .table-holder.gl-overflow-visible
.ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-15{ role: 'columnheader' }= _('Status')
diff --git a/app/views/projects/feature_flags/_errors.html.haml b/app/views/projects/feature_flags/_errors.html.haml
new file mode 100644
index 00000000000..a32245640be
--- /dev/null
+++ b/app/views/projects/feature_flags/_errors.html.haml
@@ -0,0 +1,4 @@
+#error_explanation
+ .alert.alert-danger
+ - @feature_flag.errors.full_messages.each do |message|
+ %p= message
diff --git a/app/views/projects/feature_flags/edit.html.haml b/app/views/projects/feature_flags/edit.html.haml
new file mode 100644
index 00000000000..4de41ca4080
--- /dev/null
+++ b/app/views/projects/feature_flags/edit.html.haml
@@ -0,0 +1,16 @@
+- @gfm_form = Feature.enabled?(:feature_flags_issue_links, @project, default_enabled: true)
+
+- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
+- breadcrumb_title @feature_flag.name
+- page_title s_('FeatureFlags|Edit Feature Flag')
+
+#js-edit-feature-flag{ data: { endpoint: project_feature_flag_path(@project, @feature_flag),
+ project_id: @project.id,
+ feature_flags_path: project_feature_flags_path(@project),
+ environments_endpoint: search_project_environments_path(@project, format: :json),
+ user_callouts_path: user_callouts_path,
+ user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERISION,
+ show_user_callout: show_feature_flags_new_version?.to_s,
+ strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
+ environments_scope_docs_path: help_page_path('ci/environments', anchor: 'scoping-environments-with-specs'),
+ feature_flag_issues_endpoint: feature_flag_issues_links_endpoint(@project, @feature_flag, current_user) } }
diff --git a/app/views/projects/feature_flags/index.html.haml b/app/views/projects/feature_flags/index.html.haml
new file mode 100644
index 00000000000..f425de91d12
--- /dev/null
+++ b/app/views/projects/feature_flags/index.html.haml
@@ -0,0 +1,15 @@
+- page_title s_('FeatureFlags|Feature Flags')
+
+#feature-flags-vue{ data: { endpoint: project_feature_flags_path(@project, format: :json),
+ "project-id" => @project.id,
+ "project-name" => @project.name,
+ "error-state-svg-path" => image_path('illustrations/feature_flag.svg'),
+ "feature-flags-help-page-path" => help_page_path("operations/feature_flags"),
+ "feature-flags-client-libraries-help-page-path" => help_page_path("operations/feature_flags", anchor: "choose-a-client-library"),
+ "feature-flags-client-example-help-page-path" => help_page_path("operations/feature_flags", anchor: "golang-application-example"),
+ "unleash-api-url" => (unleash_api_url(@project) if can?(current_user, :admin_feature_flag, @project)),
+ "unleash-api-instance-id" => (unleash_api_instance_id(@project) if can?(current_user, :admin_feature_flag, @project)),
+ "can-user-admin-feature-flag" => can?(current_user, :admin_feature_flag, @project),
+ "new-feature-flag-path" => can?(current_user, :create_feature_flag, @project) ? new_project_feature_flag_path(@project): nil,
+ "rotate-instance-id-path" => can?(current_user, :admin_feature_flags_client, @project) ? reset_token_project_feature_flags_client_path(@project, format: :json) : nil,
+ "new-user-list-path" => can?(current_user, :admin_feature_flags_user_lists, @project) ? new_project_feature_flags_user_list_path(@project) : nil } }
diff --git a/app/views/projects/feature_flags/new.html.haml b/app/views/projects/feature_flags/new.html.haml
new file mode 100644
index 00000000000..a7388361da5
--- /dev/null
+++ b/app/views/projects/feature_flags/new.html.haml
@@ -0,0 +1,14 @@
+- @breadcrumb_link = new_project_feature_flag_path(@project)
+- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
+- breadcrumb_title s_('FeatureFlags|New')
+- page_title s_('FeatureFlags|New Feature Flag')
+
+#js-new-feature-flag{ data: { endpoint: project_feature_flags_path(@project, format: :json),
+ feature_flags_path: project_feature_flags_path(@project),
+ environments_endpoint: search_project_environments_path(@project, format: :json),
+ user_callouts_path: user_callouts_path,
+ user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERISION,
+ show_user_callout: show_feature_flags_new_version?.to_s,
+ strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
+ environments_scope_docs_path: help_page_path('ci/environments', anchor: 'scoping-environments-with-specs'),
+ project_id: @project.id } }
diff --git a/app/views/projects/feature_flags_user_lists/edit.html.haml b/app/views/projects/feature_flags_user_lists/edit.html.haml
new file mode 100644
index 00000000000..ea47cc06c0e
--- /dev/null
+++ b/app/views/projects/feature_flags_user_lists/edit.html.haml
@@ -0,0 +1,7 @@
+- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
+- breadcrumb_title s_('FeatureFlags|Edit User List')
+- page_title s_('FeatureFlags|Edit User List')
+
+#js-edit-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags.md', anchor: 'user-list'),
+ 'user-list-iid' => @user_list.iid,
+ 'project-id' => @project.id } }
diff --git a/app/views/projects/feature_flags_user_lists/new.html.haml b/app/views/projects/feature_flags_user_lists/new.html.haml
new file mode 100644
index 00000000000..3d25453cb66
--- /dev/null
+++ b/app/views/projects/feature_flags_user_lists/new.html.haml
@@ -0,0 +1,8 @@
+- @breadcrumb_link = new_project_feature_flags_user_list_path(@project)
+- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
+- breadcrumb_title s_('FeatureFlags|New User List')
+- page_title s_('FeatureFlags|New User List')
+
+#js-new-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags.md', anchor: 'user-list'),
+ 'feature-flags-path' => project_feature_flags_path(@project),
+ 'project-id' => @project.id } }
diff --git a/app/views/projects/feature_flags_user_lists/show.html.haml b/app/views/projects/feature_flags_user_lists/show.html.haml
new file mode 100644
index 00000000000..add256f0190
--- /dev/null
+++ b/app/views/projects/feature_flags_user_lists/show.html.haml
@@ -0,0 +1,7 @@
+- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
+- breadcrumb_title s_('FeatureFlags|List details')
+- page_title s_('FeatureFlags|Feature Flag User List Details')
+
+#js-edit-user-list{ data: { project_id: @project.id,
+ user_list_iid: @user_list.iid,
+ empty_state_path: image_path('illustrations/feature_flag.svg') } }
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 786af3714a6..194b10e9ef4 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -24,4 +24,4 @@
%p.text-secondary
= _('Try using a different search term to find the file you are looking for.')
.text-center.gl-mt-3.loading
- .spinner.spinner-md
+ = loading_icon(size: 'md')
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index 5d527f1bcfb..0c15796b667 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -1,20 +1,22 @@
- page_title _("Fork project")
- if @forked_project && !@forked_project.saved?
- .alert.alert-danger.alert-block
- %h4
+ .gl-alert.gl-alert-danger.gl-mt-5
+ = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon')
+ %h4.gl-alert-title
= sprite_icon('fork')
= _("Fork Error!")
- %p
- = _("You tried to fork %{link_to_the_project} but it failed for the following reason:").html_safe % { link_to_the_project: link_to_project(@project) }
-
- - if @forked_project && @forked_project.errors.any?
+ .gl-alert-body
%p
- &ndash;
- - error = @forked_project.errors.full_messages.first
- - if error.include?("already been taken")
- = _("Name has already been taken")
- - else
- = error
+ = _("You tried to fork %{link_to_the_project} but it failed for the following reason:").html_safe % { link_to_the_project: link_to_project(@project) }
+
+ - if @forked_project && @forked_project.errors.any?
+ %p
+ &ndash;
+ - error = @forked_project.errors.full_messages.first
+ - if error.include?("already been taken")
+ = _("Name has already been taken")
+ - else
+ = error
- %p
- = link_to _("Try to fork again"), new_project_fork_path(@project), title: _("Fork"), class: "btn"
+ .gl-alert-actions
+ = link_to _("Try to fork again"), new_project_fork_path(@project), title: _("Fork"), class: "btn gl-alert-action btn-info btn-md gl-button"
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 495a4ac50bf..a73e367733b 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,6 +1,6 @@
- page_title _('Contributors')
-.sub-header-block.bg-gray-light.gl-p-3-deprecated-no-really-do-not-use-me
+.sub-header-block.bg-gray-light.gl-p-5
.tree-ref-holder.inline.vertical-align-middle
= render 'shared/ref_switcher', destination: 'graphs'
= link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 09624b771ea..0c1efab2195 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -4,7 +4,7 @@
.save-project-loader
.center
%h2
- %i.loading.spinner.spinner-sm
+ = loading_icon
= import_in_progress_title
- if !has_ci_cd_only_params? && @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url}
diff --git a/app/views/projects/issues/_design_management.html.haml b/app/views/projects/issues/_design_management.html.haml
index 9d88d77eac9..6fc2f41b122 100644
--- a/app/views/projects/issues/_design_management.html.haml
+++ b/app/views/projects/issues/_design_management.html.haml
@@ -1,23 +1,12 @@
+- return if @issue.incident?
+
- requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements')
- requirements_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: requirements_link_url }
- link_end = '</a>'.html_safe
-- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
+- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
- if @project.design_management_enabled?
- - if Feature.enabled?(:design_management_moved, @project, default_enabled: true)
- .js-design-management-new{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
- - else
- .js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
+ .js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
- else
- - if Feature.enabled?(:design_management_moved, @project, default_enabled: true)
- .gl-border-solid.gl-border-1.gl-border-gray-100.gl-rounded-base.gl-mt-5.gl-p-3.gl-text-center
- = enable_lfs_message
- - else
- .mt-4
- .row.empty-state
- .col-12
- .text-content
- %h4.center
- = _('The one place for your designs')
- %p.center
- = enable_lfs_message
+ .gl-border-solid.gl-border-1.gl-border-gray-100.gl-rounded-base.gl-mt-5.gl-p-3.gl-text-center
+ = enable_lfs_message
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index 1e24b08ece2..1a557cce33c 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,4 +1,6 @@
-- if Feature.enabled?(:vue_issuables_list, @project)
+- is_project_overview = local_assigns.fetch(:is_project_overview, false)
+
+- if Feature.enabled?(:vue_issuables_list, @project) && !is_project_overview
- data_endpoint = local_assigns.fetch(:data_endpoint, expose_path(api_v4_projects_issues_path(id: @project.id)))
- default_empty_state_meta = { create_issue_path: new_project_issue_path(@project), svg_path: image_path('illustrations/issues.svg') }
- data_empty_state_meta = local_assigns.fetch(:data_empty_state_meta, default_empty_state_meta)
diff --git a/app/views/projects/issues/_related_issues.html.haml b/app/views/projects/issues/_related_issues.html.haml
new file mode 100644
index 00000000000..d131d20f079
--- /dev/null
+++ b/app/views/projects/issues/_related_issues.html.haml
@@ -0,0 +1,6 @@
+- if can?(current_user, :read_issue_link, @project)
+ .js-related-issues-root{ data: { endpoint: project_issue_links_path(@project, @issue),
+ can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}",
+ help_path: help_page_path('user/project/issues/related_issues'),
+ show_categorized_issues: "false" } }
+ - render('projects/issues/related_issues_block')
diff --git a/app/views/projects/issues/_related_issues_block.html.haml b/app/views/projects/issues/_related_issues_block.html.haml
new file mode 100644
index 00000000000..8d986b64b1d
--- /dev/null
+++ b/app/views/projects/issues/_related_issues_block.html.haml
@@ -0,0 +1,5 @@
+.related-issues-block
+ .card.card-slim
+ .card-header.panel-empty-heading.border-bottom-0
+ %h3.card-title.mt-0.mb-0.h5
+ = _('Linked issues')
diff --git a/app/views/projects/issues/_tabs.html.haml b/app/views/projects/issues/_tabs.html.haml
deleted file mode 100644
index d998a01623f..00000000000
--- a/app/views/projects/issues/_tabs.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%ul.nav-tabs.nav.nav-links{ role: 'tablist' }
- %li
- = link_to '#discussion-tab', class: 'active js-issue-tabs', id: 'discussion', role: 'tab', 'aria-controls': 'js-discussion', 'aria-selected': 'true', data: { toggle: 'tab', target: '#discussion-tab', qa_selector: 'discussion_tab_link' } do
- = _('Discussion')
- %span.badge.badge-pill.js-discussions-count
- %li
- = link_to '#designs-tab', class: 'js-issue-tabs', id: 'designs', role: 'tab', 'aria-controls': 'js-designs', 'aria-selected': 'false', data: { toggle: 'tab', target: '#designs-tab', qa_selector: 'designs_tab_link' } do
- = _('Designs')
- %span.badge.badge-pill.js-designs-count
-.tab-content
- #discussion-tab.tab-pane.show.active{ role: 'tabpanel', 'aria-labelledby': 'discussion', data: { qa_selector: 'discussion_tab_content' } }
- = render 'projects/issues/discussion'
- #designs-tab.tab-pane{ role: 'tabpanel', 'aria-labelledby': 'designs', data: { qa_selector: 'designs_tab_content' } }
- = render 'projects/issues/design_management'
diff --git a/app/views/projects/issues/service_desk.html.haml b/app/views/projects/issues/service_desk.html.haml
index bd260bdf143..65580a94cd0 100644
--- a/app/views/projects/issues/service_desk.html.haml
+++ b/app/views/projects/issues/service_desk.html.haml
@@ -7,7 +7,7 @@
- support_bot_attrs = { service_desk_enabled: @project.service_desk_enabled?, **UserSerializer.new.represent(User.support_bot) }.to_json
-- data_endpoint = "#{expose_path(api_v4_projects_issues_path(id: @project.id))}?author_id=#{User.support_bot.id}"
+- data_endpoint = "#{expose_path(api_v4_projects_issues_path(id: @project.id))}?author_username=#{User.support_bot.username}"
%div{ class: "js-service-desk-issues service-desk-issues", data: { support_bot: support_bot_attrs, service_desk_meta: service_desk_meta(@project) } }
.top-area
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index a7817ad5552..c762b044c3e 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -64,7 +64,8 @@
-# haml-lint:disable InlineJavaScript
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app
- %h2.title= markdown_field(@issue, :title)
+ .title-container
+ %h2.title= markdown_field(@issue, :title)
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.md= markdown_field(@issue, :description)
@@ -75,8 +76,7 @@
- if @issue.sentry_issue.present?
#js-sentry-error-stack-trace{ data: error_details_data(@project, @issue.sentry_issue.sentry_issue_identifier) }
- - if Feature.enabled?(:design_management_moved, @project, default_enabled: true)
- = render 'projects/issues/design_management'
+ = render 'projects/issues/design_management'
= render_if_exists 'projects/issues/related_issues'
@@ -96,9 +96,6 @@
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button?
- - if Feature.enabled?(:design_management_moved, @project, default_enabled: true)
- = render 'projects/issues/discussion'
- - else
- = render 'projects/issues/tabs'
+ = render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index 4f537ee8014..0b4b4aafeee 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -7,9 +7,9 @@
.nav-controls
- if can?(current_user, :update_build, @project)
- unless @repository.gitlab_ci_yml
- = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
+ = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn gl-button btn-info'
- = link_to project_ci_lint_path(@project), class: 'btn btn-default' do
+ = link_to project_ci_lint_path(@project), class: 'btn gl-button btn-default' do
%span CI lint
.content-list.builds-content-list
diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index df81e608c3e..a831972a823 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -53,4 +53,4 @@
%strong Tip:
= succeed '.' do
You can also checkout merge requests locally by
- = link_to 'following these guidelines', help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
+ = link_to 'following these guidelines', help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: "checkout-merge-requests-locally-through-the-head-ref"), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 8aa4a935384..454a0694355 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -4,8 +4,10 @@
- state_human_name, state_icon_name = state_name_with_icon(@merge_request)
- if @merge_request.closed_without_fork?
- .alert.alert-danger
- The source project of this merge request has been removed.
+ .gl-alert.gl-alert-danger.gl-mb-5
+ = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ The source project of this merge request has been removed.
.detail-page-header.border-bottom-0.pt-0.pb-0
.detail-page-header-body
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index 64b14f8889c..9736071b03f 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -7,7 +7,7 @@
window.gl.mrWidgetData.ci_troubleshooting_docs_path = '#{help_page_path('ci/troubleshooting.md')}';
window.gl.mrWidgetData.mr_troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}';
window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')}';
- window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}';
+ window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}';
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml
index 6c23661fb86..28ba2b6ac75 100644
--- a/app/views/projects/merge_requests/conflicts/show.html.haml
+++ b/app/views/projects/merge_requests/conflicts/show.html.haml
@@ -6,7 +6,7 @@
.merge-request-details.issuable-details
= render "projects/merge_requests/mr_box"
-= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees
+= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, source_branch: @merge_request.source_branch
#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json),
resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } }
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 874adb19734..f0a68512326 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -23,7 +23,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
+ = dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select source branch"))
= dropdown_filter(_("Search branches"))
@@ -52,7 +52,7 @@
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
- = dropdown_toggle f.object.target_branch || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
+ = dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select target branch"))
= dropdown_filter(_("Search branches"))
diff --git a/app/views/projects/merge_requests/diffs/_different_base.html.haml b/app/views/projects/merge_requests/diffs/_different_base.html.haml
index 0e57066f9c9..06a15b96653 100644
--- a/app/views/projects/merge_requests/diffs/_different_base.html.haml
+++ b/app/views/projects/merge_requests/diffs/_different_base.html.haml
@@ -1,7 +1,7 @@
- if @merge_request_diff && different_base?(@start_version, @merge_request_diff)
.mr-version-controls
.content-block
- = icon('info-circle')
+ = sprite_icon('information-o')
Selected versions have different base commits.
Changes will include
= link_to project_compare_path(@project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
diff --git a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
index 8d7138747fb..b9dc37c9b54 100644
--- a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
+++ b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
@@ -1,7 +1,7 @@
- if @commit || @start_version || (@merge_request_diff && !@merge_request_diff.latest?)
.mr-version-controls
.content-block.comments-disabled-notif.clearfix
- = icon('info-circle')
+ = sprite_icon('information-o')
= succeed '.' do
- if @commit
Only comments from the following commit are shown below
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 746d613934c..b579f7510f9 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -1,5 +1,5 @@
- @gfm_form = true
-- @content_class = "limit-container-width" unless fluid_layout
+- @content_class = "merge-request-container#{' limit-container-width' unless fluid_layout}"
- add_to_breadcrumbs _("Merge Requests"), project_merge_requests_path(@project)
- breadcrumb_title @merge_request.to_reference
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge Requests")
@@ -70,10 +70,13 @@
= render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
- if mr_action === "diffs"
- add_page_startup_api_call @endpoint_metadata_url
+ - params = request.query_parameters
+ - if Feature.enabled?(:default_merge_ref_for_diffs, @project)
+ - params = params.merge(diff_head: true)
= render "projects/merge_requests/tabs/pane", name: "diffs", id: "js-diffs-app", class: "diffs", data: { "is-locked": @merge_request.discussion_locked?,
- endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
+ endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', params),
endpoint_metadata: @endpoint_metadata_url,
- endpoint_batch: diffs_batch_project_json_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
+ endpoint_batch: diffs_batch_project_json_merge_request_path(@project, @merge_request, 'json', params),
endpoint_coverage: @coverage_path,
help_page_path: suggest_changes_help_path,
current_user_data: @current_user_data,
@@ -89,7 +92,7 @@
.loading.hide
.spinner.spinner-md
-= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees
+= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, source_branch: @merge_request.source_branch
- if @merge_request.can_be_reverted?(current_user)
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 2f55cce70dc..d7098bbb69d 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -30,7 +30,7 @@
.form-check.gl-mb-3
= check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input'
= label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label'
- = link_to icon('question-circle'), help_page_path('user/project/protected_branches'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('user/project/protected_branches'), target: '_blank'
.panel-footer.gl-display-flex.gl-justify-content-end
= f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
@@ -74,4 +74,4 @@
- if mirror.ssh_key_auth?
= clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
- %button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o')
+ %button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml
index 39ceaedab61..03839146f3b 100644
--- a/app/views/projects/mirrors/_mirror_repos_push.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml
@@ -10,4 +10,4 @@
.form-check.gl-mb-3
= check_box_tag :keep_divergent_refs, '1', false, class: 'js-mirror-keep-divergent-refs form-check-input'
= label_tag :keep_divergent_refs, _('Keep divergent refs'), class: 'form-check-label'
- = link_to icon('question-circle'), help_page_path('user/project/repository/repository_mirroring', anchor: 'keep-divergent-refs-core'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('user/project/repository/repository_mirroring', anchor: 'keep-divergent-refs'), target: '_blank'
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index 66721a28e62..058366eb75d 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -1,9 +1,10 @@
-- access = note_max_access_for_user(note)
-- if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR)
- %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
- = sprite_icon('first-contribution', css_class: 'gl-icon gl-vertical-align-top')
-- if access.nonzero?
- %span.note-role.user-access-role= Gitlab::Access.human_access(access)
+- access = note_human_max_access(note)
+- if note.noteable_author?(@noteable)
+ %span{ class: 'note-role user-access-role has-tooltip d-none d-md-inline-block', title: _("This user is the author of this %{noteable}.") % { noteable: @noteable.human_class_name } }= _("Author")
+- if access
+ %span{ class: 'note-role user-access-role has-tooltip', title: _("This user is a %{access} of the %{name} project.") % { access: access.downcase, name: note.project_name } }= access
+- elsif note.contributor?
+ %span{ class: 'note-role user-access-role has-tooltip', title: _("This user has previously committed to the %{name} project.") % { name: note.project_name } }= _("Contributor")
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
diff --git a/app/views/projects/packages/packages/_legacy_package_list.html.haml b/app/views/projects/packages/packages/_legacy_package_list.html.haml
deleted file mode 100644
index 43dbb5c3eee..00000000000
--- a/app/views/projects/packages/packages/_legacy_package_list.html.haml
+++ /dev/null
@@ -1,60 +0,0 @@
-- sort_value = @sort
-- sort_title = packages_sort_option_title(sort_value)
-
-- if @packages.any?
- .d-flex.justify-content-end
- .dropdown.inline.gl-mt-3.gl-mb-3.package-sort-dropdown
- .btn-group{ role: 'group' }
- .btn-group{ role: 'group' }
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
- = sort_title
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
- %li
- = sortable_item(sort_title_created_date, package_sort_path(sort: sort_value_recently_created), sort_title)
- = sortable_item(sort_title_name, package_sort_path(sort: sort_value_name_desc), sort_title)
- = sortable_item(sort_title_version, package_sort_path(sort: sort_value_version_desc), sort_title)
- = sortable_item(sort_title_type, package_sort_path(sort: sort_value_type_desc), sort_title)
- = packages_sort_direction_button(sort_value)
-
- .table-holder
- .gl-responsive-table-row.table-row-header.bg-secondary-50.px-2.border-top{ role: 'row' }
- .table-section.section-30{ role: 'rowheader' }
- = _('Name')
- .table-section.section-20{ role: 'rowheader' }
- = _('Version')
- .table-section.section-20{ role: 'rowheader' }
- = _('Type')
- .table-section.section-20{ role: 'rowheader' }
- = _('Created')
- .table-section.section-10{ role: 'rowheader' }
- - @packages.each do |package|
- .gl-responsive-table-row.package-row.px-2{ data: { qa_selector: "package_row" } }
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= _("Name")
- .table-mobile-content.flex-truncate-parent
- = link_to package.name, project_package_path(@project, package), class: 'flex-truncate-child', data: { qa_selector: "package_link" }
- .table-section.section-20
- .table-mobile-header{ role: "rowheader" }= _("Version")
- .table-mobile-content
- = package.version
- .table-section.section-20
- .table-mobile-header{ role: "rowheader" }= _("Type")
- .table-mobile-content
- = package.package_type
- .table-section.section-20
- .table-mobile-header{ role: "rowheader" }= _("Created")
- .table-mobile-content
- = time_ago_with_tooltip(package.created_at)
- .table-section.section-10
- .table-mobile-header{ role: "rowheader" }
- .table-mobile-content
- - if can_destroy_package
- .float-right
- = link_to project_package_path(@project, package), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-remove", title: _('Delete Package') do
- = icon('trash')
- = paginate @packages, theme: "gitlab"
-- else
- .row.empty-state
- .col-12
- = render 'shared/packages/no_packages'
diff --git a/app/views/projects/packages/packages/index.html.haml b/app/views/projects/packages/packages/index.html.haml
index c81326f3760..0d5350ab62b 100644
--- a/app/views/projects/packages/packages/index.html.haml
+++ b/app/views/projects/packages/packages/index.html.haml
@@ -1,4 +1,5 @@
-- page_title _("Packages")
+- page_title _("Package Registry")
+- @content_class = "limit-container-width" unless fluid_layout
.row
.col-12
diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml
index a66ae466d9d..97a3c6e7092 100644
--- a/app/views/projects/packages/packages/show.html.haml
+++ b/app/views/projects/packages/packages/show.html.haml
@@ -1,19 +1,19 @@
-- add_to_breadcrumbs _("Packages"), project_packages_path(@project)
+- add_to_breadcrumbs _("Package Registry"), project_packages_path(@project)
- add_to_breadcrumbs @package.name, project_packages_path(@project)
- breadcrumb_title @package.version
-- page_title _("Packages")
+- page_title _("Package Registry")
+- @content_class = "limit-container-width" unless fluid_layout
.row
.col-12
#js-vue-packages-detail{ data: { package: package_from_presenter(@package),
can_delete: can?(current_user, :destroy_package, @project).to_s,
- destroy_path: project_package_path(@project, @package),
svg_path: image_path('illustrations/no-packages.svg'),
npm_path: package_registry_instance_url(:npm),
npm_help_path: help_page_path('user/packages/npm_registry/index'),
maven_path: package_registry_project_url(@project.id, :maven),
maven_help_path: help_page_path('user/packages/maven_repository/index'),
- conan_path: package_registry_instance_url(:conan),
+ conan_path: package_registry_project_url(@project.id, :conan),
conan_help_path: help_page_path('user/packages/conan_repository/index'),
nuget_path: nuget_package_registry_url(@project.id),
nuget_help_path: help_page_path('user/packages/nuget_repository/index'),
@@ -22,4 +22,6 @@
pypi_help_path: help_page_path('user/packages/pypi_repository/index'),
composer_path: composer_registry_url(@project&.group&.id),
composer_help_path: help_page_path('user/packages/composer_repository/index'),
- project_name: @project.name} }
+ project_name: @project.name,
+ project_list_url: project_packages_path(@project),
+ group_list_url: @project.group ? group_packages_path(@project.group) : ''} }
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
index 63dd7ca1def..5b23234791b 100644
--- a/app/views/projects/pages/_access.html.haml
+++ b/app/views/projects/pages/_access.html.haml
@@ -14,10 +14,10 @@
%p
= external_link(domain.url, domain.url)
- unless @project.public_pages?
- .card-footer.alert-warning
+ .card-footer.gl-alert-warning
- help_page = help_page_path('/user/project/pages/pages_access_control')
- - link_start = '<a href="%{url}" target="_blank" class="alert-link" rel="noopener noreferrer">'.html_safe % { url: help_page }
+ - link_start = '<a href="%{url}" target="_blank" class="gl-alert-link" rel="noopener noreferrer">'.html_safe % { url: help_page }
- link_end = '</a>'.html_safe
= html_escape_once(s_('GitLabPages|Access Control is enabled for this Pages website; only authorized users will be able to access it. To make your website publicly available, navigate to your project\'s %{strong_start}Settings &gt; General &gt; Visibility%{strong_end} and select %{strong_start}Everyone%{strong_end} in pages section. Read the %{link_start}documentation%{link_end} for more information.')).html_safe % { link_start: link_start, link_end: link_end, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- .card-footer.alert-primary
+ .card-footer.gl-alert-info
= s_('GitLabPages|It may take up to 30 minutes before the site is available after the first deployment.')
diff --git a/app/views/projects/pipelines/_pipeline_warnings.html.haml b/app/views/projects/pipelines/_pipeline_warnings.html.haml
deleted file mode 100644
index e27bd440462..00000000000
--- a/app/views/projects/pipelines/_pipeline_warnings.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- if warnings.any?
- - warnings.map(&:content).each do |warning|
- .bs-callout.bs-callout-warning
- %p
- %b= _("Warning:")
- = markdown(warning)
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 4ae06e1e16f..f1ed67f8f82 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -26,7 +26,7 @@
= render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
.tab-content
- #js-tab-pipeline.tab-pane.position-absolute.position-left-0.w-100
+ #js-tab-pipeline.tab-pane.gl-absolute.gl-left-0.gl-w-full
#js-pipeline-graph-vue
#js-tab-builds.tab-pane
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 726bf9af223..2be75106000 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -7,7 +7,7 @@
%hr
- if Feature.enabled?(:new_pipeline_form)
- #js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project) } }
+ #js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project), max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
- else
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
@@ -43,5 +43,4 @@
= f.submit s_('Pipeline|Run Pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
= link_to _('Cancel'), project_pipelines_path(@project), class: 'btn btn-default float-right'
- -# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index e1a606b1765..a9c140aee5f 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -19,7 +19,6 @@
- lint_link_start = '<a href="%{url}">'.html_safe % { url: lint_link_url }
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
- = render "projects/pipelines/pipeline_warnings", warnings: @pipeline.warning_messages
= render "projects/pipelines/with_tabs", pipeline: @pipeline, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } }
diff --git a/app/views/projects/product_analytics/graphs.html.haml b/app/views/projects/product_analytics/graphs.html.haml
index 89286061594..c345561e6ce 100644
--- a/app/views/projects/product_analytics/graphs.html.haml
+++ b/app/views/projects/product_analytics/graphs.html.haml
@@ -5,6 +5,10 @@
%p
= _('Showing graphs based on events of the last %{timerange} days.') % { timerange: @timerange }
+
+.gl-mb-3
+ = render 'graph', graph: @activity_graph
+
- @graphs.each_slice(2) do |pair|
.row.append-bottom-10
- pair.each do |graph|
diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml
index 383c187b398..b17e6df1e8e 100644
--- a/app/views/projects/serverless/functions/index.html.haml
+++ b/app/views/projects/serverless/functions/index.html.haml
@@ -10,11 +10,10 @@
help_path: help_page_path('user/project/clusters/serverless/index'),
empty_image_path: image_path('illustrations/empty-state/empty-serverless-lg.svg') } }
-%div{ class: [('limit-container-width' unless fluid_layout)] }
- .js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } }
+.js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } }
- .js-serverless-functions-notice
- .flash-container
+.js-serverless-functions-notice
+ .flash-container
- .top-area.adjust.d-flex.justify-content-center.gl-border-none
- .serverless-functions-table#js-serverless-functions
+.top-area.adjust.d-flex.justify-content-center.gl-border-none
+ .serverless-functions-table#js-serverless-functions
diff --git a/app/views/projects/serverless/functions/show.html.haml b/app/views/projects/serverless/functions/show.html.haml
index 79bb943d6ed..dd81d957e51 100644
--- a/app/views/projects/serverless/functions/show.html.haml
+++ b/app/views/projects/serverless/functions/show.html.haml
@@ -11,10 +11,9 @@
clusters_path: clusters_path,
help_path: help_path } }
-%div{ class: [('limit-container-width' unless fluid_layout)] }
- .serverless-function-details#js-serverless-function-details
+.serverless-function-details#js-serverless-function-details
- .js-serverless-function-notice
- .flash-container
+.js-serverless-function-notice
+ .flash-container
- .function-holder.js-function-holder.input-group
+.function-holder.js-function-holder.input-group
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 24b47f6e4b6..2b1e08f4880 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -13,13 +13,9 @@
- if @service.respond_to?(:detailed_description)
%p= @service.detailed_description
.col-lg-8
- = form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
+ = form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, integration: @service
- .footer-block.row-content-block{ :class => "#{'gl-display-none' if @service.is_a?(AlertsService)}" }
- %input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
- = service_save_button(disabled: @service.is_a?(AlertsService))
- &nbsp;
- = link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'
+ %input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
%hr
diff --git a/app/views/projects/services/prometheus/_custom_metrics.html.haml b/app/views/projects/services/prometheus/_custom_metrics.html.haml
index 3642460467b..57100282c34 100644
--- a/app/views/projects/services/prometheus/_custom_metrics.html.haml
+++ b/app/views/projects/services/prometheus/_custom_metrics.html.haml
@@ -20,7 +20,7 @@
.flash-text
.loading-metrics.js-loading-custom-metrics
%p.m-3
- = icon('spinner spin', class: 'metrics-load-spinner')
+ = loading_icon(css_class: 'metrics-load-spinner')
= s_('PrometheusService|Finding custom metrics...')
.empty-metrics.hidden.js-empty-custom-metrics
%p.text-tertiary.m-3.js-no-active-integration-text.hidden
diff --git a/app/views/projects/services/prometheus/_metrics.html.haml b/app/views/projects/services/prometheus/_metrics.html.haml
index 79f5e846bd7..732a084d476 100644
--- a/app/views/projects/services/prometheus/_metrics.html.haml
+++ b/app/views/projects/services/prometheus/_metrics.html.haml
@@ -16,7 +16,7 @@
.card-body
.loading-metrics.js-loading-metrics
%p.m-3
- = icon('spinner spin', class: 'metrics-load-spinner')
+ = loading_icon(css_class: 'metrics-load-spinner')
= s_('PrometheusService|Finding and configuring metrics...')
.empty-metrics.hidden.js-empty-metrics
%p.text-tertiary.m-3
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index b4c9e51f53a..453deff7756 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -40,18 +40,18 @@
= form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input'
= form.label :deploy_strategy_continuous, class: 'form-check-label' do
= s_('CICD|Continuous deployment to production')
- = link_to icon('question-circle'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank'
.form-check
= form.radio_button :deploy_strategy, 'timed_incremental', class: 'form-check-input'
= form.label :deploy_strategy_timed_incremental, class: 'form-check-label' do
= s_('CICD|Continuous deployment to production using timed incremental rollout')
- = link_to icon('question-circle'), help_page_path('topics/autodevops/customize.md', anchor: 'timed-incremental-rollout-to-production-premium'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank'
.form-check
= form.radio_button :deploy_strategy, 'manual', class: 'form-check-input'
= form.label :deploy_strategy_manual, class: 'form-check-label' do
= s_('CICD|Automatic deployment to staging, manual deployment to production')
- = link_to icon('question-circle'), help_page_path('topics/autodevops/customize.md', anchor: 'incremental-rollout-to-production-premium'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'incremental-rollout-to-production'), target: '_blank'
= f.submit _('Save changes'), class: "btn btn-success gl-mt-5", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index a0dd06e3304..414a5f264bd 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -8,7 +8,7 @@
= _("Git strategy for pipelines")
%p
= html_escape(_("Choose between %{code_open}clone%{code_close} or %{code_open}fetch%{code_close} to get the recent application code")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- = link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
.form-check
= f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' }
= f.label :build_allow_git_fetch_false, class: 'form-check-label' do
@@ -38,7 +38,7 @@
= f.text_field :build_timeout_human_readable, class: 'form-control'
%p.form-text.text-muted
= _('If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like "1 hour". Values without specification represent seconds.')
- = link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'timeout'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'timeout'), target: '_blank'
- if can?(current_user, :update_max_artifacts_size, @project)
%hr
@@ -47,7 +47,7 @@
= f.number_field :max_artifacts_size, class: 'form-control'
%p.form-text.text-muted
= _("Set the maximum file size for each job's artifacts")
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size-core-only'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size'), target: '_blank'
%hr
.form-group
@@ -55,7 +55,7 @@
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted
= html_escape(_("The path to the CI configuration file. Defaults to %{code_open}.gitlab-ci.yml%{code_close}")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- = link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'custom-ci-configuration-path'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'custom-ci-configuration-path'), target: '_blank'
%hr
.form-group
@@ -65,7 +65,7 @@
%strong= _("Public pipelines")
.form-text.text-muted
= _("Allow public access to pipelines and job details, including output logs and artifacts")
- = link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
.bs-callout.bs-callout-info
%p #{_("If enabled")}:
%ul
@@ -86,7 +86,7 @@
%strong= _("Auto-cancel redundant, pending pipelines")
.form-text.text-muted
= _("New pipelines will cancel older, pending pipelines on the same branch")
- = link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank'
.form-group
.form-check
@@ -95,7 +95,7 @@
%strong= _("Skip outdated deployment jobs")
.form-text.text-muted
= _("When a deployment job is successful, skip older deployment jobs that are still pending")
- = link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'skip-outdated-deployment-jobs'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'skip-outdated-deployment-jobs'), target: '_blank'
%hr
.form-group
@@ -108,7 +108,7 @@
.input-group-text /
%p.form-text.text-muted
= _("A regular expression that will be used to find the test coverage output in the job log. Leave blank to disable")
- = link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
.bs-callout.bs-callout-info
%p= _("Below are examples of regex for existing tools:")
%ul
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4a521f2f46e..67bdcd0d9d6 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -7,22 +7,21 @@
= render partial: 'flash_messages', locals: { project: @project }
-%div{ class: [("limit-container-width" unless fluid_layout)] }
- = render "projects/last_push"
+= render "projects/last_push"
- = render "home_panel"
+= render "home_panel"
- - if can?(current_user, :download_code, @project) && @project.repository_languages.present?
- = repository_languages_bar(@project.repository_languages)
+- if can?(current_user, :download_code, @project) && @project.repository_languages.present?
+ = repository_languages_bar(@project.repository_languages)
- = render "archived_notice", project: @project
- = render_if_exists "projects/marked_for_deletion_notice", project: @project
- = render_if_exists "projects/ancestor_group_marked_for_deletion_notice", project: @project
+= render "archived_notice", project: @project
+= render_if_exists "projects/marked_for_deletion_notice", project: @project
+= render_if_exists "projects/ancestor_group_marked_for_deletion_notice", project: @project
- - view_path = @project.default_view
+- view_path = @project.default_view
- - if show_auto_devops_callout?(@project)
- = render 'shared/auto_devops_callout'
+- if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
- %div{ class: project_child_container_class(view_path) }
- = render view_path, is_project_overview: true
+%div{ class: project_child_container_class(view_path) }
+ = render view_path, is_project_overview: true
diff --git a/app/views/projects/static_site_editor/show.html.haml b/app/views/projects/static_site_editor/show.html.haml
index 2d817912335..cbe27cefba3 100644
--- a/app/views/projects/static_site_editor/show.html.haml
+++ b/app/views/projects/static_site_editor/show.html.haml
@@ -1 +1 @@
-#static-site-editor{ data: @config.payload.merge({ merge_requests_illustration_path: image_path('illustrations/merge_requests.svg') }) }
+#static-site-editor{ data: @data }
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index c8a6168edfc..dba9b20fcff 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -2,8 +2,8 @@
- release = @releases.find { |release| release.tag == tag.name }
%li.flex-row.allow-wrap
.row-main-content
- = icon('tag')
- = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name gl-ml-2'
+ = sprite_icon('tag')
+ = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name'
- if protected_tag?(@project, tag)
%span.badge.badge-success.gl-ml-2
@@ -39,4 +39,4 @@
= link_to edit_project_tag_release_path(@project, tag.name), class: 'btn btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
= sprite_icon("pencil")
= link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip gl-ml-3 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
- = icon("trash-o")
+ = sprite_icon("remove")
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
index cde23e03d54..59d359bbf10 100644
--- a/app/views/projects/tags/destroy.js.haml
+++ b/app/views/projects/tags/destroy.js.haml
@@ -1,4 +1,4 @@
- if @error.present?
- new Flash('#{escape_javascript(@error)}', 'alert');
+ new Flash({ message: '#{escape_javascript(@error)}', type: 'alert' });
- elsif @repository.tags.empty?
$('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index c32318df7cc..fe42394d919 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -2,8 +2,10 @@
- default_ref = params[:ref] || @project.default_branch
- if @error
- .alert.alert-danger
- %button.close{ type: "button", "data-dismiss" => "alert" } &times;
+ .gl-alert.gl-alert-danger
+ = sprite_icon('error', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
+ = sprite_icon('close', css_class: 'gl-icon')
= @error
%h3.page-title
@@ -52,5 +54,4 @@
.form-actions
= button_tag s_('TagsPage|Create tag'), class: 'btn btn-success', data: { qa_selector: "create_tag_button" }
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
--# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index ff973e2922f..25a560da5c6 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -10,7 +10,7 @@
.nav-text
.title
%span.item-title.ref-name{ data: { qa_selector: 'tag_name_content' } }
- = icon('tag')
+ = sprite_icon('tag')
= @tag.name
- if protected_tag?(@project, @tag)
%span.badge.badge-success
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index eab6d750a02..268858f8ff8 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,5 +1,6 @@
- can_collaborate = can_collaborate_with_project?(@project)
- can_create_mr_from_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
+- can_visit_ide = can_collaborate || current_user&.already_forked?(@project)
.tree-ref-container
.tree-ref-holder
@@ -14,12 +15,12 @@
= render 'projects/find_file_link'
- - if can_collaborate || current_user&.already_forked?(@project)
- #js-tree-web-ide-link.d-inline-block
- - elsif can_create_mr_from_fork
- = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
- = _('Web IDE')
- = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
+ - if can_visit_ide || can_create_mr_from_fork
+ #js-tree-web-ide-link.d-inline-block{ data: { options: vue_ide_link_data(@project, @ref).to_json } }
+ - if !can_visit_ide
+ = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
+ - unless current_user&.gitpod_enabled
+ = render 'shared/gitpod/enable_gitpod_modal'
- if show_xcode_link?(@project)
.project-action-button.project-xcode.inline<
diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml
index 300cd5423bf..04496914c02 100644
--- a/app/views/projects/tree/_tree_row.html.haml
+++ b/app/views/projects/tree/_tree_row.html.haml
@@ -23,5 +23,5 @@
%td.d-none.d-sm-table-cell.tree-commit
%td.tree-time-ago.text-right
%span.log_loading.hide
- %i.fa.fa-spinner.fa-spin
+ = loading_icon
Loading commit data...
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 3036e918160..579b8ba2766 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -34,4 +34,4 @@
= sprite_icon('pencil')
- if can?(current_user, :manage_trigger, trigger)
= link_to project_trigger_path(@project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do
- %i.fa.fa-trash
+ = sprite_icon('remove')
diff --git a/app/views/registrations/welcome.html.haml b/app/views/registrations/welcome.html.haml
index ef3e0b1b4c0..5ad0fbf8fbc 100644
--- a/app/views/registrations/welcome.html.haml
+++ b/app/views/registrations/welcome.html.haml
@@ -1,22 +1,25 @@
-- content_for(:page_title, _('Welcome to GitLab %{name}!') % { name: current_user.name })
-.text-center.mb-3
- = html_escape(_('In order to tailor your experience with GitLab we%{br_tag}would like to know a bit more about you.')) % { br_tag: '<br/>'.html_safe }
-.signup-box.p-3.mb-2
- .signup-body
- = form_for(current_user, url: users_sign_up_update_registration_path, html: { class: 'new_new_user gl-show-field-errors', 'aria-live' => 'assertive' }) do |f|
- .devise-errors.mt-0
- = render 'devise/shared/error_messages', resource: current_user
- .form-group
- = f.label :role, _('Role'), class: 'label-bold'
- = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control'
- .form-group
- = f.label :setup_for_company, _('Are you setting up GitLab for a company?'), class: 'label-bold'
- .d-flex.justify-content-center
- .w-25
- = f.radio_button :setup_for_company, true
- = f.label :setup_for_company, _('Yes'), value: 'true'
- .w-25
- = f.radio_button :setup_for_company, false
- = f.label :setup_for_company, _('No'), value: 'false'
- .submit-container.mt-3
- = f.submit _('Get started!'), class: 'btn-register btn btn-block mb-0 p-2'
+- page_title _('Your profile')
+
+.row.gl-flex-grow-1.gl-bg-gray-10
+ .d-flex.gl-flex-direction-column.gl-align-items-center.gl-w-full.gl-p-5
+ .edit-profile.login-page.d-flex.flex-column.gl-align-items-center.pt-lg-3
+ = render_if_exists "registrations/welcome/progress_bar"
+ %h2.gl-text-center= html_escape(_('Welcome to GitLab%{br_tag}%{name}!')) % { name: html_escape(current_user.first_name), br_tag: '<br/>'.html_safe }
+ %p
+ .gl-text-center= html_escape(_('In order to personalize your experience with GitLab%{br_tag}we would like to know a bit more about you.')) % { br_tag: '<br/>'.html_safe }
+
+ = form_for(current_user, url: users_sign_up_update_registration_path, html: { class: 'card gl-w-full! gl-p-5', 'aria-live' => 'assertive' }) do |f|
+ .devise-errors
+ = render 'devise/shared/error_messages', resource: current_user
+ .row
+ .form-group.col-sm-12
+ = f.label :role, _('Role'), class: 'label-bold'
+ = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control', autofocus: true
+ .form-text.gl-text-gray-500.gl-mt-3= _('This will help us personalize your onboarding experience.')
+ = render_if_exists "registrations/welcome/setup_for_company", f: f
+ .row
+ .form-group.col-sm-12.gl-mb-0
+ - if partial_exists? "registrations/welcome/button"
+ = render "registrations/welcome/button"
+ - else
+ = f.submit _('Get started!'), class: 'btn-register btn btn-block gl-mb-0 gl-p-3', data: { qa_selector: 'get_started_button' }
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 79f01c61833..e0dbb5135e9 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -22,6 +22,8 @@
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
= render_if_exists 'shared/promotions/promote_advanced_search'
+ #js-search-filter-by-state{ 'v-cloak': true, data: { scope: @scope, state: params[:state] } }
+
.results.gl-mt-3
- if @scope == 'commits'
%ul.content-list.commit-list
diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml
index 218de30d707..27d4dbe1085 100644
--- a/app/views/search/results/_blob_data.html.haml
+++ b/app/views/search/results/_blob_data.html.haml
@@ -1,7 +1,7 @@
.blob-result{ data: { qa_selector: 'result_item_content' } }
.file-holder
.js-file-title.file-title{ data: { qa_selector: 'file_title_content' } }
- = link_to blob_link do
+ = link_to blob_link, data: {track_event: 'click_text', track_label: 'blob_path', track_property: 'search_result'} do
%i.fa.fa-file
%strong
= search_blob_title(project, path)
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
index ed5a3badf11..3e5ea785aae 100644
--- a/app/views/search/results/_commit.html.haml
+++ b/app/views/search/results/_commit.html.haml
@@ -1 +1 @@
-= render 'projects/commits/commit', project: commit.project, commit: commit, ref: nil, show_project_name: @project.nil?
+= render 'projects/commits/commit', project: commit.project, commit: commit, ref: nil, show_project_name: @project.nil?, link_data_attrs: {track_event: 'click_text', track_label: 'commit_title', track_property: 'search_result'}
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 2f6024c3f2b..e0336d98f04 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -1,13 +1,14 @@
-.search-result-row
- %h4
- = confidential_icon(issue)
- = link_to project_issue_path(issue.project, issue) do
- %span.term.str-truncated= issue.title
+%div{ class: 'search-result-row gl-pb-3! gl-mt-5 gl-mb-0!' }
+ %span.gl-display-flex.gl-align-items-center
- if issue.closed?
- %span.badge.badge-danger.gl-ml-2= _("Closed")
- .float-right ##{issue.iid}
+ %span.badge.badge-info.badge-pill.gl-badge.sm= _("Closed")
+ - else
+ %span.badge.badge-success.badge-pill.gl-badge.sm= _("Open")
+ = sprite_icon('eye-slash', css_class: 'gl-text-gray-500 gl-ml-2') if issue.confidential?
+ = link_to project_issue_path(issue.project, issue), data: { track_event: 'click_text', track_label: 'issue_title', track_property: 'search_result' }, class: 'gl-w-full' do
+ %span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issue.title
+ .gl-text-gray-500.gl-my-3
+ = sprintf(s_(' %{project_name}#%{issue_iid} &middot; opened %{issue_created} by %{author}'), { project_name: issue.project.full_name, issue_iid: issue.iid, issue_created: time_ago_with_tooltip(issue.created_at, placement: 'bottom'), author: link_to_member(@project, issue.author, avatar: false) }).html_safe
- if issue.description.present?
- .description.term
- = search_md_sanitize(issue, :description)
- %span.light
- #{issue.project.full_name}
+ .description.term.col-sm-10.gl-px-0
+ = truncate(issue.description, length: 200)
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 680c2ea0208..3135ab9a17e 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -1,6 +1,6 @@
.search-result-row
%h4
- = link_to project_merge_request_path(merge_request.target_project, merge_request) do
+ = link_to project_merge_request_path(merge_request.target_project, merge_request), data: {track_event: 'click_text', track_label: 'merge_request_title', track_property: 'search_result'} do
%span.term.str-truncated= merge_request.title
- if merge_request.merged?
%span.badge.badge-primary.gl-ml-2= _("Merged")
@@ -9,6 +9,6 @@
.float-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
- = search_md_sanitize(merge_request, :description)
+ = search_md_sanitize(merge_request.description)
%span.light
#{merge_request.project.full_name}
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index 53c2d380bc5..6d4ce88a377 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -1,8 +1,8 @@
.search-result-row
%h4
- = link_to project_milestone_path(milestone.project, milestone) do
+ = link_to project_milestone_path(milestone.project, milestone), data: {track_event: 'click_text', track_label: 'milestone_title', track_property: 'search_result'} do
%span.term.str-truncated= milestone.title
- if milestone.description.present?
.description.term
- = search_md_sanitize(milestone, :description)
+ = search_md_sanitize(milestone.description)
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index a83b003a516..d88b7b32ed6 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -18,8 +18,13 @@
- else
%span #{note.noteable_type.titleize} ##{noteable_identifier}
&middot;
- = link_to note.noteable.title, note_url
+ = link_to note.noteable.title, note_url, data: {track_event: 'click_text', track_label: 'noteable_title', track_property: 'search_result'}
+
+ %span.note-headline-light.note-headline-meta
+ %span.system-note-separator
+ &middot;
+ %span.system-note-separator= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
.note-search-result
.term
- = search_md_sanitize(note, :note)
+ = search_md_sanitize(note.note)
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index bf1683be32d..443d801d672 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -12,4 +12,4 @@
%button.gl-banner-close.close.js-close-callout{ type: 'button',
'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box') }
- = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
+ = sprite_icon('close', size: 16, css_class: 'dismiss-icon')
diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml
index e313946a968..7be11b0fb81 100644
--- a/app/views/shared/_broadcast_message.html.haml
+++ b/app/views/shared/_broadcast_message.html.haml
@@ -2,11 +2,11 @@
%div{ class: "broadcast-message #{'alert-warning' if is_banner} broadcast-#{message.broadcast_type}-message #{opts[:preview] && 'preview'} js-broadcast-notification-#{message.id} gl-display-flex",
style: broadcast_message_style(message), dir: 'auto' }
- .flex-grow-1.text-right.pr-2
+ .gl-flex-grow-1.gl-text-right.gl-pr-3
= sprite_icon('bullhorn', css_class: 'vertical-align-text-top')
%div{ class: !fluid_layout && 'container-limited' }
= render_broadcast_message(message)
- .flex-grow-1.text-right{ style: 'flex-basis: 0' }
+ .gl-flex-grow-1.gl-flex-basis-0.gl-text-right
- if (message.notification? || message.dismissable?) && opts[:preview].blank?
- %button.broadcast-message-dismiss.js-dismiss-current-broadcast-notification.btn.btn-link.pl-2.pr-2{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
- %i.fa.fa-times
+ %button.broadcast-message-dismiss.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
+ = sprite_icon('close', size: 16, css_class: 'gl-icon gl-text-white gl-mx-3!')
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index a99c992af49..7d328728332 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -1,4 +1,4 @@
-.file-content.code.js-syntax-highlight
+#blob-content.file-content.code.js-syntax-highlight
.line-numbers
- if blob.data.present?
- link_icon = sprite_icon('link', size: 12)
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index d497937833a..ca603eed703 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -34,10 +34,12 @@
%p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking group URL availability...')
- if @group.persisted?
- .alert.alert-warning.gl-mt-3
- = _('Changing group URL can have unintended side effects.')
- = succeed '.' do
- = link_to _('Learn more'), help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
+ .gl-alert.gl-alert-warning.gl-mt-3.gl-mb-3
+ = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ = _('Changing group URL can have unintended side effects.')
+ = succeed '.' do
+ = link_to _('Learn more'), help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank', class: 'gl-link'
- if @group.persisted?
.row
diff --git a/app/views/shared/_help_dropdown_forum_link.html.haml b/app/views/shared/_help_dropdown_forum_link.html.haml
new file mode 100644
index 00000000000..351c875475a
--- /dev/null
+++ b/app/views/shared/_help_dropdown_forum_link.html.haml
@@ -0,0 +1,2 @@
+= link_to _("Community forum"), "https://forum.gitlab.com/", target: '_blank', class: 'text-nowrap',
+ rel: 'noopener noreferrer', data: { 'track_event': 'click_forum', 'track_property': 'question_menu' }
diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml
index 9b1a467df6b..76ae63ca5e8 100644
--- a/app/views/shared/_no_password.html.haml
+++ b/app/views/shared/_no_password.html.haml
@@ -1,9 +1,12 @@
- if show_no_password_message?
- .no-password-message.alert.alert-warning
- - translation_params = { protocol: gitlab_config.protocol.upcase, set_password_link: link_to_set_password }
- - set_password_message = _("You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account") % translation_params
- = set_password_message.html_safe
- .alert-link-group
- = link_to _("Don't show again"), profile_path(user: {hide_no_password: true}), method: :put
- |
- = link_to _('Remind later'), '#', class: 'hide-no-password-message'
+ .no-password-message.gl-alert.gl-alert-warning
+ = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label': _('Dismiss') }
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ .gl-alert-body
+ - translation_params = { protocol: gitlab_config.protocol.upcase, set_password_link: link_to_set_password }
+ - set_password_message = _("You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account") % translation_params
+ = set_password_message.html_safe
+ .gl-alert-actions
+ = link_to _('Remind later'), '#', class: 'hide-no-password-message btn gl-alert-action btn-info btn-md gl-button'
+ = link_to _("Don't show again"), profile_path(user: {hide_no_password: true}), method: :put, role: 'button', class: 'btn gl-alert-action btn-md btn-default gl-button btn-default-secondary'
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index abf39fdc644..a083a772233 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -7,4 +7,4 @@
= s_("MissingSSHKeyWarningLink|You won't be able to pull or push project code via SSH until you add an SSH key to your profile").html_safe
.gl-alert-actions
= link_to s_('MissingSSHKeyWarningLink|Add SSH key'), profile_keys_path, class: "btn gl-alert-action btn-warning btn-md new-gl-button"
- = link_to s_("MissingSSHKeyWarningLink|Don't show again"), profile_path(user: {hide_no_ssh_key: true}), method: :put, role: 'button', class: 'btn gl-alert-action btn-md btn-warning btn-secondary new-gl-button'
+ = link_to s_("MissingSSHKeyWarningLink|Don't show again"), profile_path(user: {hide_no_ssh_key: true}), method: :put, role: 'button', class: 'btn gl-alert-action btn-md btn-warning gl-button btn-warning-secondary'
diff --git a/app/views/shared/_old_visibility_level.html.haml b/app/views/shared/_old_visibility_level.html.haml
index e8f3d888cce..8a9cc7ab8a2 100644
--- a/app/views/shared/_old_visibility_level.html.haml
+++ b/app/views/shared/_old_visibility_level.html.haml
@@ -1,6 +1,6 @@
.form-group.row
.col-sm-2.col-form-label
= _('Visibility level')
- = link_to icon('question-circle'), help_page_path("public_access/public_access"), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('public_access/public_access'), target: '_blank'
.col-sm-10
= render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_visibility_level, form_model: form_model, with_label: with_label
diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml
index 624cc99440c..f5a32050a79 100644
--- a/app/views/shared/_outdated_browser.html.haml
+++ b/app/views/shared/_outdated_browser.html.haml
@@ -2,14 +2,7 @@
.gl-alert.gl-alert-danger.outdated-browser{ :role => "alert" }
= sprite_icon('error', css_class: "gl-alert-icon gl-alert-icon-no-title gl-icon")
.gl-alert-body
- - if browser.ie? && browser.version.to_i == 11
- - feedback_link_url = 'https://gitlab.com/gitlab-org/gitlab/issues/197987'
- - feedback_link_start = '<a href="%{url}" class="gl-link" target="_blank" rel="noopener noreferrer">'.html_safe % { url: feedback_link_url }
- = s_('OutdatedBrowser|From May 2020 GitLab no longer supports Internet Explorer 11.')
- %br
- = s_('OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels.').html_safe % { feedback_link_start: feedback_link_start, feedback_link_end: '</a>'.html_safe }
- - else
- = s_('OutdatedBrowser|GitLab may not work properly, because you are using an outdated web browser.')
+ = s_('OutdatedBrowser|GitLab may not work properly, because you are using an outdated web browser.')
%br
- browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('install/requirements', anchor: 'supported-web-browsers') }
= s_('OutdatedBrowser|Please install a %{browser_link_start}supported web browser%{browser_link_end} for a better experience.').html_safe % { browser_link_start: browser_link_start, browser_link_end: '</a>'.html_safe }
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index 88f213612fc..3d5229f87b5 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -1,5 +1,5 @@
- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0
- .project-limit-message.alert.alert-warning.d-none.d-sm-block
+ .project-limit-message.gl-alert.gl-alert-warning.gl-display-none.gl-display-sm-block
= _("You won't be able to create new projects because you have reached your project limit.")
.float-right
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 2425bcf61d9..647421a8fbe 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -8,6 +8,6 @@
= markdown integration.help
.service-settings
- - if @admin_integration
- .js-vue-admin-integration-settings{ data: integration_form_data(@admin_integration) }
+ - if @default_integration
+ .js-vue-default-integration-settings{ data: integration_form_data(@default_integration) }
.js-vue-integration-settings{ data: integration_form_data(integration) }
diff --git a/app/views/shared/_zen.html.haml b/app/views/shared/_zen.html.haml
index 66e0ecadb65..9cf189e8120 100644
--- a/app/views/shared/_zen.html.haml
+++ b/app/views/shared/_zen.html.haml
@@ -16,4 +16,4 @@
- else
= text_area_tag attr, current_text, data: { qa_selector: qa_selector }, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave.gl-text-gray-500{ href: "#" }
- = sprite_icon('compress')
+ = sprite_icon('minimize')
diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml
index ceac4d1820d..255ec9995db 100644
--- a/app/views/shared/access_tokens/_table.html.haml
+++ b/app/views/shared/access_tokens/_table.html.haml
@@ -18,7 +18,7 @@
%th= s_('AccessTokens|Created')
%th
= _('Last Used')
- = link_to icon('question-circle'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'token-activity'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'token-activity'), target: '_blank'
%th= _('Expires')
%th= _('Scopes')
%th
@@ -35,7 +35,7 @@
%td
- if token.expires?
- if token.expires_at.past? || token.expires_at.today?
- %span{ class: 'text-danger has-tooltip', title: _('Expiration not enforced') }
+ %span{ class: 'text-danger has-tooltip', title: _('Token valid until revoked') }
= _('Expired')
- else
%span{ class: ('text-warning' if token.expires_soon?) }
diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml
index c1ffdc7184a..085206714c6 100644
--- a/app/views/shared/blob/_markdown_buttons.html.haml
+++ b/app/views/shared/blob/_markdown_buttons.html.haml
@@ -1,13 +1,25 @@
+- modifier_key = client_js_flags[:isMac] ? '⌘' : s_('KeyboardKey|Ctrl+');
+
.md-header-toolbar.active
- = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: _("Add bold text") })
- = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: _("Add italic text") })
+ = markdown_toolbar_button({ icon: "bold",
+ data: { "md-tag" => "**", "md-shortcuts": '["mod+b"]' },
+ title: sprintf(s_("MarkdownEditor|Add bold text (%{modifier_key}B)") % { modifier_key: modifier_key }) })
+
+ = markdown_toolbar_button({ icon: "italic",
+ data: { "md-tag" => "_", "md-shortcuts": '["mod+i"]' },
+ title: sprintf(s_("MarkdownEditor|Add italic text (%{modifier_key}I)") % { modifier_key: modifier_key }) })
+
= 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: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: _("Add a link") })
+
+ = markdown_toolbar_button({ icon: "link",
+ data: { "md-tag" => "[{text}](url)", "md-select" => "url", "md-shortcuts": '["mod+k"]' },
+ title: sprintf(s_("MarkdownEditor|Add a link (%{modifier_key}K)") % { modifier_key: modifier_key }) })
+
= 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: "list-task", data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a task list") })
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
- if show_fullscreen_button
%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")
+ = sprite_icon("maximize")
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 7a4c495e177..e5808bfe878 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -2,7 +2,6 @@
- group = local_assigns.fetch(:group, false)
-# TODO: Move group_id and can_admin_list to the board store
See: https://gitlab.com/gitlab-org/gitlab/-/issues/213082
-- group_id = @group&.id || "null"
- can_admin_list = can?(current_user, :admin_list, current_board_parent) == true
- @no_breadcrumb_container = true
- @no_container = true
@@ -12,23 +11,18 @@
- content_for :page_specific_javascripts do
- -# haml-lint:disable InlineJavaScript
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal, show_sorting_dropdown: false
%script#js-board-promotion{ type: "text/x-template" }= render_if_exists "shared/promotions/promote_issue_board"
#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
= render 'shared/issuable/search_bar', type: :boards, board: board
- - if Feature.enabled?(:boards_with_swimlanes, current_board_parent)
+ - if Feature.enabled?(:boards_with_swimlanes, current_board_parent) || Feature.enabled?(:graphql_board_lists, current_board_parent)
%board-content{ "v-cloak" => "true",
"ref" => "board_content",
":lists" => "state.lists",
":can-admin-list" => can_admin_list,
- ":group-id" => group_id,
- ":disabled" => "disabled",
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
- ":board-id" => "boardId" }
+ ":disabled" => "disabled" }
- else
.boards-list.w-100.py-3.px-2.text-nowrap{ data: { qa_selector: "boards_list" } }
.boards-app-loading.w-100.text-center{ "v-if" => "loading" }
@@ -37,12 +31,8 @@
"v-for" => "list in state.lists",
"ref" => "board",
":can-admin-list" => can_admin_list,
- ":group-id" => group_id,
":list" => "list",
":disabled" => "disabled",
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
- ":board-id" => "boardId",
":key" => "list.id" }
= render "shared/boards/components/sidebar", group: group
= render_if_exists 'shared/boards/components/board_settings_sidebar'
@@ -51,6 +41,4 @@
"milestone-path" => milestones_filter_dropdown_path,
"label-path" => labels_filter_path_with_defaults,
"empty-state-svg" => image_path('illustrations/issues.svg'),
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
":project-id" => @project.id }
diff --git a/app/views/shared/boards/_switcher.html.haml b/app/views/shared/boards/_switcher.html.haml
index 09a365a290a..58e877f20fe 100644
--- a/app/views/shared/boards/_switcher.html.haml
+++ b/app/views/shared/boards/_switcher.html.haml
@@ -10,6 +10,7 @@
can_admin_board: can?(current_user, :admin_board, parent).to_s,
multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s,
labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: true),
+ labels_web_url: parent.is_a?(Project) ? project_labels_path(@project) : group_labels_path(@group),
project_id: @project&.id,
group_id: @group&.id,
scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false',
diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml
index 510e05ce888..2c894e9b1b3 100644
--- a/app/views/shared/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml
@@ -16,9 +16,10 @@
name: "issue[milestone_id]",
"v-if" => "issue.milestone" }
.dropdown
- %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
+ %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", ability_name: "issue", use_id: "true", default_no: "true" },
":data-selected" => "milestoneTitle",
- ":data-issuable-id" => "issue.iid" }
+ ":data-issuable-id" => "issue.iid",
+ ":data-project-id" => "issue.project_id" }
= _("Milestone")
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 1eda439c9a5..cc5addaa3a0 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -35,6 +35,7 @@
= label_tag ("deploy_token_write_registry"), 'write_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows write access to the registry images')
+ - if packages_registry_enabled?(group_or_project)
%fieldset.form-group.form-check
= f.check_box :read_package_registry, class: 'form-check-input'
= label_tag ("deploy_token_read_package_registry"), 'read_package_registry', class: 'label-bold form-check-label'
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 3fd64291fb2..8c5319e2178 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -51,7 +51,7 @@
%strong
= s_('JiraService|Using Jira for issue tracking?')
%p.gl-text-center.gl-mb-0
- - jira_docs_link_url = help_page_url('user/project/integrations/jira', anchor: 'view-jira-issues-premium')
+ - jira_docs_link_url = help_page_url('user/project/integrations/jira', anchor: 'view-jira-issues')
- jira_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: jira_docs_link_url }
= html_escape(s_('JiraService|%{jira_docs_link_start}Enable the Jira integration%{jira_docs_link_end} to view your Jira issues in GitLab.')) % { jira_docs_link_start: jira_docs_link_start.html_safe, jira_docs_link_end: '</a>'.html_safe }
%p.gl-text-center.gl-mb-0.gl-text-gray-500
diff --git a/app/views/shared/form_elements/_apply_template_warning.html.haml b/app/views/shared/form_elements/_apply_template_warning.html.haml
index 09ca59a520c..b1edfba6df4 100644
--- a/app/views/shared/form_elements/_apply_template_warning.html.haml
+++ b/app/views/shared/form_elements/_apply_template_warning.html.haml
@@ -1,4 +1,4 @@
-.form-group.row.js-template-warning.mb-0.hidden.js-issuable-template-warning
+.form-group.row.js-template-warning.mb-0.hidden.js-issuable-template-warning{ :class => ("gl-mb-5!" if issuable.supports_issue_type? && can?(current_user, :admin_issue, @project)) }
.offset-sm-2.col-sm-10
.warning_message.mb-0{ role: 'alert' }
diff --git a/app/views/shared/gitpod/_enable_gitpod_modal.html.haml b/app/views/shared/gitpod/_enable_gitpod_modal.html.haml
new file mode 100644
index 00000000000..a6bd1d10e43
--- /dev/null
+++ b/app/views/shared/gitpod/_enable_gitpod_modal.html.haml
@@ -0,0 +1,12 @@
+#modal-enable-gitpod.modal.qa-enable-gitpod-modal
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %h3.page-title= _('Enable Gitpod?')
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
+ .modal-body.p-3
+ %p= (_("To use Gitpod you must first enable the feature in the integrations section of your %{user_prefs}.") % { user_prefs: link_to(_('user preferences'), profile_preferences_path(anchor: 'gitpod')) }).html_safe
+ .modal-footer
+ = link_to _('Cancel'), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+ = link_to _('Enable Gitpod'), profile_path(user: { gitpod_enabled: true}), class: 'btn btn-success', method: :put
diff --git a/app/views/shared/icons/_dev_ops_score_no_data.svg b/app/views/shared/icons/_dev_ops_report_no_data.svg
index 5de929859ae..5de929859ae 100644
--- a/app/views/shared/icons/_dev_ops_score_no_data.svg
+++ b/app/views/shared/icons/_dev_ops_report_no_data.svg
diff --git a/app/views/shared/icons/_dev_ops_score_no_index.svg b/app/views/shared/icons/_dev_ops_report_no_index.svg
index 0577efca93f..0577efca93f 100644
--- a/app/views/shared/icons/_dev_ops_score_no_index.svg
+++ b/app/views/shared/icons/_dev_ops_report_no_index.svg
diff --git a/app/views/shared/icons/_dev_ops_score_overview.svg b/app/views/shared/icons/_dev_ops_report_overview.svg
index 2f31113bad7..2f31113bad7 100644
--- a/app/views/shared/icons/_dev_ops_score_overview.svg
+++ b/app/views/shared/icons/_dev_ops_report_overview.svg
diff --git a/app/views/shared/integrations/_form.html.haml b/app/views/shared/integrations/_form.html.haml
index 5826cb280bd..11e390a47e2 100644
--- a/app/views/shared/integrations/_form.html.haml
+++ b/app/views/shared/integrations/_form.html.haml
@@ -6,10 +6,5 @@
= integration.title
.col-lg-8
- = form_for integration, as: :service, url: scoped_integration_path(integration), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => integration.can_test?, 'test-url' => scoped_test_integration_path(integration) } } do |form|
+ = form_for integration, as: :service, url: scoped_integration_path(integration), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => scoped_test_integration_path(integration) } } do |form|
= render 'shared/service_settings', form: form, integration: integration
-
- - if integration.editable?
- .footer-block.row-content-block
- = service_save_button
- = link_to _('Cancel'), scoped_integration_path(integration), class: 'btn btn-cancel'
diff --git a/app/views/shared/integrations/_index.html.haml b/app/views/shared/integrations/_index.html.haml
index 2dbd612ea38..2f299ad5c89 100644
--- a/app/views/shared/integrations/_index.html.haml
+++ b/app/views/shared/integrations/_index.html.haml
@@ -3,7 +3,7 @@
%col
%col
%col.d-none.d-sm-table-column
- %col{ width: 130 }
+ %col{ width: 135 }
%thead{ role: 'rowgroup' }
%tr{ role: 'row' }
%th{ role: 'columnheader', scope: 'col', 'aria-colindex': 1 }
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index 0c15d20bfe0..09abe9e89c4 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -40,7 +40,7 @@
.title
= _('Milestone')
.filter-item
- = dropdown_tag(_("Select milestone"), options: { title: _("Assign milestone"), toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: _("Search milestones"), data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: project_milestones_path(@project, :json), use_id: true, default_label: _("Milestone") } })
+ = dropdown_tag(_("Select milestone"), options: { title: _("Assign milestone"), toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: _("Search milestones"), data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, use_id: true, default_label: _("Milestone") } })
.block
.title
= _('Labels')
diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
index 3fc6a3b545b..4c7aee09406 100644
--- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
+++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
@@ -9,7 +9,7 @@
- add_blocked_class = !issuable.closed? && warn_before_close
.float-left.btn-group.gl-ml-3.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
- %button{ class: "#{button_class} btn-#{button_action} #{(add_blocked_class ? 'btn-issue-blocked' : '')}", data: { qa_selector: 'close_issue_button', endpoint: close_reopen_issuable_path(issuable) } }
+ %button{ class: "#{button_class} btn-#{button_action} #{(add_blocked_class ? 'btn-issue-blocked' : '')}", data: { testid: 'close-issue-button', qa_selector: 'close_issue_button', endpoint: close_reopen_issuable_path(issuable) } }
#{display_button_action} #{display_issuable_type}
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
@@ -39,10 +39,8 @@
%li.divider.droplab-item-ignore
- %li.report-item{ data: { text: _('Report abuse'), url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
- button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
- %button.btn.btn-transparent
- = icon('check', class: 'icon')
+ %li.report-item{ data: { text: _('Report abuse'), button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
+ %a.report-abuse-link{ :href => new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)) }
.description
%strong.title= _('Report abuse')
%p.text
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 86cd2923fac..728b527f499 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -6,7 +6,7 @@
= form_errors(issuable)
- if @conflict
- .alert.alert-danger
+ .gl-alert.gl-alert-danger.gl-mb-5
Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
Please check out
= link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project, issuable]), target: "_blank", rel: 'noopener noreferrer'
@@ -20,7 +20,10 @@
= render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
#js-suggestions{ data: { project_path: @project.full_path } }
-= render 'shared/form_elements/apply_template_warning'
+= render 'shared/form_elements/apply_template_warning', issuable: issuable
+
+= render 'shared/issuable/form/type_selector', issuable: issuable, form: form
+
= render 'shared/form_elements/description', model: issuable, form: form, project: project
- if issuable.respond_to?(:confidential)
@@ -42,7 +45,7 @@
- if @merge_request_to_resolve_discussions_of
.form-group.row
.col-sm-10.offset-sm-2
- = icon('info-circle')
+ = sprite_icon('information-o')
- if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user)
= hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid
- if @discussion_to_resolve
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index c2da363b8c6..f58156b7c08 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -8,7 +8,7 @@
- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "qa-issuable-milestone-dropdown js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "qa-issuable-dropdown-menu-milestone dropdown-menu-selectable dropdown-menu-milestone",
- placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: "Milestone" } }) do
- if project
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 3f3b9146e71..cd7d792738d 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -165,7 +165,7 @@
= render_if_exists 'shared/issuable/filter_epic', type: type
%button.clear-search.hidden{ type: 'button' }
- = icon('times')
+ = sprite_icon('close', size: 16, css_class: 'clear-search-icon')
.filter-dropdown-container.d-flex.flex-column.flex-md-row
- if type == :boards
#js-board-labels-toggle
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 6f31d7290b7..620e9b5ea31 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -4,6 +4,7 @@
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
+- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
- if Feature.enabled?(:vue_issuable_sidebar, @project.group)
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
@@ -29,47 +30,50 @@
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
- - milestone = issuable_sidebar[:milestone] || {}
- .block.milestone{ data: { qa_selector: 'milestone_block' } }
- .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
- = sprite_icon('clock')
- %span.milestone-title.collapse-truncated-title
+ - if issuable_sidebar[:supports_milestone]
+ - milestone = issuable_sidebar[:milestone] || {}
+ .block.milestone{ data: { qa_selector: 'milestone_block' } }
+ .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ = sprite_icon('clock')
+ %span.milestone-title.collapse-truncated-title
+ - if milestone.present?
+ = milestone[:title]
+ - else
+ = _('None')
+ .title.hide-collapsed
+ = _('Milestone')
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
+ - if can_edit_issuable
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_milestone_link", track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
+ .value.hide-collapsed
- if milestone.present?
- = milestone[:title]
+ - milestone_title = milestone[:expired] ? _("%{milestone_name} (Past due)").html_safe % { milestone_name: milestone[:title] } : milestone[:title]
+ = link_to milestone_title, milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] }
- else
- = _('None')
- .title.hide-collapsed
- = _('Milestone')
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- - if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_milestone_link", track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
- .value.hide-collapsed
- - if milestone.present?
- = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] }
- - else
- %span.no-value
- = _('None')
-
- .selectbox.hide-collapsed
- = f.hidden_field 'milestone_id', value: milestone[:id], id: nil
- = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
- - if @project.group.present?
- = render_if_exists 'shared/issuable/iteration_select', { can_edit: can_edit_issuable, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type }
- #issuable-time-tracker.block
- // Fallback while content is loading
- .title.hide-collapsed
- = _('Time tracking')
- = icon('spinner spin', 'aria-hidden': 'true')
-
+ %span.no-value
+ = _('None')
+
+ .selectbox.hide-collapsed
+ = f.hidden_field 'milestone_id', value: milestone[:id], id: nil
+ = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
+ - if @project.group.present?
+ = render_if_exists 'shared/issuable/iteration_select', { can_edit: can_edit_issuable, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type }
+
+ - if issuable_sidebar[:supports_time_tracking]
+ #issuable-time-tracker.block
+ // Fallback while content is loading
+ .title.hide-collapsed
+ = _('Time tracking')
+ = loading_icon
- if issuable_sidebar.has_key?(:due_date)
.block.due_date
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
- = icon('calendar', 'aria-hidden': 'true')
+ = sprite_icon('calendar')
%span.js-due-date-sidebar-value
= issuable_sidebar[:due_date].try(:to_s, :medium) || _('None')
.title.hide-collapsed
= _('Due date')
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
@@ -97,48 +101,63 @@
= dropdown_content do
.js-due-date-calendar
- - selected_labels = issuable_sidebar[:labels]
- .block.labels
- .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
- = icon('tags', 'aria-hidden': 'true')
- %span
- = selected_labels.size
- .title.hide-collapsed
- = _('Labels')
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- - if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_labels_link", track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
- .value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?), data: { qa_selector: 'labels_block' } }
- - if selected_labels.any?
- - selected_labels.each do |label_hash|
- = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]), dataset: { qa_selector: 'label', qa_label_name: label_hash[:title] })
- - else
- %span.no-value
- = _('None')
- .selectbox.hide-collapsed
- - selected_labels.each do |label|
- = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
- .dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) }
- %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
- = multi_label_name(selected_labels, "Labels")
- = icon('chevron-down', 'aria-hidden': 'true')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
- = render partial: "shared/issuable/label_page_default"
- - if issuable_sidebar.dig(:current_user, :can_admin_label)
- = render partial: "shared/issuable/label_page_create"
+
+ - if Feature.enabled?(:vue_sidebar_labels, @project)
+ .js-sidebar-labels{ data: { allow_label_create: issuable_sidebar.dig(:current_user, :can_admin_label).to_s,
+ allow_scoped_labels: issuable_sidebar[:scoped_labels_available].to_s,
+ can_edit: can_edit_issuable.to_s,
+ iid: issuable_sidebar[:iid],
+ issuable_type: issuable_type,
+ labels_fetch_path: issuable_sidebar[:project_labels_path],
+ labels_manage_path: project_labels_path(@project),
+ labels_update_path: issuable_sidebar[:issuable_json_path],
+ project_issues_path: issuable_sidebar[:project_issuables_path],
+ project_path: @project.full_path,
+ selected_labels: issuable_sidebar[:labels].to_json } }
+ - else
+ - selected_labels = issuable_sidebar[:labels]
+ .block.labels
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
+ = sprite_icon('labels')
+ %span
+ = selected_labels.size
+ .title.hide-collapsed
+ = _('Labels')
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
+ - if can_edit_issuable
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_labels_link", track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
+ .value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?), data: { qa_selector: 'labels_block' } }
+ - if selected_labels.any?
+ - selected_labels.each do |label_hash|
+ = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]), dataset: { qa_selector: 'label', qa_label_name: label_hash[:title] })
+ - else
+ %span.no-value
+ = _('None')
+ .selectbox.hide-collapsed
+ - selected_labels.each do |label|
+ = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) }
+ %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
+ = multi_label_name(selected_labels, "Labels")
+ = icon('chevron-down', 'aria-hidden': 'true')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
+ = render partial: "shared/issuable/label_page_default"
+ - if issuable_sidebar.dig(:current_user, :can_admin_label)
+ = render partial: "shared/issuable/label_page_create"
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
+ - if issuable_sidebar[:supports_severity]
+ #js-severity
+
- if issuable_sidebar.dig(:features_available, :health_status)
.js-sidebar-status-entry-point
- if issuable_sidebar.has_key?(:confidential)
- -# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
- -# haml-lint:disable InlineJavaScript
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
@@ -148,15 +167,24 @@
.js-sidebar-subscriptions-entry-point
- project_ref = issuable_sidebar[:reference]
- .block.project-reference
- .sidebar-collapsed-icon.dont-change-state
- = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
- .cross-project-reference.hide-collapsed
- %span
- = _('Reference:')
- %cite{ title: project_ref }
- = project_ref
- = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
+ .block.with-sub-blocks
+ .project-reference.sub-block
+ .sidebar-collapsed-icon.dont-change-state
+ = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
+ .cross-project-reference.hide-collapsed
+ %span
+ = _('Reference:')
+ %cite{ title: project_ref }
+ = project_ref
+ = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
+ - if issuable_type == 'merge_request'
+ .sidebar-source-branch.sub-block
+ .sidebar-collapsed-icon.dont-change-state
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport')
+ .sidebar-mr-source-branch.hide-collapsed
+ %span
+ = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<cite class='ref-name' title='#{source_branch}'>".html_safe, source_branch_close: "</cite>".html_safe, source_branch: source_branch }
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport')
- if issuable_sidebar.dig(:current_user, :can_move)
.block.js-sidebar-move-issue-block
@@ -174,7 +202,7 @@
= dropdown_footer add_content_class: true do
%button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true }
= _('Move')
- = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom sidebar-move-issue-confirmation-loading-icon')
-# haml-lint:disable InlineJavaScript
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index cf239a5d04c..175713751ef 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -4,7 +4,7 @@
#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
- .spinner.spinner-sm.align-bottom
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom')
.selectbox.hide-collapsed
- if assignees.none?
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
index de4df016cfb..7b5926fc186 100644
--- a/app/views/shared/issuable/_sidebar_todo.html.haml
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -12,4 +12,4 @@
data: todo_button_data }
%span.issuable-todo-inner.js-issuable-todo-inner<
= is_collapsed ? button_icon : button_title
- = icon('spin spinner', 'aria-hidden': 'true')
+ = loading_icon
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index 30a1f0febc3..94fa43746e2 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -6,7 +6,7 @@
- source_title, target_title = format_mr_branch_names(@merge_request)
-.form-group.row.d-flex.gl-pl-3-deprecated-no-really-do-not-use-me.gl-pr-3-deprecated-no-really-do-not-use-me.branch-selector
+.form-group.row.d-flex.gl-px-5.branch-selector
.align-self-center
%span
= html_escape(_('From %{code_open}%{source_title}%{code_close} into')) % { source_title: source_title, code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml
deleted file mode 100644
index 0d13fccaf3e..00000000000
--- a/app/views/shared/issuable/form/_issue_assignee.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-- issue = issuable
-- assignees = issue.assignees
-.block.assignee
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee_list) }
- - if assignees.any?
- - assignees.each do |assignee|
- = link_to_member(@project, assignee, size: 24)
- - else
- = icon('user', 'aria-hidden': 'true')
- .title.hide-collapsed
- Assignee
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- - if can_edit_issuable
- = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
- .value.hide-collapsed
- - if assignees.any?
- - assignees.each do |assignee|
- = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
- %span.username
- = assignee.to_reference
- - else
- %span.assign-yourself.no-value
- No assignee
- - if can_edit_issuable
- \-
- %a.js-assign-yourself{ href: '#' }
- assign yourself
-
- .selectbox.hide-collapsed
- = f.hidden_field 'assignee_ids', value: issuable.assignee_ids, id: 'issue_assignee_ids'
- = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index 5c5c8c816d3..e29627304b4 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -25,7 +25,7 @@
= check_box_tag 'merge_request[squash]', '1', issuable_squash_option?(issuable, project), class: 'form-check-input'
= label_tag 'merge_request[squash]', class: 'form-check-label' do
Squash commits when merge request is accepted.
- = link_to icon('question-circle'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank'
- if project.squash_always?
.gl-text-gray-400
= _('Required in this project.')
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 1389bc2ab4d..459eb112e4f 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -12,13 +12,19 @@
.form-group.row.merge-request-assignee
= render "shared/issuable/form/metadata_issuable_assignee", issuable: issuable, form: form, has_due_date: has_due_date
+ - if issuable.allows_reviewers?
+ .form-group.row.merge-request-reviewer
+ = render "shared/issuable/form/metadata_issuable_reviewer", issuable: issuable, form: form, has_due_date: has_due_date
+
= render_if_exists "shared/issuable/form/epic", issuable: issuable, form: form, project: project
- .form-group.row.issue-milestone
- = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
- .col-sm-10{ class: ("col-md-8" if has_due_date) }
- .issuable-form-select-holder
- = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ - if issuable.supports_milestone?
+ .form-group.row.issue-milestone
+ = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
+ .col-sm-10{ class: ("col-md-8" if has_due_date) }
+ .issuable-form-select-holder
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+
.form-group.row
= form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
= form.hidden_field :label_ids, multiple: true, value: ''
@@ -28,7 +34,7 @@
= render_if_exists "shared/issuable/form/merge_request_blocks", issuable: issuable, form: form
- - if has_due_date || issuable.supports_weight?
+ - if has_due_date
.col-lg-6
= render_if_exists "shared/issuable/form/weight", issuable: issuable, form: form
.form-group.row
diff --git a/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml b/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
new file mode 100644
index 00000000000..a8b033bba36
--- /dev/null
+++ b/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
@@ -0,0 +1,10 @@
+= form.label :reviewer_id, "Reviewer", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
+.col-sm-10{ class: ("col-md-8" if has_due_date) }
+ .issuable-form-select-holder.selectbox
+ - issuable.reviewers.each do |reviewer|
+ = hidden_field_tag "#{issuable.to_ability_name}[reviewer_ids][]", reviewer.id, id: nil, data: { meta: reviewer.name, avatar_url: reviewer.avatar_url, name: reviewer.name, username: reviewer.username }
+
+ - if issuable.reviewers.empty?
+ = hidden_field_tag "#{issuable.to_ability_name}[reviewer_ids][]", 0, id: nil, data: { meta: '' }
+
+ = dropdown_tag(users_dropdown_label(issuable.reviewers), options: reviewers_dropdown_options(issuable.to_ability_name))
diff --git a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
deleted file mode 100644
index 60c34094108..00000000000
--- a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}"
-.col-sm-10{ class: ("col-lg-8" if has_due_date) }
- .issuable-form-select-holder
- = form.hidden_field :assignee_id
-
- = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
- = link_to 'Assign to me', '#', class: "assign-to-me-link qa-assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml
new file mode 100644
index 00000000000..7a8120d2d02
--- /dev/null
+++ b/app/views/shared/issuable/form/_type_selector.html.haml
@@ -0,0 +1,30 @@
+- return unless issuable.supports_issue_type? && can?(current_user, :admin_issue, @project)
+
+.form-group.row.gl-mb-0
+ = form.label :type, 'Type', class: 'col-form-label col-sm-2'
+ .col-sm-10
+ .issuable-form-select-holder.selectbox.form-group.gl-mb-0
+ .dropdown.js-issuable-type-filter-dropdown-wrap
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %span.dropdown-toggle-text.is-default
+ = issuable.issue_type.capitalize || _("Select type")
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-menu-selectable.dropdown-select
+ .dropdown-title.gl-display-flex
+ %span.gl-ml-auto
+ = _("Select type")
+ %button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ "aria-label" => _('Close') }
+ = sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
+ .dropdown-content
+ %ul
+ %li.js-filter-issuable-type
+ = link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
+ = _("Issue")
+ %li.js-filter-issuable-type
+ = link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
+ = _("Incident")
+ - if issuable.incident?
+ %p.form-text.text-muted
+ - incident_docs_url = help_page_path('operations/incident_management/incidents.md', anchor: 'create-and-manage-incidents-in-gitlab')
+ - incident_docs_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: incident_docs_url }
+ = _('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.').html_safe % { incident_docs_start: incident_docs_start, incident_docs_end: '</a>'.html_safe }
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index f59e0f92c60..8e5763842d9 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -38,7 +38,7 @@
data: { id: role_id, el_id: dom_id }
.clearable-input.member-form-control.d-sm-inline-block
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member
- %i.clear-icon.js-clear-input
+ = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input gl-text-gray-200')
- if can_admin_member
= link_to group_link_path,
method: :delete,
diff --git a/app/views/shared/members/_invite_group.html.haml b/app/views/shared/members/_invite_group.html.haml
index a2fb33aa757..a87a4c6a45c 100644
--- a/app/views/shared/members/_invite_group.html.haml
+++ b/app/views/shared/members/_invite_group.html.haml
@@ -22,5 +22,5 @@
= label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: _('Expiration date'), id: 'expires_at_groups'
- %i.clear-icon.js-clear-input
+ = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input gl-text-gray-200')
= submit_tag _("Invite"), class: "btn btn-success", data: { qa_selector: 'invite_group_button' }
diff --git a/app/views/shared/members/_invite_member.html.haml b/app/views/shared/members/_invite_member.html.haml
index 284d7fdb6da..5f9046b3dcb 100644
--- a/app/views/shared/members/_invite_member.html.haml
+++ b/app/views/shared/members/_invite_member.html.haml
@@ -19,10 +19,10 @@
- link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path }
= _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.form-group
+ = label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
.clearable-input
- = label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
- %i.clear-icon.js-clear-input
+ = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input gl-text-gray-200')
= submit_tag _("Invite"), class: "btn btn-success", data: { qa_selector: 'invite_member_button' }
- if can_import_members
= link_to _("Import"), import_path, class: "btn btn-default", title: _("Import members from another project")
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 20473b47484..7573c2f6d56 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -33,7 +33,7 @@
- if source.instance_of?(Group) && source != @group
&middot;
- = link_to source.full_name, source, class: "member-group-link"
+ = link_to source.full_name, source, class: "gl-display-inline-block inline-link"
.cgray
- if member.request?
@@ -97,7 +97,7 @@
placeholder: _('Expiration date'),
id: "member_expires_at_#{member.id}",
data: { el_id: dom_id(member) }
- %i.clear-icon.js-clear-input
+ = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input gl-text-gray-200')
- else
%span.member-access-text.user-access-role= member.human_access
diff --git a/app/views/shared/milestones/_description.html.haml b/app/views/shared/milestones/_description.html.haml
index 76d6c765ed6..747e22f47ac 100644
--- a/app/views/shared/milestones/_description.html.haml
+++ b/app/views/shared/milestones/_description.html.haml
@@ -1,9 +1,8 @@
.detail-page-description.milestone-detail
- %h2{ data: { qa_selector: "milestone_title_content" } }
- .title
+ %h2.gl-m-0{ data: { qa_selector: "milestone_title_content" } }
= markdown_field(milestone, :title)
- if milestone.try(:description).present?
%div{ data: { qa_selector: "milestone_description_content" } }
- .description.md
+ .description.md.gl-px-0.gl-pt-4.gl-border-1.gl-border-t-solid.gl-border-gray-100
= markdown_field(milestone, :description)
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 4ef8a9dd842..27b771b281b 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -8,7 +8,7 @@
.gl-mb-2
%strong{ data: { qa_selector: "milestone_link", qa_milestone_title: milestone.title } }
= link_to truncate(milestone.title, length: 100), milestone_path(milestone)
- - if @group
+ - if @group || dashboard
= " - #{milestone_type}"
- if milestone.due_date || milestone.start_date
@@ -62,7 +62,3 @@
= link_to s_('Milestones|Reopen Milestone'), milestone_path(milestone, milestone: { state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else
= link_to s_('Milestones|Close Milestone'), milestone_path(milestone, milestone: { state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
-
- - if dashboard
- .label-badge.label-badge-gray
- = milestone_type
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 51c1ee0c4d1..3703cca2290 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -27,7 +27,7 @@
or
%button.attach-new-file.markdown-selector{ type: 'button' }= _("attach a new file")
- %button.btn.markdown-selector.button-attach-file.btn-link{ type: 'button', tabindex: '-1' }
+ %button.btn.markdown-selector.button-attach-file.btn-link{ type: 'button' }
= sprite_icon('media')
%span.text-attach-file<>
= _("Attach a file")
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index da665f17975..97ed2852871 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -76,4 +76,4 @@
= note.attachment_identifier
= link_to delete_attachment_project_note_path(note.project, note),
title: _('Delete this attachment'), method: :delete, remote: true, data: { confirm: _('Are you sure you want to remove the attachment?') }, class: 'danger js-note-attachment-delete' do
- = icon('trash-o', class: 'cred')
+ = sprite_icon('remove', css_class: 'cred')
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 2e98b06ec4a..5b7a0b99598 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -28,5 +28,4 @@
= sprite_icon('lock', css_class: 'icon')
%span
= html_escape(_("This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment.")) % { issuable: issuable.class.to_s.titleize.downcase, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
--# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/shared/promotions/_promote_servicedesk.html.haml b/app/views/shared/promotions/_promote_servicedesk.html.haml
index fbac5ef0bbd..237416a869b 100644
--- a/app/views/shared/promotions/_promote_servicedesk.html.haml
+++ b/app/views/shared/promotions/_promote_servicedesk.html.haml
@@ -1,7 +1,7 @@
.user-callout.promotion-callout.js-service-desk-callout#promote_service_desk{ data: { uid: 'promote_service_desk_dismissed' } }
.bordered-box.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss Service Desk promotion' }
- = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
+ = sprite_icon('close', size: 16, css_class: 'dismiss-icon')
.svg-container
= custom_icon('icon_service_desk')
.user-callout-copy
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index 8a78f12bdd8..1af04b808bf 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -1,71 +1,71 @@
- page_title "#{@runner.description} ##{@runner.id}", _("Runners")
%h3.page-title
- Runner ##{@runner.id}
+ = s_('Runners|Runner #%{id}' % { id: @runner.id })
.float-right
- if @runner.instance_type?
%span.runner-state.runner-state-shared
- Shared
+ = s_('Runners|Shared')
- elsif @runner.group_type?
%span.runner-state.runner-state-shared
- Group
+ = s_('Runners|Group')
- else
%span.runner-state.runner-state-specific
- Specific
+ = s_('Runners|Specific')
.table-holder
%table.table
%thead
%tr
- %th Property Name
- %th Value
+ %th= s_('Runners|Property Name')
+ %th= s_('Runners|Value')
%tr
- %td Active
- %td= @runner.active? ? 'Yes' : 'No'
+ %td= s_('Runners|Active')
+ %td= @runner.active? ? _('Yes') : _('No')
%tr
- %td Protected
- %td= @runner.ref_protected? ? 'Yes' : 'No'
+ %td= s_('Runners|Protected')
+ %td= @runner.ref_protected? ? _('Yes') : _('No')
%tr
- %td Can run untagged jobs
- %td= @runner.run_untagged? ? 'Yes' : 'No'
+ %td= s_('Runners|Can run untagged jobs')
+ %td= @runner.run_untagged? ? _('Yes') : _('No')
- unless @runner.group_type?
%tr
- %td Locked to this project
- %td= @runner.locked? ? 'Yes' : 'No'
+ %td= s_('Runners|Locked to this project')
+ %td= @runner.locked? ? _('Yes') : _('No')
%tr
- %td Tags
+ %td= s_('Runners|Tags')
%td
- @runner.tag_list.sort.each do |tag|
%span.badge.badge-primary
= tag
%tr
- %td Name
+ %td= s_('Runners|Name')
%td= @runner.name
%tr
- %td Version
+ %td= s_('Runners|Version')
%td= @runner.version
%tr
- %td IP Address
+ %td= s_('Runners|IP Address')
%td= @runner.ip_address
%tr
- %td Revision
+ %td= s_('Runners|Revision')
%td= @runner.revision
%tr
- %td Platform
+ %td= s_('Runners|Platform')
%td= @runner.platform
%tr
- %td Architecture
+ %td= s_('Runners|Architecture')
%td= @runner.architecture
%tr
- %td Description
+ %td= s_('Runners|Description')
%td= @runner.description
%tr
- %td Maximum job timeout
+ %td= s_('Runners|Maximum job timeout')
%td= @runner.maximum_timeout_human_readable
%tr
- %td Last contact
+ %td= s_('Runners|Last contact')
%td
- if @runner.contacted_at
= time_ago_with_tooltip @runner.contacted_at
- else
- Never
+ = s_('Never')
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 81277b50d13..198735df5ee 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,5 +1,6 @@
-- if Feature.enabled?(:snippets_edit_vue)
- #js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access") } }
+- if Feature.enabled?(:snippets_edit_vue, default_enabled: true)
+ - available_visibility_levels = available_visibility_levels(@snippet)
+ #js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access"), 'visibility_levels': available_visibility_levels, 'selected_level': snippets_selected_visibility_level(available_visibility_levels, @snippet.visibility_level), 'multiple_levels_restricted': multiple_visibility_levels_restricted? } }
- else
.snippet-form-holder
= form_for @snippet, url: url,
@@ -36,7 +37,7 @@
.form-group
.font-weight-bold
= _('Visibility level')
- = link_to icon('question-circle'), help_page_path("public_access/public_access"), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('public_access/public_access'), target: '_blank'
= render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet, with_label: false
- if params[:files]
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index d6019e45b25..a9226117727 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -5,10 +5,7 @@
= visibility_level_label(@snippet.visibility_level)
= visibility_level_icon(@snippet.visibility_level)
%span.creator
- Authored
- = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "d-none d-sm-inline")}
- = user_status(@snippet.author)
+ = s_('Snippets|Authored %{time_ago} by %{author}').html_safe % { time_ago: time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago'), author: link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "d-none d-sm-inline") + user_status(@snippet.author) }
.detail-page-header-actions
- if @snippet.project_id?
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 0f6188fa334..96da5136908 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -1,77 +1,77 @@
= form_errors(hook)
.form-group
- = form.label :url, 'URL', class: 'label-bold'
+ = form.label :url, s_('Webhooks|URL'), class: 'label-bold'
= form.text_field :url, class: 'form-control', placeholder: 'http://example.com/trigger-ci.json'
.form-group
- = form.label :token, 'Secret Token', class: 'label-bold'
+ = form.label :token, s_('Webhooks|Secret Token'), class: 'label-bold'
= form.text_field :token, class: 'form-control', placeholder: ''
%p.form-text.text-muted
- Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
+ = s_('Webhooks|Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.')
.form-group
- = form.label :url, 'Trigger', class: 'label-bold'
+ = form.label :url, s_('Webhooks|Trigger'), class: 'label-bold'
%ul.list-unstyled.prepend-left-20
%li
= form.check_box :push_events, class: 'form-check-input'
= form.label :push_events, class: 'list-label form-check-label ml-1' do
- %strong Push events
+ %strong= s_('Webhooks|Push events')
= form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)'
%p.text-muted.ml-1
- This URL will be triggered by a push to the repository
+ = s_('Webhooks|This URL will be triggered by a push to the repository')
%li
= form.check_box :tag_push_events, class: 'form-check-input'
= form.label :tag_push_events, class: 'list-label form-check-label ml-1' do
- %strong Tag push events
+ %strong= s_('Webhooks|Tag push events')
%p.text-muted.ml-1
- This URL will be triggered when a new tag is pushed to the repository
+ = s_('Webhooks|This URL will be triggered when a new tag is pushed to the repository')
%li
= form.check_box :note_events, class: 'form-check-input'
= form.label :note_events, class: 'list-label form-check-label ml-1' do
- %strong Comments
+ %strong= s_('Webhooks|Comments')
%p.text-muted.ml-1
- This URL will be triggered when someone adds a comment
+ = s_('Webhooks|This URL will be triggered when someone adds a comment')
%li
= form.check_box :confidential_note_events, class: 'form-check-input'
= form.label :confidential_note_events, class: 'list-label form-check-label ml-1' do
- %strong Confidential Comments
+ %strong= s_('Webhooks|Confidential Comments')
%p.text-muted.ml-1
- This URL will be triggered when someone adds a comment on a confidential issue
+ = s_('Webhooks|This URL will be triggered when someone adds a comment on a confidential issue')
%li
= form.check_box :issues_events, class: 'form-check-input'
= form.label :issues_events, class: 'list-label form-check-label ml-1' do
- %strong Issues events
+ %strong= s_('Webhooks|Issues events')
%p.text-muted.ml-1
- This URL will be triggered when an issue is created/updated/merged
+ = s_('Webhooks|This URL will be triggered when an issue is created/updated/merged')
%li
= form.check_box :confidential_issues_events, class: 'form-check-input'
= form.label :confidential_issues_events, class: 'list-label form-check-label ml-1' do
- %strong Confidential Issues events
+ %strong= s_('Webhooks|Confidential Issues events')
%p.text-muted.ml-1
- This URL will be triggered when a confidential issue is created/updated/merged
+ = s_('Webhooks|This URL will be triggered when a confidential issue is created/updated/merged')
%li
= form.check_box :merge_requests_events, class: 'form-check-input'
= form.label :merge_requests_events, class: 'list-label form-check-label ml-1' do
- %strong Merge request events
+ %strong= s_('Webhooks|Merge request events')
%p.text-muted.ml-1
- This URL will be triggered when a merge request is created/updated/merged
+ = s_('Webhooks|This URL will be triggered when a merge request is created/updated/merged')
%li
= form.check_box :job_events, class: 'form-check-input'
= form.label :job_events, class: 'list-label form-check-label ml-1' do
- %strong Job events
+ %strong= s_('Webhooks|Job events')
%p.text-muted.ml-1
- This URL will be triggered when the job status changes
+ = s_('Webhooks|This URL will be triggered when the job status changes')
%li
= form.check_box :pipeline_events, class: 'form-check-input'
= form.label :pipeline_events, class: 'list-label form-check-label ml-1' do
- %strong Pipeline events
+ %strong= s_('Webhooks|Pipeline events')
%p.text-muted.ml-1
- This URL will be triggered when the pipeline status changes
+ = s_('Webhooks|This URL will be triggered when the pipeline status changes')
%li
= form.check_box :wiki_page_events, class: 'form-check-input'
= form.label :wiki_page_events, class: 'list-label form-check-label ml-1' do
- %strong Wiki Page events
+ %strong= s_('Webhooks|Wiki Page events')
%p.text-muted.ml-1
- This URL will be triggered when a wiki page is created/updated
+ = s_('Webhooks|This URL will be triggered when a wiki page is created/updated')
%li
= form.check_box :deployment_events, class: 'form-check-input'
= form.label :deployment_events, class: 'list-label form-check-label ml-1' do
@@ -79,8 +79,8 @@
%p.text-muted.ml-1
= s_('Webhooks|This URL will be triggered when a deployment is finished/failed/canceled')
.form-group
- = form.label :enable_ssl_verification, 'SSL verification', class: 'label-bold checkbox'
+ = form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox'
.form-check
= form.check_box :enable_ssl_verification, class: 'form-check-input'
= form.label :enable_ssl_verification, class: 'form-check-label ml-1' do
- %strong Enable SSL verification
+ %strong= s_('Webhooks|Enable SSL verification')
diff --git a/app/views/shared/wikis/_form.html.haml b/app/views/shared/wikis/_form.html.haml
index 4d64521f9b0..66c0f64c32c 100644
--- a/app/views/shared/wikis/_form.html.haml
+++ b/app/views/shared/wikis/_form.html.haml
@@ -21,10 +21,10 @@
.col-sm-12
= f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: s_('Wiki|Page title')
%span.d-inline-block.mw-100.gl-mt-2
- = icon('lightbulb-o')
+ = sprite_icon('bulb', size: 12, css_class: 'gl-mr-n1')
- if @page.persisted?
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
- = link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'),
+ = link_to sprite_icon('question-o'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'),
target: '_blank', rel: 'noopener noreferrer'
- else
= s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
index cc8bdbae55d..5fef56f7fc3 100644
--- a/app/views/sherlock/file_samples/show.html.haml
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -6,7 +6,7 @@
.row-content-block
.float-right
= link_to(sherlock_transaction_path(@transaction), class: 'btn') do
- %i.fa.fa-arrow-left
+ = sprite_icon('arrow-left')
= t('sherlock.transaction')
.oneline
= t('sherlock.file_sample')
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index 413130a2907..e4a48943115 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -12,7 +12,7 @@
.row-content-block
.float-right
= link_to(sherlock_transaction_path(@transaction), class: 'btn') do
- %i.fa.fa-arrow-left
+ = sprite_icon('arrow-left')
= t('sherlock.transaction')
.oneline
= t('sherlock.query')
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
index 4d9df01ae31..1e16c88571e 100644
--- a/app/views/sherlock/transactions/index.html.haml
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -6,7 +6,7 @@
= link_to(destroy_all_sherlock_transactions_path,
class: 'btn btn-danger',
method: :delete) do
- %i.fa.fa-trash
+ = sprite_icon('remove')
= t('sherlock.delete_all_transactions')
.oneline= t('sherlock.introduction')
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
index 565b337d446..8f2b36123bb 100644
--- a/app/views/sherlock/transactions/show.html.haml
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -19,7 +19,7 @@
.row-content-block
.float-right
= link_to(sherlock_transactions_path, class: 'btn') do
- %i.fa.fa-arrow-left
+ = sprite_icon('arrow-left')
= t('sherlock.all_transactions')
.oneline
= t('sherlock.transaction')
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
deleted file mode 100644
index a83b55379da..00000000000
--- a/app/views/u2f/_register.html.haml
+++ /dev/null
@@ -1,40 +0,0 @@
-#js-register-u2f
-
--# haml-lint:disable InlineJavaScript
-%script#js-register-u2f-not-supported{ type: "text/template" }
- %p= _("Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).")
-
-%script#js-register-u2f-setup{ type: "text/template" }
- - if current_user.two_factor_otp_enabled?
- .row.gl-mb-3
- .col-md-4
- %button#js-setup-u2f-device.btn.btn-info.btn-block= _("Set up new U2F device")
- .col-md-8
- %p= _("Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.")
- - else
- .row.gl-mb-3
- .col-md-4
- %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true }= _("Set up new U2F device")
- .col-md-8
- %p= _("You need to register a two-factor authentication app before you can set up a U2F device.")
-
-%script#js-register-u2f-in-progress{ type: "text/template" }
- %p= _("Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.")
-
-%script#js-register-u2f-error{ type: "text/template" }
- %div
- %p
- %span <%= error_message %> (#{_("error code:")} <%= error_code %>)
- %a.btn.btn-warning#js-token-2fa-try-again= _("Try again?")
-
-%script#js-register-u2f-registered{ type: "text/template" }
- .row.gl-mb-3
- .col-md-12
- %p= _("Your device was successfully set up! Give it a name and register it with the GitLab server.")
- = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
- .row.gl-mb-3
- .col-md-3
- = text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: _("Pick a name")
- .col-md-3
- = hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
- = submit_tag _("Register U2F device"), class: "btn btn-success"
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index e1d1df9de1a..fbda9b79e82 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -13,24 +13,24 @@
.cover-block.user-cover-block{ class: [('border-bottom' if profile_tabs.empty?)] }
= render layout: 'users/cover_controls' do
- if @user == current_user
- = link_to profile_path, class: link_classes + 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
+ = link_to profile_path, class: link_classes + 'btn gl-button btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
= sprite_icon('pencil')
- elsif current_user
- if @user.abuse_report
- %button{ class: link_classes + 'btn btn-danger mr-1', title: s_('UserProfile|Already reported for abuse'),
- data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
- = icon('exclamation-circle')
+ %button{ class: link_classes + 'btn gl-button btn-danger', title: s_('UserProfile|Already reported for abuse'),
+ data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }>
+ = sprite_icon('error')
- else
- = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: link_classes + 'btn',
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: link_classes + 'btn gl-button',
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('exclamation-circle')
+ = sprite_icon('error')
- if can?(current_user, :read_user_profile, @user)
- = link_to user_path(@user, rss_url_options), class: link_classes + 'btn btn-svg btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
+ = link_to user_path(@user, rss_url_options), class: link_classes + 'btn gl-button btn-svg btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
= sprite_icon('rss', css_class: 'qa-rss-icon')
- if current_user && current_user.admin?
- = link_to [:admin, @user], class: link_classes + 'btn btn-default', title: s_('UserProfile|View user in admin area'),
+ = link_to [:admin, @user], class: link_classes + 'btn gl-button btn-default', title: s_('UserProfile|View user in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('users')
+ = sprite_icon('user')
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] }
.avatar-holder
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 2c871c55f0a..451decce9fb 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -115,6 +115,14 @@
:weight: 1
:idempotent:
:tags: []
+- :name: cronjob:analytics_instance_statistics_count_job_trigger
+ :feature_category: :instance_statistics
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:authorized_project_update_periodic_recalculate
:feature_category: :source_code_management
:has_external_dependencies:
@@ -131,6 +139,14 @@
:weight: 1
:idempotent:
:tags: []
+- :name: cronjob:ci_platform_metrics_update_cron
+ :feature_category: :continuous_integration
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :cpu
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: cronjob:container_expiration_policy
:feature_category: :container_registry
:has_external_dependencies:
@@ -723,6 +739,22 @@
:weight: 2
:idempotent:
:tags: []
+- :name: jira_connect:jira_connect_sync_branch
+ :feature_category: :integrations
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
+- :name: jira_connect:jira_connect_sync_merge_request
+ :feature_category: :integrations
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: jira_importer:jira_import_advance_stage
:feature_category: :importers
:has_external_dependencies:
@@ -881,7 +913,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: true
:tags: []
- :name: pipeline_background:ci_daily_build_group_report_results
:feature_category: :continuous_integration
@@ -899,6 +931,14 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: pipeline_background:ci_pipelines_create_artifact
+ :feature_category: :continuous_integration
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: pipeline_background:ci_ref_delete_unlock_artifacts
:feature_category: :continuous_integration
:has_external_dependencies:
@@ -987,14 +1027,6 @@
:weight: 3
:idempotent:
:tags: []
-- :name: pipeline_default:pipeline_update_ci_ref_status
- :feature_category: :continuous_integration
- :has_external_dependencies:
- :urgency: :high
- :resource_boundary: :cpu
- :weight: 3
- :idempotent:
- :tags: []
- :name: pipeline_hooks:build_hooks
:feature_category: :continuous_integration
:has_external_dependencies:
@@ -1180,6 +1212,14 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: analytics_instance_statistics_counter_job
+ :feature_category: :instance_statistics
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: authorized_keys
:feature_category: :source_code_management
:has_external_dependencies:
@@ -1350,7 +1390,7 @@
:tags: []
- :name: flush_counter_increments
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1428,6 +1468,22 @@
:weight: 1
:idempotent:
:tags: []
+- :name: issue_placement
+ :feature_category: :issue_tracking
+ :has_external_dependencies:
+ :urgency: :high
+ :resource_boundary: :cpu
+ :weight: 2
+ :idempotent: true
+ :tags: []
+- :name: issue_rebalancing
+ :feature_category: :issue_tracking
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: mailers
:feature_category:
:has_external_dependencies:
@@ -1442,7 +1498,15 @@
:urgency: :high
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent:
+ :tags: []
+- :name: merge_request_cleanup_refs
+ :feature_category: :source_code_management
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
:tags: []
- :name: merge_request_mergeability_check
:feature_category: :source_code_management
@@ -1524,6 +1588,22 @@
:weight: 1
:idempotent:
:tags: []
+- :name: pages_remove
+ :feature_category: :pages
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
+- :name: pages_transfer
+ :feature_category: :pages
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: pages_update_configuration
:feature_category: :pages
:has_external_dependencies:
diff --git a/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb b/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
new file mode 100644
index 00000000000..a9976c6e5cb
--- /dev/null
+++ b/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Analytics
+ module InstanceStatistics
+ class CountJobTriggerWorker
+ include ApplicationWorker
+ include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+
+ DEFAULT_DELAY = 3.minutes.freeze
+
+ feature_category :instance_statistics
+ urgency :low
+
+ idempotent!
+
+ def perform
+ return if Feature.disabled?(:store_instance_statistics_measurements, default_enabled: true)
+
+ recorded_at = Time.zone.now
+ measurement_identifiers = Analytics::InstanceStatistics::Measurement.identifiers
+
+ worker_arguments = Gitlab::Analytics::InstanceStatistics::WorkersArgumentBuilder.new(
+ measurement_identifiers: measurement_identifiers.values,
+ recorded_at: recorded_at
+ ).execute
+
+ perform_in = DEFAULT_DELAY.minutes.from_now
+ worker_arguments.each do |args|
+ CounterJobWorker.perform_in(perform_in, *args)
+
+ perform_in += DEFAULT_DELAY
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/analytics/instance_statistics/counter_job_worker.rb b/app/workers/analytics/instance_statistics/counter_job_worker.rb
new file mode 100644
index 00000000000..062b5ccc207
--- /dev/null
+++ b/app/workers/analytics/instance_statistics/counter_job_worker.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Analytics
+ module InstanceStatistics
+ class CounterJobWorker
+ include ApplicationWorker
+
+ feature_category :instance_statistics
+ urgency :low
+
+ idempotent!
+
+ def perform(measurement_identifier, min_id, max_id, recorded_at)
+ query_scope = ::Analytics::InstanceStatistics::Measurement::IDENTIFIER_QUERY_MAPPING[measurement_identifier].call
+
+ count = if min_id.nil? || max_id.nil? # table is empty
+ 0
+ else
+ Gitlab::Database::BatchCount.batch_count(query_scope, start: min_id, finish: max_id)
+ end
+
+ return if count == Gitlab::Database::BatchCounter::FALLBACK
+
+ InstanceStatistics::Measurement.insert_all([{ recorded_at: recorded_at, count: count, identifier: measurement_identifier }])
+ end
+ end
+ end
+end
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
index fe59ba896a4..2908c7c2d0b 100644
--- a/app/workers/ci/build_trace_chunk_flush_worker.rb
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -1,14 +1,16 @@
# frozen_string_literal: true
module Ci
- class BuildTraceChunkFlushWorker # rubocop:disable Scalability/IdempotentWorker
+ class BuildTraceChunkFlushWorker
include ApplicationWorker
include PipelineBackgroundQueue
+ idempotent!
+
# rubocop: disable CodeReuse/ActiveRecord
- def perform(build_trace_chunk_id)
- ::Ci::BuildTraceChunk.find_by(id: build_trace_chunk_id).try do |build_trace_chunk|
- build_trace_chunk.persist_data!
+ def perform(chunk_id)
+ ::Ci::BuildTraceChunk.find_by(id: chunk_id).try do |chunk|
+ chunk.persist_data!
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/ci/create_cross_project_pipeline_worker.rb b/app/workers/ci/create_cross_project_pipeline_worker.rb
index 713d0092b32..679574d9f60 100644
--- a/app/workers/ci/create_cross_project_pipeline_worker.rb
+++ b/app/workers/ci/create_cross_project_pipeline_worker.rb
@@ -9,7 +9,7 @@ module Ci
def perform(bridge_id)
::Ci::Bridge.find_by_id(bridge_id).try do |bridge|
- ::Ci::CreateCrossProjectPipelineService
+ ::Ci::CreateDownstreamPipelineService
.new(bridge.project, bridge.user)
.execute(bridge)
end
diff --git a/app/workers/ci/pipelines/create_artifact_worker.rb b/app/workers/ci/pipelines/create_artifact_worker.rb
new file mode 100644
index 00000000000..220df975503
--- /dev/null
+++ b/app/workers/ci/pipelines/create_artifact_worker.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Ci
+ module Pipelines
+ class CreateArtifactWorker
+ include ApplicationWorker
+ include PipelineBackgroundQueue
+
+ idempotent!
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
+ Ci::Pipelines::CreateArtifactService.new.execute(pipeline)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
index 3b4a6fcf630..aaa77efbb74 100644
--- a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
+++ b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
@@ -10,7 +10,7 @@ module Ci
def perform(project_id, user_id, ref_path)
::Project.find_by_id(project_id).try do |project|
::User.find_by_id(user_id).try do |user|
- ::Ci::Ref.find_by_ref_path(ref_path).try do |ci_ref|
+ project.ci_refs.find_by_ref_path(ref_path).try do |ci_ref|
::Ci::UnlockArtifactsService
.new(project, user)
.execute(ci_ref)
diff --git a/app/workers/ci_platform_metrics_update_cron_worker.rb b/app/workers/ci_platform_metrics_update_cron_worker.rb
new file mode 100644
index 00000000000..ec1fc26fad3
--- /dev/null
+++ b/app/workers/ci_platform_metrics_update_cron_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CiPlatformMetricsUpdateCronWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ # This worker does not perform work scoped to a context
+ include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+
+ feature_category :continuous_integration
+ urgency :low
+ worker_resource_boundary :cpu
+
+ def perform
+ CiPlatformMetric.insert_auto_devops_platform_targets!
+ end
+end
diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb
index 22ba7c97309..482a74f49f7 100644
--- a/app/workers/concerns/new_issuable.rb
+++ b/app/workers/concerns/new_issuable.rb
@@ -27,6 +27,6 @@ module NewIssuable
# rubocop: enable CodeReuse/ActiveRecord
def log_error(record_class, record_id)
- Rails.logger.error("#{self.class}: couldn't find #{record_class} with ID=#{record_id}, skipping job") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error("#{self.class}: couldn't find #{record_class} with ID=#{record_id}, skipping job")
end
end
diff --git a/app/workers/delete_diff_files_worker.rb b/app/workers/delete_diff_files_worker.rb
index a6759a9d7c4..a31cf650b83 100644
--- a/app/workers/delete_diff_files_worker.rb
+++ b/app/workers/delete_diff_files_worker.rb
@@ -12,11 +12,11 @@ class DeleteDiffFilesWorker # rubocop:disable Scalability/IdempotentWorker
return if merge_request_diff.without_files?
MergeRequestDiff.transaction do
- merge_request_diff.clean!
-
MergeRequestDiffFile
.where(merge_request_diff_id: merge_request_diff.id)
.delete_all
+
+ merge_request_diff.clean!
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/delete_stored_files_worker.rb b/app/workers/delete_stored_files_worker.rb
index 9cf5631b7d8..689ac3dd0ce 100644
--- a/app/workers/delete_stored_files_worker.rb
+++ b/app/workers/delete_stored_files_worker.rb
@@ -9,8 +9,8 @@ class DeleteStoredFilesWorker # rubocop:disable Scalability/IdempotentWorker
def perform(class_name, keys)
klass = begin
class_name.constantize
- rescue NameError
- nil
+ rescue NameError
+ nil
end
unless klass
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 6e4feea1b26..b0307571448 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -37,10 +37,7 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
- if task != :pack_refs
- project.repository.expire_statistics_caches
- Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]).execute
- end
+ update_repository_statistics(project) if task != :pack_refs
# In case pack files are deleted, release libgit2 cache and open file
# descriptors ASAP instead of waiting for Ruby garbage collection
@@ -106,6 +103,13 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
project.repository.has_visible_content?
end
+ def update_repository_statistics(project)
+ project.repository.expire_statistics_caches
+ return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary
+
+ Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]).execute
+ end
+
def bitmaps_enabled?
Gitlab::CurrentSettings.housekeeping_bitmaps_enabled
end
diff --git a/app/workers/issue_placement_worker.rb b/app/workers/issue_placement_worker.rb
new file mode 100644
index 00000000000..a8d59e9125c
--- /dev/null
+++ b/app/workers/issue_placement_worker.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class IssuePlacementWorker
+ include ApplicationWorker
+
+ idempotent!
+ feature_category :issue_tracking
+ urgency :high
+ worker_resource_boundary :cpu
+ weight 2
+
+ # Move at most the most recent 100 issues
+ QUERY_LIMIT = 100
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(issue_id, project_id = nil)
+ issue = find_issue(issue_id, project_id)
+ return unless issue
+
+ # Move the oldest 100 unpositioned items to the end.
+ # This is to deal with out-of-order execution of the worker,
+ # while preserving creation order.
+ to_place = Issue
+ .relative_positioning_query_base(issue)
+ .where(relative_position: nil)
+ .order({ created_at: :asc }, { id: :asc })
+ .limit(QUERY_LIMIT + 1)
+ .to_a
+
+ leftover = to_place.pop if to_place.count > QUERY_LIMIT
+
+ Issue.move_nulls_to_end(to_place)
+ Issues::BaseService.new(nil).rebalance_if_needed(to_place.max_by(&:relative_position))
+ IssuePlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
+ rescue RelativePositioning::NoSpaceLeft => e
+ Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)
+ IssueRebalancingWorker.perform_async(nil, project_id.presence || issue.project_id)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def find_issue(issue_id, project_id)
+ return Issue.id_in(issue_id).first if issue_id
+
+ project = Project.id_in(project_id).first
+ return unless project
+
+ project.issues.first
+ end
+end
diff --git a/app/workers/issue_rebalancing_worker.rb b/app/workers/issue_rebalancing_worker.rb
new file mode 100644
index 00000000000..032ba5534e6
--- /dev/null
+++ b/app/workers/issue_rebalancing_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class IssueRebalancingWorker
+ include ApplicationWorker
+
+ idempotent!
+ urgency :low
+ feature_category :issue_tracking
+
+ def perform(ignore = nil, project_id = nil)
+ return if project_id.nil?
+
+ project = Project.find(project_id)
+ issue = project.issues.first # All issues are equivalent as far as we are concerned
+
+ IssueRebalancingService.new(issue).execute
+ rescue ActiveRecord::RecordNotFound, IssueRebalancingService::TooManyIssues => e
+ Gitlab::ErrorTracking.log_exception(e, project_id: project_id)
+ end
+end
diff --git a/app/workers/jira_connect/sync_branch_worker.rb b/app/workers/jira_connect/sync_branch_worker.rb
new file mode 100644
index 00000000000..8c3416478fd
--- /dev/null
+++ b/app/workers/jira_connect/sync_branch_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class SyncBranchWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ queue_namespace :jira_connect
+ feature_category :integrations
+ loggable_arguments 1, 2
+
+ def perform(project_id, branch_name, commit_shas)
+ project = Project.find_by_id(project_id)
+
+ return unless project
+
+ branches = [project.repository.find_branch(branch_name)] if branch_name.present?
+ commits = project.commits_by(oids: commit_shas) if commit_shas.present?
+
+ JiraConnect::SyncService.new(project).execute(commits: commits, branches: branches)
+ end
+ end
+end
diff --git a/app/workers/jira_connect/sync_merge_request_worker.rb b/app/workers/jira_connect/sync_merge_request_worker.rb
new file mode 100644
index 00000000000..b78bb8dfe16
--- /dev/null
+++ b/app/workers/jira_connect/sync_merge_request_worker.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class SyncMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ queue_namespace :jira_connect
+ feature_category :integrations
+
+ def perform(merge_request_id)
+ merge_request = MergeRequest.find_by_id(merge_request_id)
+
+ return unless merge_request && merge_request.project
+
+ JiraConnect::SyncService.new(merge_request.project).execute(merge_requests: [merge_request])
+ end
+ end
+end
diff --git a/app/workers/merge_request_cleanup_refs_worker.rb b/app/workers/merge_request_cleanup_refs_worker.rb
new file mode 100644
index 00000000000..37774658ba8
--- /dev/null
+++ b/app/workers/merge_request_cleanup_refs_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class MergeRequestCleanupRefsWorker
+ include ApplicationWorker
+
+ feature_category :source_code_management
+ idempotent!
+
+ def perform(merge_request_id)
+ merge_request = MergeRequest.find_by_id(merge_request_id)
+
+ unless merge_request
+ logger.error("Failed to find merge request with ID: #{merge_request_id}")
+ return
+ end
+
+ result = ::MergeRequests::CleanupRefsService.new(merge_request).execute
+
+ return if result[:status] == :success
+
+ logger.error("Failed cleanup refs of merge request (#{merge_request_id}): #{result[:message]}")
+ end
+end
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index e0e28767f8d..be9a168c3f6 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -12,8 +12,8 @@ class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker
def perform(issue_id, user_id)
return unless objects_found?(issue_id, user_id)
- EventCreateService.new.open_issue(issuable, user)
- NotificationService.new.new_issue(issuable, user)
+ ::EventCreateService.new.open_issue(issuable, user)
+ ::NotificationService.new.new_issue(issuable, user)
issuable.create_cross_references!(user)
end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index b31311b0e44..2bb2d0db55c 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -13,17 +13,11 @@ class NewNoteWorker # rubocop:disable Scalability/IdempotentWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
- NotificationService.new.new_note(note) unless skip_notification?(note)
+ NotificationService.new.new_note(note) unless note.skip_notification?
Notes::PostProcessService.new(note).execute
else
Gitlab::AppLogger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
end
end
# rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def skip_notification?(note)
- note.review.present?
- end
end
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index 33a0e0f5f88..f489e428e8d 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -41,16 +41,14 @@ module ObjectStorage
end
end
- # rubocop:disable Gitlab/RailsLogger
def report!(results)
success, failures = results.partition(&:success?)
- Rails.logger.info header(success, failures)
- Rails.logger.warn failures(failures)
+ Gitlab::AppLogger.info header(success, failures)
+ Gitlab::AppLogger.warn failures(failures)
raise MigrationFailures.new(failures.map(&:error)) if failures.any?
end
- # rubocop:enable Gitlab/RailsLogger
def header(success, failures)
_("Migrated %{success_count}/%{total_count} files.") % { success_count: success.count, total_count: success.count + failures.count }
@@ -104,7 +102,7 @@ module ObjectStorage
report!(results)
rescue SanityCheckError => e
# do not retry: the job is insane
- Rails.logger.warn "#{self.class}: Sanity check error (#{e.message})" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn "#{self.class}: Sanity check error (#{e.message})"
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/pages_remove_worker.rb b/app/workers/pages_remove_worker.rb
new file mode 100644
index 00000000000..b83168fd7bd
--- /dev/null
+++ b/app/workers/pages_remove_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class PagesRemoveWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ sidekiq_options retry: 3
+ feature_category :pages
+ loggable_arguments 0
+
+ def perform(project_id)
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ project.remove_pages
+ project.pages_domains.delete_all
+ end
+end
diff --git a/app/workers/pages_transfer_worker.rb b/app/workers/pages_transfer_worker.rb
new file mode 100644
index 00000000000..f78564cc69d
--- /dev/null
+++ b/app/workers/pages_transfer_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class PagesTransferWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ TransferFailedError = Class.new(StandardError)
+
+ feature_category :pages
+ loggable_arguments 0, 1
+
+ def perform(method, args)
+ return unless Gitlab::PagesTransfer::Async::METHODS.include?(method)
+
+ result = Gitlab::PagesTransfer.new.public_send(method, *args) # rubocop:disable GitlabSecurity/PublicSend
+
+ # If result isn't truthy, the move failed. Promote this to an
+ # exception so that it will be logged and retried appropriately
+ raise TransferFailedError unless result
+ end
+end
diff --git a/app/workers/pages_update_configuration_worker.rb b/app/workers/pages_update_configuration_worker.rb
index d0904db6b42..07238bae8c2 100644
--- a/app/workers/pages_update_configuration_worker.rb
+++ b/app/workers/pages_update_configuration_worker.rb
@@ -10,14 +10,6 @@ class PagesUpdateConfigurationWorker
project = Project.find_by_id(project_id)
return unless project
- result = Projects::UpdatePagesConfigurationService.new(project).execute
-
- # The ConfigurationService swallows all exceptions and wraps them in a status
- # we need to keep this while the feature flag still allows running this
- # service within a request.
- # But we might as well take advantage of sidekiq retries here.
- # We should let the service raise after we remove the feature flag
- # https://gitlab.com/gitlab-org/gitlab/-/issues/230695
- raise result[:exception] if result[:exception]
+ Projects::UpdatePagesConfigurationService.new(project).execute
end
end
diff --git a/app/workers/partition_creation_worker.rb b/app/workers/partition_creation_worker.rb
index b833e818b32..119ecd28003 100644
--- a/app/workers/partition_creation_worker.rb
+++ b/app/workers/partition_creation_worker.rb
@@ -9,5 +9,7 @@ class PartitionCreationWorker
def perform
Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions
+ ensure
+ Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
end
end
diff --git a/app/workers/personal_access_tokens/expired_notification_worker.rb b/app/workers/personal_access_tokens/expired_notification_worker.rb
index c1b1f1a461d..2ff64ec51f3 100644
--- a/app/workers/personal_access_tokens/expired_notification_worker.rb
+++ b/app/workers/personal_access_tokens/expired_notification_worker.rb
@@ -8,8 +8,6 @@ module PersonalAccessTokens
feature_category :authentication_and_authorization
def perform(*args)
- return unless Feature.enabled?(:expired_pat_email_notification)
-
notification_service = NotificationService.new
User.with_personal_access_tokens_expired_today.find_each do |user|
diff --git a/app/workers/pipeline_update_ci_ref_status_worker.rb b/app/workers/pipeline_update_ci_ref_status_worker.rb
deleted file mode 100644
index 9b1a5d8e7cf..00000000000
--- a/app/workers/pipeline_update_ci_ref_status_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This class is unused and to be removed in 13.1~
-class PipelineUpdateCiRefStatusWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
- include PipelineQueue
-
- urgency :high
- worker_resource_boundary :cpu
-
- def perform(pipeline_id)
- pipeline = Ci::Pipeline.find_by_id(pipeline_id)
-
- return unless pipeline
-
- Ci::UpdateCiRefStatusService.new(pipeline).call
- end
-end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 8f844bd0b47..4a93b1af166 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -12,8 +12,8 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
def perform(gl_repository, identifier, changes, push_options = {})
container, project, repo_type = Gitlab::GlRepository.parse(gl_repository)
- if project.nil? && (!repo_type.snippet? || container.is_a?(ProjectSnippet))
- log("Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"")
+ if container.nil? || (container.is_a?(ProjectSnippet) && project.nil?)
+ log("Triggered hook for non-existing gl_repository \"#{gl_repository}\"")
return false
end
@@ -24,7 +24,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
post_received = Gitlab::GitPostReceive.new(container, identifier, changes, push_options)
if repo_type.wiki?
- process_wiki_changes(post_received, container)
+ process_wiki_changes(post_received, container.wiki)
elsif repo_type.project?
process_project_changes(post_received, container)
elsif repo_type.snippet?
@@ -59,18 +59,15 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
after_project_changes_hooks(project, user, changes.refs, changes.repository_data)
end
- def process_wiki_changes(post_received, project)
- project.touch(:last_activity_at, :last_repository_updated_at)
- project.wiki.repository.expire_statistics_caches
- ProjectCacheWorker.perform_async(project.id, [], [:wiki_size])
-
+ def process_wiki_changes(post_received, wiki)
user = identify_user(post_received)
return false unless user
# We only need to expire certain caches once per push
- expire_caches(post_received, project.wiki.repository)
+ expire_caches(post_received, wiki.repository)
+ wiki.repository.expire_statistics_caches
- ::Git::WikiPushService.new(project, user, changes: post_received.changes).execute
+ ::Git::WikiPushService.new(wiki, user, changes: post_received.changes).execute
end
def process_snippet_changes(post_received, snippet)
@@ -78,10 +75,16 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
return false unless user
+ replicate_snippet_changes(snippet)
+
expire_caches(post_received, snippet.repository)
Snippets::UpdateStatisticsService.new(snippet).execute
end
+ def replicate_snippet_changes(snippet)
+ # Used by Gitlab Geo
+ end
+
# Expire the repository status, branch, and tag cache once per push.
def expire_caches(post_received, repository)
repository.expire_status_cache if repository.empty?
diff --git a/app/workers/propagate_integration_worker.rb b/app/workers/propagate_integration_worker.rb
index 15c0e761a0a..68e38386372 100644
--- a/app/workers/propagate_integration_worker.rb
+++ b/app/workers/propagate_integration_worker.rb
@@ -7,10 +7,8 @@ class PropagateIntegrationWorker
idempotent!
loggable_arguments 1
- def perform(integration_id, overwrite)
- Admin::PropagateIntegrationService.propagate(
- integration: Service.find(integration_id),
- overwrite: overwrite
- )
+ # Keep overwrite parameter for backwards compatibility.
+ def perform(integration_id, overwrite = nil)
+ Admin::PropagateIntegrationService.propagate(Service.find(integration_id))
end
end
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index 37d5ccb656d..b02525b5106 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -12,7 +12,7 @@ class PropagateServiceTemplateWorker # rubocop:disable Scalability/IdempotentWor
def perform(template_id)
return unless try_obtain_lease_for(template_id)
- Projects::PropagateServiceTemplate.propagate(Service.find_by(id: template_id))
+ Admin::PropagateServiceTemplate.propagate(Service.find_by(id: template_id))
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 395fce0696c..fc7999e7837 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -34,7 +34,7 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
def start_fork(project)
return true if start(project.import_state)
- Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
end
@@ -52,7 +52,7 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
def link_lfs_objects(source_project, target_project)
Projects::LfsPointers::LfsLinkService
.new(target_project)
- .execute(source_project.all_lfs_objects_oids)
+ .execute(source_project.lfs_objects_oids)
rescue Projects::LfsPointers::LfsLinkService::TooManyOidsError
raise_fork_failure(
source_project,
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index 7d76cbed77f..605dd624260 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -31,11 +31,10 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
private
- # rubocop:disable Gitlab/RailsLogger
def error(schedule, error)
failed_creation_counter.increment
- Rails.logger.error "Failed to create a scheduled pipeline. " \
+ Gitlab::AppLogger.error "Failed to create a scheduled pipeline. " \
"schedule_id: #{schedule.id} message: #{error.message}"
Gitlab::ErrorTracking
@@ -43,7 +42,6 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/41231',
schedule_id: schedule.id)
end
- # rubocop:enable Gitlab/RailsLogger
def failed_creation_counter
@failed_creation_counter ||=
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index b3b1ed66efc..f8209ae5e63 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -17,7 +17,7 @@ class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
def perform
return unless try_obtain_lease
- Rails.logger.info "#{self.class}: Cleaning stuck builds" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info "#{self.class}: Cleaning stuck builds" # rubocop:disable Gitlab/RailsLogger
drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure
drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure
@@ -69,7 +69,7 @@ class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
# rubocop: enable CodeReuse/ActiveRecord
def drop_build(type, build, status, timeout, reason)
- Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout}, reason: #{reason})" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout}, reason: #{reason})"
Gitlab::OptimisticLocking.retry_lock(build, 3) do |b|
b.drop(reason)
end
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index e0209b8237a..0f9b4ddb980 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -7,7 +7,7 @@ class StuckMergeJobsWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :source_code_management
def self.logger
- Rails.logger # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
index ee7724e0fa8..eb1a7f4fef9 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -11,7 +11,7 @@ class TrendingProjectsWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :source_code_management
def perform
- Rails.logger.info('Refreshing trending projects') # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info('Refreshing trending projects')
TrendingProject.refresh!
end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 98534b258a7..402c1777662 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -9,8 +9,6 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
weight 3
loggable_arguments 2, 3, 4
- LOG_TIME_THRESHOLD = 90 # seconds
-
# rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id)
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
index dc2511f718c..ce43b56bbd8 100644
--- a/app/workers/upload_checksum_worker.rb
+++ b/app/workers/upload_checksum_worker.rb
@@ -10,6 +10,6 @@ class UploadChecksumWorker # rubocop:disable Scalability/IdempotentWorker
upload.calculate_checksum!
upload.save!
rescue ActiveRecord::RecordNotFound
- Rails.logger.error("UploadChecksumWorker: couldn't find upload #{upload_id}, skipping") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error("UploadChecksumWorker: couldn't find upload #{upload_id}, skipping")
end
end