summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue1
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue41
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue40
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue7
-rw-r--r--app/graphql/mutations/issues/common_mutation_arguments.rb5
-rw-r--r--app/helpers/projects_helper.rb82
-rw-r--r--app/models/project_services/issue_tracker_service.rb6
-rw-r--r--app/models/project_services/youtrack_service.rb4
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--changelogs/unreleased/330470-default-enable-sync_traversal_ids.yml5
-rw-r--r--changelogs/unreleased/msj-ui-trackers-help.yml5
-rw-r--r--changelogs/unreleased/sy-create-incidents-via-graphql.yml5
-rw-r--r--config/feature_flags/development/sync_traversal_ids.yml2
-rw-r--r--config/metrics/counts_28d/20210216181139_issues.yml11
-rw-r--r--config/metrics/counts_all/20210216181102_issues.yml1
-rw-r--r--config/metrics/counts_all/20210216181115_issues.yml1
-rw-r--r--doc/administration/monitoring/prometheus/postgres_exporter.md2
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/development/usage_ping/dictionary.md2
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb18
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric.rb15
-rw-r--r--locale/gitlab.pot24
-rw-r--r--package.json4
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js (renamed from spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js)83
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js55
-rw-r--r--spec/graphql/mutations/issues/create_spec.rb13
-rw-r--r--spec/graphql/mutations/issues/update_spec.rb8
-rw-r--r--spec/helpers/projects_helper_spec.rb87
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb9
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb17
-rw-r--r--spec/lib/gitlab/usage_data_metrics_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/issues/create_spec.rb5
-rw-r--r--spec/requests/api/graphql/mutations/issues/update_spec.rb3
-rw-r--r--yarn.lock16
35 files changed, 322 insertions, 273 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
index 554c7a573fe..1c9791bba78 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
@@ -64,6 +64,7 @@ export default {
<sidebar-status
:project-path="projectPath"
:alert="alert"
+ :sidebar-collapsed="sidebarStatus"
@toggle-sidebar="$emit('toggle-sidebar')"
@alert-error="$emit('alert-error', $event)"
/>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index 2a999b908f9..2c138fefd6d 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -192,21 +192,34 @@ export default {
</script>
<template>
- <div class="block alert-assignees">
- <div ref="assignees" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
- <gl-icon name="user" :size="14" />
- <gl-loading-icon v-if="isUpdating" />
- </div>
- <gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left">
- <gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK">
- <template #assignees>
- {{ userName }}
- </template>
- </gl-sprintf>
- </gl-tooltip>
+ <div
+ class="alert-assignees gl-py-5 gl-w-70p"
+ :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': !sidebarCollapsed }"
+ style="width: 70%"
+ >
+ <template v-if="sidebarCollapsed">
+ <div
+ ref="assignees"
+ class="gl-mb-6 gl-ml-6"
+ data-testid="assignees-icon"
+ @click="$emit('toggle-sidebar')"
+ >
+ <gl-icon name="user" />
+ <gl-loading-icon v-if="isUpdating" />
+ </div>
+ <gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left">
+ <gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK">
+ <template #assignees>
+ {{ userName }}
+ </template>
+ </gl-sprintf>
+ </gl-tooltip>
+ </template>
- <div class="hide-collapsed">
- <p class="title gl-display-flex gl-justify-content-space-between">
+ <div v-else>
+ <p
+ class="gl-text-gray-900 gl-mb-2 gl-line-height-20 gl-display-flex gl-justify-content-space-between"
+ >
{{ __('Assignee') }}
<a
v-if="isEditable"
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
index fd40b5d9f65..832b154b312 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
@@ -25,7 +25,7 @@ export default {
</script>
<template>
- <div class="block gl-display-flex gl-justify-content-space-between">
+ <div class="block gl-display-flex gl-justify-content-space-between gl-border-b-gray-100!">
<span class="issuable-header-text hide-collapsed">
{{ __('To Do') }}
</span>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index 3822b9153a4..2a792dfa19a 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
@@ -30,6 +30,10 @@ export default {
required: false,
default: true,
},
+ sidebarCollapsed: {
+ type: Boolean,
+ required: false,
+ },
},
data() {
return {
@@ -61,21 +65,29 @@ export default {
</script>
<template>
- <div class="block alert-status">
- <div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
- <gl-icon name="status" :size="14" />
- <gl-loading-icon v-if="isUpdating" />
- </div>
- <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
- <gl-sprintf :message="s__('AlertManagement|Alert status: %{status}')">
- <template #status>
- {{ alert.status.toLowerCase() }}
- </template>
- </gl-sprintf>
- </gl-tooltip>
+ <div
+ class="alert-status gl-py-5 gl-w-70p"
+ :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': !sidebarCollapsed }"
+ style="width: 70%"
+ >
+ <template v-if="sidebarCollapsed">
+ <div ref="status" class="gl-ml-6" data-testid="status-icon" @click="$emit('toggle-sidebar')">
+ <gl-icon name="status" />
+ <gl-loading-icon v-if="isUpdating" />
+ </div>
+ <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
+ <gl-sprintf :message="s__('AlertManagement|Alert status: %{status}')">
+ <template #status>
+ {{ alert.status.toLowerCase() }}
+ </template>
+ </gl-sprintf>
+ </gl-tooltip>
+ </template>
- <div class="hide-collapsed">
- <p class="title gl-display-flex justify-content-between">
+ <div v-else>
+ <p
+ class="gl-text-gray-900 gl-mb-2 gl-line-height-20 gl-display-flex gl-justify-content-space-between"
+ >
{{ s__('AlertManagement|Status') }}
<a
v-if="isEditable"
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
index 271f0b4e4bb..a2a4046ab81 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
@@ -134,7 +134,12 @@ export default {
</script>
<template>
- <div :class="{ 'block todo': sidebarCollapsed, 'gl-ml-auto': !sidebarCollapsed }">
+ <div
+ :class="{
+ 'block todo': sidebarCollapsed,
+ 'gl-ml-auto': !sidebarCollapsed,
+ }"
+ >
<todo
data-testid="alert-todo-button"
:collapsed="sidebarCollapsed"
diff --git a/app/graphql/mutations/issues/common_mutation_arguments.rb b/app/graphql/mutations/issues/common_mutation_arguments.rb
index 4b5b246281f..65768b85d14 100644
--- a/app/graphql/mutations/issues/common_mutation_arguments.rb
+++ b/app/graphql/mutations/issues/common_mutation_arguments.rb
@@ -22,6 +22,11 @@ module Mutations
as: :discussion_locked,
required: false,
description: copy_field_description(Types::IssueType, :discussion_locked)
+
+ argument :type, Types::IssueTypeEnum,
+ as: :issue_type,
+ required: false,
+ description: copy_field_description(Types::IssueType, :type)
end
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 45130dfc76d..24ed1db86ba 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -125,34 +125,12 @@ module ProjectsHelper
project.fork_source if project.fork_source && can?(current_user, :read_project, project.fork_source)
end
- def project_nav_tabs
- @nav_tabs ||= get_project_nav_tabs(@project, current_user)
- end
-
def project_search_tabs?(tab)
abilities = Array(search_tab_ability_map[tab])
abilities.any? { |ability| can?(current_user, ability, @project) }
end
- def project_nav_tab?(name)
- project_nav_tabs.include? name
- end
-
- def any_project_nav_tab?(tabs)
- tabs.any? { |tab| project_nav_tab?(tab) }
- end
-
- def project_for_deploy_key(deploy_key)
- if deploy_key.has_access_to?(@project)
- @project
- else
- deploy_key.projects.find do |project|
- can?(current_user, :read_project, project)
- end
- end
- end
-
def can_change_visibility_level?(project, current_user)
can?(current_user, :change_visibility_level, project)
end
@@ -374,66 +352,6 @@ module ProjectsHelper
private
- # rubocop:disable Metrics/CyclomaticComplexity
- def get_project_nav_tabs(project, current_user)
- nav_tabs = [:home]
-
- unless project.empty_repo?
- nav_tabs += [:files, :commits, :network, :graphs, :forks] if can?(current_user, :download_code, project)
- nav_tabs << :releases if can?(current_user, :read_release, project)
- end
-
- if project.repo_exists? && can?(current_user, :read_merge_request, project)
- nav_tabs << :merge_requests
- end
-
- if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
- nav_tabs << :container_registry
- end
-
- if Feature.enabled?(:infrastructure_registry_page)
- nav_tabs << :infrastructure_registry
- end
-
- # Pipelines feature is tied to presence of builds
- if can?(current_user, :read_build, project)
- nav_tabs << :pipelines
- end
-
- tab_ability_map.each do |tab, ability|
- if can?(current_user, ability, project)
- nav_tabs << tab
- end
- end
-
- apply_external_nav_tabs(nav_tabs, project)
-
- nav_tabs += package_nav_tabs(project, current_user)
-
- nav_tabs << :learn_gitlab if learn_gitlab_experiment_enabled?(project)
-
- nav_tabs
- end
- # rubocop:enable Metrics/CyclomaticComplexity
-
- def package_nav_tabs(project, current_user)
- [].tap do |tabs|
- if ::Gitlab.config.packages.enabled && can?(current_user, :read_package, project)
- tabs << :packages
- end
- end
- end
-
- def apply_external_nav_tabs(nav_tabs, project)
- nav_tabs << :external_issue_tracker if project.external_issue_tracker
- nav_tabs << :external_wiki if project.external_wiki
-
- if project.has_confluence?
- nav_tabs.delete(:wiki)
- nav_tabs << :confluence
- end
- end
-
def tab_ability_map
{
cycle_analytics: :read_cycle_analytics,
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 19a5b4a74bb..46aad221597 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -73,9 +73,9 @@ class IssueTrackerService < Service
def fields
[
- { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true },
- { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in the external issue tracker.'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('IssueTracker|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true },
+ { type: 'text', name: 'new_issue_url', title: s_('IssueTracker|New issue URL'), help: s_('IssueTracker|The URL to create an issue in the external issue tracker.'), required: true }
]
end
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
index 94cbd6c5959..9760a22a872 100644
--- a/app/models/project_services/youtrack_service.rb
+++ b/app/models/project_services/youtrack_service.rb
@@ -33,8 +33,8 @@ class YoutrackService < IssueTrackerService
def fields
[
- { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in YouTrack.'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the YouTrack project. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true }
]
end
end
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 3d0c6baffd5..a06f9f8d6ef 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -1,3 +1 @@
--# We're migration the project sidebar to a logical model based structure. If you need to update
--# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_project_menus.html.haml.
= render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user, current_ref))
diff --git a/changelogs/unreleased/330470-default-enable-sync_traversal_ids.yml b/changelogs/unreleased/330470-default-enable-sync_traversal_ids.yml
new file mode 100644
index 00000000000..67b05da0b9d
--- /dev/null
+++ b/changelogs/unreleased/330470-default-enable-sync_traversal_ids.yml
@@ -0,0 +1,5 @@
+---
+title: Sync traversal path of namespaces
+merge_request: 61329
+author:
+type: performance
diff --git a/changelogs/unreleased/msj-ui-trackers-help.yml b/changelogs/unreleased/msj-ui-trackers-help.yml
new file mode 100644
index 00000000000..a02502b2321
--- /dev/null
+++ b/changelogs/unreleased/msj-ui-trackers-help.yml
@@ -0,0 +1,5 @@
+---
+title: Add issue tracker integrations help text
+merge_request: 61158
+author:
+type: other
diff --git a/changelogs/unreleased/sy-create-incidents-via-graphql.yml b/changelogs/unreleased/sy-create-incidents-via-graphql.yml
new file mode 100644
index 00000000000..4c68c526774
--- /dev/null
+++ b/changelogs/unreleased/sy-create-incidents-via-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for creating/modifying different issue types via GraphQL API
+merge_request: 60747
+author:
+type: added
diff --git a/config/feature_flags/development/sync_traversal_ids.yml b/config/feature_flags/development/sync_traversal_ids.yml
index 52777c502e6..bd612f9646c 100644
--- a/config/feature_flags/development/sync_traversal_ids.yml
+++ b/config/feature_flags/development/sync_traversal_ids.yml
@@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52854
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321947
group: group::access
type: development
-default_enabled: false
+default_enabled: true
diff --git a/config/metrics/counts_28d/20210216181139_issues.yml b/config/metrics/counts_28d/20210216181139_issues.yml
index 46494caaff7..04734857bdd 100644
--- a/config/metrics/counts_28d/20210216181139_issues.yml
+++ b/config/metrics/counts_28d/20210216181139_issues.yml
@@ -1,18 +1,19 @@
---
key_path: usage_activity_by_stage_monthly.plan.issues
-description: Count of MAU creating issues
+description: Count of users creating Issues in last 28 days.
product_section: dev
-product_stage: plan
+product_stage: plan
product_group: group::project management
-product_category: issue_tracking
+product_category: issue_tracking
value_type: number
status: data_available
time_frame: 28d
data_source: database
+instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountUsersCreatingIssuesMetric'
distribution:
- ce
-- ee
+- ee
tier:
- free
-- premium
+- premium
- ultimate
diff --git a/config/metrics/counts_all/20210216181102_issues.yml b/config/metrics/counts_all/20210216181102_issues.yml
index ca89db705f1..c4426915d02 100644
--- a/config/metrics/counts_all/20210216181102_issues.yml
+++ b/config/metrics/counts_all/20210216181102_issues.yml
@@ -9,6 +9,7 @@ value_type: number
status: data_available
time_frame: all
data_source: database
+instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountIssuesMetric'
distribution:
- ce
- ee
diff --git a/config/metrics/counts_all/20210216181115_issues.yml b/config/metrics/counts_all/20210216181115_issues.yml
index 0c4db95b275..d3c7fc4b79b 100644
--- a/config/metrics/counts_all/20210216181115_issues.yml
+++ b/config/metrics/counts_all/20210216181115_issues.yml
@@ -9,6 +9,7 @@ value_type: number
status: data_available
time_frame: all
data_source: database
+instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountUsersCreatingIssuesMetric'
distribution:
- ce
- ee
diff --git a/doc/administration/monitoring/prometheus/postgres_exporter.md b/doc/administration/monitoring/prometheus/postgres_exporter.md
index 783030a9220..8a851afe35b 100644
--- a/doc/administration/monitoring/prometheus/postgres_exporter.md
+++ b/doc/administration/monitoring/prometheus/postgres_exporter.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# PostgreSQL Server Exporter **(FREE SELF)**
-The [PostgreSQL Server Exporter](https://github.com/wrouesnel/postgres_exporter) allows you to export various PostgreSQL metrics.
+The [PostgreSQL Server Exporter](https://github.com/prometheus-community/postgres_exporter) allows you to export various PostgreSQL metrics.
For installations from source you must install and configure it yourself.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 4a2a7c0de75..8628f956b99 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1188,6 +1188,7 @@ Input type: `CreateIssueInput`
| <a id="mutationcreateissuemilestoneid"></a>`milestoneId` | [`MilestoneID`](#milestoneid) | The ID of the milestone to assign to the issue. On update milestone will be removed if set to null. |
| <a id="mutationcreateissueprojectpath"></a>`projectPath` | [`ID!`](#id) | Project full path the issue is associated with. |
| <a id="mutationcreateissuetitle"></a>`title` | [`String!`](#string) | Title of the issue. |
+| <a id="mutationcreateissuetype"></a>`type` | [`IssueType`](#issuetype) | Type of the issue. |
| <a id="mutationcreateissueweight"></a>`weight` | [`Int`](#int) | The weight of the issue. |
#### Fields
@@ -3893,6 +3894,7 @@ Input type: `UpdateIssueInput`
| <a id="mutationupdateissueremovelabelids"></a>`removeLabelIds` | [`[ID!]`](#id) | The IDs of labels to be removed from the issue. |
| <a id="mutationupdateissuestateevent"></a>`stateEvent` | [`IssueStateEvent`](#issuestateevent) | Close or reopen an issue. |
| <a id="mutationupdateissuetitle"></a>`title` | [`String`](#string) | Title of the issue. |
+| <a id="mutationupdateissuetype"></a>`type` | [`IssueType`](#issuetype) | Type of the issue. |
| <a id="mutationupdateissueweight"></a>`weight` | [`Int`](#int) | The weight of the issue. |
#### Fields
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index 8cee670f72a..f64dd1c7d4d 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -18256,7 +18256,7 @@ Tiers: `free`
### `usage_activity_by_stage_monthly.plan.issues`
-Count of MAU creating issues
+Count of users creating Issues in last 28 days.
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216181139_issues.yml)
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb
new file mode 100644
index 00000000000..34247f4f6dd
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountIssuesMetric < DatabaseMetric
+ operation :count
+
+ start { Issue.minimum(:id) }
+ finish { Issue.maximum(:id) }
+
+ relation { Issue }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric.rb
new file mode 100644
index 00000000000..c8331ce5b31
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountUsersCreatingIssuesMetric < DatabaseMetric
+ operation :distinct_count, column: :author_id
+
+ relation { Issue }
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c479217861d..211376dac02 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18195,6 +18195,27 @@ msgstr ""
msgid "IssueTracker|Custom issue tracker"
msgstr ""
+msgid "IssueTracker|Issue URL"
+msgstr ""
+
+msgid "IssueTracker|New issue URL"
+msgstr ""
+
+msgid "IssueTracker|The URL to create an issue in the external issue tracker."
+msgstr ""
+
+msgid "IssueTracker|The URL to the project in YouTrack."
+msgstr ""
+
+msgid "IssueTracker|The URL to the project in the external issue tracker."
+msgstr ""
+
+msgid "IssueTracker|The URL to view an issue in the YouTrack project. Must contain %{colon_id}."
+msgstr ""
+
+msgid "IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}."
+msgstr ""
+
msgid "IssueTracker|Use Bugzilla as this project's issue tracker."
msgstr ""
@@ -25366,9 +25387,6 @@ msgstr ""
msgid "ProjectService|Must have permission to trigger a manual build in TeamCity."
msgstr ""
-msgid "ProjectService|New issue URL"
-msgstr ""
-
msgid "ProjectService|Perform common operations on GitLab project: %{project_name}"
msgstr ""
diff --git a/package.json b/package.json
index 5463bd0c63c..47dce38a769 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",
"@sentry/browser": "^5.22.3",
- "@sourcegraph/code-host-integration": "0.0.52",
+ "@sourcegraph/code-host-integration": "0.0.57",
"@tiptap/core": "^2.0.0-beta.38",
"@tiptap/extension-blockquote": "^2.0.0-beta.6",
"@tiptap/extension-bold": "^2.0.0-beta.6",
@@ -101,7 +101,7 @@
"codesandbox-api": "0.0.23",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
- "core-js": "^3.12.0",
+ "core-js": "^3.12.1",
"cron-validator": "^1.1.1",
"cropper": "^2.3.0",
"css-loader": "^2.1.1",
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
index 28646994ed1..db9b0930c06 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
@@ -1,7 +1,7 @@
import { GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SidebarAssignee from '~/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue';
import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
import AlertSetAssignees from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
@@ -13,6 +13,29 @@ describe('Alert Details Sidebar Assignees', () => {
let wrapper;
let mock;
+ const mockPath = '/-/autocomplete/users.json';
+ const mockUsers = [
+ {
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 1,
+ name: 'User 1',
+ username: 'root',
+ },
+ {
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 2,
+ name: 'User 2',
+ username: 'not-root',
+ },
+ ];
+
+ const findAssigned = () => wrapper.findByTestId('assigned-users');
+ const findDropdown = () => wrapper.findComponent(GlDropdownItem);
+ const findSidebarIcon = () => wrapper.findByTestId('assignees-icon');
+ const findUnassigned = () => wrapper.findByTestId('unassigned-users');
+
function mountComponent({
data,
users = [],
@@ -21,7 +44,7 @@ describe('Alert Details Sidebar Assignees', () => {
loading = false,
stubs = {},
} = {}) {
- wrapper = shallowMount(SidebarAssignees, {
+ wrapper = shallowMountExtended(SidebarAssignees, {
data() {
return {
users,
@@ -56,10 +79,7 @@ describe('Alert Details Sidebar Assignees', () => {
mock.restore();
});
- const findAssigned = () => wrapper.find('[data-testid="assigned-users"]');
- const findUnassigned = () => wrapper.find('[data-testid="unassigned-users"]');
-
- describe('updating the alert status', () => {
+ describe('sidebar expanded', () => {
const mockUpdatedMutationResult = {
data: {
alertSetAssignees: {
@@ -73,30 +93,13 @@ describe('Alert Details Sidebar Assignees', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- const path = '/-/autocomplete/users.json';
- const users = [
- {
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- id: 1,
- name: 'User 1',
- username: 'root',
- },
- {
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- id: 2,
- name: 'User 2',
- username: 'not-root',
- },
- ];
- mock.onGet(path).replyOnce(200, users);
+ mock.onGet(mockPath).replyOnce(200, mockUsers);
mountComponent({
data: { alert: mockAlert },
sidebarCollapsed: false,
loading: false,
- users,
+ users: mockUsers,
stubs: {
SidebarAssignee,
},
@@ -106,7 +109,11 @@ describe('Alert Details Sidebar Assignees', () => {
it('renders a unassigned option', async () => {
wrapper.setData({ isDropdownSearching: false });
await wrapper.vm.$nextTick();
- expect(wrapper.find(GlDropdownItem).text()).toBe('Unassigned');
+ expect(findDropdown().text()).toBe('Unassigned');
+ });
+
+ it('does not display the collapsed sidebar icon', () => {
+ expect(findSidebarIcon().exists()).toBe(false);
});
it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => {
@@ -170,4 +177,28 @@ describe('Alert Details Sidebar Assignees', () => {
expect(findAssigned().find('.dropdown-menu-user-username').text()).toBe('@root');
});
});
+
+ describe('sidebar collapsed', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onGet(mockPath).replyOnce(200, mockUsers);
+
+ mountComponent({
+ data: { alert: mockAlert },
+ loading: false,
+ users: mockUsers,
+ stubs: {
+ SidebarAssignee,
+ },
+ });
+ });
+ it('does not display the status dropdown', () => {
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ it('does display the collapsed sidebar icon', () => {
+ expect(findSidebarIcon().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
index 0014957517f..1b069c1fe90 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
@@ -1,5 +1,5 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
import AlertSidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
@@ -13,9 +13,10 @@ describe('Alert Details Sidebar Status', () => {
const findStatusDropdown = () => wrapper.find(GlDropdown);
const findStatusDropdownItem = () => wrapper.find(GlDropdownItem);
const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findStatusDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]');
+ const findStatusDropdownHeader = () => wrapper.findByTestId('dropdown-header');
const findAlertStatus = () => wrapper.findComponent(AlertStatus);
- const findStatus = () => wrapper.find('[data-testid="status"]');
+ const findStatus = () => wrapper.findByTestId('status');
+ const findSidebarIcon = () => wrapper.findByTestId('status-icon');
function mountComponent({
data,
@@ -24,7 +25,7 @@ describe('Alert Details Sidebar Status', () => {
stubs = {},
provide = {},
} = {}) {
- wrapper = mount(AlertSidebarStatus, {
+ wrapper = mountExtended(AlertSidebarStatus, {
propsData: {
alert: { ...mockAlert },
...data,
@@ -52,7 +53,7 @@ describe('Alert Details Sidebar Status', () => {
}
});
- describe('Alert Sidebar Dropdown Status', () => {
+ describe('sidebar expanded', () => {
beforeEach(() => {
mountComponent({
data: { alert: mockAlert },
@@ -69,6 +70,10 @@ describe('Alert Details Sidebar Status', () => {
expect(findStatusDropdownHeader().exists()).toBe(true);
});
+ it('does not display the collapsed sidebar icon', () => {
+ expect(findSidebarIcon().exists()).toBe(false);
+ });
+
describe('updating the alert status', () => {
const mockUpdatedMutationResult = {
data: {
@@ -109,22 +114,40 @@ describe('Alert Details Sidebar Status', () => {
expect(findStatusLoadingIcon().exists()).toBe(false);
expect(findStatus().text()).toBe('Triggered');
});
+
+ it('renders default translated statuses', () => {
+ mountComponent({ sidebarCollapsed: false });
+ expect(findAlertStatus().props('statuses')).toBe(PAGE_CONFIG.OPERATIONS.STATUSES);
+ expect(findStatus().text()).toBe('Triggered');
+ });
+
+ it('renders translated statuses', () => {
+ const status = 'TEST';
+ const statuses = { [status]: 'Test' };
+ mountComponent({
+ data: { alert: { ...mockAlert, status } },
+ provide: { statuses },
+ sidebarCollapsed: false,
+ });
+ expect(findAlertStatus().props('statuses')).toBe(statuses);
+ expect(findStatus().text()).toBe(statuses.TEST);
+ });
});
});
- describe('Statuses', () => {
- it('renders default translated statuses', () => {
- mountComponent({});
- expect(findAlertStatus().props('statuses')).toBe(PAGE_CONFIG.OPERATIONS.STATUSES);
- expect(findStatus().text()).toBe('Triggered');
+ describe('sidebar collapsed', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { alert: mockAlert },
+ loading: false,
+ });
+ });
+ it('does not display the status dropdown', () => {
+ expect(findStatusDropdown().exists()).toBe(false);
});
- it('renders translated statuses', () => {
- const status = 'TEST';
- const statuses = { [status]: 'Test' };
- mountComponent({ data: { alert: { ...mockAlert, status } }, provide: { statuses } });
- expect(findAlertStatus().props('statuses')).toBe(statuses);
- expect(findStatus().text()).toBe(statuses.TEST);
+ it('does display the collapsed sidebar icon', () => {
+ expect(findSidebarIcon().exists()).toBe(true);
});
});
});
diff --git a/spec/graphql/mutations/issues/create_spec.rb b/spec/graphql/mutations/issues/create_spec.rb
index 422ad40a9cb..b32f0991959 100644
--- a/spec/graphql/mutations/issues/create_spec.rb
+++ b/spec/graphql/mutations/issues/create_spec.rb
@@ -19,7 +19,8 @@ RSpec.describe Mutations::Issues::Create do
description: 'new description',
confidential: true,
due_date: Date.tomorrow,
- discussion_locked: true
+ discussion_locked: true,
+ issue_type: 'issue'
}
end
@@ -93,6 +94,16 @@ RSpec.describe Mutations::Issues::Create do
expect(mutated_issue.iid).not_to eq(special_params[:iid])
end
end
+
+ context 'when creating a non-default issue type' do
+ before do
+ mutation_params[:issue_type] = 'incident'
+ end
+
+ it 'creates issue with correct values' do
+ expect(mutated_issue.issue_type).to eq('incident')
+ end
+ end
end
context 'when creating an issue as owner' do
diff --git a/spec/graphql/mutations/issues/update_spec.rb b/spec/graphql/mutations/issues/update_spec.rb
index f10e257e153..bd780477658 100644
--- a/spec/graphql/mutations/issues/update_spec.rb
+++ b/spec/graphql/mutations/issues/update_spec.rb
@@ -128,6 +128,14 @@ RSpec.describe Mutations::Issues::Update do
expect(issue.reload.labels).to match_array([project_label, label_2])
end
end
+
+ context 'when changing type' do
+ it 'changes the type of the issue' do
+ mutation_params[:issue_type] = 'incident'
+
+ expect { subject }.to change { issue.reload.issue_type }.from('issue').to('incident')
+ end
+ end
end
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index fe7aa9894e9..1804a9a99cf 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -390,93 +390,6 @@ RSpec.describe ProjectsHelper do
end
end
- describe '#get_project_nav_tabs' do
- before do
- allow(helper).to receive(:current_user).and_return(user)
- allow(helper).to receive(:can?) { true }
- end
-
- subject do
- helper.send(:get_project_nav_tabs, project, user)
- end
-
- context 'when builds feature is enabled' do
- before do
- allow(project).to receive(:builds_enabled?).and_return(true)
- end
-
- it "does include pipelines tab" do
- is_expected.to include(:pipelines)
- end
- end
-
- context 'when builds feature is disabled' do
- before do
- allow(project).to receive(:builds_enabled?).and_return(false)
- end
-
- context 'when user has access to builds' do
- it "does include pipelines tab" do
- is_expected.to include(:pipelines)
- end
- end
-
- context 'when user does not have access to builds' do
- before do
- allow(helper).to receive(:can?) { false }
- end
-
- it "does not include pipelines tab" do
- is_expected.not_to include(:pipelines)
- end
- end
- end
-
- context 'when project has external wiki' do
- it 'includes external wiki tab' do
- project.create_external_wiki_service(active: true, properties: { 'external_wiki_url' => 'https://gitlab.com' })
- project.reload
-
- is_expected.to include(:external_wiki)
- end
- end
-
- context 'when project does not have external wiki' do
- it 'does not include external wiki tab' do
- expect(project.external_wiki).to be_nil
- is_expected.not_to include(:external_wiki)
- end
- end
-
- context 'when project has confluence enabled' do
- before do
- allow(project).to receive(:has_confluence?).and_return(true)
- end
-
- it { is_expected.to include(:confluence) }
- it { is_expected.not_to include(:wiki) }
- end
-
- context 'when project does not have confluence enabled' do
- it { is_expected.not_to include(:confluence) }
- it { is_expected.to include(:wiki) }
- end
-
- context 'learn gitlab experiment' do
- context 'when it is enabled' do
- before do
- expect(helper).to receive(:learn_gitlab_experiment_enabled?).with(project).and_return(true)
- end
-
- it { is_expected.to include(:learn_gitlab) }
- end
-
- context 'when it is not enabled' do
- it { is_expected.not_to include(:learn_gitlab) }
- end
- end
- end
-
describe '#show_projects' do
let(:projects) do
Project.all
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb
new file mode 100644
index 00000000000..c3b59904f41
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountIssuesMetric do
+ let_it_be(:issue) { create(:issue) }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }, 1
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb
new file mode 100644
index 00000000000..9f4686ab6cd
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUsersCreatingIssuesMetric do
+ let_it_be(:author) { create(:user) }
+ let_it_be(:issues) { create_list(:issue, 2, author: author, created_at: 4.days.ago) }
+ let_it_be(:old_issue) { create(:issue, author: author, created_at: 2.months.ago) }
+
+ context 'with all time frame' do
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }, 1
+ end
+
+ context 'for 28d time frame' do
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'database' }, 1
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb
index 2692e70cabf..18acd767c6d 100644
--- a/spec/lib/gitlab/usage_data_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_metrics_spec.rb
@@ -30,6 +30,18 @@ RSpec.describe Gitlab::UsageDataMetrics do
expect(subject[:redis_hll_counters][:quickactions]).to include(:i_quickactions_approve_monthly)
expect(subject[:redis_hll_counters][:quickactions]).to include(:i_quickactions_approve_weekly)
end
+
+ it 'includes counts keys' do
+ expect(subject[:counts]).to include(:issues)
+ end
+
+ it 'includes usage_activity_by_stage keys' do
+ expect(subject[:usage_activity_by_stage][:plan]).to include(:issues)
+ end
+
+ it 'includes usage_activity_by_stage_monthly keys' do
+ expect(subject[:usage_activity_by_stage_monthly][:plan]).to include(:issues)
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb
index 39b408faa90..66450f8c604 100644
--- a/spec/requests/api/graphql/mutations/issues/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb
@@ -20,7 +20,8 @@ RSpec.describe 'Create an issue' do
'title' => 'new title',
'description' => 'new description',
'confidential' => true,
- 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d')
+ 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d'),
+ 'type' => 'ISSUE'
}
end
@@ -37,7 +38,7 @@ RSpec.describe 'Create an issue' do
project.add_developer(current_user)
end
- it 'updates the issue' do
+ it 'creates the issue' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb
index 71f25dbbe49..adfa2a2bc08 100644
--- a/spec/requests/api/graphql/mutations/issues/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb
@@ -14,7 +14,8 @@ RSpec.describe 'Update of an existing issue' do
'title' => 'new title',
'description' => 'new description',
'confidential' => true,
- 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d')
+ 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d'),
+ 'type' => 'ISSUE'
}
end
diff --git a/yarn.lock b/yarn.lock
index 0e09c594867..4684471a671 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1256,10 +1256,10 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
-"@sourcegraph/code-host-integration@0.0.52":
- version "0.0.52"
- resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.52.tgz#3668364647b9248a0c43d738f7b046c551311338"
- integrity sha512-HYmiGuQ3XOQHJIQaRv63Wcdl6y1rgryBrCLzJd/mG5gkkhydTqBuf3wWFKgfL3PCm026OrjXu4PvOYU1fCTZJQ==
+"@sourcegraph/code-host-integration@0.0.57":
+ version "0.0.57"
+ resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.57.tgz#aed4649a51745deef5e4ee79b9a4fdc092471237"
+ integrity sha512-LLQp58+fqzM1IjAgti4zPwXrVVu2mNC8fpwNVnF23ead6JZPQe6Ap5fhOTZVE7ILQcFt78brGX/49Qib1Hsq0A==
"@stylelint/postcss-css-in-js@^0.37.2":
version "0.37.2"
@@ -3605,10 +3605,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
-core-js@^3.1.3, core-js@^3.12.0:
- version "3.12.0"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.12.0.tgz#62bac86f7d7f087d40dba3e90a211c2c3c8559ea"
- integrity sha512-SaMnchL//WwU2Ot1hhkPflE8gzo7uq1FGvUJ8GKmi3TOU7rGTHIU+eir1WGf6qOtTyxdfdcp10yPdGZ59sQ3hw==
+core-js@^3.1.3, core-js@^3.12.1:
+ version "3.12.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.12.1.tgz#6b5af4ff55616c08a44d386f1f510917ff204112"
+ integrity sha512-Ne9DKPHTObRuB09Dru5AjwKjY4cJHVGu+y5f7coGn1E9Grkc3p2iBwE9AI/nJzsE29mQF7oq+mhYYRqOMFN1Bw==
core-js@~2.3.0:
version "2.3.0"