Commit 2f96930d authored by Phil Hughes's avatar Phil Hughes

Merge branch 'winh-stop-all-environments' into 'master'

Support manually stopping any environment from the UI

Closes #25388

See merge request gitlab-org/gitlab-ce!20077
parents ca1deb9e d79cef3a
<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,
},
components: {
loadingIcon,
Icon,
},
props: {
actions: {
type: Array,
required: false,
default: () => [],
}, },
components: { },
loadingIcon, data() {
Icon, return {
isLoading: false,
};
},
computed: {
title() {
return 'Deploy to...';
}, },
props: { },
actions: { methods: {
type: Array, onClickAction(endpoint) {
required: false, this.isLoading = true;
default: () => [],
},
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return 'Deploy to...';
},
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
eventHub.$emit('postAction', endpoint); eventHub.$emit('postAction', { endpoint });
}, },
isActionDisabled(action) { isActionDisabled(action) {
if (action.playable === undefined) { if (action.playable === undefined) {
return false; return false;
} }
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,
},
directives: {
tooltip,
},
props: {
externalUrl: {
type: String,
required: true,
}, },
directives: { },
tooltip, computed: {
title() {
return s__('Environments|Open live environment');
}, },
props: { },
externalUrl: { };
type: String,
required: true,
},
},
computed: {
title() {
return s__('Environments|Open');
},
},
};
</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>
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility'; import { humanize } from '~/lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue'; import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue'; import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue'; import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue'; import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue'; import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
/** /**
* Envrionment Item Component * Envrionment Item Component
* *
* Renders a table row for each environment. * Renders a table row for each environment.
*/ */
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
export default { export default {
components: { components: {
UserAvatarLink, UserAvatarLink,
CommitComponent, CommitComponent,
ActionsComponent, ActionsComponent,
ExternalUrlComponent, ExternalUrlComponent,
StopComponent, StopComponent,
RollbackComponent, RollbackComponent,
TerminalButtonComponent, TerminalButtonComponent,
MonitoringButtonComponent, MonitoringButtonComponent,
},
directives: {
tooltip,
},
props: {
model: {
type: Object,
required: true,
default: () => ({}),
}, },
directives: { canCreateDeployment: {
tooltip, type: Boolean,
required: false,
default: false,
}, },
props: { canReadEnvironment: {
model: { type: Boolean,
type: Object, required: false,
required: true, default: false,
default: () => ({}), },
}, },
canCreateDeployment: { computed: {
type: Boolean, /**
required: false, * Verifies if `last_deployment` key exists in the current Envrionment.
default: false, * This key is required to render most of the html - this method works has
}, * an helper.
*
canReadEnvironment: { * @returns {Boolean}
type: Boolean, */
required: false, hasLastDeploymentKey() {
default: false, if (this.model && this.model.last_deployment && !_.isEmpty(this.model.last_deployment)) {
}, return true;
}
return false;
},
/**
* Verifies is the given environment has manual actions.
* Used to verify if we should render them or nor.
*
* @returns {Boolean|Undefined}
*/
hasManualActions() {
return (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.manual_actions &&
this.model.last_deployment.manual_actions.length > 0
);
},
/**
* Returns whether the environment can be stopped.
*
* @returns {Boolean}
*/
canStopEnvironment() {
return this.model && this.model.can_stop;
},
/**
* Verifies if the `deployable` key is present in `last_deployment` key.
* Used to verify whether we should or not render the rollback partial.
*
* @returns {Boolean|Undefined}
*/
canRetry() {
return (
this.model &&
this.hasLastDeploymentKey &&
this.model.last_deployment &&
this.model.last_deployment.deployable
);
},
/**
* Verifies if the date to be shown is present.
*
* @returns {Boolean|Undefined}
*/
canShowDate() {
return (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable !== undefined
);
},
/**
* Human readable date.
*
* @returns {String}
*/
createdDate() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.created_at
) {
return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
}
return '';
},
/**
* Returns the manual actions with the name parsed.
*
* @returns {Array.<Object>|Undefined}
*/
manualActions() {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map(action => {
const parsedAction = {
name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
return parsedAction;
});
}
return [];
},
/**
* Builds the string used in the user image alt attribute.
*
* @returns {String}
*/
userImageAltDescription() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.user &&
this.model.last_deployment.user.username
) {
return `${this.model.last_deployment.user.username}'s avatar'`;
}
return '';
},
/**
* If provided, returns the commit tag.
*
* @returns {String|Undefined}
*/
commitTag() {
if (this.model && this.model.last_deployment && this.model.last_deployment.tag) {
return this.model.last_deployment.tag;
}
return undefined;
},
/**
* If provided, returns the commit ref.
*
* @returns {Object|Undefined}
*/
commitRef() {
if (this.model && this.model.last_deployment && this.model.last_deployment.ref) {
return this.model.last_deployment.ref;
}
return undefined;
},
/**
* If provided, returns the commit url.
*
* @returns {String|Undefined}
*/
commitUrl() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.commit_path
) {
return this.model.last_deployment.commit.commit_path;
}
return undefined;
},
/**
* If provided, returns the commit short sha.
*
* @returns {String|Undefined}
*/
commitShortSha() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.short_id
) {
return this.model.last_deployment.commit.short_id;
}
return undefined;
},
/**
* If provided, returns the commit title.
*
* @returns {String|Undefined}
*/
commitTitle() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.title
) {
return this.model.last_deployment.commit.title;
}
return undefined;
},
/**
* If provided, returns the commit tag.
*
* @returns {Object|Undefined}
*/
commitAuthor() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.author
) {
return this.model.last_deployment.commit.author;
}
return undefined;
},
/**
* Verifies if the `retry_path` key is present and returns its value.
*
* @returns {String|Undefined}
*/
retryUrl() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.retry_path
) {
return this.model.last_deployment.deployable.retry_path;
}
return undefined;
},
/**
* Verifies if the `last?` key is present and returns its value.
*
* @returns {Boolean|Undefined}
*/
isLastDeployment() {
return this.model && this.model.last_deployment && this.model.last_deployment['last?'];
},
/**
* Builds the name of the builds needed to display both the name and the id.
*
* @returns {String}
*/
buildName() {
if (this.model && this.model.last_deployment && this.model.last_deployment.deployable) {
const { deployable } = this.model.last_deployment;
return `${deployable.name} #${deployable.id}`;
}
return '';
},
/**
* Builds the needed string to show the internal id.
*
* @returns {String}
*/
deploymentInternalId() {
if (this.model && this.model.last_deployment && this.model.last_deployment.iid) {
return `#${this.model.last_deployment.iid}`;
}
return '';
}, },
computed: { /**
/** * Verifies if the user object is present under last_deployment object.
* Verifies if `last_deployment` key exists in the current Envrionment. *
* This key is required to render most of the html - this method works has * @returns {Boolean}
* an helper. */
* deploymentHasUser() {
* @returns {Boolean} return (
*/ this.model &&
hasLastDeploymentKey() { !_.isEmpty(this.model.last_deployment) &&
if (this.model && !_.isEmpty(this.model.last_deployment.user)
this.model.last_deployment && );
!_.isEmpty(this.model.last_deployment)) {
return true;
}
return false;
},
/**
* Verifies is the given environment has manual actions.
* Used to verify if we should render them or nor.
*
* @returns {Boolean|Undefined}
*/
hasManualActions() {
return this.model &&
this.model.last_deployment &&
this.model.last_deployment.manual_actions &&
this.model.last_deployment.manual_actions.length > 0;
},
/**
* Returns the value of the `stop_action?` key provided in the response.
*
* @returns {Boolean}
*/
hasStopAction() {
return this.model && this.model['stop_action?'];
},
/**
* Verifies if the `deployable` key is present in `last_deployment` key.
* Used to verify whether we should or not render the rollback partial.
*
* @returns {Boolean|Undefined}
*/
canRetry() {
return this.model &&
this.hasLastDeploymentKey &&
this.model.last_deployment &&
this.model.last_deployment.deployable;
},
/**
* Verifies if the date to be shown is present.
*
* @returns {Boolean|Undefined}
*/
canShowDate() {
return this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable !== undefined;
},
/**
* Human readable date.
*
* @returns {String}
*/
createdDate() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.created_at) {
return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
}
return '';
},
/**
* Returns the manual actions with the name parsed.
*
* @returns {Array.<Object>|Undefined}
*/
manualActions() {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
return parsedAction;
});
}
return [];
},
/**
* Builds the string used in the user image alt attribute.
*
* @returns {String}
*/
userImageAltDescription() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.user &&
this.model.last_deployment.user.username) {
return `${this.model.last_deployment.user.username}'s avatar'`;
}
return '';
},
/**
* If provided, returns the commit tag.
*
* @returns {String|Undefined}
*/
commitTag() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.tag) {
return this.model.last_deployment.tag;
}
return undefined;
},
/**
* If provided, returns the commit ref.
*
* @returns {Object|Undefined}
*/
commitRef() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.ref) {
return this.model.last_deployment.ref;
}
return undefined;
},
/**
* If provided, returns the commit url.
*
* @returns {String|Undefined}
*/
commitUrl() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.commit_path) {
return this.model.last_deployment.commit.commit_path;
}
return undefined;
},
/**
* If provided, returns the commit short sha.
*
* @returns {String|Undefined}
*/
commitShortSha() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.short_id) {
return this.model.last_deployment.commit.short_id;
}
return undefined;
},
/**
* If provided, returns the commit title.
*
* @returns {String|Undefined}
*/
commitTitle() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.title) {
return this.model.last_deployment.commit.title;
}
return undefined;
},
/**
* If provided, returns the commit tag.
*
* @returns {Object|Undefined}
*/
commitAuthor() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.author) {
return this.model.last_deployment.commit.author;
}
return undefined;
},
/**
* Verifies if the `retry_path` key is present and returns its value.
*
* @returns {String|Undefined}
*/
retryUrl() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.retry_path) {
return this.model.last_deployment.deployable.retry_path;
}
return undefined;
},
/**
* Verifies if the `last?` key is present and returns its value.
*
* @returns {Boolean|Undefined}
*/
isLastDeployment() {
return this.model && this.model.last_deployment &&
this.model.last_deployment['last?'];
},
/**
* Builds the name of the builds needed to display both the name and the id.
*
* @returns {String}
*/
buildName() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable) {
const { deployable } = this.model.last_deployment;
return `${deployable.name} #${deployable.id}`;
}
return '';
},
/**
* Builds the needed string to show the internal id.
*
* @returns {String}
*/
deploymentInternalId() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.iid) {
return `#${this.model.last_deployment.iid}`;
}
return '';
},
/**
* Verifies if the user object is present under last_deployment object.
*
* @returns {Boolean}
*/
deploymentHasUser() {
return this.model &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.user);
},
/**
* Returns the user object nested with the last_deployment object.
* Used to render the template.
*
* @returns {Object}
*/
deploymentUser() {
if (this.model &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.user)) {
return this.model.last_deployment.user;
}
return {};
},
/**
* Verifies if the build name column should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderBuildName() {
return !this.model.isFolder &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.deployable);
},
/**
* Verifies the presence of all the keys needed to render the buil_path.
*
* @return {String}
*/
buildPath() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.build_path) {
return this.model.last_deployment.deployable.build_path;
}
return '';
},
/**
* Verifies the presence of all the keys needed to render the external_url.
*
* @return {String}
*/
externalURL() {
if (this.model && this.model.external_url) {
return this.model.external_url;
}
return '';
},
/**
* Verifies if deplyment internal ID should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderDeploymentID() {
return !this.model.isFolder &&
!_.isEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined;
},
environmentPath() {
if (this.model && this.model.environment_path) {
return this.model.environment_path;
}
return '';
},
monitoringUrl() {
if (this.model && this.model.metrics_path) {
return this.model.metrics_path;
}
return '';
},
displayEnvironmentActions() {
return this.hasManualActions ||
this.externalURL ||
this.monitoringUrl ||
this.hasStopAction ||
this.canRetry;
},
}, },
methods: { /**
onClickFolder() { * Returns the user object nested with the last_deployment object.
eventHub.$emit('toggleFolder', this.model); * Used to render the template.
}, *
* @returns {Object}
*/
deploymentUser() {
if (
this.model &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.user)
) {
return this.model.last_deployment.user;
}
return {};
}, },
};
/**
* Verifies if the build name column should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderBuildName() {
return (
!this.model.isFolder &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.deployable)
);
},
/**
* Verifies the presence of all the keys needed to render the buil_path.
*
* @return {String}
*/
buildPath() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.build_path
) {
return this.model.last_deployment.deployable.build_path;
}
return '';
},
/**
* Verifies the presence of all the keys needed to render the external_url.
*
* @return {String}
*/
externalURL() {
if (this.model && this.model.external_url) {
return this.model.external_url;
}
return '';
},
/**
* Verifies if deplyment internal ID should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderDeploymentID() {
return (
!this.model.isFolder &&
!_.isEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined
);
},
environmentPath() {
if (this.model && this.model.environment_path) {
return this.model.environment_path;
}
return '';
},
monitoringUrl() {
if (this.model && this.model.metrics_path) {
return this.model.metrics_path;
}
return '';
},
displayEnvironmentActions() {
return (
this.hasManualActions ||
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
this.canRetry
);
},
},
methods: {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model);
},
},
};
</script> </script>
<template> <template>
<div <div
...@@ -580,11 +601,6 @@ ...@@ -580,11 +601,6 @@
class="btn-group table-action-buttons" class="btn-group table-action-buttons"
role="group"> role="group">
<actions-component
v-if="hasManualActions && canCreateDeployment"
:actions="manualActions"
/>
<external-url-component <external-url-component
v-if="externalURL && canReadEnvironment" v-if="externalURL && canReadEnvironment"
:external-url="externalURL" :external-url="externalURL"
...@@ -595,21 +611,26 @@ ...@@ -595,21 +611,26 @@
:monitoring-url="monitoringUrl" :monitoring-url="monitoringUrl"
/> />
<actions-component
v-if="hasManualActions && canCreateDeployment"
:actions="manualActions"
/>
<terminal-button-component <terminal-button-component
v-if="model && model.terminal_path" v-if="model && model.terminal_path"
:terminal-path="model.terminal_path" :terminal-path="model.terminal_path"
/> />
<stop-component
v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
/>
<rollback-component <rollback-component
v-if="canRetry && canCreateDeployment" v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment" :is-last-deployment="isLastDeployment"
:retry-url="retryUrl" :retry-url="retryUrl"
/> />
<stop-component
v-if="canStopEnvironment"
:environment="model"
/>
</div> </div>
</div> </div>
</div> </div>
......
<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,
},
directives: {
tooltip,
},
props: {
monitoringUrl: {
type: String,
required: true,
}, },
directives: { },
tooltip, computed: {
title() {
return 'Monitoring';
}, },
props: { },
monitoringUrl: { };
type: String,
required: true,
},
},
computed: {
title() {
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';
export default { import eventHub from '../event_hub';
components: { import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
loadingIcon,
export default {
components: {
Icon,
LoadingIcon,
},
directives: {
tooltip,
},
props: {
retryUrl: {
type: String,
default: '',
}, },
props: {
retryUrl: { isLastDeployment: {
type: String, type: Boolean,
default: '', default: true,
},
isLastDeployment: {
type: Boolean,
default: true,
},
}, },
data() { },
return { data() {
isLoading: false, return {
}; isLoading: false,
};
},
computed: {
title() {
return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment');
}, },
methods: { },
onClick() {
this.isLoading = true; methods: {
onClick() {
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: {
tooltip, tooltip,
}, },
props: { props: {
stopUrl: { environment: {
type: String, type: Object,
default: '', required: true,
},
}, },
},
data() { data() {
return { return {
isLoading: false, isLoading: false,
}; };
}, },
computed: { computed: {
title() { title() {
return 'Stop'; return s__('Environments|Stop environment');
},
}, },
},
methods: { mounted() {
onClick() { eventHub.$on('stopEnvironment', this.onStopEnvironment);
// 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'); beforeDestroy() {
eventHub.$off('stopEnvironment', this.onStopEnvironment);
},
eventHub.$emit('postAction', this.stopUrl); methods: {
} onClick() {
}, $(this.$el).tooltip('dispose');
eventHub.$emit('requestStopEnvironment', this.environment);
},
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,
},
directives: {
tooltip,
},
props: {
terminalPath: {
type: String,
required: false,
default: '',
}, },
directives: { },
tooltip, computed: {
title() {
return 'Terminal';
}, },
props: { },
terminalPath: { };
type: String,
required: false,
default: '',
},
},
computed: {
title() {
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: [
...@@ -90,6 +92,8 @@ ...@@ -90,6 +92,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]
...@@ -175,4 +175,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -175,4 +175,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
...@@ -192,7 +192,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -192,7 +192,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
...@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity ...@@ -7,7 +7,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 :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment| expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment|
metrics_project_environment_path(environment.project, environment) metrics_project_environment_path(environment.project, environment)
...@@ -31,4 +31,14 @@ class EnvironmentEntity < Grape::Entity ...@@ -31,4 +31,14 @@ class EnvironmentEntity < Grape::Entity
end end
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
def current_user
request.current_user
end
end end
...@@ -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
...@@ -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)
......
...@@ -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 08:28+0200\n" "POT-Creation-Date: 2018-07-09 19:16+0200\n"
"PO-Revision-Date: 2018-07-09 08:28+0200\n" "PO-Revision-Date: 2018-07-09 19:16+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"
...@@ -2099,9 +2099,18 @@ msgstr "" ...@@ -2099,9 +2099,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 ""
...@@ -2114,27 +2123,39 @@ msgstr "" ...@@ -2114,27 +2123,39 @@ 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 ""
msgid "Environments|No deployments yet" msgid "Environments|No deployments yet"
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|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 ""
...@@ -3799,9 +3820,6 @@ msgstr "" ...@@ -3799,9 +3820,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 ""
...@@ -3936,9 +3954,6 @@ msgstr "" ...@@ -3936,9 +3954,6 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})" msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr "" msgstr ""
msgid "Rollback"
msgstr ""
msgid "Runner token" msgid "Runner token"
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');
}); });
}); });
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
it 'does not include ability to stop environment' do where(:access_level, :allowed?) do
expect(policy).to be_disallowed :stop_environment nil | false
end :guest | false
end :reporter | false
:developer | true
:master | true
end
context 'when anonymous user has access to the project' do with_them do
let(:project) { create(:project, :public, :repository) } before do
project.add_user(user, access_level) unless access_level.nil?
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
end
context 'when team member has access to the project' do context 'when an admin user' do
let(:project) { create(:project, :public, :repository) } let(:user) { create(:user, :admin) }
before do it { expect(policy).to be_allowed :stop_environment }
project.add_developer(user) 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
context 'when team member has ability to stop environment' do it { expect(policy).to be_disallowed :stop_environment }
it 'does includes ability to stop environment' do end
expect(policy).to be_allowed :stop_environment
context 'when an admin user' do
let(:user) { create(:user, :admin) }
it { expect(policy).to be_allowed :stop_environment }
end
end end
end end
context 'when team member has no ability to stop environment' do context 'without stop action' do
before do let(:environment) do
create(:protected_branch, :no_one_can_push, create(:environment, project: project)
name: 'master', project: project) end
where(:access_level, :allowed?) do
nil | false
:guest | false
:reporter | false
:developer | false
:master | true
end 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
it { expect(policy.allowed?(:stop_environment)).to be allowed? }
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 end
require 'spec_helper' require 'spec_helper'
describe EnvironmentEntity do describe EnvironmentEntity do
let(:request) { double('request') }
let(:entity) do let(:entity) do
described_class.new(environment, request: double) described_class.new(environment, request: spy('request'))
end end
let(:environment) { create(:environment) } let(:environment) { create(:environment) }
......
...@@ -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(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(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(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