Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-26 09:14:20 +00:00
parent 4b198b6289
commit 30b1000678
44 changed files with 810 additions and 116 deletions

View File

@ -153,21 +153,38 @@ export default {
@mousedown="handleParallelLineMouseDown"
>
<template v-for="(line, index) in diffLines">
<div
v-if="line.isMatchLineLeft || line.isMatchLineRight"
:key="`expand-${index}`"
class="diff-tr line_expansion match"
>
<div class="diff-td text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line.left"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
<template v-if="line.isMatchLineLeft || line.isMatchLineRight">
<div :key="`expand-${index}`" class="diff-tr line_expansion match">
<div class="diff-td text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line.left"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
</div>
</div>
</div>
<div
v-if="line.left.rich_text"
:key="`expand-definition-${index}`"
class="diff-grid-row diff-tr line_holder match"
>
<div class="diff-grid-left diff-grid-3-col left-side">
<div class="diff-td diff-line-num"></div>
<div v-if="inline" class="diff-td diff-line-num"></div>
<div class="diff-td line_content left-side gl-white-space-normal!">
{{ line.left.rich_text }}
</div>
</div>
<div v-if="!inline" class="diff-grid-right diff-grid-3-col right-side">
<div class="diff-td diff-line-num"></div>
<div class="diff-td line_content right-side gl-white-space-normal!">
{{ line.left.rich_text }}
</div>
</div>
</div>
</template>
<diff-row
v-if="!line.isMatchLineLeft && !line.isMatchLineRight"
:key="line.line_code"

View File

@ -30,7 +30,8 @@ export default {
page: 1,
totalItems: 0,
errorMessage: null,
searchTerm: '',
userSearchTerm: '',
searchValue: '',
};
},
computed: {
@ -45,16 +46,11 @@ export default {
},
methods: {
loadGroups() {
// fetchGroups returns no results for search terms 0 < {length} < 3.
// The desired UX is to return the unfiltered results for searches {length} < 3.
// Here, we set the search to an empty string if {length} < 3
const search = this.searchTerm?.length < MINIMUM_SEARCH_TERM_LENGTH ? '' : this.searchTerm;
this.isLoadingMore = true;
return fetchGroups(this.groupsPath, {
page: this.page,
perPage: this.$options.DEFAULT_GROUPS_PER_PAGE,
search,
search: this.searchValue,
})
.then((response) => {
const { page, total } = parseIntPagination(normalizeHeaders(response.headers));
@ -69,12 +65,24 @@ export default {
this.isLoadingMore = false;
});
},
onGroupSearch(searchTerm) {
// keep a copy of the search term for pagination
this.searchTerm = searchTerm;
// reset the current page
onGroupSearch(userSearchTerm = '') {
this.userSearchTerm = userSearchTerm;
// fetchGroups returns no results for search terms 0 < {length} < 3.
// The desired UX is to return the unfiltered results for searches {length} < 3.
// Here, we set the search to an empty string '' if {length} < 3
const newSearchValue =
this.userSearchTerm.length < MINIMUM_SEARCH_TERM_LENGTH ? '' : this.userSearchTerm;
// don't fetch new results if the search value didn't change.
if (newSearchValue === this.searchValue) {
return;
}
// reset the page.
this.page = 1;
return this.loadGroups();
this.searchValue = newSearchValue;
this.loadGroups();
},
},
DEFAULT_GROUPS_PER_PAGE,
@ -92,7 +100,7 @@ export default {
debounce="500"
:placeholder="__('Search by name')"
:is-loading="isLoadingMore"
:value="searchTerm"
:value="userSearchTerm"
@input="onGroupSearch"
/>

View File

@ -603,6 +603,14 @@ table.code {
grid-template-columns: 50px 8px 0 1fr;
}
.diff-grid-3-col {
grid-template-columns: 50px 1fr !important;
}
&.inline-diff-view .diff-grid-3-col {
grid-template-columns: 50px 50px 1fr !important;
}
.diff-grid-comments {
display: grid;
grid-template-columns: 1fr 1fr;

View File

@ -168,8 +168,8 @@ $dark-il: #de935f;
}
}
.diff-grid-left:hover,
.diff-grid-right:hover,
&:not(.match) .diff-grid-left:hover,
&:not(.match) .diff-grid-right:hover,
&.code-search-line:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover;

View File

@ -169,8 +169,8 @@ $monokai-gh: #75715e;
}
}
.diff-grid-left:hover,
.diff-grid-right:hover,
&:not(.match) .diff-grid-left:hover,
&:not(.match) .diff-grid-right:hover,
&.code-search-line:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover;

View File

@ -66,9 +66,9 @@
}
}
.diff-grid-left:hover,
.diff-grid-right:hover,
&.code-search-line:hover {
&:not(.match) .diff-grid-left:hover,
&:not(.match) .diff-grid-right:hover,
&:not(.match) &.code-search-line:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover;
}

View File

@ -148,8 +148,8 @@ $solarized-dark-il: #2aa198;
@include line-coverage-border-color($solarized-dark-coverage, $solarized-dark-no-coverage);
}
.diff-grid-left:hover,
.diff-grid-right:hover,
&:not(.match) .diff-grid-left:hover,
&:not(.match) .diff-grid-right:hover,
&.code-search-line:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover;

View File

@ -168,8 +168,8 @@ $solarized-light-il: #2aa198;
}
}
.diff-grid-left:hover,
.diff-grid-right:hover,
&:not(.match) .diff-grid-left:hover,
&:not(.match) .diff-grid-right:hover,
&.code-search-line:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover;

View File

@ -118,6 +118,15 @@ pre.code,
.line_expansion {
@include diff-expansion($gray-light, $border-color, $blue-600);
&.diff-tr:last-child {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
.diff-td {
border-bottom: 0;
}
}
}
// Diff line
@ -128,8 +137,8 @@ pre.code,
@include match-line;
}
.diff-grid-left:hover,
.diff-grid-right:hover,
&:not(.match) .diff-grid-left:hover,
&:not(.match) .diff-grid-right:hover,
&.code-search-line:hover {
.diff-line-num:not(.empty-cell):not(.conflict_marker_their):not(.conflict_marker_our) {
@include line-number-hover;

View File

@ -14,7 +14,7 @@ module Mutations
project = issue.project
authorize_escalation_status!(project)
check_feature_availability!(project, issue)
check_feature_availability!(issue)
::Issues::UpdateService.new(
project: project,
@ -36,8 +36,8 @@ module Mutations
raise_resource_not_available_error!
end
def check_feature_availability!(project, issue)
return if Feature.enabled?(:incident_escalations, project) && issue.supports_escalation?
def check_feature_availability!(issue)
return if issue.supports_escalation?
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue'
end

View File

@ -173,9 +173,7 @@ module Types
end
def escalation_status
return unless Feature.enabled?(:incident_escalations, object.project) && object.supports_escalation?
object.escalation_status&.status_name
object.supports_escalation? ? object.escalation_status&.status_name : nil
end
end
end

View File

@ -194,6 +194,8 @@ module Issuable
end
def supports_escalation?
return false unless ::Feature.enabled?(:incident_escalations, project)
incident?
end

View File

@ -125,17 +125,6 @@ module Timebox
fuzzy_search(query, [:title, :description])
end
# Searches for timeboxes with a matching title.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search_title(query)
fuzzy_search(query, [:title])
end
def filter_by_state(timeboxes, state)
case state
when 'closed' then timeboxes.closed

View File

@ -52,6 +52,17 @@ class Milestone < ApplicationRecord
state :active
end
# Searches for timeboxes with a matching title.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def self.search_title(query)
fuzzy_search(query, [:title])
end
def self.min_chars_for_partial_matching
2
end

View File

@ -110,6 +110,7 @@ class IssuableSidebarBasicEntity < Grape::Entity
expose :supports_time_tracking?, as: :supports_time_tracking
expose :supports_milestone?, as: :supports_milestone
expose :supports_severity?, as: :supports_severity
expose :supports_escalation?, as: :supports_escalation
private

View File

@ -4,6 +4,12 @@ class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
expose :due_date
expose :confidential
expose :severity
expose :current_user, merge: true do
expose :can_update_escalation_status, if: -> (issue, _) { issue.supports_escalation? } do |issue|
can?(current_user, :update_escalation_status, issue.project)
end
end
end
IssueSidebarBasicEntity.prepend_mod_with('IssueSidebarBasicEntity')

View File

@ -155,8 +155,7 @@ module AlertManagement
end
def should_sync_to_incident?
Feature.enabled?(:incident_escalations, project) &&
alert.issue &&
alert.issue &&
alert.issue.supports_escalation? &&
alert.issue.escalation_status &&
alert.issue.escalation_status.status != alert.status

View File

@ -33,9 +33,8 @@ module IncidentManagement
attr_reader :issuable, :current_user, :params, :project
def available?
Feature.enabled?(:incident_escalations, project) &&
issuable.supports_escalation? &&
user_has_permissions? &&
issuable.supports_escalation? &&
escalation_status.present?
end

View File

@ -39,15 +39,15 @@
= _('Namespace:')
%strong
- if @project.namespace
= link_to @project.namespace.human_name, [:admin, @project.group || @project.owner]
= link_to @project.namespace.human_name, [:admin, @project.personal? ? @project.namespace.owner : @project.group]
- else
= s_('ProjectSettings|Global')
%li
%span.light
= _('Owned by:')
%strong
- if @project.owner
= link_to @project.owner_name, [:admin, @project.owner]
- if @project.owners.any?
= safe_join(@project.owners.map { |owner| link_to(owner.name, [:admin, owner]) }, ", ".html_safe)
- else
= _('(deleted)')

View File

@ -25,6 +25,11 @@
.block.reviewer.qa-reviewer-block
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
- if issuable_sidebar[:supports_escalation]
.block.escalation-status{ data: { testid: 'escalation_status_container' } }
#js-escalation-status{ data: { can_edit: issuable_sidebar.dig(:current_user, :can_update_escalation_status).to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
= render_if_exists 'shared/issuable/sidebar_escalation_policy', issuable_sidebar: issuable_sidebar
- if @project.group.present?
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type

View File

@ -0,0 +1,8 @@
---
name: ci_secure_files
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78227
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350748
milestone: '14.8'
type: development
group: group::incubation
default_enabled: false

View File

@ -404,6 +404,23 @@ production: &base
# aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## CI Secure Files
ci_secure_files:
enabled: true
# storage_path: shared/ci_secure_files
object_store:
enabled: false
remote_directory: ci_secure_files # The bucket name
connection:
provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: us-east-1
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
# aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## GitLab Pages
pages:
enabled: false

View File

@ -5,6 +5,6 @@
issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/300862'
breaking_change: true
body: |
In GitLab 14.0, we will update the [Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/stages.html#auto-deploy) CI template to the latest version. This includes new features, bug fixes, and performance improvements with a dependency on the v2 [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image). Auto Deploy CI tempalte v1 will is deprecated going forward.
In GitLab 14.0, we will update the [Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/stages.html#auto-deploy) CI template to the latest version. This includes new features, bug fixes, and performance improvements with a dependency on the v2 [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image). Auto Deploy CI template v1 will is deprecated going forward.
Since the v1 and v2 versions are not backward-compatible, your project might encounter an unexpected failure if you already have a deployed application. Follow the [upgrade guide](https://docs.gitlab.com/ee/topics/autodevops/upgrading_auto_deploy_dependencies.html#upgrade-guide) to upgrade your environments. You can also start using the latest template today by following the [early adoption guide](https://docs.gitlab.com/ee/topics/autodevops/upgrading_auto_deploy_dependencies.html#early-adopters).

View File

@ -16,7 +16,7 @@
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/user/profile/#user-profile-readme'
url: 'https://docs.gitlab.com/ee/user/profile/#add-details-to-your-profile-with-a-readme'
image_url: https://about.gitlab.com/images/14_5/user_profiles_readme.png
published_at: 2021-11-22
release: 14.5

View File

@ -11064,12 +11064,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="groupiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. |
| <a id="groupiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. |
| <a id="groupiterationsin"></a>`in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. |
| <a id="groupiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. |
| <a id="groupiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. |
| <a id="groupiterationssearch"></a>`search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. |
| <a id="groupiterationssort"></a>`sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. |
| <a id="groupiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="groupiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. |
| <a id="groupiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="groupiterationstitle"></a>`title` | [`String`](#string) | Fuzzy search by title. |
| <a id="groupiterationstitle"></a>`title` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. The argument will be removed in 15.4. Please use `search` and `in` fields instead. |
##### `Group.label`
@ -13762,12 +13765,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="projectiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. |
| <a id="projectiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. |
| <a id="projectiterationsin"></a>`in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. |
| <a id="projectiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. |
| <a id="projectiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. |
| <a id="projectiterationssearch"></a>`search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. |
| <a id="projectiterationssort"></a>`sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. |
| <a id="projectiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="projectiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. |
| <a id="projectiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="projectiterationstitle"></a>`title` | [`String`](#string) | Fuzzy search by title. |
| <a id="projectiterationstitle"></a>`title` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. The argument will be removed in 15.4. Please use `search` and `in` fields instead. |
##### `Project.jobs`
@ -17000,6 +17006,23 @@ Issue type.
| <a id="issuetyperequirement"></a>`REQUIREMENT` | Requirement issue type. |
| <a id="issuetypetest_case"></a>`TEST_CASE` | Test Case issue type. |
### `IterationSearchableField`
Fields to perform the search in.
| Value | Description |
| ----- | ----------- |
| <a id="iterationsearchablefieldcadence_title"></a>`CADENCE_TITLE` | Search in cadence_title field. |
| <a id="iterationsearchablefieldtitle"></a>`TITLE` | Search in title field. |
### `IterationSort`
Iteration sort values.
| Value | Description |
| ----- | ----------- |
| <a id="iterationsortcadence_and_due_date_asc"></a>`CADENCE_AND_DUE_DATE_ASC` | Sort by cadence id and due date in ascending order. |
### `IterationState`
State of a GitLab iteration.

View File

@ -408,7 +408,7 @@ for batched background migration:
1. Change the index pattern to `pubsub-postgres-inf-gprd*`.
1. Add filter for `json.endpoint_id.keyword: Database::BatchedBackgroundMigrationWorker`.
1. Optional. To see only updates, add a filter for `json.command_tag.keyword: UPDATE`.
1. Optional. To see only failed statements, add a filter for `json.error_severiry.keyword: ERROR`.
1. Optional. To see only failed statements, add a filter for `json.error_severity.keyword: ERROR`.
1. Optional. Add a filter by table name.
#### Grafana dashboards

View File

@ -368,7 +368,7 @@ then the query might perform worse than the non-optimized query. The `milestone_
"index_issues_on_milestone_id" btree (milestone_id)
```
Adding the `miletone_id = X` filter to the `scope` argument or to the optimized scope causes bad performance.
Adding the `milestone_id = X` filter to the `scope` argument or to the optimized scope causes bad performance.
Example (bad):
@ -618,7 +618,7 @@ The following example shows the final `ORDER BY` clause:
ORDER BY extract('epoch' FROM epics.closed_at - epics.created_at) DESC, epics.id DESC
```
Snippet for loading records ordered by the calcualted duration:
Snippet for loading records ordered by the calculated duration:
```ruby
arel_table = Epic.arel_table
@ -641,7 +641,7 @@ records = Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
array_mapping_scope: -> (id_expression) { Epic.where(Epic.arel_table[:group_id].eq(id_expression)) }
).execute.limit(20)
puts records.pluck(:duration_in_seconds, :id) # other columnns are not available
puts records.pluck(:duration_in_seconds, :id) # other columns are not available
```
Building the query requires quite a bit of configuration. For the order configuration you

View File

@ -596,7 +596,7 @@ NOTE:
This method of stubbing in Jest specs will not automatically un-stub itself at the end of the test. We merge our stubbed experiment in with all the other global data in `window.gl`. If you need to remove the stubbed experiment(s) after your test or ensure a clean global object before your test, you'll need to manage the global object directly yourself:
```javascript
desribe('tests that care about global state', () => {
describe('tests that care about global state', () => {
const originalObjects = [];
beforeEach(() => {

View File

@ -245,7 +245,7 @@ For now, CI will try to fetch the branch on the [GitLab JH mirror](https://gitla
The `rspec:undercoverage` job runs [`undercover`](https://rubygems.org/gems/undercover)
to detect, and fail if any changes introduced in the merge request has zero coverage.
The `rsepc:undercoverage` job obtains coverage data from the `rspec:coverage`
The `rspec:undercoverage` job obtains coverage data from the `rspec:coverage`
job.
In the event of an emergency, or false positive from this job, add the

View File

@ -268,7 +268,7 @@ On the EC2 dashboard, look for Load Balancer in the left navigation bar:
1. Click **Configure Health Check** and set up a health check for your EC2 instances.
1. For **Ping Protocol**, select HTTP.
1. For **Ping Port**, enter 80.
1. For **Ping Path** - we recommend that you [use the Readiness check endpoint](../../administration/load_balancer.md#readiness-check). You'll need to add [the VPC IP Adddress Range (CIDR)](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html#elb-vpc-nacl) to the [IP Allowlist](../../administration/monitoring/ip_whitelist.md) for the [Health Check endpoints](../../user/admin_area/monitoring/health_check.md)
1. For **Ping Path** - we recommend that you [use the Readiness check endpoint](../../administration/load_balancer.md#readiness-check). You'll need to add [the VPC IP Address Range (CIDR)](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html#elb-vpc-nacl) to the [IP Allowlist](../../administration/monitoring/ip_whitelist.md) for the [Health Check endpoints](../../user/admin_area/monitoring/health_check.md)
1. Keep the default **Advanced Details** or adjust them according to your needs.
1. Click **Add EC2 Instances** - don't add anything as we will create an Auto Scaling Group later to manage instances for us.
1. Click **Add Tags** and add any tags you need.
@ -588,8 +588,8 @@ Let's create an EC2 instance where we'll install Gitaly:
1. In the **Subnet** dropdown, select `gitlab-private-10.0.1.0` from the list of subnets we created earlier.
1. Double check that **Auto-assign Public IP** is set to `Use subnet setting (Disable)`.
1. Click **Add Storage**.
1. Increase the Root volume size to `20 GiB` and change the **Volume Type** to `Provisoned IOPS SSD (io1)`. (This is an arbitrary size. Create a volume big enough for your repository storage requirements.)
1. For **IOPS** set `1000` (20 GiB x 50 IOPS). You can provision up to 50 IOPS per GiB. If you select a larger volume, increase the IOPS accordingly. Workloads where many small files are written in a serialized manner, like `git`, requires performant storage, hence the choice of `Provisoned IOPS SSD (io1)`.
1. Increase the Root volume size to `20 GiB` and change the **Volume Type** to `Provisioned IOPS SSD (io1)`. (This is an arbitrary size. Create a volume big enough for your repository storage requirements.)
1. For **IOPS** set `1000` (20 GiB x 50 IOPS). You can provision up to 50 IOPS per GiB. If you select a larger volume, increase the IOPS accordingly. Workloads where many small files are written in a serialized manner, like `git`, requires performant storage, hence the choice of `Provisioned IOPS SSD (io1)`.
1. Click on **Add Tags** and add your tags. In our case, we'll only set `Key: Name` and `Value: Gitaly`.
1. Click on **Configure Security Group** and let's **Create a new security group**.
1. Give your security group a name and description. We'll use `gitlab-gitaly-sec-group` for both.

View File

@ -63,4 +63,4 @@ Use GitLab [Releases](../user/project/releases/index.md) to plan, build, and del
### Feature flags
Use [feature flags](../operations/feature_flags.md) to control and strategically roullout application deployments.
Use [feature flags](../operations/feature_flags.md) to control and strategically rollout application deployments.

View File

@ -624,7 +624,7 @@ as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#brea
Before updating GitLab, review the details carefully to determine if you need to make any
changes to your code, settings, or workflow.
In GitLab 14.0, we will update the [Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/stages.html#auto-deploy) CI template to the latest version. This includes new features, bug fixes, and performance improvements with a dependency on the v2 [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image). Auto Deploy CI tempalte v1 will is deprecated going forward.
In GitLab 14.0, we will update the [Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/stages.html#auto-deploy) CI template to the latest version. This includes new features, bug fixes, and performance improvements with a dependency on the v2 [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image). Auto Deploy CI template v1 will is deprecated going forward.
Since the v1 and v2 versions are not backward-compatible, your project might encounter an unexpected failure if you already have a deployed application. Follow the [upgrade guide](https://docs.gitlab.com/ee/topics/autodevops/upgrading_auto_deploy_dependencies.html#upgrade-guide) to upgrade your environments. You can also start using the latest template today by following the [early adoption guide](https://docs.gitlab.com/ee/topics/autodevops/upgrading_auto_deploy_dependencies.html#early-adopters).

View File

@ -102,6 +102,8 @@ user profiles are only visible to signed-in users.
## Add details to your profile with a README
### *Add personal README to profile*
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5.
If you want to add more information to your profile page, you can create a README file. When you populate the README file with information, it's included on your profile page.

View File

@ -171,6 +171,7 @@ module API
mount ::API::Ci::ResourceGroups
mount ::API::Ci::Runner
mount ::API::Ci::Runners
mount ::API::Ci::SecureFiles
mount ::API::Ci::Triggers
mount ::API::Ci::Variables
mount ::API::Commits

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
module API
module Ci
class SecureFiles < ::API::Base
include PaginationParams
before do
authenticate!
authorize! :admin_build, user_project
feature_flag_enabled?
end
feature_category :pipeline_authoring
default_format :json
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'List all Secure Files for a Project'
params do
use :pagination
end
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
get ':id/secure_files' do
secure_files = user_project.secure_files
present paginate(secure_files), with: Entities::Ci::SecureFile
end
desc 'Get an individual Secure File'
params do
requires :id, type: Integer, desc: 'The Secure File ID'
end
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
get ':id/secure_files/:secure_file_id' do
secure_file = user_project.secure_files.find(params[:secure_file_id])
present secure_file, with: Entities::Ci::SecureFile
end
desc 'Download a Secure File'
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
get ':id/secure_files/:secure_file_id/download' do
secure_file = user_project.secure_files.find(params[:secure_file_id])
content_type 'application/octet-stream'
env['api.format'] = :binary
header['Content-Disposition'] = "attachment; filename=#{secure_file.name}"
body secure_file.file.read
end
desc 'Upload a Secure File'
params do
requires :name, type: String, desc: 'The name of the file'
requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The secure file to be uploaded'
optional :permissions, type: String, desc: 'The file permissions', default: 'read_only', values: %w[read_only read_write execute]
end
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
post ':id/secure_files' do
secure_file = user_project.secure_files.new(
name: params[:name],
permissions: params[:permissions] || :read_only
)
secure_file.file = params[:file]
file_too_large! unless secure_file.file.size < ::Ci::SecureFile::FILE_SIZE_LIMIT.to_i
if secure_file.save
present secure_file, with: Entities::Ci::SecureFile
else
render_validation_error!(secure_file)
end
end
desc 'Delete an individual Secure File'
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
delete ':id/secure_files/:secure_file_id' do
secure_file = user_project.secure_files.find(params[:secure_file_id])
secure_file.destroy!
no_content!
end
end
helpers do
def feature_flag_enabled?
service_unavailable! unless Feature.enabled?(:ci_secure_files, user_project, default_enabled: :yaml)
end
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module API
module Entities
module Ci
class SecureFile < Grape::Entity
expose :id
expose :name
expose :permissions
expose :checksum
expose :checksum_algorithm
end
end
end
end

View File

@ -6228,6 +6228,15 @@ msgstr ""
msgid "By default, all projects and groups will use the global notifications setting."
msgstr ""
msgid "By month"
msgstr ""
msgid "By quarter"
msgstr ""
msgid "By week"
msgstr ""
msgid "ByAuthor|by"
msgstr ""

View File

@ -50,8 +50,11 @@ describe('DiffView', () => {
};
it('renders a match line', () => {
const wrapper = createWrapper({ diffLines: [{ isMatchLineLeft: true }] });
const wrapper = createWrapper({
diffLines: [{ isMatchLineLeft: true, left: { rich_text: 'matched text', lineDraft: {} } }],
});
expect(wrapper.find(DiffExpansionCell).exists()).toBe(true);
expect(wrapper.text()).toContain('matched text');
});
it.each`

View File

@ -101,6 +101,8 @@ describe('GroupsList', () => {
});
createComponent();
// wait for the initial loadGroups
// to finish.
await waitForPromises();
});
@ -137,6 +139,8 @@ describe('GroupsList', () => {
describe('while groups are loading', () => {
beforeEach(async () => {
fetchGroups.mockClear();
// return a never-ending promise to make test
// deterministic.
fetchGroups.mockReturnValue(new Promise(() => {}));
findSearchBox().vm.$emit('input', mockSearchTeam);
@ -173,7 +177,7 @@ describe('GroupsList', () => {
describe('when group search finishes loading', () => {
beforeEach(async () => {
fetchGroups.mockResolvedValue({ data: [mockGroup1] });
findSearchBox().vm.$emit('input');
findSearchBox().vm.$emit('input', mockSearchTeam);
await waitForPromises();
});
@ -184,32 +188,48 @@ describe('GroupsList', () => {
});
});
it.each`
userSearchTerm | finalSearchTerm
${'gitl'} | ${'gitl'}
${'git'} | ${'git'}
${'gi'} | ${''}
${'g'} | ${''}
${''} | ${''}
${undefined} | ${undefined}
describe.each`
previousSearch | newSearch | shouldSearch | expectedSearchValue
${''} | ${'git'} | ${true} | ${'git'}
${'g'} | ${'git'} | ${true} | ${'git'}
${'git'} | ${'gitl'} | ${true} | ${'gitl'}
${'git'} | ${'gi'} | ${true} | ${''}
${'gi'} | ${'g'} | ${false} | ${undefined}
${'g'} | ${''} | ${false} | ${undefined}
${''} | ${'g'} | ${false} | ${undefined}
`(
'searches for "$finalSearchTerm" when user enters "$userSearchTerm"',
async ({ userSearchTerm, finalSearchTerm }) => {
fetchGroups.mockResolvedValue({
data: [mockGroup1],
headers: { 'X-PAGE': 1, 'X-TOTAL': 1 },
'when previous search was "$previousSearch" and user enters "$newSearch"',
({ previousSearch, newSearch, shouldSearch, expectedSearchValue }) => {
beforeEach(async () => {
fetchGroups.mockResolvedValue({
data: [mockGroup1],
headers: { 'X-PAGE': 1, 'X-TOTAL': 1 },
});
// wait for initial load
createComponent();
await waitForPromises();
// set up the "previous search"
findSearchBox().vm.$emit('input', previousSearch);
await waitForPromises();
fetchGroups.mockClear();
});
createComponent();
await waitForPromises();
it(`${shouldSearch ? 'should' : 'should not'} execute fetch new results`, () => {
// enter the new search
findSearchBox().vm.$emit('input', newSearch);
const searchBox = findSearchBox();
searchBox.vm.$emit('input', userSearchTerm);
expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
page: 1,
perPage: DEFAULT_GROUPS_PER_PAGE,
search: finalSearchTerm,
if (shouldSearch) {
expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, {
page: 1,
perPage: DEFAULT_GROUPS_PER_PAGE,
search: expectedSearchValue,
});
} else {
expect(fetchGroups).not.toHaveBeenCalled();
}
});
},
);
@ -227,7 +247,13 @@ describe('GroupsList', () => {
await waitForPromises();
const paginationEl = findPagination();
paginationEl.vm.$emit('input', 2);
// mock the response from page 2
fetchGroups.mockResolvedValue({
headers: { 'X-TOTAL': totalItems, 'X-PAGE': 2 },
data: mockGroups,
});
await paginationEl.vm.$emit('input', 2);
});
it('should load results for page 2', () => {
@ -238,18 +264,23 @@ describe('GroupsList', () => {
});
});
it('resets page to 1 on search `input` event', () => {
const mockSearchTerm = 'gitlab';
const searchBox = findSearchBox();
it.each`
scenario | searchTerm | expectedPage | expectedSearchTerm
${'preserves current page'} | ${'gi'} | ${2} | ${''}
${'resets page to 1'} | ${'gitlab'} | ${1} | ${'gitlab'}
`(
'$scenario when search term is $searchTerm',
({ searchTerm, expectedPage, expectedSearchTerm }) => {
const searchBox = findSearchBox();
searchBox.vm.$emit('input', searchTerm);
searchBox.vm.$emit('input', mockSearchTerm);
expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
page: 1,
perPage: DEFAULT_GROUPS_PER_PAGE,
search: mockSearchTerm,
});
});
expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
page: expectedPage,
perPage: DEFAULT_GROUPS_PER_PAGE,
search: expectedSearchTerm,
});
},
);
});
});

View File

@ -935,6 +935,14 @@ RSpec.describe Issuable do
subject { issuable.supports_escalation? }
it { is_expected.to eq(supports_escalation) }
context 'with feature disabled' do
before do
stub_feature_flags(incident_escalations: false)
end
it { is_expected.to eq(false) }
end
end
end

View File

@ -0,0 +1,314 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::SecureFiles do
before do
stub_ci_secure_file_object_storage
stub_feature_flags(ci_secure_files: true)
end
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:project) { create(:project, creator_id: user.id) }
let_it_be(:maintainer) { create(:project_member, :maintainer, user: user, project: project) }
let_it_be(:developer) { create(:project_member, :developer, user: user2, project: project) }
let_it_be(:secure_file) { create(:ci_secure_file, project: project) }
describe 'GET /projects/:id/secure_files' do
context 'feature flag' do
it 'returns a 503 when the feature flag is disabled' do
stub_feature_flags(ci_secure_files: false)
get api("/projects/#{project.id}/secure_files", user)
expect(response).to have_gitlab_http_status(:service_unavailable)
end
it 'returns a 200 when the feature flag is enabled' do
get api("/projects/#{project.id}/secure_files", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a(Array)
end
end
context 'authorized user with proper permissions' do
it 'returns project secure files' do
get api("/projects/#{project.id}/secure_files", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a(Array)
end
end
context 'authorized user with invalid permissions' do
it 'does not return project secure files' do
get api("/projects/#{project.id}/secure_files", user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
it 'does not return project secure files' do
get api("/projects/#{project.id}/secure_files")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'GET /projects/:id/secure_files/:secure_file_id' do
context 'authorized user with proper permissions' do
it 'returns project secure file details' do
get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(secure_file.name)
expect(json_response['permissions']).to eq(secure_file.permissions)
end
it 'responds with 404 Not Found if requesting non-existing secure file' do
get api("/projects/#{project.id}/secure_files/99999", user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'authorized user with invalid permissions' do
it 'does not return project secure file details' do
get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
it 'does not return project secure file details' do
get api("/projects/#{project.id}/secure_files/#{secure_file.id}")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'GET /projects/:id/secure_files/:secure_file_id/download' do
context 'authorized user with proper permissions' do
it 'returns a secure file' do
sample_file = fixture_file('ci_secure_files/upload-keystore.jks')
secure_file.file = CarrierWaveStringFile.new(sample_file)
secure_file.save!
get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user)
expect(response).to have_gitlab_http_status(:ok)
expect(Base64.encode64(response.body)).to eq(Base64.encode64(sample_file))
end
it 'responds with 404 Not Found if requesting non-existing secure file' do
get api("/projects/#{project.id}/secure_files/99999/download", user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'authorized user with invalid permissions' do
it 'does not return project secure file details' do
get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
it 'does not return project secure file details' do
get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'POST /projects/:id/secure_files' do
context 'authorized user with proper permissions' do
it 'creates a secure file' do
params = {
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
name: 'upload-keystore.jks',
permissions: 'execute'
}
expect do
post api("/projects/#{project.id}/secure_files", user), params: params
end.to change {project.secure_files.count}.by(1)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq('upload-keystore.jks')
expect(json_response['permissions']).to eq('execute')
expect(json_response['checksum']).to eq(secure_file.checksum)
expect(json_response['checksum_algorithm']).to eq('sha256')
secure_file = Ci::SecureFile.find(json_response['id'])
expect(secure_file.checksum).to eq(
Digest::SHA256.hexdigest(fixture_file('ci_secure_files/upload-keystore.jks'))
)
expect(json_response['id']).to eq(secure_file.id)
end
it 'creates a secure file with read_only permissions by default' do
params = {
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
name: 'upload-keystore.jks'
}
expect do
post api("/projects/#{project.id}/secure_files", user), params: params
end.to change {project.secure_files.count}.by(1)
expect(json_response['permissions']).to eq('read_only')
end
it 'uploads and downloads a secure file' do
post_params = {
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
name: 'upload-keystore.jks',
permissions: 'read_write'
}
post api("/projects/#{project.id}/secure_files", user), params: post_params
secure_file_id = json_response['id']
get api("/projects/#{project.id}/secure_files/#{secure_file_id}/download", user)
expect(Base64.encode64(response.body)).to eq(Base64.encode64(fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks').read))
end
it 'returns an error when the file checksum fails to validate' do
secure_file.update!(checksum: 'foo')
get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user)
expect(response.code).to eq("500")
end
it 'returns an error when no file is uploaded' do
post_params = {
name: 'upload-keystore.jks'
}
post api("/projects/#{project.id}/secure_files", user), params: post_params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('file is missing')
end
it 'returns an error when the file name is missing' do
post_params = {
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks')
}
post api("/projects/#{project.id}/secure_files", user), params: post_params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('name is missing')
end
it 'returns an error when an unexpected permission is supplied' do
post_params = {
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
name: 'upload-keystore.jks',
permissions: 'foo'
}
post api("/projects/#{project.id}/secure_files", user), params: post_params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('permissions does not have a valid value')
end
it 'returns an error when an unexpected validation failure happens' do
allow_next_instance_of(Ci::SecureFile) do |instance|
allow(instance).to receive(:valid?).and_return(false)
allow(instance).to receive_message_chain(:errors, :any?).and_return(true)
allow(instance).to receive_message_chain(:errors, :messages).and_return(['Error 1', 'Error 2'])
end
post_params = {
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
name: 'upload-keystore.jks'
}
post api("/projects/#{project.id}/secure_files", user), params: post_params
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns a 413 error when the file size is too large' do
allow_next_instance_of(Ci::SecureFile) do |instance|
allow(instance).to receive_message_chain(:file, :size).and_return(6.megabytes.to_i)
end
post_params = {
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
name: 'upload-keystore.jks'
}
post api("/projects/#{project.id}/secure_files", user), params: post_params
expect(response).to have_gitlab_http_status(:payload_too_large)
end
end
context 'authorized user with invalid permissions' do
it 'does not create a secure file' do
post api("/projects/#{project.id}/secure_files", user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
it 'does not create a secure file' do
post api("/projects/#{project.id}/secure_files")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'DELETE /projects/:id/secure_files/:secure_file_id' do
context 'authorized user with proper permissions' do
it 'deletes the secure file' do
expect do
delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
end.to change {project.secure_files.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing secure_file' do
delete api("/projects/#{project.id}/secure_files/99999", user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'authorized user with invalid permissions' do
it 'does not delete the secure_file' do
delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
it 'does not delete the secure_file' do
delete api("/projects/#{project.id}/secure_files/#{secure_file.id}")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssueSidebarBasicEntity do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
let_it_be(:issue) { create(:issue, project: project, assignees: [user]) }
let(:serializer) { IssueSerializer.new(current_user: user, project: project) }
subject(:entity) { serializer.represent(issue, serializer: 'sidebar') }
it 'contains keys related to issuables' do
expect(entity).to include(
:id, :iid, :type, :author_id, :project_id, :discussion_locked, :reference, :milestone,
:labels, :current_user, :issuable_json_path, :namespace_path, :project_path,
:project_full_path, :project_issuables_path, :create_todo_path, :project_milestones_path,
:project_labels_path, :toggle_subscription_path, :move_issue_path, :projects_autocomplete_path,
:project_emails_disabled, :create_note_email, :supports_time_tracking, :supports_milestone,
:supports_severity, :supports_escalation
)
end
it 'contains attributes related to the issue' do
expect(entity).to include(:due_date, :confidential, :severity)
end
describe 'current_user' do
it 'contains attributes related to the current user' do
expect(entity[:current_user]).to include(
:id, :name, :username, :state, :avatar_url, :web_url, :todo,
:can_edit, :can_move, :can_admin_label
)
end
describe 'can_update_escalation_status' do
context 'for a standard issue' do
it 'is not present' do
expect(entity[:current_user]).not_to have_key(:can_update_escalation_status)
end
end
context 'for an incident issue' do
before do
issue.update!(issue_type: Issue.issue_types[:incident])
end
it 'is present and true' do
expect(entity[:current_user][:can_update_escalation_status]).to be(true)
end
context 'without permissions' do
let(:serializer) { IssueSerializer.new(current_user: create(:user), project: project) }
it 'is present and false' do
expect(entity[:current_user]).to have_key(:can_update_escalation_status)
expect(entity[:current_user][:can_update_escalation_status]).to be(false)
end
end
context 'with :incident_escalations feature flag disabled' do
before do
stub_feature_flags(incident_escalations: false)
end
it 'is not present' do
expect(entity[:current_user]).not_to include(:can_update_escalation_status)
end
end
end
end
end
end

View File

@ -146,7 +146,7 @@ RSpec.describe Releases::CreateService do
end
end
context 'when multiple miletone titles are passed in but one of them does not exist' do
context 'when multiple milestone titles are passed in but one of them does not exist' do
let(:title) { 'v1.0' }
let(:inexistent_title) { 'v111.0' }
let!(:milestone) { create(:milestone, :active, project: project, title: title) }

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'shared/issuable/_sidebar.html.haml' do
let_it_be(:user) { create(:user) }
subject(:rendered) do
render 'shared/issuable/sidebar', issuable_sidebar: IssueSerializer.new(current_user: user)
.represent(issuable, serializer: 'sidebar'), assignees: []
end
context 'project in a group' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:incident) { create(:incident, project: project) }
before do
assign(:project, project)
end
context 'issuable that does not support escalations' do
let(:issuable) { incident }
it 'shows escalation policy dropdown' do
expect(rendered).to have_css('[data-testid="escalation_status_container"]')
end
end
context 'issuable that supports escalations' do
let(:issuable) { issue }
it 'does not show escalation policy dropdown' do
expect(rendered).not_to have_css('[data-testid="escalation_status_container"]')
end
end
end
end