Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7b0ffedd69
commit
1b3eb6f388
|
|
@ -230,7 +230,7 @@ export default {
|
|||
:aria-label="__('Toggle sidebar')"
|
||||
category="primary"
|
||||
variant="default"
|
||||
class="d-sm-none position-absolute toggle-sidebar-mobile-button"
|
||||
class="d-sm-none gl-absolute toggle-sidebar-mobile-button"
|
||||
type="button"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
<script>
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import SidebarHeader from './sidebar/sidebar_header.vue';
|
||||
import SidebarTodo from './sidebar/sidebar_todo.vue';
|
||||
import SidebarStatus from './sidebar/sidebar_status.vue';
|
||||
import SidebarAssignees from './sidebar/sidebar_assignees.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SidebarAssignees,
|
||||
SidebarHeader,
|
||||
SidebarTodo,
|
||||
SidebarStatus,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
sidebarCollapsed: {
|
||||
type: Boolean,
|
||||
|
|
@ -28,11 +32,6 @@ export default {
|
|||
return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleAlertSidebarError(errorMessage) {
|
||||
this.$emit('alert-sidebar-error', errorMessage);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -48,7 +47,14 @@ export default {
|
|||
:project-path="projectPath"
|
||||
:alert="alert"
|
||||
@toggle-sidebar="$emit('toggle-sidebar')"
|
||||
@alert-sidebar-error="handleAlertSidebarError"
|
||||
@alert-sidebar-error="$emit('alert-sidebar-error', $event)"
|
||||
/>
|
||||
<sidebar-assignees
|
||||
v-if="glFeatures.alertAssignee"
|
||||
:project-path="projectPath"
|
||||
:alert="alert"
|
||||
@toggle-sidebar="$emit('toggle-sidebar')"
|
||||
@alert-sidebar-error="$emit('alert-sidebar-error', $event)"
|
||||
/>
|
||||
<!-- TODO: Remove after adding extra attribute blocks to sidebar -->
|
||||
<div class="block"></div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
<script>
|
||||
import { GlDropdownItem } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdownItem,
|
||||
},
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isActive(name) {
|
||||
return this.alert.assignees.nodes.some(({ username }) => username === name);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown-item
|
||||
:key="user.username"
|
||||
data-testid="assigneeDropdownItem"
|
||||
class="assignee-dropdown-item gl-vertical-align-middle"
|
||||
:active="active"
|
||||
active-class="is-active"
|
||||
@click="$emit('update-alert-assignees', user.username)"
|
||||
>
|
||||
<span class="gl-relative mr-2">
|
||||
<img
|
||||
:alt="user.username"
|
||||
:src="user.avatar_url"
|
||||
:width="32"
|
||||
class="avatar avatar-inline gl-m-0 s32"
|
||||
data-qa-selector="avatar_image"
|
||||
/>
|
||||
</span>
|
||||
<span class="d-flex gl-flex-direction-column gl-overflow-hidden">
|
||||
<strong class="dropdown-menu-user-full-name">
|
||||
{{ user.name }}
|
||||
</strong>
|
||||
<span class="dropdown-menu-user-username"> {{ user.username }}</span>
|
||||
</span>
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
<script>
|
||||
import {
|
||||
GlIcon,
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownHeader,
|
||||
GlDropdownItem,
|
||||
GlLoadingIcon,
|
||||
GlTooltip,
|
||||
GlButton,
|
||||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { s__ } from '~/locale';
|
||||
import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.graphql';
|
||||
import SidebarAssignee from './sidebar_assignee.vue';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const DATA_REFETCH_DELAY = 250;
|
||||
|
||||
export default {
|
||||
FETCH_USERS_ERROR: s__(
|
||||
'AlertManagement|There was an error while updating the assignee(s) list. Please try again.',
|
||||
),
|
||||
UPDATE_ALERT_ASSIGNEES_ERROR: s__(
|
||||
'AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again.',
|
||||
),
|
||||
components: {
|
||||
GlIcon,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
GlDropdownHeader,
|
||||
GlLoadingIcon,
|
||||
GlTooltip,
|
||||
GlButton,
|
||||
GlSprintf,
|
||||
SidebarAssignee,
|
||||
},
|
||||
props: {
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
alert: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isEditable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDropdownShowing: false,
|
||||
isDropdownSearching: false,
|
||||
isUpdating: false,
|
||||
search: '',
|
||||
users: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
assignedUsers() {
|
||||
return this.alert.assignees.nodes.length > 0
|
||||
? this.alert.assignees.nodes[0].username
|
||||
: s__('AlertManagement|Unassigned');
|
||||
},
|
||||
dropdownClass() {
|
||||
return this.isDropdownShowing ? 'show' : 'gl-display-none';
|
||||
},
|
||||
userListValid() {
|
||||
return !this.isDropdownSearching && this.users.length > 0;
|
||||
},
|
||||
userListEmpty() {
|
||||
return !this.isDropdownSearching && this.users.length === 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search: debounce(function debouncedUserSearch() {
|
||||
this.updateAssigneesDropdown();
|
||||
}, DATA_REFETCH_DELAY),
|
||||
},
|
||||
mounted() {
|
||||
this.updateAssigneesDropdown();
|
||||
},
|
||||
methods: {
|
||||
hideDropdown() {
|
||||
this.isDropdownShowing = false;
|
||||
},
|
||||
toggleFormDropdown() {
|
||||
this.isDropdownShowing = !this.isDropdownShowing;
|
||||
const { dropdown } = this.$refs.dropdown.$refs;
|
||||
if (dropdown && this.isDropdownShowing) {
|
||||
dropdown.show();
|
||||
}
|
||||
},
|
||||
isActive(name) {
|
||||
return this.alert.assignees.nodes.some(({ username }) => username === name);
|
||||
},
|
||||
buildUrl(urlRoot, url) {
|
||||
let newUrl;
|
||||
if (urlRoot != null) {
|
||||
newUrl = urlRoot.replace(/\/$/, '') + url;
|
||||
}
|
||||
return newUrl;
|
||||
},
|
||||
updateAssigneesDropdown() {
|
||||
this.isDropdownSearching = true;
|
||||
return axios
|
||||
.get(this.buildUrl(gon.relative_url_root, '/autocomplete/users.json'), {
|
||||
params: {
|
||||
search: this.search,
|
||||
per_page: 20,
|
||||
active: true,
|
||||
current_user: true,
|
||||
project_id: gon.current_project_id,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.users = data;
|
||||
})
|
||||
.catch(() => {
|
||||
this.$emit('alert-sidebar-error', this.$options.FETCH_USERS_ERROR);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isDropdownSearching = false;
|
||||
});
|
||||
},
|
||||
updateAlertAssignees(assignees) {
|
||||
this.isUpdating = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: alertSetAssignees,
|
||||
variables: {
|
||||
iid: this.alert.iid,
|
||||
assigneeUsernames: [this.isActive(assignees) ? '' : assignees],
|
||||
projectPath: this.projectPath,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.hideDropdown();
|
||||
})
|
||||
.catch(() => {
|
||||
this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isUpdating = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="block alert-status">
|
||||
<div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
|
||||
<gl-icon name="user" :size="14" />
|
||||
<gl-loading-icon v-if="isUpdating" />
|
||||
<p v-else class="collapse-truncated-title px-1">{{ assignedUsers }}</p>
|
||||
</div>
|
||||
<gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
|
||||
<gl-sprintf :message="s__('AlertManagement|Alert assignee(s): %{assignees}')">
|
||||
<template #assignees>
|
||||
{{ assignedUsers }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-tooltip>
|
||||
|
||||
<div class="hide-collapsed">
|
||||
<p class="title gl-display-flex gl-justify-content-space-between">
|
||||
{{ s__('AlertManagement|Assignee') }}
|
||||
<a
|
||||
v-if="isEditable"
|
||||
ref="editButton"
|
||||
class="btn-link"
|
||||
href="#"
|
||||
@click="toggleFormDropdown"
|
||||
@keydown.esc="hideDropdown"
|
||||
>
|
||||
{{ s__('AlertManagement|Edit') }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
|
||||
<gl-dropdown
|
||||
ref="dropdown"
|
||||
:text="assignedUsers"
|
||||
class="w-100"
|
||||
toggle-class="dropdown-menu-toggle"
|
||||
variant="outline-default"
|
||||
@keydown.esc.native="hideDropdown"
|
||||
@hide="hideDropdown"
|
||||
>
|
||||
<div class="dropdown-title">
|
||||
<span class="alert-title">{{ s__('AlertManagement|Assign Assignees') }}</span>
|
||||
<gl-button
|
||||
:aria-label="__('Close')"
|
||||
variant="link"
|
||||
class="dropdown-title-button dropdown-menu-close"
|
||||
icon="close"
|
||||
@click="hideDropdown"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropdown-input">
|
||||
<input
|
||||
v-model.trim="search"
|
||||
class="dropdown-input-field"
|
||||
type="search"
|
||||
:placeholder="__('Search users')"
|
||||
/>
|
||||
<gl-icon name="search" class="dropdown-input-search ic-search" data-hidden="true" />
|
||||
</div>
|
||||
<div class="dropdown-content dropdown-body">
|
||||
<template v-if="userListValid">
|
||||
<gl-dropdown-item @click="updateAlertAssignees('')">
|
||||
{{ s__('AlertManagement|Unassigned') }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
|
||||
<gl-dropdown-header class="mt-0">
|
||||
{{ s__('AlertManagement|Assignee(s)') }}
|
||||
</gl-dropdown-header>
|
||||
|
||||
<template v-for="user in users">
|
||||
<sidebar-assignee
|
||||
v-if="isActive(user.username)"
|
||||
:key="user.username"
|
||||
:user="user"
|
||||
:active="true"
|
||||
@update-alert-assignees="updateAlertAssignees"
|
||||
/>
|
||||
</template>
|
||||
<gl-dropdown-divider />
|
||||
<template v-for="user in users">
|
||||
<sidebar-assignee
|
||||
v-if="!isActive(user.username)"
|
||||
:key="user.username"
|
||||
:user="user"
|
||||
:active="false"
|
||||
@update-alert-assignees="updateAlertAssignees"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<gl-dropdown-item v-else-if="userListEmpty">
|
||||
{{ s__('AlertManagement|No Matching Results') }}
|
||||
</gl-dropdown-item>
|
||||
<gl-loading-icon v-else />
|
||||
</div>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
|
||||
<gl-loading-icon v-if="isUpdating" :inline="true" />
|
||||
<p
|
||||
v-else-if="!isDropdownShowing"
|
||||
class="value gl-m-0"
|
||||
:class="{ 'no-value': !alert.assignees.nodes }"
|
||||
>
|
||||
<span v-if="alert.assignees.nodes" class="gl-text-gray-700" data-testid="assigned-users">{{
|
||||
assignedUsers
|
||||
}}</span>
|
||||
<span v-else>
|
||||
{{ s__('AlertManagement|None') }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -17,13 +17,13 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="block">
|
||||
<span class="issuable-header-text hide-collapsed float-left">
|
||||
<div class="block d-flex justify-content-between">
|
||||
<span class="issuable-header-text hide-collapsed">
|
||||
{{ __('Quick actions') }}
|
||||
</span>
|
||||
<toggle-sidebar
|
||||
:collapsed="sidebarCollapsed"
|
||||
css-classes="float-right"
|
||||
css-classes="ml-auto"
|
||||
@toggle="$emit('toggle-sidebar')"
|
||||
/>
|
||||
<!-- TODO: Implement after or as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/215946 -->
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
dropdownClass() {
|
||||
return this.isDropdownShowing ? 'show' : 'd-none';
|
||||
return this.isDropdownShowing ? 'show' : 'gl-display-none';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -81,7 +81,6 @@ export default {
|
|||
})
|
||||
.then(() => {
|
||||
this.trackStatusUpdate(status);
|
||||
|
||||
this.hideDropdown();
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -172,12 +171,15 @@ export default {
|
|||
<gl-loading-icon v-if="isUpdating" :inline="true" />
|
||||
<p
|
||||
v-else-if="!isDropdownShowing"
|
||||
class="value m-0"
|
||||
class="value gl-m-0"
|
||||
:class="{ 'no-value': !$options.statuses[alert.status] }"
|
||||
>
|
||||
<span v-if="$options.statuses[alert.status]" class="gl-text-gray-700">{{
|
||||
$options.statuses[alert.status]
|
||||
}}</span>
|
||||
<span
|
||||
v-if="$options.statuses[alert.status]"
|
||||
class="gl-text-gray-700"
|
||||
data-testid="status"
|
||||
>{{ $options.statuses[alert.status] }}</span
|
||||
>
|
||||
<span v-else>
|
||||
{{ s__('AlertManagement|None') }}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
mutation($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
|
||||
alertSetAssignees(
|
||||
input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $projectPath }
|
||||
) {
|
||||
errors
|
||||
alert {
|
||||
iid
|
||||
assignees {
|
||||
nodes {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
export default {
|
||||
getAlertManagementList({ endpoint }) {
|
||||
return axios.get(endpoint);
|
||||
},
|
||||
};
|
||||
|
|
@ -49,4 +49,15 @@
|
|||
background-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.assignee-dropdown-item {
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
top: 50% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class Clusters::BaseController < ApplicationController
|
|||
before_action :authorize_read_cluster!
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:managed_apps_local_tiller)
|
||||
push_frontend_feature_flag(:managed_apps_local_tiller, clusterable)
|
||||
end
|
||||
|
||||
helper_method :clusterable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module AlertManagement
|
||||
module Alerts
|
||||
class SetAssignees < Base
|
||||
graphql_name 'AlertSetAssignees'
|
||||
|
||||
argument :assignee_usernames,
|
||||
[GraphQL::STRING_TYPE],
|
||||
required: true,
|
||||
description: 'The usernames to assign to the alert. Replaces existing assignees by default.'
|
||||
|
||||
argument :operation_mode,
|
||||
Types::MutationOperationModeEnum,
|
||||
required: false,
|
||||
description: 'The operation to perform. Defaults to REPLACE.'
|
||||
|
||||
def resolve(args)
|
||||
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
|
||||
result = set_assignees(alert, args[:assignee_usernames], args[:operation_mode])
|
||||
|
||||
prepare_response(result)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_assignees(alert, assignee_usernames, operation_mode)
|
||||
operation_mode ||= Types::MutationOperationModeEnum.enum[:replace]
|
||||
|
||||
original_assignees = alert.assignees
|
||||
target_users = find_target_users(assignee_usernames)
|
||||
|
||||
assignees = case Types::MutationOperationModeEnum.enum.key(operation_mode).to_sym
|
||||
when :replace then target_users.uniq
|
||||
when :append then (original_assignees + target_users).uniq
|
||||
when :remove then (original_assignees - target_users)
|
||||
end
|
||||
|
||||
::AlertManagement::Alerts::UpdateService.new(alert, current_user, assignees: assignees).execute
|
||||
end
|
||||
|
||||
def find_target_users(assignee_usernames)
|
||||
UsersFinder.new(current_user, username: assignee_usernames).execute
|
||||
end
|
||||
|
||||
def prepare_response(result)
|
||||
{
|
||||
alert: result.payload[:alert],
|
||||
errors: result.error? ? [result.message] : []
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -9,6 +9,7 @@ module Types
|
|||
mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs
|
||||
mount_mutation Mutations::AlertManagement::CreateAlertIssue
|
||||
mount_mutation Mutations::AlertManagement::UpdateAlertStatus
|
||||
mount_mutation Mutations::AlertManagement::Alerts::SetAssignees
|
||||
mount_mutation Mutations::AwardEmojis::Add
|
||||
mount_mutation Mutations::AwardEmojis::Remove
|
||||
mount_mutation Mutations::AwardEmojis::Toggle
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ module Clusters
|
|||
chart: chart,
|
||||
files: files.merge(cluster_issuer_file),
|
||||
preinstall: pre_install_script,
|
||||
postinstall: post_install_script
|
||||
postinstall: post_install_script,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -47,7 +48,8 @@ module Clusters
|
|||
name: 'certmanager',
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
files: files,
|
||||
postdelete: post_delete_script
|
||||
postdelete: post_delete_script,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ module Clusters
|
|||
version: VERSION,
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
files: files
|
||||
files: files,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ module Clusters
|
|||
repository: repository,
|
||||
files: files,
|
||||
preinstall: migrate_to_3_script,
|
||||
postinstall: post_install_script
|
||||
postinstall: post_install_script,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -43,7 +44,8 @@ module Clusters
|
|||
name: 'elastic-stack',
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
files: files,
|
||||
postdelete: post_delete_script
|
||||
postdelete: post_delete_script,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -118,7 +120,8 @@ module Clusters
|
|||
Gitlab::Kubernetes::Helm::DeleteCommand.new(
|
||||
name: 'elastic-stack',
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
files: files
|
||||
files: files,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
).delete_command,
|
||||
Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack", "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ module Clusters
|
|||
version: VERSION,
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
files: files
|
||||
files: files,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ module Clusters
|
|||
Gitlab::Kubernetes::Helm::InitCommand.new(
|
||||
name: name,
|
||||
files: files,
|
||||
rbac: cluster.platform_kubernetes_rbac?
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -60,7 +61,8 @@ module Clusters
|
|||
Gitlab::Kubernetes::Helm::ResetCommand.new(
|
||||
name: name,
|
||||
files: files,
|
||||
rbac: cluster.platform_kubernetes_rbac?
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ module Clusters
|
|||
version: VERSION,
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
files: files
|
||||
files: files,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ module Clusters
|
|||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
files: files,
|
||||
repository: repository
|
||||
repository: repository,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ module Clusters
|
|||
chart: chart,
|
||||
files: files,
|
||||
repository: REPOSITORY,
|
||||
postinstall: install_knative_metrics
|
||||
postinstall: install_knative_metrics,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -99,7 +100,8 @@ module Clusters
|
|||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
files: files,
|
||||
predelete: delete_knative_services_and_metrics,
|
||||
postdelete: delete_knative_istio_leftovers
|
||||
postdelete: delete_knative_istio_leftovers,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ module Clusters
|
|||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
files: files,
|
||||
postinstall: install_knative_metrics
|
||||
postinstall: install_knative_metrics,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -76,7 +77,8 @@ module Clusters
|
|||
version: version,
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
files: files_with_replaced_values(values)
|
||||
files: files_with_replaced_values(values),
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -85,7 +87,8 @@ module Clusters
|
|||
name: name,
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
files: files,
|
||||
predelete: delete_knative_istio_metrics
|
||||
predelete: delete_knative_istio_metrics,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ module Clusters
|
|||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
files: files,
|
||||
repository: repository
|
||||
repository: repository,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -326,6 +326,10 @@ module Clusters
|
|||
end
|
||||
end
|
||||
|
||||
def local_tiller_enabled?
|
||||
Feature.enabled?(:managed_apps_local_tiller, clusterable, default_enabled: false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unique_management_project_environment_scope
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ module Clusters
|
|||
def set_initial_status
|
||||
return unless not_installable?
|
||||
|
||||
self.status = status_states[:installable] if cluster&.application_helm_available? || ::Gitlab::Kubernetes::Helm.local_tiller_enabled?
|
||||
self.status = status_states[:installable] if cluster&.application_helm_available? || cluster&.local_tiller_enabled?
|
||||
end
|
||||
|
||||
def can_uninstall?
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ module Clusters
|
|||
Gitlab::Kubernetes::Helm::DeleteCommand.new(
|
||||
name: name,
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
files: files
|
||||
files: files,
|
||||
local_tiller_enabled: cluster.local_tiller_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ module Clusters
|
|||
private
|
||||
|
||||
def use_tiller_ssl?
|
||||
return false if ::Gitlab::Kubernetes::Helm.local_tiller_enabled?
|
||||
return false if cluster.local_tiller_enabled?
|
||||
|
||||
cluster.application_helm.has_ssl?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ module Clusters
|
|||
end
|
||||
|
||||
before_transition any => [:installed, :updated] do |application, transition|
|
||||
unless ::Gitlab::Kubernetes::Helm.local_tiller_enabled? || application.is_a?(Clusters::Applications::Helm)
|
||||
unless application.cluster.local_tiller_enabled? || application.is_a?(Clusters::Applications::Helm)
|
||||
if transition.event == :make_externally_installed
|
||||
# If an application is externally installed
|
||||
# We assume the helm application is externally installed too
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AlertManagement
|
||||
module Alerts
|
||||
class UpdateService
|
||||
# @param alert [AlertManagement::Alert]
|
||||
# @param current_user [User]
|
||||
# @param params [Hash] Attributes of the alert
|
||||
def initialize(alert, current_user, params)
|
||||
@alert = alert
|
||||
@current_user = current_user
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
return error_no_permissions unless allowed?
|
||||
return error_no_updates if params.empty?
|
||||
|
||||
filter_assignees
|
||||
|
||||
if alert.update(params)
|
||||
success
|
||||
else
|
||||
error(alert.errors.full_messages.to_sentence)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :alert, :current_user, :params
|
||||
|
||||
def allowed?
|
||||
current_user.can?(:update_alert_management_alert, alert)
|
||||
end
|
||||
|
||||
def filter_assignees
|
||||
return if params[:assignees].nil?
|
||||
|
||||
# Take first assignee while multiple are not currently supported
|
||||
params[:assignees] = Array(params[:assignees].first)
|
||||
end
|
||||
|
||||
def success
|
||||
ServiceResponse.success(payload: { alert: alert })
|
||||
end
|
||||
|
||||
def error(message)
|
||||
ServiceResponse.error(payload: { alert: alert }, message: message)
|
||||
end
|
||||
|
||||
def error_no_permissions
|
||||
error(_('You have no permissions'))
|
||||
end
|
||||
|
||||
def error_no_updates
|
||||
error(_('Please provide attributes to update'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow the assignment of alerts to users from the alert detail view
|
||||
merge_request: 33122
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -495,6 +495,61 @@ enum AlertManagementStatus {
|
|||
TRIGGERED
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of AlertSetAssignees
|
||||
"""
|
||||
input AlertSetAssigneesInput {
|
||||
"""
|
||||
The usernames to assign to the alert. Replaces existing assignees by default.
|
||||
"""
|
||||
assigneeUsernames: [String!]!
|
||||
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
The iid of the alert to mutate
|
||||
"""
|
||||
iid: String!
|
||||
|
||||
"""
|
||||
The operation to perform. Defaults to REPLACE.
|
||||
"""
|
||||
operationMode: MutationOperationMode
|
||||
|
||||
"""
|
||||
The project the alert to mutate is in
|
||||
"""
|
||||
projectPath: ID!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of AlertSetAssignees
|
||||
"""
|
||||
type AlertSetAssigneesPayload {
|
||||
"""
|
||||
The alert after mutation
|
||||
"""
|
||||
alert: AlertManagementAlert
|
||||
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Errors encountered during execution of the mutation.
|
||||
"""
|
||||
errors: [String!]!
|
||||
|
||||
"""
|
||||
The issue created after mutation
|
||||
"""
|
||||
issue: Issue
|
||||
}
|
||||
|
||||
"""
|
||||
An emoji awarded by a user.
|
||||
"""
|
||||
|
|
@ -7451,6 +7506,7 @@ type Mutation {
|
|||
addAwardEmoji(input: AddAwardEmojiInput!): AddAwardEmojiPayload
|
||||
addProjectToSecurityDashboard(input: AddProjectToSecurityDashboardInput!): AddProjectToSecurityDashboardPayload
|
||||
adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload
|
||||
alertSetAssignees(input: AlertSetAssigneesInput!): AlertSetAssigneesPayload
|
||||
boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload
|
||||
commitCreate(input: CommitCreateInput!): CommitCreatePayload
|
||||
createAlertIssue(input: CreateAlertIssueInput!): CreateAlertIssuePayload
|
||||
|
|
|
|||
|
|
@ -1175,6 +1175,168 @@
|
|||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AlertSetAssigneesInput",
|
||||
"description": "Autogenerated input type of AlertSetAssignees",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "projectPath",
|
||||
"description": "The project the alert to mutate is in",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "iid",
|
||||
"description": "The iid of the alert to mutate",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsernames",
|
||||
"description": "The usernames to assign to the alert. Replaces existing assignees by default.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "operationMode",
|
||||
"description": "The operation to perform. Defaults to REPLACE.",
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "MutationOperationMode",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AlertSetAssigneesPayload",
|
||||
"description": "Autogenerated return type of AlertSetAssignees",
|
||||
"fields": [
|
||||
{
|
||||
"name": "alert",
|
||||
"description": "The alert after mutation",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AlertManagementAlert",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "errors",
|
||||
"description": "Errors encountered during execution of the mutation.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "issue",
|
||||
"description": "The issue created after mutation",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Issue",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AwardEmoji",
|
||||
|
|
@ -20963,6 +21125,33 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "alertSetAssignees",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AlertSetAssigneesInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AlertSetAssigneesPayload",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "boardListUpdateLimitMetrics",
|
||||
"description": null,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,17 @@ Represents total number of alerts for the represented categories
|
|||
| `resolved` | Int | Number of alerts with status RESOLVED for the project |
|
||||
| `triggered` | Int | Number of alerts with status TRIGGERED for the project |
|
||||
|
||||
## AlertSetAssigneesPayload
|
||||
|
||||
Autogenerated return type of AlertSetAssignees
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `alert` | AlertManagementAlert | The alert after mutation |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `issue` | Issue | The issue created after mutation |
|
||||
|
||||
## AwardEmoji
|
||||
|
||||
An emoji awarded by a user.
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ To enable pipelines for merged results for your project:
|
|||
1. [Configure your CI/CD configuration file](../index.md#configuring-pipelines-for-merge-requests)
|
||||
so that the pipeline or individual jobs run for merge requests.
|
||||
1. Visit your project's **Settings > General** and expand **Merge requests**.
|
||||
1. Check **Merge pipelines will try to validate the post-merge result prior to merging**.
|
||||
1. Check **Enable merge trains and pipelines for merged results**.
|
||||
1. Click **Save changes**.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ To enable merge trains for your project:
|
|||
1. [Configure your CI/CD configuration file](../../index.md#configuring-pipelines-for-merge-requests)
|
||||
so that the pipeline or individual jobs run for merge requests.
|
||||
1. Visit your project's **Settings > General** and expand **Merge requests**.
|
||||
1. Check **Merge pipelines will try to validate the post-merge result prior to merging**.
|
||||
1. Check **Enable merge trains and pipelines for merged results**.
|
||||
1. Click **Save changes**.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
|
|
@ -187,7 +187,7 @@ run a new successful pipeline before you can re-add a merge request to a merge t
|
|||
|
||||
Merge trains ensure that each pipeline has succeeded before a merge happens, so
|
||||
you can clear the **Pipelines must succeed** check box and keep
|
||||
**Merge pipelines will try to validate the post-merge result prior to merging** (merge trains) enabled.
|
||||
**Enable merge trains and pipelines for merged results** (merge trains) enabled.
|
||||
|
||||
If you want to keep the **Pipelines must succeed** option enabled along with Merge
|
||||
Trains, you can create a new pipeline for merged results when this error occurs by
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ projects.
|
|||
|
||||
## Cluster precedence
|
||||
|
||||
GitLab will try [to match](../../../ci/environments/index.md#scoping-environments-with-specs) clusters in
|
||||
the following order:
|
||||
GitLab will try to match clusters in the following order:
|
||||
|
||||
- Project-level clusters.
|
||||
- Group-level clusters.
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
|
|||
- **VPC** - Select a [VPC](https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html)
|
||||
to use for your EKS Cluster resources.
|
||||
- **Subnets** - Choose the [subnets](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Subnets.html)
|
||||
in your VPC where your worker nodes will run.
|
||||
in your VPC where your worker nodes will run. You must select at least two.
|
||||
- **Security group** - Choose the [security group](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html)
|
||||
to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.
|
||||
- **Instance type** - The [instance type](https://aws.amazon.com/ec2/instance-types/) of your worker nodes.
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ requirements are met:
|
|||
Starting from [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/issues/25925), all the GKE clusters
|
||||
provisioned by GitLab are [VPC-native](https://cloud.google.com/kubernetes-engine/docs/how-to/alias-ips).
|
||||
|
||||
### Important notes
|
||||
|
||||
Note the following:
|
||||
|
||||
- The [Google authentication integration](../../../integration/google.md) must be enabled in GitLab
|
||||
|
|
@ -38,9 +36,10 @@ Note the following:
|
|||
cluster's pod address IP range will be set to /16 instead of the regular /14. /16 is a CIDR
|
||||
notation.
|
||||
- GitLab requires basic authentication enabled and a client certificate issued for the cluster to
|
||||
set up an [initial service account](add_remove_clusters.md#access-controls). Starting from [GitLab
|
||||
11.10](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/58208), the cluster creation process will
|
||||
explicitly request that basic authentication and client certificate is enabled.
|
||||
set up an [initial service account](add_remove_clusters.md#access-controls). In [GitLab versions
|
||||
11.10 and later](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/58208), the cluster creation process
|
||||
explicitly requests GKE to create clusters with basic authentication enabled and a client
|
||||
certificate.
|
||||
|
||||
### Creating the cluster on GKE
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,6 @@ module Gitlab
|
|||
SERVICE_ACCOUNT = 'tiller'
|
||||
CLUSTER_ROLE_BINDING = 'tiller-admin'
|
||||
CLUSTER_ROLE = 'cluster-admin'
|
||||
|
||||
MANAGED_APPS_LOCAL_TILLER_FEATURE_FLAG = :managed_apps_local_tiller
|
||||
|
||||
def self.local_tiller_enabled?
|
||||
Feature.enabled?(MANAGED_APPS_LOCAL_TILLER_FEATURE_FLAG)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,24 @@
|
|||
module Gitlab
|
||||
module Kubernetes
|
||||
module Helm
|
||||
module BaseCommand
|
||||
class BaseCommand
|
||||
attr_reader :name, :files
|
||||
|
||||
def initialize(rbac:, name:, files:, local_tiller_enabled:)
|
||||
@rbac = rbac
|
||||
@name = name
|
||||
@files = files
|
||||
@local_tiller_enabled = local_tiller_enabled
|
||||
end
|
||||
|
||||
def rbac?
|
||||
@rbac
|
||||
end
|
||||
|
||||
def local_tiller_enabled?
|
||||
@local_tiller_enabled
|
||||
end
|
||||
|
||||
def pod_resource
|
||||
pod_service_account_name = rbac? ? service_account_name : nil
|
||||
|
||||
|
|
@ -46,18 +63,6 @@ module Gitlab
|
|||
files.keys
|
||||
end
|
||||
|
||||
def name
|
||||
raise "Not implemented"
|
||||
end
|
||||
|
||||
def rbac?
|
||||
raise "Not implemented"
|
||||
end
|
||||
|
||||
def files
|
||||
raise "Not implemented"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def files_dir
|
||||
|
|
|
|||
|
|
@ -57,10 +57,6 @@ module Gitlab
|
|||
'--tls-key', "#{files_dir}/key.pem"
|
||||
]
|
||||
end
|
||||
|
||||
def local_tiller_enabled?
|
||||
::Gitlab::Kubernetes::Helm.local_tiller_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,17 +3,13 @@
|
|||
module Gitlab
|
||||
module Kubernetes
|
||||
module Helm
|
||||
class DeleteCommand
|
||||
include BaseCommand
|
||||
class DeleteCommand < BaseCommand
|
||||
include ClientCommand
|
||||
|
||||
attr_reader :predelete, :postdelete
|
||||
attr_accessor :name, :files
|
||||
|
||||
def initialize(name:, rbac:, files:, predelete: nil, postdelete: nil)
|
||||
@name = name
|
||||
@files = files
|
||||
@rbac = rbac
|
||||
def initialize(predelete: nil, postdelete: nil, **args)
|
||||
super(**args)
|
||||
@predelete = predelete
|
||||
@postdelete = postdelete
|
||||
end
|
||||
|
|
@ -32,10 +28,6 @@ module Gitlab
|
|||
"uninstall-#{name}"
|
||||
end
|
||||
|
||||
def rbac?
|
||||
@rbac
|
||||
end
|
||||
|
||||
def delete_command
|
||||
command = ['helm', 'delete', '--purge', name] + tls_flags_if_remote_tiller
|
||||
|
||||
|
|
|
|||
|
|
@ -3,27 +3,13 @@
|
|||
module Gitlab
|
||||
module Kubernetes
|
||||
module Helm
|
||||
class InitCommand
|
||||
include BaseCommand
|
||||
|
||||
attr_reader :name, :files
|
||||
|
||||
def initialize(name:, files:, rbac:)
|
||||
@name = name
|
||||
@files = files
|
||||
@rbac = rbac
|
||||
end
|
||||
|
||||
class InitCommand < BaseCommand
|
||||
def generate_script
|
||||
super + [
|
||||
init_helm_command
|
||||
].join("\n")
|
||||
end
|
||||
|
||||
def rbac?
|
||||
@rbac
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_helm_command
|
||||
|
|
|
|||
|
|
@ -3,19 +3,16 @@
|
|||
module Gitlab
|
||||
module Kubernetes
|
||||
module Helm
|
||||
class InstallCommand
|
||||
include BaseCommand
|
||||
class InstallCommand < BaseCommand
|
||||
include ClientCommand
|
||||
|
||||
attr_reader :name, :files, :chart, :repository, :preinstall, :postinstall
|
||||
attr_reader :chart, :repository, :preinstall, :postinstall
|
||||
attr_accessor :version
|
||||
|
||||
def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil)
|
||||
@name = name
|
||||
def initialize(chart:, version: nil, repository: nil, preinstall: nil, postinstall: nil, **args)
|
||||
super(**args)
|
||||
@chart = chart
|
||||
@version = version
|
||||
@rbac = rbac
|
||||
@files = files
|
||||
@repository = repository
|
||||
@preinstall = preinstall
|
||||
@postinstall = postinstall
|
||||
|
|
@ -33,10 +30,6 @@ module Gitlab
|
|||
].compact.join("\n")
|
||||
end
|
||||
|
||||
def rbac?
|
||||
@rbac
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Uses `helm upgrade --install` which means we can use this for both
|
||||
|
|
|
|||
|
|
@ -5,23 +5,21 @@
|
|||
module Gitlab
|
||||
module Kubernetes
|
||||
module Helm
|
||||
class PatchCommand
|
||||
include BaseCommand
|
||||
class PatchCommand < BaseCommand
|
||||
include ClientCommand
|
||||
|
||||
attr_reader :name, :files, :chart, :repository
|
||||
attr_reader :chart, :repository
|
||||
attr_accessor :version
|
||||
|
||||
def initialize(name:, chart:, files:, rbac:, version:, repository: nil)
|
||||
def initialize(chart:, version:, repository: nil, **args)
|
||||
super(**args)
|
||||
|
||||
# version is mandatory to prevent chart mismatches
|
||||
# we do not want our values interpreted in the context of the wrong version
|
||||
raise ArgumentError, 'version is required' if version.blank?
|
||||
|
||||
@name = name
|
||||
@chart = chart
|
||||
@version = version
|
||||
@rbac = rbac
|
||||
@files = files
|
||||
@repository = repository
|
||||
end
|
||||
|
||||
|
|
@ -35,10 +33,6 @@ module Gitlab
|
|||
].compact.join("\n")
|
||||
end
|
||||
|
||||
def rbac?
|
||||
@rbac
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def upgrade_command
|
||||
|
|
|
|||
|
|
@ -3,18 +3,9 @@
|
|||
module Gitlab
|
||||
module Kubernetes
|
||||
module Helm
|
||||
class ResetCommand
|
||||
include BaseCommand
|
||||
class ResetCommand < BaseCommand
|
||||
include ClientCommand
|
||||
|
||||
attr_reader :name, :files
|
||||
|
||||
def initialize(name:, rbac:, files:)
|
||||
@name = name
|
||||
@files = files
|
||||
@rbac = rbac
|
||||
end
|
||||
|
||||
def generate_script
|
||||
super + [
|
||||
reset_helm_command,
|
||||
|
|
@ -23,10 +14,6 @@ module Gitlab
|
|||
].join("\n")
|
||||
end
|
||||
|
||||
def rbac?
|
||||
@rbac
|
||||
end
|
||||
|
||||
def pod_name
|
||||
"uninstall-#{name}"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1846,6 +1846,9 @@ msgstr ""
|
|||
msgid "AlertManagement|Alert"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|Alert assignee(s): %{assignees}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|Alert detail"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1861,9 +1864,18 @@ msgstr ""
|
|||
msgid "AlertManagement|All alerts"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|Assign Assignees"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|Assign status"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|Assignee"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|Assignee(s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|Assignees"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1903,6 +1915,9 @@ msgstr ""
|
|||
msgid "AlertManagement|More information"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|No Matching Results"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|No alert data to display."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1951,6 +1966,12 @@ msgstr ""
|
|||
msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|There was an error while updating the assignee(s) list. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|There was an error while updating the status of the alert. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16318,6 +16339,9 @@ msgstr ""
|
|||
msgid "Please provide a valid email address."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please provide attributes to update"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17221,6 +17245,9 @@ msgstr ""
|
|||
msgid "ProjectSettings|Enable 'Delete source branch' option by default"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Enable merge trains and pipelines for merged results"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Every merge creates a merge commit"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17287,9 +17314,6 @@ msgstr ""
|
|||
msgid "ProjectSettings|Merge options"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Merge pipelines will try to validate the post-merge result prior to merging"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Merge requests"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17317,6 +17341,9 @@ msgstr ""
|
|||
msgid "ProjectSettings|Pipelines"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Pipelines for merge requests must be enabled in the CI/CD configuration file, or pipelines could be unresolvable or dropped"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Pipelines must succeed"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ module QA
|
|||
extend Forwardable
|
||||
|
||||
attr_reader :title
|
||||
attr_accessor :expires_at
|
||||
|
||||
attribute :id
|
||||
|
||||
|
|
@ -53,13 +54,27 @@ module QA
|
|||
def api_post_body
|
||||
{
|
||||
title: title,
|
||||
key: public_key
|
||||
key: public_key,
|
||||
expires_at: expires_at
|
||||
}
|
||||
end
|
||||
|
||||
def api_delete_path
|
||||
"/user/keys/#{id}"
|
||||
end
|
||||
|
||||
def replicated?
|
||||
api_client = Runtime::API::Client.new(:geo_secondary)
|
||||
|
||||
QA::Runtime::Logger.debug('Checking for SSH key replication')
|
||||
|
||||
Support::Retrier.retry_until(max_duration: QA::EE::Runtime::Geo.max_db_replication_time, sleep_interval: 3) do
|
||||
response = get Runtime::API::Request.new(api_client, api_get_path).url
|
||||
|
||||
response.code == QA::Support::Api::HTTP_STATUS_OK &&
|
||||
parse_body(response)[:title].include?(title)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
context 'Monitor', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217705', type: :flaky } do
|
||||
describe 'with Prometheus Gitlab-managed cluster', :orchestrated, :kubernetes, :docker, :runner do
|
||||
context 'Monitor' do
|
||||
describe 'with Prometheus in a Gitlab-managed cluster', :orchestrated, :kubernetes do
|
||||
before :all do
|
||||
Flow::Login.sign_in
|
||||
@project, @runner = deploy_project_with_prometheus
|
||||
@cluster = Service::KubernetesCluster.new.create!
|
||||
@project = Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'monitoring-project'
|
||||
project.auto_devops_enabled = true
|
||||
end
|
||||
|
||||
deploy_project_with_prometheus
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
@ -14,7 +19,6 @@ module QA
|
|||
end
|
||||
|
||||
after :all do
|
||||
@runner.remove_via_api!
|
||||
@cluster.remove!
|
||||
end
|
||||
|
||||
|
|
@ -27,81 +31,94 @@ module QA
|
|||
it 'duplicates to create dashboard to custom' do
|
||||
Page::Project::Menu.perform(&:go_to_operations_metrics)
|
||||
|
||||
Page::Project::Operations::Metrics::Show.perform do |dashboard|
|
||||
dashboard.duplicate_dashboard
|
||||
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
|
||||
on_dashboard.duplicate_dashboard
|
||||
|
||||
expect(dashboard).to have_metrics
|
||||
expect(dashboard).to have_edit_dashboard_enabled
|
||||
expect(on_dashboard).to have_metrics
|
||||
expect(on_dashboard).to have_edit_dashboard_enabled
|
||||
end
|
||||
end
|
||||
|
||||
it 'verifies data on filtered deployed environment' do
|
||||
Page::Project::Menu.perform(&:go_to_operations_metrics)
|
||||
|
||||
Page::Project::Operations::Metrics::Show.perform do |dashboard|
|
||||
dashboard.filter_environment
|
||||
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
|
||||
on_dashboard.filter_environment
|
||||
|
||||
expect(dashboard).to have_metrics
|
||||
expect(on_dashboard).to have_metrics
|
||||
end
|
||||
end
|
||||
|
||||
it 'filters using the quick range' do
|
||||
Page::Project::Menu.perform(&:go_to_operations_metrics)
|
||||
|
||||
Page::Project::Operations::Metrics::Show.perform do |dashboard|
|
||||
dashboard.show_last('30 minutes')
|
||||
expect(dashboard).to have_metrics
|
||||
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
|
||||
on_dashboard.show_last('30 minutes')
|
||||
expect(on_dashboard).to have_metrics
|
||||
|
||||
dashboard.show_last('3 hours')
|
||||
expect(dashboard).to have_metrics
|
||||
on_dashboard.show_last('3 hours')
|
||||
expect(on_dashboard).to have_metrics
|
||||
|
||||
dashboard.show_last('1 day')
|
||||
expect(dashboard).to have_metrics
|
||||
on_dashboard.show_last('1 day')
|
||||
expect(on_dashboard).to have_metrics
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def deploy_project_with_prometheus
|
||||
project = Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'cluster-with-prometheus'
|
||||
project.description = 'Cluster with Prometheus'
|
||||
%w[
|
||||
CODE_QUALITY_DISABLED TEST_DISABLED LICENSE_MANAGEMENT_DISABLED
|
||||
SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED
|
||||
CONTAINER_SCANNING_DISABLED PERFORMANCE_DISABLED
|
||||
].each do |key|
|
||||
Resource::CiVariable.fabricate_via_api! do |resource|
|
||||
resource.project = @project
|
||||
resource.key = key
|
||||
resource.value = '1'
|
||||
resource.masked = false
|
||||
end
|
||||
end
|
||||
|
||||
runner = Resource::Runner.fabricate_via_api! do |runner|
|
||||
runner.project = project
|
||||
runner.name = project.name
|
||||
end
|
||||
Flow::Login.sign_in
|
||||
|
||||
@cluster = Service::KubernetesCluster.new.create!
|
||||
|
||||
cluster_props = Resource::KubernetesCluster::ProjectCluster.fabricate! do |cluster_settings|
|
||||
cluster_settings.project = project
|
||||
Resource::KubernetesCluster::ProjectCluster.fabricate! do |cluster_settings|
|
||||
cluster_settings.project = @project
|
||||
cluster_settings.cluster = @cluster
|
||||
cluster_settings.install_helm_tiller = true
|
||||
cluster_settings.install_runner = true
|
||||
cluster_settings.install_ingress = true
|
||||
cluster_settings.install_prometheus = true
|
||||
end
|
||||
|
||||
Resource::CiVariable.fabricate_via_api! do |ci_variable|
|
||||
ci_variable.project = project
|
||||
ci_variable.key = 'AUTO_DEVOPS_DOMAIN'
|
||||
ci_variable.value = cluster_props.ingress_ip
|
||||
ci_variable.masked = false
|
||||
end
|
||||
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
push.project = project
|
||||
push.project = @project
|
||||
push.directory = Pathname
|
||||
.new(__dir__)
|
||||
.join('../../../../fixtures/monitored_auto_devops')
|
||||
.join('../../../../fixtures/auto_devops_rack')
|
||||
push.commit_message = 'Create AutoDevOps compatible Project for Monitoring'
|
||||
end
|
||||
|
||||
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
|
||||
Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success_or_retry)
|
||||
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
|
||||
|
||||
[project, runner]
|
||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||
pipeline.click_job('build')
|
||||
end
|
||||
Page::Project::Job::Show.perform do |job|
|
||||
expect(job).to be_successful(timeout: 600)
|
||||
|
||||
job.click_element(:pipeline_path)
|
||||
end
|
||||
|
||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||
pipeline.click_job('production')
|
||||
end
|
||||
Page::Project::Job::Show.perform do |job|
|
||||
expect(job).to be_successful(timeout: 1200)
|
||||
|
||||
job.click_element(:pipeline_path)
|
||||
end
|
||||
end
|
||||
|
||||
def verify_add_custom_metric
|
||||
|
|
@ -115,8 +132,8 @@ module QA
|
|||
|
||||
Page::Project::Menu.perform(&:go_to_operations_metrics)
|
||||
|
||||
Page::Project::Operations::Metrics::Show.perform do |dashboard|
|
||||
expect(dashboard).to have_custom_metric('HTTP Requests Total')
|
||||
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
|
||||
expect(on_dashboard).to have_custom_metric('HTTP Requests Total')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -130,8 +147,8 @@ module QA
|
|||
|
||||
Page::Project::Menu.perform(&:go_to_operations_metrics)
|
||||
|
||||
Page::Project::Operations::Metrics::Show.perform do |dashboard|
|
||||
expect(dashboard).to have_custom_metric('Throughput')
|
||||
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
|
||||
expect(on_dashboard).to have_custom_metric('Throughput')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -146,8 +163,8 @@ module QA
|
|||
|
||||
Page::Project::Menu.perform(&:go_to_operations_metrics)
|
||||
|
||||
Page::Project::Operations::Metrics::Show.perform do |dashboard|
|
||||
expect(dashboard).not_to have_custom_metric('Throughput')
|
||||
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
|
||||
expect(on_dashboard).not_to have_custom_metric('Throughput')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import AlertDetails from '~/alert_management/components/alert_details.vue';
|
||||
import createIssueQuery from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
|
|
@ -14,6 +16,7 @@ const mockAlert = mockAlerts[0];
|
|||
|
||||
describe('AlertDetails', () => {
|
||||
let wrapper;
|
||||
let mock;
|
||||
const projectPath = 'root/alerts';
|
||||
const projectIssuesPath = 'root/alerts/-/issues';
|
||||
|
||||
|
|
@ -43,12 +46,17 @@ describe('AlertDetails', () => {
|
|||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
}
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
const findCreateIssueBtn = () => wrapper.find('[data-testid="createIssueBtn"]');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { GlDropdownItem } from '@gitlab/ui';
|
||||
import SidebarAssignee from '~/alert_management/components/sidebar/sidebar_assignee.vue';
|
||||
import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
|
||||
import AlertSetAssignees from '~/alert_management/graphql/mutations/alert_set_assignees.graphql';
|
||||
import mockAlerts from '../mocks/alerts.json';
|
||||
|
||||
const mockAlert = mockAlerts[0];
|
||||
|
||||
describe('Alert Details Sidebar Assignees', () => {
|
||||
let wrapper;
|
||||
let mock;
|
||||
|
||||
function mountComponent({
|
||||
data,
|
||||
users = [],
|
||||
isDropdownSearching = false,
|
||||
sidebarCollapsed = true,
|
||||
loading = false,
|
||||
stubs = {},
|
||||
} = {}) {
|
||||
wrapper = shallowMount(SidebarAssignees, {
|
||||
data() {
|
||||
return {
|
||||
users,
|
||||
isDropdownSearching,
|
||||
};
|
||||
},
|
||||
propsData: {
|
||||
alert: { ...mockAlert },
|
||||
...data,
|
||||
sidebarCollapsed,
|
||||
projectPath: 'projectPath',
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
mutate: jest.fn(),
|
||||
queries: {
|
||||
alert: {
|
||||
loading,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
stubs,
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('updating the alert status', () => {
|
||||
const mockUpdatedMutationResult = {
|
||||
data: {
|
||||
updateAlertStatus: {
|
||||
errors: [],
|
||||
alert: {
|
||||
assigneeUsernames: ['root'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
const path = '/autocomplete/users.json';
|
||||
const users = [
|
||||
{
|
||||
avatar_url:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
id: 1,
|
||||
name: 'User 1',
|
||||
username: 'root',
|
||||
},
|
||||
{
|
||||
avatar_url:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
id: 2,
|
||||
name: 'User 2',
|
||||
username: 'not-root',
|
||||
},
|
||||
];
|
||||
|
||||
mock.onGet(path).replyOnce(200, users);
|
||||
mountComponent({
|
||||
data: { alert: mockAlert },
|
||||
sidebarCollapsed: false,
|
||||
loading: false,
|
||||
users,
|
||||
stubs: {
|
||||
SidebarAssignee,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a unassigned option', () => {
|
||||
wrapper.setData({ isDropdownSearching: false });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.find(GlDropdownItem).text()).toBe('Unassigned');
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', () => {
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
|
||||
wrapper.setData({ isDropdownSearching: false });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: AlertSetAssignees,
|
||||
variables: {
|
||||
iid: '1527542',
|
||||
assigneeUsernames: ['root'],
|
||||
projectPath: 'projectPath',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('stops updating and cancels loading when the request fails', () => {
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
|
||||
wrapper.vm.updateAlertAssignees('root');
|
||||
expect(wrapper.find('[data-testid="assigned-users"]').text()).toBe('Unassigned');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,20 +1,32 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import AlertSidebar from '~/alert_management/components/alert_sidebar.vue';
|
||||
import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
|
||||
import mockAlerts from '../mocks/alerts.json';
|
||||
|
||||
const mockAlert = mockAlerts[0];
|
||||
|
||||
describe('Alert Details Sidebar', () => {
|
||||
let wrapper;
|
||||
let mock;
|
||||
|
||||
function mountComponent({
|
||||
sidebarCollapsed = true,
|
||||
mountMethod = shallowMount,
|
||||
alertAssignee = false,
|
||||
stubs = {},
|
||||
alert = {},
|
||||
} = {}) {
|
||||
wrapper = mountMethod(AlertSidebar, {
|
||||
propsData: {
|
||||
alert: {},
|
||||
alert,
|
||||
sidebarCollapsed,
|
||||
projectPath: 'projectPath',
|
||||
},
|
||||
provide: {
|
||||
glFeatures: { alertAssignee },
|
||||
},
|
||||
stubs,
|
||||
});
|
||||
}
|
||||
|
|
@ -23,15 +35,30 @@ describe('Alert Details Sidebar', () => {
|
|||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('the sidebar renders', () => {
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('open as default', () => {
|
||||
expect(wrapper.props('sidebarCollapsed')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not render side bar assignee dropdown by default', () => {
|
||||
expect(wrapper.find(SidebarAssignees).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render side bar assignee dropdown if feature flag enabled', () => {
|
||||
mountComponent({
|
||||
mountMethod: mount,
|
||||
alertAssignee: true,
|
||||
alert: mockAlert,
|
||||
});
|
||||
expect(wrapper.find(SidebarAssignees).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,14 +13,8 @@ describe('Alert Details Sidebar Status', () => {
|
|||
const findStatusDropdownItem = () => wrapper.find(GlDropdownItem);
|
||||
const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
|
||||
function mountComponent({
|
||||
data,
|
||||
sidebarCollapsed = true,
|
||||
loading = false,
|
||||
mountMethod = shallowMount,
|
||||
stubs = {},
|
||||
} = {}) {
|
||||
wrapper = mountMethod(AlertSidebarStatus, {
|
||||
function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) {
|
||||
wrapper = shallowMount(AlertSidebarStatus, {
|
||||
propsData: {
|
||||
alert: { ...mockAlert },
|
||||
...data,
|
||||
|
|
@ -85,7 +79,7 @@ describe('Alert Details Sidebar Status', () => {
|
|||
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
|
||||
findStatusDropdownItem().vm.$emit('click');
|
||||
expect(findStatusLoadingIcon().exists()).toBe(false);
|
||||
expect(wrapper.find('.gl-text-gray-700').text()).toBe('Triggered');
|
||||
expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Mutations::AlertManagement::Alerts::SetAssignees do
|
||||
let_it_be(:starting_assignee) { create(:user) }
|
||||
let_it_be(:unassigned_user) { create(:user) }
|
||||
let_it_be(:alert) { create(:alert_management_alert, assignees: [starting_assignee]) }
|
||||
let_it_be(:project) { alert.project }
|
||||
|
||||
let(:current_user) { starting_assignee }
|
||||
let(:assignee_usernames) { [unassigned_user.username] }
|
||||
let(:operation_mode) { nil }
|
||||
|
||||
let(:args) do
|
||||
{
|
||||
project_path: project.full_path,
|
||||
iid: alert.iid,
|
||||
assignee_usernames: assignee_usernames,
|
||||
operation_mode: operation_mode
|
||||
}
|
||||
end
|
||||
|
||||
before_all do
|
||||
project.add_developer(starting_assignee)
|
||||
project.add_developer(unassigned_user)
|
||||
end
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) }
|
||||
|
||||
describe '#resolve' do
|
||||
let(:expected_assignees) { [unassigned_user] }
|
||||
|
||||
subject(:resolve) { mutation_for(project, current_user).resolve(args) }
|
||||
|
||||
shared_examples 'successful resolution' do
|
||||
after do
|
||||
alert.assignees = [starting_assignee]
|
||||
end
|
||||
|
||||
it 'successfully resolves' do
|
||||
expect(resolve).to eq(alert: alert.reload, errors: [])
|
||||
expect(alert.assignees).to eq(expected_assignees)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'noop' do
|
||||
it 'makes no changes' do
|
||||
original_assignees = alert.assignees
|
||||
|
||||
expect(resolve).to eq(alert: alert.reload, errors: [])
|
||||
expect(alert.assignees).to eq(original_assignees)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when operation mode is not specified' do
|
||||
it_behaves_like 'successful resolution'
|
||||
end
|
||||
|
||||
context 'when user does not have permission to update alerts' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it 'raises an error if the resource is not accessible to the user' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for APPEND operation' do
|
||||
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
|
||||
|
||||
# Only allow a single assignee
|
||||
context 'when a different user is already assigned' do
|
||||
it_behaves_like 'noop'
|
||||
end
|
||||
|
||||
context 'when no users are specified' do
|
||||
let(:assignee_usernames) { [] }
|
||||
|
||||
it_behaves_like 'noop'
|
||||
end
|
||||
|
||||
context 'when a user is specified and no user is assigned' do
|
||||
before do
|
||||
alert.assignees = []
|
||||
end
|
||||
|
||||
it_behaves_like 'successful resolution'
|
||||
end
|
||||
|
||||
context 'when the specified user is already assigned to the alert' do
|
||||
let(:assignee_usernames) { [starting_assignee.username] }
|
||||
|
||||
it_behaves_like 'noop'
|
||||
end
|
||||
end
|
||||
|
||||
context 'for REPLACE operation' do
|
||||
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:replace] }
|
||||
|
||||
context 'when a different user is already assigned' do
|
||||
it_behaves_like 'successful resolution'
|
||||
end
|
||||
|
||||
context 'when no users are specified' do
|
||||
let(:assignee_usernames) { [] }
|
||||
let(:expected_assignees) { [] }
|
||||
|
||||
it_behaves_like 'successful resolution'
|
||||
end
|
||||
|
||||
context 'when a user is specified and no user is assigned' do
|
||||
before do
|
||||
alert.assignees = []
|
||||
end
|
||||
|
||||
it_behaves_like 'successful resolution'
|
||||
end
|
||||
|
||||
context 'when the specified user is already assigned to the alert' do
|
||||
let(:assignee_usernames) { [starting_assignee.username] }
|
||||
|
||||
it_behaves_like 'noop'
|
||||
end
|
||||
|
||||
context 'when multiple users are specified' do
|
||||
let(:assignees) { [starting_assignee, unassigned_user] }
|
||||
let(:assignee_usernames) { assignees.map(&:username) }
|
||||
let(:expected_assignees) { [assignees.last] }
|
||||
|
||||
it_behaves_like 'successful resolution'
|
||||
end
|
||||
end
|
||||
|
||||
context 'for REMOVE operation' do
|
||||
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
|
||||
|
||||
context 'when a different user is already assigned' do
|
||||
it_behaves_like 'noop'
|
||||
end
|
||||
|
||||
context 'when no users are specified' do
|
||||
let(:assignee_usernames) { [] }
|
||||
|
||||
it_behaves_like 'noop'
|
||||
end
|
||||
|
||||
context 'when a user is specified and no user is assigned' do
|
||||
before do
|
||||
alert.assignees = []
|
||||
end
|
||||
|
||||
it_behaves_like 'noop'
|
||||
end
|
||||
|
||||
context 'when the specified user is already assigned to the alert' do
|
||||
let(:assignee_usernames) { [starting_assignee.username] }
|
||||
let(:expected_assignees) { [] }
|
||||
|
||||
it_behaves_like 'successful resolution'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
@ -17,7 +17,8 @@ describe Gitlab::Kubernetes::Helm::API do
|
|||
name: application_name,
|
||||
chart: 'chart-name',
|
||||
rbac: rbac,
|
||||
files: files
|
||||
files: files,
|
||||
local_tiller_enabled: true
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -142,7 +143,7 @@ describe Gitlab::Kubernetes::Helm::API do
|
|||
end
|
||||
|
||||
context 'with a service account' do
|
||||
let(:command) { Gitlab::Kubernetes::Helm::InitCommand.new(name: application_name, files: files, rbac: rbac) }
|
||||
let(:command) { Gitlab::Kubernetes::Helm::InitCommand.new(name: application_name, files: files, rbac: rbac, local_tiller_enabled: true) }
|
||||
|
||||
context 'rbac-enabled cluster' do
|
||||
let(:rbac) { true }
|
||||
|
|
|
|||
|
|
@ -11,25 +11,14 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do
|
|||
let(:rbac) { false }
|
||||
|
||||
let(:test_class) do
|
||||
Class.new do
|
||||
include Gitlab::Kubernetes::Helm::BaseCommand
|
||||
|
||||
Class.new(Gitlab::Kubernetes::Helm::BaseCommand) do
|
||||
def initialize(rbac)
|
||||
@rbac = rbac
|
||||
end
|
||||
|
||||
def name
|
||||
"test-class-name"
|
||||
end
|
||||
|
||||
def rbac?
|
||||
@rbac
|
||||
end
|
||||
|
||||
def files
|
||||
{
|
||||
some: 'value'
|
||||
}
|
||||
super(
|
||||
name: 'test-class-name',
|
||||
rbac: rbac,
|
||||
files: { some: 'value' },
|
||||
local_tiller_enabled: false
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Kubernetes::Helm::DeleteCommand do
|
||||
subject(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) }
|
||||
subject(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files, local_tiller_enabled: local_tiller_enabled) }
|
||||
|
||||
let(:app_name) { 'app-name' }
|
||||
let(:rbac) { true }
|
||||
let(:files) { {} }
|
||||
let(:local_tiller_enabled) { true }
|
||||
|
||||
it_behaves_like 'helm command generator' do
|
||||
let(:commands) do
|
||||
|
|
@ -21,9 +22,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
|
|||
end
|
||||
|
||||
context 'tillerless feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(managed_apps_local_tiller: false)
|
||||
end
|
||||
let(:local_tiller_enabled) { false }
|
||||
|
||||
it_behaves_like 'helm command generator' do
|
||||
let(:commands) do
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Kubernetes::Helm::InitCommand do
|
||||
subject(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) }
|
||||
subject(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac, local_tiller_enabled: false) }
|
||||
|
||||
let(:application) { create(:clusters_applications_helm) }
|
||||
let(:rbac) { false }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
|
|||
version: version,
|
||||
repository: repository,
|
||||
preinstall: preinstall,
|
||||
postinstall: postinstall
|
||||
postinstall: postinstall,
|
||||
local_tiller_enabled: local_tiller_enabled
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
|
|||
let(:version) { '1.2.3' }
|
||||
let(:preinstall) { nil }
|
||||
let(:postinstall) { nil }
|
||||
let(:local_tiller_enabled) { true }
|
||||
|
||||
it_behaves_like 'helm command generator' do
|
||||
let(:commands) do
|
||||
|
|
@ -51,9 +53,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
|
|||
end
|
||||
|
||||
context 'tillerless feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(managed_apps_local_tiller: false)
|
||||
end
|
||||
let(:local_tiller_enabled) { false }
|
||||
|
||||
let(:tls_flags) do
|
||||
<<~EOS.squish
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
|
|||
let(:repository) { 'https://repository.example.com' }
|
||||
let(:rbac) { false }
|
||||
let(:version) { '1.2.3' }
|
||||
let(:local_tiller_enabled) { true }
|
||||
|
||||
subject(:patch_command) do
|
||||
described_class.new(
|
||||
|
|
@ -15,14 +16,13 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
|
|||
rbac: rbac,
|
||||
files: files,
|
||||
version: version,
|
||||
repository: repository
|
||||
repository: repository,
|
||||
local_tiller_enabled: local_tiller_enabled
|
||||
)
|
||||
end
|
||||
|
||||
context 'when local tiller feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(managed_apps_local_tiller: false)
|
||||
end
|
||||
let(:local_tiller_enabled) { false }
|
||||
|
||||
let(:tls_flags) do
|
||||
<<~EOS.squish
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Kubernetes::Helm::ResetCommand do
|
||||
subject(:reset_command) { described_class.new(name: name, rbac: rbac, files: files) }
|
||||
subject(:reset_command) { described_class.new(name: name, rbac: rbac, files: files, local_tiller_enabled: false) }
|
||||
|
||||
let(:rbac) { true }
|
||||
let(:name) { 'helm' }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Setting assignees of an alert' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:alert) { create(:alert_management_alert, project: project) }
|
||||
let(:input) { { assignee_usernames: [current_user.username] } }
|
||||
|
||||
let(:mutation) do
|
||||
graphql_mutation(
|
||||
:alert_set_assignees,
|
||||
{ project_path: project.full_path, iid: alert.iid.to_s }.merge(input),
|
||||
<<~QL
|
||||
clientMutationId
|
||||
errors
|
||||
alert {
|
||||
assignees {
|
||||
nodes {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
QL
|
||||
)
|
||||
end
|
||||
|
||||
let(:mutation_response) { graphql_mutation_response(:alert_set_assignees) }
|
||||
|
||||
before_all do
|
||||
project.add_developer(current_user)
|
||||
end
|
||||
|
||||
it 'updates the assignee of the alert' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['alert']['assignees']['nodes'].first['username']).to eq(current_user.username)
|
||||
expect(alert.reload.assignees).to contain_exactly(current_user)
|
||||
end
|
||||
|
||||
context 'with operation_mode specified' do
|
||||
let(:input) do
|
||||
{
|
||||
assignee_usernames: [current_user.username],
|
||||
operation_mode: Types::MutationOperationModeEnum.enum[:remove]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
alert.assignees = [current_user]
|
||||
end
|
||||
|
||||
it 'updates the assignee of the alert' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['alert']['assignees']['nodes']).to be_empty
|
||||
expect(alert.reload.assignees).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe AlertManagement::Alerts::UpdateService do
|
||||
let_it_be(:user_with_permissions) { create(:user) }
|
||||
let_it_be(:user_without_permissions) { create(:user) }
|
||||
let_it_be(:alert, reload: true) { create(:alert_management_alert) }
|
||||
let_it_be(:project) { alert.project }
|
||||
|
||||
let(:current_user) { user_with_permissions }
|
||||
let(:params) { {} }
|
||||
|
||||
let(:service) { described_class.new(alert, current_user, params) }
|
||||
|
||||
before_all do
|
||||
project.add_developer(user_with_permissions)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject(:response) { service.execute }
|
||||
|
||||
context 'when user does not have permission to update alerts' do
|
||||
let(:current_user) { user_without_permissions }
|
||||
|
||||
it 'results in an error' do
|
||||
expect(response).to be_error
|
||||
expect(response.message).to eq('You have no permissions')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no parameters are included' do
|
||||
it 'results in an error' do
|
||||
expect(response).to be_error
|
||||
expect(response.message).to eq('Please provide attributes to update')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an error occures during update' do
|
||||
let(:params) { { title: nil } }
|
||||
|
||||
it 'results in an error' do
|
||||
expect(response).to be_error
|
||||
expect(response.message).to eq("Title can't be blank")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a model attribute is included' do
|
||||
let(:params) { { title: 'This is an updated alert.' } }
|
||||
|
||||
it 'updates the attribute' do
|
||||
original_title = alert.title
|
||||
|
||||
expect { response }.to change { alert.title }.from(original_title).to(params[:title])
|
||||
expect(response).to be_success
|
||||
end
|
||||
end
|
||||
|
||||
context 'when assignees are included' do
|
||||
let(:params) { { assignees: [user_with_permissions] } }
|
||||
|
||||
after do
|
||||
alert.assignees = []
|
||||
end
|
||||
|
||||
it 'assigns the user' do
|
||||
expect { response }.to change { alert.reload.assignees }.from([]).to(params[:assignees])
|
||||
expect(response).to be_success
|
||||
end
|
||||
|
||||
context 'with multiple users included' do
|
||||
let(:params) { { assignees: [user_with_permissions, user_without_permissions] } }
|
||||
|
||||
it 'assigns the first permissioned user' do
|
||||
expect { response }.to change { alert.reload.assignees }.from([]).to([user_with_permissions])
|
||||
expect(response).to be_success
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -54,7 +54,7 @@ RSpec.shared_examples 'cluster application helm specs' do |application_name|
|
|||
|
||||
context 'managed_apps_local_tiller feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(managed_apps_local_tiller: true)
|
||||
stub_feature_flags(managed_apps_local_tiller: application.cluster.clusterable)
|
||||
end
|
||||
|
||||
it 'does not include cert files' do
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ RSpec.shared_examples 'cluster application initial status specs' do
|
|||
|
||||
context 'local tiller feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(managed_apps_local_tiller: true)
|
||||
stub_feature_flags(managed_apps_local_tiller: cluster.clusterable)
|
||||
end
|
||||
|
||||
it 'sets a default status' do
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
|
|||
|
||||
context 'managed_apps_local_tiller feature flag enabled' do
|
||||
before do
|
||||
stub_feature_flags(managed_apps_local_tiller: true)
|
||||
stub_feature_flags(managed_apps_local_tiller: subject.cluster.clusterable)
|
||||
end
|
||||
|
||||
it 'does not update the helm version' do
|
||||
|
|
|
|||
Loading…
Reference in New Issue