Commit 3fe34368 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 41d446ba
...@@ -81,3 +81,20 @@ export const lineChartOptions = ({ width, numberOfPoints, shouldAdjustFontSize } ...@@ -81,3 +81,20 @@ export const lineChartOptions = ({ width, numberOfPoints, shouldAdjustFontSize }
}, },
}, },
}); });
/**
* Takes a dataset and returns an array containing the y-values of it's first and last entry.
* (e.g., [['xValue1', 'yValue1'], ['xValue2', 'yValue2'], ['xValue3', 'yValue3']] will yield ['yValue1', 'yValue3'])
*
* @param {Array} data
* @returns {[*, *]}
*/
export const firstAndLastY = data => {
const [firstEntry] = data;
const [lastEntry] = data.slice(-1);
const firstY = firstEntry[1];
const lastY = lastEntry[1];
return [firstY, lastY];
};
...@@ -117,3 +117,36 @@ export const median = arr => { ...@@ -117,3 +117,36 @@ export const median = arr => {
const sorted = arr.sort((a, b) => a - b); const sorted = arr.sort((a, b) => a - b);
return arr.length % 2 !== 0 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2; return arr.length % 2 !== 0 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
}; };
/**
* Computes the change from one value to the other as a percentage.
* @param {Number} firstY
* @param {Number} lastY
* @returns {Number}
*/
export const changeInPercent = (firstY, lastY) => {
if (firstY === lastY) {
return 0;
}
return Math.round(((lastY - firstY) / Math.abs(firstY)) * 100);
};
/**
* Computes and formats the change from one value to the other as a percentage.
* Prepends the computed percentage with either "+" or "-" to indicate an in- or decrease and
* returns a given string if the result is not finite (for example, if the first value is "0").
* @param firstY
* @param lastY
* @param nonFiniteResult
* @returns {String}
*/
export const formattedChangeInPercent = (firstY, lastY, { nonFiniteResult = '-' } = {}) => {
const change = changeInPercent(firstY, lastY);
if (!Number.isFinite(change)) {
return nonFiniteResult;
}
return `${change >= 0 ? '+' : ''}${change}%`;
};
...@@ -265,7 +265,11 @@ export default { ...@@ -265,7 +265,11 @@ export default {
<div class="table-section section-10 commit-link"> <div class="table-section section-10 commit-link">
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Status') }}</div> <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Status') }}</div>
<div class="table-mobile-content"> <div class="table-mobile-content">
<ci-badge :status="pipelineStatus" :show-text="!isChildView" /> <ci-badge
:status="pipelineStatus"
:show-text="!isChildView"
data-qa-selector="pipeline_commit_status"
/>
</div> </div>
</div> </div>
......
...@@ -562,6 +562,8 @@ img.emoji { ...@@ -562,6 +562,8 @@ img.emoji {
} }
.gl-font-size-small { font-size: $gl-font-size-small; } .gl-font-size-small { font-size: $gl-font-size-small; }
.gl-font-size-large { font-size: $gl-font-size-large; }
.gl-line-height-24 { line-height: $gl-line-height-24; } .gl-line-height-24 { line-height: $gl-line-height-24; }
.gl-font-size-12 { font-size: $gl-font-size-12; } .gl-font-size-12 { font-size: $gl-font-size-12; }
......
...@@ -110,7 +110,10 @@ class ProjectsFinder < UnionFinder ...@@ -110,7 +110,10 @@ class ProjectsFinder < UnionFinder
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def by_ids(items) def by_ids(items)
project_ids_relation ? items.where(id: project_ids_relation) : items items = items.where(id: project_ids_relation) if project_ids_relation
items = items.where('id > ?', params[:id_after]) if params[:id_after]
items = items.where('id < ?', params[:id_before]) if params[:id_before]
items
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
# frozen_string_literal: true
module Mutations
module MergeRequests
class SetLabels < Base
graphql_name 'MergeRequestSetLabels'
argument :label_ids,
[GraphQL::ID_TYPE],
required: true,
description: <<~DESC
The Label IDs to set. Replaces existing labels by default.
DESC
argument :operation_mode,
Types::MutationOperationModeEnum,
required: false,
description: <<~DESC
Changes the operation mode. Defaults to REPLACE.
DESC
def resolve(project_path:, iid:, label_ids:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
label_ids = label_ids
.select(&method(:label_descendant?))
.map { |gid| GlobalID.parse(gid).model_id } # MergeRequests::UpdateService expects integers
attribute_name = case operation_mode
when Types::MutationOperationModeEnum.enum[:append]
:add_label_ids
when Types::MutationOperationModeEnum.enum[:remove]
:remove_label_ids
else
:label_ids
end
::MergeRequests::UpdateService.new(project, current_user, attribute_name => label_ids)
.execute(merge_request)
{
merge_request: merge_request,
errors: merge_request.errors.full_messages
}
end
def label_descendant?(gid)
GlobalID.parse(gid)&.model_class&.ancestors&.include?(Label)
end
end
end
end
# frozen_string_literal: true
module Mutations
module MergeRequests
class SetLocked < Base
graphql_name 'MergeRequestSetLocked'
argument :locked,
GraphQL::BOOLEAN_TYPE,
required: true,
description: <<~DESC
Whether or not to lock the merge request.
DESC
def resolve(project_path:, iid:, locked:)
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
::MergeRequests::UpdateService.new(project, current_user, discussion_locked: locked)
.execute(merge_request)
{
merge_request: merge_request,
errors: merge_request.errors.full_messages
}
end
end
end
end
# frozen_string_literal: true
module Mutations
module MergeRequests
class SetSubscription < Base
graphql_name 'MergeRequestSetSubscription'
argument :subscribed_state,
GraphQL::BOOLEAN_TYPE,
required: true,
description: 'The desired state of the subscription'
def resolve(project_path:, iid:, subscribed_state:)
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
merge_request.set_subscription(current_user, subscribed_state, project)
{
merge_request: merge_request,
errors: merge_request.errors.full_messages
}
end
end
end
end
...@@ -6,6 +6,8 @@ module Types ...@@ -6,6 +6,8 @@ module Types
authorize :read_label authorize :read_label
field :id, GraphQL::ID_TYPE, null: false,
description: 'Label ID'
field :description, GraphQL::STRING_TYPE, null: true, field :description, GraphQL::STRING_TYPE, null: true,
description: 'Description of the label (markdown rendered as HTML for caching)' description: 'Description of the label (markdown rendered as HTML for caching)'
markdown_field :description_html, null: true markdown_field :description_html, null: true
......
...@@ -9,7 +9,10 @@ module Types ...@@ -9,7 +9,10 @@ module Types
mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::MergeRequests::SetLabels
mount_mutation Mutations::MergeRequests::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone mount_mutation Mutations::MergeRequests::SetMilestone
mount_mutation Mutations::MergeRequests::SetSubscription
mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.container.section-body .container.section-body
.row .row
.blank-state-welcome.w-100 .blank-state-welcome.w-100
%h2.blank-state-welcome-title %h2.blank-state-welcome-title{ data: { qa_selector: 'welcome_title_content' } }
= _('Welcome to GitLab') = _('Welcome to GitLab')
%p.blank-state-text %p.blank-state-text
= _('Faster releases. Better code. Less pain.') = _('Faster releases. Better code. Less pain.')
......
---
title: Vulnerabilities history chart - use sparklines
merge_request: 19745
author:
type: changed
---
title: 'GraphQL: Create MR mutations needed for the sidebar'
merge_request: 19913
author:
type: added
---
title: Add id_before, id_after filter param to projects API
merge_request: 19949
author:
type: added
---
title: Change the default concurrency factor of merge train to 20
merge_request: 20201
author:
type: changed
...@@ -2814,6 +2814,11 @@ type Label { ...@@ -2814,6 +2814,11 @@ type Label {
""" """
descriptionHtml: String descriptionHtml: String
"""
Label ID
"""
id: ID!
""" """
Text color of the label Text color of the label
""" """
...@@ -3407,6 +3412,101 @@ type MergeRequestSetAssigneesPayload { ...@@ -3407,6 +3412,101 @@ type MergeRequestSetAssigneesPayload {
mergeRequest: MergeRequest mergeRequest: MergeRequest
} }
"""
Autogenerated input type of MergeRequestSetLabels
"""
input MergeRequestSetLabelsInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the merge request to mutate
"""
iid: String!
"""
The Label IDs to set. Replaces existing labels by default.
"""
labelIds: [ID!]!
"""
Changes the operation mode. Defaults to REPLACE.
"""
operationMode: MutationOperationMode
"""
The project the merge request to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of MergeRequestSetLabels
"""
type MergeRequestSetLabelsPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The merge request after mutation
"""
mergeRequest: MergeRequest
}
"""
Autogenerated input type of MergeRequestSetLocked
"""
input MergeRequestSetLockedInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the merge request to mutate
"""
iid: String!
"""
Whether or not to lock the merge request.
"""
locked: Boolean!
"""
The project the merge request to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of MergeRequestSetLocked
"""
type MergeRequestSetLockedPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The merge request after mutation
"""
mergeRequest: MergeRequest
}
""" """
Autogenerated input type of MergeRequestSetMilestone Autogenerated input type of MergeRequestSetMilestone
""" """
...@@ -3452,6 +3552,51 @@ type MergeRequestSetMilestonePayload { ...@@ -3452,6 +3552,51 @@ type MergeRequestSetMilestonePayload {
mergeRequest: MergeRequest mergeRequest: MergeRequest
} }
"""
Autogenerated input type of MergeRequestSetSubscription
"""
input MergeRequestSetSubscriptionInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the merge request to mutate
"""
iid: String!
"""
The project the merge request to mutate is in
"""
projectPath: ID!
"""
The desired state of the subscription
"""
subscribedState: Boolean!
}
"""
Autogenerated return type of MergeRequestSetSubscription
"""
type MergeRequestSetSubscriptionPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The merge request after mutation
"""
mergeRequest: MergeRequest
}
""" """
Autogenerated input type of MergeRequestSetWip Autogenerated input type of MergeRequestSetWip
""" """
...@@ -3588,7 +3733,10 @@ type Mutation { ...@@ -3588,7 +3733,10 @@ type Mutation {
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload
mergeRequestSetLocked(input: MergeRequestSetLockedInput!): MergeRequestSetLockedPayload
mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload
mergeRequestSetSubscription(input: MergeRequestSetSubscriptionInput!): MergeRequestSetSubscriptionPayload
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
......
...@@ -410,6 +410,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -410,6 +410,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `id` | ID! | Label ID |
| `description` | String | Description of the label (markdown rendered as HTML for caching) | | `description` | String | Description of the label (markdown rendered as HTML for caching) |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `title` | String! | Content of the label | | `title` | String! | Content of the label |
...@@ -491,6 +492,22 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -491,6 +492,22 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation | | `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetLabelsPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetLockedPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetMilestonePayload ### MergeRequestSetMilestonePayload
| Name | Type | Description | | Name | Type | Description |
...@@ -499,6 +516,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -499,6 +516,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation | | `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetSubscriptionPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetWipPayload ### MergeRequestSetWipPayload
| Name | Type | Description | | Name | Type | Description |
......
...@@ -58,6 +58,8 @@ GET /projects ...@@ -58,6 +58,8 @@ GET /projects
| `wiki_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the wiki checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | | `wiki_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the wiki checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) |
| `repository_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | | `repository_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) |
| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) | | `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) |
| `id_after` | integer | no | Limit results to projects with IDs greater than the specified ID |
| `id_before` | integer | no | Limit results to projects with IDs less than the specified ID |
When `simple=true` or the user is unauthenticated this returns something like: When `simple=true` or the user is unauthenticated this returns something like:
...@@ -304,6 +306,8 @@ GET /users/:user_id/projects ...@@ -304,6 +306,8 @@ GET /users/:user_id/projects
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature | | `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
| `with_programming_language` | string | no | Limit by projects which use the given programming language | | `with_programming_language` | string | no | Limit by projects which use the given programming language |
| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) | | `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) |
| `id_after` | integer | no | Limit results to projects with IDs greater than the specified ID |
| `id_before` | integer | no | Limit results to projects with IDs less than the specified ID |
```json ```json
[ [
......
...@@ -32,8 +32,8 @@ Merge trains have the following requirements and limitations: ...@@ -32,8 +32,8 @@ Merge trains have the following requirements and limitations:
- This feature requires that - This feature requires that
[pipelines for merged results](../index.md#pipelines-for-merged-results-premium) are [pipelines for merged results](../index.md#pipelines-for-merged-results-premium) are
**configured properly**. **configured properly**.
- Each merge train can run a maximum of **four** pipelines in parallel. - Each merge train can run a maximum of **twenty** pipelines in parallel.
If more than four merge requests are added to the merge train, the merge requests If more than twenty merge requests are added to the merge train, the merge requests
will be queued until a slot in the merge train is free. There is no limit to the will be queued until a slot in the merge train is free. There is no limit to the
number of merge requests that can be queued. number of merge requests that can be queued.
- This feature does not support [squash and merge](../../../../user/project/merge_requests/squash_and_merge.md). - This feature does not support [squash and merge](../../../../user/project/merge_requests/squash_and_merge.md).
......
...@@ -76,7 +76,7 @@ To the right of the filters, you should see a **Hide dismissed** toggle button ( ...@@ -76,7 +76,7 @@ To the right of the filters, you should see a **Hide dismissed** toggle button (
NOTE: **Note:** NOTE: **Note:**
The dashboard only shows projects with [security reports](#supported-reports) enabled in a group. The dashboard only shows projects with [security reports](#supported-reports) enabled in a group.
![dashboard with action buttons and metrics](img/group_security_dashboard_v12_3.png) ![dashboard with action buttons and metrics](img/group_security_dashboard_v12_4.png)
Selecting one or more filters will filter the results in this page. Disabling the **Hide dismissed** Selecting one or more filters will filter the results in this page. Disabling the **Hide dismissed**
toggle button will let you also see vulnerabilities that have been dismissed. toggle button will let you also see vulnerabilities that have been dismissed.
......
...@@ -479,6 +479,8 @@ module API ...@@ -479,6 +479,8 @@ module API
finder_params[:user] = params.delete(:user) if params[:user] finder_params[:user] = params.delete(:user) if params[:user]
finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes] finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level] finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level]
finder_params[:id_after] = params[:id_after] if params[:id_after]
finder_params[:id_before] = params[:id_before] if params[:id_before]
finder_params finder_params
end end
......
...@@ -61,6 +61,8 @@ module API ...@@ -61,6 +61,8 @@ module API
optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature' optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature'
optional :with_programming_language, type: String, desc: 'Limit to repositories which use the given programming language' optional :with_programming_language, type: String, desc: 'Limit to repositories which use the given programming language'
optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user' optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user'
optional :id_after, type: Integer, desc: 'Limit results to projects with IDs greater than the specified ID'
optional :id_before, type: Integer, desc: 'Limit results to projects with IDs less than the specified ID'
use :optional_filter_params_ee use :optional_filter_params_ee
end end
......
...@@ -4994,6 +4994,9 @@ msgstr "" ...@@ -4994,6 +4994,9 @@ msgstr ""
msgid "Current password" msgid "Current password"
msgstr "" msgstr ""
msgid "Current vulnerabilities count"
msgstr ""
msgid "CurrentUser|Profile" msgid "CurrentUser|Profile"
msgstr "" msgstr ""
...@@ -5791,6 +5794,9 @@ msgstr "" ...@@ -5791,6 +5794,9 @@ msgstr ""
msgid "Diff limits" msgid "Diff limits"
msgstr "" msgstr ""
msgid "Difference between start date and now"
msgstr ""
msgid "DiffsCompareBaseBranch|(base)" msgid "DiffsCompareBaseBranch|(base)"
msgstr "" msgstr ""
...@@ -19180,6 +19186,9 @@ msgstr "" ...@@ -19180,6 +19186,9 @@ msgstr ""
msgid "VulnerabilityChart|%{formattedStartDate} to today" msgid "VulnerabilityChart|%{formattedStartDate} to today"
msgstr "" msgstr ""
msgid "VulnerabilityChart|Severity"
msgstr ""
msgid "Vulnerability|Class" msgid "Vulnerability|Class"
msgstr "" msgstr ""
......
...@@ -165,6 +165,7 @@ module QA ...@@ -165,6 +165,7 @@ module QA
module Dashboard module Dashboard
autoload :Projects, 'qa/page/dashboard/projects' autoload :Projects, 'qa/page/dashboard/projects'
autoload :Groups, 'qa/page/dashboard/groups' autoload :Groups, 'qa/page/dashboard/groups'
autoload :Welcome, 'qa/page/dashboard/welcome'
module Snippet module Snippet
autoload :New, 'qa/page/dashboard/snippet/new' autoload :New, 'qa/page/dashboard/snippet/new'
......
...@@ -135,6 +135,40 @@ module QA ...@@ -135,6 +135,40 @@ module QA
has_no_css?('.fa-spinner.block-loading', wait: Capybara.default_max_wait_time) has_no_css?('.fa-spinner.block-loading', wait: Capybara.default_max_wait_time)
end end
def has_loaded_all_images?
# I don't know of a foolproof way to wait for all images to load
# This loop gives time for the img tags to be rendered and for
# images to start loading.
previous_total_images = 0
wait(interval: 1) do
current_total_images = all("img").size
result = previous_total_images == current_total_images
previous_total_images = current_total_images
result
end
# Retry until all images found can be fetched via HTTP, and
# check that the image has a non-zero natural width (a broken
# img tag could have a width, but wouldn't have a natural width)
# Unfortunately, this doesn't account for SVGs. They're rendered
# as HTML, so there doesn't seem to be a way to check that they
# display properly via Selenium. However, if the SVG couldn't be
# rendered (e.g., because the file doesn't exist), the whole page
# won't display properly, so we should catch that with the test
# this method is called from.
# The user's avatar is an img, which could be a gravatar image,
# so we skip that by only checking for images hosted internally
retry_until(sleep_interval: 1) do
all("img").all? do |image|
next true unless URI(image['src']).host == URI(page.current_url).host
asset_exists?(image['src']) && image['naturalWidth'].to_i > 0
end
end
end
def wait_for_animated_element(name) def wait_for_animated_element(name)
# It would be ideal if we could detect when the animation is complete # It would be ideal if we could detect when the animation is complete
# but in some cases there's nothing we can easily access via capybara # but in some cases there's nothing we can easily access via capybara
......
# frozen_string_literal: true
module QA
module Page
module Dashboard
class Welcome < Page::Base
view 'app/views/dashboard/projects/_zero_authorized_projects.html.haml' do
element :welcome_title_content
end
def has_welcome_title?(text)
has_element?(:welcome_title_content, text: text)
end
end
end
end
end
...@@ -7,6 +7,10 @@ module QA::Page ...@@ -7,6 +7,10 @@ module QA::Page
element :pipeline_link, 'class="js-pipeline-url-link' # rubocop:disable QA/ElementWithPattern element :pipeline_link, 'class="js-pipeline-url-link' # rubocop:disable QA/ElementWithPattern
end end
view 'app/assets/javascripts/pipelines/components/pipelines_table_row.vue' do
element :pipeline_commit_status
end
def click_on_latest_pipeline def click_on_latest_pipeline
css = '.js-pipeline-url-link' css = '.js-pipeline-url-link'
...@@ -16,6 +20,14 @@ module QA::Page ...@@ -16,6 +20,14 @@ module QA::Page
link.click link.click
end end
def wait_for_latest_pipeline_success
wait(reload: false, max: 300) do
within_element_by_index(:pipeline_commit_status, 0) do
has_text?('passed')
end
end
end
end end
end end
end end
...@@ -7,13 +7,14 @@ module QA ...@@ -7,13 +7,14 @@ module QA
class User < Base class User < Base
attr_reader :unique_id attr_reader :unique_id
attr_writer :username, :password attr_writer :username, :password
attr_accessor :provider, :extern_uid attr_accessor :admin, :provider, :extern_uid
attribute :id attribute :id
attribute :name attribute :name
attribute :email attribute :email
def initialize def initialize
@admin = false
@unique_id = SecureRandom.hex(8) @unique_id = SecureRandom.hex(8)
end end
...@@ -75,6 +76,16 @@ module QA ...@@ -75,6 +76,16 @@ module QA
super super
end end
def api_delete
super
QA::Runtime::Logger.debug("Deleted user '#{username}'") if Runtime::Env.debug?
end
def api_delete_path
"/users/#{id}"
end
def api_get_path def api_get_path
"/users/#{fetch_id(username)}" "/users/#{fetch_id(username)}"
end end
...@@ -85,6 +96,7 @@ module QA ...@@ -85,6 +96,7 @@ module QA
def api_post_body def api_post_body
{ {
admin: admin,
email: email, email: email,
password: password, password: password,
username: username, username: username,
......
# frozen_string_literal: true
require 'nokogiri'
module QA
context 'Manage' do
describe 'Check for broken images', :requires_admin do
before(:context) do
admin = QA::Resource::User.new.tap do |user|
user.username = QA::Runtime::User.admin_username
user.password = QA::Runtime::User.admin_password
end
@api_client = Runtime::API::Client.new(:gitlab, user: admin)
@new_user = Resource::User.fabricate_via_api! do |user|
user.api_client = @api_client
end
@new_admin = Resource::User.fabricate_via_api! do |user|
user.admin = true
user.api_client = @api_client
end
Page::Main::Menu.perform(&:sign_out_if_signed_in)
end
after(:context) do
@new_user.remove_via_api!
@new_admin.remove_via_api!
end
shared_examples 'loads all images' do
it 'loads all images' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: new_user) }
Page::Dashboard::Welcome.perform do |welcome|
expect(welcome).to have_welcome_title("Welcome to GitLab")
# This would be better if it were a visual validation test
expect(welcome).to have_loaded_all_images
end
end
end
context 'when logged in as a new user' do
it_behaves_like 'loads all images' do
let(:new_user) { @new_user }
end
end
context 'when logged in as a new admin' do
it_behaves_like 'loads all images' do
let(:new_user) { @new_admin }
end
end
end
end
end
...@@ -58,6 +58,31 @@ describe ProjectsFinder, :do_not_mock_admin_mode do ...@@ -58,6 +58,31 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to eq([internal_project]) } it { is_expected.to eq([internal_project]) }
end end
describe 'with id_after' do
context 'only returns projects with a project id greater than given' do
let(:params) { { id_after: internal_project.id }}
it { is_expected.to eq([public_project]) }
end
end
describe 'with id_before' do
context 'only returns projects with a project id less than given' do
let(:params) { { id_before: public_project.id }}
it { is_expected.to eq([internal_project]) }
end
end
describe 'with both id_before and id_after' do
context 'only returns projects with a project id less than given' do
let!(:projects) { create_list(:project, 5, :public) }
let(:params) { { id_after: projects.first.id, id_before: projects.last.id }}
it { is_expected.to contain_exactly(*projects[1..-2]) }
end
end
describe 'filter by visibility_level' do describe 'filter by visibility_level' do
before do before do
private_project.add_maintainer(user) private_project.add_maintainer(user)
......
import { firstAndLastY } from '~/lib/utils/chart_utils';
describe('Chart utils', () => {
describe('firstAndLastY', () => {
it('returns the first and last y-values of a given data set as an array', () => {
const data = [['', 1], ['', 2], ['', 3]];
expect(firstAndLastY(data)).toEqual([1, 3]);
});
});
});
...@@ -7,6 +7,8 @@ import { ...@@ -7,6 +7,8 @@ import {
sum, sum,
isOdd, isOdd,
median, median,
changeInPercent,
formattedChangeInPercent,
} from '~/lib/utils/number_utils'; } from '~/lib/utils/number_utils';
describe('Number Utils', () => { describe('Number Utils', () => {
...@@ -122,4 +124,42 @@ describe('Number Utils', () => { ...@@ -122,4 +124,42 @@ describe('Number Utils', () => {
expect(median(items)).toBe(14.5); expect(median(items)).toBe(14.5);
}); });
}); });
describe('changeInPercent', () => {
it.each`
firstValue | secondValue | expectedOutput
${99} | ${100} | ${1}
${100} | ${99} | ${-1}
${0} | ${99} | ${Infinity}
${2} | ${2} | ${0}
${-100} | ${-99} | ${1}
`(
'computes the change between $firstValue and $secondValue in percent',
({ firstValue, secondValue, expectedOutput }) => {
expect(changeInPercent(firstValue, secondValue)).toBe(expectedOutput);
},
);
});
describe('formattedChangeInPercent', () => {
it('prepends "%" to the output', () => {
expect(formattedChangeInPercent(1, 2)).toMatch(/%$/);
});
it('indicates if the change was a decrease', () => {
expect(formattedChangeInPercent(100, 99)).toContain('-1');
});
it('indicates if the change was an increase', () => {
expect(formattedChangeInPercent(99, 100)).toContain('+1');
});
it('shows "-" per default if the change can not be expressed in an integer', () => {
expect(formattedChangeInPercent(0, 1)).toBe('-');
});
it('shows the given fallback if the change can not be expressed in an integer', () => {
expect(formattedChangeInPercent(0, 1, { nonFiniteResult: '*' })).toBe('*');
});
});
}); });
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::MergeRequests::SetLabels do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
describe '#resolve' do
let(:label) { create(:label, project: merge_request.project) }
let(:label2) { create(:label, project: merge_request.project) }
let(:label_ids) { [label.to_global_id] }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, label_ids: label_ids) }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can update the merge request' do
before do
merge_request.project.add_developer(user)
end
it 'sets the labels, removing all others' do
merge_request.update!(labels: [label2])
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.labels).to contain_exactly(label)
expect(subject[:errors]).to be_empty
end
it 'returns errors merge request could not be updated' do
# Make the merge request invalid
merge_request.allow_broken = true
merge_request.update!(source_project: nil)
expect(subject[:errors]).not_to be_empty
end
context 'when passing an empty array' do
let(:label_ids) { [] }
it 'removes all labels' do
merge_request.update!(labels: [label])
expect(mutated_merge_request.labels).to be_empty
end
end
context 'when passing operation_mode as APPEND' do
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, label_ids: label_ids, operation_mode: Types::MutationOperationModeEnum.enum[:append]) }
it 'sets the labels, without removing others' do
merge_request.update!(labels: [label2])
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.labels).to contain_exactly(label, label2)
expect(subject[:errors]).to be_empty
end
end
context 'when passing operation_mode as REMOVE' do
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, label_ids: label_ids, operation_mode: Types::MutationOperationModeEnum.enum[:remove])}
it 'removes the labels, without removing others' do
merge_request.update!(labels: [label, label2])
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.labels).to contain_exactly(label2)
expect(subject[:errors]).to be_empty
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::MergeRequests::SetLocked do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
describe '#resolve' do
let(:locked) { true }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, locked: locked) }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can update the merge request' do
before do
merge_request.project.add_developer(user)
end
it 'returns the merge request as discussion locked' do
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request).to be_discussion_locked
expect(subject[:errors]).to be_empty
end
it 'returns errors merge request could not be updated' do
# Make the merge request invalid
merge_request.allow_broken = true
merge_request.update!(source_project: nil)
expect(subject[:errors]).not_to be_empty
end
context 'when passing locked as false' do
let(:locked) { false }
it 'unlocks the discussion' do
merge_request.update(discussion_locked: true)
expect(mutated_merge_request).not_to be_discussion_locked
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::MergeRequests::SetSubscription do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
describe '#resolve' do
let(:subscribe) { true }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, subscribed_state: subscribe) }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can update the merge request' do
before do
merge_request.project.add_developer(user)
end
it 'returns the merge request as discussion locked' do
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.subscribed?(user, project)).to eq(true)
expect(subject[:errors]).to be_empty
end
context 'when passing subscribe as false' do
let(:subscribe) { false }
it 'unsubscribes from the discussion' do
merge_request.subscribe(user, project)
expect(mutated_merge_request.subscribed?(user, project)).to eq(false)
end
end
end
end
end
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe GitlabSchema.types['Label'] do describe GitlabSchema.types['Label'] do
it 'has the correct fields' do it 'has the correct fields' do
expected_fields = [:description, :description_html, :title, :color, :text_color] expected_fields = [:id, :description, :description_html, :title, :color, :text_color]
is_expected.to have_graphql_fields(*expected_fields) is_expected.to have_graphql_fields(*expected_fields)
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Setting labels of a merge request' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:label) { create(:label, project: project) }
let(:label2) { create(:label, project: project) }
let(:input) { { label_ids: [GitlabSchema.id_from_object(label).to_s] } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_labels, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
mergeRequest {
id
labels {
nodes {
id
}
}
}
QL
)
end
def mutation_response
graphql_mutation_response(:merge_request_set_labels)
end
def mutation_label_nodes
mutation_response['mergeRequest']['labels']['nodes']
end
before do
project.add_developer(current_user)
end
it 'returns an error if the user is not allowed to update the merge request' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'sets the merge request labels, removing existing ones' do
merge_request.update(labels: [label2])
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_label_nodes.count).to eq(1)
expect(mutation_label_nodes[0]['id']).to eq(label.to_global_id.to_s)
end
context 'when passing label_ids empty array as input' do
let(:input) { { label_ids: [] } }
it 'removes the merge request labels' do
merge_request.update!(labels: [label])
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_label_nodes.count).to eq(0)
end
end
context 'when passing operation_mode as APPEND' do
let(:input) { { operation_mode: Types::MutationOperationModeEnum.enum[:append], label_ids: [GitlabSchema.id_from_object(label).to_s] } }
before do
merge_request.update!(labels: [label2])
end
it 'sets the labels, without removing others' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_label_nodes.count).to eq(2)
expect(mutation_label_nodes).to contain_exactly({ 'id' => label.to_global_id.to_s }, { 'id' => label2.to_global_id.to_s })
end
end
context 'when passing operation_mode as REMOVE' do
let(:input) { { operation_mode: Types::MutationOperationModeEnum.enum[:remove], label_ids: [GitlabSchema.id_from_object(label).to_s] } }
before do
merge_request.update!(labels: [label, label2])
end
it 'removes the labels, without removing others' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_label_nodes.count).to eq(1)
expect(mutation_label_nodes[0]['id']).to eq(label2.to_global_id.to_s)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Setting locked status of a merge request' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:input) { { locked: true } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_locked, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
mergeRequest {
id
discussionLocked
}
QL
)
end
def mutation_response
graphql_mutation_response(:merge_request_set_locked)['mergeRequest']['discussionLocked']
end
before do
project.add_developer(current_user)
end
it 'returns an error if the user is not allowed to update the merge request' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'marks the merge request as WIP' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(true)
end
it 'does not do anything if the merge request was already locked' do
merge_request.update!(discussion_locked: true)
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(true)
end
context 'when passing locked false as input' do
let(:input) { { locked: false } }
it 'does not do anything if the merge request was not marked locked' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(false)
end
it 'unmarks the merge request as locked' do
merge_request.update!(discussion_locked: true)
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(false)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Setting subscribed status of a merge request' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:input) { { subscribed_state: true } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_subscription, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
mergeRequest {
id
subscribed
}
QL
)
end
def mutation_response
graphql_mutation_response(:merge_request_set_subscription)['mergeRequest']['subscribed']
end
before do
project.add_developer(current_user)
end
it 'returns an error if the user is not allowed to update the merge request' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'marks the merge request as WIP' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(true)
end
context 'when passing subscribe false as input' do
let(:input) { { subscribed_state: false } }
it 'unmarks the merge request as subscribed' do
merge_request.subscribe(current_user, project)
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(false)
end
end
end
...@@ -362,6 +362,30 @@ describe API::Projects do ...@@ -362,6 +362,30 @@ describe API::Projects do
end end
end end
context 'and using id_after' do
it_behaves_like 'projects response' do
let(:filter) { { id_after: project2.id } }
let(:current_user) { user }
let(:projects) { [public_project, project, project2, project3].select { |p| p.id > project2.id } }
end
end
context 'and using id_before' do
it_behaves_like 'projects response' do
let(:filter) { { id_before: project2.id } }
let(:current_user) { user }
let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id } }
end
end
context 'and using both id_after and id_before' do
it_behaves_like 'projects response' do
let(:filter) { { id_before: project2.id, id_after: public_project.id } }
let(:current_user) { user }
let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id && p.id > public_project.id } }
end
end
context 'and membership=true' do context 'and membership=true' do
it_behaves_like 'projects response' do it_behaves_like 'projects response' do
let(:filter) { { membership: true } } let(:filter) { { membership: true } }
...@@ -848,6 +872,63 @@ describe API::Projects do ...@@ -848,6 +872,63 @@ describe API::Projects do
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id) expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
end end
context 'and using id_after' do
let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) }
it 'only returns projects with id_after filter given' do
get api("/users/#{user4.id}/projects?id_after=#{public_project.id}", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(another_public_project.id)
end
it 'returns both projects without a id_after filter' do
get api("/users/#{user4.id}/projects", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id, another_public_project.id)
end
end
context 'and using id_before' do
let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) }
it 'only returns projects with id_before filter given' do
get api("/users/#{user4.id}/projects?id_before=#{another_public_project.id}", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
end
it 'returns both projects without a id_before filter' do
get api("/users/#{user4.id}/projects", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id, another_public_project.id)
end
end
context 'and using both id_before and id_after' do
let!(:more_projects) { create_list(:project, 5, :public, creator_id: user4.id, namespace: user4.namespace) }
it 'only returns projects with id matching the range' do
get api("/users/#{user4.id}/projects?id_after=#{more_projects.first.id}&id_before=#{more_projects.last.id}", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(*more_projects[1..-2].map(&:id))
end
end
it 'returns projects filtered by username' do it 'returns projects filtered by username' do
get api("/users/#{user4.username}/projects/", user) get api("/users/#{user4.username}/projects/", user)
......
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