diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-15 15:09:21 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-15 15:09:21 +0000 |
commit | e69e3f1eb695b4e852c56e7ddf8c52915ae2631b (patch) | |
tree | f0a093bc9faf84f94c75401e5c8c3754ee24ee0e | |
parent | 9215d9f7619929f9da16744fa37636635b66949b (diff) | |
download | gitlab-ce-e69e3f1eb695b4e852c56e7ddf8c52915ae2631b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
61 files changed, 1342 insertions, 353 deletions
diff --git a/app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue index 7a2956f7a6d..7b829d63330 100644 --- a/app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue +++ b/app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue @@ -91,20 +91,15 @@ export default { data-testid="dropzone-area" > <gl-icon name="upload" :size="24" :class="hasDesigns ? 'gl-mb-2' : 'gl-mr-4'" /> - <gl-sprintf - :message=" - __( - '%{contentStart}Drop files to attach, or %{contentEnd}%{linkStart}browse%{linkEnd}', - ) - " - > - <template #content="{ content }"> - <span class="gl-font-weight-bold">{{ content }} </span> - </template> - <template #link="{ content }"> - <gl-link @click.stop="openFileUpload">{{ content }}</gl-link> - </template> - </gl-sprintf> + <p class="gl-font-weight-bold gl-mb-0"> + <gl-sprintf :message="__('Drop or %{linkStart}upload%{linkEnd} Designs to attach')"> + <template #link="{ content }"> + <gl-link class="gl-font-weight-normal" @click.stop="openFileUpload"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </p> </div> </button> diff --git a/app/assets/javascripts/design_management_new/pages/index.vue b/app/assets/javascripts/design_management_new/pages/index.vue index 272f530bf0d..2a100fae280 100644 --- a/app/assets/javascripts/design_management_new/pages/index.vue +++ b/app/assets/javascripts/design_management_new/pages/index.vue @@ -267,7 +267,7 @@ export default { </script> <template> - <div data-testid="designs-root" class="gl-mt-2"> + <div data-testid="designs-root" class="gl-mt-5"> <header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex"> <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full"> <div> diff --git a/app/assets/javascripts/design_management_new/router/routes.js b/app/assets/javascripts/design_management_new/router/routes.js index 2a25a2bcadc..d888b856611 100644 --- a/app/assets/javascripts/design_management_new/router/routes.js +++ b/app/assets/javascripts/design_management_new/router/routes.js @@ -7,6 +7,7 @@ export default [ name: DESIGNS_ROUTE_NAME, path: '/', component: Home, + alias: '/designs', }, { name: DESIGN_ROUTE_NAME, @@ -16,7 +17,7 @@ export default [ { params: { id }, }, - from, + _, next, ) { if (typeof id === 'string') { diff --git a/app/assets/javascripts/jira_import/components/jira_import_app.vue b/app/assets/javascripts/jira_import/components/jira_import_app.vue index 25068d498d2..6222bd28c9d 100644 --- a/app/assets/javascripts/jira_import/components/jira_import_app.vue +++ b/app/assets/javascripts/jira_import/components/jira_import_app.vue @@ -3,6 +3,7 @@ import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { last } from 'lodash'; import { __ } from '~/locale'; import getJiraImportDetailsQuery from '../queries/get_jira_import_details.query.graphql'; +import getJiraUserMappingMutation from '../queries/get_jira_user_mapping.mutation.graphql'; import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql'; import { addInProgressImportToStore } from '../utils/cache_update'; import { isInProgress, extractJiraProjectsOptions } from '../utils/jira_import_utils'; @@ -37,6 +38,10 @@ export default { type: String, required: true, }, + projectId: { + type: String, + required: true, + }, projectPath: { type: String, required: true, @@ -48,10 +53,12 @@ export default { }, data() { return { + isSubmitting: false, jiraImportDetails: {}, + selectedProject: undefined, + userMappings: [], errorMessage: '', showAlert: false, - selectedProject: undefined, }; }, apollo: { @@ -89,16 +96,43 @@ export default { : 'jira-import::KEY-1'; }, }, + mounted() { + if (this.isJiraConfigured) { + this.$apollo + .mutate({ + mutation: getJiraUserMappingMutation, + variables: { + input: { + projectPath: this.projectPath, + startAt: 1, + }, + }, + }) + .then(({ data }) => { + if (data.jiraImportUsers.errors.length) { + this.setAlertMessage(data.jiraImportUsers.errors.join('. ')); + } else { + this.userMappings = data.jiraImportUsers.jiraUsers; + } + }) + .catch(() => this.setAlertMessage(__('There was an error retrieving the Jira users.'))); + } + }, methods: { initiateJiraImport(project) { + this.isSubmitting = true; + this.$apollo .mutate({ mutation: initiateJiraImportMutation, variables: { input: { - projectPath: this.projectPath, jiraProjectKey: project, - usersMapping: [], + projectPath: this.projectPath, + usersMapping: this.userMappings.map(({ gitlabId, jiraAccountId }) => ({ + gitlabId, + jiraAccountId, + })), }, }, update: (store, { data }) => @@ -111,7 +145,21 @@ export default { this.selectedProject = undefined; } }) - .catch(() => this.setAlertMessage(__('There was an error importing the Jira project.'))); + .catch(() => this.setAlertMessage(__('There was an error importing the Jira project.'))) + .finally(() => { + this.isSubmitting = false; + }); + }, + updateMapping(jiraAccountId, gitlabId, gitlabUsername) { + this.userMappings = this.userMappings.map(userMapping => + userMapping.jiraAccountId === jiraAccountId + ? { + ...userMapping, + gitlabId, + gitlabUsername, + } + : userMapping, + ); }, setAlertMessage(message) { this.errorMessage = message; @@ -156,9 +204,13 @@ export default { v-else v-model="selectedProject" :import-label="importLabel" + :is-submitting="isSubmitting" :issues-path="issuesPath" :jira-projects="jiraImportDetails.projects" + :project-id="projectId" + :user-mappings="userMappings" @initiateJiraImport="initiateJiraImport" + @updateMapping="updateMapping" /> </div> </template> diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue index d9ee655ea08..24bfb49a7d1 100644 --- a/app/assets/javascripts/jira_import/components/jira_import_form.vue +++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue @@ -1,22 +1,61 @@ <script> -import { GlAvatar, GlButton, GlFormGroup, GlFormSelect, GlLabel } from '@gitlab/ui'; +import { + GlButton, + GlNewDropdown, + GlNewDropdownItem, + GlNewDropdownText, + GlFormGroup, + GlFormSelect, + GlIcon, + GlLabel, + GlLoadingIcon, + GlSearchBoxByType, + GlTable, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; export default { name: 'JiraImportForm', components: { - GlAvatar, GlButton, + GlNewDropdown, + GlNewDropdownItem, + GlNewDropdownText, GlFormGroup, GlFormSelect, + GlIcon, GlLabel, + GlLoadingIcon, + GlSearchBoxByType, + GlTable, }, - currentUserAvatarUrl: gon.current_user_avatar_url, currentUsername: gon.current_username, + dropdownLabel: __('The GitLab user to which the Jira user %{jiraDisplayName} will be mapped'), + tableConfig: [ + { + key: 'jiraDisplayName', + label: __('Jira display name'), + }, + { + key: 'arrow', + label: '', + }, + { + key: 'gitlabUsername', + label: __('GitLab username'), + }, + ], props: { importLabel: { type: String, required: true, }, + isSubmitting: { + type: Boolean, + required: true, + }, issuesPath: { type: String, required: true, @@ -25,6 +64,14 @@ export default { type: Array, required: true, }, + projectId: { + type: String, + required: true, + }, + userMappings: { + type: Array, + required: true, + }, value: { type: String, required: false, @@ -33,10 +80,53 @@ export default { }, data() { return { + isFetching: false, + searchTerm: '', selectState: null, + users: [], }; }, + computed: { + shouldShowNoMatchesFoundText() { + return !this.isFetching && this.users.length === 0; + }, + }, + watch: { + searchTerm: debounce(function debouncedUserSearch() { + this.searchUsers(); + }, 500), + }, + mounted() { + this.searchUsers() + .then(data => { + this.initialUsers = data; + }) + .catch(() => {}); + }, methods: { + searchUsers() { + const params = { + active: true, + project_id: this.projectId, + search: this.searchTerm, + }; + + this.isFetching = true; + + return axios + .get('/-/autocomplete/users.json', { params }) + .then(({ data }) => { + this.users = data; + return data; + }) + .finally(() => { + this.isFetching = false; + }); + }, + resetDropdown() { + this.searchTerm = ''; + this.users = this.initialUsers; + }, initiateJiraImport(event) { event.preventDefault(); if (this.value) { @@ -80,7 +170,7 @@ export default { </gl-form-group> <gl-form-group - class="row align-items-center" + class="row gl-align-items-center gl-mb-6" :label="__('Issue label')" label-cols-sm="2" label-for="jira-project-label" @@ -94,46 +184,54 @@ export default { /> </gl-form-group> - <hr /> + <h4 class="gl-mb-4">{{ __('Jira-GitLab user mapping template') }}</h4> - <p class="offset-md-1"> + <p> {{ __( - "For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:", + `Jira users have been matched with similar GitLab users. + This can be overwritten by selecting a GitLab user from the dropdown in the "GitLab + username" column. + If it wasn't possible to match a Jira user with a GitLab user, the dropdown defaults to + the user conducting the import.`, ) }} </p> - <gl-form-group - class="row align-items-center mb-1" - :label="__('Title')" - label-cols-sm="2" - label-for="jira-project-title" - > - <p id="jira-project-title" class="mb-2">{{ __('jira.issue.summary') }}</p> - </gl-form-group> - <gl-form-group - class="row align-items-center mb-1" - :label="__('Reporter')" - label-cols-sm="2" - label-for="jira-project-reporter" - > - <gl-avatar - id="jira-project-reporter" - class="mb-2" - :src="$options.currentUserAvatarUrl" - :size="24" - :aria-label="$options.currentUsername" - /> - </gl-form-group> - <gl-form-group - class="row align-items-center mb-1" - :label="__('Description')" - label-cols-sm="2" - label-for="jira-project-description" - > - <p id="jira-project-description" class="mb-2">{{ __('jira.issue.description.content') }}</p> - </gl-form-group> + <gl-table :fields="$options.tableConfig" :items="userMappings" fixed> + <template #cell(arrow)> + <gl-icon name="arrow-right" :aria-label="__('Will be mapped to')" /> + </template> + <template #cell(gitlabUsername)="data"> + <gl-new-dropdown + :text="data.value || $options.currentUsername" + class="w-100" + :aria-label=" + sprintf($options.dropdownLabel, { jiraDisplayName: data.item.jiraDisplayName }) + " + @hide="resetDropdown" + > + <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" /> + + <div v-if="isFetching" class="gl-text-center"> + <gl-loading-icon /> + </div> + + <gl-new-dropdown-item + v-for="user in users" + v-else + :key="user.id" + @click="$emit('updateMapping', data.item.jiraAccountId, user.id, user.username)" + > + {{ user.username }} ({{ user.name }}) + </gl-new-dropdown-item> + + <gl-new-dropdown-text v-show="shouldShowNoMatchesFoundText" class="text-secondary"> + {{ __('No matches found') }} + </gl-new-dropdown-text> + </gl-new-dropdown> + </template> + </gl-table> <div class="footer-block row-content-block d-flex justify-content-between"> <gl-button @@ -141,9 +239,10 @@ export default { category="primary" variant="success" class="js-no-auto-disable" + :loading="isSubmitting" data-qa-selector="jira_issues_import_button" > - {{ __('Next') }} + {{ __('Continue') }} </gl-button> <gl-button :href="issuesPath">{{ __('Cancel') }}</gl-button> </div> diff --git a/app/assets/javascripts/jira_import/index.js b/app/assets/javascripts/jira_import/index.js index 924cc7e6864..695a237bf50 100644 --- a/app/assets/javascripts/jira_import/index.js +++ b/app/assets/javascripts/jira_import/index.js @@ -28,6 +28,7 @@ export default function mountJiraImportApp() { isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured), issuesPath: el.dataset.issuesPath, jiraIntegrationPath: el.dataset.jiraIntegrationPath, + projectId: el.dataset.projectId, projectPath: el.dataset.projectPath, setupIllustration: el.dataset.setupIllustration, }, diff --git a/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql b/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql new file mode 100644 index 00000000000..1f7c52eec58 --- /dev/null +++ b/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql @@ -0,0 +1,11 @@ +mutation($input: JiraImportUsersInput!) { + jiraImportUsers(input: $input) { + jiraUsers { + jiraAccountId + jiraDisplayName + jiraEmail + gitlabId + } + errors + } +} diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue index 01a4cbd41f6..86fd6b583d3 100644 --- a/app/assets/javascripts/logs/components/environment_logs.vue +++ b/app/assets/javascripts/logs/components/environment_logs.vue @@ -202,7 +202,7 @@ export default { <log-control-buttons ref="scrollButtons" - class="flex-grow-0 pr-2 mb-2 controllers" + class="flex-grow-0 pr-2 mb-2 controllers gl-display-inline-flex" :scroll-down-button-disabled="scrollDownButtonDisabled" @refresh="refreshPodLogs()" @scrollDown="scrollDown" diff --git a/app/assets/javascripts/logs/components/log_control_buttons.vue b/app/assets/javascripts/logs/components/log_control_buttons.vue index 3f5de4c22e0..e44b5394fa1 100644 --- a/app/assets/javascripts/logs/components/log_control_buttons.vue +++ b/app/assets/javascripts/logs/components/log_control_buttons.vue @@ -1,11 +1,9 @@ <script> -import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; export default { components: { - Icon, - GlDeprecatedButton, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -51,14 +49,16 @@ export default { :title="__('Scroll to top')" aria-labelledby="scroll-to-top" > - <gl-deprecated-button + <gl-button id="scroll-to-top" - class="btn-blank js-scroll-to-top" + class="js-scroll-to-top gl-mr-2 btn-blank" :aria-label="__('Scroll to top')" :disabled="scrollUpButtonDisabled" + icon="scroll_up" + category="primary" + variant="default" @click="handleScrollUp()" - ><icon name="scroll_up" - /></gl-deprecated-button> + /> </div> <div v-if="scrollDownAvailable" @@ -68,25 +68,28 @@ export default { :title="__('Scroll to bottom')" aria-labelledby="scroll-to-bottom" > - <gl-deprecated-button + <gl-button id="scroll-to-bottom" - class="btn-blank js-scroll-to-bottom" + class="js-scroll-to-bottom gl-mr-2 btn-blank" :aria-label="__('Scroll to bottom')" :v-if="scrollDownAvailable" :disabled="scrollDownButtonDisabled" + icon="scroll_down" + category="primary" + variant="default" @click="handleScrollDown()" - ><icon name="scroll_down" - /></gl-deprecated-button> + /> </div> - <gl-deprecated-button + <gl-button id="refresh-log" v-gl-tooltip - class="ml-1 px-2 js-refresh-log" + class="js-refresh-log" :title="__('Refresh')" :aria-label="__('Refresh')" + icon="retry" + category="primary" + variant="default" @click="handleRefreshClick" - > - <icon name="retry" /> - </gl-deprecated-button> + /> </div> </template> diff --git a/app/assets/stylesheets/pages/environment_logs.scss b/app/assets/stylesheets/pages/environment_logs.scss index 81cec14062f..03993e5321d 100644 --- a/app/assets/stylesheets/pages/environment_logs.scss +++ b/app/assets/stylesheets/pages/environment_logs.scss @@ -31,10 +31,6 @@ width: 160px; } } - - .controllers { - @include build-controllers(16px, flex-end, false, 2, inline); - } } .log-lines, diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index f1d03602fc0..4ea2ec10f11 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -440,3 +440,5 @@ class JiraService < IssueTrackerService end end end + +JiraService.prepend_if_ee('EE::JiraService') diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb index 813ccddbdef..fef733a7d09 100644 --- a/app/services/audit_event_service.rb +++ b/app/services/audit_event_service.rb @@ -16,6 +16,7 @@ class AuditEventService @author = build_author(author) @entity = entity @details = details + @ip_address = (@details[:ip_address].presence || @author.current_sign_in_ip) end # Builds the @details attribute for authentication @@ -49,6 +50,8 @@ class AuditEventService private + attr_reader :ip_address + def build_author(author) case author when User diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index faceefd8114..017a4f16b4c 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -119,6 +119,8 @@ class EventCreateService event.update_columns(updated_at: time_stamp, created_at: time_stamp) end + Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: action, event_target: wiki_page_meta.class, author_id: author.id) + event end @@ -163,7 +165,13 @@ class EventCreateService .merge(action: action, target_id: record.id, target_type: record.class.name) end - Event.insert_all(attribute_sets, returning: %w[id]) + result = Event.insert_all(attribute_sets, returning: %w[id]) + + pairs.each do |record, status| + Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: status, event_target: record.class, author_id: current_user.id) + end + + result end def create_push_event(service_class, project, current_user, push_data) @@ -178,6 +186,8 @@ class EventCreateService new_event end + Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: :pushed, event_target: Project, author_id: current_user.id) + Users::LastPushEventService.new(current_user) .cache_last_push_event(event) diff --git a/app/views/projects/import/jira/show.html.haml b/app/views/projects/import/jira/show.html.haml index fe6cc6fa828..3c0664e4d5f 100644 --- a/app/views/projects/import/jira/show.html.haml +++ b/app/views/projects/import/jira/show.html.haml @@ -3,4 +3,5 @@ jira_integration_path: edit_project_service_path(@project, :jira), is_jira_configured: @project.jira_service&.active? && @project.jira_service&.valid_connection?.to_s, in_progress_illustration: image_path('illustrations/export-import.svg'), + project_id: @project.id, setup_illustration: image_path('illustrations/manual_action.svg') } } diff --git a/app/views/projects/issues/_design_management.html.haml b/app/views/projects/issues/_design_management.html.haml index bcc5305dbc4..045f032e6e7 100644 --- a/app/views/projects/issues/_design_management.html.haml +++ b/app/views/projects/issues/_design_management.html.haml @@ -1,10 +1,10 @@ - if @project.design_management_enabled? - - if Feature.enabled?(:design_management_moved, @project) + - if Feature.enabled?(:design_management_moved, @project, default_enabled: true) .js-design-management-new{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } } - else .js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } } - else - - if Feature.enabled?(:design_management_moved, @project) + - if Feature.enabled?(:design_management_moved, @project, default_enabled: true) .row.empty-state.design-dropzone-border.gl-mt-5 .text-content.center.gl-font-weight-bold - requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements') diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 753d2f1d794..68b43673d75 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -74,7 +74,7 @@ - if @issue.sentry_issue.present? #js-sentry-error-stack-trace{ data: error_details_data(@project, @issue.sentry_issue.sentry_issue_identifier) } - - if Feature.enabled?(:design_management_moved, @project) + - if Feature.enabled?(:design_management_moved, @project, default_enabled: true) = render 'projects/issues/design_management' = render_if_exists 'projects/issues/related_issues' @@ -94,7 +94,7 @@ #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } } = render 'new_branch' if show_new_branch_button? - - if Feature.enabled?(:design_management_moved, @project) + - if Feature.enabled?(:design_management_moved, @project, default_enabled: true) = render 'projects/issues/discussion' - else = render 'projects/issues/tabs' diff --git a/changelogs/unreleased/207472-create-confidential-note-api.yml b/changelogs/unreleased/207472-create-confidential-note-api.yml new file mode 100644 index 00000000000..9ea01d24be5 --- /dev/null +++ b/changelogs/unreleased/207472-create-confidential-note-api.yml @@ -0,0 +1,5 @@ +--- +title: Add confidential attribute to public API for notes creation +merge_request: 36793 +author: +type: added diff --git a/changelogs/unreleased/216145-frontend-create-jira-import-user-mapping-form.yml b/changelogs/unreleased/216145-frontend-create-jira-import-user-mapping-form.yml new file mode 100644 index 00000000000..265c5479373 --- /dev/null +++ b/changelogs/unreleased/216145-frontend-create-jira-import-user-mapping-form.yml @@ -0,0 +1,5 @@ +--- +title: Add Jira Importer user mapping form +merge_request: 33320 +author: +type: added diff --git a/changelogs/unreleased/219956-instrument-last-git-write-operation-per-user.yml b/changelogs/unreleased/219956-instrument-last-git-write-operation-per-user.yml new file mode 100644 index 00000000000..30913008201 --- /dev/null +++ b/changelogs/unreleased/219956-instrument-last-git-write-operation-per-user.yml @@ -0,0 +1,5 @@ +--- +title: Track the number of unique users who push, change wikis and change design managerment +merge_request: +author: +type: other diff --git a/changelogs/unreleased/223196-update-design-management-documentation.yml b/changelogs/unreleased/223196-update-design-management-documentation.yml new file mode 100644 index 00000000000..2362a31f1e0 --- /dev/null +++ b/changelogs/unreleased/223196-update-design-management-documentation.yml @@ -0,0 +1,5 @@ +--- +title: Make the Design Collection more visible in the Issue UI +merge_request: 36681 +author: +type: changed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index ce22f636184..7ba256b39cd 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -360,7 +360,7 @@ production: &base # storage_path: shared/terraform_state object_store: enabled: false - remote_directory: terraform_state # The bucket name + remote_directory: terraform # The bucket name connection: provider: AWS aws_access_key_id: AWS_ACCESS_KEY_ID @@ -1253,7 +1253,7 @@ test: storage_path: tmp/tests/terraform_state object_store: enabled: false - remote_directory: terraform_state + remote_directory: terraform connection: provider: AWS # Only AWS supported at the moment aws_access_key_id: AWS_ACCESS_KEY_ID diff --git a/doc/administration/terraform_state.md b/doc/administration/terraform_state.md index dabbb4f8c1c..8d3ddc4e306 100644 --- a/doc/administration/terraform_state.md +++ b/doc/administration/terraform_state.md @@ -78,7 +78,7 @@ See [the available connection settings for different providers](object_storage.m ```ruby gitlab_rails['terraform_state_enabled'] = true gitlab_rails['terraform_state_object_store_enabled'] = true - gitlab_rails['terraform_state_object_store_remote_directory'] = "terraform_state" + gitlab_rails['terraform_state_object_store_remote_directory'] = "terraform" gitlab_rails['terraform_state_object_store_connection'] = { 'provider' => 'AWS', 'region' => 'eu-central-1', @@ -110,7 +110,7 @@ See [the available connection settings for different providers](object_storage.m enabled: true object_store: enabled: true - remote_directory: "terraform_state" # The bucket name + remote_directory: "terraform" # The bucket name connection: provider: AWS # Only AWS supported at the moment aws_access_key_id: AWS_ACESS_KEY_ID diff --git a/doc/api/notes.md b/doc/api/notes.md index 74d941edec1..9a75b950f28 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -116,6 +116,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `issue_iid` (required) - The IID of an issue - `body` (required) - The content of a note. Limited to 1,000,000 characters. +- `confidential` (optional) - The confidential flag of a note. Default is false. - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) ```shell diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md index ad2d4ca07f2..6ecd0f3c578 100644 --- a/doc/development/telemetry/usage_ping.md +++ b/doc/development/telemetry/usage_ping.md @@ -525,6 +525,7 @@ appear to be associated to any of the services running, since they all appear to | `projects_jira_cloud_active` | `counts` | | | | | | `projects_jira_dvcs_cloud_active` | `counts` | | | | | | `projects_jira_dvcs_server_active` | `counts` | | | | | +| `projects_jira_issuelist_active` | `counts` | `create` | | EE | Total Jira Issue feature enabled | | `labels` | `counts` | | | | | | `merge_requests` | `counts` | | | | | | `merge_requests_users` | `counts` | | | | | @@ -658,6 +659,9 @@ appear to be associated to any of the services running, since they all appear to | `remote_mirrors` | `usage_activity_by_stage` | `create` | | CE+EE | | | `snippets` | `usage_activity_by_stage` | `create` | | CE+EE | | | `merge_requests_users` | `usage_activity_by_stage_monthly` | `create` | | CE+EE | Unique count of users who used a merge request | +| `action_monthly_active_users_project_repo` | `usage_activity_by_stage_monthly` | `create` | | CE+EE | Unique count of users who pushed to a project repo | +| `action_monthly_active_users_design_management` | `usage_activity_by_stage_monthly` | `create` | | CE+EE | Unique count of users who interacted with the design system management | +| `action_monthly_active_users_wiki_repo` | `usage_activity_by_stage_monthly` | `create` | | CE+EE | Unique count of users who created or updated a wiki repo | | `projects_enforcing_code_owner_approval` | `usage_activity_by_stage` | `create` | | EE | | | `merge_requests_with_optional_codeowners` | `usage_activity_by_stage` | `create` | | EE | | | `merge_requests_with_required_codeowners` | `usage_activity_by_stage` | `create` | | EE | | diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md index c66a0625110..093c2d94217 100644 --- a/doc/policy/maintenance.md +++ b/doc/policy/maintenance.md @@ -5,12 +5,16 @@ type: concepts # GitLab release and maintenance policy GitLab has strict policies governing version naming, as well as release pace for major, minor, -patch, and security releases. New releases are usually announced on the [GitLab blog](https://about.gitlab.com/releases/categories/releases/). +patch, and security releases. New releases are announced on the [GitLab blog](https://about.gitlab.com/releases/categories/releases/). Our current policy is: - Backporting bug fixes for **only the current stable release** at any given time. (See [patch releases](#patch-releases).) -- Backporting **to the previous two monthly releases in addition to the current stable release**. (See [security releases](#security-releases).) +- Backporting security fixes **to the previous two monthly releases in addition to the current stable release**. (See [security releases](#security-releases).) + +In rare cases, release managers may make an exception and backport to more than +the last two monthly releases. See [Backporting to older +releases](#backporting-to-older-releases) for more information. ## Versioning @@ -177,7 +181,7 @@ accessible. ### Backporting to older releases -Backporting to more than one stable release is reserved for [security releases](#security-releases). +Backporting to more than one stable release is normally reserved for [security releases](#security-releases). In some cases, however, we may need to backport *a bug fix* to more than one stable release, depending on the severity of the bug. @@ -188,16 +192,13 @@ based on *all* of the following: 1. Estimated [severity](../development/contributing/issue_workflow.md#severity-labels) of the bug: Highest possible impact to users based on the current definition of severity. - 1. Estimated [priority](../development/contributing/issue_workflow.md#priority-labels) of the bug: Immediate impact on all impacted users based on the above estimated severity. - 1. Potentially incurring data loss and/or security breach. - 1. Potentially affecting one or more strategic accounts due to a proven inability by the user to upgrade to the current stable version. If *all* of the above are satisfied, the backport releases can be created for -the current stable release, and two previous monthly releases. +the current stable release, and two previous monthly releases. In rare cases a release manager may grant an exception to backport to more than two previous monthly releases. For instance, if we release `11.2.1` with a fix for a severe bug introduced in `11.0.0`, we could backport the fix to a new `11.0.x`, and `11.1.x` patch release. diff --git a/doc/user/admin_area/merge_requests_approvals.md b/doc/user/admin_area/merge_requests_approvals.md index ac5ac98c54d..6d9d634ce14 100644 --- a/doc/user/admin_area/merge_requests_approvals.md +++ b/doc/user/admin_area/merge_requests_approvals.md @@ -37,12 +37,12 @@ Merge request approval rules that can be set at an instance level are: ## Scope rules to compliance-labeled projects -> Introduced in [GitLab Premium](https://gitlab.com/groups/gitlab-org/-/epics/3432) 13.1. +> Introduced in [GitLab Premium](https://gitlab.com/groups/gitlab-org/-/epics/3432) 13.2. Merge request approval rules can be further scoped to specific compliance frameworks. When the compliance framework label is selected and the project is assigned the compliance -label, the instance-level MR approval settings will take effect and +label, the instance-level MR approval settings will take effect and the [project-level settings](../project/merge_requests/merge_request_approvals.md#adding--editing-a-default-approval-rule) is locked for modification. @@ -53,18 +53,3 @@ Maintainer role and above can modify these. | Instance-level | Project-level | | -------------- | ------------- | | ![Scope MR approval settings to compliance frameworks](img/scope_mr_approval_settings_v13_1.png) | ![MR approval settings on compliance projects](img/mr_approval_settings_compliance_project_v13_1.png) | - -### Enabling the feature - -This feature comes with two feature flags which are disabled by default. - -- The configuration in Admin area is controlled via `admin_compliance_merge_request_approval_settings`. -- The application of these rules is controlled via `project_compliance_merge_request_approval_settings`. - -These feature flags can be managed by feature flag [API endpoint](../../api/features.md#set-or-create-a-feature) or -by [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) with the following commands: - -```ruby -Feature.enable(:admin_compliance_merge_request_approval_settings) -Feature.enable(:project_compliance_merge_request_approval_settings) -``` diff --git a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png Binary files differindex 44fa8dc0a58..591a08f4d7a 100644 --- a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png +++ b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index 1887bf653a8..fb287fb2bf6 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -510,6 +510,29 @@ license_scanning: GOFLAGS: '-insecure' ``` +#### Using private NuGet registries + +If you have a private NuGet registry you can add it as a source +by adding it to the [`packageSources`](https://docs.microsoft.com/en-us/nuget/reference/nuget-config-file#package-source-sections) +section of a [`nuget.config`](https://docs.microsoft.com/en-us/nuget/reference/nuget-config-file) file. + +For example: + +```xml +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <packageSources> + <clear /> + <add key="custom" value="https://nuget.example.com/v3/index.json" /> + </packageSources> +</configuration> +``` + +#### Custom root certificates for NuGet + +You can supply a custom root certificate to complete TLS verification by using the +`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables). + ### Migration from `license_management` to `license_scanning` In GitLab 12.8 a new name for `license_management` job was introduced. This change was made to improve clarity around the purpose of the scan, which is to scan and collect the types of licenses present in a projects dependencies. diff --git a/doc/user/project/import/img/jira/import_issues_from_jira_button_v12_10.png b/doc/user/project/import/img/jira/import_issues_from_jira_button_v12_10.png Binary files differindex 4ab42485d0b..3c1dc44df93 100644 --- a/doc/user/project/import/img/jira/import_issues_from_jira_button_v12_10.png +++ b/doc/user/project/import/img/jira/import_issues_from_jira_button_v12_10.png diff --git a/doc/user/project/import/img/jira/import_issues_from_jira_form_v12_10.png b/doc/user/project/import/img/jira/import_issues_from_jira_form_v12_10.png Binary files differindex 6278cb5f970..d98ad2aaa6e 100644 --- a/doc/user/project/import/img/jira/import_issues_from_jira_form_v12_10.png +++ b/doc/user/project/import/img/jira/import_issues_from_jira_form_v12_10.png diff --git a/doc/user/project/import/img/jira/import_issues_from_jira_form_v13_2.png b/doc/user/project/import/img/jira/import_issues_from_jira_form_v13_2.png Binary files differnew file mode 100644 index 00000000000..9cbffe2bb36 --- /dev/null +++ b/doc/user/project/import/img/jira/import_issues_from_jira_form_v13_2.png diff --git a/doc/user/project/import/img/jira/import_issues_from_jira_projects_v12_10.png b/doc/user/project/import/img/jira/import_issues_from_jira_projects_v12_10.png Binary files differdeleted file mode 100644 index bf9728e0311..00000000000 --- a/doc/user/project/import/img/jira/import_issues_from_jira_projects_v12_10.png +++ /dev/null diff --git a/doc/user/project/import/jira.md b/doc/user/project/import/jira.md index 0b8807bb9b3..395cca4726d 100644 --- a/doc/user/project/import/jira.md +++ b/doc/user/project/import/jira.md @@ -40,6 +40,8 @@ Make sure you have the integration set up before trying to import Jira issues. ## Import Jira issues to GitLab +> New import form [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216145) in GitLab 13.2. + To import Jira issues to a GitLab project, follow the steps below. NOTE: **Note:** @@ -47,27 +49,34 @@ Importing Jira issues is done as an asynchronous background job, which may result in delays based on import queues load, system load, or other factors. Importing large projects may take several minutes depending on the size of the import. -1. On the **{issues}** **Issues** page, click the **Import Issues** (**{import}**) button. -1. Select **Import from Jira**. - This option is only visible if you have the [correct permissions](#permissions). +1. On the **{issues}** **Issues** page, click **Import Issues** (**{import}**) **> Import from Jira**. ![Import issues from Jira button](img/jira/import_issues_from_jira_button_v12_10.png) + The **Import from Jira** option is only visible if you have the [correct permissions](#permissions). + The following form appears. + If you've previously set up the [Jira integration](../integrations/jira.md), you can now see + the Jira projects that you have access to in the dropdown. + + ![Import issues from Jira form](img/jira/import_issues_from_jira_form_v13_2.png) - ![Import issues from Jira form](img/jira/import_issues_from_jira_form_v12_10.png) +1. Click the **Import from** dropdown and select the Jira project that you wish to import issues from. - If you've previously set up the [Jira integration](../integrations/jira.md), you now see the Jira - projects that you have access to in the dropdown. + In the **Jira-GitLab user mapping template** section, the table shows to which GitLab users your Jira + users will be mapped. + If it wasn't possible to match a Jira user with a GitLab user, the dropdown defaults to the user + conducting the import. -1. Select the Jira project that you wish to import issues from. +1. To change any of the suggested mappings, click the dropdown in the **GitLab username** column and + select the user you want to map to each Jira user. - ![Import issues from Jira form](img/jira/import_issues_from_jira_projects_v12_10.png) + The dropdown may not show all the users, so use the search bar to find a specific + user in this GitLab project. + +1. Click **Continue**. You're presented with a confirmation that import has started. -1. Click **Import Issues**. You're presented with a confirmation that import has started. While the import is running in the background, you can navigate away from the import status page to the issues page, and you'll see the new issues appearing in the issues list. -1. To check the status of your import, go back to the Jira import page. - - ![Import issues from Jira button](img/jira/import_issues_from_jira_button_v12_10.png) +1. To check the status of your import, go to the Jira import page again. diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md index 8f090593984..f1c3ef3fa5c 100644 --- a/doc/user/project/issues/design_management.md +++ b/doc/user/project/issues/design_management.md @@ -60,20 +60,25 @@ and [PDFs](https://gitlab.com/gitlab-org/gitlab/-/issues/32811) is planned for a - Only the latest version of the designs can be deleted. - Deleted designs cannot be recovered but you can see them on previous designs versions. -## The Design Management page +## The Design Management section -Navigate to the **Design Management** page from any issue by clicking the **Designs** tab: +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223193) in GitLab 13.2, Designs are displayed directly on the issue description rather than on a separate tab. -![Designs tab](img/design_management_v12_3.png) +You can find to the **Design Management** section in the issue description: + +![Designs section](img/design_management_v13_2.png) ## Adding designs -To upload design images, click the **Upload Designs** button and select images to upload. +To upload Design images, drag files from your computer and drop them in the Design Management section, +or click **upload** to select images from your file browser: + +![Designs empty state](img/design_management_upload_v13.2.png) [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34353) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9, you can drag and drop designs onto the dedicated drop zone to upload them. -![Drag and drop design uploads](img/design_drag_and_drop_uploads_v12_9.png) +![Drag and drop design uploads](img/design_drag_and_drop_uploads_v13_2.png) [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202634) in GitLab 12.10, you can also copy images from your file system and diff --git a/doc/user/project/issues/img/design_drag_and_drop_uploads_v13_2.png b/doc/user/project/issues/img/design_drag_and_drop_uploads_v13_2.png Binary files differnew file mode 100644 index 00000000000..d60f1234b6d --- /dev/null +++ b/doc/user/project/issues/img/design_drag_and_drop_uploads_v13_2.png diff --git a/doc/user/project/issues/img/design_management_upload_v13.2.png b/doc/user/project/issues/img/design_management_upload_v13.2.png Binary files differnew file mode 100644 index 00000000000..1d4b10307fc --- /dev/null +++ b/doc/user/project/issues/img/design_management_upload_v13.2.png diff --git a/doc/user/project/issues/img/design_management_v13_2.png b/doc/user/project/issues/img/design_management_v13_2.png Binary files differnew file mode 100644 index 00000000000..0a6e2be17ab --- /dev/null +++ b/doc/user/project/issues/img/design_management_v13_2.png diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 4fb7bffb3d5..bfd09dcd496 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -68,6 +68,7 @@ module API params do requires :noteable_id, type: Integer, desc: 'The ID of the noteable' requires :body, type: String, desc: 'The content of a note' + optional :confidential, type: Boolean, desc: 'Confidentiality note flag, default is false' optional :created_at, type: String, desc: 'The creation date of the note' end post ":id/#{noteables_str}/:noteable_id/notes" do @@ -77,6 +78,7 @@ module API note: params[:body], noteable_type: noteables_str.classify, noteable_id: noteable.id, + confidential: params[:confidential], created_at: params[:created_at] } diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 5bf7a4e06e3..60e5986e0dc 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -490,7 +490,10 @@ module Gitlab remote_mirrors: distinct_count(::Project.with_remote_mirrors.where(time_period), :creator_id), snippets: distinct_count(::Snippet.where(time_period), :author_id) }.tap do |h| - h[:merge_requests_users] = merge_requests_users(time_period) if time_period.present? + if time_period.present? + h[:merge_requests_users] = merge_requests_users(time_period) + h.merge!(action_monthly_active_users(time_period)) + end end end # rubocop: enable CodeReuse/ActiveRecord @@ -582,6 +585,42 @@ module Gitlab { analytics_unique_visits: results } end + def action_monthly_active_users(time_period) + return {} unless Feature.enabled?(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG) + + counter = Gitlab::UsageDataCounters::TrackUniqueActions + + project_count = redis_usage_data do + counter.count_unique_events( + event_action: Gitlab::UsageDataCounters::TrackUniqueActions::PUSH_ACTION, + date_from: time_period[:created_at].first, + date_to: time_period[:created_at].last + ) + end + + design_count = redis_usage_data do + counter.count_unique_events( + event_action: Gitlab::UsageDataCounters::TrackUniqueActions::DESIGN_ACTION, + date_from: time_period[:created_at].first, + date_to: time_period[:created_at].last + ) + end + + wiki_count = redis_usage_data do + counter.count_unique_events( + event_action: Gitlab::UsageDataCounters::TrackUniqueActions::WIKI_ACTION, + date_from: time_period[:created_at].first, + date_to: time_period[:created_at].last + ) + end + + { + action_monthly_active_users_project_repo: project_count, + action_monthly_active_users_design_management: design_count, + action_monthly_active_users_wiki_repo: wiki_count + } + end + private def unique_visit_service diff --git a/lib/gitlab/usage_data_counters/track_unique_actions.rb b/lib/gitlab/usage_data_counters/track_unique_actions.rb new file mode 100644 index 00000000000..9fb5a29748e --- /dev/null +++ b/lib/gitlab/usage_data_counters/track_unique_actions.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module Gitlab + module UsageDataCounters + module TrackUniqueActions + KEY_EXPIRY_LENGTH = 29.days + FEATURE_FLAG = :track_unique_actions + + WIKI_ACTION = :wiki_action + DESIGN_ACTION = :design_action + PUSH_ACTION = :project_action + + ACTION_TRANSFORMATIONS = HashWithIndifferentAccess.new({ + wiki: { + created: WIKI_ACTION, + updated: WIKI_ACTION, + destroyed: WIKI_ACTION + }, + design: { + created: DESIGN_ACTION, + updated: DESIGN_ACTION, + destroyed: DESIGN_ACTION + }, + project: { + pushed: PUSH_ACTION + } + }).freeze + + class << self + def track_action(event_action:, event_target:, author_id:, time: Time.zone.now) + return unless Gitlab::CurrentSettings.usage_ping_enabled + return unless Feature.enabled?(FEATURE_FLAG) + return unless valid_target?(event_target) + return unless valid_action?(event_action) + + transformed_target = transform_target(event_target) + transformed_action = transform_action(event_action, transformed_target) + + add_event(transformed_action, author_id, time) + end + + def count_unique_events(event_action:, date_from:, date_to:) + keys = (date_from.to_date..date_to.to_date).map { |date| key(event_action, date) } + + Gitlab::Redis::SharedState.with do |redis| + redis.pfcount(*keys) + end + end + + private + + def transform_action(event_action, event_target) + ACTION_TRANSFORMATIONS.dig(event_target, event_action) || event_action + end + + def transform_target(event_target) + Event::TARGET_TYPES.key(event_target) + end + + def valid_target?(target) + Event::TARGET_TYPES.value?(target) + end + + def valid_action?(action) + Event.actions.key?(action) + end + + def key(event_action, date) + year_day = date.strftime('%G-%j') + "#{year_day}-{#{event_action}}" + end + + def add_event(event_action, author_id, date) + target_key = key(event_action, date) + + Gitlab::Redis::SharedState.with do |redis| + redis.multi do |multi| + multi.pfadd(target_key, author_id) + multi.expire(target_key, KEY_EXPIRY_LENGTH) + end + end + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1af227968e5..1f149873dbb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -304,9 +304,6 @@ msgstr "" msgid "%{containerScanningLinkStart}Container Scanning%{containerScanningLinkEnd} and/or %{dependencyScanningLinkStart}Dependency Scanning%{dependencyScanningLinkEnd} must be enabled. %{securityBotLinkStart}GitLab-Security-Bot%{securityBotLinkEnd} will be the author of the auto-created merge request. %{moreInfoLinkStart}More information%{moreInfoLinkEnd}." msgstr "" -msgid "%{contentStart}Drop files to attach, or %{contentEnd}%{linkStart}browse%{linkEnd}" -msgstr "" - msgid "%{cores} cores" msgstr "" @@ -8409,6 +8406,9 @@ msgstr "" msgid "Downvotes" msgstr "" +msgid "Drop or %{linkStart}upload%{linkEnd} Designs to attach" +msgstr "" + msgid "Drop your designs to start your upload." msgstr "" @@ -10410,9 +10410,6 @@ msgstr "" msgid "Footer message" msgstr "" -msgid "For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:" -msgstr "" - msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)" msgstr "" @@ -11082,6 +11079,9 @@ msgstr "" msgid "GitLab single sign-on URL" msgstr "" +msgid "GitLab username" +msgstr "" + msgid "GitLab uses %{jaeger_link} to monitor distributed systems." msgstr "" @@ -13065,6 +13065,9 @@ msgstr "" msgid "Jira Issues" msgstr "" +msgid "Jira display name" +msgstr "" + msgid "Jira import is already running." msgstr "" @@ -13080,6 +13083,12 @@ msgstr "" msgid "Jira service not configured." msgstr "" +msgid "Jira users have been matched with similar GitLab users. This can be overwritten by selecting a GitLab user from the dropdown in the \"GitLab username\" column. If it wasn't possible to match a Jira user with a GitLab user, the dropdown defaults to the user conducting the import." +msgstr "" + +msgid "Jira-GitLab user mapping template" +msgstr "" + msgid "JiraService| on branch %{branch_link}" msgstr "" @@ -19630,9 +19639,6 @@ msgstr "" msgid "Reported %{timeAgo} by %{reportedBy}" msgstr "" -msgid "Reporter" -msgstr "" - msgid "Reporting" msgstr "" @@ -23138,6 +23144,9 @@ msgstr "" msgid "The Git LFS objects will <strong>not</strong> be synced." msgstr "" +msgid "The GitLab user to which the Jira user %{jiraDisplayName} will be mapped" +msgstr "" + msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project" msgstr "" @@ -23671,6 +23680,9 @@ msgstr "" msgid "There was an error resetting user pipeline minutes." msgstr "" +msgid "There was an error retrieving the Jira users." +msgstr "" + msgid "There was an error saving this Geo Node." msgstr "" @@ -26594,6 +26606,9 @@ msgstr "" msgid "Will be created" msgstr "" +msgid "Will be mapped to" +msgstr "" + msgid "Will deploy to" msgstr "" @@ -27959,12 +27974,6 @@ msgstr "" msgid "jigsaw is not defined" msgstr "" -msgid "jira.issue.description.content" -msgstr "" - -msgid "jira.issue.summary" -msgstr "" - msgid "latest" msgstr "" diff --git a/qa/Dockerfile b/qa/Dockerfile index 7f90e4bf5bf..1a2e6d3edcd 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -52,7 +52,7 @@ RUN rm -f chromedriver_linux64.zip # Install K3d local cluster support # https://github.com/rancher/k3d # -RUN curl -s https://raw.githubusercontent.com/rancher/k3d/master/install.sh | TAG="v${K3D_VERSION}" bash +RUN curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | TAG="v${K3D_VERSION}" bash ## # Install gcloud and kubectl CLI used in Auto DevOps test to create K8s diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 1c9e122b07b..9a521336fee 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -77,13 +77,16 @@ FactoryBot.define do username { 'jira_username' } password { 'jira_password' } jira_issue_transition_id { '56-1' } + issues_enabled { false } + project_key { nil } end after(:build) do |service, evaluator| if evaluator.create_data create(:jira_tracker_data, service: service, url: evaluator.url, api_url: evaluator.api_url, jira_issue_transition_id: evaluator.jira_issue_transition_id, - username: evaluator.username, password: evaluator.password + username: evaluator.username, password: evaluator.password, issues_enabled: evaluator.issues_enabled, + project_key: evaluator.project_key ) end end diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb index dfe78aa7ebc..20a271f9c0e 100644 --- a/spec/features/security/project/snippet/public_access_spec.rb +++ b/spec/features/security/project/snippet/public_access_spec.rb @@ -5,11 +5,10 @@ require 'spec_helper' RSpec.describe "Public Project Snippets Access" do include AccessMatchers - let(:project) { create(:project, :public) } - - let(:public_snippet) { create(:project_snippet, :public, project: project, author: project.owner) } - let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) } - let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:public_snippet) { create(:project_snippet, :public, project: project, author: project.owner) } + let_it_be(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) } + let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) } describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/frontend/design_management_new/components/upload/__snapshots__/design_dropzone_spec.js.snap b/spec/frontend/design_management_new/components/upload/__snapshots__/design_dropzone_spec.js.snap index 9631aa33ee2..c53c6c889b0 100644 --- a/spec/frontend/design_management_new/components/upload/__snapshots__/design_dropzone_spec.js.snap +++ b/spec/frontend/design_management_new/components/upload/__snapshots__/design_dropzone_spec.js.snap @@ -17,9 +17,13 @@ exports[`Design management dropzone component when dragging renders correct temp size="24" /> - <gl-sprintf-stub - message="%{contentStart}Drop files to attach, or %{contentEnd}%{linkStart}browse%{linkEnd}" - /> + <p + class="gl-font-weight-bold gl-mb-0" + > + <gl-sprintf-stub + message="Drop or %{linkStart}upload%{linkEnd} Designs to attach" + /> + </p> </div> </button> @@ -89,9 +93,13 @@ exports[`Design management dropzone component when dragging renders correct temp size="24" /> - <gl-sprintf-stub - message="%{contentStart}Drop files to attach, or %{contentEnd}%{linkStart}browse%{linkEnd}" - /> + <p + class="gl-font-weight-bold gl-mb-0" + > + <gl-sprintf-stub + message="Drop or %{linkStart}upload%{linkEnd} Designs to attach" + /> + </p> </div> </button> @@ -161,9 +169,13 @@ exports[`Design management dropzone component when dragging renders correct temp size="24" /> - <gl-sprintf-stub - message="%{contentStart}Drop files to attach, or %{contentEnd}%{linkStart}browse%{linkEnd}" - /> + <p + class="gl-font-weight-bold gl-mb-0" + > + <gl-sprintf-stub + message="Drop or %{linkStart}upload%{linkEnd} Designs to attach" + /> + </p> </div> </button> @@ -232,9 +244,13 @@ exports[`Design management dropzone component when dragging renders correct temp size="24" /> - <gl-sprintf-stub - message="%{contentStart}Drop files to attach, or %{contentEnd}%{linkStart}browse%{linkEnd}" - /> + <p + class="gl-font-weight-bold gl-mb-0" + > + <gl-sprintf-stub + message="Drop or %{linkStart}upload%{linkEnd} Designs to attach" + /> + </p> </div> </button> @@ -303,9 +319,13 @@ exports[`Design management dropzone component when dragging renders correct temp size="24" /> - <gl-sprintf-stub - message="%{contentStart}Drop files to attach, or %{contentEnd}%{linkStart}browse%{linkEnd}" - /> + <p + class="gl-font-weight-bold gl-mb-0" + > + <gl-sprintf-stub + message="Drop or %{linkStart}upload%{linkEnd} Designs to attach" + /> + </p> </div> </button> @@ -374,9 +394,13 @@ exports[`Design management dropzone component when no slot provided renders defa size="24" /> - <gl-sprintf-stub - message="%{contentStart}Drop files to attach, or %{contentEnd}%{linkStart}browse%{linkEnd}" - /> + <p + class="gl-font-weight-bold gl-mb-0" + > + <gl-sprintf-stub + message="Drop or %{linkStart}upload%{linkEnd} Designs to attach" + /> + </p> </div> </button> diff --git a/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap index 99cd39dd62c..3d1fe143ac3 100644 --- a/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap @@ -2,7 +2,7 @@ exports[`Design management index page designs does not render toolbar when there is no permission 1`] = ` <div - class="gl-mt-2" + class="gl-mt-5" data-testid="designs-root" > <!----> @@ -87,7 +87,7 @@ exports[`Design management index page designs does not render toolbar when there exports[`Design management index page designs renders designs list and header with upload button 1`] = ` <div - class="gl-mt-2" + class="gl-mt-5" data-testid="designs-root" > <header @@ -227,7 +227,7 @@ exports[`Design management index page designs renders designs list and header wi exports[`Design management index page designs renders error 1`] = ` <div - class="gl-mt-2" + class="gl-mt-5" data-testid="designs-root" > <!----> @@ -258,7 +258,7 @@ exports[`Design management index page designs renders error 1`] = ` exports[`Design management index page designs renders loading icon 1`] = ` <div - class="gl-mt-2" + class="gl-mt-5" data-testid="designs-root" > <!----> @@ -281,7 +281,7 @@ exports[`Design management index page designs renders loading icon 1`] = ` exports[`Design management index page when has no designs renders design dropzone 1`] = ` <div - class="gl-mt-2" + class="gl-mt-5" data-testid="designs-root" > <!----> diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap new file mode 100644 index 00000000000..edd3ca1ced2 --- /dev/null +++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap @@ -0,0 +1,299 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JiraImportForm table body shows correct information in each cell 1`] = ` +<table + aria-busy="false" + aria-colcount="3" + class="table b-table gl-table b-table-fixed" + role="table" +> + <!----> + <!----> + <thead + class="" + role="rowgroup" + > + <!----> + <tr + class="" + role="row" + > + <th + aria-colindex="1" + class="" + role="columnheader" + scope="col" + > + Jira display name + </th> + <th + aria-colindex="2" + aria-label="Arrow" + class="" + role="columnheader" + scope="col" + /> + <th + aria-colindex="3" + class="" + role="columnheader" + scope="col" + > + GitLab username + </th> + </tr> + </thead> + <tbody + role="rowgroup" + > + <!----> + <tr + class="" + role="row" + > + <td + aria-colindex="1" + class="" + role="cell" + > + Jane Doe + </td> + <td + aria-colindex="2" + class="" + role="cell" + > + <svg + aria-label="Will be mapped to" + class="gl-icon s16" + > + <use + href="#arrow-right" + /> + </svg> + </td> + <td + aria-colindex="3" + class="" + role="cell" + > + <div + aria-label="The GitLab user to which the Jira user Jane Doe will be mapped" + class="dropdown b-dropdown gl-new-dropdown w-100 btn-group" + > + <!----> + <button + aria-expanded="false" + aria-haspopup="true" + class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" + type="button" + > + <!----> + + <span + class="gl-new-dropdown-button-text" + > + janedoe + </span> + + <svg + class="dropdown-chevron gl-icon s16" + > + <use + href="#chevron-down" + /> + </svg> + </button> + <ul + class="dropdown-menu" + role="menu" + tabindex="-1" + > + <!----> + + <div + class="gl-search-box-by-type m-2" + > + <svg + class="gl-search-box-by-type-search-icon gl-icon s16" + > + <use + href="#search" + /> + </svg> + + <input + aria-label="Search" + class="gl-form-input gl-search-box-by-type-input form-control" + placeholder="Search" + type="text" + /> + + <div + class="gl-search-box-by-type-right-icons" + > + <!----> + + <button + aria-hidden="true" + class="gl-clear-icon-button gl-search-box-by-type-clear gl-clear-icon-button" + name="clear" + style="display: none;" + title="Clear" + > + <svg + class="gl-icon s16" + > + <use + href="#clear" + /> + </svg> + </button> + </div> + </div> + + <li + class="gl-new-dropdown-text text-secondary" + role="presentation" + > + <p + class="b-dropdown-text" + > + + No matches found + + </p> + </li> + + </ul> + </div> + </td> + </tr> + <tr + class="" + role="row" + > + <td + aria-colindex="1" + class="" + role="cell" + > + Fred Chopin + </td> + <td + aria-colindex="2" + class="" + role="cell" + > + <svg + aria-label="Will be mapped to" + class="gl-icon s16" + > + <use + href="#arrow-right" + /> + </svg> + </td> + <td + aria-colindex="3" + class="" + role="cell" + > + <div + aria-label="The GitLab user to which the Jira user Fred Chopin will be mapped" + class="dropdown b-dropdown gl-new-dropdown w-100 btn-group" + > + <!----> + <button + aria-expanded="false" + aria-haspopup="true" + class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" + type="button" + > + <!----> + + <span + class="gl-new-dropdown-button-text" + > + mrgitlab + </span> + + <svg + class="dropdown-chevron gl-icon s16" + > + <use + href="#chevron-down" + /> + </svg> + </button> + <ul + class="dropdown-menu" + role="menu" + tabindex="-1" + > + <!----> + + <div + class="gl-search-box-by-type m-2" + > + <svg + class="gl-search-box-by-type-search-icon gl-icon s16" + > + <use + href="#search" + /> + </svg> + + <input + aria-label="Search" + class="gl-form-input gl-search-box-by-type-input form-control" + placeholder="Search" + type="text" + /> + + <div + class="gl-search-box-by-type-right-icons" + > + <!----> + + <button + aria-hidden="true" + class="gl-clear-icon-button gl-search-box-by-type-clear gl-clear-icon-button" + name="clear" + style="display: none;" + title="Clear" + > + <svg + class="gl-icon s16" + > + <use + href="#clear" + /> + </svg> + </button> + </div> + </div> + + <li + class="gl-new-dropdown-text text-secondary" + role="presentation" + > + <p + class="b-dropdown-text" + > + + No matches found + + </p> + </li> + + </ul> + </div> + </td> + </tr> + <!----> + <!----> + </tbody> + <!----> +</table> +`; diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js index 54c137788aa..074f9842512 100644 --- a/spec/frontend/jira_import/components/jira_import_app_spec.js +++ b/spec/frontend/jira_import/components/jira_import_app_spec.js @@ -1,88 +1,19 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; import Vue from 'vue'; +import axios from '~/lib/utils/axios_utils'; import JiraImportApp from '~/jira_import/components/jira_import_app.vue'; import JiraImportForm from '~/jira_import/components/jira_import_form.vue'; import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue'; import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue'; import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql'; - -const mountComponent = ({ - isJiraConfigured = true, - errorMessage = '', - selectedProject = 'MTG', - showAlert = false, - isInProgress = false, - loading = false, - mutate = jest.fn(() => Promise.resolve()), - mountType, -} = {}) => { - const mountFunction = mountType === 'mount' ? mount : shallowMount; - - return mountFunction(JiraImportApp, { - propsData: { - inProgressIllustration: 'in-progress-illustration.svg', - isJiraConfigured, - issuesPath: 'gitlab-org/gitlab-test/-/issues', - jiraIntegrationPath: 'gitlab-org/gitlab-test/-/services/jira/edit', - projectPath: 'gitlab-org/gitlab-test', - setupIllustration: 'setup-illustration.svg', - }, - data() { - return { - errorMessage, - showAlert, - selectedProject, - jiraImportDetails: { - isInProgress, - imports: [ - { - jiraProjectKey: 'MTG', - scheduledAt: '2020-04-08T10:11:12+00:00', - scheduledBy: { - name: 'John Doe', - }, - }, - { - jiraProjectKey: 'MSJP', - scheduledAt: '2020-04-09T13:14:15+00:00', - scheduledBy: { - name: 'Jimmy Doe', - }, - }, - { - jiraProjectKey: 'MTG', - scheduledAt: '2020-04-09T16:17:18+00:00', - scheduledBy: { - name: 'Jane Doe', - }, - }, - ], - mostRecentImport: { - jiraProjectKey: 'MTG', - scheduledAt: '2020-04-09T16:17:18+00:00', - scheduledBy: { - name: 'Jane Doe', - }, - }, - projects: [ - { text: 'My Jira Project (MJP)', value: 'MJP' }, - { text: 'My Second Jira Project (MSJP)', value: 'MSJP' }, - { text: 'Migrate to GitLab (MTG)', value: 'MTG' }, - ], - }, - }; - }, - mocks: { - $apollo: { - loading, - mutate, - }, - }, - }); -}; +import getJiraUserMappingMutation from '~/jira_import/queries/get_jira_user_mapping.mutation.graphql'; +import { imports, issuesPath, jiraIntegrationPath, jiraProjects, userMappings } from '../mock_data'; describe('JiraImportApp', () => { + let axiosMock; + let mutateSpy; let wrapper; const getFormComponent = () => wrapper.find(JiraImportForm); @@ -95,7 +26,64 @@ describe('JiraImportApp', () => { const getLoadingIcon = () => wrapper.find(GlLoadingIcon); + const mountComponent = ({ + isJiraConfigured = true, + errorMessage = '', + selectedProject = 'MTG', + showAlert = false, + isInProgress = false, + loading = false, + mutate = mutateSpy, + mountFunction = shallowMount, + } = {}) => + mountFunction(JiraImportApp, { + propsData: { + inProgressIllustration: 'in-progress-illustration.svg', + isJiraConfigured, + issuesPath, + jiraIntegrationPath, + projectId: '5', + projectPath: 'gitlab-org/gitlab-test', + setupIllustration: 'setup-illustration.svg', + }, + data() { + return { + isSubmitting: false, + selectedProject, + userMappings, + errorMessage, + showAlert, + jiraImportDetails: { + isInProgress, + imports, + mostRecentImport: imports[imports.length - 1], + projects: jiraProjects, + }, + }; + }, + mocks: { + $apollo: { + loading, + mutate, + }, + }, + }); + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + mutateSpy = jest.fn(() => + Promise.resolve({ + data: { + jiraImportStart: { errors: [] }, + jiraImportUsers: { jiraUsers: [], errors: [] }, + }, + }), + ); + }); + afterEach(() => { + axiosMock.restore(); + mutateSpy.mockRestore(); wrapper.destroy(); wrapper = null; }); @@ -223,7 +211,7 @@ describe('JiraImportApp', () => { }); it('shows warning alert to explain project MTG has been imported 2 times before', () => { - wrapper = mountComponent({ mountType: 'mount' }); + wrapper = mountComponent({ mountFunction: mount }); expect(getAlert().text()).toBe( 'You have imported from this project 2 times before. Each new import will create duplicate issues.', @@ -248,9 +236,7 @@ describe('JiraImportApp', () => { describe('initiating a Jira import', () => { it('calls the mutation with the expected arguments', () => { - const mutate = jest.fn(() => Promise.resolve()); - - wrapper = mountComponent({ mutate }); + wrapper = mountComponent(); const mutationArguments = { mutation: initiateJiraImportMutation, @@ -258,14 +244,23 @@ describe('JiraImportApp', () => { input: { jiraProjectKey: 'MTG', projectPath: 'gitlab-org/gitlab-test', - usersMapping: [], + usersMapping: [ + { + jiraAccountId: 'aei23f98f-q23fj98qfj', + gitlabId: 15, + }, + { + jiraAccountId: 'fu39y8t34w-rq3u289t3h4i', + gitlabId: undefined, + }, + ], }, }, }; getFormComponent().vm.$emit('initiateJiraImport', 'MTG'); - expect(mutate).toHaveBeenCalledWith(expect.objectContaining(mutationArguments)); + expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments)); }); it('shows alert message with error message on error', () => { @@ -284,19 +279,53 @@ describe('JiraImportApp', () => { }); }); - it('can dismiss alert message', () => { - wrapper = mountComponent({ - errorMessage: 'There was an error importing the Jira project.', - showAlert: true, - selectedProject: null, + describe('alert', () => { + it('can be dismissed', () => { + wrapper = mountComponent({ + errorMessage: 'There was an error importing the Jira project.', + showAlert: true, + selectedProject: null, + }); + + expect(getAlert().exists()).toBe(true); + + getAlert().vm.$emit('dismiss'); + + return Vue.nextTick().then(() => { + expect(getAlert().exists()).toBe(false); + }); }); + }); - expect(getAlert().exists()).toBe(true); + describe('on mount', () => { + it('makes a GraphQL mutation call to get user mappings', () => { + wrapper = mountComponent(); - getAlert().vm.$emit('dismiss'); + const mutationArguments = { + mutation: getJiraUserMappingMutation, + variables: { + input: { + projectPath: 'gitlab-org/gitlab-test', + startAt: 1, + }, + }, + }; + + expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments)); + }); + + it('does not make a GraphQL mutation call to get user mappings when Jira is not configured', () => { + wrapper = mountComponent({ isJiraConfigured: false }); + + expect(mutateSpy).not.toHaveBeenCalled(); + }); + + it('shows error message when there is an error with the GraphQL mutation call', () => { + const mutate = jest.fn(() => Promise.reject()); + + wrapper = mountComponent({ mutate }); - return Vue.nextTick().then(() => { - expect(getAlert().exists()).toBe(false); + expect(getAlert().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js index dea94e7bf1f..685b0288e92 100644 --- a/spec/frontend/jira_import/components/jira_import_form_spec.js +++ b/spec/frontend/jira_import/components/jira_import_form_spec.js @@ -1,44 +1,51 @@ -import { GlAvatar, GlButton, GlFormSelect, GlLabel } from '@gitlab/ui'; +import { GlButton, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui'; +import { getByRole } from '@testing-library/dom'; import { mount, shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import JiraImportForm from '~/jira_import/components/jira_import_form.vue'; - -const importLabel = 'jira-import::MTG-1'; -const value = 'MTG'; - -const mountComponent = ({ mountType } = {}) => { - const mountFunction = mountType === 'mount' ? mount : shallowMount; - - return mountFunction(JiraImportForm, { - propsData: { - importLabel, - issuesPath: 'gitlab-org/gitlab-test/-/issues', - jiraProjects: [ - { - text: 'My Jira Project', - value: 'MJP', - }, - { - text: 'My Second Jira Project', - value: 'MSJP', - }, - { - text: 'Migrate to GitLab', - value: 'MTG', - }, - ], - value, - }, - }); -}; +import { issuesPath, jiraProjects, userMappings } from '../mock_data'; describe('JiraImportForm', () => { + let axiosMock; let wrapper; + const currentUsername = 'mrgitlab'; + const importLabel = 'jira-import::MTG-1'; + const value = 'MTG'; + const getSelectDropdown = () => wrapper.find(GlFormSelect); const getCancelButton = () => wrapper.findAll(GlButton).at(1); + const getHeader = name => getByRole(wrapper.element, 'columnheader', { name }); + + const mountComponent = ({ isSubmitting = false, mountFunction = shallowMount } = {}) => + mountFunction(JiraImportForm, { + propsData: { + importLabel, + isSubmitting, + issuesPath, + jiraProjects, + projectId: '5', + userMappings, + value, + }, + data: () => ({ + isFetching: false, + searchTerm: '', + selectState: null, + users: [], + }), + currentUsername, + }); + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + afterEach(() => { + axiosMock.restore(); wrapper.destroy(); wrapper = null; }); @@ -51,16 +58,22 @@ describe('JiraImportForm', () => { }); it('contains a list of Jira projects to select from', () => { - wrapper = mountComponent({ mountType: 'mount' }); - - const optionItems = ['My Jira Project', 'My Second Jira Project', 'Migrate to GitLab']; + wrapper = mountComponent({ mountFunction: mount }); getSelectDropdown() .findAll('option') .wrappers.forEach((optionEl, index) => { - expect(optionEl.text()).toBe(optionItems[index]); + expect(optionEl.text()).toBe(jiraProjects[index].text); }); }); + + it('emits an "input" event when the input select value changes', () => { + wrapper = mountComponent(); + + getSelectDropdown().vm.$emit('change', value); + + expect(wrapper.emitted('input')[0]).toEqual([value]); + }); }); describe('form information', () => { @@ -72,64 +85,90 @@ describe('JiraImportForm', () => { expect(wrapper.find(GlLabel).props('title')).toBe(importLabel); }); + it('shows a heading for the user mapping section', () => { + expect( + getByRole(wrapper.element, 'heading', { name: 'Jira-GitLab user mapping template' }), + ).toBeTruthy(); + }); + it('shows information to the user', () => { expect(wrapper.find('p').text()).toBe( - "For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:", + 'Jira users have been matched with similar GitLab users. This can be overwritten by selecting a GitLab user from the dropdown in the "GitLab username" column. If it wasn\'t possible to match a Jira user with a GitLab user, the dropdown defaults to the user conducting the import.', ); }); + }); - it('shows jira.issue.summary for the Title', () => { - expect(wrapper.find('[id="jira-project-title"]').text()).toBe('jira.issue.summary'); - }); + describe('table', () => { + describe('headers', () => { + beforeEach(() => { + wrapper = mountComponent({ mountFunction: mount }); + }); - it('shows an avatar for the Reporter', () => { - expect(wrapper.contains(GlAvatar)).toBe(true); - }); + it('has a "Jira display name" column', () => { + expect(getHeader('Jira display name')).toBeTruthy(); + }); - it('shows jira.issue.description.content for the Description', () => { - expect(wrapper.find('[id="jira-project-description"]').text()).toBe( - 'jira.issue.description.content', - ); - }); - }); + it('has an "arrow" column', () => { + expect(getHeader('Arrow')).toBeTruthy(); + }); - describe('Next button', () => { - beforeEach(() => { - wrapper = mountComponent(); + it('has a "GitLab username" column', () => { + expect(getHeader('GitLab username')).toBeTruthy(); + }); }); - it('is shown', () => { - expect(wrapper.find(GlButton).text()).toBe('Next'); + describe('body', () => { + it('shows all user mappings', () => { + wrapper = mountComponent({ mountFunction: mount }); + + expect(wrapper.find(GlTable).findAll('tbody tr').length).toBe(userMappings.length); + }); + + it('shows correct information in each cell', () => { + wrapper = mountComponent({ mountFunction: mount }); + + expect(wrapper.find(GlTable).element).toMatchSnapshot(); + }); }); }); - describe('Cancel button', () => { - beforeEach(() => { - wrapper = mountComponent(); - }); + describe('buttons', () => { + describe('"Continue" button', () => { + it('is shown', () => { + wrapper = mountComponent(); - it('is shown', () => { - expect(getCancelButton().text()).toBe('Cancel'); - }); + expect(wrapper.find(GlButton).text()).toBe('Continue'); + }); + + it('is in loading state when the form is submitting', async () => { + wrapper = mountComponent({ isSubmitting: true }); - it('links to the Issues page', () => { - expect(getCancelButton().attributes('href')).toBe('gitlab-org/gitlab-test/-/issues'); + expect(wrapper.find(GlButton).props('loading')).toBe(true); + }); }); - }); - it('emits an "input" event when the input select value changes', () => { - wrapper = mountComponent({ mountType: 'mount' }); + describe('"Cancel" button', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); - getSelectDropdown().vm.$emit('change', value); + it('is shown', () => { + expect(getCancelButton().text()).toBe('Cancel'); + }); - expect(wrapper.emitted('input')[0]).toEqual([value]); + it('links to the Issues page', () => { + expect(getCancelButton().attributes('href')).toBe(issuesPath); + }); + }); }); - it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => { - wrapper = mountComponent(); + describe('form', () => { + it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => { + wrapper = mountComponent(); - wrapper.find('form').trigger('submit'); + wrapper.find('form').trigger('submit'); - expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]); + expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]); + }); }); }); diff --git a/spec/frontend/jira_import/components/jira_import_progress_spec.js b/spec/frontend/jira_import/components/jira_import_progress_spec.js index 3ccf14554e1..ed7e1824fa3 100644 --- a/spec/frontend/jira_import/components/jira_import_progress_spec.js +++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js @@ -1,14 +1,13 @@ import { GlEmptyState } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue'; - -const illustration = 'illustration.svg'; -const importProject = 'JIRAPROJECT'; -const issuesPath = 'gitlab-org/gitlab-test/-/issues'; +import { illustration, issuesPath } from '../mock_data'; describe('JiraImportProgress', () => { let wrapper; + const importProject = 'JIRAPROJECT'; + const getGlEmptyStateProp = attribute => wrapper.find(GlEmptyState).props(attribute); const getParagraphText = () => wrapper.find('p').text(); diff --git a/spec/frontend/jira_import/components/jira_import_setup_spec.js b/spec/frontend/jira_import/components/jira_import_setup_spec.js index aa94dc4f503..7c84d4a166a 100644 --- a/spec/frontend/jira_import/components/jira_import_setup_spec.js +++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js @@ -1,9 +1,7 @@ import { GlEmptyState } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue'; - -const illustration = 'illustration.svg'; -const jiraIntegrationPath = 'gitlab-org/gitlab-test/-/services/jira/edit'; +import { illustration, jiraIntegrationPath } from '../mock_data'; describe('JiraImportSetup', () => { let wrapper; diff --git a/spec/frontend/jira_import/mock_data.js b/spec/frontend/jira_import/mock_data.js index e82ab53cb6f..a7447221b15 100644 --- a/spec/frontend/jira_import/mock_data.js +++ b/spec/frontend/jira_import/mock_data.js @@ -70,3 +70,56 @@ export const jiraImportMutationResponse = { __typename: 'JiraImportStartPayload', }, }; + +export const issuesPath = 'gitlab-org/gitlab-test/-/issues'; + +export const jiraIntegrationPath = 'gitlab-org/gitlab-test/-/services/jira/edit'; + +export const illustration = 'illustration.svg'; + +export const jiraProjects = [ + { text: 'My Jira Project (MJP)', value: 'MJP' }, + { text: 'My Second Jira Project (MSJP)', value: 'MSJP' }, + { text: 'Migrate to GitLab (MTG)', value: 'MTG' }, +]; + +export const imports = [ + { + jiraProjectKey: 'MTG', + scheduledAt: '2020-04-08T10:11:12+00:00', + scheduledBy: { + name: 'John Doe', + }, + }, + { + jiraProjectKey: 'MSJP', + scheduledAt: '2020-04-09T13:14:15+00:00', + scheduledBy: { + name: 'Jimmy Doe', + }, + }, + { + jiraProjectKey: 'MTG', + scheduledAt: '2020-04-09T16:17:18+00:00', + scheduledBy: { + name: 'Jane Doe', + }, + }, +]; + +export const userMappings = [ + { + jiraAccountId: 'aei23f98f-q23fj98qfj', + jiraDisplayName: 'Jane Doe', + jiraEmail: 'janedoe@example.com', + gitlabId: 15, + gitlabUsername: 'janedoe', + }, + { + jiraAccountId: 'fu39y8t34w-rq3u289t3h4i', + jiraDisplayName: 'Fred Chopin', + jiraEmail: 'fredchopin@example.com', + gitlabId: undefined, + gitlabUsername: undefined, + }, +]; diff --git a/spec/frontend/logs/components/log_control_buttons_spec.js b/spec/frontend/logs/components/log_control_buttons_spec.js index 85fc5a040d6..38e568f569f 100644 --- a/spec/frontend/logs/components/log_control_buttons_spec.js +++ b/spec/frontend/logs/components/log_control_buttons_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlDeprecatedButton } from '@gitlab/ui'; +import { GlButton } from '@gitlab/ui'; import LogControlButtons from '~/logs/components/log_control_buttons.vue'; describe('LogControlButtons', () => { @@ -31,9 +31,9 @@ describe('LogControlButtons', () => { expect(wrapper.isVueInstance()).toBe(true); expect(wrapper.isEmpty()).toBe(false); - expect(findScrollToTop().is(GlDeprecatedButton)).toBe(true); - expect(findScrollToBottom().is(GlDeprecatedButton)).toBe(true); - expect(findRefreshBtn().is(GlDeprecatedButton)).toBe(true); + expect(findScrollToTop().is(GlButton)).toBe(true); + expect(findScrollToBottom().is(GlButton)).toBe(true); + expect(findRefreshBtn().is(GlButton)).toBe(true); }); it('emits a `refresh` event on click on `refresh` button', () => { diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 3b3b0eddc21..89917e515d0 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -229,7 +229,6 @@ RSpec.describe Gitlab::Profiler do .map { |(total)| total.to_f } expect(total_times).to eq(total_times.sort.reverse) - expect(total_times).not_to eq(total_times.uniq) end end diff --git a/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb b/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb new file mode 100644 index 00000000000..584d8407e79 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::TrackUniqueActions, :clean_gitlab_redis_shared_state do + subject(:track_unique_events) { described_class } + + let(:time) { Time.zone.now } + + def track_action(params) + track_unique_events.track_action(params) + end + + def count_unique_events(params) + track_unique_events.count_unique_events(params) + end + + context 'tracking an event' do + context 'when tracking successfully' do + context 'when the feature flag and the application setting is enabled' do + context 'when the target and the action is valid' do + before do + stub_feature_flags(described_class::FEATURE_FLAG => true) + stub_application_setting(usage_ping_enabled: true) + end + + it 'tracks and counts the events as expected' do + project = Event::TARGET_TYPES[:project] + design = Event::TARGET_TYPES[:design] + wiki = Event::TARGET_TYPES[:wiki] + + expect(track_action(event_action: :pushed, event_target: project, author_id: 1)).to be_truthy + expect(track_action(event_action: :pushed, event_target: project, author_id: 1)).to be_truthy + expect(track_action(event_action: :pushed, event_target: project, author_id: 2)).to be_truthy + expect(track_action(event_action: :pushed, event_target: project, author_id: 3)).to be_truthy + expect(track_action(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days)).to be_truthy + expect(track_action(event_action: :created, event_target: project, author_id: 5, time: time - 3.days)).to be_truthy + + expect(track_action(event_action: :destroyed, event_target: design, author_id: 3)).to be_truthy + expect(track_action(event_action: :created, event_target: design, author_id: 4)).to be_truthy + expect(track_action(event_action: :updated, event_target: design, author_id: 5)).to be_truthy + expect(track_action(event_action: :pushed, event_target: design, author_id: 6)).to be_truthy + + expect(track_action(event_action: :destroyed, event_target: wiki, author_id: 5)).to be_truthy + expect(track_action(event_action: :created, event_target: wiki, author_id: 3)).to be_truthy + expect(track_action(event_action: :updated, event_target: wiki, author_id: 4)).to be_truthy + expect(track_action(event_action: :pushed, event_target: wiki, author_id: 6)).to be_truthy + + expect(count_unique_events(event_action: described_class::PUSH_ACTION, date_from: time, date_to: Date.today)).to eq(3) + expect(count_unique_events(event_action: described_class::PUSH_ACTION, date_from: time - 5.days, date_to: Date.tomorrow)).to eq(4) + expect(count_unique_events(event_action: described_class::DESIGN_ACTION, date_from: time - 5.days, date_to: Date.today)).to eq(3) + expect(count_unique_events(event_action: described_class::WIKI_ACTION, date_from: time - 5.days, date_to: Date.today)).to eq(3) + expect(count_unique_events(event_action: described_class::PUSH_ACTION, date_from: time - 5.days, date_to: time - 2.days)).to eq(1) + end + end + end + end + + context 'when tracking unsuccessfully' do + using RSpec::Parameterized::TableSyntax + + where(:feature_flag, :application_setting, :target, :action) do + true | true | Project | :invalid_action + false | true | Project | :pushed + true | false | Project | :pushed + true | true | :invalid_target | :pushed + end + + with_them do + before do + stub_application_setting(usage_ping_enabled: application_setting) + stub_feature_flags(described_class::FEATURE_FLAG => feature_flag) + end + + it 'returns the expected values' do + expect(track_action(event_action: action, event_target: target, author_id: 2)).to be_nil + expect(count_unique_events(event_action: described_class::PUSH_ACTION, date_from: time, date_to: Date.today)).to eq(0) + end + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index a29d3ed40b5..4fc8d49a873 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -919,6 +919,53 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end + describe '#action_monthly_active_users', :clean_gitlab_redis_shared_state do + let(:time_period) { { created_at: 2.days.ago..time } } + let(:time) { Time.zone.now } + + before do + stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => feature_flag) + end + + context 'when the feature flag is enabled' do + let(:feature_flag) { true } + + before do + counter = Gitlab::UsageDataCounters::TrackUniqueActions + project = Event::TARGET_TYPES[:project] + wiki = Event::TARGET_TYPES[:wiki] + design = Event::TARGET_TYPES[:design] + + counter.track_action(event_action: :pushed, event_target: project, author_id: 1) + counter.track_action(event_action: :pushed, event_target: project, author_id: 1) + counter.track_action(event_action: :pushed, event_target: project, author_id: 2) + counter.track_action(event_action: :pushed, event_target: project, author_id: 3) + counter.track_action(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days) + counter.track_action(event_action: :created, event_target: project, author_id: 5, time: time - 3.days) + counter.track_action(event_action: :created, event_target: wiki, author_id: 3) + counter.track_action(event_action: :created, event_target: design, author_id: 3) + end + + it 'returns the distinct count of user actions within the specified time period' do + expect(described_class.action_monthly_active_users(time_period)).to eq( + { + action_monthly_active_users_design_management: 1, + action_monthly_active_users_project_repo: 3, + action_monthly_active_users_wiki_repo: 1 + } + ) + end + end + + context 'when the feature flag is disabled' do + let(:feature_flag) { false } + + it 'returns an empty hash' do + expect(described_class.action_monthly_active_users(time_period)).to eq({}) + end + end + end + describe '.analytics_unique_visits_data' do subject { described_class.analytics_unique_visits_data } diff --git a/spec/models/project_services/jira_tracker_data_spec.rb b/spec/models/project_services/jira_tracker_data_spec.rb index 844036dc372..9e38bced46c 100644 --- a/spec/models/project_services/jira_tracker_data_spec.rb +++ b/spec/models/project_services/jira_tracker_data_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe JiraTrackerData do - let(:service) { create(:jira_service, active: false) } + let(:service) { build(:jira_service) } describe 'Associations' do it { is_expected.to belong_to(:service) } diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 2bcdda6d276..7e4b61ed1b7 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -166,7 +166,7 @@ RSpec.describe EventCreateService do end end - describe '#wiki_event' do + describe '#wiki_event', :clean_gitlab_redis_shared_state do let_it_be(:user) { create(:user) } let_it_be(:wiki_page) { create(:wiki_page) } let_it_be(:meta) { create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) } @@ -186,6 +186,16 @@ RSpec.describe EventCreateService do ) end + it 'records the event in the event counter' do + stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) + counter_class = Gitlab::UsageDataCounters::TrackUniqueActions + tracking_params = { event_action: counter_class::WIKI_ACTION, date_from: Date.yesterday, date_to: Date.today } + + expect { event } + .to change { counter_class.count_unique_events(tracking_params) } + .from(0).to(1) + end + it 'is idempotent', :aggregate_failures do expect { event }.to change(Event, :count).by(1) duplicate = nil @@ -224,6 +234,16 @@ RSpec.describe EventCreateService do subject { service.push(project, user, push_data) } it_behaves_like 'service for creating a push event', PushEventPayloadService + + it 'records the event in the event counter' do + stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) + counter_class = Gitlab::UsageDataCounters::TrackUniqueActions + tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today } + + expect { subject } + .to change { counter_class.count_unique_events(tracking_params) } + .from(0).to(1) + end end describe '#bulk_push', :clean_gitlab_redis_shared_state do @@ -238,6 +258,16 @@ RSpec.describe EventCreateService do subject { service.bulk_push(project, user, push_data) } it_behaves_like 'service for creating a push event', BulkPushEventPayloadService + + it 'records the event in the event counter' do + stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) + counter_class = Gitlab::UsageDataCounters::TrackUniqueActions + tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today } + + expect { subject } + .to change { counter_class.count_unique_events(tracking_params) } + .from(0).to(1) + end end describe 'Project' do @@ -256,7 +286,7 @@ RSpec.describe EventCreateService do end end - describe 'design events' do + describe 'design events', :clean_gitlab_redis_shared_state do let_it_be(:design) { create(:design, project: project) } let_it_be(:author) { user } @@ -297,6 +327,16 @@ RSpec.describe EventCreateService do end it_behaves_like 'feature flag gated multiple event creation' + + it 'records the event in the event counter' do + stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) + counter_class = Gitlab::UsageDataCounters::TrackUniqueActions + tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today } + + expect { result } + .to change { counter_class.count_unique_events(tracking_params) } + .from(0).to(1) + end end describe '#destroy_designs' do @@ -317,6 +357,16 @@ RSpec.describe EventCreateService do end it_behaves_like 'feature flag gated multiple event creation' + + it 'records the event in the event counter' do + stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) + counter_class = Gitlab::UsageDataCounters::TrackUniqueActions + tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today } + + expect { result } + .to change { counter_class.count_unique_events(tracking_params) } + .from(0).to(1) + end end end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 9f40070029f..fd824621db7 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Notes::CreateService do let_it_be(:issue) { create(:issue, project: project) } let_it_be(:user) { create(:user) } let(:opts) do - { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id } + { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id, confidential: true } end describe '#execute' do diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index 3f4c70ce389..6056359d026 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -92,7 +92,7 @@ module StubObjectStorage def stub_terraform_state_object_storage(uploader = described_class, **params) stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store, uploader: uploader, - remote_directory: 'terraform_state', + remote_directory: 'terraform', **params) end diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index 60ed61269df..a34c48a5ba4 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -132,6 +132,16 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq('hi!') + expect(json_response['confidential']).to be_falsey + expect(json_response['author']['username']).to eq(user.username) + end + + it "creates a confidential note if confidential is set to true" do + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!', confidential: true } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['body']).to eq('hi!') + expect(json_response['confidential']).to be_truthy expect(json_response['author']['username']).to eq(user.username) end |