Commit b11f7057 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent e50050a8
...@@ -499,6 +499,7 @@ ...@@ -499,6 +499,7 @@
.review:rules:review-gcp-cleanup: .review:rules:review-gcp-cleanup:
rules: rules:
- <<: *if-dot-com-gitlab-org-merge-request - <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
when: manual when: manual
- <<: *if-dot-com-gitlab-org-schedule - <<: *if-dot-com-gitlab-org-schedule
when: on_success when: on_success
......
...@@ -422,7 +422,6 @@ RSpec/RepeatedExample: ...@@ -422,7 +422,6 @@ RSpec/RepeatedExample:
- 'spec/services/notification_service_spec.rb' - 'spec/services/notification_service_spec.rb'
- 'spec/services/web_hook_service_spec.rb' - 'spec/services/web_hook_service_spec.rb'
- 'ee/spec/models/group_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/requests/api/merge_request_approvals_spec.rb'
- 'ee/spec/services/boards/lists/update_service_spec.rb' - 'ee/spec/services/boards/lists/update_service_spec.rb'
- 'ee/spec/services/geo/repository_verification_primary_service_spec.rb' - 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
......
...@@ -172,7 +172,6 @@ export default { ...@@ -172,7 +172,6 @@ export default {
/> />
<a <a
v-once v-once
id="diffFile.file_path"
ref="titleWrapper" ref="titleWrapper"
class="append-right-4" class="append-right-4"
:href="titleLink" :href="titleLink"
......
...@@ -7,21 +7,18 @@ export default { ...@@ -7,21 +7,18 @@ export default {
BlobHeaderEdit, BlobHeaderEdit,
BlobContentEdit, BlobContentEdit,
}, },
inheritAttrs: false,
props: { props: {
content: {
type: String,
required: true,
},
fileName: { fileName: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
}, },
data() { methods: {
return { emitFileNameChange(newFileName) {
name: this.fileName, this.$emit('name-change', newFileName);
blobContent: this.content, },
};
}, },
}; };
</script> </script>
...@@ -29,8 +26,8 @@ export default { ...@@ -29,8 +26,8 @@ export default {
<div class="form-group file-editor"> <div class="form-group file-editor">
<label>{{ s__('Snippets|File') }}</label> <label>{{ s__('Snippets|File') }}</label>
<div class="file-holder snippet"> <div class="file-holder snippet">
<blob-header-edit v-model="name" /> <blob-header-edit :value="fileName" @input="emitFileNameChange" />
<blob-content-edit v-model="blobContent" :file-name="name" /> <blob-content-edit v-bind="$attrs" :file-name="fileName" v-on="$listeners" />
</div> </div>
</div> </div>
</template> </template>
...@@ -9,11 +9,6 @@ export default { ...@@ -9,11 +9,6 @@ export default {
MarkdownField, MarkdownField,
}, },
props: { props: {
description: {
type: String,
default: '',
required: false,
},
markdownPreviewPath: { markdownPreviewPath: {
type: String, type: String,
required: true, required: true,
...@@ -22,11 +17,11 @@ export default { ...@@ -22,11 +17,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
}, value: {
data() { type: String,
return { required: false,
text: this.description, default: '',
}; },
}, },
mounted() { mounted() {
setupCollapsibleInputs(); setupCollapsibleInputs();
...@@ -37,7 +32,7 @@ export default { ...@@ -37,7 +32,7 @@ export default {
<div class="form-group js-description-input"> <div class="form-group js-description-input">
<label>{{ s__('Snippets|Description (optional)') }}</label> <label>{{ s__('Snippets|Description (optional)') }}</label>
<div class="js-collapsible-input"> <div class="js-collapsible-input">
<div class="js-collapsed" :class="{ 'd-none': text }"> <div class="js-collapsed" :class="{ 'd-none': value }">
<gl-form-input <gl-form-input
class="form-control" class="form-control"
:placeholder=" :placeholder="
...@@ -50,20 +45,21 @@ export default { ...@@ -50,20 +45,21 @@ export default {
</div> </div>
<markdown-field <markdown-field
class="js-expanded" class="js-expanded"
:class="{ 'd-none': !text }" :class="{ 'd-none': !value }"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
> >
<textarea <textarea
id="snippet-description" id="snippet-description"
slot="textarea" slot="textarea"
v-model="text"
class="note-textarea js-gfm-input js-autosize markdown-area class="note-textarea js-gfm-input js-autosize markdown-area
qa-description-textarea" qa-description-textarea"
dir="auto" dir="auto"
data-supports-quick-actions="false" data-supports-quick-actions="false"
:value="value"
:aria-label="__('Description')" :aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')" :placeholder="__('Write a comment or drag your files here…')"
@input="$emit('input', $event.target.value)"
> >
</textarea> </textarea>
</markdown-field> </markdown-field>
......
<script> <script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui'; 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 { export default {
components: { components: {
...@@ -21,48 +21,22 @@ export default { ...@@ -21,48 +21,22 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
visibilityLevel: { value: {
type: String, type: String,
default: '0',
required: false, required: false,
default: SNIPPET_VISIBILITY_PRIVATE,
}, },
}, },
data() {
return {
selected: this.visibilityLevel,
};
},
computed: { computed: {
visibilityOptions() { visibilityOptions() {
return [ const options = [];
{ Object.keys(SNIPPET_VISIBILITY).forEach(key => {
value: '0', options.push({
icon: 'lock', value: key,
text: SNIPPET_VISIBILITY.private.label, ...SNIPPET_VISIBILITY[key],
description: this.isProjectSnippet });
? SNIPPET_VISIBILITY.private.description_project });
: SNIPPET_VISIBILITY.private.description, return options;
},
{
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;
}
}, },
}, },
}; };
...@@ -76,18 +50,22 @@ export default { ...@@ -76,18 +50,22 @@ export default {
/></gl-link> /></gl-link>
</label> </label>
<gl-form-group id="visibility-level-setting"> <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 <gl-form-radio
v-for="option in visibilityOptions" v-for="option in visibilityOptions"
:key="option.icon" :key="option.value"
:value="option.value" :value="option.value"
class="mb-3" class="mb-3"
> >
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<gl-icon :size="16" :name="option.icon" /> <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> </div>
<template #help>{{ option.description }}</template> <template #help>{{
isProjectSnippet && option.description_project
? option.description_project
: option.description
}}</template>
</gl-form-radio> </gl-form-radio>
</gl-form-radio-group> </gl-form-radio-group>
</gl-form-group> </gl-form-group>
......
...@@ -5,17 +5,20 @@ export const SNIPPET_VISIBILITY_INTERNAL = 'internal'; ...@@ -5,17 +5,20 @@ export const SNIPPET_VISIBILITY_INTERNAL = 'internal';
export const SNIPPET_VISIBILITY_PUBLIC = 'public'; export const SNIPPET_VISIBILITY_PUBLIC = 'public';
export const SNIPPET_VISIBILITY = { export const SNIPPET_VISIBILITY = {
private: { [SNIPPET_VISIBILITY_PRIVATE]: {
label: __('Private'), label: __('Private'),
icon: 'lock',
description: __('The snippet is visible only to me.'), description: __('The snippet is visible only to me.'),
description_project: __('The snippet is visible only to project members.'), description_project: __('The snippet is visible only to project members.'),
}, },
internal: { [SNIPPET_VISIBILITY_INTERNAL]: {
label: __('Internal'), label: __('Internal'),
icon: 'shield',
description: __('The snippet is visible to any logged in user.'), description: __('The snippet is visible to any logged in user.'),
}, },
public: { [SNIPPET_VISIBILITY_PUBLIC]: {
label: __('Public'), label: __('Public'),
icon: 'earth',
description: __('The snippet can be accessed without any authentication.'), description: __('The snippet can be accessed without any authentication.'),
}, },
}; };
...@@ -281,7 +281,7 @@ table { ...@@ -281,7 +281,7 @@ table {
display: table; display: table;
svg { svg {
fill: $gray-darkest; fill: $gray-700;
} }
.btn-group { .btn-group {
......
...@@ -855,7 +855,7 @@ $note-form-margin-left: 72px; ...@@ -855,7 +855,7 @@ $note-form-margin-left: 72px;
line-height: $gl-line-height; line-height: $gl-line-height;
svg { svg {
fill: $gray-darkest; fill: $gray-700;
} }
&.discussion-create-issue-btn { &.discussion-create-issue-btn {
...@@ -893,7 +893,7 @@ $note-form-margin-left: 72px; ...@@ -893,7 +893,7 @@ $note-form-margin-left: 72px;
.line-resolve-btn { .line-resolve-btn {
margin-right: 5px; margin-right: 5px;
color: $gray-darkest; color: $gray-700;
svg { svg {
vertical-align: middle; vertical-align: middle;
......
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
# non_archived: boolean # non_archived: boolean
# archived: 'only' or boolean # archived: 'only' or boolean
# min_access_level: integer # min_access_level: integer
# last_activity_after: datetime
# last_activity_before: datetime
# #
class ProjectsFinder < UnionFinder class ProjectsFinder < UnionFinder
include CustomAttributesFilter include CustomAttributesFilter
...@@ -73,6 +75,8 @@ class ProjectsFinder < UnionFinder ...@@ -73,6 +75,8 @@ class ProjectsFinder < UnionFinder
collection = by_archived(collection) collection = by_archived(collection)
collection = by_custom_attributes(collection) collection = by_custom_attributes(collection)
collection = by_deleted_status(collection) collection = by_deleted_status(collection)
collection = by_last_activity_after(collection)
collection = by_last_activity_before(collection)
collection collection
end end
...@@ -179,6 +183,22 @@ class ProjectsFinder < UnionFinder ...@@ -179,6 +183,22 @@ class ProjectsFinder < UnionFinder
params[:without_deleted].present? ? items.without_deleted : items params[:without_deleted].present? ? items.without_deleted : items
end 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) def sort(items)
params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.projects_order_id_desc params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.projects_order_id_desc
end end
......
...@@ -159,6 +159,11 @@ class Label < ApplicationRecord ...@@ -159,6 +159,11 @@ class Label < ApplicationRecord
on_project_boards(project_id).where(id: label_id).exists? on_project_boards(project_id).where(id: label_id).exists?
end 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) def open_issues_count(user = nil)
issues_count(user, state: 'opened') issues_count(user, state: 'opened')
end end
......
...@@ -9,8 +9,8 @@ module Gitlab ...@@ -9,8 +9,8 @@ module Gitlab
include Gitlab::Import::DatabaseHelpers include Gitlab::Import::DatabaseHelpers
def perform(project_id, jira_issue_id, issue_attributes, waiter_key) def perform(project_id, jira_issue_id, issue_attributes, waiter_key)
issue_id = insert_and_return_id(issue_attributes, Issue) issue_id = create_issue(issue_attributes, project_id)
cache_issue_mapping(issue_id, jira_issue_id, project_id) JiraImport.cache_issue_mapping(issue_id, jira_issue_id, project_id)
rescue => ex rescue => ex
# Todo: Record jira issue id(or better jira issue key), # 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 # so that we can report the list of failed to import issues to the user
...@@ -27,9 +27,31 @@ module Gitlab ...@@ -27,9 +27,31 @@ module Gitlab
private private
def cache_issue_mapping(issue_id, jira_issue_id, project_id) def create_issue(issue_attributes, project_id)
cache_key = JiraImport.jira_issue_cache_key(project_id, jira_issue_id) issue_id = insert_and_return_id(issue_attributes, Issue)
Gitlab::Cache::Import::Caching.write(cache_key, issue_id)
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 end
end end
......
...@@ -9,10 +9,8 @@ module Gitlab ...@@ -9,10 +9,8 @@ module Gitlab
private private
def import(project) def import(project)
# fake labels import workers for now job_waiter = Gitlab::JiraImport::LabelsImporter.new(project).execute
# new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { job_waiter.key => job_waiter.jobs_remaining }, :issues)
fake_waiter = JobWaiter.new
Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { fake_waiter.key => fake_waiter.jobs_remaining }, :issues)
end end
end end
end end
......
---
title: Add app server type to usage ping
merge_request: 28189
author:
type: added
---
title: Add last_activity_before and last_activity_after filter to /api/projects endpoint
merge_request: 28221
author: Roger Meier
type: added
---
title: Fix merge request thread’s icon buttons color
merge_request: 28465
author:
type: other
...@@ -41,26 +41,28 @@ GET /projects ...@@ -41,26 +41,28 @@ GET /projects
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `archived` | boolean | no | Limit by archived status | | `archived` | boolean | no | Limit by archived status |
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | | `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` | | `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` | | `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` | string | no | Return list of projects matching the search criteria |
| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` | | `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. | | `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 | | `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 | | `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 | | `starred` | boolean | no | Limit by projects starred by the current user |
| `statistics` | boolean | no | Include project statistics | | `statistics` | boolean | no | Include project statistics |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | | `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_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature | | `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
| `with_programming_language` | string | no | Limit by projects which use the given programming language | | `with_programming_language` | string | no | Limit by projects which use the given programming language |
| `wiki_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the wiki checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | | `wiki_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the wiki checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) |
| `repository_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | | `repository_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) |
| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) | | `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) |
| `id_after` | integer | no | Limit results to projects with IDs greater than the specified ID | | `id_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 | | `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:** NOTE: **Note:**
This endpoint supports [keyset pagination](README.md#keyset-based-pagination) for selected `order_by` options. This endpoint supports [keyset pagination](README.md#keyset-based-pagination) for selected `order_by` options.
......
...@@ -6,7 +6,7 @@ type: reference ...@@ -6,7 +6,7 @@ type: reference
> **Notes:** > **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. > 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 > 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` > 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. ...@@ -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 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. 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 To enable this feature, ask a GitLab administrator with Rails console access to run the
following command: following command:
......
...@@ -51,7 +51,7 @@ and when hovering or tapping (on touchscreen devices) they will expand and be sh ...@@ -51,7 +51,7 @@ and when hovering or tapping (on touchscreen devices) they will expand and be sh
## Triggering multi-project pipelines through API ## 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. > - 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 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. ...@@ -362,7 +362,7 @@ Check specific punctuation rules for [lists](#lists) below.
| Rule | Example | | Rule | Example |
| ---- | ------- | | ---- | ------- |
| Always end full sentences with a period. | _For a complete overview, read through this document._| | 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 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. | --- | | 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._ | | 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. ...@@ -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. 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 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 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`) 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">`. 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, ...@@ -1000,7 +1000,7 @@ Whenever you need to call special attention to particular sentences,
use the following markup for highlighting. use the following markup for highlighting.
_Note that the alert boxes only work for one paragraph only. Multiple paragraphs, _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>). 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 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: ...@@ -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) - [`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. - 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) - [`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 - Creates a new index in the destination cluster from the source index using
remote](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote) Elasticsearch "reindex from remote", where the source index is copied to the
such that the index is fully copied from the source index. This can be destination. This is useful when migrating to a new cluster because it should be
useful when you wish to perform a migration to a new cluster as this quicker than reindexing via GitLab.
reindexing should be quicker than reindexing via GitLab. Note that remote
reindex requires your source cluster to be whitelisted in your destination NOTE: **Note:**
cluster in Elasticsearch settings as per [the Your source cluster must be whitelisted in your destination cluster's Elasticsearch
documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote). settings. See [Reindex from remote](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote).
### Environment Variables ### Environment Variables
......
...@@ -1036,8 +1036,8 @@ Then add any extra changes you want. Your additions will be merged with the ...@@ -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 [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). [`include`](../../ci/yaml/README.md#include).
It is also possible to copy and paste the contents of the [Auto DevOps 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)
template] into your project and edit this as needed. You may prefer to do it 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. that way if you want to specifically remove any part of it.
### Customizing the Kubernetes namespace ### Customizing the Kubernetes namespace
......
...@@ -20,7 +20,7 @@ comment at any time, and anyone with [Maintainer access level](../permissions.md ...@@ -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. 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 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 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. [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. ...@@ -140,7 +140,7 @@ You can now proceed to merge the merge request from the UI.
### Moving a single thread to a new issue ### 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 To create a new issue for a single thread, you can use the **Resolve this
thread in a new issue** button. thread in a new issue** button.
......
...@@ -35,7 +35,7 @@ to be careful when using canaries with user-facing changes, because by default, ...@@ -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 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 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 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. this document.
## Enabling Canary Deployments ## Enabling Canary Deployments
......
...@@ -459,7 +459,7 @@ Note the following properties: ...@@ -459,7 +459,7 @@ Note the following properties:
| Property | Type | Required | Description | | Property | Type | Required | Description |
| ------ | ------ | ------ | ------ | | ------ | ------ | ------ | ------ |
| `type` | string | yes | Type of panel to be rendered. For bar chart types, set to `bar` | | `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) ![bar chart panel type](img/prometheus_dashboard_bar_chart_panel_type_v12.10.png)
......
...@@ -505,20 +505,28 @@ module API ...@@ -505,20 +505,28 @@ module API
protected protected
def project_finder_params_ce def project_finder_params_visibility_ce
finder_params = { without_deleted: true } 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[:owned] = true if params[:owned].present?
finder_params[:non_public] = true if params[:membership].present? finder_params[:non_public] = true if params[:membership].present?
finder_params[:starred] = true if params[:starred].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[: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] = params[:search] if params[:search]
finder_params[:search_namespaces] = true if params[:search_namespaces].present? finder_params[:search_namespaces] = true if params[:search_namespaces].present?
finder_params[:user] = params.delete(:user) if params[:user] finder_params[:user] = params.delete(:user) if params[:user]
finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes] finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level]
finder_params[:id_after] = params[:id_after] if params[:id_after] finder_params[:id_after] = params[:id_after] if params[:id_after]
finder_params[:id_before] = params[:id_before] if params[:id_before] 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 finder_params
end end
......
...@@ -73,6 +73,8 @@ module API ...@@ -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 :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_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 :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 use :optional_filter_params_ee
end end
......
...@@ -6,6 +6,7 @@ module Gitlab ...@@ -6,6 +6,7 @@ module Gitlab
FAILED_ISSUES_COUNTER_KEY = 'jira-import/failed/%{project_id}/%{collection_type}' FAILED_ISSUES_COUNTER_KEY = 'jira-import/failed/%{project_id}/%{collection_type}'
NEXT_ITEMS_START_AT_KEY = 'jira-import/paginator/%{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}' 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}' ALREADY_IMPORTED_ITEMS_CACHE_KEY = 'jira-importer/already-imported/%{project}/%{collection_type}'
...@@ -25,23 +26,45 @@ module Gitlab ...@@ -25,23 +26,45 @@ module Gitlab
FAILED_ISSUES_COUNTER_KEY % { project_id: project_id, collection_type: :issues } FAILED_ISSUES_COUNTER_KEY % { project_id: project_id, collection_type: :issues }
end end
def self.import_label_cache_key(project_id)
JIRA_IMPORT_LABEL % { project_id: project_id }
end
def self.increment_issue_failures(project_id) 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 end
def self.get_issues_next_start_at(project_id) 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 end
def self.store_issues_next_started_at(project_id, value) def self.store_issues_next_started_at(project_id, value)
cache_key = self.jira_issues_next_page_cache_key(project_id) 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 end
def self.cache_cleanup(project_id) def self.cache_cleanup(project_id)
Gitlab::Cache::Import::Caching.expire(self.failed_issues_counter_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT) cache_class.expire(self.import_label_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) cache_class.expire(self.failed_issues_counter_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.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 end
end end
# 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 ...@@ -174,10 +174,19 @@ module Gitlab
git: { version: Gitlab::Git.version }, git: { version: Gitlab::Git.version },
gitaly: { version: Gitaly::Server.all.first.server_version, servers: Gitaly::Server.count, filesystems: Gitaly::Server.filesystems }, 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 }, 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 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 def ingress_modsecurity_usage
::Clusters::Applications::IngressModsecurityUsageService.new.execute ::Clusters::Applications::IngressModsecurityUsageService.new.execute
end end
......
...@@ -6901,9 +6901,6 @@ msgstr "" ...@@ -6901,9 +6901,6 @@ msgstr ""
msgid "Detect host keys" msgid "Detect host keys"
msgstr "" msgstr ""
msgid "Detected %{timeago} in pipeline %{pipelineLink}"
msgstr ""
msgid "DevOps Score" msgid "DevOps Score"
msgstr "" msgstr ""
...@@ -8481,6 +8478,9 @@ msgstr "" ...@@ -8481,6 +8478,9 @@ msgstr ""
msgid "Failed to create a branch for this issue. Please try again." msgid "Failed to create a branch for this issue. Please try again."
msgstr "" msgstr ""
msgid "Failed to create import label for jira import."
msgstr ""
msgid "Failed to create repository" msgid "Failed to create repository"
msgstr "" msgstr ""
...@@ -22577,18 +22577,33 @@ msgstr "" ...@@ -22577,18 +22577,33 @@ msgstr ""
msgid "VulnerabilityManagement|Confirm" msgid "VulnerabilityManagement|Confirm"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Confirmed %{timeago} by %{user}"
msgstr ""
msgid "VulnerabilityManagement|Create issue" msgid "VulnerabilityManagement|Create issue"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Detected %{timeago} in pipeline %{pipelineLink}"
msgstr ""
msgid "VulnerabilityManagement|Dismiss" msgid "VulnerabilityManagement|Dismiss"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Dismissed %{timeago} by %{user}"
msgstr ""
msgid "VulnerabilityManagement|Resolved" msgid "VulnerabilityManagement|Resolved"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not create an issue." msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not get user."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state." msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
msgstr "" msgstr ""
......
...@@ -222,6 +222,28 @@ describe ProjectsFinder, :do_not_mock_admin_mode do ...@@ -222,6 +222,28 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to match_array([public_project, internal_project]) } it { is_expected.to match_array([public_project, internal_project]) }
end 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 describe 'sorting' do
let(:params) { { sort: 'name_asc' } } let(:params) { { sort: 'name_asc' } }
......
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 { export default {
createNumberRandomUsers(numberUsers) { createNumberRandomUsers(numberUsers) {
const users = []; const users = [];
for (let i = 0; i < numberUsers; i += 1) { for (let i = 0; i < numberUsers; i += 1) {
users.push({ users.push({
avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', avatar_url: getRandomUrl(),
id: i + 1, id: id + 1,
name: `GitLab User ${i}`, name: getRandomString(),
username: `gitlab${i}`, username: getRandomString(),
user_path: getRandomUrl(),
}); });
id += 1;
} }
return users; return users;
}, },
createRandomUser() {
return this.createNumberRandomUsers(1)[0];
},
}; };
...@@ -101,14 +101,14 @@ describe('Assignee component', () => { ...@@ -101,14 +101,14 @@ describe('Assignee component', () => {
const first = collapsedChildren.at(0); 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(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
expect(trimText(first.find('.author').text())).toBe(users[0].name); expect(trimText(first.find('.author').text())).toBe(users[0].name);
const second = collapsedChildren.at(1); 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(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`);
expect(trimText(second.find('.author').text())).toBe(users[1].name); expect(trimText(second.find('.author').text())).toBe(users[1].name);
...@@ -127,7 +127,7 @@ describe('Assignee component', () => { ...@@ -127,7 +127,7 @@ describe('Assignee component', () => {
const first = collapsedChildren.at(0); 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(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
expect(trimText(first.find('.author').text())).toBe(users[0].name); expect(trimText(first.find('.author').text())).toBe(users[0].name);
......
...@@ -23,7 +23,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -23,7 +23,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
id="visibility-level-setting" id="visibility-level-setting"
> >
<gl-form-radio-group-stub <gl-form-radio-group-stub
checked="0" checked="private"
disabledfield="disabled" disabledfield="disabled"
htmlfield="html" htmlfield="html"
options="" options=""
...@@ -33,7 +33,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -33,7 +33,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
> >
<gl-form-radio-stub <gl-form-radio-stub
class="mb-3" class="mb-3"
value="0" value="private"
> >
<div <div
class="d-flex align-items-center" class="d-flex align-items-center"
...@@ -44,7 +44,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -44,7 +44,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/> />
<span <span
class="font-weight-bold ml-1" class="font-weight-bold ml-1 js-visibility-option"
> >
Private Private
</span> </span>
...@@ -52,7 +52,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -52,7 +52,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub> </gl-form-radio-stub>
<gl-form-radio-stub <gl-form-radio-stub
class="mb-3" class="mb-3"
value="1" value="internal"
> >
<div <div
class="d-flex align-items-center" class="d-flex align-items-center"
...@@ -63,7 +63,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -63,7 +63,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/> />
<span <span
class="font-weight-bold ml-1" class="font-weight-bold ml-1 js-visibility-option"
> >
Internal Internal
</span> </span>
...@@ -71,7 +71,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -71,7 +71,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub> </gl-form-radio-stub>
<gl-form-radio-stub <gl-form-radio-stub
class="mb-3" class="mb-3"
value="2" value="public"
> >
<div <div
class="d-flex align-items-center" class="d-flex align-items-center"
...@@ -82,7 +82,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -82,7 +82,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/> />
<span <span
class="font-weight-bold ml-1" class="font-weight-bold ml-1 js-visibility-option"
> >
Public Public
</span> </span>
......
...@@ -2,18 +2,21 @@ import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; ...@@ -2,18 +2,21 @@ import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import BlobContentEdit from '~/blob/components/blob_edit_content.vue'; import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
jest.mock('~/blob/utils', () => jest.fn()); jest.mock('~/blob/utils', () => jest.fn());
describe('Snippet Blob Edit component', () => { describe('Snippet Blob Edit component', () => {
let wrapper; 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 fileName = 'lorem.txt';
const findHeader = () => wrapper.find(BlobHeaderEdit);
const findContent = () => wrapper.find(BlobContentEdit);
function createComponent() { function createComponent() {
wrapper = shallowMount(SnippetBlobEdit, { wrapper = shallowMount(SnippetBlobEdit, {
propsData: { propsData: {
content, value,
fileName, fileName,
}, },
}); });
...@@ -33,8 +36,20 @@ describe('Snippet Blob Edit component', () => { ...@@ -33,8 +36,20 @@ describe('Snippet Blob Edit component', () => {
}); });
it('renders required components', () => { it('renders required components', () => {
expect(wrapper.contains(BlobHeaderEdit)).toBe(true); expect(findHeader().exists()).toBe(true);
expect(wrapper.contains(BlobContentEdit)).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', () => { ...@@ -6,11 +6,12 @@ describe('Snippet Description Edit component', () => {
const defaultDescription = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; const defaultDescription = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
const markdownPreviewPath = 'foo/'; const markdownPreviewPath = 'foo/';
const markdownDocsPath = 'help/'; const markdownDocsPath = 'help/';
const findTextarea = () => wrapper.find('textarea');
function createComponent(description = defaultDescription) { function createComponent(value = defaultDescription) {
wrapper = shallowMount(SnippetDescriptionEdit, { wrapper = shallowMount(SnippetDescriptionEdit, {
propsData: { propsData: {
description, value,
markdownPreviewPath, markdownPreviewPath,
markdownDocsPath, markdownDocsPath,
}, },
...@@ -49,4 +50,14 @@ describe('Snippet Description Edit component', () => { ...@@ -49,4 +50,14 @@ describe('Snippet Description Edit component', () => {
expect(isHidden('.js-expanded')).toBe(true); 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]);
});
});
}); });
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import { GlFormRadio } from '@gitlab/ui'; import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { SNIPPET_VISIBILITY } from '~/snippets/constants'; import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
describe('Snippet Visibility Edit component', () => { describe('Snippet Visibility Edit component', () => {
let wrapper; let wrapper;
let radios;
const defaultHelpLink = '/foo/bar'; const defaultHelpLink = '/foo/bar';
const defaultVisibilityLevel = '0'; const defaultVisibilityLevel = 'private';
function findElements(sel) { function createComponent(propsData = {}, deep = false) {
return wrapper.findAll(sel);
}
function createComponent(
{
helpLink = defaultHelpLink,
isProjectSnippet = false,
visibilityLevel = defaultVisibilityLevel,
} = {},
deep = false,
) {
const method = deep ? mount : shallowMount; const method = deep ? mount : shallowMount;
wrapper = method.call(this, SnippetVisibilityEdit, { wrapper = method.call(this, SnippetVisibilityEdit, {
propsData: { propsData: {
helpLink, helpLink: defaultHelpLink,
isProjectSnippet, isProjectSnippet: false,
visibilityLevel, 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(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -42,53 +47,66 @@ describe('Snippet Visibility Edit component', () => { ...@@ -42,53 +47,66 @@ describe('Snippet Visibility Edit component', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it.each` it('renders visibility options', () => {
label | value createComponent({}, true);
${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));
expect(radio.attributes('value')).toBe(value); expect(findRadiosData()).toEqual([
expect(radio.text()).toContain(label); {
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('when project snippet, renders special private description', () => {
it.each` createComponent({ isProjectSnippet: true }, true);
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);
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 label help link', () => {
createComponent();
it('renders correct Private description for a project snippet', () => { expect(
createComponent({ isProjectSnippet: true }, true); findLabel()
.find(GlLink)
.attributes('href'),
).toBe(defaultHelpLink);
});
const helpText = findElements('.help-text') it('when helpLink is not defined, does not render label help link', () => {
.at(0) createComponent({ helpLink: null });
.text();
expect(helpText).not.toContain(SNIPPET_VISIBILITY.private.description); expect(findLabel().contains(GlLink)).toBe(false);
expect(helpText).toBe(SNIPPET_VISIBILITY.private.description_project);
});
}); });
}); });
describe('functionality', () => { describe('functionality', () => {
it('pre-selects correct option in the list', () => { it('pre-selects correct option in the list', () => {
const pos = 1; const value = SNIPPET_VISIBILITY_INTERNAL;
createComponent({ value });
createComponent({ visibilityLevel: `${pos}` }, true); expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
const radio = radios.at(pos);
expect(radio.find('input[type="radio"]').element.checked).toBe(true);
}); });
}); });
}); });
# 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 ...@@ -147,6 +147,8 @@ describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.components_usage_data } subject { described_class.components_usage_data }
it 'gathers components usage data' do 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][:enabled]).to eq(Gitlab.config.pages.enabled)
expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION) expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION)
expect(subject[:git][:version]).to eq(Gitlab::Git.version) expect(subject[:git][:version]).to eq(Gitlab::Git.version)
...@@ -159,6 +161,28 @@ describe Gitlab::UsageData, :aggregate_failures do ...@@ -159,6 +161,28 @@ describe Gitlab::UsageData, :aggregate_failures do
end end
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 describe '#cycle_analytics_usage_data' do
subject { described_class.cycle_analytics_usage_data } subject { described_class.cycle_analytics_usage_data }
......
...@@ -32,12 +32,28 @@ describe Gitlab::JiraImport::ImportIssueWorker do ...@@ -32,12 +32,28 @@ describe Gitlab::JiraImport::ImportIssueWorker do
end end
context 'when record is successfully inserted' do context 'when record is successfully inserted' do
before do let(:label) { create(:label, project: project) }
subject.perform(project.id, 123, issue_attrs, 'some-key')
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 end
it 'does not record import failure' do context 'when import label exists' do
expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0) 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 end
end end
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::JiraImport::Stage::ImportLabelsWorker do describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
describe 'modules' do describe 'modules' do
...@@ -30,9 +31,24 @@ describe Gitlab::JiraImport::Stage::ImportLabelsWorker do ...@@ -30,9 +31,24 @@ describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
end end
context 'when import started' do 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) } let!(:import_state) { create(:import_state, status: :started, project: project) }
it_behaves_like 'advance to next stage', :issues 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 end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment