summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-15 15:09:21 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-15 15:09:21 +0000
commite69e3f1eb695b4e852c56e7ddf8c52915ae2631b (patch)
treef0a093bc9faf84f94c75401e5c8c3754ee24ee0e
parent9215d9f7619929f9da16744fa37636635b66949b (diff)
downloadgitlab-ce-e69e3f1eb695b4e852c56e7ddf8c52915ae2631b.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue23
-rw-r--r--app/assets/javascripts/design_management_new/pages/index.vue2
-rw-r--r--app/assets/javascripts/design_management_new/router/routes.js3
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_app.vue60
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue175
-rw-r--r--app/assets/javascripts/jira_import/index.js1
-rw-r--r--app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql11
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue2
-rw-r--r--app/assets/javascripts/logs/components/log_control_buttons.vue37
-rw-r--r--app/assets/stylesheets/pages/environment_logs.scss4
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/services/audit_event_service.rb3
-rw-r--r--app/services/event_create_service.rb12
-rw-r--r--app/views/projects/import/jira/show.html.haml1
-rw-r--r--app/views/projects/issues/_design_management.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml4
-rw-r--r--changelogs/unreleased/207472-create-confidential-note-api.yml5
-rw-r--r--changelogs/unreleased/216145-frontend-create-jira-import-user-mapping-form.yml5
-rw-r--r--changelogs/unreleased/219956-instrument-last-git-write-operation-per-user.yml5
-rw-r--r--changelogs/unreleased/223196-update-design-management-documentation.yml5
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--doc/administration/terraform_state.md4
-rw-r--r--doc/api/notes.md1
-rw-r--r--doc/development/telemetry/usage_ping.md4
-rw-r--r--doc/policy/maintenance.md15
-rw-r--r--doc/user/admin_area/merge_requests_approvals.md19
-rw-r--r--doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.pngbin70913 -> 73101 bytes
-rw-r--r--doc/user/compliance/license_compliance/index.md23
-rw-r--r--doc/user/project/import/img/jira/import_issues_from_jira_button_v12_10.pngbin8504 -> 8422 bytes
-rw-r--r--doc/user/project/import/img/jira/import_issues_from_jira_form_v12_10.pngbin116641 -> 56306 bytes
-rw-r--r--doc/user/project/import/img/jira/import_issues_from_jira_form_v13_2.pngbin0 -> 108152 bytes
-rw-r--r--doc/user/project/import/img/jira/import_issues_from_jira_projects_v12_10.pngbin521845 -> 0 bytes
-rw-r--r--doc/user/project/import/jira.md33
-rw-r--r--doc/user/project/issues/design_management.md15
-rw-r--r--doc/user/project/issues/img/design_drag_and_drop_uploads_v13_2.pngbin0 -> 1260905 bytes
-rw-r--r--doc/user/project/issues/img/design_management_upload_v13.2.pngbin0 -> 62146 bytes
-rw-r--r--doc/user/project/issues/img/design_management_v13_2.pngbin0 -> 1017975 bytes
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/gitlab/usage_data.rb41
-rw-r--r--lib/gitlab/usage_data_counters/track_unique_actions.rb86
-rw-r--r--locale/gitlab.pot39
-rw-r--r--qa/Dockerfile2
-rw-r--r--spec/factories/services.rb5
-rw-r--r--spec/features/security/project/snippet/public_access_spec.rb9
-rw-r--r--spec/frontend/design_management_new/components/upload/__snapshots__/design_dropzone_spec.js.snap60
-rw-r--r--spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap10
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap299
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js209
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js179
-rw-r--r--spec/frontend/jira_import/components/jira_import_progress_spec.js7
-rw-r--r--spec/frontend/jira_import/components/jira_import_setup_spec.js4
-rw-r--r--spec/frontend/jira_import/mock_data.js53
-rw-r--r--spec/frontend/logs/components/log_control_buttons_spec.js8
-rw-r--r--spec/lib/gitlab/profiler_spec.rb1
-rw-r--r--spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb82
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb47
-rw-r--r--spec/models/project_services/jira_tracker_data_spec.rb2
-rw-r--r--spec/services/event_create_service_spec.rb54
-rw-r--r--spec/services/notes/create_service_spec.rb2
-rw-r--r--spec/support/helpers/stub_object_storage.rb2
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb10
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 }}&nbsp;</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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
new file mode 100644
index 00000000000..9cbffe2bb36
--- /dev/null
+++ b/doc/user/project/import/img/jira/import_issues_from_jira_form_v13_2.png
Binary files differ
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
deleted file mode 100644
index bf9728e0311..00000000000
--- a/doc/user/project/import/img/jira/import_issues_from_jira_projects_v12_10.png
+++ /dev/null
Binary files differ
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
new file mode 100644
index 00000000000..d60f1234b6d
--- /dev/null
+++ b/doc/user/project/issues/img/design_drag_and_drop_uploads_v13_2.png
Binary files differ
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
new file mode 100644
index 00000000000..1d4b10307fc
--- /dev/null
+++ b/doc/user/project/issues/img/design_management_upload_v13.2.png
Binary files differ
diff --git a/doc/user/project/issues/img/design_management_v13_2.png b/doc/user/project/issues/img/design_management_v13_2.png
new file mode 100644
index 00000000000..0a6e2be17ab
--- /dev/null
+++ b/doc/user/project/issues/img/design_management_v13_2.png
Binary files differ
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