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>
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
components: {
loadingIcon,
Icon,
},
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
components: {
loadingIcon,
Icon,
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return 'Deploy to...';
},
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return 'Deploy to...';
},
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
eventHub.$emit('postAction', endpoint);
},
eventHub.$emit('postAction', { endpoint });
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
return !action.playable;
},
};
},
};
</script>
<template>
<div
......@@ -61,10 +61,7 @@
data-toggle="dropdown"
>
<span>
<icon
:size="12"
name="play"
/>
<icon name="play" />
<i
class="fa fa-caret-down"
aria-hidden="true"
......@@ -85,10 +82,6 @@
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
>
<icon
:size="12"
name="play"
/>
<span>
{{ action.name }}
</span>
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
/**
* Renders the external url link in environments table.
*/
export default {
components: {
Icon,
/**
* Renders the external url link in environments table.
*/
export default {
components: {
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>
<template>
<a
......@@ -37,9 +37,6 @@
target="_blank"
rel="noopener noreferrer nofollow"
>
<icon
:size="12"
name="external-link"
/>
<icon name="external-link" />
</a>
</template>
<script>
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
export default {
components: {
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>
<template>
<a
......@@ -35,9 +35,6 @@
data-container="body"
rel="noopener noreferrer nofollow"
>
<icon
:size="12"
name="chart"
/>
<icon name="chart" />
</a>
</template>
<script>
/**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`.
*
* Makes a post request when the button is clicked.
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
loadingIcon,
/**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`.
*
* Makes a post request when the button is clicked.
*/
import { s__ } from '~/locale';
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 {
components: {
Icon,
LoadingIcon,
},
directives: {
tooltip,
},
props: {
retryUrl: {
type: String,
default: '',
},
props: {
retryUrl: {
type: String,
default: '',
},
isLastDeployment: {
type: Boolean,
default: true,
},
isLastDeployment: {
type: Boolean,
default: true,
},
data() {
return {
isLoading: false,
};
},
data() {
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>
<template>
<button
v-tooltip
:disabled="isLoading"
:title="title"
type="button"
class="btn d-none d-sm-none d-md-block"
@click="onClick"
>
<span v-if="isLastDeployment">
{{ s__("Environments|Re-deploy") }}
</span>
<span v-else>
{{ s__("Environments|Rollback") }}
</span>
<icon
v-if="isLastDeployment"
name="repeat" />
<icon
v-else
name="redo"/>
<loading-icon v-if="isLoading" />
</button>
......
<script>
/**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
/**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
import $ from 'jquery';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
loadingIcon,
},
export default {
components: {
Icon,
LoadingButton,
},
directives: {
tooltip,
},
directives: {
tooltip,
},
props: {
stopUrl: {
type: String,
default: '',
},
props: {
environment: {
type: Object,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return 'Stop';
},
computed: {
title() {
return s__('Environments|Stop environment');
},
},
methods: {
onClick() {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
mounted() {
eventHub.$on('stopEnvironment', this.onStopEnvironment);
},
$(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>
<template>
<button
<loading-button
v-tooltip
:disabled="isLoading"
:loading="isLoading"
:title="title"
:aria-label="title"
type="button"
class="btn stop-env-link d-none d-sm-none d-md-block"
container-class="btn btn-danger d-none d-sm-none d-md-block"
data-container="body"
data-toggle="modal"
data-target="#stop-environment-modal"
@click="onClick"
>
<i
class="fa fa-stop stop-env-icon"
aria-hidden="true"
>
</i>
<loading-icon v-if="isLoading" />
</button>
<icon name="stop"/>
</loading-button>
</template>
<script>
/**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
/**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
export default {
components: {
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>
<template>
<a
......@@ -36,9 +36,6 @@
class="btn terminal-button d-none d-sm-none d-md-block"
data-container="body"
>
<icon
:size="12"
name="terminal"
/>
<icon name="terminal" />
</a>
</template>
......@@ -5,10 +5,12 @@
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from './stop_environment_modal.vue';
export default {
components: {
emptyState,
StopEnvironmentModal,
},
mixins: [
......@@ -100,6 +102,8 @@
</script>
<template>
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<div class="top-area">
<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>
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
export default {
components: {
StopEnvironmentModal,
},
mixins: [
environmentsMixin,
CIPaginationMixin,
],
props: {
endpoint: {
type: String,
......@@ -38,6 +44,8 @@
</script>
<template>
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<div
v-if="!isLoading"
class="top-area"
......
......@@ -40,6 +40,7 @@ export default {
scope: getParameterByName('scope') || 'available',
page: getParameterByName('page') || '1',
requestData: {},
environmentInStopModal: {},
};
},
......@@ -85,7 +86,7 @@ export default {
Flash(s__('Environments|An error occurred while fetching the environments.'));
},
postAction(endpoint) {
postAction({ endpoint, errorMessage }) {
if (!this.isMakingRequest) {
this.isLoading = true;
......@@ -93,7 +94,7 @@ export default {
.then(() => this.fetchEnvironments())
.catch(() => {
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 {
.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: {
......@@ -162,9 +172,13 @@ export default {
});
eventHub.$on('postAction', this.postAction);
eventHub.$on('requestStopEnvironment', this.updateStopModal);
eventHub.$on('stopEnvironment', this.stopEnvironment);
},
beforeDestroyed() {
eventHub.$off('postAction');
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
eventHub.$off('requestStopEnvironment', this.updateStopModal);
eventHub.$off('stopEnvironment', this.stopEnvironment);
},
};
......@@ -13,7 +13,7 @@ export default class EnvironmentsService {
// eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
return axios.post(endpoint, {}, { emulateJSON: true });
return axios.post(endpoint, {});
}
getFolderContent(folderUrl) {
......
......@@ -23,7 +23,7 @@
}
.btn-group {
> a {
> .btn:not(.btn-danger) {
color: $gl-text-color-secondary;
}
......
......@@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
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_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
......@@ -177,4 +177,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def environment
@environment ||= project.environments.find(params[:id])
end
def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment)
end
end
......@@ -196,7 +196,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
deployment = environment.first_deployment_for(@merge_request.diff_head_sha)
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)
end
......
class EnvironmentPolicy < BasePolicy
delegate { @subject.project }
condition(:stop_action_allowed) do
@subject.stop_action? && can?(:update_build, @subject.stop_action)
condition(:stop_with_deployment_allowed) do
@subject.stop_action? && can?(:create_deployment) && can?(:update_build, @subject.stop_action)
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
......@@ -9,7 +9,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url
expose :environment_type
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
......@@ -36,6 +36,10 @@ class EnvironmentEntity < Grape::Entity
expose :created_at, :updated_at
expose :can_stop do |environment|
environment.available? && can?(current_user, :stop_environment, environment)
end
private
alias_method :environment, :object
......
......@@ -3,13 +3,12 @@
- if actions.present?
.btn-group
.dropdown
%button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
%button.dropdown.dropdown-new.btn.btn-default.has-tooltip{ type: 'button', 'data-toggle' => 'dropdown', title: s_('Environments|Deploy to...') }
= sprite_icon('play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-right
- actions.each do |action|
- next unless can?(current_user, :update_build, action)
%li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow', class: 'btn' do
%span= action.name.humanize
- 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?
= _("Re-deploy")
= sprite_icon('repeat')
- else
= _("Rollback")
= sprite_icon('redo')
- 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')
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 @@
- page_title "Environments"
%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
.col-md-7
%h3.page-title= @environment.name
......@@ -15,7 +42,10 @@
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_project_environment_path(@project, @environment), class: 'btn'
- 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
- 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) {
],
preprocessors: {
'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
'ee/spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
},
reporters: [progressReporter],
webpack: webpackConfig,
......
......@@ -107,6 +107,7 @@ module.exports = {
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_images: path.join(ROOT_PATH, 'ee/app/assets/images'),
ee_spec: path.join(ROOT_PATH, 'ee/spec/javascripts'),
},
},
......
......@@ -3,18 +3,21 @@
> [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
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
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:**
In the future, we plan to also support merge requests from the Development Panel.
This integration connects all GitLab projects within a top-level group or a personal namespace to projects in the JIRA instance.
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
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
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
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)
......@@ -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)
4. Repeat the above steps for each GitLab group's projects that you want to be made known to JIRA.
Specify the GitLab group name accordingly. (Note that you can also specify GitLab user names, as they
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.)
To connect additional GitLab projects from other GitLab top-level groups (or personal namespaces), repeat the above
steps with additional JIRA DVCS accounts.
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
......
......@@ -36,7 +36,7 @@ here's a quick guide:
- 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.
- 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 a bug in display or sound, use `|`: `bug display | sound`
- To group terms together, use parentheses: `bug | (display +sound)`
......
<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 CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import WidgetApprovals from './components/approvals/mr_widget_approvals.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 {
components: {
......
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 {
constructor(data) {
......
......@@ -13,7 +13,7 @@ module EE
{
primary_version: version.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
}
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
end
def self.database_secondary?
ActiveRecord::Base.connection.execute('SELECT pg_is_in_recovery()')
.first
.fetch('pg_is_in_recovery') == 't'
Gitlab::Database.db_read_only?
end
def self.db_replication_lag_seconds
......
......@@ -9,7 +9,7 @@ module SystemCheck
end
def check?
ActiveRecord::Base.connection.execute('SELECT pg_is_in_recovery()').first.fetch('pg_is_in_recovery') == 't'
Gitlab::Database.db_read_only?
end
def show_error
......
......@@ -39,7 +39,7 @@
}
]
},
"stop_action?": {
"has_stop_action": {
"type": "boolean"
},
"rollout_status": {
......@@ -65,6 +65,9 @@
},
"updated_at": {
"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
requires :environment_id, type: Integer, desc: 'The environment ID'
end
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])
authorize! :stop_environment, environment
environment.stop_with_action!(current_user)
......
......@@ -2,6 +2,8 @@ module Gitlab
module Middleware
class ReadOnly
class Controller
prepend EE::Gitlab::Middleware::ReadOnly::Controller
DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
APPLICATION_JSON = 'application/json'.freeze
APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze
......
......@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-09 18:42-0500\n"
"PO-Revision-Date: 2018-07-09 18:42-0500\n"
"POT-Creation-Date: 2018-07-09 20:54+0200\n"
"PO-Revision-Date: 2018-07-09 20:54+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -2442,9 +2442,18 @@ msgstr ""
msgid "Environments|An error occurred while making the request."
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"
msgstr ""
msgid "Environments|Deploy to..."
msgstr ""
msgid "Environments|Deployment"
msgstr ""
......@@ -2457,6 +2466,9 @@ msgstr ""
msgid "Environments|Job"
msgstr ""
msgid "Environments|Learn more about stopping environments"
msgstr ""
msgid "Environments|New environment"
msgstr ""
......@@ -2466,24 +2478,33 @@ msgstr ""
msgid "Environments|No pod name has been specified"
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 ""
msgid "Environments|Pod logs from"
msgstr ""
msgid "Environments|Re-deploy"
msgid "Environments|Re-deploy to environment"
msgstr ""
msgid "Environments|Read more about environments"
msgstr ""
msgid "Environments|Rollback"
msgid "Environments|Rollback environment"
msgstr ""
msgid "Environments|Show all"
msgstr ""
msgid "Environments|Stop"
msgstr ""
msgid "Environments|Stop environment"
msgstr ""
msgid "Environments|Updated"
msgstr ""
......@@ -4726,9 +4747,6 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
msgid "Re-deploy"
msgstr ""
msgid "Read more"
msgstr ""
......@@ -4887,9 +4905,6 @@ msgstr ""
msgid "Roadmap"
msgstr ""
msgid "Rollback"
msgstr ""
msgid "Run CI/CD pipelines for external repositories"
msgstr ""
......
......@@ -166,7 +166,8 @@ describe 'Environment' do
end
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')
end
......@@ -174,7 +175,7 @@ describe 'Environment' do
context 'when user has no ability 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
......@@ -182,7 +183,7 @@ describe 'Environment' do
let(:role) { :reporter }
it 'does not show stop button' do
expect(page).not_to have_link('Stop')
expect(page).not_to have_button('Stop')
end
end
end
......@@ -192,7 +193,7 @@ describe 'Environment' do
let(:environment) { create(:environment, project: project, state: :stopped) }
it 'does not show stop button' do
expect(page).not_to have_link('Stop')
expect(page).not_to have_button('Stop')
end
end
end
......@@ -230,7 +231,7 @@ describe 'Environment' do
it 'user visits environment page' do
visit_environment(environment)
expect(page).to have_link('Stop')
expect(page).to have_button('Stop')
end
it 'user deletes the branch with running environment' do
......@@ -242,7 +243,7 @@ describe 'Environment' do
visit_environment(environment)
expect(page).to have_no_link('Stop')
expect(page).not_to have_button('Stop')
end
##
......
......@@ -10,6 +10,10 @@ describe 'Environments page', :js do
sign_in(user)
end
def stop_button_selector
%q{button[data-original-title="Stop environment"]}
end
describe 'page tabs' do
it 'shows "Available" and "Stopped" tab with links' do
visit_environments(project)
......@@ -120,7 +124,7 @@ describe 'Environments page', :js do
end
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
......@@ -178,7 +182,7 @@ describe 'Environments page', :js do
end
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
it 'does not show external link button' do
......@@ -211,14 +215,14 @@ describe 'Environments page', :js do
end
it 'shows a stop button' do
expect(page).to have_selector('.stop-env-link')
expect(page).to have_selector(stop_button_selector)
end
context 'when user is a reporter' do
let(:role) { :reporter }
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
......
......@@ -18,7 +18,7 @@ describe('Rollback Component', () => {
},
}).$mount();
expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
expect(component.$el).toHaveSpriteIcon('repeat');
});
it('Should render Rollback label when isLastDeployment is false', () => {
......@@ -30,6 +30,6 @@ describe('Rollback Component', () => {
},
}).$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';
describe('Stop Component', () => {
let StopComponent;
let component;
const stopURL = '/stop';
beforeEach(() => {
StopComponent = Vue.extend(stopComp);
......@@ -12,20 +11,13 @@ describe('Stop Component', () => {
component = new StopComponent({
propsData: {
stopUrl: stopURL,
environment: {},
},
}).$mount();
});
describe('computed', () => {
it('title', () => {
expect(component.title).toEqual('Stop');
});
});
it('should render a button to stop the environment', () => {
expect(component.$el.tagName).toEqual('BUTTON');
expect(component.$el.getAttribute('data-original-title')).toEqual('Stop');
expect(component.$el.getAttribute('aria-label')).toEqual('Stop');
expect(component.$el.getAttribute('data-original-title')).toEqual('Stop environment');
});
});
......@@ -85,19 +85,25 @@ beforeEach(() => {
const axiosDefaultAdapter = getDefaultAdapter();
// render all of our tests
const testsContext = require.context('.', true, /_spec$/);
testsContext.keys().forEach(function(path) {
try {
testsContext(path);
} catch (err) {
console.log(err);
console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path);
describe('Test bundle', function() {
it(`includes '${path}'`, function() {
expect(err).toBeNull();
const testContexts = [
require.context('spec', true, /_spec$/),
require.context('ee_spec', true, /_spec$/),
];
testContexts.forEach(context => {
context.keys().forEach(path => {
try {
context(path);
} catch (err) {
console.log(err);
console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path);
describe('Test bundle', function() {
it(`includes '${path}'`, function() {
expect(err).toBeNull();
});
});
});
}
}
});
});
describe('test errors', () => {
......@@ -166,24 +172,31 @@ if (process.env.BABEL_ENV === 'coverage') {
];
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);
sourceFiles.keys().forEach(function(path) {
// ignore if there is a matching spec file
if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
return;
}
it(`includes '${path}'`, function() {
try {
sourceFiles(path);
} catch (err) {
if (troubleMakers.indexOf(path) === -1) {
expect(err).toBeNull();
}
sourceFilesContexts.forEach(context => {
context.keys().forEach(path => {
// ignore if there is a matching spec file
if (allTestFiles.indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
return;
}
it(`includes '${path}'`, function() {
try {
context(path);
} catch (err) {
if (troubleMakers.indexOf(path) === -1) {
expect(err).toBeNull();
}
}
});
});
});
});
......
require 'spec_helper'
describe EnvironmentPolicy do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
using RSpec::Parameterized::TableSyntax
let(:environment) do
create(:environment, :with_review_app, project: project)
end
let(:user) { create(:user) }
let(:policy) do
described_class.new(user, environment)
end
describe '#rules' do
context 'when user does not have access to the project' do
let(:project) { create(:project, :private, :repository) }
shared_examples 'project permissions' do
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
expect(policy).to be_disallowed :stop_environment
end
end
where(:access_level, :allowed?) do
nil | false
:guest | false
:reporter | false
:developer | true
:master | true
end
context 'when anonymous user has access to the project' do
let(:project) { create(:project, :public, :repository) }
with_them do
before do
project.add_user(user, access_level) unless access_level.nil?
end
it 'does not include ability to stop environment' do
expect(policy).to be_disallowed :stop_environment
end
end
it { expect(policy.allowed?(:stop_environment)).to be allowed? }
end
context 'when team member has access to the project' do
let(:project) { create(:project, :public, :repository) }
context 'when an admin user' do
let(:user) { create(:user, :admin) }
before do
project.add_developer(user)
end
it { expect(policy).to be_allowed :stop_environment }
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 'does includes ability to stop environment' do
expect(policy).to be_allowed :stop_environment
it { expect(policy).to be_disallowed :stop_environment }
end
context 'when an admin user' do
let(:user) { create(:user, :admin) }
it { expect(policy).to be_allowed :stop_environment }
end
end
end
context 'when team member has no ability to stop environment' do
before do
create(:protected_branch, :no_one_can_push,
name: 'master', project: project)
context 'without stop action' do
let(:environment) do
create(:environment, project: project)
end
where(:access_level, :allowed?) do
nil | false
:guest | false
:reporter | false
:developer | false
:master | true
end
it 'does not include ability to stop environment' do
expect(policy).to be_disallowed :stop_environment
with_them do
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
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
......@@ -54,7 +54,9 @@ describe EnvironmentSerializer do
context 'when representing environments within folders' 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
let(:resource) { Environment.all }
......@@ -123,7 +125,8 @@ describe EnvironmentSerializer do
let(:pagination) { { page: 1, per_page: 2 } }
let(:serializer) do
described_class.new(current_user: user, project: project)
described_class
.new(current_user: user, project: project)
.with_pagination(request, response)
end
......@@ -169,7 +172,8 @@ describe EnvironmentSerializer do
context 'when grouping environments within folders' do
let(:serializer) do
described_class.new(current_user: user, project: project)
described_class
.new(current_user: user, project: project)
.with_pagination(request, response)
.within_folders
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