summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-09 21:09:12 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-09 21:09:12 +0000
commitd183d2d76bcc25f983c0836805c712af096bcc2f (patch)
tree982987f91e1cf268dbad55e51c4ea57292abbbd6
parent453634293e24164ffaa5cd708e47a1cebc07bcd3 (diff)
downloadgitlab-ce-d183d2d76bcc25f983c0836805c712af096bcc2f.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/airflow/dags/components/dags.vue10
-rw-r--r--app/assets/javascripts/airflow/dags/components/incubation_alert.vue50
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue21
-rw-r--r--app/assets/javascripts/import_entities/import_groups/constants.js3
-rw-r--r--app/controllers/groups_controller.rb4
-rw-r--r--app/graphql/subscriptions/notes/created.rb11
-rw-r--r--app/models/concerns/work_item_resource_event.rb12
-rw-r--r--app/models/group.rb12
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/services/resource_events/change_labels_service.rb14
-rw-r--r--app/views/layouts/dashboard.html.haml8
-rw-r--r--app/views/layouts/explore.html.haml2
-rw-r--r--app/views/layouts/snippets.html.haml2
-rw-r--r--config/feature_flags/development/show_group_readme.yml (renamed from config/feature_flags/development/your_work_sidebar.yml)10
-rw-r--r--doc/administration/geo/replication/troubleshooting.md2
-rw-r--r--doc/api/linked_epics.md134
-rw-r--r--doc/development/documentation/styleguide/index.md1
-rw-r--r--doc/development/documentation/styleguide/word_list.md7
-rw-r--r--doc/user/group/contribution_analytics/img/group_stats_graph.pngbin35400 -> 69052 bytes
-rw-r--r--doc/user/group/contribution_analytics/index.md45
-rw-r--r--locale/gitlab.pot8
-rw-r--r--spec/frontend/airflow/dags/components/incubation_alert_spec.js35
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js26
-rw-r--r--spec/models/group_spec.rb24
-rw-r--r--spec/models/merge_request_spec.rb28
-rw-r--r--spec/requests/api/graphql/subscriptions/notes/created_spec.rb59
-rw-r--r--spec/services/resource_events/change_labels_service_spec.rb23
-rw-r--r--spec/support/shared_examples/models/resource_event_shared_examples.rb20
-rw-r--r--spec/views/layouts/snippets.html.haml_spec.rb30
29 files changed, 431 insertions, 172 deletions
diff --git a/app/assets/javascripts/airflow/dags/components/dags.vue b/app/assets/javascripts/airflow/dags/components/dags.vue
index 71d79e59dbf..88eb3fd5aba 100644
--- a/app/assets/javascripts/airflow/dags/components/dags.vue
+++ b/app/assets/javascripts/airflow/dags/components/dags.vue
@@ -4,7 +4,7 @@ import { s__ } from '~/locale';
import { setUrlParams } from '~/lib/utils/url_utility';
import { formatDate } from '~/lib/utils/datetime/date_format_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import IncubationAlert from './incubation_alert.vue';
+import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
export default {
name: 'AirflowDags',
@@ -73,13 +73,19 @@ export default {
isActiveLabel: s__('Airflow|Is active'),
isPausedLabel: s__('Airflow|Is paused'),
fileLocLabel: s__('Airflow|DAG file location'),
+ featureName: s__('Airflow|GitLab Airflow integration'),
},
+ linkToFeedbackIssue:
+ 'https://gitlab.com/gitlab-org/incubation-engineering/airflow/meta/-/issues/2',
};
</script>
<template>
<div>
- <incubation-alert />
+ <incubation-alert
+ :feature-name="$options.i18n.featureName"
+ :link-to-feedback-issue="$options.linkToFeedbackIssue"
+ />
<gl-empty-state
v-if="!dags.length"
:title="$options.i18n.emptyStateLabel"
diff --git a/app/assets/javascripts/airflow/dags/components/incubation_alert.vue b/app/assets/javascripts/airflow/dags/components/incubation_alert.vue
deleted file mode 100644
index a89490254ab..00000000000
--- a/app/assets/javascripts/airflow/dags/components/incubation_alert.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<script>
-import { GlAlert, GlLink } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
-
-export default {
- name: 'AirflowIncubationAlert',
- components: { GlAlert, GlLink },
- data() {
- return {
- isAlertDismissed: false,
- };
- },
- computed: {
- hasAlert() {
- return !this.isAlertDismissed;
- },
- },
- methods: {
- dismissAlert() {
- this.isAlertDismissed = true;
- },
- },
- i18n: {
- titleLabel: s__('Airflow|GitLab Airflow integration is in the Incubating Phase.'),
- contentLabel: s__(
- 'Incubation|GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited.',
- ),
- learnMoreLabel: __('Learn more'),
- feedbackLabel: __('Feedback'),
- },
-};
-</script>
-
-<template>
- <gl-alert
- v-if="hasAlert"
- :title="$options.i18n.titleLabel"
- variant="warning"
- :primary-button-text="$options.i18n.feedbackLabel"
- primary-button-link="https://gitlab.com/gitlab-org/incubation-engineering/airflow/meta/-/issues/2"
- @dismiss="dismissAlert"
- >
- {{ $options.i18n.contentLabel }}
- <gl-link
- href="https://about.gitlab.com/handbook/engineering/incubation/airflow/"
- target="_blank"
- >{{ $options.i18n.learnMoreLabel }}</gl-link
- >
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 68e255e14b9..d686522c748 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -17,6 +17,7 @@ import {
import { debounce } from 'lodash';
import { createAlert } from '~/flash';
import { s__, __, n__, sprintf } from '~/locale';
+import { HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
@@ -181,7 +182,7 @@ export default {
const status = this.getStatus(group);
const flags = {
- isInvalid: importTarget.validationErrors?.length > 0,
+ isInvalid: (importTarget.validationErrors ?? []).filter((e) => !e.nonBlocking).length > 0,
isAvailableForImport: isAvailableForImport(group) && status !== STATUSES.SCHEDULING,
isFinished: isFinished(group),
};
@@ -376,11 +377,19 @@ export default {
variables: { importRequests },
});
} catch (error) {
- createAlert({
- message: i18n.ERROR_IMPORT,
- captureError: true,
- error,
- });
+ if (error.networkError?.response?.status === HTTP_STATUS_TOO_MANY_REQUESTS) {
+ newPendingGroupsIds.forEach((id) => {
+ this.importTargets[id].validationErrors = [
+ { field: NEW_NAME_FIELD, message: i18n.ERROR_TOO_MANY_REQUESTS, nonBlocking: true },
+ ];
+ });
+ } else {
+ createAlert({
+ message: i18n.ERROR_IMPORT,
+ captureError: true,
+ error,
+ });
+ }
} finally {
this.pendingGroupsIds = this.pendingGroupsIds.filter(
(id) => !newPendingGroupsIds.includes(id),
diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js
index 7e532dfec05..60938272d11 100644
--- a/app/assets/javascripts/import_entities/import_groups/constants.js
+++ b/app/assets/javascripts/import_entities/import_groups/constants.js
@@ -11,6 +11,9 @@ export const i18n = {
),
ERROR_IMPORT: s__('BulkImport|Importing the group failed.'),
ERROR_IMPORT_COMPLETED: s__('BulkImport|Import is finished. Pick another name for re-import'),
+ ERROR_TOO_MANY_REQUESTS: s__(
+ 'Bulkmport|Over six imports in one minute were attempted. Wait at least one minute and try again.',
+ ),
NO_GROUPS_FOUND: s__('BulkImport|No groups found'),
OWNER: __('Owner'),
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 7af6e666c26..8f7a2c177b7 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -40,6 +40,10 @@ class GroupsController < Groups::ApplicationController
push_force_frontend_feature_flag(:work_items, group.work_items_feature_flag_enabled?)
end
+ before_action only: :show do
+ push_frontend_feature_flag(:show_group_readme, group)
+ end
+
helper_method :captcha_required?
skip_cross_project_access_check :index, :new, :create, :edit, :update,
diff --git a/app/graphql/subscriptions/notes/created.rb b/app/graphql/subscriptions/notes/created.rb
index 873280286f7..07b7b308163 100644
--- a/app/graphql/subscriptions/notes/created.rb
+++ b/app/graphql/subscriptions/notes/created.rb
@@ -4,6 +4,17 @@ module Subscriptions
module Notes
class Created < Base
payload_type ::Types::Notes::NoteType
+
+ def update(*args)
+ case object
+ when ResourceEvent
+ object.work_item_synthetic_system_note
+ when Array
+ object.first.work_item_synthetic_system_note(events: object)
+ else
+ object
+ end
+ end
end
end
end
diff --git a/app/models/concerns/work_item_resource_event.rb b/app/models/concerns/work_item_resource_event.rb
index d0323feb029..ddf39787f63 100644
--- a/app/models/concerns/work_item_resource_event.rb
+++ b/app/models/concerns/work_item_resource_event.rb
@@ -5,6 +5,18 @@ module WorkItemResourceEvent
included do
belongs_to :work_item, foreign_key: 'issue_id'
+
+ scope :with_work_item, -> { preload(:work_item) }
+
+ # These events are created also on non work items, e.g. MRs, Epic however system notes subscription
+ # is only implemented on work items, so we do check if this event is linked to an work item. This can be
+ # expanded to other issuables later on.
+ after_commit :trigger_note_subscription_create, on: :create, if: -> { work_item.present? }
+ end
+
+ # System notes are not updated or deleted, so firing just the noteCreated event.
+ def trigger_note_subscription_create(events: self)
+ GraphqlTriggers.work_item_note_created(work_item.to_gid, events)
end
def work_item_synthetic_system_note(events: nil)
diff --git a/app/models/group.rb b/app/models/group.rb
index 3455b4d8507..7e09280dfff 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -25,6 +25,8 @@ class Group < Namespace
extend ::Gitlab::Utils::Override
+ README_PROJECT_PATH = 'gitlab-profile'
+
def self.sti_name
'Group'
end
@@ -946,6 +948,16 @@ class Group < Namespace
direct_and_indirect_members.find_each(&:update_two_factor_requirement)
end
+ def readme_project
+ projects.find_by(path: README_PROJECT_PATH)
+ end
+ strong_memoize_attr :readme_project
+
+ def group_readme
+ readme_project&.repository&.readme
+ end
+ strong_memoize_attr :group_readme
+
private
def feature_flag_enabled_for_self_or_ancestor?(feature_flag)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 3bc60ee1f8e..8646e6aecb1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -287,7 +287,7 @@ class MergeRequest < ApplicationRecord
validates :merge_user, presence: true, if: :auto_merge_enabled?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?]
validate :validate_fork, unless: :closed_or_merged_without_fork?
- validate :validate_target_project, on: :create
+ validate :validate_target_project, on: :create, unless: :importing?
validate :validate_reviewer_size_length, unless: :importing?
scope :by_source_or_target_branch, ->(branch_name) do
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index 7e176f95db0..02182bc3a77 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -23,16 +23,22 @@ module ResourceEvents
label_hash.merge(label_id: label.id, action: ResourceLabelEvent.actions['remove'])
end
- ApplicationRecord.legacy_bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert
+ ids = ApplicationRecord.legacy_bulk_insert(ResourceLabelEvent.table_name, labels, return_ids: true) # rubocop:disable Gitlab/BulkInsert
- create_timeline_events_from(added_labels: added_labels, removed_labels: removed_labels)
+ if resource.is_a?(Issue)
+ events = ResourceLabelEvent.id_in(ids)
+ events.first.trigger_note_subscription_create(events: events.to_a) if events.any?
+ end
+ create_timeline_events_from(added_labels: added_labels, removed_labels: removed_labels)
resource.expire_note_etag_cache
return unless resource.is_a?(Issue)
- Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_label_changed_action(author: user,
- project: resource.project)
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_label_changed_action(
+ author: user, project: resource.project)
+
+ events
end
private
diff --git a/app/views/layouts/dashboard.html.haml b/app/views/layouts/dashboard.html.haml
index 028c22fe9e5..89f238eb6b3 100644
--- a/app/views/layouts/dashboard.html.haml
+++ b/app/views/layouts/dashboard.html.haml
@@ -1,9 +1,7 @@
- page_title _("Dashboard")
- header_title _("Dashboard"), root_path unless header_title
-- if Feature.enabled?(:your_work_sidebar, current_user)
- - @left_sidebar = true
- - nav "your_work"
-- else
- - @hide_breadcrumbs = true
+
+- @left_sidebar = true
+- nav "your_work"
= render template: "layouts/application"
diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml
index 389dee853ba..c495bab4547 100644
--- a/app/views/layouts/explore.html.haml
+++ b/app/views/layouts/explore.html.haml
@@ -1,6 +1,6 @@
- page_title _("Explore")
-- if current_user && Feature.enabled?(:your_work_sidebar, current_user)
+- if current_user
- @left_sidebar = true
- nav "your_work"
diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml
index fd331d4b6c8..95a204a3319 100644
--- a/app/views/layouts/snippets.html.haml
+++ b/app/views/layouts/snippets.html.haml
@@ -2,7 +2,7 @@
- header_title _("Snippets"), snippets_path
- snippets_upload_path = snippets_upload_path(@snippet, current_user)
-- if current_user && Feature.enabled?(:your_work_sidebar, current_user)
+- if current_user
- @left_sidebar = true
- nav "your_work"
diff --git a/config/feature_flags/development/your_work_sidebar.yml b/config/feature_flags/development/show_group_readme.yml
index b24af6e1ff4..b5764b9195f 100644
--- a/config/feature_flags/development/your_work_sidebar.yml
+++ b/config/feature_flags/development/show_group_readme.yml
@@ -1,8 +1,8 @@
---
-name: your_work_sidebar
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107345
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385855
-milestone: '15.8'
+name: show_group_readme
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109480
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/390230
+milestone: '15.9'
type: development
-group: group::foundations
+group: group::organization
default_enabled: false
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 804abad22a2..baa6cc4736e 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -42,7 +42,7 @@ to help identify if something is wrong:
![Geo health check](img/geo_site_health_v14_0.png)
-A site shows as "Unhealthy" if the site's status is more than 10 minutes old. It that case, try running the following in the [Rails console](../../operations/rails_console.md) on the affected site:
+A site shows as "Unhealthy" if the site's status is more than 10 minutes old. In that case, try running the following in the [Rails console](../../operations/rails_console.md) on the affected site:
```ruby
Geo::MetricsUpdateWorker.new.perform
diff --git a/doc/api/linked_epics.md b/doc/api/linked_epics.md
index 65f4c338b97..434e6080ffb 100644
--- a/doc/api/linked_epics.md
+++ b/doc/api/linked_epics.md
@@ -11,7 +11,129 @@ info: To determine the technical writer assigned to the Stage/Group associated w
If the Related Epics feature is not available in your GitLab plan, a `403` status code is returned.
-## List linked epics
+## List related epic links from a group
+
+Get a list of a given group's related epic links within group and sub-groups, filtered according to the user authorizations.
+The user needs to have access to the `source_epic` and `target_epic` to access the related epic link.
+
+```plaintext
+GET /groups/:id/epics/related_epic_links
+```
+
+Supported attributes:
+
+| Attribute | Type | Required | Description |
+| ---------- | -------------- | ---------------------- | ------------------------------------------------------------------------- |
+| `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
+| `created_after` | string | no | Return related epic links created on or after the given time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) |
+| `created_before` | string | no | Return related epic links created on or before the given time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) |
+| `updated_after` | string | no | Return related epic links updated on or after the given time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) |
+| `updated_before` | string | no | Return related epic links updated on or before the given time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/related_epic_links"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "created_at": "2022-01-31T15:10:44.988Z",
+ "updated_at": "2022-01-31T15:10:44.988Z",
+ "link_type": "relates_to",
+ "source_epic": {
+ "id": 21,
+ "iid": 1,
+ "color": "#1068bf",
+ "text_color": "#FFFFFF",
+ "group_id": 26,
+ "parent_id": null,
+ "parent_iid": null,
+ "title": "Aspernatur recusandae distinctio omnis et qui est iste.",
+ "description": "some description",
+ "confidential": false,
+ "author": {
+ "id": 15,
+ "username": "trina",
+ "name": "Theresia Robel",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/085e28df717e16484cbf6ceca75e9a93?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/trina"
+ },
+ "start_date": null,
+ "end_date": null,
+ "due_date": null,
+ "state": "opened",
+ "web_url": "http://gitlab.example.com/groups/flightjs/-/epics/1",
+ "references": {
+ "short": "&1",
+ "relative": "&1",
+ "full": "flightjs&1"
+ },
+ "created_at": "2022-01-31T15:10:44.988Z",
+ "updated_at": "2022-03-16T09:32:35.712Z",
+ "closed_at": null,
+ "labels": [],
+ "upvotes": 0,
+ "downvotes": 0,
+ "_links": {
+ "self": "http://gitlab.example.com/api/v4/groups/26/epics/1",
+ "epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/1/issues",
+ "group": "http://gitlab.example.com/api/v4/groups/26",
+ "parent": null
+ }
+ },
+ "target_epic": {
+ "id": 25,
+ "iid": 5,
+ "color": "#1068bf",
+ "text_color": "#FFFFFF",
+ "group_id": 26,
+ "parent_id": null,
+ "parent_iid": null,
+ "title": "Aut assumenda id nihil distinctio fugiat vel numquam est.",
+ "description": "some description",
+ "confidential": false,
+ "author": {
+ "id": 3,
+ "username": "valerie",
+ "name": "Erika Wolf",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/9ef7666abb101418a4716a8ed4dded80?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/valerie"
+ },
+ "start_date": null,
+ "end_date": null,
+ "due_date": null,
+ "state": "opened",
+ "web_url": "http://gitlab.example.com/groups/flightjs/-/epics/5",
+ "references": {
+ "short": "&5",
+ "relative": "&5",
+ "full": "flightjs&5"
+ },
+ "created_at": "2022-01-31T15:10:45.080Z",
+ "updated_at": "2022-03-16T09:32:35.842Z",
+ "closed_at": null,
+ "labels": [],
+ "upvotes": 0,
+ "downvotes": 0,
+ "_links": {
+ "self": "http://gitlab.example.com/api/v4/groups/26/epics/5",
+ "epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/5/issues",
+ "group": "http://gitlab.example.com/api/v4/groups/26",
+ "parent": null
+ }
+ },
+ }
+]
+```
+
+## List linked epics from an epic
Get a list of a given epic's linked epics filtered according to the user authorizations.
@@ -118,6 +240,10 @@ Example response:
```json
{
+ "id": 1,
+ "created_at": "2022-01-31T15:10:44.988Z",
+ "updated_at": "2022-01-31T15:10:44.988Z",
+ "link_type": "relates_to",
"source_epic": {
"id": 21,
"iid": 1,
@@ -202,7 +328,6 @@ Example response:
"parent": null
}
},
- "link_type": "relates_to"
}
```
@@ -235,6 +360,10 @@ Example response:
```json
{
+ "id": 1,
+ "created_at": "2022-01-31T15:10:44.988Z",
+ "updated_at": "2022-01-31T15:10:44.988Z",
+ "link_type": "relates_to",
"source_epic": {
"id": 21,
"iid": 1,
@@ -319,6 +448,5 @@ Example response:
"parent": null
}
},
- "link_type": "relates_to"
}
```
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 9ae3bba564f..74437ea46c9 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -182,6 +182,7 @@ the page is rendered to HTML. There can be only **one** level 1 heading per page
Heading levels greater than `H5` do not display in the right sidebar navigation.
- Do not skip a level. For example: `##` > `####`.
- Leave one blank line before and after the topic title.
+- If you use code in topic titles, ensure the code is in backticks.
### Backticks in Markdown
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index 5d7c94a6721..7e77e19c4a2 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -1363,16 +1363,19 @@ in present tense, active voice.
## you, your, yours
-Use **you**, **your**, and **yours** instead of **the user** and **the user's**.
-Documentation should be from the [point of view](https://design.gitlab.com/content/voice-and-tone#point-of-view) of the reader.
+Use **you** instead of **the user**, **the administrator** or **the customer**.
+Documentation should speak directly to the user, whether that user is someone installing the product,
+configuring it, administering it, or using it.
Use:
- You can configure a pipeline.
+- You can reset a user's password. (In content for an administrator)
Instead of:
- Users can configure a pipeline.
+- Administrators can reset a user's password.
## you can
diff --git a/doc/user/group/contribution_analytics/img/group_stats_graph.png b/doc/user/group/contribution_analytics/img/group_stats_graph.png
index ccfd3782c6f..1c38a9c1fdf 100644
--- a/doc/user/group/contribution_analytics/img/group_stats_graph.png
+++ b/doc/user/group/contribution_analytics/img/group_stats_graph.png
Binary files differ
diff --git a/doc/user/group/contribution_analytics/index.md b/doc/user/group/contribution_analytics/index.md
index 55345a0b865..b0347ba5caa 100644
--- a/doc/user/group/contribution_analytics/index.md
+++ b/doc/user/group/contribution_analytics/index.md
@@ -4,25 +4,26 @@ stage: Plan
group: Optimize
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# Contribution Analytics **(PREMIUM)**
+# Contribution analytics **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3090) in GitLab 12.2 for subgroups.
-With Contribution Analytics, you can get an overview of the [contribution events](../../profile/contributions_calendar.md#user-contribution-events) in your
-group.
+Contribution analytics provide an overview of the
+[contribution events](../../profile/contributions_calendar.md#user-contribution-events) made by your group's members.
-- Analyze your team's contributions over a period of time.
-- Identify opportunities for improvement with group members who may benefit from additional
- support.
+Use contribution analytics data visualizations to:
-## View Contribution Analytics
+- Analyze your group's contributions over a period of time.
+- Identify group members who are high-performers or may benefit from additional support.
-To view Contribution Analytics:
+## View contribution analytics
+
+To view contribution analytics:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Analytics > Contribution**.
-Three bar graphs illustrate the number of contributions made by each group member:
+Three bar charts and a table illustrate the number of contributions made by each group member:
- Push events
- Merge requests
@@ -30,7 +31,9 @@ Three bar graphs illustrate the number of contributions made by each group membe
### View a member's contributions
-Hover over each bar to display the number of events for a specific group member.
+You can view the number of events associated with a specific group member.
+
+To do this, hover over the bar with the member's name.
![Contribution analytics bar graphs](img/group_stats_graph.png)
@@ -42,30 +45,24 @@ To do this, select the sliders (**{status-paused}**) below the chart and slide t
### Sort contributions
-Contributions per group member are also presented in tabular format. Select a column header to sort the table by that column:
-
-- Member name
-- Number of pushed events
-- Number of opened issues
-- Number of closed issues
-- Number of opened MRs
-- Number of merged MRs
-- Number of closed MRs
-- Number of total contributions
+Contributions per group member are also displayed in tabular format.
+The table columns include the members' names and the number of contributions for different events.
-![Contribution analytics contributions table](img/group_stats_table.png)
+To sort the table by a column, select the column header or the chevron (**{chevron-lg-down}**
+for descending order, **{chevron-lg-up}** for ascending order).
## Change the time period
-You can choose from the following three periods:
+You can display contribution analytics over different time periods:
- Last week (default)
- Last month
- Last three months
-Select the desired period from the calendar dropdown list.
+To change the time period of the contribution analytics, select one of the three tabs
+under **Contribution Analytics**.
-![Contribution analytics choose period](img/group_stats_cal.png)
+The selected time period applies to all charts and the table.
## Contribution analytics GraphQL API
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f4f1fd3fb24..39e9d8e1f11 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3712,7 +3712,7 @@ msgstr ""
msgid "Airflow|Either the Airflow instance does not contain DAGs or has yet to be configured"
msgstr ""
-msgid "Airflow|GitLab Airflow integration is in the Incubating Phase."
+msgid "Airflow|GitLab Airflow integration"
msgstr ""
msgid "Airflow|Is active"
@@ -7652,6 +7652,9 @@ msgstr ""
msgid "BulkImport|must be a group"
msgstr ""
+msgid "Bulkmport|Over six imports in one minute were attempted. Wait at least one minute and try again."
+msgstr ""
+
msgid "Bullet list"
msgstr ""
@@ -17524,9 +17527,6 @@ msgstr ""
msgid "February"
msgstr ""
-msgid "Feedback"
-msgstr ""
-
msgid "Feedback and Updates"
msgstr ""
diff --git a/spec/frontend/airflow/dags/components/incubation_alert_spec.js b/spec/frontend/airflow/dags/components/incubation_alert_spec.js
deleted file mode 100644
index 29188de8025..00000000000
--- a/spec/frontend/airflow/dags/components/incubation_alert_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { GlAlert, GlButton, GlLink } from '@gitlab/ui';
-import IncubationAlert from '~/airflow/dags/components/incubation_alert.vue';
-
-describe('IncubationAlert', () => {
- let wrapper;
-
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- const findButton = () => wrapper.findComponent(GlButton);
-
- const findHref = () => wrapper.findComponent(GlLink);
-
- beforeEach(() => {
- wrapper = mount(IncubationAlert);
- });
-
- it('displays link to issue', () => {
- expect(findButton().attributes().href).toBe(
- 'https://gitlab.com/gitlab-org/incubation-engineering/airflow/meta/-/issues/2',
- );
- });
-
- it('displays link to handbook', () => {
- expect(findHref().attributes().href).toBe(
- 'https://about.gitlab.com/handbook/engineering/incubation/airflow/',
- );
- });
-
- it('is removed if dismissed', async () => {
- await wrapper.find('[aria-label="Dismiss"]').trigger('click');
-
- expect(findAlert().exists()).toBe(false);
- });
-});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index 8d06c196d70..480f1bad8d4 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createAlert } from '~/flash';
-import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK, HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
import { i18n, ROOT_NAMESPACE } from '~/import_entities/import_groups/constants';
@@ -270,8 +270,6 @@ describe('import table', () => {
},
});
- axiosMock.onPost('/import/bulk_imports.json').reply(HTTP_STATUS_BAD_REQUEST);
-
await waitForPromises();
await findImportButtons()[0].trigger('click');
await waitForPromises();
@@ -283,6 +281,28 @@ describe('import table', () => {
);
});
+ it('displays inline error if importing group reports rate limit', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [FAKE_GROUP],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ importGroups: () => {
+ const error = new Error();
+ error.response = { status: HTTP_STATUS_TOO_MANY_REQUESTS };
+ throw error;
+ },
+ });
+
+ await waitForPromises();
+ await findImportButtons()[0].trigger('click');
+ await waitForPromises();
+
+ expect(createAlert).not.toHaveBeenCalled();
+ expect(wrapper.find('tbody tr').text()).toContain(i18n.ERROR_TOO_MANY_REQUESTS);
+ });
+
describe('pagination', () => {
const bulkImportSourceGroupsQueryMock = jest.fn().mockResolvedValue({
nodes: [FAKE_GROUP],
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index c51e098556c..0a05c558d45 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -3717,4 +3717,28 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
end
+
+ describe '#readme_project' do
+ it 'returns groups project containing metadata' do
+ readme_project = create(:project, path: Group::README_PROJECT_PATH, namespace: group)
+ create(:project, namespace: group)
+
+ expect(group.readme_project).to eq(readme_project)
+ end
+ end
+
+ describe '#group_readme' do
+ it 'returns readme from group readme project' do
+ create(:project, :repository, path: Group::README_PROJECT_PATH, namespace: group)
+
+ expect(group.group_readme.name).to eq('README.md')
+ expect(group.group_readme.data).to include('testme')
+ end
+
+ it 'returns nil if no readme project is present' do
+ create(:project, :repository, namespace: group)
+
+ expect(group.group_readme).to be(nil)
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 466c5574146..2e2355ba710 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -314,6 +314,34 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
expect(subject).to be_valid
end
end
+
+ describe '#validate_target_project' do
+ let(:merge_request) do
+ build(:merge_request, source_project: project, target_project: project, importing: importing)
+ end
+
+ let(:project) { build_stubbed(:project) }
+ let(:importing) { false }
+
+ context 'when projects #merge_requests_enabled? is true' do
+ it { expect(merge_request.valid?(false)).to eq true }
+ end
+
+ context 'when projects #merge_requests_enabled? is false' do
+ let(:project) { build_stubbed(:project, merge_requests_enabled: false) }
+
+ it 'is invalid' do
+ expect(merge_request.valid?(false)).to eq false
+ expect(merge_request.errors.full_messages).to contain_exactly('Target project has disabled merge requests')
+ end
+
+ context 'when #import? is true' do
+ let(:importing) { true }
+
+ it { expect(merge_request.valid?(false)).to eq true }
+ end
+ end
+ end
end
describe 'callbacks' do
diff --git a/spec/requests/api/graphql/subscriptions/notes/created_spec.rb b/spec/requests/api/graphql/subscriptions/notes/created_spec.rb
index 7161b17d0a8..f955c14ef3b 100644
--- a/spec/requests/api/graphql/subscriptions/notes/created_spec.rb
+++ b/spec/requests/api/graphql/subscriptions/notes/created_spec.rb
@@ -115,4 +115,63 @@ RSpec.describe 'Subscriptions::Notes::Created', feature_category: :team_planning
end
end
end
+
+ context 'when resource events are triggering note subscription' do
+ let_it_be(:label1) { create(:label, project: project, title: 'foo') }
+ let_it_be(:label2) { create(:label, project: project, title: 'bar') }
+
+ subject(:response) do
+ subscription_response do
+ # this creates note defined with let lazily and triggers the subscription event
+ resource_event
+ end
+ end
+
+ context 'when user is unauthorized' do
+ let(:resource_event) { create(:resource_label_event, issue: task, label: label1) }
+
+ it "does not receive discussion data" do
+ expect(response).to be_nil
+ end
+ end
+
+ context 'when user is authorized' do
+ let(:current_user) { guest }
+ let(:resource_event) { create(:resource_label_event, issue: task, label: label1) }
+
+ it "receives created synthetic note as a discussion" do
+ response
+
+ event = ResourceLabelEvent.find(resource_event.id)
+ discussion_id = event.discussion_id
+ discussion_gid = ::Gitlab::GlobalId.as_global_id(discussion_id, model_name: 'Discussion').to_s
+ note_gid = ::Gitlab::GlobalId.as_global_id(discussion_id, model_name: 'LabelNote').to_s
+
+ expect(response_note['id']).to eq(note_gid)
+ expect(discussion['id']).to eq(discussion_gid)
+ expect(discussion_notes.size).to eq(1)
+ expect(discussion_notes.pluck('id')).to match_array([note_gid])
+ end
+
+ context 'when several label events are created' do
+ let(:resource_event) do
+ ResourceEvents::ChangeLabelsService.new(task, current_user).execute(added_labels: [label1, label2])
+ end
+
+ it "receives created synthetic note as a discussion" do
+ response
+
+ event = ResourceLabelEvent.where(label_id: [label1, label2]).first
+ discussion_id = event.discussion_id
+ discussion_gid = ::Gitlab::GlobalId.as_global_id(discussion_id, model_name: 'Discussion').to_s
+ note_gid = ::Gitlab::GlobalId.as_global_id(discussion_id, model_name: 'LabelNote').to_s
+
+ expect(response_note['id']).to eq(note_gid)
+ expect(discussion['id']).to eq(discussion_gid)
+ expect(discussion_notes.size).to eq(1)
+ expect(discussion_notes.pluck('id')).to match_array([note_gid])
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb
index 9b0ca54a394..d94b49de9d7 100644
--- a/spec/services/resource_events/change_labels_service_spec.rb
+++ b/spec/services/resource_events/change_labels_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe ResourceEvents::ChangeLabelsService do
+# feature category is shared among plan(issues, epics), monitor(incidents), create(merge request) stages
+RSpec.describe ResourceEvents::ChangeLabelsService, feature_category: :shared do
let_it_be(:project) { create(:project) }
let_it_be(:author) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }
@@ -86,12 +87,30 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
let(:added) { [labels[0]] }
let(:removed) { [labels[1]] }
+ it_behaves_like 'creating timeline events'
+
it 'creates all label events in a single query' do
expect(ApplicationRecord).to receive(:legacy_bulk_insert).once.and_call_original
expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(2)
end
- it_behaves_like 'creating timeline events'
+ context 'when resource is a work item' do
+ it 'triggers note created subscription' do
+ expect(GraphqlTriggers).to receive(:work_item_note_created)
+
+ change_labels
+ end
+ end
+
+ context 'when resource is an MR' do
+ let(:resource) { create(:merge_request, source_project: project) }
+
+ it 'does not trigger note created subscription' do
+ expect(GraphqlTriggers).not_to receive(:work_item_note_created)
+
+ change_labels
+ end
+ end
end
describe 'usage data' do
diff --git a/spec/support/shared_examples/models/resource_event_shared_examples.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb
index 8cab2de076d..038ff33c68a 100644
--- a/spec/support/shared_examples/models/resource_event_shared_examples.rb
+++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb
@@ -160,6 +160,16 @@ RSpec.shared_examples 'a resource event for merge requests' do
end
end
end
+
+ context 'on callbacks' do
+ it 'does not trigger note created subscription' do
+ event = build(described_class.name.underscore.to_sym, merge_request: merge_request1)
+
+ expect(GraphqlTriggers).not_to receive(:work_item_note_created)
+ expect(event).not_to receive(:trigger_note_subscription_create)
+ event.save!
+ end
+ end
end
RSpec.shared_examples 'a note for work item resource event' do
@@ -172,4 +182,14 @@ RSpec.shared_examples 'a note for work item resource event' do
expect(event.work_item_synthetic_system_note.class.name).to eq(event.synthetic_note_class.name)
end
+
+ context 'on callbacks' do
+ it 'triggers note created subscription' do
+ event = build(described_class.name.underscore.to_sym, issue: work_item)
+
+ expect(GraphqlTriggers).to receive(:work_item_note_created)
+ expect(event).to receive(:trigger_note_subscription_create).and_call_original
+ event.save!
+ end
+ end
end
diff --git a/spec/views/layouts/snippets.html.haml_spec.rb b/spec/views/layouts/snippets.html.haml_spec.rb
index 5c182f715d6..1e6963a6526 100644
--- a/spec/views/layouts/snippets.html.haml_spec.rb
+++ b/spec/views/layouts/snippets.html.haml_spec.rb
@@ -9,34 +9,18 @@ RSpec.describe 'layouts/snippets', feature_category: :source_code_management do
end
describe 'sidebar' do
- context 'when feature flag is on' do
- context 'when signed in' do
- let(:user) { build_stubbed(:user) }
-
- it 'renders the "Your work" sidebar' do
- render
-
- expect(rendered).to have_css('aside.nav-sidebar[aria-label="Your work"]')
- end
- end
-
- context 'when not signed in' do
- let(:user) { nil }
+ context 'when signed in' do
+ let(:user) { build_stubbed(:user) }
- it 'renders no sidebar' do
- render
+ it 'renders the "Your work" sidebar' do
+ render
- expect(rendered).not_to have_css('aside.nav-sidebar')
- end
+ expect(rendered).to have_css('aside.nav-sidebar[aria-label="Your work"]')
end
end
- context 'when feature flag is off' do
- before do
- stub_feature_flags(your_work_sidebar: false)
- end
-
- let(:user) { build_stubbed(:user) }
+ context 'when not signed in' do
+ let(:user) { nil }
it 'renders no sidebar' do
render