Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-15 15:09:21 +00:00
parent 9215d9f761
commit e69e3f1eb6
61 changed files with 1357 additions and 368 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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') {

View File

@ -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>

View File

@ -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>

View File

@ -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,
},

View File

@ -0,0 +1,11 @@
mutation($input: JiraImportUsersInput!) {
jiraImportUsers(input: $input) {
jiraUsers {
jiraAccountId
jiraDisplayName
jiraEmail
gitlabId
}
errors
}
}

View File

@ -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"

View File

@ -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>

View File

@ -31,10 +31,6 @@
width: 160px;
}
}
.controllers {
@include build-controllers(16px, flex-end, false, 2, inline);
}
}
.log-lines,

View File

@ -440,3 +440,5 @@ class JiraService < IssueTrackerService
end
end
end
JiraService.prepend_if_ee('EE::JiraService')

View File

@ -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

View File

@ -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)

View File

@ -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') } }

View File

@ -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')

View File

@ -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'

View File

@ -0,0 +1,5 @@
---
title: Add confidential attribute to public API for notes creation
merge_request: 36793
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add Jira Importer user mapping form
merge_request: 33320
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Track the number of unique users who push, change wikis and change design managerment
merge_request:
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Make the Design Collection more visible in the Issue UI
merge_request: 36681
author:
type: changed

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 | |

View File

@ -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.

View File

@ -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)
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 KiB

View File

@ -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_v12_10.png)
![Import issues from Jira form](img/jira/import_issues_from_jira_form_v13_2.png)
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.
1. Click the **Import from** dropdown and select the Jira project that you wish to import issues from.
1. Select the Jira project that you wish to import issues from.
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.
![Import issues from Jira form](img/jira/import_issues_from_jira_projects_v12_10.png)
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.
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.

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

View File

@ -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]
}

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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>

View File

@ -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"
>
<!---->

View File

@ -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>
`;

View File

@ -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);
});
});
});
describe('on mount', () => {
it('makes a GraphQL mutation call to get user mappings', () => {
wrapper = mountComponent();
const mutationArguments = {
mutation: getJiraUserMappingMutation,
variables: {
input: {
projectPath: 'gitlab-org/gitlab-test',
startAt: 1,
},
},
};
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
});
expect(getAlert().exists()).toBe(true);
it('does not make a GraphQL mutation call to get user mappings when Jira is not configured', () => {
wrapper = mountComponent({ isJiraConfigured: false });
getAlert().vm.$emit('dismiss');
expect(mutateSpy).not.toHaveBeenCalled();
});
return Vue.nextTick().then(() => {
expect(getAlert().exists()).toBe(false);
it('shows error message when there is an error with the GraphQL mutation call', () => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
expect(getAlert().exists()).toBe(true);
});
});
});

View File

@ -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:",
);
});
it('shows jira.issue.summary for the Title', () => {
expect(wrapper.find('[id="jira-project-title"]').text()).toBe('jira.issue.summary');
});
it('shows an avatar for the Reporter', () => {
expect(wrapper.contains(GlAvatar)).toBe(true);
});
it('shows jira.issue.description.content for the Description', () => {
expect(wrapper.find('[id="jira-project-description"]').text()).toBe(
'jira.issue.description.content',
'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.',
);
});
});
describe('Next button', () => {
beforeEach(() => {
describe('table', () => {
describe('headers', () => {
beforeEach(() => {
wrapper = mountComponent({ mountFunction: mount });
});
it('has a "Jira display name" column', () => {
expect(getHeader('Jira display name')).toBeTruthy();
});
it('has an "arrow" column', () => {
expect(getHeader('Arrow')).toBeTruthy();
});
it('has a "GitLab username" column', () => {
expect(getHeader('GitLab username')).toBeTruthy();
});
});
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('buttons', () => {
describe('"Continue" button', () => {
it('is shown', () => {
wrapper = mountComponent();
expect(wrapper.find(GlButton).text()).toBe('Continue');
});
it('is in loading state when the form is submitting', async () => {
wrapper = mountComponent({ isSubmitting: true });
expect(wrapper.find(GlButton).props('loading')).toBe(true);
});
});
describe('"Cancel" button', () => {
beforeEach(() => {
wrapper = mountComponent();
});
it('is shown', () => {
expect(getCancelButton().text()).toBe('Cancel');
});
it('links to the Issues page', () => {
expect(getCancelButton().attributes('href')).toBe(issuesPath);
});
});
});
describe('form', () => {
it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => {
wrapper = mountComponent();
wrapper.find('form').trigger('submit');
expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]);
});
it('is shown', () => {
expect(wrapper.find(GlButton).text()).toBe('Next');
});
});
describe('Cancel button', () => {
beforeEach(() => {
wrapper = mountComponent();
});
it('is shown', () => {
expect(getCancelButton().text()).toBe('Cancel');
});
it('links to the Issues page', () => {
expect(getCancelButton().attributes('href')).toBe('gitlab-org/gitlab-test/-/issues');
});
});
it('emits an "input" event when the input select value changes', () => {
wrapper = mountComponent({ mountType: 'mount' });
getSelectDropdown().vm.$emit('change', value);
expect(wrapper.emitted('input')[0]).toEqual([value]);
});
it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => {
wrapper = mountComponent();
wrapper.find('form').trigger('submit');
expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]);
});
});

View File

@ -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();

View File

@ -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;

View File

@ -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,
},
];

View File

@ -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', () => {

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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