Commit fd2c27ac authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into sh-support-bitbucket-server-import

parents 2d3fd6a1 226d4d0e
......@@ -50,6 +50,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Definition of done](#definition-of-done)
- [Style guides](#style-guides)
- [Code of conduct](#code-of-conduct)
- [Contribution Flow](#contribution-flow)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
......@@ -225,24 +226,24 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
### Bug Priority labels
### Priority labels
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
| Label | Meaning | Estimate time to fix | Guidance |
|-------|-----------------|------------------------------------------------------------------|----------|
| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
| ~P2 | High Priority | The next release | |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
| Label | Meaning | Estimate time to fix |
|-------|-----------------|------------------------------------------------------------------|
| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com |
| ~P2 | High Priority | The next release |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) |
### Bug Severity labels
### Severity labels
Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Impact of the defect | Example |
| Label | Meaning | Impact on Functionality | Example |
|-------|-------------------|-------------------------------------------------------|---------|
| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
......@@ -251,12 +252,14 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
#### Severity impact guidance
| Label | Security Impact | Availability / Performance Impact |
|-------|---------------------------------------------------------------------|--------------------------------------------------------------|
| ~S1 | >50% users impacted (possible company extinction level event) | |
| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
| ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future |
| ~S4 | No paid users/customer impacted, or expected impact within 30 days | The issue _may_ occur but it's not likely |
Severity levels can be applied further depending on the facet of the impact; e.g. Affected customers, GitLab.com availability, performance and etc. The below is a guideline.
| Severity | Affected Customers/Users | GitLab.com Availability | Performance Degradation |
|----------|---------------------------------------------------------------------|----------------------------------------------------|------------------------------|
| ~S1 | >50% users affected (possible company extinction level event) | Significant impact on all of GitLab.com | |
| ~S2 | Many users or multiple paid customers affected (but not apocalyptic)| Significant impact on large portions of GitLab.com | Degradation is guaranteed to occur in the near future |
| ~S3 | A few users or a single paid customer affected | Limited impact on important portions of GitLab.com | Degradation is likely to occur in the near future |
| ~S4 | No paid users/customer affected, or expected to in the near future | Minor impact on on GitLab.com | Degradation _may_ occur but it's not likely |
### Label for community contributors
......@@ -729,6 +732,24 @@ reported by emailing `contact@gitlab.com`.
This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
## Contribution Flow
When contributing to GitLab, your merge request is subject to review by merge request maintainers of a particular specialty.
When you submit code to GitLab, we really want it to get merged, but there will be times when it will not be merged.
When maintainers are reading through a merge request they may request guidance from other maintainers. If merge request maintainers conclude that the code should not be merged, our reasons will be fully disclosed. If it has been decided that the code quality is not up to GitLab’s standards, the merge request maintainer will refer the author to our docs and code style guides, and provide some guidance.
Sometimes style guides will be followed but the code will lack structural integrity, or the maintainer will have reservations about the code’s overall quality. When there is a reservation the maintainer will inform the author and provide some guidance. The author may then choose to update the merge request. Once the merge request has been updated and reassigned to the maintainer, they will review the code again. Once the code has been resubmitted any number of times, the maintainer may choose to close the merge request with a summary of why it will not be merged, as well as some guidance. If the merge request is closed the maintainer will be open to discussion as to how to improve the code so it can be approved in the future.
GitLab will do its best to review community contributions as quickly as possible. Specially appointed developers review community contributions daily. You may take a look at the [team page](https://about.gitlab.com/team/) for the merge request coach who specializes in the type of code you have written and mention them in the merge request. For example, if you have written some JavaScript in your code then you should mention the frontend merge request coach. If your code has multiple disciplines you may mention multiple merge request coaches.
GitLab receives a lot of community contributions, so if your code has not been reviewed within 4 days of its initial submission feel free to re-mention the appropriate merge request coach.
When submitting code to GitLab, you may feel that your contribution requires the aid of an external library. If your code includes an external library please provide a link to the library, as well as reasons for including it.
When your code contains more than 500 changes, any major breaking changes, or an external library, `@mention` a maintainer in the merge request. If you are not sure who to mention, the reviewer will add one early in the merge request process.
[core team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[getting-help]: https://about.gitlab.com/getting-help/
......
......@@ -41,11 +41,6 @@ export default {
required: true,
},
},
data() {
return {
activeFile: '',
};
},
computed: {
...mapState({
isLoading: state => state.diffs.isLoading,
......@@ -126,14 +121,6 @@ export default {
eventHub.$emit('fetchNotesData');
}
},
setActive(filePath) {
this.activeFile = filePath;
},
unsetActive(filePath) {
if (this.activeFile === filePath) {
this.activeFile = '';
}
},
adjustView() {
if (this.shouldShow && this.isParallelView) {
window.mrTabs.expandViewContainer();
......@@ -195,7 +182,6 @@ export default {
<changed-files
:diff-files="diffFiles"
:active-file="activeFile"
/>
<div
......@@ -207,8 +193,6 @@ export default {
:key="file.newPath"
:file="file"
:current-user="currentUser"
@setActive="setActive(file.filePath)"
@unsetActive="unsetActive(file.filePath)"
/>
</div>
<no-changes v-else />
......
......@@ -16,13 +16,6 @@ export default {
ClipboardButton,
},
mixins: [changedFilesMixin],
props: {
activeFile: {
type: String,
required: false,
default: '',
},
},
data() {
return {
isStuck: false,
......@@ -70,7 +63,7 @@ export default {
pluralize,
handleScroll() {
if (!this.updating) {
requestAnimationFrame(this.updateIsStuck);
this.$nextTick(this.updateIsStuck);
this.updating = true;
}
},
......@@ -148,25 +141,8 @@ export default {
/>
<span
v-show="activeFile"
class="prepend-left-5"
>
<strong class="prepend-right-5">
{{ truncatedDiffPath(activeFile) }}
</strong>
<clipboard-button
:text="activeFile"
:title="s__('Copy file name to clipboard')"
tooltip-placement="bottom"
tooltip-container="body"
class="btn btn-default btn-transparent btn-clipboard"
/>
</span>
<span
v-show="!isStuck"
id="diff-stats"
class="diff-stats-additions-deletions-expanded"
class="js-diff-stats-additions-deletions-expanded
diff-stats-additions-deletions-expanded"
>
with
<strong class="cgreen">
......@@ -177,6 +153,17 @@ export default {
{{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
</strong>
</span>
<div
class="js-diff-stats-additions-deletions-collapsed
diff-stats-additions-deletions-collapsed float-right d-sm-none"
>
<strong class="cgreen">
+{{ sumAddedLines }}
</strong>
<strong class="cred">
-{{ sumRemovedLines }}
</strong>
</div>
</div>
</div>
</div>
......
......@@ -40,7 +40,7 @@ export default {
{{ n__('%d changed file', '%d changed files', diffFiles.length) }}
</span>
<icon
:size="8"
class="caret-icon"
name="chevron-down"
/>
</button>
......
......@@ -25,7 +25,6 @@ export default {
},
data() {
return {
isActive: false,
isLoadingCollapsedDiff: false,
forkMessageVisible: false,
};
......@@ -48,12 +47,6 @@ export default {
return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge;
},
},
mounted() {
document.addEventListener('scroll', this.handleScroll);
},
beforeDestroy() {
document.removeEventListener('scroll', this.handleScroll);
},
methods: {
...mapActions('diffs', ['loadCollapsedDiff']),
handleToggle() {
......@@ -65,36 +58,6 @@ export default {
this.file.collapsed = !this.file.collapsed;
}
},
handleScroll() {
if (!this.updating) {
requestAnimationFrame(this.scrollUpdate.bind(this));
this.updating = true;
}
},
scrollUpdate() {
const header = document.querySelector('.js-diff-files-changed');
if (!header) {
this.updating = false;
return;
}
const { top, bottom } = this.$el.getBoundingClientRect();
const { top: topOfFixedHeader, bottom: bottomOfFixedHeader } = header.getBoundingClientRect();
const headerOverlapsContent = top < topOfFixedHeader && bottom > bottomOfFixedHeader;
const fullyAboveHeader = bottom < bottomOfFixedHeader;
const fullyBelowHeader = top > topOfFixedHeader;
if (headerOverlapsContent && !this.isActive) {
this.$emit('setActive');
this.isActive = true;
} else if (this.isActive && (fullyAboveHeader || fullyBelowHeader)) {
this.$emit('unsetActive');
this.isActive = false;
}
this.updating = false;
},
handleLoadCollapsedDiff() {
this.isLoadingCollapsedDiff = true;
......
......@@ -31,7 +31,7 @@ export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, searc
dispatch('requestMergeRequests', type);
dispatch('resetMergeRequests', type);
Api.mergeRequests({ scope, state, search })
return Api.mergeRequests({ scope, state, search })
.then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
.catch(() => dispatch('receiveMergeRequestsError', { type, search }));
};
......
......@@ -102,7 +102,7 @@ export const receiveJobsSuccess = ({ commit }, { id, data }) =>
export const fetchJobs = ({ dispatch }, stage) => {
dispatch('requestJobs', stage.id);
axios
return axios
.get(stage.dropdownPath)
.then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
.catch(() => dispatch('receiveJobsError', stage));
......
......@@ -32,7 +32,7 @@
};
</script>
<template>
<div class="space-children d-flex append-right-10">
<div class="space-children d-flex append-right-10 widget-status-icon">
<div
v-if="isLoading"
class="mr-widget-icon"
......
......@@ -233,7 +233,7 @@ export default {
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
<span class="btn-group">
<button
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
......
......@@ -518,6 +518,12 @@
outline: none;
color: $gl-link-hover-color;
}
.caret-icon {
position: relative;
top: 2px;
left: -1px;
}
}
// Mobile
......
......@@ -214,6 +214,10 @@
}
}
.widget-status-icon {
align-self: flex-start;
}
.mr-widget-body {
line-height: 28px;
......
......@@ -32,6 +32,7 @@ class NotificationSetting < ActiveRecord::Base
:reopen_issue,
:close_issue,
:reassign_issue,
:issue_due,
:new_merge_request,
:push_to_merge_request,
:reopen_merge_request,
......
require 'prometheus/client/formats/text'
class MetricsService
CHECKS = [
Gitlab::HealthChecks::DbCheck,
Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
def prometheus_metrics_text
Prometheus::Client::Formats::Text.marshal_multiprocess(multiprocess_metrics_path)
end
def health_metrics_text
metrics = CHECKS.flat_map(&:metrics)
formatter.marshal(metrics)
end
def metrics_text
prometheus_metrics_text.concat(health_metrics_text)
prometheus_metrics_text
end
private
def formatter
@formatter ||= Gitlab::HealthChecks::PrometheusTextFormat.new
end
def multiprocess_metrics_path
::Prometheus::Client.configuration.multiprocess_files_dir
end
......
......@@ -6,7 +6,7 @@
= render_if_exists 'admin/namespace_plan', f: f
.form-group.row.group-description-holder
= f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
= f.label :avatar, _("Group avatar"), class: 'col-form-label col-sm-2'
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
......@@ -26,12 +26,12 @@
.alert.alert-info
= render 'shared/group_tips'
.form-actions
= f.submit 'Create group', class: "btn btn-create"
= link_to 'Cancel', admin_groups_path, class: "btn btn-cancel"
= f.submit _('Create group'), class: "btn btn-create"
= link_to _('Cancel'), admin_groups_path, class: "btn btn-cancel"
- else
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
= f.submit _('Save changes'), class: "btn btn-save"
= link_to _('Cancel'), admin_group_path(@group), class: "btn btn-cancel"
= render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group
......@@ -3,8 +3,8 @@
%li.group-row{ class: css_class }
.controls
= link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
= link_to _('Edit'), admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to _('Delete'), [:admin, group], data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name } }, method: :delete, class: 'btn btn-remove'
.stats
%span.badge.badge-pill
= storage_counter(group.storage_size)
......
- page_title "Edit", @group.name, "Groups"
%h3.page-title Edit group: #{@group.name}
- page_title _("Edit"), @group.name, _("Groups")
%h3.page-title= _('Edit group: %{group_name}') % { group_name: @group.name }
%hr
= render 'form', visibility_level: @group.visibility_level
- @no_container = true
- page_title "Groups"
- page_title _("Groups")
%div{ class: container_class }
.top-area
......@@ -13,7 +13,7 @@
= icon("search", class: "search-icon")
= render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
= link_to new_admin_group_path, class: "btn btn-new" do
New group
= _('New group')
%ul.content-list
= render @groups
......
- page_title "New Group"
%h3.page-title New group
- page_title _("New Group")
%h3.page-title= _('New group')
%hr
= render 'form', visibility_level: default_group_visibility
- add_to_breadcrumbs "Groups", admin_groups_path
- add_to_breadcrumbs _("Groups"), admin_groups_path
- breadcrumb_title @group.name
- page_title @group.name, "Groups"
- page_title @group.name, _("Groups")
%h3.page-title
Group: #{@group.full_name}
= _('Group: %{group_name}') % { group_name: @group.full_name }
= link_to admin_group_edit_path(@group), class: "btn float-right" do
%i.fa.fa-pencil-square-o
Edit
= _('Edit')
%hr
.row
.col-md-6
.card
.card-header
Group info:
= _('Group info:')
%ul.content-list
%li
.avatar-container.s60
= group_icon(@group, class: "avatar s60")
%li
%span.light Name:
%span.light= _('Name:')
%strong= @group.name
%li
%span.light Path:
%span.light= _('Path:')
%strong
= @group.path
%li
%span.light Description:
%span.light= _('Description:')
%strong
= @group.description
%li
%span.light Visibility level:
%span.light= _('Visibility level:')
%strong
= visibility_level_label(@group.visibility_level)
%li
%span.light Created on:
%span.light= _('Created on:')
%strong
= @group.created_at.to_s(:medium)
= render_if_exists 'admin/namespace_plan_info', namespace: @group
%li
%span.light Storage:
%strong= storage_counter(@group.storage_size)
(
= storage_counter(@group.repository_size)
repositories,
= storage_counter(@group.build_artifacts_size)
build artifacts,
= storage_counter(@group.lfs_objects_size)
LFS
)
%span.light= _('Storage:')
- counter_storage = storage_counter(@group.storage_size)
- counter_repositories = storage_counter(@group.repository_size)
- counter_build_artifacts = storage_counter(@group.build_artifacts_size)
- counter_lfs_objects = storage_counter(@group.lfs_objects_size)
%strong
= _("%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)") % { counter_storage: counter_storage, counter_repositories: counter_repositories, counter_build_artifacts: counter_build_artifacts, counter_lfs_objects: counter_lfs_objects }
%li
%span.light Group Git LFS status:
%span.light= _('Group Git LFS status:')
%strong
= group_lfs_status(@group)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
......@@ -67,7 +64,7 @@
.card
.card-header
%h3.card-title
Projects
= _('Projects')
%span.badge.badge-pill
#{@group.projects.count}
%ul.content-list
......@@ -85,7 +82,7 @@
- if @group.shared_projects.any?
.card
.card-header
Projects shared with #{@group.name}
= _('Projects shared with %{group_name}') % { group_name: @group.name }
%span.badge.badge-pill
#{@group.shared_projects.count}
%ul.content-list
......@@ -102,11 +99,11 @@
- if can?(current_user, :admin_group_member, @group)
.card
.card-header
Add user(s) to the group:
= _('Add user(s) to the group:')
.card-body.form-holder
%p.light
Read more about project permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
- link_to_help = link_to(_("here"), help_page_path("user/permissions"), class: "vlink")
= _('Read more about project permissions <strong>%{link_to_help}</strong>').html_safe % { link_to_help: link_to_help }
= form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
......@@ -114,16 +111,15 @@
.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
= button_tag 'Add users to group', class: "btn btn-create"
= button_tag _('Add users to group'), class: "btn btn-create"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
.card
.card-header
%strong= @group.name
group members
= _("<strong>%{group_name}</strong> group members").html_safe % { group_name: @group.name }
%span.badge.badge-pill= @group.members.size
.float-right
= link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-sm"
= link_to icon('pencil-square-o', text: _('Manage access')), polymorphic_url([@group, :members]), class: "btn btn-sm"
%ul.content-list.group-users-list.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
.card-footer
......
---
title: Improves performance on Merge Request diff tab by removing the scroll event
listeners being added to every file
merge_request:
author:
type: performance
---
title: Remove healthchecks from prometheus endpoint
merge_request: 20565
author:
type: fixed
---
title: Fix performance problem of accessing tag list for projects api endpoints
merge_request:
author:
type: performance
---
title: 'Allow to toggle notifications for issues due soon'
merge_request:
author:
type: fixed
......@@ -135,10 +135,13 @@ module API
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
def self.preload_relation(projects_relation, options = {})
# Preloading tags, should be done with using only `:tags`,
# as `:tags` are defined as: `has_many :tags, through: :taggings`
# N+1 is solved then by using `subject.tags.map(&:name)`
# MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555
projects_relation.preload(:project_feature, :route)
.preload(:import_state)
.preload(namespace: [:route, :owner],
tags: :taggings)
.preload(:import_state, :tags)
.preload(namespace: [:route, :owner])
end
end
......@@ -212,11 +215,15 @@ module API
expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics
def self.preload_relation(projects_relation, options = {})
# Preloading tags, should be done with using only `:tags`,
# as `:tags` are defined as: `has_many :tags, through: :taggings`
# N+1 is solved then by using `subject.tags.map(&:name)`
# MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555
super(projects_relation).preload(:group)
.preload(project_group_links: :group,
fork_network: :root_project,
forked_project_link: :forked_from_project,
forked_from_project: [:route, :forks, namespace: :route, tags: :taggings])
forked_from_project: [:route, :forks, :tags, namespace: :route])
end
def self.forks_counting_projects(projects_relation)
......
......@@ -77,6 +77,9 @@ msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)"
msgstr ""
msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] ""
......@@ -202,6 +205,9 @@ msgstr ""
msgid "404|Please contact your GitLab administrator if you think this is a mistake."
msgstr ""
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
msgid "<strong>Removes</strong> source branch"
msgstr ""
......@@ -283,6 +289,12 @@ msgstr ""
msgid "Add todo"
msgstr ""
msgid "Add user(s) to the group:"
msgstr ""
msgid "Add users to group"
msgstr ""
msgid "AdminArea|Stop all jobs"
msgstr ""
......@@ -484,6 +496,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
msgid "Are you sure you want to remove %{group_name}?"
msgstr ""
msgid "Are you sure you want to remove this identity?"
msgstr ""
......@@ -1666,9 +1681,6 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr ""
msgid "Copy file name to clipboard"
msgstr ""
msgid "Copy file path to clipboard"
msgstr ""
......@@ -1708,6 +1720,9 @@ msgstr ""
msgid "Create file"
msgstr ""
msgid "Create group"
msgstr ""
msgid "Create group label"
msgstr ""
......@@ -1756,6 +1771,9 @@ msgstr ""
msgid "Created by me"
msgstr ""
msgid "Created on:"
msgstr ""
msgid "Cron Timezone"
msgstr ""
......@@ -1962,6 +1980,9 @@ msgstr ""
msgid "Description"
msgstr ""
msgid "Description:"
msgstr ""
msgid "Details"
msgstr ""
......@@ -2058,6 +2079,9 @@ msgstr ""
msgid "Edit files in the editor and commit changes here"
msgstr ""
msgid "Edit group: %{group_name}"
msgstr ""
msgid "Edit identity for %{user_name}"
msgstr ""
......@@ -2456,15 +2480,27 @@ msgstr ""
msgid "Group CI/CD settings"
msgstr ""
msgid "Group Git LFS status:"
msgstr ""
msgid "Group ID"
msgstr ""
msgid "Group Runners"
msgstr ""
msgid "Group avatar"
msgstr ""
msgid "Group info:"
msgstr ""
msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "Group: %{group_name}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
......@@ -2898,6 +2934,9 @@ msgstr ""
msgid "Locked to current projects"
msgstr ""
msgid "Manage access"
msgstr ""
msgid "Manage all notifications"
msgstr ""
......@@ -3084,6 +3123,9 @@ msgstr ""
msgid "Name your individual key via a title"
msgstr ""
msgid "Name:"
msgstr ""
msgid "Nav|Help"
msgstr ""
......@@ -3099,6 +3141,9 @@ msgstr ""
msgid "New"
msgstr ""
msgid "New Group"
msgstr ""
msgid "New Identity"
msgstr ""
......@@ -3386,6 +3431,9 @@ msgstr ""
msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
msgstr ""
msgid "Path:"
msgstr ""
msgid "Pause"
msgstr ""
......@@ -3746,6 +3794,9 @@ msgstr ""
msgid "Projects"
msgstr ""
msgid "Projects shared with %{group_name}"
msgstr ""
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
......@@ -3872,6 +3923,9 @@ msgstr ""
msgid "Read more"
msgstr ""
msgid "Read more about project permissions <strong>%{link_to_help}</strong>"
msgstr ""
msgid "Readme"
msgstr ""
......@@ -4431,6 +4485,9 @@ msgstr ""
msgid "Storage"
msgstr ""
msgid "Storage:"
msgstr ""
msgid "Subgroups"
msgstr ""
......@@ -5129,6 +5186,9 @@ msgstr ""
msgid "Visibility and access controls"
msgstr ""
msgid "Visibility level:"
msgstr ""
msgid "Visibility:"
msgstr ""
......@@ -5482,6 +5542,9 @@ msgstr ""
msgid "for this project"
msgstr ""
msgid "here"
msgstr ""
msgid "importing"
msgstr ""
......
$: << File.expand_path(File.dirname(__FILE__))
Encoding.default_external = 'UTF-8'
module QA
##
# GitLab QA runtime classes, mostly singletons.
......
......@@ -36,6 +36,9 @@ module QA
if @install_helm_tiller
Page::Project::Operations::Kubernetes::Show.perform do |page|
# We must wait a few seconds for permissions to be setup correctly for new cluster
sleep 10
# Helm must be installed before everything else
page.install!(:helm)
page.await_installed(:helm)
......
......@@ -8,7 +8,7 @@ module QA
end
def name
"qa-test-#{time.strftime('%Y-%m-%d-%Y-%H-%M-%S')}"
"qa-test-#{time.strftime('%Y-%m-%d-%H-%M-%S')}"
end
def path
......
......@@ -15,55 +15,16 @@ describe MetricsController do
allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(metrics_multiproc_dir)
allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(true)
allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip, whitelisted_ip_range])
allow_any_instance_of(MetricsService).to receive(:metrics_text).and_return("prometheus_counter 1")
end
describe '#index' do
shared_examples_for 'endpoint providing metrics' do
it 'returns DB ping metrics' do
it 'returns prometheus metrics' do
get :index
expect(response.body).to match(/^db_ping_timeout 0$/)
expect(response.body).to match(/^db_ping_success 1$/)
expect(response.body).to match(/^db_ping_latency_seconds [0-9\.]+$/)
end
it 'returns Redis ping metrics' do
get :index
expect(response.body).to match(/^redis_ping_timeout 0$/)
expect(response.body).to match(/^redis_ping_success 1$/)
expect(response.body).to match(/^redis_ping_latency_seconds [0-9\.]+$/)
end
it 'returns Caching ping metrics' do
get :index
expect(response.body).to match(/^redis_cache_ping_timeout 0$/)
expect(response.body).to match(/^redis_cache_ping_success 1$/)
expect(response.body).to match(/^redis_cache_ping_latency_seconds [0-9\.]+$/)
end
it 'returns Queues ping metrics' do
get :index
expect(response.body).to match(/^redis_queues_ping_timeout 0$/)
expect(response.body).to match(/^redis_queues_ping_success 1$/)
expect(response.body).to match(/^redis_queues_ping_latency_seconds [0-9\.]+$/)
end
it 'returns SharedState ping metrics' do
get :index
expect(response.body).to match(/^redis_shared_state_ping_timeout 0$/)
expect(response.body).to match(/^redis_shared_state_ping_success 1$/)
expect(response.body).to match(/^redis_shared_state_ping_latency_seconds [0-9\.]+$/)
end
it 'returns Gitaly metrics' do
get :index
expect(response.body).to match(/^gitaly_health_check_success{shard="default"} 1$/)
expect(response.body).to match(/^gitaly_health_check_latency_seconds{shard="default"} [0-9\.]+$/)
expect(response.status).to eq(200)
expect(response.body).to match(/^prometheus_counter 1$/)
end
context 'prometheus metrics are disabled' do
......@@ -101,7 +62,7 @@ describe MetricsController do
allow(Gitlab::RequestContext).to receive(:client_ip).and_return(not_whitelisted_ip)
end
it 'returns proper response' do
it 'returns the expected error response' do
get :index
expect(response.status).to eq(404)
......
......@@ -16,4 +16,36 @@ describe 'Projects > Show > User manages notifications', :js do
expect(page).to have_content 'On mention'
end
end
context 'custom notification settings' do
let(:email_events) do
[
:new_note,
:new_issue,
:reopen_issue,
:close_issue,
:reassign_issue,
:issue_due,
:new_merge_request,
:push_to_merge_request,
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
:merge_merge_request,
:failed_pipeline,
:success_pipeline
]
end
it 'shows notification settings checkbox' do
first('.notifications-btn').click
page.find('a[data-notification-level="custom"]').click
page.within('.custom-notifications-form') do
email_events.each do |event_name|
expect(page).to have_selector("input[name='notification_setting[#{event_name}]']")
end
end
end
end
end
......@@ -205,7 +205,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
actions.setSearchQuery,
{ query: 'test' },
mockedState,
[{ type: types.SET_SEARCH_QUERY }],
[{ type: types.SET_SEARCH_QUERY, payload: { query: 'test' } }],
[{ type: 'fetchSearchedItems', payload: { query: 'test' } }],
done,
);
......@@ -216,7 +216,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
actions.setSearchQuery,
null,
mockedState,
[{ type: types.SET_SEARCH_QUERY }],
[{ type: types.SET_SEARCH_QUERY, payload: null }],
[{ type: 'fetchFrequentItems' }],
done,
);
......
const noop = () => {};
/**
* helper for testing action with expected mutations inspired in
* Helper for testing action with expected mutations inspired in
* https://vuex.vuejs.org/en/testing.html
*
* @param {Function} action to be tested
* @param {Object} payload will be provided to the action
* @param {Object} state will be provided to the action
* @param {Array} [expectedMutations=[]] mutations expected to be committed
* @param {Array} [expectedActions=[]] actions expected to be dispatched
* @param {Function} [done=noop] to be executed after the tests
* @return {Promise}
*
* @example
* testAction(
* actions.actionName, // action
* { }, // mocked response
* state, // state
* { }, // mocked payload
* state, //state
* // expected mutations
* [
* { type: types.MUTATION}
* { type: types.MUTATION_1, payload: {}}
* ], // mutations
* { type: types.MUTATION_1, payload: jasmine.any(Number)}
* ],
* // expected actions
* [
* { type: 'actionName', payload: {}},
* { type: 'actionName1', payload: {}}
* ] //actions
* { type: 'actionName', payload: {param: 'foobar'}},
* { type: 'actionName1'}
* ]
* done,
* );
*
* @example
* testAction(
* actions.actionName, // action
* { }, // mocked payload
* state, //state
* [ { type: types.MUTATION} ], // expected mutations
* [], // expected actions
* ).then(done)
* .catch(done.fail);
*/
export default (action, payload, state, expectedMutations, expectedActions, done) => {
let mutationsCount = 0;
let actionsCount = 0;
export default (
action,
payload,
state,
expectedMutations = [],
expectedActions = [],
done = noop,
) => {
const mutations = [];
const actions = [];
// mock commit
const commit = (type, mutationPayload) => {
const mutation = expectedMutations[mutationsCount];
expect(mutation.type).toEqual(type);
const mutation = { type };
if (mutation.payload) {
expect(mutation.payload).toEqual(mutationPayload);
if (typeof mutationPayload !== 'undefined') {
mutation.payload = mutationPayload;
}
mutationsCount += 1;
if (mutationsCount >= expectedMutations.length) {
done();
}
mutations.push(mutation);
};
// mock dispatch
const dispatch = (type, actionPayload) => {
const actionExpected = expectedActions[actionsCount];
expect(actionExpected.type).toEqual(type);
const dispatchedAction = { type };
if (actionExpected.payload) {
expect(actionExpected.payload).toEqual(actionPayload);
if (typeof actionPayload !== 'undefined') {
dispatchedAction.payload = actionPayload;
}
actionsCount += 1;
if (actionsCount >= expectedActions.length) {
done();
}
actions.push(dispatchedAction);
};
// call the action with mocked store and arguments
action({ commit, state, dispatch, rootState: state }, payload);
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
expect(mutationsCount).toEqual(0);
const validateResults = () => {
expect({
mutations,
actions,
}).toEqual({
mutations: expectedMutations,
actions: expectedActions,
});
done();
}
};
// check if no mutations should have been dispatched
if (expectedActions.length === 0) {
expect(actionsCount).toEqual(0);
done();
}
return new Promise((resolve, reject) => {
try {
const result = action({ commit, state, dispatch, rootState: state }, payload);
resolve(result);
} catch (e) {
reject(e);
}
})
.catch(error => {
validateResults();
throw error;
})
.then(data => {
validateResults();
return data;
});
};
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import testAction from './vuex_action_helper';
describe('VueX test helper (testAction)', () => {
let originalExpect;
let assertion;
let mock;
const noop = () => {};
beforeAll(() => {
mock = new MockAdapter(axios);
/*
In order to test the helper properly, we need to overwrite the jasmine `expect` helper.
We test that the testAction helper properly passes the dispatched actions/committed mutations
to the jasmine helper.
*/
originalExpect = expect;
assertion = null;
global.expect = actual => ({
toEqual: () => {
originalExpect(actual).toEqual(assertion);
},
});
});
afterAll(() => {
mock.restore();
global.expect = originalExpect;
});
it('should properly pass on state and payload', () => {
const exampleState = { FOO: 12, BAR: 3 };
const examplePayload = { BAZ: 73, BIZ: 55 };
const action = ({ state }, payload) => {
originalExpect(state).toEqual(exampleState);
originalExpect(payload).toEqual(examplePayload);
};
assertion = { mutations: [], actions: [] };
testAction(action, examplePayload, exampleState);
});
describe('should work with synchronous actions', () => {
it('committing mutation', () => {
const action = ({ commit }) => {
commit('MUTATION');
};
assertion = { mutations: [{ type: 'MUTATION' }], actions: [] };
testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
});
it('dispatching action', () => {
const action = ({ dispatch }) => {
dispatch('ACTION');
};
assertion = { actions: [{ type: 'ACTION' }], mutations: [] };
testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
});
it('work with jasmine done once finished', done => {
assertion = { mutations: [], actions: [] };
testAction(noop, null, {}, assertion.mutations, assertion.actions, done);
});
it('provide promise interface', done => {
assertion = { mutations: [], actions: [] };
testAction(noop, null, {}, assertion.mutations, assertion.actions)
.then(done)
.catch(done.fail);
});
});
describe('should work with promise based actions (fetch action)', () => {
let lastError;
const data = { FOO: 'BAR' };
const promiseAction = ({ commit, dispatch }) => {
dispatch('ACTION');
return axios
.get(TEST_HOST)
.catch(error => {
commit('ERROR');
lastError = error;
throw error;
})
.then(() => {
commit('SUCCESS');
return data;
});
};
beforeEach(() => {
lastError = null;
});
it('work with jasmine done once finished', done => {
mock.onGet(TEST_HOST).replyOnce(200, 42);
assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
testAction(promiseAction, null, {}, assertion.mutations, assertion.actions, done);
});
it('return original data of successful promise while checking actions/mutations', done => {
mock.onGet(TEST_HOST).replyOnce(200, 42);
assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
testAction(promiseAction, null, {}, assertion.mutations, assertion.actions)
.then(res => {
originalExpect(res).toEqual(data);
done();
})
.catch(done.fail);
});
it('return original error of rejected promise while checking actions/mutations', done => {
mock.onGet(TEST_HOST).replyOnce(500, '');
assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] };
testAction(promiseAction, null, {}, assertion.mutations, assertion.actions)
.then(done.fail)
.catch(error => {
originalExpect(error).toBe(lastError);
done();
});
});
});
});
......@@ -601,10 +601,7 @@ describe('IDE store file actions', () => {
actions.unstageChange,
'path',
store.state,
[
{ type: types.UNSTAGE_CHANGE, payload: 'path' },
{ type: types.SET_LAST_COMMIT_MSG, payload: '' },
],
[{ type: types.UNSTAGE_CHANGE, payload: 'path' }],
[],
done,
);
......
......@@ -73,6 +73,7 @@ describe('IDE store project actions', () => {
branchId: store.state.currentBranchId,
},
store.state,
// mutations
[
{
type: 'SET_BRANCH_COMMIT',
......@@ -82,17 +83,9 @@ describe('IDE store project actions', () => {
commit: { id: '123' },
},
},
], // mutations
[
{
type: 'getLastCommitPipeline',
payload: {
projectId: 'abc/def',
projectIdNumber: store.state.projects['abc/def'].id,
branchId: 'master',
},
},
], // action
],
// action
[],
done,
);
});
......
......@@ -192,11 +192,8 @@ describe('Multi-file store tree actions', () => {
showTreeEntry,
'grandparent/parent/child.txt',
store.state,
[
{ type: types.SET_TREE_OPEN, payload: 'grandparent/parent' },
{ type: types.SET_TREE_OPEN, payload: 'grandparent' },
],
[{ type: 'showTreeEntry' }],
[{ type: types.SET_TREE_OPEN, payload: 'grandparent/parent' }],
[{ type: 'showTreeEntry', payload: 'grandparent/parent' }],
done,
);
});
......
......@@ -122,21 +122,6 @@ describe('IDE merge requests actions', () => {
});
});
it('dispatches request', done => {
testAction(
fetchMergeRequests,
{ type: 'created' },
mockedState,
[],
[
{ type: 'requestMergeRequests' },
{ type: 'resetMergeRequests' },
{ type: 'receiveMergeRequestsSuccess' },
],
done,
);
});
it('dispatches success with received data', done => {
testAction(
fetchMergeRequests,
......@@ -144,8 +129,8 @@ describe('IDE merge requests actions', () => {
mockedState,
[],
[
{ type: 'requestMergeRequests' },
{ type: 'resetMergeRequests' },
{ type: 'requestMergeRequests', payload: 'created' },
{ type: 'resetMergeRequests', payload: 'created' },
{
type: 'receiveMergeRequestsSuccess',
payload: { type: 'created', data: mergeRequests },
......@@ -168,9 +153,9 @@ describe('IDE merge requests actions', () => {
mockedState,
[],
[
{ type: 'requestMergeRequests' },
{ type: 'resetMergeRequests' },
{ type: 'receiveMergeRequestsError' },
{ type: 'requestMergeRequests', payload: 'created' },
{ type: 'resetMergeRequests', payload: 'created' },
{ type: 'receiveMergeRequestsError', payload: { type: 'created', search: '' } },
],
done,
);
......
......@@ -315,7 +315,7 @@ describe('IDE pipelines actions', () => {
'job',
mockedState,
[{ type: types.SET_DETAIL_JOB, payload: 'job' }],
[{ type: 'setRightPane' }],
[{ type: 'setRightPane', payload: 'jobs-detail' }],
done,
);
});
......@@ -325,7 +325,7 @@ describe('IDE pipelines actions', () => {
setDetailJob,
null,
mockedState,
[{ type: types.SET_DETAIL_JOB }],
[{ type: types.SET_DETAIL_JOB, payload: null }],
[{ type: 'setRightPane', payload: rightSidebarViews.pipelines }],
done,
);
......@@ -336,7 +336,7 @@ describe('IDE pipelines actions', () => {
setDetailJob,
'job',
mockedState,
[{ type: types.SET_DETAIL_JOB }],
[{ type: types.SET_DETAIL_JOB, payload: 'job' }],
[{ type: 'setRightPane', payload: rightSidebarViews.jobsDetail }],
done,
);
......
......@@ -166,13 +166,13 @@ if (process.env.BABEL_ENV === 'coverage') {
];
describe('Uncovered files', function() {
const sourceFiles = require.context('~', true, /\.js$/);
const sourceFiles = require.context('~', true, /\.(js|vue)$/);
$.holdReady(true);
sourceFiles.keys().forEach(function(path) {
// ignore if there is a matching spec file
if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
if (testsContext.keys().indexOf(`${path.replace(/\.(js|vue)$/, '')}_spec`) > -1) {
return;
}
......
......@@ -93,4 +93,10 @@ RSpec.describe NotificationSetting do
end
end
end
context 'email events' do
it 'includes EXCLUDED_WATCHER_EVENTS in EMAIL_EVENTS' do
expect(described_class::EMAIL_EVENTS).to include(*described_class::EXCLUDED_WATCHER_EVENTS)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment