Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7d112a9002
commit
413c91fda9
|
|
@ -2,7 +2,7 @@
|
||||||
# project here: https://gitlab.com/gitlab-org/gitlab/-/project_members
|
# project here: https://gitlab.com/gitlab-org/gitlab/-/project_members
|
||||||
# As described in https://docs.gitlab.com/ee/user/project/code_owners.html
|
# As described in https://docs.gitlab.com/ee/user/project/code_owners.html
|
||||||
|
|
||||||
* @gitlab-org/maintainers/rails-backend @gitlab-org/maintainers/frontend @gl-quality/qe-maintainers @gl-quality/tooling-maintainers @gitlab-org/delivery @gitlab-org/maintainers/cicd-templates @nolith @gitlab-org/tw-leadership
|
* @gitlab-org/maintainers/rails-backend @gitlab-org/maintainers/frontend @gitlab-org/maintainers/database @gl-quality/qe-maintainers @gl-quality/tooling-maintainers @gitlab-org/delivery @gitlab-org/maintainers/cicd-templates @nolith @gitlab-org/tw-leadership
|
||||||
|
|
||||||
.gitlab/CODEOWNERS @gitlab-org/development-leaders @gitlab-org/tw-leadership
|
.gitlab/CODEOWNERS @gitlab-org/development-leaders @gitlab-org/tw-leadership
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Please refer to the [Remote Design Sprint Handbook page](https://about.gitlab.co
|
||||||
|
|
||||||
## Design Sprint Focus
|
## Design Sprint Focus
|
||||||
* [ ] Have you [determined that a Design Sprint is appropriate for this project](https://about.gitlab.com/handbook/product/ux/design-sprint/#when-to-opt-for-a-remote-design-sprint)?
|
* [ ] Have you [determined that a Design Sprint is appropriate for this project](https://about.gitlab.com/handbook/product/ux/design-sprint/#when-to-opt-for-a-remote-design-sprint)?
|
||||||
_What is the focus of the [Design Sprint](https://about.gitlab.com/handbook/product/product-processes/#design-sprint)? What problem area will you be solving for and who is the target user?_
|
_What is the focus of the [Design Sprint](https://about.gitlab.com/handbook/product/product-processes/#remote-design-sprint)? What problem area will you be solving for and who is the target user?_
|
||||||
|
|
||||||
## Objectives
|
## Objectives
|
||||||
_What is the objective(s) this Design Sprint will entail?_
|
_What is the objective(s) this Design Sprint will entail?_
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,43 @@
|
||||||
<script>
|
<script>
|
||||||
export default {};
|
import { GlLoadingIcon } from '@gitlab/ui';
|
||||||
|
import { createAlert } from '~/alert';
|
||||||
|
import { s__ } from '~/locale';
|
||||||
|
import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
|
||||||
|
import groupsQuery from '../graphql/queries/groups.query.graphql';
|
||||||
|
import { formatGroups } from '../utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
i18n: {
|
||||||
|
errorMessage: s__(
|
||||||
|
'Organization|An error occurred loading the groups. Please refresh the page to try again.',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
components: { GlLoadingIcon, GroupsList },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
groups: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
groups: {
|
||||||
|
query: groupsQuery,
|
||||||
|
update(data) {
|
||||||
|
return formatGroups(data.organization.groups.nodes);
|
||||||
|
},
|
||||||
|
error(error) {
|
||||||
|
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoading() {
|
||||||
|
return this.$apollo.queries.groups.loading;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
|
||||||
<!-- Intentionally empty. Will be implemented in future commits. -->
|
<groups-list v-else :groups="groups" show-group-icon />
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
query getOrganizationGroups {
|
||||||
|
organization @client {
|
||||||
|
id
|
||||||
|
groups {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
parent
|
||||||
|
webUrl
|
||||||
|
descriptionHtml
|
||||||
|
avatarUrl
|
||||||
|
descendantGroupsCount
|
||||||
|
projectsCount
|
||||||
|
groupMembersCount
|
||||||
|
visibility
|
||||||
|
accessLevel {
|
||||||
|
integerValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import { organizationProjects } from 'jest/organizations/groups_and_projects/mock_data';
|
import {
|
||||||
|
organization,
|
||||||
|
organizationProjects,
|
||||||
|
organizationGroups,
|
||||||
|
} from 'jest/organizations/groups_and_projects/mock_data';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
|
|
@ -8,7 +12,11 @@ export default {
|
||||||
setTimeout(resolve, 1000);
|
setTimeout(resolve, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
return organizationProjects;
|
return {
|
||||||
|
...organization,
|
||||||
|
projects: organizationProjects,
|
||||||
|
groups: organizationGroups,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,9 @@ export const formatProjects = (projects) =>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const formatGroups = (groups) =>
|
||||||
|
groups.map(({ id, ...group }) => ({
|
||||||
|
...group,
|
||||||
|
id: getIdFromGraphQLId(id),
|
||||||
|
}));
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import initForm from '../shared/init_form';
|
|
||||||
|
|
||||||
initForm();
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import initForm from '../shared/init_form';
|
|
||||||
|
|
||||||
initForm();
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlDisclosureDropdown } from '@gitlab/ui';
|
import { GlDisclosureDropdown } from '@gitlab/ui';
|
||||||
import { sprintf, s__ } from '~/locale';
|
import { sprintf, s__ } from '~/locale';
|
||||||
|
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
GlDisclosureDropdown,
|
GlDisclosureDropdown,
|
||||||
|
AbuseCategorySelector,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
userId: {
|
userId: {
|
||||||
|
|
@ -13,14 +15,22 @@ export default {
|
||||||
},
|
},
|
||||||
rssSubscriptionPath: {
|
rssSubscriptionPath: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
reportedUserId: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
reportedFromUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// Only implement the copy function and RSS subscription in MR for now
|
|
||||||
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122971
|
|
||||||
// The rest will be implemented in the upcoming MR.
|
|
||||||
defaultDropdownItems: [
|
defaultDropdownItems: [
|
||||||
{
|
{
|
||||||
action: this.onUserIdCopy,
|
action: this.onUserIdCopy,
|
||||||
|
|
@ -30,38 +40,56 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
open: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dropdownItems() {
|
dropdownItems() {
|
||||||
|
const dropdownItems = this.defaultDropdownItems.slice();
|
||||||
if (this.rssSubscriptionPath) {
|
if (this.rssSubscriptionPath) {
|
||||||
return [
|
dropdownItems.push({
|
||||||
...this.defaultDropdownItems,
|
|
||||||
{
|
|
||||||
href: this.rssSubscriptionPath,
|
href: this.rssSubscriptionPath,
|
||||||
text: this.$options.i18n.rssSubscribe,
|
text: this.$options.i18n.rssSubscribe,
|
||||||
extraAttrs: {
|
extraAttrs: {
|
||||||
'data-testid': 'user-profile-rss-subscription-link',
|
'data-testid': 'user-profile-rss-subscription-link',
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
|
||||||
}
|
}
|
||||||
return this.defaultDropdownItems;
|
if (this.reportedUserId) {
|
||||||
|
dropdownItems.push({
|
||||||
|
action: () => this.toggleDrawer(true),
|
||||||
|
text: this.$options.i18n.reportToAdmin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dropdownItems;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onUserIdCopy() {
|
onUserIdCopy() {
|
||||||
this.$toast.show(this.$options.i18n.userIdCopied);
|
this.$toast.show(this.$options.i18n.userIdCopied);
|
||||||
},
|
},
|
||||||
|
toggleDrawer(open) {
|
||||||
|
this.open = open;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
userId: s__('UserProfile|Copy user ID: %{id}'),
|
userId: s__('UserProfile|Copy user ID: %{id}'),
|
||||||
userIdCopied: s__('UserProfile|User ID copied to clipboard'),
|
userIdCopied: s__('UserProfile|User ID copied to clipboard'),
|
||||||
rssSubscribe: s__('UserProfile|Subscribe'),
|
rssSubscribe: s__('UserProfile|Subscribe'),
|
||||||
|
reportToAdmin: s__('ReportAbuse|Report abuse to administrator'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<span>
|
||||||
<gl-disclosure-dropdown icon="ellipsis_v" category="tertiary" no-caret :items="dropdownItems" />
|
<gl-disclosure-dropdown icon="ellipsis_v" category="tertiary" no-caret :items="dropdownItems" />
|
||||||
|
<abuse-category-selector
|
||||||
|
v-if="reportedUserId"
|
||||||
|
:reported-user-id="reportedUserId"
|
||||||
|
:reported-from-url="reportedFromUrl"
|
||||||
|
:show-drawer="open"
|
||||||
|
@close-drawer="toggleDrawer(false)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,29 @@ export const initUserActionsApp = () => {
|
||||||
|
|
||||||
if (!mountingEl) return false;
|
if (!mountingEl) return false;
|
||||||
|
|
||||||
const { userId, rssSubscriptionPath } = mountingEl.dataset;
|
const {
|
||||||
|
userId,
|
||||||
|
rssSubscriptionPath,
|
||||||
|
reportAbusePath,
|
||||||
|
reportedUserId,
|
||||||
|
reportedFromUrl,
|
||||||
|
} = mountingEl.dataset;
|
||||||
|
|
||||||
Vue.use(GlToast);
|
Vue.use(GlToast);
|
||||||
|
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el: mountingEl,
|
el: mountingEl,
|
||||||
name: 'UserActionsRoot',
|
name: 'UserActionsRoot',
|
||||||
|
provide: {
|
||||||
|
reportAbusePath,
|
||||||
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement(UserActionsApp, {
|
return createElement(UserActionsApp, {
|
||||||
props: {
|
props: {
|
||||||
userId,
|
userId,
|
||||||
rssSubscriptionPath,
|
rssSubscriptionPath,
|
||||||
|
reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null,
|
||||||
|
reportedFromUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export const initReportAbuse = () => {
|
||||||
name: 'ReportAbuseButtonRoot',
|
name: 'ReportAbuseButtonRoot',
|
||||||
provide: {
|
provide: {
|
||||||
reportAbusePath,
|
reportAbusePath,
|
||||||
reportedUserId: parseInt(reportedUserId, 10),
|
reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null,
|
||||||
reportedFromUrl,
|
reportedFromUrl,
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,18 @@ export default {
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div style="height:600px;" class="gl-display-flex gl-justify-content-center gl-align-items-center">
|
<div style="height:600px;" class="gl-display-flex gl-justify-content-center gl-align-items-center">
|
||||||
<beta-badge />
|
<beta-badge :size="size" />
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Template = () => ({
|
const Template = (args, { argTypes }) => ({
|
||||||
components: { BetaBadge },
|
components: { BetaBadge },
|
||||||
|
data() {
|
||||||
|
return { value: args.value };
|
||||||
|
},
|
||||||
|
props: Object.keys(argTypes),
|
||||||
template,
|
template,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Default = Template.bind({});
|
export const Default = Template.bind({});
|
||||||
|
Default.args = {};
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,18 @@ export default {
|
||||||
"BetaBadge|A Beta feature is not production-ready, but is unlikely to change drastically before it's released. We encourage users to try Beta features and provide feedback.",
|
"BetaBadge|A Beta feature is not production-ready, but is unlikely to change drastically before it's released. We encourage users to try Beta features and provide feedback.",
|
||||||
),
|
),
|
||||||
listIntroduction: s__('BetaBadge|A Beta feature:'),
|
listIntroduction: s__('BetaBadge|A Beta feature:'),
|
||||||
listItemStability: s__('BetaBadge|May have performance or stability issues.'),
|
listItemStability: s__('BetaBadge|May be unstable.'),
|
||||||
listItemDataLoss: s__('BetaBadge|Should not cause data loss.'),
|
listItemDataLoss: s__('BetaBadge|Should not cause data loss.'),
|
||||||
listItemReasonableEffort: s__('BetaBadge|Is supported by a commercially reasonable effort.'),
|
listItemReasonableEffort: s__('BetaBadge|Is supported by a commercially reasonable effort.'),
|
||||||
listItemNearCompletion: s__('BetaBadge|Is complete or near completion.'),
|
listItemNearCompletion: s__('BetaBadge|Is complete or near completion.'),
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'md',
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
target() {
|
target() {
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,7 +42,9 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<gl-badge ref="badge" class="gl-cursor-pointer">{{ $options.i18n.badgeLabel }}</gl-badge>
|
<gl-badge ref="badge" href="#" :size="size" variant="neutral" class="gl-cursor-pointer">{{
|
||||||
|
$options.i18n.badgeLabel
|
||||||
|
}}</gl-badge>
|
||||||
<gl-popover
|
<gl-popover
|
||||||
triggers="hover focus click"
|
triggers="hover focus click"
|
||||||
:show-close-button="true"
|
:show-close-button="true"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui';
|
import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui';
|
||||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||||
|
import { InternalEvents } from '~/tracking';
|
||||||
import savedRepliesQuery from './saved_replies.query.graphql';
|
import savedRepliesQuery from './saved_replies.query.graphql';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -18,6 +19,7 @@ export default {
|
||||||
GlButton,
|
GlButton,
|
||||||
GlTooltip,
|
GlTooltip,
|
||||||
},
|
},
|
||||||
|
mixins: [InternalEvents.mixin()],
|
||||||
props: {
|
props: {
|
||||||
newCommentTemplatePath: {
|
newCommentTemplatePath: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -55,6 +57,7 @@ export default {
|
||||||
const savedReply = this.savedReplies.find((r) => r.id === id);
|
const savedReply = this.savedReplies.find((r) => r.id === id);
|
||||||
if (savedReply) {
|
if (savedReply) {
|
||||||
this.$emit('select', savedReply.content);
|
this.$emit('select', savedReply.content);
|
||||||
|
this.track_event('i_code_review_saved_replies_use');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,24 @@ module UsersHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_profile_actions_data(user)
|
||||||
|
basic_actions_data = {
|
||||||
|
user_id: user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if can?(current_user, :read_user_profile, user)
|
||||||
|
basic_actions_data[:rss_subscription_path] = user_path(user, rss_url_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
return basic_actions_data if !current_user || current_user == user
|
||||||
|
|
||||||
|
basic_actions_data.merge(
|
||||||
|
report_abuse_path: add_category_abuse_reports_path,
|
||||||
|
reported_user_id: user.id,
|
||||||
|
reported_from_url: user_url(user)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def admin_users_paths
|
def admin_users_paths
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ module Metrics
|
||||||
|
|
||||||
STAGES = ::Gitlab::Metrics::Dashboard::Stages
|
STAGES = ::Gitlab::Metrics::Dashboard::Stages
|
||||||
SEQUENCE = [
|
SEQUENCE = [
|
||||||
STAGES::PanelIdsInserter,
|
|
||||||
STAGES::TrackPanelType,
|
STAGES::TrackPanelType,
|
||||||
STAGES::UrlValidator
|
STAGES::UrlValidator
|
||||||
].freeze
|
].freeze
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,6 @@ module Metrics
|
||||||
DASHBOARD_PATH = nil
|
DASHBOARD_PATH = nil
|
||||||
DASHBOARD_NAME = nil
|
DASHBOARD_NAME = nil
|
||||||
|
|
||||||
SEQUENCE = [
|
|
||||||
STAGES::PanelIdsInserter
|
|
||||||
].freeze
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def valid_params?(params)
|
def valid_params?(params)
|
||||||
matching_dashboard?(params[:dashboard_path])
|
matching_dashboard?(params[:dashboard_path])
|
||||||
|
|
@ -52,10 +48,6 @@ module Metrics
|
||||||
|
|
||||||
load_yaml(yml)
|
load_yaml(yml)
|
||||||
end
|
end
|
||||||
|
|
||||||
def sequence
|
|
||||||
self.class::SEQUENCE
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ module Metrics
|
||||||
# SHA256 hash of dashboard content
|
# SHA256 hash of dashboard content
|
||||||
DASHBOARD_VERSION = 'ce9ae27d2913f637de851d61099bc4151583eae68b1386a2176339ef6e653223'
|
DASHBOARD_VERSION = 'ce9ae27d2913f637de851d61099bc4151583eae68b1386a2176339ef6e653223'
|
||||||
|
|
||||||
SEQUENCE = [
|
|
||||||
STAGES::PanelIdsInserter
|
|
||||||
].freeze
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def all_dashboard_paths(_project)
|
def all_dashboard_paths(_project)
|
||||||
[{
|
[{
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
= s_("UserProfile|Edit profile")
|
= s_("UserProfile|Edit profile")
|
||||||
= render 'users/view_gpg_keys'
|
= render 'users/view_gpg_keys'
|
||||||
= render 'users/view_user_in_admin_area'
|
= render 'users/view_user_in_admin_area'
|
||||||
.js-user-profile-actions{ data: { user_id: @user.id, rss_subscription_path: can?(current_user, :read_user_profile, @user) ? user_path(@user, rss_url_options) : '' } }
|
.js-user-profile-actions{ data: user_profile_actions_data(@user) }
|
||||||
- else
|
- else
|
||||||
= render layout: 'users/cover_controls' do
|
= render layout: 'users/cover_controls' do
|
||||||
- if @user == current_user
|
- if @user == current_user
|
||||||
|
|
|
||||||
|
|
@ -140,3 +140,4 @@ options:
|
||||||
- 'i_code_review_merge_request_widget_security_reports_expand_warning'
|
- 'i_code_review_merge_request_widget_security_reports_expand_warning'
|
||||||
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
|
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
|
||||||
- 'i_code_review_saved_replies_create'
|
- 'i_code_review_saved_replies_create'
|
||||||
|
- 'i_code_review_saved_replies_use'
|
||||||
|
|
|
||||||
|
|
@ -145,3 +145,4 @@ options:
|
||||||
- 'i_code_review_merge_request_widget_security_reports_expand_warning'
|
- 'i_code_review_merge_request_widget_security_reports_expand_warning'
|
||||||
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
|
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
|
||||||
- 'i_code_review_saved_replies_create'
|
- 'i_code_review_saved_replies_create'
|
||||||
|
- 'i_code_review_saved_replies_use'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
key_path: redis_hll_counters.code_review.i_code_review_saved_replies_use_monthly
|
||||||
|
description: Number of unique users per month who use a saved reply
|
||||||
|
product_section: dev
|
||||||
|
product_stage: create
|
||||||
|
product_group: code_review
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: "16.3"
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127442
|
||||||
|
time_frame: 28d
|
||||||
|
data_source: redis_hll
|
||||||
|
data_category: optional
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
performance_indicator_type: []
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- i_code_review_saved_replies_use
|
||||||
|
|
@ -143,3 +143,4 @@ options:
|
||||||
- 'i_code_review_merge_request_widget_security_reports_expand_warning'
|
- 'i_code_review_merge_request_widget_security_reports_expand_warning'
|
||||||
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
|
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
|
||||||
- 'i_code_review_saved_replies_create'
|
- 'i_code_review_saved_replies_create'
|
||||||
|
- 'i_code_review_saved_replies_use'
|
||||||
|
|
|
||||||
|
|
@ -138,3 +138,4 @@ options:
|
||||||
- 'i_code_review_merge_request_widget_security_reports_expand_warning'
|
- 'i_code_review_merge_request_widget_security_reports_expand_warning'
|
||||||
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
|
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
|
||||||
- 'i_code_review_saved_replies_create'
|
- 'i_code_review_saved_replies_create'
|
||||||
|
- 'i_code_review_saved_replies_use'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
key_path: redis_hll_counters.code_review.i_code_review_saved_replies_use_weekly
|
||||||
|
description: Number of unique users per week who use a saved reply
|
||||||
|
product_section: dev
|
||||||
|
product_stage: create
|
||||||
|
product_group: code_review
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: "16.3"
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127442
|
||||||
|
time_frame: 7d
|
||||||
|
data_source: redis_hll
|
||||||
|
data_category: optional
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
performance_indicator_type: []
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- i_code_review_saved_replies_use
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
key_path: counts.i_code_review_saved_replies_count_use
|
||||||
|
description: Total number of times a saved reply comment was used
|
||||||
|
product_section: dev
|
||||||
|
product_stage: create
|
||||||
|
product_group: code_review
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: "16.3"
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127442
|
||||||
|
time_frame: all
|
||||||
|
data_source: redis
|
||||||
|
data_category: optional
|
||||||
|
instrumentation_class: RedisMetric
|
||||||
|
performance_indicator_type: []
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
options:
|
||||||
|
event: use
|
||||||
|
prefix: i_code_review_saved_replies
|
||||||
|
|
@ -5,12 +5,18 @@ group: Project Management
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
# Rate limits on issue creation **(FREE SELF)**
|
# Rate limits on issue and epic creation **(FREE SELF)**
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28129) in GitLab 12.10.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28129) in GitLab 12.10.
|
||||||
|
|
||||||
This setting allows you to rate limit the requests to the issue and epic creation endpoints.
|
Rate limits control the pace at which new epics and issues can be created.
|
||||||
To can change its value:
|
For example, if you set the limit to `300`, the
|
||||||
|
[Projects::IssuesController#create](https://gitlab.com/gitlab-org/gitlab/blob/master/app/controllers/projects/issues_controller.rb)
|
||||||
|
action blocks requests that exceed a rate of 300 per minute. Access to the endpoint is available after one minute.
|
||||||
|
|
||||||
|
## Set the rate limit
|
||||||
|
|
||||||
|
To limit the number of requests made to the issue and epic creation endpoints:
|
||||||
|
|
||||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||||
1. Select **Admin Area**.
|
1. Select **Admin Area**.
|
||||||
|
|
@ -19,18 +25,12 @@ To can change its value:
|
||||||
1. Under **Max requests per minute**, enter the new value.
|
1. Under **Max requests per minute**, enter the new value.
|
||||||
1. Select **Save changes**.
|
1. Select **Save changes**.
|
||||||
|
|
||||||
For example, if you set a limit of 300, requests using the
|
|
||||||
[Projects::IssuesController#create](https://gitlab.com/gitlab-org/gitlab/blob/master/app/controllers/projects/issues_controller.rb)
|
|
||||||
action exceeding a rate of 300 per minute are blocked. Access to the endpoint is allowed after one minute.
|
|
||||||
|
|
||||||
When using [epics](../../user/group/epics/index.md), epic creation shares this rate limit with issues.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
This limit is:
|
The limit for [epic](../../user/group/epics/index.md) creation is the same limit applied to issue creation. The rate limit:
|
||||||
|
|
||||||
- Applied independently per project and per user.
|
- Is applied independently per project and per user.
|
||||||
- Not applied per IP address.
|
- Is not applied per IP address.
|
||||||
- Disabled by default. To enable it, set the option to any value other than `0`.
|
- Can be set to `0` to disable the rate limit.
|
||||||
|
|
||||||
Requests over the rate limit are logged into the `auth.log` file.
|
Requests over the rate limit are logged into the `auth.log` file.
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Gitlab
|
|
||||||
module Metrics
|
|
||||||
module Dashboard
|
|
||||||
module Stages
|
|
||||||
# Acts on metrics which have been ingested from source controlled dashboards
|
|
||||||
class CustomDashboardMetricsInserter < BaseStage
|
|
||||||
# For each metric in the dashboard config, attempts to
|
|
||||||
# find a corresponding database record. If found, includes
|
|
||||||
# the record's id in the dashboard config.
|
|
||||||
def transform!
|
|
||||||
database_metrics = ::PrometheusMetricsFinder.new(common: false, group: :custom, project: project).execute
|
|
||||||
|
|
||||||
for_metrics do |metric|
|
|
||||||
metric_record = database_metrics.find { |m| m.identifier == metric[:id] }
|
|
||||||
metric[:metric_id] = metric_record.id if metric_record
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -7351,7 +7351,7 @@ msgstr ""
|
||||||
msgid "BetaBadge|Is supported by a commercially reasonable effort."
|
msgid "BetaBadge|Is supported by a commercially reasonable effort."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "BetaBadge|May have performance or stability issues."
|
msgid "BetaBadge|May be unstable."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "BetaBadge|Should not cause data loss."
|
msgid "BetaBadge|Should not cause data loss."
|
||||||
|
|
@ -32794,6 +32794,9 @@ msgstr ""
|
||||||
msgid "Organizations"
|
msgid "Organizations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Organization|An error occurred loading the groups. Please refresh the page to try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Organization|An error occurred loading the projects. Please refresh the page to try again."
|
msgid "Organization|An error occurred loading the projects. Please refresh the page to try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ module QA
|
||||||
raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!"
|
raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!"
|
||||||
end
|
end
|
||||||
|
|
||||||
resource_web_url(api_post)
|
resource_web_url = resource_web_url(api_post)
|
||||||
|
wait_for_resource_availability(resource_web_url)
|
||||||
|
|
||||||
|
resource_web_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def reload!
|
def reload!
|
||||||
|
|
@ -222,6 +225,22 @@ module QA
|
||||||
v.is_a?(Hash) ? a.merge(flatten_hash(v)) : a.merge(k.to_sym => v)
|
v.is_a?(Hash) ? a.merge(flatten_hash(v)) : a.merge(k.to_sym => v)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Given a URL, wait for the given URL to return 200
|
||||||
|
# @param [String] resource_web_url the URL to check
|
||||||
|
# @example
|
||||||
|
# wait_for_resource_availability('https://gitlab.com/api/v4/projects/1234')
|
||||||
|
# @example
|
||||||
|
# wait_for_resource_availability(resource_web_url(Resource::Issue.fabricate_via_api!))
|
||||||
|
def wait_for_resource_availability(resource_web_url)
|
||||||
|
return unless Runtime::Address.valid?(resource_web_url)
|
||||||
|
|
||||||
|
Support::Retrier.retry_until(sleep_interval: 3, max_attempts: 5, raise_on_failure: false) do
|
||||||
|
response_check = get(resource_web_url)
|
||||||
|
Runtime::Logger.debug("Resource availability check ... #{response_check.code}")
|
||||||
|
response_check.code == HTTP_STATUS_OK
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,19 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
|
||||||
before do
|
before do
|
||||||
sign_in(reporter1)
|
sign_in(reporter1)
|
||||||
stub_feature_flags(moved_mr_sidebar: false)
|
stub_feature_flags(moved_mr_sidebar: false)
|
||||||
stub_feature_flags(user_profile_overflow_menu_vue: false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'report abuse to administrator' do
|
describe 'report abuse to administrator' do
|
||||||
|
shared_examples 'cancel report' do
|
||||||
|
it 'redirects backs to user profile when cancel button is clicked' do
|
||||||
|
fill_and_submit_abuse_category_form
|
||||||
|
|
||||||
|
click_link 'Cancel'
|
||||||
|
|
||||||
|
expect(page).to have_current_path(user_path(abusive_user))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when reporting an issue for abuse' do
|
context 'when reporting an issue for abuse' do
|
||||||
before do
|
before do
|
||||||
visit project_issue_path(project, issue)
|
visit project_issue_path(project, issue)
|
||||||
|
|
@ -46,11 +55,64 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
|
||||||
it_behaves_like 'reports the user with an abuse category'
|
it_behaves_like 'reports the user with an abuse category'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'when user_profile_overflow_menu FF turned on' do
|
||||||
context 'when reporting a user profile for abuse' do
|
context 'when reporting a user profile for abuse' do
|
||||||
let_it_be(:reporter2) { create(:user) }
|
let_it_be(:reporter2) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
visit user_path(abusive_user)
|
visit user_path(abusive_user)
|
||||||
|
find('[data-testid="base-dropdown-toggle"').click
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'reports the user with an abuse category'
|
||||||
|
|
||||||
|
it 'allows the reporter to report the same user for different abuse categories' do
|
||||||
|
visit user_path(abusive_user)
|
||||||
|
|
||||||
|
find('[data-testid="base-dropdown-toggle"').click
|
||||||
|
fill_and_submit_abuse_category_form
|
||||||
|
fill_and_submit_report_abuse_form
|
||||||
|
|
||||||
|
expect(page).to have_content 'Thank you for your report'
|
||||||
|
|
||||||
|
visit user_path(abusive_user)
|
||||||
|
|
||||||
|
find('[data-testid="base-dropdown-toggle"').click
|
||||||
|
fill_and_submit_abuse_category_form("They're being offensive or abusive.")
|
||||||
|
fill_and_submit_report_abuse_form
|
||||||
|
|
||||||
|
expect(page).to have_content 'Thank you for your report'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows multiple users to report the same user' do
|
||||||
|
fill_and_submit_abuse_category_form
|
||||||
|
fill_and_submit_report_abuse_form
|
||||||
|
|
||||||
|
expect(page).to have_content 'Thank you for your report'
|
||||||
|
|
||||||
|
gitlab_sign_out
|
||||||
|
gitlab_sign_in(reporter2)
|
||||||
|
|
||||||
|
visit user_path(abusive_user)
|
||||||
|
|
||||||
|
find('[data-testid="base-dropdown-toggle"').click
|
||||||
|
fill_and_submit_abuse_category_form
|
||||||
|
fill_and_submit_report_abuse_form
|
||||||
|
|
||||||
|
expect(page).to have_content 'Thank you for your report'
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'cancel report'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when user_profile_overflow_menu FF turned off' do
|
||||||
|
context 'when reporting a user profile for abuse' do
|
||||||
|
let_it_be(:reporter2) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(user_profile_overflow_menu_vue: false)
|
||||||
|
visit user_path(abusive_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'reports the user with an abuse category'
|
it_behaves_like 'reports the user with an abuse category'
|
||||||
|
|
@ -88,12 +150,7 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
|
||||||
expect(page).to have_content 'Thank you for your report'
|
expect(page).to have_content 'Thank you for your report'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects backs to user profile when cancel button is clicked' do
|
it_behaves_like 'cancel report'
|
||||||
fill_and_submit_abuse_category_form
|
|
||||||
|
|
||||||
click_link 'Cancel'
|
|
||||||
|
|
||||||
expect(page).to have_current_path(user_path(abusive_user))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { GlLoadingIcon } from '@gitlab/ui';
|
||||||
|
import GroupsPage from '~/organizations/groups_and_projects/components/groups_page.vue';
|
||||||
|
import { formatGroups } from '~/organizations/groups_and_projects/utils';
|
||||||
|
import resolvers from '~/organizations/groups_and_projects/graphql/resolvers';
|
||||||
|
import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
|
||||||
|
import { createAlert } from '~/alert';
|
||||||
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import { organizationGroups } from '../mock_data';
|
||||||
|
|
||||||
|
jest.mock('~/alert');
|
||||||
|
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
describe('GroupsPage', () => {
|
||||||
|
let wrapper;
|
||||||
|
let mockApollo;
|
||||||
|
|
||||||
|
const createComponent = ({ mockResolvers = resolvers } = {}) => {
|
||||||
|
mockApollo = createMockApollo([], mockResolvers);
|
||||||
|
|
||||||
|
wrapper = shallowMountExtended(GroupsPage, { apolloProvider: mockApollo });
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockApollo = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when API call is loading', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockResolvers = {
|
||||||
|
Query: {
|
||||||
|
organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
createComponent({ mockResolvers });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders loading icon', () => {
|
||||||
|
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when API call is successful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders `GroupsList` component and passes correct props', async () => {
|
||||||
|
jest.runAllTimers();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(wrapper.findComponent(GroupsList).props()).toEqual({
|
||||||
|
groups: formatGroups(organizationGroups.nodes),
|
||||||
|
showGroupIcon: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when API call is not successful', () => {
|
||||||
|
const error = new Error();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockResolvers = {
|
||||||
|
Query: {
|
||||||
|
organization: jest.fn().mockRejectedValueOnce(error),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
createComponent({ mockResolvers });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error alert', async () => {
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(createAlert).toHaveBeenCalledWith({
|
||||||
|
message: GroupsPage.i18n.errorMessage,
|
||||||
|
error,
|
||||||
|
captureError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -56,7 +56,7 @@ describe('ProjectsPage', () => {
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(wrapper.findComponent(ProjectsList).props()).toEqual({
|
expect(wrapper.findComponent(ProjectsList).props()).toEqual({
|
||||||
projects: formatProjects(organizationProjects.projects.nodes),
|
projects: formatProjects(organizationProjects.nodes),
|
||||||
showProjectIcon: true,
|
showProjectIcon: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
export const organizationProjects = {
|
export const organization = {
|
||||||
id: 'gid://gitlab/Organization/1',
|
id: 'gid://gitlab/Organization/1',
|
||||||
__typename: 'Organization',
|
__typename: 'Organization',
|
||||||
projects: {
|
};
|
||||||
|
|
||||||
|
export const organizationProjects = {
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
id: 'gid://gitlab/Project/8',
|
id: 'gid://gitlab/Project/8',
|
||||||
|
|
@ -94,5 +96,152 @@ export const organizationProjects = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
|
|
||||||
|
export const organizationGroups = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/Group/29',
|
||||||
|
fullName: 'Commit451',
|
||||||
|
parent: null,
|
||||||
|
webUrl: 'http://127.0.0.1:3000/groups/Commit451',
|
||||||
|
descriptionHtml:
|
||||||
|
'<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa.</p>',
|
||||||
|
avatarUrl: null,
|
||||||
|
descendantGroupsCount: 0,
|
||||||
|
projectsCount: 3,
|
||||||
|
groupMembersCount: 2,
|
||||||
|
visibility: 'public',
|
||||||
|
accessLevel: {
|
||||||
|
integerValue: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/Group/33',
|
||||||
|
fullName: 'Flightjs',
|
||||||
|
parent: null,
|
||||||
|
webUrl: 'http://127.0.0.1:3000/groups/flightjs',
|
||||||
|
descriptionHtml:
|
||||||
|
'<p data-sourcepos="1:1-1:60" dir="auto">Ipsa reiciendis deleniti officiis illum nostrum quo aliquam.</p>',
|
||||||
|
avatarUrl: null,
|
||||||
|
descendantGroupsCount: 4,
|
||||||
|
projectsCount: 3,
|
||||||
|
groupMembersCount: 1,
|
||||||
|
visibility: 'private',
|
||||||
|
accessLevel: {
|
||||||
|
integerValue: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/Group/24',
|
||||||
|
fullName: 'Gitlab Org',
|
||||||
|
parent: null,
|
||||||
|
webUrl: 'http://127.0.0.1:3000/groups/gitlab-org',
|
||||||
|
descriptionHtml:
|
||||||
|
'<p data-sourcepos="1:1-1:64" dir="auto">Dolorem dolorem omnis impedit cupiditate pariatur officia velit.</p>',
|
||||||
|
avatarUrl: null,
|
||||||
|
descendantGroupsCount: 1,
|
||||||
|
projectsCount: 1,
|
||||||
|
groupMembersCount: 2,
|
||||||
|
visibility: 'internal',
|
||||||
|
accessLevel: {
|
||||||
|
integerValue: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/Group/27',
|
||||||
|
fullName: 'Gnuwget',
|
||||||
|
parent: null,
|
||||||
|
webUrl: 'http://127.0.0.1:3000/groups/gnuwgetf',
|
||||||
|
descriptionHtml:
|
||||||
|
'<p data-sourcepos="1:1-1:47" dir="auto">Culpa soluta aut eius dolores est vel sapiente.</p>',
|
||||||
|
avatarUrl: null,
|
||||||
|
descendantGroupsCount: 4,
|
||||||
|
projectsCount: 2,
|
||||||
|
groupMembersCount: 3,
|
||||||
|
visibility: 'public',
|
||||||
|
accessLevel: {
|
||||||
|
integerValue: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/Group/31',
|
||||||
|
fullName: 'Jashkenas',
|
||||||
|
parent: null,
|
||||||
|
webUrl: 'http://127.0.0.1:3000/groups/jashkenas',
|
||||||
|
descriptionHtml: '<p data-sourcepos="1:1-1:25" dir="auto">Ut ut id aliquid nostrum.</p>',
|
||||||
|
avatarUrl: null,
|
||||||
|
descendantGroupsCount: 3,
|
||||||
|
projectsCount: 3,
|
||||||
|
groupMembersCount: 10,
|
||||||
|
visibility: 'private',
|
||||||
|
accessLevel: {
|
||||||
|
integerValue: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/Group/22',
|
||||||
|
fullName: 'Toolbox',
|
||||||
|
parent: null,
|
||||||
|
webUrl: 'http://127.0.0.1:3000/groups/toolbox',
|
||||||
|
descriptionHtml:
|
||||||
|
'<p data-sourcepos="1:1-1:46" dir="auto">Quo voluptatem magnam facere voluptates alias.</p>',
|
||||||
|
avatarUrl: null,
|
||||||
|
descendantGroupsCount: 2,
|
||||||
|
projectsCount: 3,
|
||||||
|
groupMembersCount: 40,
|
||||||
|
visibility: 'internal',
|
||||||
|
accessLevel: {
|
||||||
|
integerValue: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/Group/35',
|
||||||
|
fullName: 'Twitter',
|
||||||
|
parent: null,
|
||||||
|
webUrl: 'http://127.0.0.1:3000/groups/twitter',
|
||||||
|
descriptionHtml:
|
||||||
|
'<p data-sourcepos="1:1-1:40" dir="auto">Quae nulla consequatur assumenda id quo.</p>',
|
||||||
|
avatarUrl: null,
|
||||||
|
descendantGroupsCount: 20,
|
||||||
|
projectsCount: 30,
|
||||||
|
groupMembersCount: 100,
|
||||||
|
visibility: 'public',
|
||||||
|
accessLevel: {
|
||||||
|
integerValue: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/Group/73',
|
||||||
|
fullName: 'test',
|
||||||
|
parent: null,
|
||||||
|
webUrl: 'http://127.0.0.1:3000/groups/test',
|
||||||
|
descriptionHtml: '',
|
||||||
|
avatarUrl: null,
|
||||||
|
descendantGroupsCount: 1,
|
||||||
|
projectsCount: 1,
|
||||||
|
groupMembersCount: 1,
|
||||||
|
visibility: 'private',
|
||||||
|
accessLevel: {
|
||||||
|
integerValue: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/Group/74',
|
||||||
|
fullName: 'Twitter / test subgroup',
|
||||||
|
parent: {
|
||||||
|
id: 'gid://gitlab/Group/35',
|
||||||
|
},
|
||||||
|
webUrl: 'http://127.0.0.1:3000/groups/twitter/test-subgroup',
|
||||||
|
descriptionHtml: '',
|
||||||
|
avatarUrl: null,
|
||||||
|
descendantGroupsCount: 4,
|
||||||
|
projectsCount: 4,
|
||||||
|
groupMembersCount: 4,
|
||||||
|
visibility: 'internal',
|
||||||
|
accessLevel: {
|
||||||
|
integerValue: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { formatProjects } from '~/organizations/groups_and_projects/utils';
|
import { formatProjects, formatGroups } from '~/organizations/groups_and_projects/utils';
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
import { organizationProjects } from './mock_data';
|
import { organizationProjects, organizationGroups } from './mock_data';
|
||||||
|
|
||||||
describe('formatProjects', () => {
|
describe('formatProjects', () => {
|
||||||
it('correctly formats the projects', () => {
|
it('correctly formats the projects', () => {
|
||||||
const [firstMockProject] = organizationProjects.projects.nodes;
|
const [firstMockProject] = organizationProjects.nodes;
|
||||||
const formattedProjects = formatProjects(organizationProjects.projects.nodes);
|
const formattedProjects = formatProjects(organizationProjects.nodes);
|
||||||
const [firstFormattedProject] = formattedProjects;
|
const [firstFormattedProject] = formattedProjects;
|
||||||
|
|
||||||
expect(firstFormattedProject).toMatchObject({
|
expect(firstFormattedProject).toMatchObject({
|
||||||
|
|
@ -17,6 +17,17 @@ describe('formatProjects', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(formattedProjects.length).toBe(organizationProjects.projects.nodes.length);
|
expect(formattedProjects.length).toBe(organizationProjects.nodes.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatGroups', () => {
|
||||||
|
it('correctly formats the groups', () => {
|
||||||
|
const [firstMockGroup] = organizationGroups.nodes;
|
||||||
|
const formattedGroups = formatGroups(organizationGroups.nodes);
|
||||||
|
const [firstFormattedGroup] = formattedGroups;
|
||||||
|
|
||||||
|
expect(firstFormattedGroup.id).toBe(getIdFromGraphQLId(firstMockGroup.id));
|
||||||
|
expect(formattedGroups.length).toBe(organizationGroups.nodes.length);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { GlDisclosureDropdown } from '@gitlab/ui';
|
import { GlDisclosureDropdown } from '@gitlab/ui';
|
||||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import UserActionsApp from '~/users/profile/actions/components/user_actions_app.vue';
|
import UserActionsApp from '~/users/profile/actions/components/user_actions_app.vue';
|
||||||
|
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||||
|
|
||||||
describe('User Actions App', () => {
|
describe('User Actions App', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
@ -40,6 +41,15 @@ describe('User Actions App', () => {
|
||||||
});
|
});
|
||||||
expect(findActions()).toHaveLength(2);
|
expect(findActions()).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show items with report abuse', () => {
|
||||||
|
createWrapper({
|
||||||
|
rssSubscriptionPath: '/test/path',
|
||||||
|
reportedUserId: 1,
|
||||||
|
reportedFromUrl: '/report/path',
|
||||||
|
});
|
||||||
|
expect(findActions()).toHaveLength(3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows copy user id action', () => {
|
it('shows copy user id action', () => {
|
||||||
|
|
@ -60,4 +70,21 @@ describe('User Actions App', () => {
|
||||||
expect(rssLink.attributes('href')).toBe(testSubscriptionPath);
|
expect(rssLink.attributes('href')).toBe(testSubscriptionPath);
|
||||||
expect(rssLink.text()).toBe('Subscribe');
|
expect(rssLink.text()).toBe('Subscribe');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows report abuse action when reported user id was presented', () => {
|
||||||
|
const reportUrl = '/path/to/report';
|
||||||
|
const reportUserId = 1;
|
||||||
|
createWrapper({
|
||||||
|
rssSubscriptionPath: '/test/path',
|
||||||
|
reportedUserId: reportUserId,
|
||||||
|
reportedFromUrl: reportUrl,
|
||||||
|
});
|
||||||
|
const abuseCategorySelector = wrapper.findComponent(AbuseCategorySelector);
|
||||||
|
expect(abuseCategorySelector.exists()).toBe(true);
|
||||||
|
expect(abuseCategorySelector.props()).toEqual({
|
||||||
|
reportedUserId: reportUserId,
|
||||||
|
reportedFromUrl: reportUrl,
|
||||||
|
showDrawer: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ exports[`Beta badge component renders the badge 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<gl-badge-stub
|
<gl-badge-stub
|
||||||
class="gl-cursor-pointer"
|
class="gl-cursor-pointer"
|
||||||
|
href="#"
|
||||||
iconsize="md"
|
iconsize="md"
|
||||||
size="md"
|
size="md"
|
||||||
variant="muted"
|
variant="neutral"
|
||||||
>
|
>
|
||||||
Beta
|
Beta
|
||||||
</gl-badge-stub>
|
</gl-badge-stub>
|
||||||
|
|
@ -33,7 +34,7 @@ exports[`Beta badge component renders the badge 1`] = `
|
||||||
class="gl-pl-4"
|
class="gl-pl-4"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
May have performance or stability issues.
|
May be unstable.
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,32 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { GlBadge } from '@gitlab/ui';
|
||||||
import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
|
import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
|
||||||
|
|
||||||
describe('Beta badge component', () => {
|
describe('Beta badge component', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
const findBadge = () => wrapper.findComponent(GlBadge);
|
||||||
|
const createWrapper = (props = {}) => {
|
||||||
|
wrapper = shallowMount(BetaBadge, {
|
||||||
|
propsData: { ...props },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
it('renders the badge', () => {
|
it('renders the badge', () => {
|
||||||
wrapper = shallowMount(BetaBadge);
|
createWrapper();
|
||||||
|
|
||||||
expect(wrapper.element).toMatchSnapshot();
|
expect(wrapper.element).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes default size to badge', () => {
|
||||||
|
createWrapper();
|
||||||
|
|
||||||
|
expect(findBadge().props('size')).toBe('md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes given size to badge', () => {
|
||||||
|
createWrapper({ size: 'sm' });
|
||||||
|
|
||||||
|
expect(findBadge().props('size')).toBe('sm');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { GlCollapsibleListbox } from '@gitlab/ui';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
|
import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
|
||||||
|
import { mockTracking } from 'helpers/tracking_helper';
|
||||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
|
@ -31,6 +33,10 @@ function createComponent(options = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findDropdownComponent() {
|
||||||
|
return wrapper.findComponent(GlCollapsibleListbox);
|
||||||
|
}
|
||||||
|
|
||||||
describe('Comment templates dropdown', () => {
|
describe('Comment templates dropdown', () => {
|
||||||
it('fetches data when dropdown gets opened', async () => {
|
it('fetches data when dropdown gets opened', async () => {
|
||||||
const mockApollo = createMockApolloProvider(savedRepliesResponse);
|
const mockApollo = createMockApolloProvider(savedRepliesResponse);
|
||||||
|
|
@ -43,10 +49,17 @@ describe('Comment templates dropdown', () => {
|
||||||
expect(savedRepliesResp).toHaveBeenCalled();
|
expect(savedRepliesResp).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds emits a select event on selecting a comment', async () => {
|
describe('when selecting a comment', () => {
|
||||||
const mockApollo = createMockApolloProvider(savedRepliesResponse);
|
let trackingSpy;
|
||||||
wrapper = createComponent({ mockApollo });
|
let mockApollo;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
trackingSpy = mockTracking(undefined, window.document, jest.spyOn);
|
||||||
|
mockApollo = createMockApolloProvider(savedRepliesResponse);
|
||||||
|
wrapper = createComponent({ mockApollo });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits a select event', async () => {
|
||||||
wrapper.find('.js-comment-template-toggle').trigger('click');
|
wrapper.find('.js-comment-template-toggle').trigger('click');
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
@ -55,4 +68,23 @@ describe('Comment templates dropdown', () => {
|
||||||
|
|
||||||
expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']);
|
expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tracks the usage of the saved comment', async () => {
|
||||||
|
const dropdown = findDropdownComponent();
|
||||||
|
|
||||||
|
dropdown.vm.$emit('shown');
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
dropdown.vm.$emit('select', savedRepliesResponse.data.currentUser.savedReplies.nodes[0].id);
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(trackingSpy).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'i_code_review_saved_replies_use',
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -682,4 +682,58 @@ RSpec.describe UsersHelper do
|
||||||
it { is_expected.to eq('Active') }
|
it { is_expected.to eq('Active') }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#user_profile_actions_data' do
|
||||||
|
let(:user_1) { create(:user) }
|
||||||
|
let(:user_2) { create(:user) }
|
||||||
|
let(:user_path) { '/users/root' }
|
||||||
|
|
||||||
|
subject { helper.user_profile_actions_data(user_1) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:user_path).and_return(user_path)
|
||||||
|
allow(helper).to receive(:user_url).and_return(user_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'user cannot report' do
|
||||||
|
it 'returns data without reporting related data' do
|
||||||
|
is_expected.to match({
|
||||||
|
user_id: user_1.id,
|
||||||
|
rss_subscription_path: user_path
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'user is current user' do
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:current_user).and_return(user_1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'user cannot report'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'user is not current user' do
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:current_user).and_return(user_2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns data for reporting related data' do
|
||||||
|
is_expected.to match({
|
||||||
|
user_id: user_1.id,
|
||||||
|
rss_subscription_path: user_path,
|
||||||
|
report_abuse_path: add_category_abuse_reports_path,
|
||||||
|
reported_user_id: user_1.id,
|
||||||
|
reported_from_url: user_path
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logged out' do
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:current_user).and_return(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'user cannot report'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do
|
||||||
describe 'process' do
|
describe 'process' do
|
||||||
let(:sequence) do
|
let(:sequence) do
|
||||||
[
|
[
|
||||||
Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter,
|
|
||||||
Gitlab::Metrics::Dashboard::Stages::UrlValidator
|
Gitlab::Metrics::Dashboard::Stages::UrlValidator
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -20,12 +19,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do
|
||||||
let(:process_params) { [project, dashboard_yml, sequence, { environment: environment }] }
|
let(:process_params) { [project, dashboard_yml, sequence, { environment: environment }] }
|
||||||
let(:dashboard) { described_class.new(*process_params).process }
|
let(:dashboard) { described_class.new(*process_params).process }
|
||||||
|
|
||||||
it 'includes an id for each dashboard panel' do
|
|
||||||
expect(all_panels).to satisfy_all do |panel|
|
|
||||||
panel[:id].present?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the dashboard is not present' do
|
context 'when the dashboard is not present' do
|
||||||
let(:dashboard_yml) { nil }
|
let(:dashboard_yml) { nil }
|
||||||
|
|
||||||
|
|
@ -33,69 +26,5 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do
|
||||||
expect(dashboard).to be_nil
|
expect(dashboard).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples_for 'errors with message' do |expected_message|
|
|
||||||
it 'raises a DashboardLayoutError' do
|
|
||||||
error_class = Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError
|
|
||||||
|
|
||||||
expect { dashboard }.to raise_error(error_class, expected_message)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the dashboard is missing panel_groups' do
|
|
||||||
let(:dashboard_yml) { {} }
|
|
||||||
|
|
||||||
it_behaves_like 'errors with message', 'Top-level key :panel_groups must be an array'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the dashboard contains a panel_group which is missing panels' do
|
|
||||||
let(:dashboard_yml) { { panel_groups: [{}] } }
|
|
||||||
|
|
||||||
it_behaves_like 'errors with message', 'Each "panel_group" must define an array :panels'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def all_metrics
|
|
||||||
all_panels.flat_map { |panel| panel[:metrics] }
|
|
||||||
end
|
|
||||||
|
|
||||||
def all_panels
|
|
||||||
dashboard[:panel_groups].flat_map { |group| group[:panels] }
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_metric_details(metric)
|
|
||||||
{
|
|
||||||
query_range: metric.query,
|
|
||||||
unit: metric.unit,
|
|
||||||
label: metric.legend,
|
|
||||||
metric_id: metric.id,
|
|
||||||
edit_path: edit_metric_path(metric)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def prometheus_path(query)
|
|
||||||
Gitlab::Routing.url_helpers.prometheus_api_project_environment_path(
|
|
||||||
project,
|
|
||||||
environment,
|
|
||||||
proxy_path: :query_range,
|
|
||||||
query: query
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def sample_metrics_path(metric)
|
|
||||||
Gitlab::Routing.url_helpers.sample_metrics_project_environment_path(
|
|
||||||
project,
|
|
||||||
environment,
|
|
||||||
identifier: metric
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def edit_metric_path(metric)
|
|
||||||
Gitlab::Routing.url_helpers.edit_project_prometheus_metric_path(
|
|
||||||
project,
|
|
||||||
metric.id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
RSpec.describe Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter do
|
|
||||||
include MetricsDashboardHelpers
|
|
||||||
|
|
||||||
let(:project) { build_stubbed(:project) }
|
|
||||||
|
|
||||||
def fetch_panel_ids(dashboard_hash)
|
|
||||||
dashboard_hash[:panel_groups].flat_map { |group| group[:panels].flat_map { |panel| panel[:id] } }
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#transform!' do
|
|
||||||
subject(:transform!) { described_class.new(project, dashboard, nil).transform! }
|
|
||||||
|
|
||||||
let(:dashboard) { load_sample_dashboard.deep_symbolize_keys }
|
|
||||||
|
|
||||||
context 'when dashboard panels are present' do
|
|
||||||
it 'assigns unique ids to each panel using PerformanceMonitoring::PrometheusPanel', :aggregate_failures do
|
|
||||||
dashboard.fetch(:panel_groups).each do |group|
|
|
||||||
group.fetch(:panels).each do |panel|
|
|
||||||
panel_double = instance_double(::PerformanceMonitoring::PrometheusPanel)
|
|
||||||
|
|
||||||
expect(::PerformanceMonitoring::PrometheusPanel).to receive(:new).with(panel).and_return(panel_double)
|
|
||||||
expect(panel_double).to receive(:id).with(group[:group]).and_return(FFaker::Lorem.unique.characters(125))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
transform!
|
|
||||||
|
|
||||||
expect(fetch_panel_ids(dashboard)).not_to include nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when dashboard panels has duplicated ids' do
|
|
||||||
it 'no panel has assigned id' do
|
|
||||||
panel_double = instance_double(::PerformanceMonitoring::PrometheusPanel)
|
|
||||||
allow(::PerformanceMonitoring::PrometheusPanel).to receive(:new).and_return(panel_double)
|
|
||||||
allow(panel_double).to receive(:id).and_return('duplicated id')
|
|
||||||
|
|
||||||
transform!
|
|
||||||
|
|
||||||
expect(fetch_panel_ids(dashboard)).to all be_nil
|
|
||||||
expect(fetch_panel_ids(dashboard)).not_to include 'duplicated id'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are no panels in the dashboard' do
|
|
||||||
it 'raises a processing error' do
|
|
||||||
dashboard[:panel_groups][0].delete(:panels)
|
|
||||||
|
|
||||||
expect { transform! }.to(
|
|
||||||
raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are no panel_groups in the dashboard' do
|
|
||||||
it 'raises a processing error' do
|
|
||||||
dashboard.delete(:panel_groups)
|
|
||||||
|
|
||||||
expect { transform! }.to(
|
|
||||||
raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when dashboard panels has unknown schema attributes' do
|
|
||||||
before do
|
|
||||||
error = ActiveModel::UnknownAttributeError.new(double, 'unknown_panel_attribute')
|
|
||||||
allow(::PerformanceMonitoring::PrometheusPanel).to receive(:new).and_raise(error)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'no panel has assigned id' do
|
|
||||||
transform!
|
|
||||||
|
|
||||||
expect(fetch_panel_ids(dashboard)).to all be_nil
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'logs the failure' do
|
|
||||||
expect(Gitlab::ErrorTracking).to receive(:log_exception)
|
|
||||||
|
|
||||||
transform!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Loading…
Reference in New Issue