Commit c5dcbff6 authored by Felipe Artur's avatar Felipe Artur

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee

parents 792e4ddb 0f73163e
...@@ -55,7 +55,15 @@ export default Vue.component('pipelines-table', { ...@@ -55,7 +55,15 @@ export default Vue.component('pipelines-table', {
}, },
shouldRenderEmptyState() { shouldRenderEmptyState() {
return !this.state.pipelines.length && !this.isLoading; return !this.state.pipelines.length &&
!this.isLoading &&
!this.hasError;
},
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
}, },
}, },
...@@ -145,8 +153,12 @@ export default Vue.component('pipelines-table', { ...@@ -145,8 +153,12 @@ export default Vue.component('pipelines-table', {
template: ` template: `
<div class="content-list pipelines"> <div class="content-list pipelines">
<div class="realtime-loading" v-if="isLoading"> <div
<i class="fa fa-spinner fa-spin"></i> class="realtime-loading"
v-if="isLoading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</div> </div>
<empty-state <empty-state
...@@ -155,8 +167,9 @@ export default Vue.component('pipelines-table', { ...@@ -155,8 +167,9 @@ export default Vue.component('pipelines-table', {
<error-state v-if="shouldRenderErrorState" /> <error-state v-if="shouldRenderErrorState" />
<div class="table-holder" <div
v-if="!isLoading && state.pipelines.length > 0"> class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:service="service" /> :service="service" />
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global Flash */ /* global Flash */
import Vue from 'vue'; import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table'; import EnvironmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination'; import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
......
<script>
/* global Flash */ /* global Flash */
/* eslint-disable no-new */ /* eslint-disable no-new */
...@@ -15,7 +16,6 @@ export default { ...@@ -15,7 +16,6 @@ export default {
service: { service: {
type: Object, type: Object,
required: true, required: true,
default: () => ({}),
}, },
}, },
...@@ -57,45 +57,47 @@ export default { ...@@ -57,45 +57,47 @@ export default {
return !action.playable; return !action.playable;
}, },
}, },
};
</script>
<template>
<div
class="btn-group"
role="group">
<button
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body"
data-toggle="dropdown"
ref="tooltip"
:title="title"
:aria-label="title"
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
<i
class="fa fa-caret-down"
aria-hidden="true"/>
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"/>
</span>
</button>
template: ` <ul class="dropdown-menu dropdown-menu-align-right">
<div class="btn-group" role="group"> <li v-for="action in actions">
<button <button
type="button" type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" class="js-manual-action-link no-btn btn"
data-container="body" @click="onClickAction(action.play_path)"
data-toggle="dropdown" :class="{ disabled: isActionDisabled(action) }"
ref="tooltip" :disabled="isActionDisabled(action)">
:title="title"
:aria-label="title"
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
<i <span>
class="fa fa-caret-down" {{action.name}}
aria-hidden="true"/> </span>
<i </button>
v-if="isLoading" </li>
class="fa fa-spinner fa-spin" </ul>
aria-hidden="true"/>
</span>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
:class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg}
<span>
{{action.name}}
</span>
</button>
</li>
</ul>
</div> </div>
`, </template>
};
/** <script>
* Environment Item Component
*
* Renders a table row for each environment.
*/
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import '../../lib/utils/text_utility'; import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions'; 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'; import RollbackComponent from './environment_rollback';
...@@ -441,133 +436,156 @@ export default { ...@@ -441,133 +436,156 @@ export default {
eventHub.$emit('toggleFolder', this.model, this.folderUrl); eventHub.$emit('toggleFolder', this.model, this.folderUrl);
}, },
}, },
};
template: ` </script>
<tr :class="{ 'js-child-row': model.isChildren }"> <template>
<td> <tr :class="{ 'js-child-row': model.isChildren }">
<span class="deploy-board-icon" <td>
v-if="model.hasDeployBoard" <span
@click="toggleDeployBoard(model)"> class="deploy-board-icon"
v-if="model.hasDeployBoard"
<i v-show="!model.isDeployBoardVisible" @click="toggleDeployBoard(model)">
class="fa fa-caret-right"
aria-hidden="true" /> <i
v-show="!model.isDeployBoardVisible"
class="fa fa-caret-right"
<i v-show="model.isDeployBoardVisible" aria-hidden="true" />
<i
v-show="model.isDeployBoardVisible"
class="fa fa-caret-down"
aria-hidden="true" />
</span>
<a
v-if="!model.isFolder"
class="environment-name"
:class="{ 'prepend-left-default': model.isChildren }"
:href="environmentPath">
{{model.name}}
</a>
<span
v-else
class="folder-name"
@click="onClickFolder"
role="button">
<span class="folder-icon">
<i
v-show="model.isOpen"
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true" /> aria-hidden="true" />
<i
v-show="!model.isOpen"
class="fa fa-caret-right"
aria-hidden="true"/>
</span> </span>
<a v-if="!model.isFolder" <span class="folder-icon">
class="environment-name" <i
:class="{ 'prepend-left-default': model.isChildren }" class="fa fa-folder"
:href="environmentPath"> aria-hidden="true" />
{{model.name}}
</a>
<span v-if="model.isFolder"
class="folder-name"
@click="onClickFolder"
role="button">
<span class="folder-icon">
<i
v-show="model.isOpen"
class="fa fa-caret-down"
aria-hidden="true" />
<i
v-show="!model.isOpen"
class="fa fa-caret-right"
aria-hidden="true"/>
</span>
<span class="folder-icon">
<i class="fa fa-folder" aria-hidden="true"></i>
</span>
<span>
{{model.folderName}}
</span>
<span class="badge">
{{model.size}}
</span>
</span> </span>
</td>
<td class="deployment-column"> <span>
<span v-if="shouldRenderDeploymentID"> {{model.folderName}}
{{deploymentInternalId}}
</span> </span>
<span v-if="!model.isFolder && deploymentHasUser"> <span class="badge">
by {{model.size}}
<a :href="deploymentUser.web_url" class="js-deploy-user-container">
<img class="avatar has-tooltip s20"
:src="deploymentUser.avatar_url"
:alt="userImageAltDescription"
:title="deploymentUser.username" />
</a>
</span> </span>
</td> </span>
</td>
<td class="environments-build-cell">
<a v-if="shouldRenderBuildName" <td class="deployment-column">
class="build-link" <span v-if="shouldRenderDeploymentID">
:href="buildPath"> {{deploymentInternalId}}
{{buildName}} </span>
<span v-if="!model.isFolder && deploymentHasUser">
by
<a
:href="deploymentUser.web_url"
class="js-deploy-user-container">
<img
class="avatar has-tooltip s20"
:src="deploymentUser.avatar_url"
:alt="userImageAltDescription"
:title="deploymentUser.username" />
</a> </a>
</td> </span>
</td>
<td>
<div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component"> <td class="environments-build-cell">
<commit-component <a
:tag="commitTag" v-if="shouldRenderBuildName"
:commit-ref="commitRef" class="build-link"
:commit-url="commitUrl" :href="buildPath">
:short-sha="commitShortSha" {{buildName}}
:title="commitTitle" </a>
:author="commitAuthor"/> </td>
</div>
<p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title"> <td>
No deployments yet <div
</p> v-if="!model.isFolder && hasLastDeploymentKey"
</td> class="js-commit-component">
<commit-component
<td> :tag="commitTag"
<span v-if="!model.isFolder && canShowDate" :commit-ref="commitRef"
class="environment-created-date-timeago"> :commit-url="commitUrl"
{{createdDate}} :short-sha="commitShortSha"
</span> :title="commitTitle"
</td> :author="commitAuthor"/>
</div>
<td class="environments-actions"> <p
<div v-if="!model.isFolder" class="btn-group pull-right" role="group"> v-if="!model.isFolder && !hasLastDeploymentKey"
<actions-component v-if="hasManualActions && canCreateDeployment" class="commit-title">
:service="service" No deployments yet
:actions="manualActions"/> </p>
</td>
<external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/> <td>
<span
<monitoring-button-component v-if="monitoringUrl && canReadEnvironment" v-if="!model.isFolder && canShowDate"
:monitoring-url="monitoringUrl"/> class="environment-created-date-timeago">
{{createdDate}}
<terminal-button-component v-if="model && model.terminal_path" </span>
:terminal-path="model.terminal_path"/> </td>
<stop-component v-if="hasStopAction && canCreateDeployment" <td class="environments-actions">
:stop-url="model.stop_path" <div
:service="service"/> v-if="!model.isFolder"
class="btn-group pull-right"
<rollback-component v-if="canRetry && canCreateDeployment" role="group">
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl" <actions-component
:service="service"/> v-if="hasManualActions && canCreateDeployment"
</div> :service="service"
</td> :actions="manualActions"/>
</tr>
`, <external-url-component
}; v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/>
<monitoring-button-component
v-if="monitoringUrl && canReadEnvironment"
:monitoring-url="monitoringUrl"/>
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
<stop-component
v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
:service="service"/>
<rollback-component
v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
:service="service"/>
</div>
</td>
</tr>
</template>
<script>
/** /**
* Render environments table. * Render environments table.
*
* Dumb component used to render top level environments and
* the folder view.
*/ */
import EnvironmentTableRowComponent from './environment_item'; import EnvironmentTableRowComponent from './environment_item.vue';
import DeployBoard from './deploy_board_component'; import DeployBoard from './deploy_board_component';
export default { export default {
...@@ -32,6 +30,17 @@ export default { ...@@ -32,6 +30,17 @@ export default {
default: false, default: false,
}, },
service: {
type: Object,
required: true,
},
isLoadingFolderContent: {
type: Boolean,
required: false,
default: false,
},
toggleDeployBoard: { toggleDeployBoard: {
type: Function, type: Function,
required: false, required: false,
...@@ -43,18 +52,6 @@ export default { ...@@ -43,18 +52,6 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
service: {
type: Object,
required: true,
default: () => ({}),
},
isLoadingFolderContent: {
type: Boolean,
required: false,
default: false,
},
}, },
methods: { methods: {
...@@ -62,68 +59,87 @@ export default { ...@@ -62,68 +59,87 @@ export default {
return `${window.location.pathname}/folders/${model.folderName}`; return `${window.location.pathname}/folders/${model.folderName}`;
}, },
}, },
};
</script>
<template>
<table class="table ci-table">
<thead>
<tr>
<th class="environments-name">
Environment
</th>
<th class="environments-deploy">
Last deployment
</th>
<th class="environments-build">
Job
</th>
<th class="environments-commit">
Commit
</th>
<th class="environments-date">
Updated
</th>
<th class="environments-actions"></th>
</tr>
</thead>
<tbody>
<template
v-for="model in environments"
v-bind:model="model">
<tr
is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service"
:toggleDeployBoard="toggleDeployBoard"
/>
template: ` <tr v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row">
<table class="table ci-table"> <td colspan="6" class="deploy-board-container">
<thead> <deploy-board
<tr> :store="store"
<th class="environments-name">Environment</th> :service="service"
<th class="environments-deploy">Last deployment</th> :environmentID="model.id"
<th class="environments-build">Job</th> :deployBoardData="model.deployBoardData"
<th class="environments-commit">Commit</th> :endpoint="model.rollout_status_path"
<th class="environments-date">Updated</th> />
<th class="environments-actions"></th> </td>
</tr> </tr>
</thead>
<tbody>
<template v-for="model in environments"
v-bind:model="model">
<tr is="environment-item" <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
:model="model" <tr v-if="isLoadingFolderContent">
:can-create-deployment="canCreateDeployment" <td colspan="6" class="text-center">
:can-read-environment="canReadEnvironment" <i
:toggleDeployBoard="toggleDeployBoard" class="fa fa-spin fa-spinner fa-2x"
:service="service"></tr> aria-hidden="true" />
<tr v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row">
<td colspan="6" class="deploy-board-container">
<deploy-board
:store="store"
:service="service"
:environmentID="model.id"
:deployBoardData="model.deployBoardData"
:endpoint="model.rollout_status_path">
</deploy-board>
</td> </td>
</tr> </tr>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> <template v-else>
<tr v-if="isLoadingFolderContent"> <tr
<td colspan="6" class="text-center"> is="environment-item"
<i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/> v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service" />
<tr>
<td
colspan="6"
class="text-center">
<a
:href="folderUrl(model)"
class="btn btn-default">
Show all
</a>
</td> </td>
</tr> </tr>
<template v-else>
<tr is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service"></tr>
<tr>
<td colspan="6" class="text-center">
<a :href="folderUrl(model)" class="btn btn-default">
Show all
</a>
</td>
</tr>
</template>
</template> </template>
</template> </template>
</tbody> </template>
</table> </tbody>
`, </table>
}; </template>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global Flash */ /* global Flash */
import Vue from 'vue'; import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table'; import EnvironmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination'; import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
......
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
</script> </script>
<template> <template>
<div class="row empty-state"> <div class="row empty-state js-empty-state">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="svg-content" v-html="pipelinesEmptyStateSVG" /> <div class="svg-content" v-html="pipelinesEmptyStateSVG" />
</div> </div>
......
...@@ -18,11 +18,6 @@ export default { ...@@ -18,11 +18,6 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
isInstanceAdmin: {
type: Boolean,
required: false,
default: false,
},
}, },
methods: { methods: {
...@@ -40,7 +35,6 @@ export default { ...@@ -40,7 +35,6 @@ export default {
ref="enabled-checkbox" ref="enabled-checkbox"
type="checkbox" type="checkbox"
id="service-desk-enabled-checkbox" id="service-desk-enabled-checkbox"
:disabled="!isInstanceAdmin"
:checked="isEnabled" :checked="isEnabled"
@change="onCheckboxToggle($event)"> @change="onCheckboxToggle($event)">
<span class="descr"> <span class="descr">
...@@ -48,12 +42,6 @@ export default { ...@@ -48,12 +42,6 @@ export default {
</span> </span>
</label> </label>
</div> </div>
<p
ref="only-instance-admin-activate-message"
v-if="!isInstanceAdmin"
class="settings-message">
Only instance admins can activate/deactivate Service Desk
</p>
<template v-if="isEnabled"> <template v-if="isEnabled">
<div <div
class="panel-slim panel-default"> class="panel-slim panel-default">
......
...@@ -12,13 +12,10 @@ class ServiceDeskRoot { ...@@ -12,13 +12,10 @@ class ServiceDeskRoot {
this.wrapperElement.dataset.enabled !== 'false'; this.wrapperElement.dataset.enabled !== 'false';
const incomingEmail = this.wrapperElement.dataset.incomingEmail; const incomingEmail = this.wrapperElement.dataset.incomingEmail;
const endpoint = this.wrapperElement.dataset.endpoint; const endpoint = this.wrapperElement.dataset.endpoint;
const isInstanceAdmin = typeof this.wrapperElement.dataset.isInstanceAdmin !== 'undefined' &&
this.wrapperElement.dataset.isInstanceAdmin !== 'false';
this.store = new ServiceDeskStore({ this.store = new ServiceDeskStore({
isEnabled, isEnabled,
incomingEmail, incomingEmail,
isInstanceAdmin,
}); });
this.service = new ServiceDeskService(endpoint); this.service = new ServiceDeskService(endpoint);
} }
...@@ -51,8 +48,7 @@ class ServiceDeskRoot { ...@@ -51,8 +48,7 @@ class ServiceDeskRoot {
<service-desk-setting <service-desk-setting
:isEnabled="isEnabled" :isEnabled="isEnabled"
:incomingEmail="incomingEmail" :incomingEmail="incomingEmail"
:fetchError="fetchError" :fetchError="fetchError" />
:isInstanceAdmin="isInstanceAdmin" />
`, `,
components: { components: {
'service-desk-setting': ServiceDeskSetting, 'service-desk-setting': ServiceDeskSetting,
......
...@@ -4,7 +4,6 @@ class ServiceDeskStore { ...@@ -4,7 +4,6 @@ class ServiceDeskStore {
isEnabled: false, isEnabled: false,
incomingEmail: '', incomingEmail: '',
fetchError: null, fetchError: null,
isInstanceAdmin: false,
}, initialState); }, initialState);
} }
...@@ -19,10 +18,6 @@ class ServiceDeskStore { ...@@ -19,10 +18,6 @@ class ServiceDeskStore {
setFetchError(value) { setFetchError(value) {
this.state.fetchError = value; this.state.fetchError = value;
} }
setIsInstanceAdmin(value) {
this.state.isInstanceAdmin = value;
}
} }
export default ServiceDeskStore; export default ServiceDeskStore;
...@@ -247,6 +247,11 @@ ...@@ -247,6 +247,11 @@
} }
} }
.burndown-docs-link {
color: inherit;
text-decoration: underline;
}
.burndown-header { .burndown-header {
margin: 24px 0 12px; margin: 24px 0 12px;
......
class Projects::ServiceDeskController < Projects::ApplicationController class Projects::ServiceDeskController < Projects::ApplicationController
before_action :authorize_admin_instance!, only: :update before_action :authorize_admin_project!
before_action :authorize_admin_project!, only: :show
def show def show
json_response json_response
...@@ -22,8 +21,4 @@ class Projects::ServiceDeskController < Projects::ApplicationController ...@@ -22,8 +21,4 @@ class Projects::ServiceDeskController < Projects::ApplicationController
format.json { render json: service_desk_attributes } format.json { render json: service_desk_attributes }
end end
end end
def authorize_admin_instance!
return render_404 unless current_user.admin?
end
end end
...@@ -115,4 +115,21 @@ module MilestonesHelper ...@@ -115,4 +115,21 @@ module MilestonesHelper
end end
end end
end end
def data_warning_for(burndown)
return unless burndown
message =
if burndown.empty?
"The burndown chart can’t be shown, as all issues assigned to this milestone were closed on an older GitLab version before data was recorded. "
elsif !burndown.accurate?
"Some issues can’t be shown in the burndown chart, as they were closed on an older GitLab version before data was recorded. "
end
if message
message += link_to "About burndown charts", help_page_path('user/project/milestones/index', anchor: 'burndown-charts'), class: 'burndown-docs-link'
content_tag(:div, message.html_safe, id: "data-warning", class: "settings-message prepend-top-20")
end
end
end end
class Burndown class Burndown
attr_accessor :start_date, :due_date, :end_date, :issues_count, :issues_weight attr_reader :start_date, :due_date, :end_date, :issues_count, :issues_weight, :accurate, :legacy_data
alias_method :accurate?, :accurate
alias_method :empty?, :legacy_data
def initialize(milestone) def initialize(milestone)
@milestone = milestone @milestone = milestone
...@@ -8,6 +10,9 @@ class Burndown ...@@ -8,6 +10,9 @@ class Burndown
@end_date = @milestone.due_date @end_date = @milestone.due_date
@end_date = Date.today if @end_date.present? && @end_date > Date.today @end_date = Date.today if @end_date.present? && @end_date > Date.today
@accurate = milestone_closed_issues.all?(&:closed_at)
@legacy_data = milestone_closed_issues.any? && milestone_closed_issues.none?(&:closed_at)
@issues_count, @issues_weight = milestone.issues.reorder(nil).pluck('COUNT(*), COALESCE(SUM(weight), 0)').first @issues_count, @issues_weight = milestone.issues.reorder(nil).pluck('COUNT(*), COALESCE(SUM(weight), 0)').first
end end
...@@ -51,16 +56,20 @@ class Burndown ...@@ -51,16 +56,20 @@ class Burndown
def closed_and_reopened_issues_by(date) def closed_and_reopened_issues_by(date)
current_date = date.to_date current_date = date.to_date
closed = issues_with_closed_at.select { |issue| issue.closed_at.to_date == current_date } closed =
milestone_closed_issues.select do |issue|
(issue.closed_at&.to_date || start_date) == current_date
end
reopened = closed.select { |issue| issue.state == 'reopened' } reopened = closed.select { |issue| issue.state == 'reopened' }
[closed, reopened] [closed, reopened]
end end
def issues_with_closed_at def milestone_closed_issues
@issues_with_closed_at ||= @milestone_closed_issues ||=
@milestone.issues.select('closed_at, weight, state'). @milestone.issues.select("closed_at, weight, state").
where('closed_at IS NOT NULL'). where("state IN ('reopened', 'closed')").
order('closed_at ASC') order("closed_at ASC")
end end
end end
...@@ -14,8 +14,6 @@ module EE ...@@ -14,8 +14,6 @@ module EE
delegate :actual_shared_runners_minutes_limit, delegate :actual_shared_runners_minutes_limit,
:shared_runners_minutes_used?, to: :namespace :shared_runners_minutes_used?, to: :namespace
before_validation :auto_refresh_service_desk_key
end end
def shared_runners_available? def shared_runners_available?
...@@ -29,29 +27,18 @@ module EE ...@@ -29,29 +27,18 @@ module EE
def service_desk_address def service_desk_address
return nil unless service_desk_available? return nil unless service_desk_available?
refresh_service_desk_key! if service_desk_mail_key.blank? config = ::Gitlab.config.incoming_email
wildcard = ::Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER
from = "service_desk+#{service_desk_mail_key}" config.address&.gsub(wildcard, full_path)
::Gitlab::IncomingEmail.reply_address(from)
end
def refresh_service_desk_key!
return unless service_desk_available?
self.service_desk_mail_key = SentNotification.reply_key
end end
private private
def service_desk_available? def service_desk_available?
@service_desk_available ||= return @service_desk_available if defined?(@service_desk_available)
EE::Gitlab::ServiceDesk.enabled? && service_desk_enabled?
end
def auto_refresh_service_desk_key @service_desk_available = EE::Gitlab::ServiceDesk.enabled? && service_desk_enabled?
if service_desk_mail_key.blank? || service_desk_enabled_changed?
refresh_service_desk_key!
end
end end
end end
end end
...@@ -134,8 +134,7 @@ ...@@ -134,8 +134,7 @@
= link_to icon('question-circle'), help_page_path('user/project/service_desk') = link_to icon('question-circle'), help_page_path('user/project/service_desk')
.js-service-desk-setting-root{ data: { endpoint: namespace_project_service_desk_path(@project.namespace, @project), .js-service-desk-setting-root{ data: { endpoint: namespace_project_service_desk_path(@project.namespace, @project),
enabled: @project.service_desk_enabled, enabled: @project.service_desk_enabled,
incoming_email: (@project.service_desk_address if @project.service_desk_enabled), incoming_email: (@project.service_desk_address if @project.service_desk_enabled) } }
is_instance_admin: current_user.admin? } }
%hr %hr
%fieldset.features.append-bottom-default %fieldset.features.append-bottom-default
......
- milestone = local_assigns[:milestone] - milestone = local_assigns[:milestone]
- project = local_assigns[:project] - project = local_assigns[:project]
- burndown = local_assigns[:burndown] - burndown = local_assigns[:burndown]
- can_generate_chart = burndown&.valid? - can_generate_chart = burndown&.valid? && !burndown&.empty?
- warning = data_warning_for(burndown)
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('burndown_chart') = page_specific_javascript_bundle_tag('burndown_chart')
= warning
- if can_generate_chart - if can_generate_chart
.burndown-header .burndown-header
%h3 %h3
...@@ -18,7 +21,7 @@ ...@@ -18,7 +21,7 @@
Issue weight Issue weight
.burndown-chart{ data: { start_date: burndown.start_date.strftime("%Y-%m-%d"), due_date: burndown.due_date.strftime("%Y-%m-%d"), chart_data: burndown.to_json } } .burndown-chart{ data: { start_date: burndown.start_date.strftime("%Y-%m-%d"), due_date: burndown.due_date.strftime("%Y-%m-%d"), chart_data: burndown.to_json } }
- elsif can?(current_user, :admin_milestone, @project) && cookies['hide_burndown_message'].nil? - elsif can?(current_user, :admin_milestone, @project) && cookies['hide_burndown_message'].nil? && warning.nil?
.burndown-hint.content-block.container-fluid .burndown-hint.content-block.container-fluid
= icon("times", class: "dismiss-icon") = icon("times", class: "dismiss-icon")
.row .row
......
---
title: Add index to approvals.merge_request_id
merge_request:
author:
---
title: Add warning when burndown data is not accurate
merge_request:
author:
---
title: Check if incoming emails and email key are available for service desk
merge_request:
author:
class RemoveServiceDeskMailKeyFromProjects < ActiveRecord::Migration
DOWNTIME = false
def change
remove_column :projects, :service_desk_mail_key, :string
end
end
class AddIndexToApprovalsMergeRequestId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :approvals, :merge_request_id
end
def down
remove_concurrent_index :approvals, :merge_request_id
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170419065104) do ActiveRecord::Schema.define(version: 20170421113144) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -141,6 +141,8 @@ ActiveRecord::Schema.define(version: 20170419065104) do ...@@ -141,6 +141,8 @@ ActiveRecord::Schema.define(version: 20170419065104) do
t.datetime "updated_at" t.datetime "updated_at"
end end
add_index "approvals", ["merge_request_id"], name: "index_approvals_on_merge_request_id", using: :btree
create_table "approver_groups", force: :cascade do |t| create_table "approver_groups", force: :cascade do |t|
t.integer "target_id", null: false t.integer "target_id", null: false
t.string "target_type", null: false t.string "target_type", null: false
...@@ -1109,7 +1111,6 @@ ActiveRecord::Schema.define(version: 20170419065104) do ...@@ -1109,7 +1111,6 @@ ActiveRecord::Schema.define(version: 20170419065104) do
t.boolean "printing_merge_request_link_enabled", default: true, null: false t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.string "import_jid" t.string "import_jid"
t.boolean "service_desk_enabled" t.boolean "service_desk_enabled"
t.string "service_desk_mail_key"
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
...@@ -1125,7 +1126,6 @@ ActiveRecord::Schema.define(version: 20170419065104) do ...@@ -1125,7 +1126,6 @@ ActiveRecord::Schema.define(version: 20170419065104) do
add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["service_desk_mail_key"], name: "index_projects_on_service_desk_mail_key", unique: true, using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["sync_time"], name: "index_projects_on_sync_time", using: :btree add_index "projects", ["sync_time"], name: "index_projects_on_sync_time", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
......
...@@ -15,6 +15,7 @@ All technical content published by GitLab lives in the documentation, including: ...@@ -15,6 +15,7 @@ All technical content published by GitLab lives in the documentation, including:
- [API](api/README.md) Automate GitLab via a simple and powerful API. - [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples. - [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry. - [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
- [Discussions](user/discussions/index.md) Threads, comments, and resolvable discussions in issues, commits, and merge requests.
- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file. - [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations. - [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
......
# Analytics # Analytics
- [Contribution Analytics](contribution_analytics.md) - [Contribution Analytics](contribution_analytics.md)
- (EE) [Burndown charts](../user/project/milestones/index.md#burndown-charts)
...@@ -13,7 +13,7 @@ Create issues, labels, milestones, cast your vote, and review issues. ...@@ -13,7 +13,7 @@ Create issues, labels, milestones, cast your vote, and review issues.
- [Create a new issue](../gitlab-basics/create-issue.md) - [Create a new issue](../gitlab-basics/create-issue.md)
- [Assign labels to issues](../user/project/labels.md) - [Assign labels to issues](../user/project/labels.md)
- [Use milestones as an overview of your project's tracker](../workflow/milestones.md) - [Use milestones as an overview of your project's tracker](../user/project/milestones/index.md)
- [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md) - [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md)
## Collaborate ## Collaborate
......
...@@ -333,7 +333,7 @@ A [platform](https://www.meteor.com) for building javascript apps. ...@@ -333,7 +333,7 @@ A [platform](https://www.meteor.com) for building javascript apps.
### Milestones ### Milestones
Allow you to [organize issues](https://docs.gitlab.com/ce/workflow/milestones.html) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project. Allow you to [organize issues](https://docs.gitlab.com/ce/user/project/milestones/) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project.
### Mirror Repositories ### Mirror Repositories
......
# Discussions
The ability to contribute conversationally is offered throughout GitLab.
You can leave a comment in the following places:
- issues
- merge requests
- snippets
- commits
- commit diffs
The comment area supports [Markdown] and [slash commands]. One can edit their
own comment at any time, and anyone with [Master access level][permissions] or
higher can also a comment made by someone else.
Apart from the standard comments, you also have the option to create a comment
in the form of a resolvable or threaded discussion.
## Resolvable discussions
>**Notes:**
- The main feature was [introduced][ce-5022] in GitLab 8.11.
- Resolvable discussions can be added only to merge request diffs.
Discussion resolution helps keep track of progress during planning or code review.
Resolving comments prevents you from forgetting to address feedback and lets you
hide discussions that are no longer relevant.
!["A discussion between two people on a piece of code"][discussion-view]
Comments and discussions can be resolved by anyone with at least Developer
access to the project or the author of the merge request.
### Jumping between unresolved discussions
When a merge request has a large number of comments it can be difficult to track
what remains unresolved. You can jump between unresolved discussions with the
Jump button next to the Reply field on a discussion.
You can also jump to the first unresolved discussion from the button next to the
resolved discussions tracker.
!["3/4 discussions resolved"][discussions-resolved]
### Marking a comment or discussion as resolved
You can mark a discussion as resolved by clicking the **Resolve discussion**
button at the bottom of the discussion.
!["Resolve discussion" button][resolve-discussion-button]
Alternatively, you can mark each comment as resolved individually.
!["Resolve comment" button][resolve-comment-button]
### Move all unresolved discussions in a merge request to an issue
> [Introduced][ce-8266] in GitLab 9.1
To continue all open discussions from a merge request in a new issue, click the
**Resolve all discussions in new issue** button.
![Open new issue for all unresolved discussions](img/btn_new_issue_for_all_discussions.png)
Alternatively, when your project only accepts merge requests [when all discussions
are resolved](#only-allow-merge-requests-to-be-merged-if-all-discussions-are-resolved),
there will be an **open an issue to resolve them later** link in the merge
request widget.
![Link in merge request widget](img/resolve_discussion_open_issue.png)
This will prepare an issue with its content referring to the merge request and
the unresolved discussions.
![Issue mentioning discussions in a merge request](img/preview_issue_for_discussions.png)
Hitting **Submit issue** will cause all discussions to be marked as resolved and
add a note referring to the newly created issue.
![Mark discussions as resolved notice](img/resolve_discussion_issue_notice.png)
You can now proceed to merge the merge request from the UI.
### Moving a single discussion to a new issue
> [Introduced][ce-8266] in GitLab 9.1
To create a new issue for a single discussion, you can use the **Resolve this
discussion in a new issue** button.
![Create issue for discussion](img/new_issue_for_discussion.png)
This will direct you to a new issue prefilled with the content of the
discussion, similar to the issues created for delegating multiple
discussions at once. Saving the issue will mark the discussion as resolved and
add a note to the merge request discussion referencing the new issue.
![New issue for a single discussion](img/preview_issue_for_discussion.png)
### Only allow merge requests to be merged if all discussions are resolved
> [Introduced][ce-7125] in GitLab 8.14.
You can prevent merge requests from being merged until all discussions are
resolved.
Navigate to your project's settings page, select the
**Only allow merge requests to be merged if all discussions are resolved** check
box and hit **Save** for the changes to take effect.
![Only allow merge if all the discussions are resolved settings](img/only_allow_merge_if_all_discussions_are_resolved.png)
From now on, you will not be able to merge from the UI until all discussions
are resolved.
![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png)
## Threaded discussions
> [Introduced][ce-7527] in GitLab 9.1.
While resolvable discussions are only available to merge request diffs,
discussions can also be added without a diff. You can start a specific
discussion which will look like a thread, on issues, commits, snippets, and
merge requests.
To start a threaded discussion, click on the **Comment** button toggle dropdown,
select **Start discussion** and click **Start discussion** when you're ready to
post the comment.
![Comment type toggle](img/comment_type_toggle.gif)
This will post a comment with a single thread to allow you to discuss specific
comments in greater detail.
![Discussion comment](img/discussion_comment.png)
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
[discussion-view]: img/discussion_view.png
[discussions-resolved]: img/discussions_resolved.png
[markdown]: ../markdown.md
[slash commands]: ../project/slash_commands.md
[permissions]: ../permissions.md
...@@ -34,7 +34,7 @@ Keep track of the progress during a code review with resolving comments. ...@@ -34,7 +34,7 @@ Keep track of the progress during a code review with resolving comments.
Resolving comments prevents you from forgetting to address feedback and lets Resolving comments prevents you from forgetting to address feedback and lets
you hide discussions that are no longer relevant. you hide discussions that are no longer relevant.
[Read more about resolving discussion comments in merge requests reviews.](merge_request_discussion_resolution.md) [Read more about resolving discussion comments in merge requests reviews.](../../discussions/index.md)
## Squash and merge ## Squash and merge
......
# Merge Request discussion resolution This document was moved to [another location](../../discussions/index.md).
> [Introduced][ce-5022] in GitLab 8.11.
Discussion resolution helps keep track of progress during code review.
Resolving comments prevents you from forgetting to address feedback and lets you
hide discussions that are no longer relevant.
!["A discussion between two people on a piece of code"][discussion-view]
Comments and discussions can be resolved by anyone with at least Developer
access to the project, as well as by the author of the merge request.
## Marking a comment or discussion as resolved
You can mark a discussion as resolved by clicking the "Resolve discussion"
button at the bottom of the discussion.
!["Resolve discussion" button][resolve-discussion-button]
Alternatively, you can mark each comment as resolved individually.
!["Resolve comment" button][resolve-comment-button]
## Jumping between unresolved discussions
When a merge request has a large number of comments it can be difficult to track
what remains unresolved. You can jump between unresolved discussions with the
Jump button next to the Reply field on a discussion.
You can also jump to the first unresolved discussion from the button next to the
resolved discussions tracker.
!["3/4 discussions resolved"][discussions-resolved]
## Only allow merge requests to be merged if all discussions are resolved
> [Introduced][ce-7125] in GitLab 8.14.
You can prevent merge requests from being merged until all discussions are
resolved.
Navigate to your project's settings page, select the
**Only allow merge requests to be merged if all discussions are resolved** check
box and hit **Save** for the changes to take effect.
![Only allow merge if all the discussions are resolved settings](img/only_allow_merge_if_all_discussions_are_resolved.png)
From now on, you will not be able to merge from the UI until all discussions
are resolved.
![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png)
## Move all unresolved discussions in a merge request to an issue
> [Introduced][ce-8266]
To continue all open discussions in a merge request, click the button **Resolve
all discussions in new issue**
![Open new issue for all unresolved discussions](img/btn_new_issue_for_all_discussions.png)
Alternatively, when your project only accepts merge requests when all discussions
are resolved, there will be an **open an issue to resolve them later** link in
the merge request-widget.
![Link in merge request widget](img/resolve_discussion_open_issue.png)
This will prepare an issue with content referring to the merge request and
discussions.
![Issue mentioning discussions in a merge request](img/preview_issue_for_discussions.png)
Hitting **Submit issue** will cause all discussions to be marked as resolved and
add a note referring to the newly created issue.
![Mark discussions as resolved notice](img/resolve_discussion_issue_notice.png)
You can now proceed to merge the merge request from the UI.
## Moving a single discussion to a new issue
> [Introduced][ce-8266]
To create a new issue for a single discussion, you can use the **Resolve this
discussion in a new issue** button.
![Create issue for discussion](img/new_issue_for_discussion.png)
This will direct you to a new issue prefilled with the content of the
discussion, similar to the issues created for delegating multiple
discussions at once.
![New issue for a single discussion](img/preview_issue_for_discussion.png)
Saving the issue will mark the discussion as resolved and add a note
to the discussion referencing the new issue.
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
[discussion-view]: img/discussion_view.png
[discussions-resolved]: img/discussions_resolved.png
# Milestones
Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
A common use is keeping track of an upcoming software version. Milestones are created per-project.
You can find the milestones page under your project's **Issues ➔ Milestones**.
## Creating a milestone
To create a new milestone, simply click the **New milestone** button when in the
milestones page. A milestone can have a title, a description and start/due dates.
Once you fill in all the details, hit the **Create milestone** button.
>**Note:**
The start/due dates are required if you intend to use [Burndown charts](#burndown-charts).
![Creating a milestone](img/milestone_create.png)
## Groups and milestones
You can create a milestone for several projects in the same group simultaneously.
On the group's **Issues ➔ Milestones** page, you will be able to see the status
of that milestone across all of the selected projects. To create a new milestone
for selected projects in the group, click the **New milestone** button. The
form is the same as when creating a milestone for a specific project with the
addition of the selection of the projects you want to inherit this milestone.
![Creating a group milestone](img/milestone_group_create.png)
## Special milestone filters
In addition to the milestones that exist in the project or group, there are some
special options available when filtering by milestone:
* **No Milestone** - only show issues or merge requests without a milestone.
* **Upcoming** - show issues or merge request that belong to the next open
milestone with a due date, by project. (For example: if project A has
milestone v1 due in three days, and project B has milestone v2 due in a week,
then this will show issues or merge requests from milestone v1 in project A
and milestone v2 in project B.)
* **Started** - show issues or merge requests from any milestone with a start
date less than today. Note that this can return results from several
milestones in the same project.
## Burndown charts
>**Notes:**
- [Introduced][ee-1540] in GitLab Enterprise Edition 9.1 and is available for
[Enterprise Edition Starter][ee] users.
- Closed or reopened issues prior to GitLab 9.1 won't have a `closed_at`
value, so the burndown chart considers them as closed on the milestone
`start_date`. In that case, a warning will be displayed.
A burndown chart is available for every project milestone that has a set start
date and a set due date and is located on the project's milestone page.
It indicates the project's progress throughout that milestone (for issues that
have that milestone assigned to it). In particular, it shows how many issues
were or are still open for a given day in the milestone period. Since GitLab
only tracks when an issue was last closed (and not its full history), the chart
assumes that issue was open on days prior to that date. Reopened issues are
considered as open on one day after they were closed.
The burndown chart can also be toggled to display the cumulative open issue
weight for a given day. When using this feature, make sure your weights have
been properly assigned, since an open issue with no weight adds zero to the
cumulative value.
![burndown chart](img/burndown_chart.png)
[ee-1540]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1540
[ee]: https://about.gitlab.com/gitlab-ee
...@@ -30,12 +30,12 @@ ...@@ -30,12 +30,12 @@
- [Time tracking](time_tracking.md) - [Time tracking](time_tracking.md)
- [Web Editor](../user/project/repository/web_editor.md) - [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md) - [Releases](releases.md)
- [Milestones](milestones.md) - [Milestones](../user/project/milestones/index.md)
- [Merge Requests](../user/project/merge_requests/index.md) - [Merge Requests](../user/project/merge_requests/index.md)
- [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md) - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
- [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md) - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
- [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md) - [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
- [Resolve discussion comments in merge requests reviews](../user/project/merge_requests/merge_request_discussion_resolution.md) - [Resolve discussion comments in merge requests reviews](../user/discussions/index.md)
- [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md) - [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md)
- [Revert changes in the UI](../user/project/merge_requests/revert_changes.md) - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md)
- [Merge requests versions](../user/project/merge_requests/versions.md) - [Merge requests versions](../user/project/merge_requests/versions.md)
......
# Milestones This document was moved to [another location](../user/project/milestones/index.md).
Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
A common use is keeping track of an upcoming software version. Milestones are created per-project.
![milestone form](milestones/form.png)
## Groups and milestones
You can create a milestone for several projects in the same group simultaneously.
On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects.
![group milestone form](milestones/group_form.png)
## Special milestone filters
In addition to the milestones that exist in the project or group, there are some
special options available when filtering by milestone:
* **No Milestone** - only show issues or merge requests without a milestone.
* **Upcoming** - show issues or merge request that belong to the next open
milestone with a due date, by project. (For example: if project A has
milestone v1 due in three days, and project B has milestone v2 due in a week,
then this will show issues or merge requests from milestone v1 in project A
and milestone v2 in project B.)
* **Started** - show issues or merge requests from any milestone with a start
date less than today. Note that this can return results from several
milestones in the same project.
...@@ -2,7 +2,10 @@ module EE ...@@ -2,7 +2,10 @@ module EE
module Gitlab module Gitlab
module ServiceDesk module ServiceDesk
def self.enabled? def self.enabled?
::License.current && ::License.current.add_on?('GitLab_ServiceDesk') ::License.current &&
::License.current.add_on?('GitLab_ServiceDesk') &&
::Gitlab::IncomingEmail.enabled? &&
::Gitlab::IncomingEmail.supports_wildcard?
end end
end end
end end
......
...@@ -19,20 +19,17 @@ module Gitlab ...@@ -19,20 +19,17 @@ module Gitlab
private private
def service_desk_key def service_desk_key
@service_desk_key ||= return unless mail_key.include?("/")
begin
mail_key =~ /\Aservice_desk[+](\w+)\z/ mail_key
$1
end
end end
def project def project
return @project if instance_variable_defined?(:@project) return @project if instance_variable_defined?(:@project)
@project = Project.find_by( @project =
service_desk_enabled: true, Project.where(service_desk_enabled: true)
service_desk_mail_key: service_desk_key .find_by_full_path(service_desk_key)
)
end end
def create_issue! def create_issue!
......
...@@ -21,6 +21,10 @@ module Gitlab ...@@ -21,6 +21,10 @@ module Gitlab
track == 'stable' track == 'stable'
end end
def order
stable? ? 1 : 0
end
def outdated? def outdated?
observed_generation < generation observed_generation < generation
end end
......
...@@ -20,6 +20,7 @@ module Gitlab ...@@ -20,6 +20,7 @@ module Gitlab
return new([], valid: false) if specs.empty? return new([], valid: false) if specs.empty?
deployments = specs.map { |spec| ::Gitlab::Kubernetes::Deployment.new(spec) } deployments = specs.map { |spec| ::Gitlab::Kubernetes::Deployment.new(spec) }
deployments.sort_by!(&:order)
new(deployments) new(deployments)
end end
......
...@@ -2,11 +2,13 @@ require 'spec_helper' ...@@ -2,11 +2,13 @@ require 'spec_helper'
describe Projects::ServiceDeskController do describe Projects::ServiceDeskController do
let(:project) { create(:project_empty_repo, :private) } let(:project) { create(:project_empty_repo, :private) }
let(:user) { create(:user, admin: true) } let(:user) { create(:user) }
before do before do
allow_any_instance_of(License).to receive(:add_on?).and_call_original allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true } allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
allow(Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
project.update(service_desk_enabled: true) project.update(service_desk_enabled: true)
project.add_master(user) project.add_master(user)
sign_in(user) sign_in(user)
...@@ -17,6 +19,7 @@ describe Projects::ServiceDeskController do ...@@ -17,6 +19,7 @@ describe Projects::ServiceDeskController do
get :show, namespace_id: project.namespace.to_param, project_id: project, format: :json get :show, namespace_id: project.namespace.to_param, project_id: project, format: :json
body = JSON.parse(response.body) body = JSON.parse(response.body)
expect(body["service_desk_address"]).to match(/\A[^@]+@[^@]+\z/) expect(body["service_desk_address"]).to match(/\A[^@]+@[^@]+\z/)
expect(body["service_desk_enabled"]).to be_truthy expect(body["service_desk_enabled"]).to be_truthy
expect(response.status).to eq(200) expect(response.status).to eq(200)
...@@ -38,23 +41,22 @@ describe Projects::ServiceDeskController do ...@@ -38,23 +41,22 @@ describe Projects::ServiceDeskController do
describe 'PUT service desk properties' do describe 'PUT service desk properties' do
it 'toggles services desk incoming email' do it 'toggles services desk incoming email' do
project.update(service_desk_enabled: true)
old_address = project.service_desk_address
project.update(service_desk_enabled: false) project.update(service_desk_enabled: false)
put :update, namespace_id: project.namespace.to_param, project_id: project, service_desk_enabled: true, format: :json put :update, namespace_id: project.namespace.to_param, project_id: project, service_desk_enabled: true, format: :json
body = JSON.parse(response.body) body = JSON.parse(response.body)
expect(body["service_desk_address"]).to be_present expect(body["service_desk_address"]).to be_present
expect(body["service_desk_address"]).not_to eq(old_address)
expect(body["service_desk_enabled"]).to be_truthy expect(body["service_desk_enabled"]).to be_truthy
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
context 'when user is not admin' do context 'when user cannot admin the project' do
before { user.update(admin: false) } let(:other_user) { create(:user) }
it 'renders 404' do it 'renders 404' do
sign_in(other_user)
put :update, namespace_id: project.namespace.to_param, project_id: project, service_desk_enabled: true, format: :json put :update, namespace_id: project.namespace.to_param, project_id: project, service_desk_enabled: true, format: :json
expect(response.status).to eq(404) expect(response.status).to eq(404)
......
...@@ -10,6 +10,7 @@ FactoryGirl.define do ...@@ -10,6 +10,7 @@ FactoryGirl.define do
trait :closed do trait :closed do
state :closed state :closed
closed_at Time.now
end end
trait :reopened do trait :reopened do
......
...@@ -3,12 +3,12 @@ require 'rails_helper' ...@@ -3,12 +3,12 @@ require 'rails_helper'
describe 'Milestone show', feature: true do describe 'Milestone show', feature: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 7.days.from_now) }
let(:labels) { create_list(:label, 2, project: project) } let(:labels) { create_list(:label, 2, project: project) }
let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone, labels: labels } } let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone, labels: labels } }
before do before do
project.add_user(user, :developer) project.add_user(user, :developer)
login_as(user) login_as(user)
end end
...@@ -23,4 +23,47 @@ describe 'Milestone show', feature: true do ...@@ -23,4 +23,47 @@ describe 'Milestone show', feature: true do
expect { visit_milestone }.not_to exceed_query_limit(control_count) expect { visit_milestone }.not_to exceed_query_limit(control_count)
end end
context 'burndown' do
let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone } }
context 'when any closed issues do not have closed_at value' do
it 'shows warning' do
create(:issue, issue_params)
create(:closed_issue, issue_params)
create(:closed_issue, issue_params.merge(closed_at: nil))
visit_milestone
expect(page).to have_selector('#data-warning', count: 1)
expect(page.find('#data-warning').text).to include("Some issues can’t be shown in the burndown chart")
expect(page).to have_selector('.burndown-chart')
end
end
context 'when all closed issues do not have closed_at value' do
it 'shows warning and hides burndown' do
create(:closed_issue, issue_params.merge(closed_at: nil))
create(:closed_issue, issue_params.merge(closed_at: nil))
visit_milestone
expect(page).to have_selector('#data-warning', count: 1)
expect(page.find('#data-warning').text).to include("The burndown chart can’t be shown")
expect(page).not_to have_selector('.burndown-chart')
end
end
context 'data is accurate' do
it 'does not show warning' do
create(:issue, issue_params)
create(:closed_issue, issue_params)
visit_milestone
expect(page).not_to have_selector('#data-warning')
expect(page).to have_selector('.burndown-chart')
end
end
end
end end
...@@ -3,63 +3,27 @@ require 'spec_helper' ...@@ -3,63 +3,27 @@ require 'spec_helper'
describe 'Service Desk Setting', js: true, feature: true do describe 'Service Desk Setting', js: true, feature: true do
include WaitForAjax include WaitForAjax
describe 'as project master/admin' do let(:project) { create(:project_empty_repo, :private) }
let(:project) { create(:project_empty_repo, :private) } let(:user) { create(:user) }
let(:user) { create(:user) }
before do
before do project.add_master(user)
project.add_master(user) login_as(user)
login_as(user) allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).and_call_original allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true } allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
end allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
describe 'when disabled' do visit edit_namespace_project_path(project.namespace, project)
before do
visit edit_namespace_project_path(project.namespace, project)
end
it 'shows disabled activation checkbox' do
expect(page).to have_selector("#service-desk-enabled-checkbox[disabled]")
end
end
describe 'when enabled' do
before do
project.update(service_desk_enabled: true)
visit edit_namespace_project_path(project.namespace, project)
end
it 'shows disabled activation checkbox' do
expect(page).to have_selector("#service-desk-enabled-checkbox[disabled]")
end
it 'shows service_desk_address when enabled' do
expect(find('.js-service-desk-setting-wrapper .panel-body')).to have_content(project.service_desk_address)
end
end
end end
describe 'as instance admin' do it 'shows activation checkbox' do
let(:project) { create(:project_empty_repo, :private) } expect(page).to have_selector("#service-desk-enabled-checkbox")
let(:user) { create(:user, :admin) } end
before do
login_as(user)
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
visit edit_namespace_project_path(project.namespace, project)
end
it 'shows activation checkbox' do
expect(page).to have_selector("#service-desk-enabled-checkbox")
end
it 'shows incoming email after activating' do it 'shows incoming email after activating' do
find("#service-desk-enabled-checkbox").click find("#service-desk-enabled-checkbox").click
wait_for_ajax wait_for_ajax
expect(find('.js-service-desk-setting-wrapper .panel-body')).to have_content(project.service_desk_address) expect(find('.js-service-desk-setting-wrapper .panel-body')).to have_content(project.service_desk_address)
end
end end
end end
...@@ -5,7 +5,7 @@ Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incom ...@@ -5,7 +5,7 @@ Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incom
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400 Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo> From: Jake the Dog <jake@adventuretime.ooo>
To: incoming+service_desk+somemailkey@appmail.adventuretime.ooo To: incoming+email/test@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
Subject: The message subject! @all Subject: The message subject! @all
Mime-Version: 1.0 Mime-Version: 1.0
......
...@@ -36,6 +36,7 @@ describe('Pipelines table in Commits and Merge requests', () => { ...@@ -36,6 +36,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
setTimeout(() => { setTimeout(() => {
expect(this.component.$el.querySelector('.empty-state')).toBeDefined(); expect(this.component.$el.querySelector('.empty-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null);
done(); done();
}, 1); }, 1);
}); });
...@@ -67,6 +68,8 @@ describe('Pipelines table in Commits and Merge requests', () => { ...@@ -67,6 +68,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
setTimeout(() => { setTimeout(() => {
expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.empty-state')).toBe(null);
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null);
done(); done();
}, 0); }, 0);
}); });
...@@ -95,10 +98,12 @@ describe('Pipelines table in Commits and Merge requests', () => { ...@@ -95,10 +98,12 @@ describe('Pipelines table in Commits and Merge requests', () => {
this.component.$destroy(); this.component.$destroy();
}); });
it('should render empty state', function (done) { it('should render error state', function (done) {
setTimeout(() => { setTimeout(() => {
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.js-empty-state')).toBe(null);
expect(this.component.$el.querySelector('table')).toBe(null);
done(); done();
}, 0); }, 0);
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import actionsComp from '~/environments/components/environment_actions'; import actionsComp from '~/environments/components/environment_actions.vue';
describe('Actions Component', () => { describe('Actions Component', () => {
let ActionsComponent; let ActionsComponent;
......
import 'timeago.js'; import 'timeago.js';
import Vue from 'vue'; import Vue from 'vue';
import environmentItemComp from '~/environments/components/environment_item'; import environmentItemComp from '~/environments/components/environment_item.vue';
describe('Environment item', () => { describe('Environment item', () => {
let EnvironmentItem; let EnvironmentItem;
......
import Vue from 'vue'; import Vue from 'vue';
import environmentTableComp from '~/environments/components/environments_table'; import environmentTableComp from '~/environments/components/environments_table.vue';
describe('Environment item', () => { describe('Environment item', () => {
let EnvironmentTable; let EnvironmentTable;
......
...@@ -31,12 +31,8 @@ describe('ServiceDeskSetting', () => { ...@@ -31,12 +31,8 @@ describe('ServiceDeskSetting', () => {
el = vm.$el; el = vm.$el;
}); });
it('should see disabled activation checkbox', () => { it('should see activation checkbox (not disabled)', () => {
expect(vm.$refs['enabled-checkbox'].getAttribute('disabled')).toEqual('disabled'); expect(vm.$refs['enabled-checkbox'].getAttribute('disabled')).toEqual(null);
});
it('should see only instance admin can activate/deactivate message', () => {
expect(vm.$refs['only-instance-admin-activate-message']).toBeDefined();
}); });
it('should see main panel with the email info', () => { it('should see main panel with the email info', () => {
...@@ -53,20 +49,6 @@ describe('ServiceDeskSetting', () => { ...@@ -53,20 +49,6 @@ describe('ServiceDeskSetting', () => {
expect(vm.$refs['recommend-protect-email-from-spam-message']).toBeDefined(); expect(vm.$refs['recommend-protect-email-from-spam-message']).toBeDefined();
}); });
}); });
describe('as instance admin', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
isInstanceAdmin: true,
});
el = vm.$el;
});
it('should see activation checkbox (not disabled)', () => {
expect(vm.$refs['enabled-checkbox'].getAttribute('disabled')).toEqual(null);
});
});
}); });
describe('with incomingEmail', () => { describe('with incomingEmail', () => {
......
...@@ -50,22 +50,4 @@ describe('ServiceDeskStore', () => { ...@@ -50,22 +50,4 @@ describe('ServiceDeskStore', () => {
expect(store.state.fetchError).toEqual(err); expect(store.state.fetchError).toEqual(err);
}); });
}); });
describe('setIsInstanceAdmin', () => {
it('defaults to false', () => {
expect(store.state.isInstanceAdmin).toEqual(false);
});
it('set true', () => {
store.setIsInstanceAdmin(true);
expect(store.state.isInstanceAdmin).toEqual(true);
});
it('set false', () => {
store.setIsInstanceAdmin(false);
expect(store.state.isInstanceAdmin).toEqual(false);
});
});
}); });
require 'spec_helper'
describe EE::Gitlab::ServiceDesk, lib: true do
before do
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
end
subject { described_class.enabled? }
it { is_expected.to be_truthy }
context 'when license does not support service desk' do
before do
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { false }
end
it { is_expected.to be_falsy }
end
context 'when incoming emails are disabled' do
before do
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { false }
end
it { is_expected.to be_falsy }
end
context 'when email key is not supported' do
before do
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { false }
end
it { is_expected.to be_falsy }
end
end
...@@ -9,18 +9,20 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do ...@@ -9,18 +9,20 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do
end end
let(:email_raw) { fixture_file('emails/service_desk.eml') } let(:email_raw) { fixture_file('emails/service_desk.eml') }
let(:project) { create(:project, :public) } let(:namespace) { create(:namespace, name: "email") }
let(:project) { create(:project, :public, namespace: namespace, path: "test") }
context 'when service desk is enabled' do context 'when service desk is enabled' do
before do before do
project.update(service_desk_enabled: true) project.update(service_desk_enabled: true)
project.update(service_desk_mail_key: 'somemailkey')
allow(Notify).to receive(:service_desk_thank_you_email) allow(Notify).to receive(:service_desk_thank_you_email)
.with(kind_of(Integer)).and_return(double(deliver_later!: true)) .with(kind_of(Integer)).and_return(double(deliver_later!: true))
allow_any_instance_of(License).to receive(:add_on?).and_call_original allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true } allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
end end
it 'sends thank you the email and creates issue' do it 'sends thank you the email and creates issue' do
...@@ -68,10 +70,7 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do ...@@ -68,10 +70,7 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do
context 'when service desk is not enabled' do context 'when service desk is not enabled' do
before do before do
project.update_attributes( project.update_attributes(service_desk_enabled: false)
service_desk_enabled: false,
service_desk_mail_key: 'somemailkey',
)
end end
it 'bounces the email' do it 'bounces the email' do
......
...@@ -16,14 +16,14 @@ describe Gitlab::Email::Handler, lib: true do ...@@ -16,14 +16,14 @@ describe Gitlab::Email::Handler, lib: true do
allow_any_instance_of(License).to receive(:add_on?).and_call_original allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk').and_return(true) allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk').and_return(true)
expect(handler_for('emails/service_desk.eml', 'service_desk+auth_token')).to be_instance_of(Gitlab::Email::Handler::EE::ServiceDeskHandler) expect(handler_for('emails/service_desk.eml', 'service_desk+some/project')).to be_instance_of(Gitlab::Email::Handler::EE::ServiceDeskHandler)
end end
it 'uses the create issue handler when Service Desk is disabled' do it 'uses the create issue handler when Service Desk is disabled' do
allow_any_instance_of(License).to receive(:add_on?).and_call_original allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk').and_return(false) allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk').and_return(false)
expect(handler_for('emails/service_desk.eml', 'service_desk+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler) expect(handler_for('emails/service_desk.eml', 'service_desk+some/project')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end end
end end
......
...@@ -53,12 +53,12 @@ describe Gitlab::Kubernetes::RolloutStatus do ...@@ -53,12 +53,12 @@ describe Gitlab::Kubernetes::RolloutStatus do
it 'stores the union of deployment instances' do it 'stores the union of deployment instances' do
expected = [ expected = [
{ status: 'finished', tooltip: 'one (pod 0) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 1) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 2) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'two (pod 0) Finished', track: 'canary', stable: false }, { status: 'finished', tooltip: 'two (pod 0) Finished', track: 'canary', stable: false },
{ status: 'finished', tooltip: 'two (pod 1) Finished', track: 'canary', stable: false }, { status: 'finished', tooltip: 'two (pod 1) Finished', track: 'canary', stable: false },
{ status: 'finished', tooltip: 'two (pod 2) Finished', track: 'canary', stable: false }, { status: 'finished', tooltip: 'two (pod 2) Finished', track: 'canary', stable: false },
{ status: 'finished', tooltip: 'one (pod 0) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 1) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 2) Finished', track: 'stable', stable: true },
] ]
expect(rollout_status.instances).to eq(expected) expect(rollout_status.instances).to eq(expected)
......
...@@ -57,6 +57,46 @@ describe Burndown, models: true do ...@@ -57,6 +57,46 @@ describe Burndown, models: true do
expect(JSON.parse(subject).last[0]).to eq(Time.now.strftime("%Y-%m-%d")) expect(JSON.parse(subject).last[0]).to eq(Time.now.strftime("%Y-%m-%d"))
end end
it "sets attribute accurate to true" do
burndown = described_class.new(milestone)
expect(burndown).to be_accurate
end
context "when all closed and reopened issues does not have closed_at" do
before do
milestone.issues.update_all(closed_at: nil)
end
it "considers closed_at as milestone start date" do
expect(subject).to eq([
["2017-03-01", 15, 30],
["2017-03-02", 27, 54],
["2017-03-03", 27, 54],
["2017-03-04", 27, 54],
["2017-03-05", 27, 54]
].to_json)
end
it "sets attribute empty to true" do
burndown = described_class.new(milestone)
expect(burndown).to be_empty
end
end
context "when one or more closed or reopened issues does not have closed_at" do
before do
milestone.issues.closed.first.update(closed_at: nil)
end
it "sets attribute accurate to false" do
burndown = described_class.new(milestone)
expect(burndown).not_to be_accurate
end
end
# Creates, closes and reopens issues only for odd days numbers # Creates, closes and reopens issues only for odd days numbers
def build_sample def build_sample
milestone.start_date.upto(milestone.due_date) do |date| milestone.start_date.upto(milestone.due_date) do |date|
......
...@@ -116,37 +116,18 @@ describe Project, models: true do ...@@ -116,37 +116,18 @@ describe Project, models: true do
end end
end end
describe '#regenerate_service_desk_key' do describe '#service_desk_address' do
let(:project) { create(:empty_project, service_desk_enabled: true) }
before do before do
allow_any_instance_of(License).to receive(:add_on?).and_call_original allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true } allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
allow(Gitlab.config.incoming_email).to receive(:address).and_return("test+%{key}@mail.com")
end end
subject { create(:project) } it 'uses project full path as service desk address key' do
expect(project.service_desk_address).to eq("test+#{project.full_path}@mail.com")
it 'leaves it blank by default' do
expect(subject.service_desk_mail_key).to be_blank
end
it 'updates when enabled' do
subject.service_desk_enabled = true
subject.validate
expect(subject.service_desk_mail_key).not_to be_blank
end
it 'changes when enabled' do
subject.update!(service_desk_mail_key: '12345')
subject.service_desk_enabled = true
expect { subject.validate }.to change { subject.service_desk_mail_key }
end
it 'ensures mail key is never nil when enabled' do
subject.update!(service_desk_enabled: true)
expect { subject.update!(service_desk_mail_key: nil) }
.to change { subject.service_desk_mail_key }
expect(subject.service_desk_mail_key).not_to be_blank
end end
end end
end end
...@@ -9,6 +9,8 @@ describe EE::NotificationService do ...@@ -9,6 +9,8 @@ describe EE::NotificationService do
allow_any_instance_of(License).to receive(:add_on?).and_call_original allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true } allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
end end
def should_email! def should_email!
......
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