Commit b11f7057 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent e50050a8
......@@ -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: '',
},
},
methods: {
emitFileNameChange(newFileName) {
this.$emit('name-change', newFileName);
},
data() {
return {
name: this.fileName,
blobContent: this.content,
};
},
};
</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,
},
value: {
type: String,
required: false,
default: '',
},
data() {
return {
text: this.description,
};
},
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>
......
<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
......
---
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
......@@ -61,6 +61,8 @@ GET /projects
| `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
# 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' } }
......
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]);
});
});
});
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));
expect(radio.attributes('value')).toBe(value);
expect(radio.text()).toContain(label);
});
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 }) => {
it('renders visibility options', () => {
createComponent({}, true);
const help = findElements('.help-text').at(parseInt(value, 10));
expect(help.text()).toBe(description);
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,
},
]);
});
it('renders correct Private description for a project snippet', () => {
it('when project snippet, renders special private description', () => {
createComponent({ isProjectSnippet: true }, true);
const helpText = findElements('.help-text')
.at(0)
.text();
expect(findRadiosData()[0]).toEqual({
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description_project,
});
});
expect(helpText).not.toContain(SNIPPET_VISIBILITY.private.description);
expect(helpText).toBe(SNIPPET_VISIBILITY.private.description_project);
it('renders label help link', () => {
createComponent();
expect(
findLabel()
.find(GlLink)
.attributes('href'),
).toBe(defaultHelpLink);
});
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({ value });
createComponent({ visibilityLevel: `${pos}` }, true);
const radio = radios.at(pos);
expect(radio.find('input[type="radio"]').element.checked).toBe(true);
expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
});
});
});
# 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,13 +32,29 @@ describe Gitlab::JiraImport::ImportIssueWorker do
end
context 'when record is successfully inserted' do
before do
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
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
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
......
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