diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-09 21:09:12 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-09 21:09:12 +0000 |
commit | d183d2d76bcc25f983c0836805c712af096bcc2f (patch) | |
tree | 982987f91e1cf268dbad55e51c4ea57292abbbd6 | |
parent | 453634293e24164ffaa5cd708e47a1cebc07bcd3 (diff) | |
download | gitlab-ce-d183d2d76bcc25f983c0836805c712af096bcc2f.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 Binary files differindex 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 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 |