Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e50050a875
commit
b11f7057d0
|
@ -499,6 +499,7 @@
|
|||
.review:rules:review-gcp-cleanup:
|
||||
rules:
|
||||
- <<: *if-dot-com-gitlab-org-merge-request
|
||||
changes: *code-qa-patterns
|
||||
when: manual
|
||||
- <<: *if-dot-com-gitlab-org-schedule
|
||||
when: on_success
|
||||
|
|
|
@ -422,7 +422,6 @@ RSpec/RepeatedExample:
|
|||
- 'spec/services/notification_service_spec.rb'
|
||||
- 'spec/services/web_hook_service_spec.rb'
|
||||
- 'ee/spec/models/group_spec.rb'
|
||||
- 'ee/spec/models/user_spec.rb'
|
||||
- 'ee/spec/requests/api/merge_request_approvals_spec.rb'
|
||||
- 'ee/spec/services/boards/lists/update_service_spec.rb'
|
||||
- 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
|
||||
|
|
|
@ -172,7 +172,6 @@ export default {
|
|||
/>
|
||||
<a
|
||||
v-once
|
||||
id="diffFile.file_path"
|
||||
ref="titleWrapper"
|
||||
class="append-right-4"
|
||||
:href="titleLink"
|
||||
|
|
|
@ -7,21 +7,18 @@ export default {
|
|||
BlobHeaderEdit,
|
||||
BlobContentEdit,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: this.fileName,
|
||||
blobContent: this.content,
|
||||
};
|
||||
methods: {
|
||||
emitFileNameChange(newFileName) {
|
||||
this.$emit('name-change', newFileName);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -29,8 +26,8 @@ export default {
|
|||
<div class="form-group file-editor">
|
||||
<label>{{ s__('Snippets|File') }}</label>
|
||||
<div class="file-holder snippet">
|
||||
<blob-header-edit v-model="name" />
|
||||
<blob-content-edit v-model="blobContent" :file-name="name" />
|
||||
<blob-header-edit :value="fileName" @input="emitFileNameChange" />
|
||||
<blob-content-edit v-bind="$attrs" :file-name="fileName" v-on="$listeners" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -9,11 +9,6 @@ export default {
|
|||
MarkdownField,
|
||||
},
|
||||
props: {
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
markdownPreviewPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -22,11 +17,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
text: this.description,
|
||||
};
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
setupCollapsibleInputs();
|
||||
|
@ -37,7 +32,7 @@ export default {
|
|||
<div class="form-group js-description-input">
|
||||
<label>{{ s__('Snippets|Description (optional)') }}</label>
|
||||
<div class="js-collapsible-input">
|
||||
<div class="js-collapsed" :class="{ 'd-none': text }">
|
||||
<div class="js-collapsed" :class="{ 'd-none': value }">
|
||||
<gl-form-input
|
||||
class="form-control"
|
||||
:placeholder="
|
||||
|
@ -50,20 +45,21 @@ export default {
|
|||
</div>
|
||||
<markdown-field
|
||||
class="js-expanded"
|
||||
:class="{ 'd-none': !text }"
|
||||
:class="{ 'd-none': !value }"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:markdown-docs-path="markdownDocsPath"
|
||||
>
|
||||
<textarea
|
||||
id="snippet-description"
|
||||
slot="textarea"
|
||||
v-model="text"
|
||||
class="note-textarea js-gfm-input js-autosize markdown-area
|
||||
qa-description-textarea"
|
||||
dir="auto"
|
||||
data-supports-quick-actions="false"
|
||||
:value="value"
|
||||
:aria-label="__('Description')"
|
||||
:placeholder="__('Write a comment or drag your files here…')"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
</textarea>
|
||||
</markdown-field>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
|
||||
import { SNIPPET_VISIBILITY } from '~/snippets/constants';
|
||||
import { SNIPPET_VISIBILITY, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -21,48 +21,22 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
visibilityLevel: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '0',
|
||||
required: false,
|
||||
default: SNIPPET_VISIBILITY_PRIVATE,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: this.visibilityLevel,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visibilityOptions() {
|
||||
return [
|
||||
{
|
||||
value: '0',
|
||||
icon: 'lock',
|
||||
text: SNIPPET_VISIBILITY.private.label,
|
||||
description: this.isProjectSnippet
|
||||
? SNIPPET_VISIBILITY.private.description_project
|
||||
: SNIPPET_VISIBILITY.private.description,
|
||||
},
|
||||
{
|
||||
value: '1',
|
||||
icon: 'shield',
|
||||
text: SNIPPET_VISIBILITY.internal.label,
|
||||
description: SNIPPET_VISIBILITY.internal.description,
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
icon: 'earth',
|
||||
text: SNIPPET_VISIBILITY.public.label,
|
||||
description: SNIPPET_VISIBILITY.public.description,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateSelectedOption(newVal) {
|
||||
if (newVal !== this.selected) {
|
||||
this.selected = newVal;
|
||||
}
|
||||
const options = [];
|
||||
Object.keys(SNIPPET_VISIBILITY).forEach(key => {
|
||||
options.push({
|
||||
value: key,
|
||||
...SNIPPET_VISIBILITY[key],
|
||||
});
|
||||
});
|
||||
return options;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -76,18 +50,22 @@ export default {
|
|||
/></gl-link>
|
||||
</label>
|
||||
<gl-form-group id="visibility-level-setting">
|
||||
<gl-form-radio-group :checked="selected" stacked @change="updateSelectedOption">
|
||||
<gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners">
|
||||
<gl-form-radio
|
||||
v-for="option in visibilityOptions"
|
||||
:key="option.icon"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
class="mb-3"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<gl-icon :size="16" :name="option.icon" />
|
||||
<span class="font-weight-bold ml-1">{{ option.text }}</span>
|
||||
<span class="font-weight-bold ml-1 js-visibility-option">{{ option.label }}</span>
|
||||
</div>
|
||||
<template #help>{{ option.description }}</template>
|
||||
<template #help>{{
|
||||
isProjectSnippet && option.description_project
|
||||
? option.description_project
|
||||
: option.description
|
||||
}}</template>
|
||||
</gl-form-radio>
|
||||
</gl-form-radio-group>
|
||||
</gl-form-group>
|
||||
|
|
|
@ -5,17 +5,20 @@ export const SNIPPET_VISIBILITY_INTERNAL = 'internal';
|
|||
export const SNIPPET_VISIBILITY_PUBLIC = 'public';
|
||||
|
||||
export const SNIPPET_VISIBILITY = {
|
||||
private: {
|
||||
[SNIPPET_VISIBILITY_PRIVATE]: {
|
||||
label: __('Private'),
|
||||
icon: 'lock',
|
||||
description: __('The snippet is visible only to me.'),
|
||||
description_project: __('The snippet is visible only to project members.'),
|
||||
},
|
||||
internal: {
|
||||
[SNIPPET_VISIBILITY_INTERNAL]: {
|
||||
label: __('Internal'),
|
||||
icon: 'shield',
|
||||
description: __('The snippet is visible to any logged in user.'),
|
||||
},
|
||||
public: {
|
||||
[SNIPPET_VISIBILITY_PUBLIC]: {
|
||||
label: __('Public'),
|
||||
icon: 'earth',
|
||||
description: __('The snippet can be accessed without any authentication.'),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -281,7 +281,7 @@ table {
|
|||
display: table;
|
||||
|
||||
svg {
|
||||
fill: $gray-darkest;
|
||||
fill: $gray-700;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
|
|
|
@ -855,7 +855,7 @@ $note-form-margin-left: 72px;
|
|||
line-height: $gl-line-height;
|
||||
|
||||
svg {
|
||||
fill: $gray-darkest;
|
||||
fill: $gray-700;
|
||||
}
|
||||
|
||||
&.discussion-create-issue-btn {
|
||||
|
@ -893,7 +893,7 @@ $note-form-margin-left: 72px;
|
|||
|
||||
.line-resolve-btn {
|
||||
margin-right: 5px;
|
||||
color: $gray-darkest;
|
||||
color: $gray-700;
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
# non_archived: boolean
|
||||
# archived: 'only' or boolean
|
||||
# min_access_level: integer
|
||||
# last_activity_after: datetime
|
||||
# last_activity_before: datetime
|
||||
#
|
||||
class ProjectsFinder < UnionFinder
|
||||
include CustomAttributesFilter
|
||||
|
@ -73,6 +75,8 @@ class ProjectsFinder < UnionFinder
|
|||
collection = by_archived(collection)
|
||||
collection = by_custom_attributes(collection)
|
||||
collection = by_deleted_status(collection)
|
||||
collection = by_last_activity_after(collection)
|
||||
collection = by_last_activity_before(collection)
|
||||
collection
|
||||
end
|
||||
|
||||
|
@ -179,6 +183,22 @@ class ProjectsFinder < UnionFinder
|
|||
params[:without_deleted].present? ? items.without_deleted : items
|
||||
end
|
||||
|
||||
def by_last_activity_after(items)
|
||||
if params[:last_activity_after].present?
|
||||
items.where("last_activity_at > ?", params[:last_activity_after]) # rubocop: disable CodeReuse/ActiveRecord
|
||||
else
|
||||
items
|
||||
end
|
||||
end
|
||||
|
||||
def by_last_activity_before(items)
|
||||
if params[:last_activity_before].present?
|
||||
items.where("last_activity_at < ?", params[:last_activity_before]) # rubocop: disable CodeReuse/ActiveRecord
|
||||
else
|
||||
items
|
||||
end
|
||||
end
|
||||
|
||||
def sort(items)
|
||||
params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.projects_order_id_desc
|
||||
end
|
||||
|
|
|
@ -159,6 +159,11 @@ class Label < ApplicationRecord
|
|||
on_project_boards(project_id).where(id: label_id).exists?
|
||||
end
|
||||
|
||||
# Generate a hex color based on hex-encoded value
|
||||
def self.color_for(value)
|
||||
"##{Digest::MD5.hexdigest(value)[0..5]}"
|
||||
end
|
||||
|
||||
def open_issues_count(user = nil)
|
||||
issues_count(user, state: 'opened')
|
||||
end
|
||||
|
|
|
@ -9,8 +9,8 @@ module Gitlab
|
|||
include Gitlab::Import::DatabaseHelpers
|
||||
|
||||
def perform(project_id, jira_issue_id, issue_attributes, waiter_key)
|
||||
issue_id = insert_and_return_id(issue_attributes, Issue)
|
||||
cache_issue_mapping(issue_id, jira_issue_id, project_id)
|
||||
issue_id = create_issue(issue_attributes, project_id)
|
||||
JiraImport.cache_issue_mapping(issue_id, jira_issue_id, project_id)
|
||||
rescue => ex
|
||||
# Todo: Record jira issue id(or better jira issue key),
|
||||
# so that we can report the list of failed to import issues to the user
|
||||
|
@ -27,9 +27,31 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def cache_issue_mapping(issue_id, jira_issue_id, project_id)
|
||||
cache_key = JiraImport.jira_issue_cache_key(project_id, jira_issue_id)
|
||||
Gitlab::Cache::Import::Caching.write(cache_key, issue_id)
|
||||
def create_issue(issue_attributes, project_id)
|
||||
issue_id = insert_and_return_id(issue_attributes, Issue)
|
||||
|
||||
label_issue(project_id, issue_id)
|
||||
|
||||
issue_id
|
||||
end
|
||||
|
||||
def label_issue(project_id, issue_id)
|
||||
label_id = JiraImport.get_import_label_id(project_id)
|
||||
return unless label_id
|
||||
|
||||
label_link_attrs = build_label_attrs(issue_id, label_id.to_i)
|
||||
insert_and_return_id(label_link_attrs, LabelLink)
|
||||
end
|
||||
|
||||
def build_label_attrs(issue_id, label_id)
|
||||
time = Time.now
|
||||
{
|
||||
label_id: label_id,
|
||||
target_id: issue_id,
|
||||
target_type: 'Issue',
|
||||
created_at: time,
|
||||
updated_at: time
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,10 +9,8 @@ module Gitlab
|
|||
private
|
||||
|
||||
def import(project)
|
||||
# fake labels import workers for now
|
||||
# new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage
|
||||
fake_waiter = JobWaiter.new
|
||||
Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { fake_waiter.key => fake_waiter.jobs_remaining }, :issues)
|
||||
job_waiter = Gitlab::JiraImport::LabelsImporter.new(project).execute
|
||||
Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { job_waiter.key => job_waiter.jobs_remaining }, :issues)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add app server type to usage ping
|
||||
merge_request: 28189
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add last_activity_before and last_activity_after filter to /api/projects endpoint
|
||||
merge_request: 28221
|
||||
author: Roger Meier
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix merge request thread’s icon buttons color
|
||||
merge_request: 28465
|
||||
author:
|
||||
type: other
|
|
@ -41,26 +41,28 @@ GET /projects
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `archived` | boolean | no | Limit by archived status |
|
||||
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
|
||||
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
|
||||
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
|
||||
| `search` | string | no | Return list of projects matching the search criteria |
|
||||
| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` |
|
||||
| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
|
||||
| `owned` | boolean | no | Limit by projects explicitly owned by the current user |
|
||||
| `membership` | boolean | no | Limit by projects that the current user is a member of |
|
||||
| `starred` | boolean | no | Limit by projects starred by the current user |
|
||||
| `statistics` | boolean | no | Include project statistics |
|
||||
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
|
||||
| `with_issues_enabled` | boolean | no | Limit by enabled issues 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 |
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `archived` | boolean | no | Limit by archived status |
|
||||
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
|
||||
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
|
||||
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
|
||||
| `search` | string | no | Return list of projects matching the search criteria |
|
||||
| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` |
|
||||
| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
|
||||
| `owned` | boolean | no | Limit by projects explicitly owned by the current user |
|
||||
| `membership` | boolean | no | Limit by projects that the current user is a member of |
|
||||
| `starred` | boolean | no | Limit by projects starred by the current user |
|
||||
| `statistics` | boolean | no | Include project statistics |
|
||||
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
|
||||
| `with_issues_enabled` | boolean | no | Limit by enabled issues 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 |
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `last_activity_after` | datetime | no | Limit results to projects with last_activity after specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
|
||||
| `last_activity_before` | datetime | no | Limit results to projects with last_activity before specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
|
||||
|
||||
NOTE: **Note:**
|
||||
This endpoint supports [keyset pagination](README.md#keyset-based-pagination) for selected `order_by` options.
|
||||
|
|
|
@ -6,7 +6,7 @@ type: reference
|
|||
|
||||
> **Notes:**
|
||||
>
|
||||
> - GitLab 8.12 introduced a new [CI job permissions model][newperms] and you
|
||||
> - GitLab 8.12 introduced a new [CI job permissions model](../user/project/new_ci_build_permissions_model.md) and you
|
||||
> are encouraged to upgrade your GitLab instance if you haven't done already.
|
||||
> If you are **not** using GitLab 8.12 or higher, you would need to work your way
|
||||
> around submodules in order to access the sources of e.g., `gitlab.com/group/project`
|
||||
|
|
|
@ -224,7 +224,7 @@ with failed showing at the top, skipped next and successful cases last.
|
|||
|
||||
This feature comes with the `:junit_pipeline_view` feature flag disabled by default. This
|
||||
feature is disabled due to some performance issues with very large data sets.
|
||||
When [the performance issue](https://gitlab.com/gitlab-org/gitlab/issues/37725) is resolved, the feature will be enabled by default.
|
||||
When [the performance is improved](https://gitlab.com/groups/gitlab-org/-/epics/2854), the feature will be enabled by default.
|
||||
|
||||
To enable this feature, ask a GitLab administrator with Rails console access to run the
|
||||
following command:
|
||||
|
|
|
@ -51,7 +51,7 @@ and when hovering or tapping (on touchscreen devices) they will expand and be sh
|
|||
|
||||
## Triggering multi-project pipelines through API
|
||||
|
||||
> - Use of `CI_JOB_TOKEN` for multi-project pipelines was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2017) in [GitLab Premium][ee] 9.3.
|
||||
> - Use of `CI_JOB_TOKEN` for multi-project pipelines was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2017) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.3.
|
||||
> - Use of `CI_JOB_TOKEN` for multi-project pipelines was [made available](https://gitlab.com/gitlab-org/gitlab/issues/31573) in all tiers in GitLab 12.4.
|
||||
|
||||
When you use the [`CI_JOB_TOKEN` to trigger pipelines](triggers/README.md#ci-job-token), GitLab
|
||||
|
|
|
@ -362,7 +362,7 @@ Check specific punctuation rules for [lists](#lists) below.
|
|||
| Rule | Example |
|
||||
| ---- | ------- |
|
||||
| Always end full sentences with a period. | _For a complete overview, read through this document._|
|
||||
| Always add a space after a period when beginning a new sentence | _For a complete overview, check this doc. For other references, check out this guide._ |
|
||||
| Always add a space after a period when beginning a new sentence. | _For a complete overview, check this doc. For other references, check out this guide._ |
|
||||
| Do not use double spaces. | --- |
|
||||
| Do not use tabs for indentation. Use spaces instead. You can configure your code editor to output spaces instead of tabs when pressing the tab key. | --- |
|
||||
| Use serial commas ("Oxford commas") before the final 'and/or' in a list. | _You can create new issues, merge requests, and milestones._ |
|
||||
|
@ -816,7 +816,7 @@ you have your MR reviewed and approved by a technical writer.
|
|||
|
||||
1. Copy the code below and paste it into your Markdown file.
|
||||
Leave a blank line above and below it. Do NOT edit the code
|
||||
(don't remove or add any spaces, etc).
|
||||
(don't remove or add any spaces).
|
||||
1. On YouTube, visit the video URL you want to display. Copy
|
||||
the regular URL from your browser (`https://www.youtube.com/watch?v=VIDEO-ID`)
|
||||
and replace the video title and link in the line under `<div class="video-fallback">`.
|
||||
|
@ -1000,7 +1000,7 @@ Whenever you need to call special attention to particular sentences,
|
|||
use the following markup for highlighting.
|
||||
|
||||
_Note that the alert boxes only work for one paragraph only. Multiple paragraphs,
|
||||
lists, headers, etc will not render correctly. For multiple lines, use blockquotes instead._
|
||||
lists, headers and so on, will not render correctly. For multiple lines, use blockquotes instead._
|
||||
|
||||
Alert boxes only render on the GitLab Docs site (<https://docs.gitlab.com>).
|
||||
Within GitLab itself, they will appear as plain Markdown text (like the examples
|
||||
|
|
|
@ -427,14 +427,14 @@ There are several Rake tasks available to you via the command line:
|
|||
- [`sudo gitlab-rake gitlab:elastic:projects_not_indexed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
|
||||
- Displays which projects are not indexed.
|
||||
- [`sudo gitlab-rake gitlab:elastic:reindex_to_another_cluster[<SOURCE_CLUSTER_URL>,<DESTINATION_CLUSTER_URL>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
|
||||
- Creates a new index in the destination cluster and triggers a [reindex from
|
||||
remote](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote)
|
||||
such that the index is fully copied from the source index. This can be
|
||||
useful when you wish to perform a migration to a new cluster as this
|
||||
reindexing should be quicker than reindexing via GitLab. Note that remote
|
||||
reindex requires your source cluster to be whitelisted in your destination
|
||||
cluster in Elasticsearch settings as per [the
|
||||
documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote).
|
||||
- Creates a new index in the destination cluster from the source index using
|
||||
Elasticsearch "reindex from remote", where the source index is copied to the
|
||||
destination. This is useful when migrating to a new cluster because it should be
|
||||
quicker than reindexing via GitLab.
|
||||
|
||||
NOTE: **Note:**
|
||||
Your source cluster must be whitelisted in your destination cluster's Elasticsearch
|
||||
settings. See [Reindex from remote](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote).
|
||||
|
||||
### Environment Variables
|
||||
|
||||
|
|
|
@ -1036,8 +1036,8 @@ Then add any extra changes you want. Your additions will be merged with the
|
|||
[Auto DevOps template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml) using the behaviour described for
|
||||
[`include`](../../ci/yaml/README.md#include).
|
||||
|
||||
It is also possible to copy and paste the contents of the [Auto DevOps
|
||||
template] into your project and edit this as needed. You may prefer to do it
|
||||
It is also possible to copy and paste the contents of the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml)
|
||||
into your project and edit this as needed. You may prefer to do it
|
||||
that way if you want to specifically remove any part of it.
|
||||
|
||||
### Customizing the Kubernetes namespace
|
||||
|
|
|
@ -20,7 +20,7 @@ comment at any time, and anyone with [Maintainer access level](../permissions.md
|
|||
higher can also edit a comment made by someone else.
|
||||
|
||||
You can also reply to a comment notification email to reply to the comment if
|
||||
[Reply by email] is configured for your GitLab instance. Replying to a standard comment
|
||||
[Reply by email](../../administration/reply_by_email.md) is configured for your GitLab instance. Replying to a standard comment
|
||||
creates another standard comment. Replying to a threaded comment creates a reply in the thread. Email replies support
|
||||
[Markdown](../markdown.md) and [quick actions](../project/quick_actions.md), just as if you replied from the web.
|
||||
|
||||
|
@ -140,7 +140,7 @@ You can now proceed to merge the merge request from the UI.
|
|||
|
||||
### Moving a single thread to a new issue
|
||||
|
||||
> [Introduced][ce-8266] in GitLab 9.1
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8266) in GitLab 9.1
|
||||
|
||||
To create a new issue for a single thread, you can use the **Resolve this
|
||||
thread in a new issue** button.
|
||||
|
|
|
@ -35,7 +35,7 @@ to be careful when using canaries with user-facing changes, because by default,
|
|||
requests from the same user will be randomly distributed between canary and
|
||||
non-canary pods, which could result in confusion or even errors. If needed, you
|
||||
may want to consider [setting `service.spec.sessionAffinity` to `ClientIP` in
|
||||
your Kubernetes service definitions][kube-net](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies), but that is beyond the scope of
|
||||
your Kubernetes service definitions](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies), but that is beyond the scope of
|
||||
this document.
|
||||
|
||||
## Enabling Canary Deployments
|
||||
|
|
|
@ -459,7 +459,7 @@ Note the following properties:
|
|||
| Property | Type | Required | Description |
|
||||
| ------ | ------ | ------ | ------ |
|
||||
| `type` | string | yes | Type of panel to be rendered. For bar chart types, set to `bar` |
|
||||
| `query_range` | yes | yes | For bar chart, you must use a [range query]
|
||||
| `query_range` | yes | yes | For bar chart, you must use a [range query](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries)
|
||||
|
||||
![bar chart panel type](img/prometheus_dashboard_bar_chart_panel_type_v12.10.png)
|
||||
|
||||
|
|
|
@ -505,20 +505,28 @@ module API
|
|||
|
||||
protected
|
||||
|
||||
def project_finder_params_ce
|
||||
finder_params = { without_deleted: true }
|
||||
def project_finder_params_visibility_ce
|
||||
finder_params = {}
|
||||
finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level]
|
||||
finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility]
|
||||
finder_params[:owned] = true if params[:owned].present?
|
||||
finder_params[:non_public] = true if params[:membership].present?
|
||||
finder_params[:starred] = true if params[:starred].present?
|
||||
finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility]
|
||||
finder_params[:archived] = archived_param unless params[:archived].nil?
|
||||
finder_params
|
||||
end
|
||||
|
||||
def project_finder_params_ce
|
||||
finder_params = project_finder_params_visibility_ce
|
||||
finder_params[:without_deleted] = true
|
||||
finder_params[:search] = params[:search] if params[:search]
|
||||
finder_params[:search_namespaces] = true if params[:search_namespaces].present?
|
||||
finder_params[:user] = params.delete(:user) if params[:user]
|
||||
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[:id_after] = params[:id_after] if params[:id_after]
|
||||
finder_params[:id_before] = params[:id_before] if params[:id_before]
|
||||
finder_params[:last_activity_after] = params[:last_activity_after] if params[:last_activity_after]
|
||||
finder_params[:last_activity_before] = params[:last_activity_before] if params[:last_activity_before]
|
||||
finder_params
|
||||
end
|
||||
|
||||
|
|
|
@ -73,6 +73,8 @@ module API
|
|||
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'
|
||||
optional :last_activity_after, type: DateTime, desc: 'Limit results to projects with last_activity after specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
|
||||
optional :last_activity_before, type: DateTime, desc: 'Limit results to projects with last_activity before specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
|
||||
|
||||
use :optional_filter_params_ee
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ module Gitlab
|
|||
|
||||
FAILED_ISSUES_COUNTER_KEY = 'jira-import/failed/%{project_id}/%{collection_type}'
|
||||
NEXT_ITEMS_START_AT_KEY = 'jira-import/paginator/%{project_id}/%{collection_type}'
|
||||
JIRA_IMPORT_LABEL = 'jira-import/import-label/%{project_id}'
|
||||
ITEMS_MAPPER_CACHE_KEY = 'jira-import/items-mapper/%{project_id}/%{collection_type}/%{jira_isssue_id}'
|
||||
ALREADY_IMPORTED_ITEMS_CACHE_KEY = 'jira-importer/already-imported/%{project}/%{collection_type}'
|
||||
|
||||
|
@ -25,23 +26,45 @@ module Gitlab
|
|||
FAILED_ISSUES_COUNTER_KEY % { project_id: project_id, collection_type: :issues }
|
||||
end
|
||||
|
||||
def self.import_label_cache_key(project_id)
|
||||
JIRA_IMPORT_LABEL % { project_id: project_id }
|
||||
end
|
||||
|
||||
def self.increment_issue_failures(project_id)
|
||||
Gitlab::Cache::Import::Caching.increment(self.failed_issues_counter_cache_key(project_id))
|
||||
cache_class.increment(self.failed_issues_counter_cache_key(project_id))
|
||||
end
|
||||
|
||||
def self.get_issues_next_start_at(project_id)
|
||||
Gitlab::Cache::Import::Caching.read(self.jira_issues_next_page_cache_key(project_id)).to_i
|
||||
cache_class.read(self.jira_issues_next_page_cache_key(project_id)).to_i
|
||||
end
|
||||
|
||||
def self.store_issues_next_started_at(project_id, value)
|
||||
cache_key = self.jira_issues_next_page_cache_key(project_id)
|
||||
Gitlab::Cache::Import::Caching.write(cache_key, value)
|
||||
cache_class.write(cache_key, value)
|
||||
end
|
||||
|
||||
def self.cache_issue_mapping(issue_id, jira_issue_id, project_id)
|
||||
cache_key = JiraImport.jira_issue_cache_key(project_id, jira_issue_id)
|
||||
cache_class.write(cache_key, issue_id)
|
||||
end
|
||||
|
||||
def self.get_import_label_id(project_id)
|
||||
cache_class.read(JiraImport.import_label_cache_key(project_id))
|
||||
end
|
||||
|
||||
def self.cache_import_label_id(project_id, label_id)
|
||||
cache_class.write(JiraImport.import_label_cache_key(project_id), label_id)
|
||||
end
|
||||
|
||||
def self.cache_cleanup(project_id)
|
||||
Gitlab::Cache::Import::Caching.expire(self.failed_issues_counter_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT)
|
||||
Gitlab::Cache::Import::Caching.expire(self.jira_issues_next_page_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT)
|
||||
Gitlab::Cache::Import::Caching.expire(self.already_imported_cache_key(:issues, project_id), JIRA_IMPORT_CACHE_TIMEOUT)
|
||||
cache_class.expire(self.import_label_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT)
|
||||
cache_class.expire(self.failed_issues_counter_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT)
|
||||
cache_class.expire(self.jira_issues_next_page_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT)
|
||||
cache_class.expire(self.already_imported_cache_key(:issues, project_id), JIRA_IMPORT_CACHE_TIMEOUT)
|
||||
end
|
||||
|
||||
def self.cache_class
|
||||
Gitlab::Cache::Import::Caching
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module JiraImport
|
||||
class LabelsImporter < BaseImporter
|
||||
attr_reader :job_waiter
|
||||
|
||||
def initialize(project)
|
||||
super
|
||||
@job_waiter = JobWaiter.new
|
||||
end
|
||||
|
||||
def execute
|
||||
create_import_label(project)
|
||||
import_jira_labels
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_import_label(project)
|
||||
label = Labels::CreateService.new(build_label_attrs(project)).execute(project: project)
|
||||
raise Projects::ImportService::Error, _('Failed to create import label for jira import.') unless label
|
||||
|
||||
JiraImport.cache_import_label_id(project.id, label.id)
|
||||
end
|
||||
|
||||
def build_label_attrs(project)
|
||||
import_start_time = project&.import_state&.last_update_started_at || Time.now
|
||||
title = "jira-import-#{import_start_time.strftime('%Y-%m-%d-%H-%M-%S')}"
|
||||
description = "Label for issues that were imported from jira on #{import_start_time.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
color = "#{Label.color_for(title)}"
|
||||
|
||||
{ title: title, description: description, color: color }
|
||||
end
|
||||
|
||||
def import_jira_labels
|
||||
# todo: import jira labels, see https://gitlab.com/gitlab-org/gitlab/-/issues/212651
|
||||
job_waiter
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -174,10 +174,19 @@ module Gitlab
|
|||
git: { version: Gitlab::Git.version },
|
||||
gitaly: { version: Gitaly::Server.all.first.server_version, servers: Gitaly::Server.count, filesystems: Gitaly::Server.filesystems },
|
||||
gitlab_pages: { enabled: Gitlab.config.pages.enabled, version: Gitlab::Pages::VERSION },
|
||||
database: { adapter: Gitlab::Database.adapter_name, version: Gitlab::Database.version }
|
||||
database: { adapter: Gitlab::Database.adapter_name, version: Gitlab::Database.version },
|
||||
app_server: { type: app_server_type }
|
||||
}
|
||||
end
|
||||
|
||||
def app_server_type
|
||||
Gitlab::Runtime.identify.to_s
|
||||
rescue Gitlab::Runtime::IdentificationError => e
|
||||
Gitlab::AppLogger.error(e.message)
|
||||
Gitlab::ErrorTracking.track_exception(e)
|
||||
'unknown_app_server_type'
|
||||
end
|
||||
|
||||
def ingress_modsecurity_usage
|
||||
::Clusters::Applications::IngressModsecurityUsageService.new.execute
|
||||
end
|
||||
|
|
|
@ -6901,9 +6901,6 @@ msgstr ""
|
|||
msgid "Detect host keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "Detected %{timeago} in pipeline %{pipelineLink}"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevOps Score"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8481,6 +8478,9 @@ msgstr ""
|
|||
msgid "Failed to create a branch for this issue. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to create import label for jira import."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to create repository"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22577,18 +22577,33 @@ msgstr ""
|
|||
msgid "VulnerabilityManagement|Confirm"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Confirmed %{timeago} by %{user}"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Create issue"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Detected %{timeago} in pipeline %{pipelineLink}"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Dismiss"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Dismissed %{timeago} by %{user}"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Something went wrong, could not get user."
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -222,6 +222,28 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
|
|||
it { is_expected.to match_array([public_project, internal_project]) }
|
||||
end
|
||||
|
||||
describe 'filter by last_activity_after' do
|
||||
let(:params) { { last_activity_after: 60.minutes.ago } }
|
||||
|
||||
before do
|
||||
internal_project.update(last_activity_at: Time.now)
|
||||
public_project.update(last_activity_at: 61.minutes.ago)
|
||||
end
|
||||
|
||||
it { is_expected.to match_array([internal_project]) }
|
||||
end
|
||||
|
||||
describe 'filter by last_activity_before' do
|
||||
let(:params) { { last_activity_before: 60.minutes.ago } }
|
||||
|
||||
before do
|
||||
internal_project.update(last_activity_at: Time.now)
|
||||
public_project.update(last_activity_at: 61.minutes.ago)
|
||||
end
|
||||
|
||||
it { is_expected.to match_array([public_project]) }
|
||||
end
|
||||
|
||||
describe 'sorting' do
|
||||
let(:params) { { sort: 'name_asc' } }
|
||||
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
let id = 1;
|
||||
|
||||
// Code taken from: https://gist.github.com/6174/6062387
|
||||
const getRandomString = () =>
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 15) +
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 15);
|
||||
|
||||
const getRandomUrl = () => `https://${getRandomString()}.com/${getRandomString()}`;
|
||||
|
||||
export default {
|
||||
createNumberRandomUsers(numberUsers) {
|
||||
const users = [];
|
||||
for (let i = 0; i < numberUsers; i += 1) {
|
||||
users.push({
|
||||
avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
id: i + 1,
|
||||
name: `GitLab User ${i}`,
|
||||
username: `gitlab${i}`,
|
||||
avatar_url: getRandomUrl(),
|
||||
id: id + 1,
|
||||
name: getRandomString(),
|
||||
username: getRandomString(),
|
||||
user_path: getRandomUrl(),
|
||||
});
|
||||
|
||||
id += 1;
|
||||
}
|
||||
return users;
|
||||
},
|
||||
|
||||
createRandomUser() {
|
||||
return this.createNumberRandomUsers(1)[0];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -101,14 +101,14 @@ describe('Assignee component', () => {
|
|||
|
||||
const first = collapsedChildren.at(0);
|
||||
|
||||
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar);
|
||||
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
|
||||
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
|
||||
|
||||
expect(trimText(first.find('.author').text())).toBe(users[0].name);
|
||||
|
||||
const second = collapsedChildren.at(1);
|
||||
|
||||
expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar);
|
||||
expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar_url);
|
||||
expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`);
|
||||
|
||||
expect(trimText(second.find('.author').text())).toBe(users[1].name);
|
||||
|
@ -127,7 +127,7 @@ describe('Assignee component', () => {
|
|||
|
||||
const first = collapsedChildren.at(0);
|
||||
|
||||
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar);
|
||||
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
|
||||
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
|
||||
|
||||
expect(trimText(first.find('.author').text())).toBe(users[0].name);
|
||||
|
|
|
@ -23,7 +23,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
|
|||
id="visibility-level-setting"
|
||||
>
|
||||
<gl-form-radio-group-stub
|
||||
checked="0"
|
||||
checked="private"
|
||||
disabledfield="disabled"
|
||||
htmlfield="html"
|
||||
options=""
|
||||
|
@ -33,7 +33,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
|
|||
>
|
||||
<gl-form-radio-stub
|
||||
class="mb-3"
|
||||
value="0"
|
||||
value="private"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-items-center"
|
||||
|
@ -44,7 +44,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
|
|||
/>
|
||||
|
||||
<span
|
||||
class="font-weight-bold ml-1"
|
||||
class="font-weight-bold ml-1 js-visibility-option"
|
||||
>
|
||||
Private
|
||||
</span>
|
||||
|
@ -52,7 +52,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
|
|||
</gl-form-radio-stub>
|
||||
<gl-form-radio-stub
|
||||
class="mb-3"
|
||||
value="1"
|
||||
value="internal"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-items-center"
|
||||
|
@ -63,7 +63,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
|
|||
/>
|
||||
|
||||
<span
|
||||
class="font-weight-bold ml-1"
|
||||
class="font-weight-bold ml-1 js-visibility-option"
|
||||
>
|
||||
Internal
|
||||
</span>
|
||||
|
@ -71,7 +71,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
|
|||
</gl-form-radio-stub>
|
||||
<gl-form-radio-stub
|
||||
class="mb-3"
|
||||
value="2"
|
||||
value="public"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-items-center"
|
||||
|
@ -82,7 +82,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
|
|||
/>
|
||||
|
||||
<span
|
||||
class="font-weight-bold ml-1"
|
||||
class="font-weight-bold ml-1 js-visibility-option"
|
||||
>
|
||||
Public
|
||||
</span>
|
||||
|
|
|
@ -2,18 +2,21 @@ import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
|
|||
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
|
||||
import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
jest.mock('~/blob/utils', () => jest.fn());
|
||||
|
||||
describe('Snippet Blob Edit component', () => {
|
||||
let wrapper;
|
||||
const content = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
|
||||
const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
|
||||
const fileName = 'lorem.txt';
|
||||
const findHeader = () => wrapper.find(BlobHeaderEdit);
|
||||
const findContent = () => wrapper.find(BlobContentEdit);
|
||||
|
||||
function createComponent() {
|
||||
wrapper = shallowMount(SnippetBlobEdit, {
|
||||
propsData: {
|
||||
content,
|
||||
value,
|
||||
fileName,
|
||||
},
|
||||
});
|
||||
|
@ -33,8 +36,20 @@ describe('Snippet Blob Edit component', () => {
|
|||
});
|
||||
|
||||
it('renders required components', () => {
|
||||
expect(wrapper.contains(BlobHeaderEdit)).toBe(true);
|
||||
expect(wrapper.contains(BlobContentEdit)).toBe(true);
|
||||
expect(findHeader().exists()).toBe(true);
|
||||
expect(findContent().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionality', () => {
|
||||
it('emits "name-change" event when the file name gets changed', () => {
|
||||
expect(wrapper.emitted('name-change')).toBeUndefined();
|
||||
const newFilename = 'foo.bar';
|
||||
findHeader().vm.$emit('input', newFilename);
|
||||
|
||||
return nextTick().then(() => {
|
||||
expect(wrapper.emitted('name-change')[0]).toEqual([newFilename]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,11 +6,12 @@ describe('Snippet Description Edit component', () => {
|
|||
const defaultDescription = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
|
||||
const markdownPreviewPath = 'foo/';
|
||||
const markdownDocsPath = 'help/';
|
||||
const findTextarea = () => wrapper.find('textarea');
|
||||
|
||||
function createComponent(description = defaultDescription) {
|
||||
function createComponent(value = defaultDescription) {
|
||||
wrapper = shallowMount(SnippetDescriptionEdit, {
|
||||
propsData: {
|
||||
description,
|
||||
value,
|
||||
markdownPreviewPath,
|
||||
markdownDocsPath,
|
||||
},
|
||||
|
@ -49,4 +50,14 @@ describe('Snippet Description Edit component', () => {
|
|||
expect(isHidden('.js-expanded')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionality', () => {
|
||||
it('emits "input" event when description is changed', () => {
|
||||
expect(wrapper.emitted('input')).toBeUndefined();
|
||||
const newDescription = 'dummy';
|
||||
findTextarea().setValue(newDescription);
|
||||
|
||||
expect(wrapper.emitted('input')[0]).toEqual([newDescription]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,37 +1,42 @@
|
|||
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
|
||||
import { GlFormRadio } from '@gitlab/ui';
|
||||
import { SNIPPET_VISIBILITY } from '~/snippets/constants';
|
||||
import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
|
||||
import {
|
||||
SNIPPET_VISIBILITY,
|
||||
SNIPPET_VISIBILITY_PRIVATE,
|
||||
SNIPPET_VISIBILITY_INTERNAL,
|
||||
SNIPPET_VISIBILITY_PUBLIC,
|
||||
} from '~/snippets/constants';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
|
||||
describe('Snippet Visibility Edit component', () => {
|
||||
let wrapper;
|
||||
let radios;
|
||||
const defaultHelpLink = '/foo/bar';
|
||||
const defaultVisibilityLevel = '0';
|
||||
const defaultVisibilityLevel = 'private';
|
||||
|
||||
function findElements(sel) {
|
||||
return wrapper.findAll(sel);
|
||||
}
|
||||
|
||||
function createComponent(
|
||||
{
|
||||
helpLink = defaultHelpLink,
|
||||
isProjectSnippet = false,
|
||||
visibilityLevel = defaultVisibilityLevel,
|
||||
} = {},
|
||||
deep = false,
|
||||
) {
|
||||
function createComponent(propsData = {}, deep = false) {
|
||||
const method = deep ? mount : shallowMount;
|
||||
wrapper = method.call(this, SnippetVisibilityEdit, {
|
||||
propsData: {
|
||||
helpLink,
|
||||
isProjectSnippet,
|
||||
visibilityLevel,
|
||||
helpLink: defaultHelpLink,
|
||||
isProjectSnippet: false,
|
||||
value: defaultVisibilityLevel,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
radios = findElements(GlFormRadio);
|
||||
}
|
||||
|
||||
const findLabel = () => wrapper.find('label');
|
||||
const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio);
|
||||
const findRadiosData = () =>
|
||||
findRadios().wrappers.map(x => {
|
||||
return {
|
||||
value: x.find('input').attributes('value'),
|
||||
icon: x.find(GlIcon).props('name'),
|
||||
description: x.find('.help-text').text(),
|
||||
text: x.find('.js-visibility-option').text(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
@ -42,53 +47,66 @@ describe('Snippet Visibility Edit component', () => {
|
|||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each`
|
||||
label | value
|
||||
${SNIPPET_VISIBILITY.private.label} | ${`0`}
|
||||
${SNIPPET_VISIBILITY.internal.label} | ${`1`}
|
||||
${SNIPPET_VISIBILITY.public.label} | ${`2`}
|
||||
`('should render correct $label label', ({ label, value }) => {
|
||||
createComponent();
|
||||
const radio = radios.at(parseInt(value, 10));
|
||||
it('renders visibility options', () => {
|
||||
createComponent({}, true);
|
||||
|
||||
expect(radio.attributes('value')).toBe(value);
|
||||
expect(radio.text()).toContain(label);
|
||||
expect(findRadiosData()).toEqual([
|
||||
{
|
||||
value: SNIPPET_VISIBILITY_PRIVATE,
|
||||
icon: SNIPPET_VISIBILITY.private.icon,
|
||||
text: SNIPPET_VISIBILITY.private.label,
|
||||
description: SNIPPET_VISIBILITY.private.description,
|
||||
},
|
||||
{
|
||||
value: SNIPPET_VISIBILITY_INTERNAL,
|
||||
icon: SNIPPET_VISIBILITY.internal.icon,
|
||||
text: SNIPPET_VISIBILITY.internal.label,
|
||||
description: SNIPPET_VISIBILITY.internal.description,
|
||||
},
|
||||
{
|
||||
value: SNIPPET_VISIBILITY_PUBLIC,
|
||||
icon: SNIPPET_VISIBILITY.public.icon,
|
||||
text: SNIPPET_VISIBILITY.public.label,
|
||||
description: SNIPPET_VISIBILITY.public.description,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('rendered help-text', () => {
|
||||
it.each`
|
||||
description | value | label
|
||||
${SNIPPET_VISIBILITY.private.description} | ${`0`} | ${SNIPPET_VISIBILITY.private.label}
|
||||
${SNIPPET_VISIBILITY.internal.description} | ${`1`} | ${SNIPPET_VISIBILITY.internal.label}
|
||||
${SNIPPET_VISIBILITY.public.description} | ${`2`} | ${SNIPPET_VISIBILITY.public.label}
|
||||
`('should render correct $label description', ({ description, value }) => {
|
||||
createComponent({}, true);
|
||||
it('when project snippet, renders special private description', () => {
|
||||
createComponent({ isProjectSnippet: true }, true);
|
||||
|
||||
const help = findElements('.help-text').at(parseInt(value, 10));
|
||||
|
||||
expect(help.text()).toBe(description);
|
||||
expect(findRadiosData()[0]).toEqual({
|
||||
value: SNIPPET_VISIBILITY_PRIVATE,
|
||||
icon: SNIPPET_VISIBILITY.private.icon,
|
||||
text: SNIPPET_VISIBILITY.private.label,
|
||||
description: SNIPPET_VISIBILITY.private.description_project,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correct Private description for a project snippet', () => {
|
||||
createComponent({ isProjectSnippet: true }, true);
|
||||
it('renders label help link', () => {
|
||||
createComponent();
|
||||
|
||||
const helpText = findElements('.help-text')
|
||||
.at(0)
|
||||
.text();
|
||||
expect(
|
||||
findLabel()
|
||||
.find(GlLink)
|
||||
.attributes('href'),
|
||||
).toBe(defaultHelpLink);
|
||||
});
|
||||
|
||||
expect(helpText).not.toContain(SNIPPET_VISIBILITY.private.description);
|
||||
expect(helpText).toBe(SNIPPET_VISIBILITY.private.description_project);
|
||||
});
|
||||
it('when helpLink is not defined, does not render label help link', () => {
|
||||
createComponent({ helpLink: null });
|
||||
|
||||
expect(findLabel().contains(GlLink)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionality', () => {
|
||||
it('pre-selects correct option in the list', () => {
|
||||
const pos = 1;
|
||||
const value = SNIPPET_VISIBILITY_INTERNAL;
|
||||
|
||||
createComponent({ visibilityLevel: `${pos}` }, true);
|
||||
const radio = radios.at(pos);
|
||||
expect(radio.find('input[type="radio"]').element.checked).toBe(true);
|
||||
createComponent({ value });
|
||||
|
||||
expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::JiraImport::LabelsImporter do
|
||||
let(:user) { create(:user) }
|
||||
let(:jira_import_data) do
|
||||
data = JiraImportData.new
|
||||
data << JiraImportData::JiraProjectDetails.new('XX', Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: user.id, name: user.name })
|
||||
data
|
||||
end
|
||||
let(:project) { create(:project, import_data: jira_import_data) }
|
||||
let!(:jira_service) { create(:jira_service, project: project) }
|
||||
|
||||
subject { described_class.new(project).execute }
|
||||
|
||||
before do
|
||||
stub_feature_flags(jira_issue_import: true)
|
||||
end
|
||||
|
||||
describe '#execute', :clean_gitlab_redis_cache do
|
||||
context 'when label creation failes' do
|
||||
before do
|
||||
allow_next_instance_of(Labels::CreateService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises error' do
|
||||
expect { subject }.to raise_error(Projects::ImportService::Error, 'Failed to create import label for jira import.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when label is created successfully' do
|
||||
it 'creates import label' do
|
||||
expect { subject }.to change { Label.count }.by(1)
|
||||
end
|
||||
|
||||
it 'caches import label' do
|
||||
expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.import_label_cache_key(project.id))).to be nil
|
||||
|
||||
subject
|
||||
|
||||
expect(Gitlab::JiraImport.get_import_label_id(project.id).to_i).to be > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -147,6 +147,8 @@ describe Gitlab::UsageData, :aggregate_failures do
|
|||
subject { described_class.components_usage_data }
|
||||
|
||||
it 'gathers components usage data' do
|
||||
expect(Gitlab::UsageData).to receive(:app_server_type).and_return('server_type')
|
||||
expect(subject[:app_server][:type]).to eq('server_type')
|
||||
expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled)
|
||||
expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION)
|
||||
expect(subject[:git][:version]).to eq(Gitlab::Git.version)
|
||||
|
@ -159,6 +161,28 @@ describe Gitlab::UsageData, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#app_server_type' do
|
||||
subject { described_class.app_server_type }
|
||||
|
||||
it 'successfully identifies runtime and returns the identifier' do
|
||||
expect(Gitlab::Runtime).to receive(:identify).and_return(:runtime_identifier)
|
||||
|
||||
is_expected.to eq('runtime_identifier')
|
||||
end
|
||||
|
||||
context 'when runtime is not identified' do
|
||||
let(:exception) { Gitlab::Runtime::IdentificationError.new('exception message from runtime identify') }
|
||||
|
||||
it 'logs the exception and returns unknown app server type' do
|
||||
expect(Gitlab::Runtime).to receive(:identify).and_raise(exception)
|
||||
|
||||
expect(Gitlab::AppLogger).to receive(:error).with(exception.message)
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception)
|
||||
expect(subject).to eq('unknown_app_server_type')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cycle_analytics_usage_data' do
|
||||
subject { described_class.cycle_analytics_usage_data }
|
||||
|
||||
|
|
|
@ -32,12 +32,28 @@ describe Gitlab::JiraImport::ImportIssueWorker do
|
|||
end
|
||||
|
||||
context 'when record is successfully inserted' do
|
||||
before do
|
||||
subject.perform(project.id, 123, issue_attrs, 'some-key')
|
||||
let(:label) { create(:label, project: project) }
|
||||
|
||||
context 'when import label does not exist' do
|
||||
it 'does not record import failure' do
|
||||
subject.perform(project.id, 123, issue_attrs, 'some-key')
|
||||
|
||||
expect(label.issues.count).to eq(0)
|
||||
expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not record import failure' do
|
||||
expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0)
|
||||
context 'when import label exists' do
|
||||
before do
|
||||
Gitlab::JiraImport.cache_import_label_id(project.id, label.id)
|
||||
end
|
||||
|
||||
it 'does not record import failure' do
|
||||
subject.perform(project.id, 123, issue_attrs, 'some-key')
|
||||
|
||||
expect(label.issues.count).to eq(1)
|
||||
expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
describe 'modules' do
|
||||
|
@ -30,9 +31,24 @@ describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
|
|||
end
|
||||
|
||||
context 'when import started' do
|
||||
let(:jira_import_data) do
|
||||
data = JiraImportData.new
|
||||
data << JiraImportData::JiraProjectDetails.new('XX', Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: user.id, name: user.name })
|
||||
data
|
||||
end
|
||||
let(:project) { create(:project, import_data: jira_import_data) }
|
||||
let!(:jira_service) { create(:jira_service, project: project) }
|
||||
let!(:import_state) { create(:import_state, status: :started, project: project) }
|
||||
|
||||
it_behaves_like 'advance to next stage', :issues
|
||||
|
||||
it 'executes labels importer' do
|
||||
expect_next_instance_of(Gitlab::JiraImport::LabelsImporter) do |instance|
|
||||
expect(instance).to receive(:execute).and_return(Gitlab::JobWaiter.new)
|
||||
end
|
||||
|
||||
described_class.new.perform(project.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue