Commit 5064bf8c authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖

Add latest changes from gitlab-org/[email protected]

parent 9c83aadd
Pipeline #129509447 passed with stages
in 67 minutes and 9 seconds
......@@ -28,7 +28,9 @@ PreCommit:
EsLint:
enabled: true
# https://github.com/sds/overcommit/issues/338
command: './node_modules/eslint/bin/eslint.js'
required_executable: 'yarn'
command: ['yarn', 'eslint']
flags: []
HamlLint:
enabled: true
MergeConflicts:
......
<script>
import { GlPopover, GlSprintf, GlButton, GlIcon } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
import { parseBoolean, scrollToElement, setCookie, getCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { glEmojiTag } from '~/emoji';
import Tracking from '~/tracking';
......@@ -51,7 +50,7 @@ export default {
},
data() {
return {
popoverDismissed: parseBoolean(Cookies.get(this.dismissKey)),
popoverDismissed: parseBoolean(getCookie(`${this.trackLabel}_${this.dismissKey}`)),
tracking: {
label: this.trackLabel,
property: this.humanAccess,
......@@ -68,17 +67,27 @@ export default {
emoji() {
return popoverStates[this.trackLabel].emoji || '';
},
dismissCookieName() {
return `${this.trackLabel}_${this.dismissKey}`;
},
commitCookieName() {
return `suggest_gitlab_ci_yml_commit_${this.dismissKey}`;
},
},
mounted() {
if (this.trackLabel === 'suggest_commit_first_project_gitlab_ci_yml' && !this.popoverDismissed)
if (
this.trackLabel === 'suggest_commit_first_project_gitlab_ci_yml' &&
!this.popoverDismissed
) {
scrollToElement(document.querySelector(this.target));
}
this.trackOnShow();
},
methods: {
onDismiss() {
this.popoverDismissed = true;
Cookies.set(this.dismissKey, this.popoverDismissed, { expires: 365 });
setCookie(this.dismissCookieName, this.popoverDismissed);
},
trackOnShow() {
if (!this.popoverDismissed) this.track();
......
......@@ -5,6 +5,7 @@ import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { setCookie } from '~/lib/utils/common_utils';
export default () => {
const editBlobForm = $('.js-edit-blob-form');
......@@ -60,6 +61,16 @@ export default () => {
}
if (suggestEl) {
const commitButton = document.querySelector('#commit-changes');
initPopover(suggestEl);
if (commitButton) {
const commitCookieName = `suggest_gitlab_ci_yml_commit_${suggestEl.dataset.dismissKey}`;
commitButton.addEventListener('click', () => {
setCookie(commitCookieName, true);
});
}
}
};
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
id: 'delete-environment-modal',
name: 'DeleteEnvironmentModal',
components: {
GlModal,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
environment: {
type: Object,
required: true,
},
},
computed: {
confirmDeleteMessage() {
return sprintf(
s__(
`Environments|Deleting the '%{environmentName}' environment cannot be undone. Do you want to delete it anyway?`,
),
{
environmentName: this.environment.name,
},
false,
);
},
},
methods: {
onSubmit() {
eventHub.$emit('deleteEnvironment', this.environment);
},
},
};
</script>
<template>
<gl-modal
:id="$options.id"
:footer-primary-button-text="s__('Environments|Delete environment')"
footer-primary-button-variant="danger"
@submit="onSubmit"
>
<template slot="header">
<h4 class="modal-title d-flex mw-100">
{{ __('Delete') }}
<span v-gl-tooltip :title="environment.name" class="text-truncate mx-1 flex-fill">
{{ environment.name }}?
</span>
</h4>
</template>
<p>{{ confirmDeleteMessage }}</p>
</gl-modal>
</template>
<script>
/**
* Renders the delete button that allows deleting a stopped environment.
* Used in the environments table and the environment detail view.
*/
import $ from 'jquery';
import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default {
components: {
Icon,
LoadingButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
environment: {
type: Object,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return s__('Environments|Delete environment');
},
},
mounted() {
eventHub.$on('deleteEnvironment', this.onDeleteEnvironment);
},
beforeDestroy() {
eventHub.$off('deleteEnvironment', this.onDeleteEnvironment);
},
methods: {
onClick() {
$(this.$el).tooltip('dispose');
eventHub.$emit('requestDeleteEnvironment', this.environment);
},
onDeleteEnvironment(environment) {
if (this.environment.id === environment.id) {
this.isLoading = true;
}
},
},
};
</script>
<template>
<loading-button
v-gl-tooltip
:loading="isLoading"
:title="title"
:aria-label="title"
container-class="btn btn-danger d-none d-sm-none d-md-block"
data-toggle="modal"
data-target="#delete-environment-modal"
@click="onClick"
>
<icon name="remove" />
</loading-button>
</template>
......@@ -15,8 +15,9 @@ import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import PinComponent from './environment_pin.vue';
import RollbackComponent from './environment_rollback.vue';
import DeleteComponent from './environment_delete.vue';
import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
/**
......@@ -33,6 +34,7 @@ export default {
Icon,
MonitoringButtonComponent,
PinComponent,
DeleteComponent,
RollbackComponent,
StopComponent,
TerminalButtonComponent,
......@@ -112,6 +114,15 @@ export default {
return this.model && this.model.can_stop;
},
/**
* Returns whether the environment can be deleted.
*
* @returns {Boolean}
*/
canDeleteEnvironment() {
return Boolean(this.model && this.model.can_delete && this.model.delete_path);
},
/**
* Verifies if the `deployable` key is present in `last_deployment` key.
* Used to verify whether we should or not render the rollback partial.
......@@ -485,6 +496,7 @@ export default {
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
this.canDeleteEnvironment ||
this.canRetry
);
},
......@@ -680,6 +692,8 @@ export default {
/>
<stop-component v-if="canStopEnvironment" :environment="model" />
<delete-component v-if="canDeleteEnvironment" :environment="model" />
</div>
</div>
</div>
......
......@@ -9,6 +9,7 @@ import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import EnableReviewAppButton from './enable_review_app_button.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
export default {
......@@ -18,6 +19,7 @@ export default {
EnableReviewAppButton,
GlButton,
StopEnvironmentModal,
DeleteEnvironmentModal,
},
mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
......@@ -95,6 +97,7 @@ export default {
<template>
<div>
<stop-environment-modal :environment="environmentInStopModal" />
<delete-environment-modal :environment="environmentInDeleteModal" />
<confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="top-area">
......
......@@ -63,10 +63,9 @@ export default {
<template slot="header">
<h4 class="modal-title d-flex mw-100">
Stopping
<span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">{{
environment.name
}}</span>
?
<span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">
{{ environment.name }}?
</span>
</h4>
</template>
......
......@@ -3,10 +3,12 @@ import folderMixin from 'ee_else_ce/environments/mixins/environments_folder_view
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
export default {
components: {
StopEnvironmentModal,
DeleteEnvironmentModal,
},
mixins: [environmentsMixin, CIPaginationMixin, folderMixin],
......@@ -39,6 +41,7 @@ export default {
<template>
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<delete-environment-modal :environment="environmentInDeleteModal" />
<h4 class="js-folder-name environments-folder-name">
{{ s__('Environments|Environments') }} /
......
......@@ -27,6 +27,10 @@ export default {
data() {
const store = new EnvironmentsStore();
const isDetailView = document.body.contains(
document.getElementById('environments-detail-view'),
);
return {
store,
state: store.state,
......@@ -36,7 +40,9 @@ export default {
page: getParameterByName('page') || '1',
requestData: {},
environmentInStopModal: {},
environmentInDeleteModal: {},
environmentInRollbackModal: {},
isDetailView,
};
},
......@@ -121,6 +127,10 @@ export default {
this.environmentInStopModal = environment;
},
updateDeleteModal(environment) {
this.environmentInDeleteModal = environment;
},
updateRollbackModal(environment) {
this.environmentInRollbackModal = environment;
},
......@@ -133,6 +143,30 @@ export default {
this.postAction({ endpoint, errorMessage });
},
deleteEnvironment(environment) {
const endpoint = environment.delete_path;
const mountedToShow = environment.mounted_to_show;
const errorMessage = s__(
'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
);
this.service
.deleteAction(endpoint)
.then(() => {
if (!mountedToShow) {
// Reload as a first solution to bust the ETag cache
window.location.reload();
return;
}
const url = window.location.href.split('/');
url.pop();
window.location.href = url.join('/');
})
.catch(() => {
Flash(errorMessage);
});
},
rollbackEnvironment(environment) {
const { retryUrl, isLastDeployment } = environment;
const errorMessage = isLastDeployment
......@@ -178,36 +212,42 @@ export default {
this.service = new EnvironmentsService(this.endpoint);
this.requestData = { page: this.page, scope: this.scope, nested: true };
this.poll = new Poll({
resource: this.service,
method: 'fetchEnvironments',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: isMakingRequest => {
this.isMakingRequest = isMakingRequest;
},
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
this.fetchEnvironments();
}
if (!this.isDetailView) {
this.poll = new Poll({
resource: this.service,
method: 'fetchEnvironments',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: isMakingRequest => {
this.isMakingRequest = isMakingRequest;
},
});
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
this.isLoading = true;
this.poll.makeRequest();
} else {
this.poll.stop();
this.fetchEnvironments();
}
});
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
eventHub.$on('postAction', this.postAction);
eventHub.$on('requestStopEnvironment', this.updateStopModal);
eventHub.$on('stopEnvironment', this.stopEnvironment);
eventHub.$on('requestDeleteEnvironment', this.updateDeleteModal);
eventHub.$on('deleteEnvironment', this.deleteEnvironment);
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
......@@ -216,9 +256,13 @@ export default {
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
eventHub.$off('requestStopEnvironment', this.updateStopModal);
eventHub.$off('stopEnvironment', this.stopEnvironment);
eventHub.$off('requestDeleteEnvironment', this.updateDeleteModal);
eventHub.$off('deleteEnvironment', this.deleteEnvironment);
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
......
import Vue from 'vue';
import DeleteEnvironmentModal from './components/delete_environment_modal.vue';
import environmentsMixin from './mixins/environments_mixin';
export default () => {
const el = document.getElementById('delete-environment-modal');
const container = document.getElementById('environments-detail-view');
return new Vue({
el,
components: {
DeleteEnvironmentModal,
},
mixins: [environmentsMixin],
data() {
const environment = JSON.parse(JSON.stringify(container.dataset));
environment.delete_path = environment.deletePath;
environment.mounted_to_show = true;
return {
environment,
};
},
render(createElement) {
return createElement('delete-environment-modal', {
props: {
environment: this.environment,
},
});
},
});
};
......@@ -16,6 +16,11 @@ export default class EnvironmentsService {
return axios.post(endpoint, {});
}
// eslint-disable-next-line class-methods-use-this
deleteAction(endpoint) {
return axios.delete(endpoint, {});
}
getFolderContent(folderUrl) {
return axios.get(`${folderUrl}.json?per_page=${this.folderResults}`);
}
......
......@@ -9,6 +9,7 @@ import { getLocationHash } from './url_utility';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { isFunction } from 'lodash';
import Cookies from 'js-cookie';
export const getPagePath = (index = 0) => {
const page = $('body').attr('data-page') || '';
......@@ -902,3 +903,10 @@ window.gl.utils = {
spriteIcon,
imagePath,
};
// Methods to set and get Cookie
export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
export const getCookie = name => Cookies.get(name);
export const removeCookie = name => Cookies.remove(name);
import initShowEnvironment from '~/environments/mount_show';
document.addEventListener('DOMContentLoaded', () => initShowEnvironment());
......@@ -4,6 +4,9 @@ module Projects
module Settings
class OperationsController < Projects::ApplicationController
before_action :authorize_admin_operations!
before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token]
respond_to :json, only: [:reset_alerting_token]
helper_method :error_tracking_setting
......@@ -27,8 +30,24 @@ module Projects
end
end
def reset_alerting_token
result = ::Projects::Operations::UpdateService
.new(project, current_user, alerting_params)
.execute
if result[:status] == :success
render json: { token: project.alerting_setting.token }
else
render json: {}, status: :unprocessable_entity
end
end
private
def alerting_params
{ alerting_setting_attributes: { regenerate_token: true } }
end
def prometheus_service
project.find_or_initialize_service(::PrometheusService.to_param)
end
......
......@@ -50,4 +50,8 @@ module EnvironmentsHelper
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack')
}
end
def can_destroy_environment?(environment)
can?(current_user, :destroy_environment, environment)
end
end
......@@ -4,6 +4,7 @@
module GitlabRoutingHelper
extend ActiveSupport::Concern
include API::Helpers::RelatedResourcesHelpers
included do
Gitlab::Routing.includes_helpers(self)
end
......@@ -29,6 +30,10 @@ module GitlabRoutingHelper
metrics_project_environment_path(environment.project, environment, *args)
end
def environment_delete_path(environment, *args)
expose_path(api_v4_projects_environments_path(id: environment.project.id, environment_id: environment.id))
end
def issue_path(entity, *args)
project_issue_path(entity.project, entity, *args)
end
......
......@@ -62,13 +62,16 @@ class CommitStatus < ApplicationRecord
preload(project: :namespace)
end
scope :match_id_and_lock_version, -> (slice) do
scope :match_id_and_lock_version, -> (items) do
# it expects that items are an array of attributes to match
# each hash needs to have `id` and `lock_version`
slice.inject(self) do |relation, item|
match = CommitStatus.where(item.slice(:id, :lock_version))
or_conditions = items.inject(none) do |relation, item|
match = CommitStatus.default_scoped.where(item.slice(:id, :lock_version))
relation.or(match)
end
merge(or_conditions)
end
# We use `CommitStatusEnums.failure_reasons` here so that EE can more easily
......
......@@ -79,6 +79,12 @@ module Noteable
.discussions(self)
end
def discussion_ids_relation
notes.select(:discussion_id)
.group(:discussion_id)
.order('MIN(created_at), MIN(id)')
end
def capped_notes_count(max)
notes.limit(max).count
end
......
......@@ -81,7 +81,7 @@ class PrometheusService < MonitoringService
def prometheus_client
return unless should_return_client?
Gitlab::PrometheusClient.new(api_url)
Gitlab::PrometheusClient.new(api_url, allow_local_requests: allow_local_api_url?)
end
def prometheus_available?
......@@ -94,7 +94,8 @@ class PrometheusService < MonitoringService
end
def allow_local_api_url?
self_monitoring_project? && internal_prometheus_url?
allow_local_requests_from_web_hooks_and_services? ||
(self_monitoring_project? && internal_prometheus_url?)
end
def configured?
......@@ -111,6 +112,10 @@ class PrometheusService < MonitoringService
api_url.present? && api_url == ::Gitlab::Prometheus::Internal.uri
end
def allow_local_requests_from_web_hooks_and_services?
current_settings.allow_local_requests_from_web_hooks_and_services?
end
def should_return_client?
api_url.present? && manual_configuration? && active? && valid?
end
......
......@@ -19,8 +19,6 @@ class Snippet < ApplicationRecord
MAX_FILE_COUNT = 1