Commit 75e1faf2 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into 'rd-add-support-for-gl-com-trials'

# Conflicts:
#   locale/gitlab.pot
parents 8fd17fd4 7abbde6e
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
eventHub.$emit('postAction', endpoint); eventHub.$emit('postAction', { endpoint });
}, },
isActionDisabled(action) { isActionDisabled(action) {
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
return !action.playable; return !action.playable;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div <div
...@@ -61,10 +61,7 @@ ...@@ -61,10 +61,7 @@
data-toggle="dropdown" data-toggle="dropdown"
> >
<span> <span>
<icon <icon name="play" />
:size="12"
name="play"
/>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true" aria-hidden="true"
...@@ -85,10 +82,6 @@ ...@@ -85,10 +82,6 @@
class="js-manual-action-link no-btn btn" class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)" @click="onClickAction(action.play_path)"
> >
<icon
:size="12"
name="play"
/>
<span> <span>
{{ action.name }} {{ action.name }}
</span> </span>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
export default { export default {
components: { components: {
Icon, Icon,
}, },
...@@ -21,10 +21,10 @@ ...@@ -21,10 +21,10 @@
}, },
computed: { computed: {
title() { title() {
return s__('Environments|Open'); return s__('Environments|Open live environment');
}, },
}, },
}; };
</script> </script>
<template> <template>
<a <a
...@@ -37,9 +37,6 @@ ...@@ -37,9 +37,6 @@
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
> >
<icon <icon name="external-link" />
:size="12"
name="external-link"
/>
</a> </a>
</template> </template>
<script> <script>
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: { components: {
Icon, Icon,
}, },
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
return 'Monitoring'; return 'Monitoring';
}, },
}, },
}; };
</script> </script>
<template> <template>
<a <a
...@@ -35,9 +35,6 @@ ...@@ -35,9 +35,6 @@
data-container="body" data-container="body"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
> >
<icon <icon name="chart" />
:size="12"
name="chart"
/>
</a> </a>
</template> </template>
<script> <script>
/** /**
* Renders Rollback or Re deploy button in environments table depending * Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`. * of the provided property `isLastDeployment`.
* *
* Makes a post request when the button is clicked. * Makes a post request when the button is clicked.
*/ */
import eventHub from '../event_hub'; import { s__ } from '~/locale';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
components: { components: {
loadingIcon, Icon,
LoadingIcon,
}, },
directives: {
tooltip,
},
props: { props: {
retryUrl: { retryUrl: {
type: String, type: String,
...@@ -28,29 +37,38 @@ ...@@ -28,29 +37,38 @@
isLoading: false, isLoading: false,
}; };
}, },
computed: {
title() {
return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment');
},
},
methods: { methods: {
onClick() { onClick() {
this.isLoading = true; this.isLoading = true;
eventHub.$emit('postAction', this.retryUrl); eventHub.$emit('postAction', { endpoint: this.retryUrl });
}, },
}, },
}; };
</script> </script>
<template> <template>
<button <button
v-tooltip
:disabled="isLoading" :disabled="isLoading"
:title="title"
type="button" type="button"
class="btn d-none d-sm-none d-md-block" class="btn d-none d-sm-none d-md-block"
@click="onClick" @click="onClick"
> >
<span v-if="isLastDeployment"> <icon
{{ s__("Environments|Re-deploy") }} v-if="isLastDeployment"
</span> name="repeat" />
<span v-else> <icon
{{ s__("Environments|Rollback") }} v-else
</span> name="redo"/>
<loading-icon v-if="isLoading" /> <loading-icon v-if="isLoading" />
</button> </button>
......
<script> <script>
/** /**
* Renders the stop "button" that allows stop an environment. * Renders the stop "button" that allows stop an environment.
* Used in environments table. * Used in environments table.
*/ */
import $ from 'jquery'; import $ from 'jquery';
import eventHub from '../event_hub'; import Icon from '~/vue_shared/components/icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import { s__ } from '~/locale';
import tooltip from '../../vue_shared/directives/tooltip'; import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: { components: {
loadingIcon, Icon,
LoadingButton,
}, },
directives: { directives: {
...@@ -19,9 +22,9 @@ ...@@ -19,9 +22,9 @@
}, },
props: { props: {
stopUrl: { environment: {
type: String, type: Object,
default: '', required: true,
}, },
}, },
...@@ -33,40 +36,43 @@ ...@@ -33,40 +36,43 @@
computed: { computed: {
title() { title() {
return 'Stop'; return s__('Environments|Stop environment');
}, },
}, },
mounted() {
eventHub.$on('stopEnvironment', this.onStopEnvironment);
},
beforeDestroy() {
eventHub.$off('stopEnvironment', this.onStopEnvironment);
},
methods: { methods: {
onClick() { onClick() {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
$(this.$el).tooltip('dispose'); $(this.$el).tooltip('dispose');
eventHub.$emit('requestStopEnvironment', this.environment);
eventHub.$emit('postAction', this.stopUrl); },
onStopEnvironment(environment) {
if (this.environment.id === environment.id) {
this.isLoading = true;
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
<button <loading-button
v-tooltip v-tooltip
:disabled="isLoading" :loading="isLoading"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
type="button" container-class="btn btn-danger d-none d-sm-none d-md-block"
class="btn stop-env-link d-none d-sm-none d-md-block"
data-container="body" data-container="body"
data-toggle="modal"
data-target="#stop-environment-modal"
@click="onClick" @click="onClick"
> >
<i <icon name="stop"/>
class="fa fa-stop stop-env-icon" </loading-button>
aria-hidden="true"
>
</i>
<loading-icon v-if="isLoading" />
</button>
</template> </template>
<script> <script>
/** /**
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
*/ */
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: { components: {
Icon, Icon,
}, },
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
return 'Terminal'; return 'Terminal';
}, },
}, },
}; };
</script> </script>
<template> <template>
<a <a
...@@ -36,9 +36,6 @@ ...@@ -36,9 +36,6 @@
class="btn terminal-button d-none d-sm-none d-md-block" class="btn terminal-button d-none d-sm-none d-md-block"
data-container="body" data-container="body"
> >
<icon <icon name="terminal" />
:size="12"
name="terminal"
/>
</a> </a>
</template> </template>
...@@ -5,10 +5,12 @@ ...@@ -5,10 +5,12 @@
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin'; import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from './stop_environment_modal.vue';
export default { export default {
components: { components: {
emptyState, emptyState,
StopEnvironmentModal,
}, },
mixins: [ mixins: [
...@@ -100,6 +102,8 @@ ...@@ -100,6 +102,8 @@
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<div class="top-area"> <div class="top-area">
<tabs <tabs
:tabs="tabs" :tabs="tabs"
......
<script>
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
export default {
id: 'stop-environment-modal',
name: 'StopEnvironmentModal',
components: {
GlModal,
LoadingButton,
},
directives: {
tooltip,
},
props: {
environment: {
type: Object,
required: true,
},
},
computed: {
noStopActionMessage() {
return sprintf(
s__(
`Environments|Note that this action will stop the environment,
but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment
due to no “stop environment action” being defined
in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`,
),
{
emphasisStart: '<strong>',
emphasisEnd: '</strong>',
ciConfigLinkStart:
'<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">',
ciConfigLinkEnd: '</a>',
},
false,
);
},
},
methods: {
onSubmit() {
eventHub.$emit('stopEnvironment', this.environment);
},
},
};
</script>
<template>
<gl-modal
:id="$options.id"
:footer-primary-button-text="s__('Environments|Stop environment')"
footer-primary-button-variant="danger"
@submit="onSubmit"
>
<template slot="header">
<h4
class="modal-title d-flex mw-100"
>
Stopping
<span
v-tooltip
:title="environment.name"
class="text-truncate ml-1 mr-1 flex-fill"
>{{ environment.name }}</span>
?
</h4>
</template>
<p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p>
<div
v-if="!environment.has_stop_action"
class="warning_message"
>
<p v-html="noStopActionMessage"></p>
<a
href="https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment"
target="_blank"
rel="noopener noreferrer"
>{{ s__('Environments|Learn more about stopping environments') }}</a>
</div>
</gl-modal>
</template>
<script> <script>
import environmentsMixin from '../mixins/environments_mixin'; import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
export default { export default {
components: {
StopEnvironmentModal,
},
mixins: [ mixins: [
environmentsMixin, environmentsMixin,
CIPaginationMixin, CIPaginationMixin,
], ],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
...@@ -38,6 +44,8 @@ ...@@ -38,6 +44,8 @@
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<div <div
v-if="!isLoading" v-if="!isLoading"
class="top-area" class="top-area"
......
...@@ -40,6 +40,7 @@ export default { ...@@ -40,6 +40,7 @@ export default {
scope: getParameterByName('scope') || 'available', scope: getParameterByName('scope') || 'available',
page: getParameterByName('page') || '1', page: getParameterByName('page') || '1',
requestData: {}, requestData: {},
environmentInStopModal: {},
}; };
}, },
...@@ -85,7 +86,7 @@ export default { ...@@ -85,7 +86,7 @@ export default {
Flash(s__('Environments|An error occurred while fetching the environments.')); Flash(s__('Environments|An error occurred while fetching the environments.'));
}, },
postAction(endpoint) { postAction({ endpoint, errorMessage }) {
if (!this.isMakingRequest) { if (!this.isMakingRequest) {
this.isLoading = true; this.isLoading = true;
...@@ -93,7 +94,7 @@ export default { ...@@ -93,7 +94,7 @@ export default {
.then(() => this.fetchEnvironments()) .then(() => this.fetchEnvironments())
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
Flash(s__('Environments|An error occurred while making the request.')); Flash(errorMessage || s__('Environments|An error occurred while making the request.'));
}); });
} }
}, },
...@@ -106,6 +107,15 @@ export default { ...@@ -106,6 +107,15 @@ export default {
.catch(this.errorCallback); .catch(this.errorCallback);
}, },
updateStopModal(environment) {
this.environmentInStopModal = environment;
},
stopEnvironment(environment) {
const endpoint = environment.stop_path;
const errorMessage = s__('Environments|An error occurred while stopping the environment, please try again');
this.postAction({ endpoint, errorMessage });
},
}, },
computed: { computed: {
...@@ -162,9 +172,13 @@ export default { ...@@ -162,9 +172,13 @@ export default {
}); });
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
eventHub.$on('requestStopEnvironment', this.updateStopModal);
eventHub.$on('stopEnvironment', this.stopEnvironment);
}, },
beforeDestroyed() { beforeDestroy() {
eventHub.$off('postAction'); eventHub.$off('postAction', this.postAction);
eventHub.$off('requestStopEnvironment', this.updateStopModal);
eventHub.$off('stopEnvironment', this.stopEnvironment);
}, },
}; };
...@@ -13,7 +13,7 @@ export default class EnvironmentsService { ...@@ -13,7 +13,7 @@ export default class EnvironmentsService {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
postAction(endpoint) { postAction(endpoint) {
return axios.post(endpoint, {}, { emulateJSON: true }); return axios.post(endpoint, {});
} }
getFolderContent(folderUrl) { getFolderContent(folderUrl) {
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
} }
.btn-group { .btn-group {
> a { > .btn:not(.btn-danger) {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
......
...@@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project' layout 'project'
before_action :authorize_read_environment! before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_stop_environment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_update_environment!, only: [:edit, :update]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
...@@ -177,4 +177,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -177,4 +177,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def environment def environment
@environment ||= project.environments.find(params[:id]) @environment ||= project.environments.find(params[:id])
end end
def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment)
end
end end
...@@ -196,7 +196,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -196,7 +196,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
deployment = environment.first_deployment_for(@merge_request.diff_head_sha) deployment = environment.first_deployment_for(@merge_request.diff_head_sha)
stop_url = stop_url =
if environment.stop_action? && can?(current_user, :create_deployment, environment) if can?(current_user, :stop_environment, environment)
stop_project_environment_path(project, environment) stop_project_environment_path(project, environment)
end end
......
class EnvironmentPolicy < BasePolicy class EnvironmentPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
condition(:stop_action_allowed) do condition(:stop_with_deployment_allowed) do
@subject.stop_action? && can?(:update_build, @subject.stop_action) @subject.stop_action? && can?(:create_deployment) && can?(:update_build, @subject.stop_action)
end end
rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment condition(:stop_with_update_allowed) do
!@subject.stop_action? && can?(:update_environment, @subject)
end
rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment
end end
...@@ -9,7 +9,7 @@ class EnvironmentEntity < Grape::Entity ...@@ -9,7 +9,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url expose :external_url
expose :environment_type expose :environment_type
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stop_action? expose :stop_action?, as: :has_stop_action
expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity
...@@ -36,6 +36,10 @@ class EnvironmentEntity < Grape::Entity ...@@ -36,6 +36,10 @@ class EnvironmentEntity < Grape::Entity
expose :created_at, :updated_at expose :created_at, :updated_at
expose :can_stop do |environment|
environment.available? && can?(current_user, :stop_environment, environment)
end
private private
alias_method :environment, :object alias_method :environment, :object
......
...@@ -3,13 +3,12 @@ ...@@ -3,13 +3,12 @@
- if actions.present? - if actions.present?
.btn-group .btn-group
.dropdown .dropdown
%button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } %button.dropdown.dropdown-new.btn.btn-default.has-tooltip{ type: 'button', 'data-toggle' => 'dropdown', title: s_('Environments|Deploy to...') }
= custom_icon('icon_play') = sprite_icon('play')
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-right %ul.dropdown-menu.dropdown-menu-right
- actions.each do |action| - actions.each do |action|
- next unless can?(current_user, :update_build, action) - next unless can?(current_user, :update_build, action)
%li %li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow', class: 'btn' do
= custom_icon('icon_play')
%span= action.name.humanize %span= action.name.humanize
- if can?(current_user, :create_deployment, deployment) && deployment.deployable - if can?(current_user, :create_deployment, deployment) && deployment.deployable
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do
- if deployment.last? - if deployment.last?
= _("Re-deploy") = sprite_icon('repeat')
- else - else
= _("Rollback") = sprite_icon('redo')
- if environment.external_url && can?(current_user, :read_environment, environment) - if environment.external_url && can?(current_user, :read_environment, environment)
= link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url has-tooltip', title: s_('Environments|Open live environment') do
= sprite_icon('external-link') = sprite_icon('external-link')
View deployment View deployment
- if can?(current_user, :create_deployment, environment) && environment.stop_action?
.inline
= link_to stop_project_environment_path(@project, environment), method: :post,
class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
= icon('stop', class: 'stop-env-icon')
...@@ -4,6 +4,33 @@ ...@@ -4,6 +4,33 @@
- page_title "Environments" - page_title "Environments"
%div{ class: container_class } %div{ class: container_class }
- if can?(current_user, :stop_environment, @environment)
#stop-environment-modal.modal.fade{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h4.modal-title.d-flex.mw-100
Stopping
%span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } }
= @environment.name
?
.modal-body
%p= s_('Environments|Are you sure you want to stop this environment?')
- unless @environment.stop_action?
.warning_message
%p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe,
emphasis_end: '</strong>'.html_safe,
ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe,
ci_config_link_end: '</a>'.html_safe }
%a{ href: 'https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment',
target: '_blank',
rel: 'noopener noreferrer' }
= s_('Environments|Learn more about stopping environments')
.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
= s_('Environments|Stop environment')
.row.top-area.adjust .row.top-area.adjust
.col-md-7 .col-md-7
%h3.page-title= @environment.name %h3.page-title= @environment.name
...@@ -15,7 +42,10 @@ ...@@ -15,7 +42,10 @@
- if can?(current_user, :update_environment, @environment) - if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_project_environment_path(@project, @environment), class: 'btn' = link_to 'Edit', edit_project_environment_path(@project, @environment), class: 'btn'
- if can?(current_user, :stop_environment, @environment) - if can?(current_user, :stop_environment, @environment)
= link_to 'Stop', stop_project_environment_path(@project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post = button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#stop-environment-modal' } do
= sprite_icon('stop')
= s_('Environments|Stop')
.environments-container .environments-container
- if @deployments.blank? - if @deployments.blank?
......
---
title: Support manually stopping any environment from the UI
merge_request: 20077
author:
type: changed
...@@ -103,6 +103,7 @@ module.exports = function(config) { ...@@ -103,6 +103,7 @@ module.exports = function(config) {
], ],
preprocessors: { preprocessors: {
'spec/javascripts/**/*.js': ['webpack', 'sourcemap'], 'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
'ee/spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
}, },
reporters: [progressReporter], reporters: [progressReporter],
webpack: webpackConfig, webpack: webpackConfig,
......
...@@ -107,6 +107,7 @@ module.exports = { ...@@ -107,6 +107,7 @@ module.exports = {
ee_empty_states: path.join(ROOT_PATH, 'ee/app/views/shared/empty_states'), ee_empty_states: path.join(ROOT_PATH, 'ee/app/views/shared/empty_states'),
ee_icons: path.join(ROOT_PATH, 'ee/app/views/shared/icons'), ee_icons: path.join(ROOT_PATH, 'ee/app/views/shared/icons'),
ee_images: path.join(ROOT_PATH, 'ee/app/assets/images'), ee_images: path.join(ROOT_PATH, 'ee/app/assets/images'),
ee_spec: path.join(ROOT_PATH, 'ee/spec/javascripts'),
}, },
}, },
......
...@@ -3,18 +3,21 @@ ...@@ -3,18 +3,21 @@
> [Introduced][ee-2381] in [GitLab Premium][eep] 10.0. > [Introduced][ee-2381] in [GitLab Premium][eep] 10.0.
As an extension to our [existing JIRA][existing-jira] project integration, you're now able to integrate As an extension to our [existing JIRA][existing-jira] project integration, you're now able to integrate
all your GitLab projects with [JIRA Development Panel][jira-development-panel]. Both can be used GitLab projects with [JIRA Development Panel][jira-development-panel]. Both can be used
simultaneously. This works with self-hosted GitLab or GitLab.com integrated with self-hosted JIRA simultaneously. This works with self-hosted GitLab or GitLab.com integrated with self-hosted JIRA
or cloud JIRA. or cloud JIRA.
By doing this you can easily access related GitLab branches and commits directly from a JIRA issue. By doing this you can easily access related GitLab merge requests, branches, and commits directly from a JIRA issue.
>**Note:** This integration connects all GitLab projects within a top-level group or a personal namespace to projects in the JIRA instance.
In the future, we plan to also support merge requests from the Development Panel. A top-level GitLab group is one that does not have any parent group itself. All the projects of that top-level group,
as well as projects of the top-level group's subgroups nesting down, are connected. Alternatively, you can specify
a GitLab personal namespace in the JIRA configuration, which will then connect the projects in that personal namespace to JIRA.
This integration connects all GitLab projects a user has access to with all projects in the JIRA instance.
(Note this is different from the [existing JIRA][existing-jira] project integration, where the mapping (Note this is different from the [existing JIRA][existing-jira] project integration, where the mapping
is one GitLab project to the entire JIRA instance.) We recommend that a GitLab group admin is one GitLab project to the entire JIRA instance.)
We recommend that a GitLab group admin
or instance admin (in the case of self-hosted GitLab) set up the integration with respect to their or instance admin (in the case of self-hosted GitLab) set up the integration with respect to their
account, in order to maximize the integrated GitLab projects used by your team. account, in order to maximize the integrated GitLab projects used by your team.
...@@ -50,7 +53,8 @@ from the left navigation menu. Click `Link GitHub account` to start creating a n ...@@ -50,7 +53,8 @@ from the left navigation menu. Click `Link GitHub account` to start creating a n
Select GitHub Enterprise for the `Host` field. Select GitHub Enterprise for the `Host` field.
For the `Team or User Account` field, enter the group name of a GitLab group that you have access to. This must be a top-level group, but all its subgroups will be imported. For the `Team or User Account` field, enter the relative path of a top-level GitLab group that you have access to,
or the relative path of your personal namespace.
![Creation of Jira DVCS integration](img/jira_dev_panel_jira_setup_2.png) ![Creation of Jira DVCS integration](img/jira_dev_panel_jira_setup_2.png)
...@@ -77,10 +81,8 @@ from the left navigation menu. Click `Link GitHub account` to start creating a n ...@@ -77,10 +81,8 @@ from the left navigation menu. Click `Link GitHub account` to start creating a n
> ![Refresh GitLab information in JIRA](img/jira_dev_panel_manual_refresh.png) > ![Refresh GitLab information in JIRA](img/jira_dev_panel_manual_refresh.png)
4. Repeat the above steps for each GitLab group's projects that you want to be made known to JIRA. To connect additional GitLab projects from other GitLab top-level groups (or personal namespaces), repeat the above
Specify the GitLab group name accordingly. (Note that you can also specify GitLab user names, as they steps with additional JIRA DVCS accounts.
are really GitLab "groups" behind the scenes. In that case, all the projects for that user would
be made known to JIRA, up to the permissions of the user setting up the integration.)
You may now refer any Jira issue by its ID in branch names, commit messages and merge request names on GitLab's side, You may now refer any Jira issue by its ID in branch names, commit messages and merge request names on GitLab's side,
and you will be able to see the linked `branches`, `commits`, and `merge requests` when entering a JIRA issue and you will be able to see the linked `branches`, `commits`, and `merge requests` when entering a JIRA issue
......
...@@ -36,7 +36,7 @@ here's a quick guide: ...@@ -36,7 +36,7 @@ here's a quick guide:
- Searches look for all the words in a query, in any order - e.g.: searching - Searches look for all the words in a query, in any order - e.g.: searching
issues for `display bug` will return all issues matching both those words, in any order. issues for `display bug` will return all issues matching both those words, in any order.
- To find the exact term, use double quotes: `"display bug"` - To find the exact phrase (stemming still applies), use double quotes: `"display bug"`
- To find bugs not mentioning display, use `-`: `bug -display` - To find bugs not mentioning display, use `-`: `bug -display`
- To find a bug in display or sound, use `|`: `bug display | sound` - To find a bug in display or sound, use `|`: `bug display | sound`
- To group terms together, use parentheses: `bug | (display +sound)` - To group terms together, use parentheses: `bug | (display +sound)`
......
<script> <script>
import ReportSection from 'ee/vue_shared/security_reports/components/report_section.vue';
import GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { n__, s__, __, sprintf } from '~/locale'; import { n__, s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import WidgetApprovals from './components/approvals/mr_widget_approvals.vue'; import WidgetApprovals from './components/approvals/mr_widget_approvals.vue';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue'; import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
import ReportSection from '../vue_shared/security_reports/components/report_section.vue';
import GroupedSecurityReportsApp from '../vue_shared/security_reports/grouped_security_reports_app.vue';
import reportsMixin from '../vue_shared/security_reports/mixins/reports_mixin';
export default { export default {
components: { components: {
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store'; import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { filterByKey } from '../../vue_shared/security_reports/store/utils'; import { filterByKey } from 'ee/vue_shared/security_reports/store/utils';
export default class MergeRequestStore extends CEMergeRequestStore { export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) { constructor(data) {
......
...@@ -13,7 +13,7 @@ module EE ...@@ -13,7 +13,7 @@ module EE
{ {
primary_version: version.to_s, primary_version: version.to_s,
primary_revision: revision.to_s, primary_revision: revision.to_s,
node_actions_allowed: ::Gitlab::Database.read_write?.to_s, node_actions_allowed: ::Gitlab::Database.db_read_write?.to_s,
node_edit_allowed: ::Gitlab::Geo.license_allows?.to_s node_edit_allowed: ::Gitlab::Geo.license_allows?.to_s
} }
end end
......
---
title: Allow Geo node to be edited once the database is failed over
merge_request: 6248
author:
type: fixed
module EE
module Gitlab
module Middleware
module ReadOnly
module Controller
extend ::Gitlab::Utils::Override
WHITELISTED_GEO_ROUTES = {
'admin/geo_nodes' => %w{update}
}.freeze
private
override :whitelisted_routes
def whitelisted_routes
super || geo_node_update_route
end
def geo_node_update_route
# Calling route_hash may be expensive. Only do it if we think there's a possible match
return false unless request.path =~ %r{/admin/geo_nodes}
::Gitlab::Database.db_read_write? &&
WHITELISTED_GEO_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end
end
end
end
end
end
...@@ -68,9 +68,7 @@ module Gitlab ...@@ -68,9 +68,7 @@ module Gitlab
end end
def self.database_secondary? def self.database_secondary?
ActiveRecord::Base.connection.execute('SELECT pg_is_in_recovery()') Gitlab::Database.db_read_only?
.first
.fetch('pg_is_in_recovery') == 't'
end end
def self.db_replication_lag_seconds def self.db_replication_lag_seconds
......
...@@ -9,7 +9,7 @@ module SystemCheck ...@@ -9,7 +9,7 @@ module SystemCheck
end end
def check? def check?
ActiveRecord::Base.connection.execute('SELECT pg_is_in_recovery()').first.fetch('pg_is_in_recovery') == 't' Gitlab::Database.db_read_only?
end end
def show_error def show_error
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
} }
] ]
}, },
"stop_action?": { "has_stop_action": {
"type": "boolean" "type": "boolean"
}, },
"rollout_status": { "rollout_status": {
...@@ -65,6 +65,9 @@ ...@@ -65,6 +65,9 @@
}, },
"updated_at": { "updated_at": {
"type": "date" "type": "date"
},
"can_stop": {
"type": "boolean"
} }
} }
} }
---
env:
jasmine: true
extends: plugin:jasmine/recommended
globals:
appendLoadFixtures: false
appendLoadStyleFixtures: false
appendSetFixtures: false
appendSetStyleFixtures: false
getJSONFixture: false
loadFixtures: false
loadJSONFixtures: false
loadStyleFixtures: false
preloadFixtures: false
preloadStyleFixtures: false
readFixtures: false
sandbox: false
setFixtures: false
setStyleFixtures: false
spyOnDependency: false
spyOnEvent: false
ClassSpecHelper: false
plugins:
- jasmine
rules:
func-names: off
jasmine/no-suite-dupes:
- warn
- branch
jasmine/no-spec-dupes:
- warn
- branch
no-console: off
prefer-arrow-callback: off
import/no-extraneous-dependencies:
- error
- devDependencies:
- ee/spec/**/*.js
require 'spec_helper'
describe Gitlab::Middleware::ReadOnly do
include Rack::Test::Methods
using RSpec::Parameterized::TableSyntax
let(:rack_stack) do
rack = Rack::Builder.new do
use ActionDispatch::Session::CacheStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
end
rack.run(subject)
rack.to_app
end
let(:observe_env) do
Module.new do
attr_reader :env
def call(env)
@env = env
super
end
end
end
let(:request) { Rack::MockRequest.new(rack_stack) }
subject do
described_class.new(fake_app).tap do |app|
app.extend(observe_env)
end
end
context 'normal requests to a read-only Gitlab instance' do
let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } }
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
end
context 'whitelisted requests' do
it 'expects a PATCH request to geo_nodes update URL to be allowed' do
expect(Rails.application.routes).to receive(:recognize_path).and_call_original
response = request.patch('/admin/geo_nodes/1')
expect(response).not_to be_redirect
expect(subject).not_to disallow_request
end
end
end
end
...@@ -89,9 +89,10 @@ module API ...@@ -89,9 +89,10 @@ module API
requires :environment_id, type: Integer, desc: 'The environment ID' requires :environment_id, type: Integer, desc: 'The environment ID'
end end
post ':id/environments/:environment_id/stop' do post ':id/environments/:environment_id/stop' do
authorize! :create_deployment, user_project authorize! :read_environment, user_project
environment = user_project.environments.find(params[:environment_id]) environment = user_project.environments.find(params[:environment_id])
authorize! :stop_environment, environment
environment.stop_with_action!(current_user) environment.stop_with_action!(current_user)
......
...@@ -2,6 +2,8 @@ module Gitlab ...@@ -2,6 +2,8 @@ module Gitlab
module Middleware module Middleware
class ReadOnly class ReadOnly
class Controller class Controller
prepend EE::Gitlab::Middleware::ReadOnly::Controller
DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
APPLICATION_JSON = 'application/json'.freeze APPLICATION_JSON = 'application/json'.freeze
APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-09 18:42-0500\n" "POT-Creation-Date: 2018-07-09 20:54+0200\n"
"PO-Revision-Date: 2018-07-09 18:42-0500\n" "PO-Revision-Date: 2018-07-09 20:54+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -2442,9 +2442,18 @@ msgstr "" ...@@ -2442,9 +2442,18 @@ msgstr ""
msgid "Environments|An error occurred while making the request." msgid "Environments|An error occurred while making the request."
msgstr "" msgstr ""
msgid "Environments|An error occurred while stopping the environment, please try again"
msgstr ""
msgid "Environments|Are you sure you want to stop this environment?"
msgstr ""
msgid "Environments|Commit" msgid "Environments|Commit"
msgstr "" msgstr ""
msgid "Environments|Deploy to..."
msgstr ""
msgid "Environments|Deployment" msgid "Environments|Deployment"
msgstr "" msgstr ""
...@@ -2457,6 +2466,9 @@ msgstr "" ...@@ -2457,6 +2466,9 @@ msgstr ""
msgid "Environments|Job" msgid "Environments|Job"
msgstr "" msgstr ""
msgid "Environments|Learn more about stopping environments"
msgstr ""
msgid "Environments|New environment" msgid "Environments|New environment"
msgstr "" msgstr ""
...@@ -2466,24 +2478,33 @@ msgstr "" ...@@ -2466,24 +2478,33 @@ msgstr ""
msgid "Environments|No pod name has been specified" msgid "Environments|No pod name has been specified"
msgstr "" msgstr ""
msgid "Environments|Open" msgid "Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file."
msgstr ""
msgid "Environments|Open live environment"
msgstr "" msgstr ""
msgid "Environments|Pod logs from" msgid "Environments|Pod logs from"
msgstr "" msgstr ""
msgid "Environments|Re-deploy" msgid "Environments|Re-deploy to environment"
msgstr "" msgstr ""
msgid "Environments|Read more about environments" msgid "Environments|Read more about environments"
msgstr "" msgstr ""
msgid "Environments|Rollback" msgid "Environments|Rollback environment"
msgstr "" msgstr ""
msgid "Environments|Show all" msgid "Environments|Show all"
msgstr "" msgstr ""
msgid "Environments|Stop"
msgstr ""
msgid "Environments|Stop environment"
msgstr ""
msgid "Environments|Updated" msgid "Environments|Updated"
msgstr "" msgstr ""
...@@ -4726,9 +4747,6 @@ msgstr "" ...@@ -4726,9 +4747,6 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes." msgid "Quick actions can be used in the issues description and comment boxes."
msgstr "" msgstr ""
msgid "Re-deploy"
msgstr ""
msgid "Read more" msgid "Read more"
msgstr "" msgstr ""
...@@ -4887,9 +4905,6 @@ msgstr "" ...@@ -4887,9 +4905,6 @@ msgstr ""
msgid "Roadmap" msgid "Roadmap"
msgstr "" msgstr ""
msgid "Rollback"
msgstr ""
msgid "Run CI/CD pipelines for external repositories" msgid "Run CI/CD pipelines for external repositories"
msgstr "" msgstr ""
......
...@@ -166,7 +166,8 @@ describe 'Environment' do ...@@ -166,7 +166,8 @@ describe 'Environment' do
end end
it 'allows to stop environment' do it 'allows to stop environment' do
click_link('Stop') click_button('Stop')
click_button('Stop environment') # confirm modal
expect(page).to have_content('close_app') expect(page).to have_content('close_app')
end end
...@@ -174,7 +175,7 @@ describe 'Environment' do ...@@ -174,7 +175,7 @@ describe 'Environment' do
context 'when user has no ability to stop environment' do context 'when user has no ability to stop environment' do
it 'does not allow to stop environment' do it 'does not allow to stop environment' do
expect(page).to have_no_link('Stop') expect(page).not_to have_button('Stop')
end end
end end
...@@ -182,7 +183,7 @@ describe 'Environment' do ...@@ -182,7 +183,7 @@ describe 'Environment' do
let(:role) { :reporter } let(:role) { :reporter }
it 'does not show stop button' do it 'does not show stop button' do
expect(page).not_to have_link('Stop') expect(page).not_to have_button('Stop')
end end
end end
end end
...@@ -192,7 +193,7 @@ describe 'Environment' do ...@@ -192,7 +193,7 @@ describe 'Environment' do
let(:environment) { create(:environment, project: project, state: :stopped) } let(:environment) { create(:environment, project: project, state: :stopped) }
it 'does not show stop button' do it 'does not show stop button' do
expect(page).not_to have_link('Stop') expect(page).not_to have_button('Stop')
end end
end end
end end
...@@ -230,7 +231,7 @@ describe 'Environment' do ...@@ -230,7 +231,7 @@ describe 'Environment' do
it 'user visits environment page' do it 'user visits environment page' do
visit_environment(environment) visit_environment(environment)
expect(page).to have_link('Stop') expect(page).to have_button('Stop')
end end
it 'user deletes the branch with running environment' do it 'user deletes the branch with running environment' do
...@@ -242,7 +243,7 @@ describe 'Environment' do ...@@ -242,7 +243,7 @@ describe 'Environment' do
visit_environment(environment) visit_environment(environment)
expect(page).to have_no_link('Stop') expect(page).not_to have_button('Stop')
end end
## ##
......
...@@ -10,6 +10,10 @@ describe 'Environments page', :js do ...@@ -10,6 +10,10 @@ describe 'Environments page', :js do
sign_in(user) sign_in(user)
end end
def stop_button_selector
%q{button[data-original-title="Stop environment"]}
end
describe 'page tabs' do describe 'page tabs' do
it 'shows "Available" and "Stopped" tab with links' do it 'shows "Available" and "Stopped" tab with links' do
visit_environments(project) visit_environments(project)
...@@ -120,7 +124,7 @@ describe 'Environments page', :js do ...@@ -120,7 +124,7 @@ describe 'Environments page', :js do
end end
it 'does not show stip button when environment is not stoppable' do it 'does not show stip button when environment is not stoppable' do
expect(page).not_to have_selector('.stop-env-link') expect(page).not_to have_selector(stop_button_selector)
end end
end end
...@@ -178,7 +182,7 @@ describe 'Environments page', :js do ...@@ -178,7 +182,7 @@ describe 'Environments page', :js do
end end
it 'shows a stop button' do it 'shows a stop button' do
expect(page).not_to have_selector('.stop-env-link') expect(page).not_to have_selector(stop_button_selector)
end end
it 'does not show external link button' do it 'does not show external link button' do
...@@ -211,14 +215,14 @@ describe 'Environments page', :js do ...@@ -211,14 +215,14 @@ describe 'Environments page', :js do
end end
it 'shows a stop button' do it 'shows a stop button' do
expect(page).to have_selector('.stop-env-link') expect(page).to have_selector(stop_button_selector)
end end
context 'when user is a reporter' do context 'when user is a reporter' do
let(:role) { :reporter } let(:role) { :reporter }
it 'does not show stop button' do it 'does not show stop button' do
expect(page).not_to have_selector('.stop-env-link') expect(page).not_to have_selector(stop_button_selector)
end end
end end
end end
......
...@@ -18,7 +18,7 @@ describe('Rollback Component', () => { ...@@ -18,7 +18,7 @@ describe('Rollback Component', () => {
}, },
}).$mount(); }).$mount();
expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); expect(component.$el).toHaveSpriteIcon('repeat');
}); });
it('Should render Rollback label when isLastDeployment is false', () => { it('Should render Rollback label when isLastDeployment is false', () => {
...@@ -30,6 +30,6 @@ describe('Rollback Component', () => { ...@@ -30,6 +30,6 @@ describe('Rollback Component', () => {
}, },
}).$mount(); }).$mount();
expect(component.$el.querySelector('span').textContent).toContain('Rollback'); expect(component.$el).toHaveSpriteIcon('redo');
}); });
}); });
...@@ -4,7 +4,6 @@ import stopComp from '~/environments/components/environment_stop.vue'; ...@@ -4,7 +4,6 @@ import stopComp from '~/environments/components/environment_stop.vue';
describe('Stop Component', () => { describe('Stop Component', () => {
let StopComponent; let StopComponent;
let component; let component;
const stopURL = '/stop';
beforeEach(() => { beforeEach(() => {
StopComponent = Vue.extend(stopComp); StopComponent = Vue.extend(stopComp);
...@@ -12,20 +11,13 @@ describe('Stop Component', () => { ...@@ -12,20 +11,13 @@ describe('Stop Component', () => {
component = new StopComponent({ component = new StopComponent({
propsData: { propsData: {
stopUrl: stopURL, environment: {},
}, },
}).$mount(); }).$mount();
}); });
describe('computed', () => {
it('title', () => {
expect(component.title).toEqual('Stop');
});
});
it('should render a button to stop the environment', () => { it('should render a button to stop the environment', () => {
expect(component.$el.tagName).toEqual('BUTTON'); expect(component.$el.tagName).toEqual('BUTTON');
expect(component.$el.getAttribute('data-original-title')).toEqual('Stop'); expect(component.$el.getAttribute('data-original-title')).toEqual('Stop environment');
expect(component.$el.getAttribute('aria-label')).toEqual('Stop');
}); });
}); });
...@@ -85,10 +85,15 @@ beforeEach(() => { ...@@ -85,10 +85,15 @@ beforeEach(() => {
const axiosDefaultAdapter = getDefaultAdapter(); const axiosDefaultAdapter = getDefaultAdapter();
// render all of our tests // render all of our tests
const testsContext = require.context('.', true, /_spec$/); const testContexts = [
testsContext.keys().forEach(function(path) { require.context('spec', true, /_spec$/),
require.context('ee_spec', true, /_spec$/),
];
testContexts.forEach(context => {
context.keys().forEach(path => {
try { try {
testsContext(path); context(path);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path); console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path);
...@@ -98,6 +103,7 @@ testsContext.keys().forEach(function(path) { ...@@ -98,6 +103,7 @@ testsContext.keys().forEach(function(path) {
}); });
}); });
} }
});
}); });
describe('test errors', () => { describe('test errors', () => {
...@@ -166,19 +172,25 @@ if (process.env.BABEL_ENV === 'coverage') { ...@@ -166,19 +172,25 @@ if (process.env.BABEL_ENV === 'coverage') {
]; ];
describe('Uncovered files', function() { describe('Uncovered files', function() {
const sourceFiles = require.context('~', true, /\.js$/); const sourceFilesContexts = [
require.context('~', true, /\.js$/),
require.context('ee', true, /\.js$/),
];
const allTestFiles = testContexts.reduce((accumulator, context) =>
accumulator.concat(context.keys()), []);
$.holdReady(true); $.holdReady(true);
sourceFiles.keys().forEach(function(path) { sourceFilesContexts.forEach(context => {
context.keys().forEach(path => {
// ignore if there is a matching spec file // ignore if there is a matching spec file
if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) { if (allTestFiles.indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
return; return;
} }
it(`includes '${path}'`, function() { it(`includes '${path}'`, function() {
try { try {
sourceFiles(path); context(path);
} catch (err) { } catch (err) {
if (troubleMakers.indexOf(path) === -1) { if (troubleMakers.indexOf(path) === -1) {
expect(err).toBeNull(); expect(err).toBeNull();
...@@ -187,4 +199,5 @@ if (process.env.BABEL_ENV === 'coverage') { ...@@ -187,4 +199,5 @@ if (process.env.BABEL_ENV === 'coverage') {
}); });
}); });
}); });
});
} }
require 'spec_helper' require 'spec_helper'
describe EnvironmentPolicy do describe EnvironmentPolicy do
let(:user) { create(:user) } using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :repository) }
let(:environment) do let(:user) { create(:user) }
create(:environment, :with_review_app, project: project)
end
let(:policy) do let(:policy) do
described_class.new(user, environment) described_class.new(user, environment)
end end
describe '#rules' do describe '#rules' do
context 'when user does not have access to the project' do shared_examples 'project permissions' do
let(:project) { create(:project, :private, :repository) } context 'with stop action' do
let(:environment) do
create(:environment, :with_review_app, project: project)
end
where(:access_level, :allowed?) do
nil | false
:guest | false
:reporter | false
:developer | true
:master | true
end
it 'does not include ability to stop environment' do with_them do
expect(policy).to be_disallowed :stop_environment before do
project.add_user(user, access_level) unless access_level.nil?
end end
it { expect(policy.allowed?(:stop_environment)).to be allowed? }
end end
context 'when anonymous user has access to the project' do context 'when an admin user' do
let(:project) { create(:project, :public, :repository) } let(:user) { create(:user, :admin) }
it 'does not include ability to stop environment' do it { expect(policy).to be_allowed :stop_environment }
expect(policy).to be_disallowed :stop_environment
end end
context 'with protected branch' do
with_them do
before do
project.add_user(user, access_level) unless access_level.nil?
create(:protected_branch, :no_one_can_push,
name: 'master', project: project)
end end
context 'when team member has access to the project' do it { expect(policy).to be_disallowed :stop_environment }
let(:project) { create(:project, :public, :repository) } end
before do context 'when an admin user' do
project.add_developer(user) let(:user) { create(:user, :admin) }
it { expect(policy).to be_allowed :stop_environment }
end
end
end end
context 'when team member has ability to stop environment' do context 'without stop action' do
it 'does includes ability to stop environment' do let(:environment) do
expect(policy).to be_allowed :stop_environment create(:environment, project: project)
end end
where(:access_level, :allowed?) do
nil | false
:guest | false
:reporter | false
:developer | false
:master | true
end end
context 'when team member has no ability to stop environment' do with_them do
before do before do
create(:protected_branch, :no_one_can_push, project.add_user(user, access_level) unless access_level.nil?
name: 'master', project: project)
end end
it 'does not include ability to stop environment' do it { expect(policy.allowed?(:stop_environment)).to be allowed? }
expect(policy).to be_disallowed :stop_environment
end end
context 'when an admin user' do
let(:user) { create(:user, :admin) }
it { expect(policy).to be_allowed :stop_environment }
end end
end end
end end
context 'when project is public' do
let(:project) { create(:project, :public, :repository) }
include_examples 'project permissions'
end
context 'when project is private' do
let(:project) { create(:project, :private, :repository) }
include_examples 'project permissions'
end
end
end end
...@@ -54,7 +54,9 @@ describe EnvironmentSerializer do ...@@ -54,7 +54,9 @@ describe EnvironmentSerializer do
context 'when representing environments within folders' do context 'when representing environments within folders' do
let(:serializer) do let(:serializer) do
described_class.new(current_user: user, project: project).within_folders described_class
.new(current_user: user, project: project)
.within_folders
end end
let(:resource) { Environment.all } let(:resource) { Environment.all }
...@@ -123,7 +125,8 @@ describe EnvironmentSerializer do ...@@ -123,7 +125,8 @@ describe EnvironmentSerializer do
let(:pagination) { { page: 1, per_page: 2 } } let(:pagination) { { page: 1, per_page: 2 } }
let(:serializer) do let(:serializer) do
described_class.new(current_user: user, project: project) described_class
.new(current_user: user, project: project)
.with_pagination(request, response) .with_pagination(request, response)
end end
...@@ -169,7 +172,8 @@ describe EnvironmentSerializer do ...@@ -169,7 +172,8 @@ describe EnvironmentSerializer do
context 'when grouping environments within folders' do context 'when grouping environments within folders' do
let(:serializer) do let(:serializer) do
described_class.new(current_user: user, project: project) described_class
.new(current_user: user, project: project)
.with_pagination(request, response) .with_pagination(request, response)
.within_folders .within_folders
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment