Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-01-12 06:08:58 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-01-12 06:08:58 +0300
commit15a3bba5243f072fa4fea79de92211672a45bb64 (patch)
treefe0c66406fd623f2a7802729c0769dbf7941e65d
parentb53b2fbb6b393f28211fa2c2c5bdb519b6e7bc08 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/index.js5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js13
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue1
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue70
-rw-r--r--app/controllers/projects/incidents_controller.rb1
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb10
-rw-r--r--app/views/projects/triggers/_index.html.haml27
-rw-r--r--app/views/projects/triggers/_trigger.html.haml37
-rw-r--r--config/feature_flags/development/incident_event_tags.yml (renamed from config/feature_flags/development/ci_pipeline_triggers_settings_vue_ui.yml)10
-rw-r--r--doc/administration/operations/moving_repositories.md9
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/group_repository_storage_moves.md2
-rw-r--r--doc/api/project_repository_storage_moves.md2
-rw-r--r--doc/api/snippet_repository_storage_moves.md2
-rw-r--r--doc/user/project/repository/branches/index.md24
-rw-r--r--locale/gitlab.pot30
-rw-r--r--spec/features/triggers_spec.rb14
-rw-r--r--spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js9
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js1
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js142
21 files changed, 275 insertions, 137 deletions
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
index f2972133aad..3ea8e0df022 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
@@ -13,11 +13,6 @@ const parseJsonArray = (triggers) => {
export default (containerId = 'js-ci-pipeline-triggers-list') => {
const containerEl = document.getElementById(containerId);
- // Note: Remove this check when FF `ci_pipeline_triggers_settings_vue_ui` is removed.
- if (!containerEl) {
- return null;
- }
-
const triggers = parseJsonArray(containerEl.dataset.triggers);
return new Vue({
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index 22db19610c1..2fdae538902 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -12,6 +12,9 @@ export const timelineFormI18n = Object.freeze({
'Incident|Something went wrong while creating the incident timeline event.',
),
areaPlaceholder: s__('Incident|Timeline text...'),
+ areaDefaultMessage: s__('Incident|Incident'),
+ selectTags: __('Select tags'),
+ tagsLabel: __('Event tag (optional)'),
save: __('Save'),
cancel: __('Cancel'),
delete: __('Delete'),
@@ -42,4 +45,14 @@ export const timelineItemI18n = Object.freeze({
timeUTC: __('%{time} UTC'),
});
+export const timelineEventTagsI18n = Object.freeze({
+ startTime: __('Start time'),
+ endTime: __('End time'),
+});
+
export const MAX_TEXT_LENGTH = 280;
+
+export const TIMELINE_EVENT_TAGS = Object.values(timelineEventTagsI18n).map((item) => ({
+ text: item,
+ value: item,
+}));
diff --git a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
index 6bb72e82778..81111d42b39 100644
--- a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
@@ -74,6 +74,7 @@ export default {
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
note: eventDetails.note,
occurredAt: eventDetails.occurredAt,
+ timelineEventTagNames: eventDetails.timelineEventTags,
},
},
update: this.updateCache,
diff --git a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
index 8cdd62ca9ef..4ef9b9c5a99 100644
--- a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
@@ -40,7 +40,7 @@ export default {
:is-event-processed="editTimelineEventActive"
:previous-occurred-at="event.occurredAt"
:previous-note="event.note"
- show-delete
+ is-editing
@save-event="saveEvent"
@cancel="$emit('hide-edit')"
@delete="$emit('delete')"
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index f1a3aebc990..6648e20865d 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -1,7 +1,9 @@
<script>
-import { GlDatepicker, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
+import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlListbox } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { MAX_TEXT_LENGTH, timelineFormI18n } from './constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __, sprintf } from '~/locale';
+import { MAX_TEXT_LENGTH, TIMELINE_EVENT_TAGS, timelineFormI18n } from './constants';
import { getUtcShiftedDate } from './utils';
export default {
@@ -23,7 +25,9 @@ export default {
GlFormInput,
GlFormGroup,
GlButton,
+ GlListbox,
},
+ mixins: [glFeatureFlagsMixin()],
i18n: timelineFormI18n,
MAX_TEXT_LENGTH,
props: {
@@ -32,7 +36,7 @@ export default {
required: false,
default: false,
},
- showDelete: {
+ isEditing: {
type: Boolean,
required: false,
default: false,
@@ -51,6 +55,16 @@ export default {
required: false,
default: '',
},
+ previousTags: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ tags: {
+ type: Array,
+ required: false,
+ default: () => TIMELINE_EVENT_TAGS,
+ },
},
data() {
// if occurredAt is null, returns "now" in UTC
@@ -58,10 +72,12 @@ export default {
return {
timelineText: this.previousNote,
+ timelineTextIsDirty: this.isEditing,
placeholderDate,
hourPickerInput: placeholderDate.getHours(),
minutePickerInput: placeholderDate.getMinutes(),
datePickerInput: placeholderDate,
+ selectedTags: [...this.previousTags],
};
},
computed: {
@@ -85,6 +101,20 @@ export default {
timelineTextCount() {
return this.timelineText.length;
},
+ dropdownText() {
+ if (!this.selectedTags.length) {
+ return timelineFormI18n.selectTags;
+ }
+
+ const dropdownText =
+ this.selectedTags.length === 1
+ ? this.selectedTags[0]
+ : sprintf(__('%{numberOfSelectedTags} tags'), {
+ numberOfSelectedTags: this.selectedTags.length,
+ });
+
+ return dropdownText;
+ },
},
mounted() {
this.focusDate();
@@ -96,14 +126,35 @@ export default {
this.hourPickerInput = newPlaceholderDate.getHours();
this.minutePickerInput = newPlaceholderDate.getMinutes();
this.timelineText = '';
+ this.selectedTags = [];
},
focusDate() {
this.$refs.datepicker.$el.querySelector('input')?.focus();
},
+ setTimelineTextDirty() {
+ this.timelineTextIsDirty = true;
+ },
+ onTagsChange(tagValue) {
+ this.selectedTags = [...tagValue];
+
+ if (!this.timelineTextIsDirty) {
+ this.timelineText = this.generateTimelineTextFromTags(this.selectedTags);
+ }
+ },
+ generateTimelineTextFromTags(tags) {
+ if (!tags.length) {
+ return '';
+ }
+
+ const tagsMessage = tags.map((tag) => tag.toLocaleLowerCase()).join(', ');
+
+ return `${timelineFormI18n.areaDefaultMessage} ${tagsMessage}`;
+ },
handleSave(addAnotherEvent) {
const event = {
note: this.timelineText,
occurredAt: this.occurredAtString,
+ timelineEventTags: this.selectedTags,
};
this.$emit('save-event', event, addAnotherEvent);
},
@@ -146,6 +197,16 @@ export default {
<p class="gl-ml-3 gl-align-self-end gl-line-height-32">{{ __('UTC') }}</p>
</div>
</div>
+ <gl-form-group v-if="glFeatures.incidentEventTags" :label="$options.i18n.tagsLabel">
+ <gl-listbox
+ :selected="selectedTags"
+ :toggle-text="dropdownText"
+ :items="tags"
+ :is-check-centered="true"
+ :multiple="true"
+ @select="onTagsChange"
+ />
+ </gl-form-group>
<div class="common-note-form">
<gl-form-group class="gl-mb-3" :label="$options.i18n.areaLabel">
<markdown-field
@@ -169,6 +230,7 @@ export default {
aria-describedby="timeline-form-hint"
:placeholder="$options.i18n.areaPlaceholder"
:maxlength="$options.MAX_TEXT_LENGTH"
+ @input="setTimelineTextDirty"
>
</textarea>
<div id="timeline-form-hint" class="gl-sr-only">{{ $options.i18n.hint }}</div>
@@ -214,7 +276,7 @@ export default {
{{ $options.i18n.cancel }}
</gl-button>
<gl-button
- v-if="showDelete"
+ v-if="isEditing"
class="gl-ml-auto btn-danger"
:disabled="isEventProcessed"
@click="$emit('delete')"
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 3842a88d15b..8e4fbf24ca2 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -10,6 +10,7 @@ class Projects::IncidentsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc, @project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
+ push_frontend_feature_flag(:incident_event_tags, project)
end
feature_category :incident_management
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 3365f36c874..f8133c5836d 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -22,13 +22,11 @@ module Projects
@entity = :project
@variable_limit = ::Plan.default.actual_limits.project_ci_variables
- if Feature.enabled?(:ci_pipeline_triggers_settings_vue_ui, @project)
- triggers = ::Ci::TriggerSerializer.new.represent(
- @project.triggers, current_user: current_user, project: @project
- )
+ triggers = ::Ci::TriggerSerializer.new.represent(
+ @project.triggers, current_user: current_user, project: @project
+ )
- @triggers_json = Gitlab::Json.dump(triggers)
- end
+ @triggers_json = Gitlab::Json.dump(triggers)
render
end
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
index a7f29b5cbf9..de127d15351 100644
--- a/app/views/projects/triggers/_index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -6,32 +6,7 @@
- c.body do
= render 'projects/triggers/form', btn_text: _('Add trigger')
.gl-mb-5
- - if Feature.enabled?(:ci_pipeline_triggers_settings_vue_ui, @project)
- #js-ci-pipeline-triggers-list.triggers-list{ data: { triggers: @triggers_json } }
- - else
- - if @triggers.any?
- .table-responsive.triggers-list
- %table.table
- %thead
- %th
- %strong
- = _('Token')
- %th
- %strong
- = _('Description')
- %th
- %strong
- = _('Owner')
- %th
- %strong
- = _('Last used')
- %th
- = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- - else
- = render Pajamas::AlertComponent.new(variant: :warning, show_icon: false, dismissible: false,
- alert_options: { data: { testid: 'no_triggers_content' }}) do |c|
- = c.body do
- = _('No triggers exist yet. Use the form above to create one.')
+ #js-ci-pipeline-triggers-list.triggers-list{ data: { triggers: @triggers_json } }
- c.footer do
%p
= _("These examples show how to trigger this project's pipeline for a branch or tag.")
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
deleted file mode 100644
index bce7dc8a94b..00000000000
--- a/app/views/projects/triggers/_trigger.html.haml
+++ /dev/null
@@ -1,37 +0,0 @@
-%tr
- %td
- - if trigger.has_token_exposed?
- %span= trigger.token
- = clipboard_button(text: trigger.token, title: _("Copy trigger token"), testid: 'clipboard-btn')
- - else
- %span= trigger.short_token
-
- .gl-display-inline-block.gl-ml-3
- - unless trigger.can_access_project?
- = gl_badge_tag s_('Trigger|invalid'), { variant: :danger }, { title: s_('Trigger|Trigger user has insufficient permissions to project'), data: { toggle: 'tooltip', container: 'body' } }
-
- %td
- - if trigger.description? && trigger.description.length > 15
- %span.has-tooltip{ title: trigger.description }= truncate(trigger.description, length: 15)
- - else
- = trigger.description
-
- %td
- - if trigger.owner
- .trigger-owner.sr-only= trigger.owner.name
- = user_avatar(user: trigger.owner, size: 20)
-
- %td
- - if trigger.last_used
- = time_ago_with_tooltip trigger.last_used
- - else
- Never
-
- %td.text-right.gl-white-space-nowrap
- - revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?"
- - if can?(current_user, :admin_trigger, trigger)
- = link_to edit_project_trigger_path(@project, trigger), method: :get, title: "Edit", class: "gl-button btn btn-default btn-icon" do
- = sprite_icon('pencil')
- - if can?(current_user, :manage_trigger, trigger)
- = link_to project_trigger_path(@project, trigger), aria: { label: _('Revoke') }, data: { confirm: revoke_trigger_confirmation, testid: 'trigger_revoke_button', confirm_btn_variant: "danger" }, method: :delete, title: "Revoke", class: "gl-button btn btn-default btn-icon btn-trigger-revoke gl-ml-3" do
- = sprite_icon('remove')
diff --git a/config/feature_flags/development/ci_pipeline_triggers_settings_vue_ui.yml b/config/feature_flags/development/incident_event_tags.yml
index 8b880772d78..68101b21569 100644
--- a/config/feature_flags/development/ci_pipeline_triggers_settings_vue_ui.yml
+++ b/config/feature_flags/development/incident_event_tags.yml
@@ -1,8 +1,8 @@
---
-name: ci_pipeline_triggers_settings_vue_ui
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41864
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247486
-milestone: '13.5'
+name: incident_event_tags
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107194
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/387647
+milestone: '15.8'
type: development
-group: group::pipeline execution
+group: group::respond
default_enabled: false
diff --git a/doc/administration/operations/moving_repositories.md b/doc/administration/operations/moving_repositories.md
index 96c1fcc422d..64118082467 100644
--- a/doc/administration/operations/moving_repositories.md
+++ b/doc/administration/operations/moving_repositories.md
@@ -59,9 +59,12 @@ To move repositories:
so that the new storages receives all new projects. This stops new projects from being created
on existing storages while the migration is in progress.
1. Schedule repository moves for:
- - [Projects](#bulk-schedule-project-moves).
- - [Snippets](#bulk-schedule-snippet-moves).
- - [Groups](#bulk-schedule-group-moves). **(PREMIUM SELF)**
+ - [All projects](#bulk-schedule-project-moves) or
+ [individual projects](../../api/project_repository_storage_moves.md#schedule-a-repository-storage-move-for-a-project).
+ - [All snippets](#bulk-schedule-snippet-moves) or
+ [individual snippets](../../api/snippet_repository_storage_moves.md#schedule-a-repository-storage-move-for-a-snippet).
+ - [All groups](#bulk-schedule-group-moves) or
+ [individual groups](../../api/group_repository_storage_moves.md#schedule-a-repository-storage-move-for-a-group). **(PREMIUM SELF)**
### Bulk schedule project moves
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index f8dfc8176ef..3edf01cf216 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -17100,6 +17100,7 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="pipelinesecurityreportfindingissuelinks"></a>`issueLinks` | [`VulnerabilityIssueLinkConnection`](#vulnerabilityissuelinkconnection) | List of issue links related to the vulnerability. (see [Connections](#connections)) |
| <a id="pipelinesecurityreportfindinglinks"></a>`links` | [`[VulnerabilityLink!]`](#vulnerabilitylink) | List of links associated with the vulnerability. |
| <a id="pipelinesecurityreportfindinglocation"></a>`location` | [`VulnerabilityLocation`](#vulnerabilitylocation) | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. |
+| <a id="pipelinesecurityreportfindingmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request that fixes the vulnerability. |
| <a id="pipelinesecurityreportfindingname"></a>`name` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.1. Use `title`. |
| <a id="pipelinesecurityreportfindingproject"></a>`project` | [`Project`](#project) | Project on which the vulnerability finding was found. |
| <a id="pipelinesecurityreportfindingprojectfingerprint"></a>`projectFingerprint` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.1. The `project_fingerprint` attribute is being deprecated. Use `uuid` to identify findings. |
diff --git a/doc/api/group_repository_storage_moves.md b/doc/api/group_repository_storage_moves.md
index a4baf7936dd..ea7764c3f39 100644
--- a/doc/api/group_repository_storage_moves.md
+++ b/doc/api/group_repository_storage_moves.md
@@ -239,6 +239,8 @@ Example response:
## Schedule repository storage moves for all groups on a storage shard
Schedules repository storage moves for each group repository stored on the source storage shard.
+This endpoint migrates all groups at once. For more information, see
+[Bulk schedule group moves](../administration/operations/moving_repositories.md#bulk-schedule-group-moves).
```plaintext
POST /group_repository_storage_moves
diff --git a/doc/api/project_repository_storage_moves.md b/doc/api/project_repository_storage_moves.md
index 429cb97c404..0f094f62af2 100644
--- a/doc/api/project_repository_storage_moves.md
+++ b/doc/api/project_repository_storage_moves.md
@@ -250,6 +250,8 @@ Example response:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47142) in GitLab 13.7.
Schedules repository storage moves for each project repository stored on the source storage shard.
+This endpoint migrates all projects at once. For more information, see
+[Bulk schedule project moves](../administration/operations/moving_repositories.md#bulk-schedule-project-moves).
```plaintext
POST /project_repository_storage_moves
diff --git a/doc/api/snippet_repository_storage_moves.md b/doc/api/snippet_repository_storage_moves.md
index acf9b50704d..29061bcbfb9 100644
--- a/doc/api/snippet_repository_storage_moves.md
+++ b/doc/api/snippet_repository_storage_moves.md
@@ -266,6 +266,8 @@ Example response:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49228) in GitLab 13.8.
Schedules repository storage moves for each snippet repository stored on the source storage shard.
+This endpoint migrates all snippets at once. For more information, see
+[Bulk schedule snippet moves](../administration/operations/moving_repositories.md#bulk-schedule-snippet-moves).
```plaintext
POST /snippet_repository_storage_moves
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index a86e32b4721..c905c3e771c 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -119,6 +119,30 @@ The Swap revisions feature allows you to swap the Source and Target revisions. W
![After swap revisions](img/swap_revisions_after_v13_12.png)
+## View branches with configured rules **(FREE SELF)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88279) in GitLab 15.1 with a flag named `branch_rules`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../feature_flags.md) named `branch_rules`.
+On GitLab.com, this feature is not available.
+This feature is not ready for production use.
+
+Branches in your repository can be [protected](../../protected_branches.md) by limiting
+who can push to a branch, require approval for those pushed changes, or merge those changes.
+To help you track the protections for all branches, the **Branch rules overview**
+page shows your branches with their configured rules.
+
+To view the **Branch rules overview** list:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Settings > Repository**.
+1. Expand **Branch Rules** to view all branches with protections.
+1. Select **Details** next to your desired branch to show information about its:
+ - [Branch protections](../../protected_branches.md).
+ - [Approval rules](../../merge_requests/approvals/rules.md).
+ - [Status checks](../../merge_requests/status_checks.md).
+
## Troubleshooting
### Error: ambiguous `HEAD` branch exists
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f35fb3e14d6..71fbff57787 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -900,6 +900,9 @@ msgid_plural "%{no_of_days} days"
msgstr[0] ""
msgstr[1] ""
+msgid "%{numberOfSelectedTags} tags"
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -11212,9 +11215,6 @@ msgstr ""
msgid "Copy token"
msgstr ""
-msgid "Copy trigger token"
-msgstr ""
-
msgid "Copy value"
msgstr ""
@@ -15523,6 +15523,9 @@ msgstr ""
msgid "End Time"
msgstr ""
+msgid "End time"
+msgstr ""
+
msgid "Ends"
msgstr ""
@@ -16363,6 +16366,9 @@ msgstr ""
msgid "Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage."
msgstr ""
+msgid "Event tag (optional)"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -22105,6 +22111,9 @@ msgstr ""
msgid "Incident|Error updating incident timeline event: %{error}"
msgstr ""
+msgid "Incident|Incident"
+msgstr ""
+
msgid "Incident|Metrics"
msgstr ""
@@ -28053,9 +28062,6 @@ msgstr ""
msgid "No test coverage"
msgstr ""
-msgid "No triggers exist yet. Use the form above to create one."
-msgstr ""
-
msgid "No user provided"
msgstr ""
@@ -38500,6 +38506,9 @@ msgstr ""
msgid "Select subscription"
msgstr ""
+msgid "Select tags"
+msgstr ""
+
msgid "Select target branch"
msgstr ""
@@ -40256,6 +40265,9 @@ msgstr ""
msgid "Start thread"
msgstr ""
+msgid "Start time"
+msgstr ""
+
msgid "Start your Free Ultimate Trial"
msgstr ""
@@ -44435,12 +44447,6 @@ msgstr ""
msgid "Trigger|Trigger description"
msgstr ""
-msgid "Trigger|Trigger user has insufficient permissions to project"
-msgstr ""
-
-msgid "Trigger|invalid"
-msgstr ""
-
msgid "Trusted"
msgstr ""
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 3616fdb2e8e..23a13994fa4 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'Triggers', :js, feature_category: :continuous_integration do
wait_for_requests
end
- shared_examples 'triggers page' do
+ describe 'triggers page' do
describe 'create trigger workflow' do
it 'prevents adding new trigger with no description' do
fill_in 'trigger_description', with: ''
@@ -139,16 +139,4 @@ RSpec.describe 'Triggers', :js, feature_category: :continuous_integration do
end
end
end
-
- context 'when ci_pipeline_triggers_settings_vue_ui is enabled' do
- it_behaves_like 'triggers page'
- end
-
- context 'when ci_pipeline_triggers_settings_vue_ui is disabled' do
- before do
- stub_feature_flags(ci_pipeline_triggers_settings_vue_ui: false)
- end
-
- it_behaves_like 'triggers page'
- end
end
diff --git a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
index 1286617d64a..6c923cae0cc 100644
--- a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
@@ -1,6 +1,6 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
-import { GlDatepicker } from '@gitlab/ui';
+import { GlDatepicker, GlListboxItem } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
@@ -27,6 +27,7 @@ const mockInputData = {
incidentId: 'gid://gitlab/Issue/1',
note: 'test',
occurredAt: '2020-07-08T00:00:00.000Z',
+ timelineEventTagNames: ['Start time'],
};
describe('Create Timeline events', () => {
@@ -51,9 +52,14 @@ describe('Create Timeline events', () => {
findHourInput().setValue(inputDate.getHours());
findMinuteInput().setValue(inputDate.getMinutes());
};
+ const findListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const setEventTags = () => {
+ findListboxItems().at(0).vm.$emit('select', true);
+ };
const fillForm = () => {
setDatetime();
setNoteInput();
+ setEventTags();
};
function createMockApolloProvider() {
@@ -80,6 +86,7 @@ describe('Create Timeline events', () => {
provide: {
fullPath: 'group/project',
issuableId: '1',
+ glFeatures: { incidentEventTags: true },
},
apolloProvider,
});
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
index 9accfcea791..6606bed1567 100644
--- a/spec/frontend/issues/show/components/incidents/mock_data.js
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -74,6 +74,7 @@ const mockUpdatedEvent = {
action: 'comment',
occurredAt: '2022-07-01T12:47:00Z',
createdAt: '2022-07-20T12:47:40Z',
+ timelineEventTags: [],
};
export const timelineEventsQueryListResponse = {
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index d5b199cc790..f06d968a4c5 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -1,11 +1,15 @@
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
-import { GlDatepicker } from '@gitlab/ui';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { GlDatepicker, GlListbox } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { timelineFormI18n } from '~/issues/show/components/incidents/constants';
+import {
+ timelineFormI18n,
+ TIMELINE_EVENT_TAGS,
+ timelineEventTagsI18n,
+} from '~/issues/show/components/incidents/constants';
import { createAlert } from '~/flash';
import { useFakeDate } from 'helpers/fake_date';
@@ -17,17 +21,23 @@ const fakeDate = '2020-07-08T00:00:00.000Z';
const mockInputDate = new Date('2021-08-12');
+const mockTags = TIMELINE_EVENT_TAGS;
+
describe('Timeline events form', () => {
// July 8 2020
useFakeDate(fakeDate);
let wrapper;
- const mountComponent = ({ mountMethod = shallowMountExtended } = {}, props = {}) => {
+ const mountComponent = ({ mountMethod = mountExtended } = {}, props = {}, glFeatures = {}) => {
wrapper = mountMethod(TimelineEventsForm, {
+ provide: {
+ glFeatures,
+ },
propsData: {
showSaveAndAdd: true,
isEventProcessed: false,
...props,
+ tags: mockTags,
},
stubs: {
GlButton: true,
@@ -35,6 +45,10 @@ describe('Timeline events form', () => {
});
};
+ beforeEach(() => {
+ mountComponent();
+ });
+
afterEach(() => {
createAlert.mockReset();
wrapper.destroy();
@@ -48,16 +62,26 @@ describe('Timeline events form', () => {
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
- const setDatetime = () => {
- findDatePicker().vm.$emit('input', mockInputDate);
- findHourInput().setValue(5);
- findMinuteInput().setValue(45);
- };
+ const findTagDropdown = () => wrapper.findComponent(GlListbox);
const findTextarea = () => wrapper.findByTestId('input-note');
+ const findTextareaValue = () => findTextarea().element.value;
const findCountNumeric = (count) => wrapper.findByText(count);
const findCountVerbose = (count) => wrapper.findByText(`${count} characters remaining`);
const findCountHint = () => wrapper.findByText(timelineFormI18n.hint);
+ const setDatetime = () => {
+ findDatePicker().vm.$emit('input', mockInputDate);
+ findHourInput().setValue(5);
+ findMinuteInput().setValue(45);
+ };
+ const selectTags = async (tags) => {
+ findTagDropdown().vm.$emit(
+ 'select',
+ tags.map((x) => x.value),
+ );
+ await nextTick();
+ };
+ const selectOneTag = () => selectTags([mockTags[0]]);
const submitForm = async () => {
findSubmitButton().vm.$emit('click');
await waitForPromises();
@@ -90,23 +114,97 @@ describe('Timeline events form', () => {
]);
});
- describe('form button behaviour', () => {
+ describe('with incident_event_tag feature flag enabled', () => {
beforeEach(() => {
- mountComponent({ mountMethod: mountExtended });
+ mountComponent(
+ {},
+ {},
+ {
+ incidentEventTags: true,
+ },
+ );
+ });
+
+ describe('event tag dropdown', () => {
+ it('should render option list from provided array', () => {
+ expect(findTagDropdown().props('items')).toEqual(mockTags);
+ });
+
+ it('should allow to choose multiple tags', async () => {
+ await selectTags(mockTags);
+
+ expect(findTagDropdown().props('selected')).toEqual(mockTags.map((x) => x.value));
+ });
+
+ it('should show default option, when none is chosen', () => {
+ expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ });
+
+ it('should show the tag, when one is selected', async () => {
+ await selectOneTag();
+
+ expect(findTagDropdown().props('toggleText')).toBe(timelineEventTagsI18n.startTime);
+ });
+
+ it('should show the number of selected tags, when more than one is selected', async () => {
+ await selectTags(mockTags);
+
+ expect(findTagDropdown().props('toggleText')).toBe('2 tags');
+ });
+
+ it('should be cleared when clear is triggered', async () => {
+ await selectTags(mockTags);
+
+ // This component expects the parent to call `clear`, so this is the only way to trigger this
+ wrapper.vm.clear();
+ await nextTick();
+
+ expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ expect(findTagDropdown().props('selected')).toEqual([]);
+ });
+
+ it('should populate incident note with tags if a note was empty', async () => {
+ await selectTags(mockTags);
+
+ expect(findTextareaValue()).toBe(
+ `${timelineFormI18n.areaDefaultMessage} ${mockTags
+ .map((x) => x.value.toLowerCase())
+ .join(', ')}`,
+ );
+ });
+
+ it('should populate incident note with tag but allow to customise it', async () => {
+ await selectOneTag();
+
+ await findTextarea().setValue('my customised event note');
+
+ await nextTick();
+
+ expect(findTextareaValue()).toBe('my customised event note');
+ });
+
+ it('should not populate incident note with tag if it had a note', async () => {
+ await findTextarea().setValue('hello');
+ await selectOneTag();
+
+ expect(findTextareaValue()).toBe('hello');
+ });
});
+ });
+ describe('form button behaviour', () => {
it('should save event on submit', async () => {
await submitForm();
expect(wrapper.emitted()).toEqual({
- 'save-event': [[{ note: '', occurredAt: fakeDate }, false]],
+ 'save-event': [[{ note: '', occurredAt: fakeDate, timelineEventTags: [] }, false]],
});
});
it('should save event on "submit and add another"', async () => {
await submitFormAndAddAnother();
expect(wrapper.emitted()).toEqual({
- 'save-event': [[{ note: '', occurredAt: fakeDate }, true]],
+ 'save-event': [[{ note: '', occurredAt: fakeDate, timelineEventTags: [] }, true]],
});
});
@@ -145,10 +243,6 @@ describe('Timeline events form', () => {
});
describe('form character limit', () => {
- beforeEach(() => {
- mountComponent({ mountMethod: mountExtended });
- });
-
it('sets a character limit hint', () => {
expect(findCountHint().exists()).toBe(true);
});
@@ -172,32 +266,32 @@ describe('Timeline events form', () => {
});
describe('Delete button', () => {
- it('does not show the delete button if showDelete prop is false', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: false });
+ it('does not show the delete button if isEditing prop is false', () => {
+ mountComponent({ mountMethod: mountExtended }, { isEditing: false });
expect(findDeleteButton().exists()).toBe(false);
});
- it('shows the delete button if showDelete prop is true', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true });
+ it('shows the delete button if isEditing prop is true', () => {
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true });
expect(findDeleteButton().exists()).toBe(true);
});
it('disables the delete button if isEventProcessed prop is true', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true, isEventProcessed: true });
expect(findDeleteButton().props('disabled')).toBe(true);
});
it('does not disable the delete button if isEventProcessed prop is false', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: false });
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true, isEventProcessed: false });
expect(findDeleteButton().props('disabled')).toBe(false);
});
it('emits delete event on click', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true, isEventProcessed: true });
deleteForm();