Commit b3a9b8db authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

Merge remote-tracking branch 'ce/master' into ce-upstream

parents 0b882ca5 d17f5068
...@@ -24,6 +24,12 @@ ...@@ -24,6 +24,12 @@
"spyOnEvent": false, "spyOnEvent": false,
"Turbolinks": false, "Turbolinks": false,
"window": false, "window": false,
<<<<<<< HEAD
"Vue": false "Vue": false
=======
"Vue": false,
"Flash": false,
"Cookies": false
>>>>>>> ce/master
} }
} }
...@@ -300,12 +300,11 @@ migration paths: ...@@ -300,12 +300,11 @@ migration paths:
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
script: script:
- git checkout HEAD . - git fetch origin v8.5.9
- git fetch --tags - git checkout -f FETCH_HEAD
- git checkout v8.5.9
- cp config/resque.yml.example config/resque.yml - cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml - sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3 - bundle install --without postgres production --jobs $(nproc) ${FLAGS[@]} --retry=3
- rake db:drop db:create db:schema:load db:seed_fu - rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_BUILD_REF - git checkout $CI_BUILD_REF
- source scripts/prepare_build.sh - source scripts/prepare_build.sh
......
...@@ -32,6 +32,7 @@ entry. ...@@ -32,6 +32,7 @@ entry.
- Fix sidekiq stats in admin area (blackst0ne) - Fix sidekiq stats in admin area (blackst0ne)
- Added label description as tooltip to issue board list title - Added label description as tooltip to issue board list title
- Created cycle analytics bundle JavaScript file - Created cycle analytics bundle JavaScript file
- Make the milestone page more responsive (yury-n)
- Hides container registry when repository is disabled - Hides container registry when repository is disabled
- API: Fix booleans not recognized as such when using the `to_boolean` helper - API: Fix booleans not recognized as such when using the `to_boolean` helper
- Removed delete branch tooltip !6954 - Removed delete branch tooltip !6954
......
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageCodeComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl">
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageIssueComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl">
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StagePlanComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component">
<img class="avatar" :src="commit.author.avatarUrl">
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
First
<span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
pushed by
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageProductionComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl">
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageReviewComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl">
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageStagingComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<img class="avatar" :src="build.author.avatarUrl">
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
<span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
by
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageTestComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
<span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.TotalTimeComponent = Vue.extend({
props: {
time: Object,
},
template: `
<span class="total-time">
<template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
</span>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable */
//= require vue //= require vue
//= require_tree ./svg
//= require_tree .
$(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
((global) => { gl.cycleAnalyticsApp = new Vue({
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
const store = gl.cycleAnalyticsStore = {
isLoading: true,
hasError: false,
isHelpDismissed: Cookies.get(COOKIE_NAME),
analytics: {}
};
gl.CycleAnalytics = class CycleAnalytics {
constructor() {
const that = this;
this.vue = new Vue({
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
created: this.fetchData(),
data: store,
methods: {
dismissLanding() {
that.dismissLanding();
}
}
});
}
fetchData(options) {
store.isLoading = true;
options = options || { startDate: 30 };
$.ajax({
url: $('#cycle-analytics').data('request-path'),
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: { data: {
cycle_analytics: { state: cycleAnalyticsStore.state,
start_date: options.startDate isLoading: false,
} isLoadingStage: false,
} isEmptyStage: false,
}).done((data) => { hasError: false,
this.decorateData(data); startDate: 30,
this.initDropdown(); isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
}) },
.error((data) => { computed: {
this.handleError(data); currentStage() {
}) return cycleAnalyticsStore.currentActiveStage();
.always(() => { },
store.isLoading = false; },
}) components: {
} 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
decorateData(data) { 'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
data.summary = data.summary || []; 'stage-test-component': gl.cycleAnalytics.StageTestComponent,
data.stats = data.stats || []; 'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
data.summary.forEach((item) => { 'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
item.value = item.value || '-'; },
}); created() {
this.fetchCycleAnalyticsData();
data.stats.forEach((item) => { },
item.value = item.value || '- - -'; methods: {
}); handleError() {
cycleAnalyticsStore.setErrorState(true);
store.analytics = data; return new Flash('There was an error while fetching cycle analytics data.');
} },
handleError(data) {
store.hasError = true;
new Flash('There was an error while fetching cycle analytics data.', 'alert');
}
dismissLanding() {
store.isHelpDismissed = true;
Cookies.set(COOKIE_NAME, true);
}
initDropdown() { initDropdown() {
const $dropdown = $('.js-ca-dropdown'); const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label'); const $label = $dropdown.find('.dropdown-label');
...@@ -86,13 +51,71 @@ ...@@ -86,13 +51,71 @@
$dropdown.find('li a').off('click').on('click', (e) => { $dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault(); e.preventDefault();
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const value = $target.data('value'); this.startDate = $target.data('value');
$label.text($target.text().trim()); $label.text($target.text().trim());
this.fetchData({ startDate: value }); this.fetchCycleAnalyticsData({ startDate: this.startDate });
});
},
fetchCycleAnalyticsData(options) {
const fetchOptions = options || { startDate: this.startDate };
this.isLoading = true;
cycleAnalyticsService
.fetchCycleAnalyticsData(fetchOptions)
.done((response) => {
cycleAnalyticsStore.setCycleAnalyticsData(response);
this.selectDefaultStage();
this.initDropdown();
})
.error(() => {
this.handleError();
}) })
.always(() => {
this.isLoading = false;
});
},
selectDefaultStage() {
const stage = this.state.stages.first();
this.selectStage(stage);
},
selectStage(stage) {
if (this.isLoadingStage) return;
if (this.currentStage === stage) return;
if (!stage.isUserAllowed) {
cycleAnalyticsStore.setActiveStage(stage);
return;
} }
} this.isLoadingStage = true;
cycleAnalyticsStore.setStageEvents([]);
cycleAnalyticsStore.setActiveStage(stage);
cycleAnalyticsService
.fetchStageData({
stage,
startDate: this.startDate,
})
.done((response) => {
this.isEmptyStage = !response.events.length;
cycleAnalyticsStore.setStageEvents(response.events);
})
.error(() => {
this.isEmptyStage = true;
})
.always(() => {
this.isLoadingStage = false;
});
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
},
},
});
})(window.gl || (window.gl = {})); // Register global components
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
class CycleAnalyticsService {
constructor(options) {
this.requestPath = options.requestPath;
}
fetchCycleAnalyticsData(options) {
options = options || { startDate: 30 };
return $.ajax({
url: this.requestPath,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate,
},
},
});
}
fetchStageData(options) {
const {
stage,
startDate,
} = options;
return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
cycle_analytics: {
start_date: startDate,
},
});
}
}
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
const EMPTY_STAGE_TEXTS = {
issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
};
global.cycleAnalytics.CycleAnalyticsStore = {
state: {
summary: '',
stats: '',
analytics: '',
events: [],
stages: [],
},
setCycleAnalyticsData(data) {
this.state = Object.assign(this.state, this.decorateData(data));
},
decorateData(data) {
const newData = {};
newData.stages = data.stats || [];
newData.summary = data.summary || [];
newData.summary.forEach((item) => {
item.value = item.value || '-';
});
newData.stages.forEach((item) => {
const stageName = item.title.toLowerCase();
item.active = false;
item.isUserAllowed = data.permissions[stageName];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageName];
item.component = `stage-${stageName}-component`;
});
newData.analytics = data;
return newData;
},
setLoadingState(state) {
this.state.isLoading = state;
},
setErrorState(state) {
this.state.hasError = state;
},
deactivateAllStages() {
this.state.stages.forEach((stage) => {
stage.active = false;
});
},
setActiveStage(stage) {
this.deactivateAllStages();
stage.active = true;
},
setStageEvents(events) {
this.state.events = this.decorateEvents(events);
},
decorateEvents(events) {
const newEvents = events;
newEvents.forEach((item) => {
item.totalTime = item.total_time;
item.author.webUrl = item.author.web_url;
item.author.avatarUrl = item.author.avatar_url;
if (item.created_at) item.createdAt = item.created_at;
if (item.short_sha) item.shortSha = item.short_sha;
if (item.commit_url) item.commitUrl = item.commit_url;
delete item.author.web_url;
delete item.author.avatar_url;
delete item.total_time;
delete item.created_at;
delete item.short_sha;
delete item.commit_url;
});
return newEvents;
},
currentActiveStage() {
return this.state.stages.find(stage => stage.active);
},
};
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>';
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>';
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>';
})(window.gl || (window.gl = {}));
...@@ -216,9 +216,6 @@ ...@@ -216,9 +216,6 @@
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
break; break;
case 'projects:cycle_analytics:show':
new gl.CycleAnalytics();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'admin': case 'admin':
......
...@@ -161,6 +161,7 @@ $settings-icon-size: 18px; ...@@ -161,6 +161,7 @@ $settings-icon-size: 18px;
$provider-btn-group-border: #e5e5e5; $provider-btn-group-border: #e5e5e5;
$provider-btn-not-active-color: #4688f1; $provider-btn-not-active-color: #4688f1;
$link-underline-blue: #4a8bee; $link-underline-blue: #4a8bee;
$active-item-blue: #4a8bee;
$layout-link-gray: #7e7c7c; $layout-link-gray: #7e7c7c;
$todo-alert-blue: #428bca; $todo-alert-blue: #428bca;
$btn-side-margin: 10px; $btn-side-margin: 10px;
...@@ -284,6 +285,9 @@ $calendar-unselectable-bg: $gray-light; ...@@ -284,6 +285,9 @@ $calendar-unselectable-bg: $gray-light;
*/ */
$cycle-analytics-box-padding: 30px; $cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c; $cycle-analytics-box-text-color: #8c8c8c;
$cycle-analytics-big-font: 19px;
$cycle-analytics-dark-text: $gl-title-color;
$cycle-analytics-light-gray: #bfbfbf;
/* /*
* Personal Access Tokens * Personal Access Tokens
......
#cycle-analytics { #cycle-analytics {
max-width: 1000px;
margin: 24px auto 0; margin: 24px auto 0;
max-width: 800px;
position: relative; position: relative;
.panel { .col-headers {
ul {
margin: 0;
padding: 0;
@include clearfix;
}
li {
display: inline-block;
float: left;
line-height: 50px;
width: 20%;
}
.fa {
color: $cycle-analytics-light-gray;
}
.stage-header {
width: 28%;
padding-left: $gl-padding;
}
.median-header {
width: 12%;
}
.event-header {
width: 45%;
padding-left: $gl-padding;
}
.total-time-header {
width: 15%;
text-align: right;
padding-right: $gl-padding;
}
.stage-name {
font-weight: 600;
}
}
.panel {
.content-block { .content-block {
padding: 24px 0; padding: 24px 0;
border-bottom: none; border-bottom: none;
...@@ -35,23 +78,20 @@ ...@@ -35,23 +78,20 @@
} }
&:last-child { &:last-child {
text-align: right;
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-min) {
text-align: center; text-align: center;
} }
} }
} }
.dropdown {
top: 13px;
} }
.js-ca-dropdown {
top: $gl-padding-top;
} }
.bordered-box { .bordered-box {
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
} }
.content-list { .content-list {
...@@ -141,4 +181,302 @@ ...@@ -141,4 +181,302 @@
margin-top: 36px; margin-top: 36px;
} }
.stage-panel-body {
display: flex;
flex-wrap: wrap;
}
.stage-nav,
.stage-entries {
display: flex;
vertical-align: top;
font-size: $gl-font-size;
}
.stage-nav {
width: 40%;
margin-bottom: 0;
ul {
padding: 0;
margin: 0;
width: 100%;
}
li {
list-style-type: none;
@include clearfix;
}
.stage-nav-item {
display: block;
line-height: 65px;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
border-right: 1px solid $border-color;
background-color: $gray-light;
cursor: default;
&.active {
background-color: transparent;
border-right-color: transparent;
border-top-color: $border-color;
border-bottom-color: $border-color;
box-shadow: inset 2px 0 0 0 $active-item-blue;
.stage-name {
font-weight: 600;
}
}
&:hover:not(.active) {
background-color: $gray-lightest;
box-shadow: inset 2px 0 0 0 $border-color;
}
&:first-child {
border-top: none;
}
&:last-child {
border-bottom: none;
}
.stage-nav-item-cell {
float: left;
&.stage-name {
width: 70%;
}
&.stage-median {
width: 30%;
}
}
.stage-name {
padding-left: 16px;
}
.stage-empty,
.not-available {
color: $gl-text-color-light;
}
}
}
.stage-panel-container {
width: 100%;
overflow: auto;
}
.stage-panel {
min-width: 968px;
.panel-heading {
padding: 0;
background-color: transparent;
}
.events-description {
line-height: 65px;
padding-left: $gl-padding;
}
}
.stage-events {
width: 60%;
overflow: scroll;
height: 467px;
}
.stage-event-list {
margin: 0;
padding: 0;
}
.stage-event-item {
list-style-type: none;
padding: 0 0 $gl-padding;
margin: 0 $gl-padding $gl-padding;
border-bottom: 1px solid $gray-darker;
@include clearfix;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
.item-details,
.item-time {
float: left;
}
.item-details {
width: 75%;
}
.item-title {
margin: 0 0 2px;
&.issue-title,
&.commit-title,
&.merge-merquest-title {
max-width: 100%;
display: block;
@include text-overflow();
a {
color: $gl-dark-link-color;
}
}
}
.item-time {
width: 25%;
text-align: right;
}
.total-time {
font-size: $cycle-analytics-big-font;
color: $cycle-analytics-dark-text;
span {
color: $gl-text-color;
font-size: $gl-font-size;
}
}
.issue-date,
.build-date {
color: $gl-text-color;
}
.issue-link,
.commit-author-link,
.issue-author-link {
color: $gl-dark-link-color;
}
// Custom CSS for components
.item-conmmit-component {
.commit-icon {
position: relative;
top: 3px;
left: 1px;
display: inline-block;
svg {
float: left;
}
}
}
.merge-request-branch {
a {
max-width: 180px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
vertical-align: bottom;
}
}
}
// Custom Styles for stage items
.item-build-component {
.item-title {
.icon-build-status {
float: left;
margin-right: 5px;
position: relative;
top: 2px;
}
.item-build-name {
color: $gl-title-color;
}
.pipeline-id {
color: $gl-title-color;
padding: 0 3px 0 0;
}
.branch-name {
color: $black;
display: inline-block;
max-width: 180px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
line-height: 1.3;
vertical-align: top;
}
.short-sha {
color: $gl-link-color;
line-height: 1.3;
vertical-align: top;
font-weight: normal;
}
.fa {
color: $gl-text-color-light;
font-size: $code_font_size;
}
}
}
.empty-stage,
.no-access-stage {
text-align: center;
width: 75%;
margin: 0 auto;
padding-top: 130px;
color: $gl-text-color-light;
h4 {
color: $gl-text-color;
}
}
.empty-stage {
.icon-no-data {
height: 36px;
width: 78px;
display: inline-block;
margin-bottom: 20px;
}
}
.no-access-stage {
.icon-lock {
height: 36px;
width: 78px;
display: inline-block;
margin-bottom: 20px;
}
}
}
.cycle-analytics-overview {
padding-top: 100px;
.overview-details {
display: flex;
align-items: center;
}
.overview-image {
text-align: right;
}
.overview-icon {
svg {
width: 365px;
height: 227px;
}
}
} }
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
} }
.progress { .progress {
width: 100%;
height: 6px; height: 6px;
} }
} }
...@@ -30,7 +31,6 @@ ...@@ -30,7 +31,6 @@
margin-right: 7px; margin-right: 7px;
} }
// Issue title
span a { span a {
color: $gl-text-color; color: $gl-text-color;
word-wrap: break-word; word-wrap: break-word;
...@@ -39,15 +39,66 @@ ...@@ -39,15 +39,66 @@
} }
.milestone-summary { .milestone-summary {
margin-bottom: 25px;
.milestone-stat { .milestone-stat {
white-space: nowrap;
margin-right: 10px; margin-right: 10px;
&.with-drilldown {
margin-right: 2px;
}
} }
.remaining-days { .remaining-days {
color: $orange-light; color: $orange-light;
} }
.milestone-stats-and-buttons {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
@media (min-width: $screen-xs-min) {
justify-content: space-between;
flex-wrap: nowrap;
}
}
.milestone-progress-buttons {
order: 1;
margin-top: 10px;
@media (min-width: $screen-xs-min) {
order: 2;
margin-top: 0;
flex-shrink: 0;
}
.btn {
float: left;
margin-right: $btn-side-margin;
&:last-child {
margin-right: 0;
}
}
}
.milestone-stats {
order: 2;
width: 100%;
padding: 7px 0;
flex-shrink: 1;
@media (min-width: $screen-xs-min) {
// when displayed on one line stats go first, buttons second
order: 1;
}
}
.progress {
width: 100%;
margin: 15px 0;
}
} }
.issues-sortable-list, .issues-sortable-list,
...@@ -82,3 +133,50 @@ ...@@ -82,3 +133,50 @@
} }
} }
} }
.milestone-page-header {
display: flex;
flex-flow: row;
align-items: center;
flex-wrap: wrap;
.status-box {
margin-top: 0;
}
.milestone-buttons {
margin-left: auto;
}
.status-box {
order: 1;
}
.milestone-buttons {
order: 2;
}
.header-text-content {
order: 3;
width: 100%;
}
.milestone-buttons .verbose {
display: none;
}
@media (min-width: $screen-xs-min) {
.milestone-buttons .verbose {
display: inline;
}
.header-text-content {
order: 2;
width: auto;
}
.milestone-buttons {
order: 3;
}
}
}
...@@ -29,15 +29,15 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -29,15 +29,15 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def generate_cycle_analytics_data def generate_cycle_analytics_data
stats_values = [] stats_values = []
cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"], cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"],
[:plan, "Plan", "Time before an issue starts implementation"], [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"],
[:code, "Code", "Time until first merge request"], [:code, "Code", "Related Merge Requests", "Time spent coding"],
[:test, "Test", "Total test time for all commits/merges"], [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"],
[:review, "Review", "Time between merge request creation and merge/close"], [:review, "Review", "Relative Merged Requests", "The time taken to review the code"],
[:staging, "Staging", "From merge request merge until deploy to production"], [:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"],
[:production, "Production", "From issue creation until deploy to production"]] [:production, "Production", "Related Issues", "The total time taken from idea to production"]]
stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)| stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)|
value = @cycle_analytics.send(stage_method).presence value = @cycle_analytics.send(stage_method).presence
stats_values << value.abs if value stats_values << value.abs if value
...@@ -45,6 +45,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -45,6 +45,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
stats << { stats << {
title: stage_text, title: stage_text,
description: stage_description, description: stage_description,
legend: stage_legend,
value: value && !value.zero? ? distance_of_time_in_words(value) : nil value: value && !value.zero? ? distance_of_time_in_words(value) : nil
} }
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
= chat_name.chat_name = chat_name.chat_name
%td %td
- if chat_name.last_used_at - if chat_name.last_used_at
time_ago_with_tooltip(chat_name.last_used_at) = time_ago_with_tooltip(chat_name.last_used_at)
- else - else
Never Never
......
.empty-stage-container
.empty-stage
.icon-no-data
= custom_icon ('icon_no_data')
%h4 We don’t have enough data to show this stage.
%p
{{currentStage.emptyStageText}}
.no-access-stage-container
.no-access-stage
.icon-lock
= custom_icon ('icon_lock')
%h4 You need permission.
%p
Want to see the data? Please ask administrator for access.
.cycle-analytics-overview
.container
.row
.col-md-10.col-md-offset-1
.row.overview-details
.col-md-6.overview-text
%h4 Introducing Cycle Analytics
%p
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
%p
%a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
.col-md-6.overview-image
%span.overview-icon
= custom_icon ('icon_cycle_analytics_overview')
- @no_container = true - @no_container = true
- page_title "Cycle Analytics" - page_title "Cycle Analytics"
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('cycle_analytics/cycle_analytics_bundle.js') = page_specific_javascript_tag("cycle_analytics/cycle_analytics_bundle.js")
= render "projects/pipelines/head" = render "projects/pipelines/head"
#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }} #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
.bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} .bordered-box.landing.content-block{"v-if" => "!isOverviewDialogDismissed"}
= icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()") = icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()")
.row .row
.col-sm-3.col-xs-12.svg-container .col-sm-3.col-xs-12.svg-container
= custom_icon('icon_cycle_analytics_splash') = custom_icon('icon_cycle_analytics_splash')
...@@ -20,21 +19,17 @@ ...@@ -20,21 +19,17 @@
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
= link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
= icon("spinner spin", "v-show" => "isLoading") = icon("spinner spin", "v-show" => "isLoading")
.wrapper{"v-show" => "!isLoading && !hasError"} .wrapper{"v-show" => "!isLoading && !hasError"}
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Pipeline Health Pipeline Health
.content-block .content-block
.container-fluid .container-fluid
.row .row
.col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"} .col-sm-3.col-xs-12.column{"v-for" => "item in state.summary"}
%h3.header {{item.value}} %h3.header {{item.value}}
%p.text {{item.title}} %p.text {{item.title}}
.col-sm-3.col-xs-12.column .col-sm-3.col-xs-12.column
.dropdown.inline.js-ca-dropdown .dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"} %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
...@@ -42,22 +37,54 @@ ...@@ -42,22 +37,54 @@
%i.fa.fa-chevron-down %i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
%a{'href' => "#", 'data-value' => '30'} %a{ "href" => "#", "data-value" => "30" }
Last 30 days Last 30 days
%li %li
%a{'href' => "#", 'data-value' => '90'} %a{ "href" => "#", "data-value" => "90" }
Last 90 days Last 90 days
.stage-panel-container
.bordered-box .panel.panel-default.stage-panel
%ul.content-list .panel-heading
%li{"v-for" => "item in analytics.stats"} %nav.col-headers
.container-fluid %ul
.row %li.stage-header
.col-xs-8.title-col %span.stage-name
%p.title Stage
{{item.title}} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" }
%p.text %li.median-header
{{item.description}} %span.stage-name
.col-xs-4.value-col Median
%span %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.", "aria-hidden" => "true" }
{{item.value}} %li.event-header
%span.stage-name
{{ currentStage ? currentStage.legend : 'Related Issues' }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The collection of events added to the data gathered for that stage.", "aria-hidden" => "true" }
%li.total-time-header
%span.stage-name
Total Time
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The time taken by each data entry gathered by that stage.", "aria-hidden" => "true" }
.stage-panel-body
%nav.stage-nav
%ul
%li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" }
.stage-nav-item-cell.stage-name
{{ stage.title }}
.stage-nav-item-cell.stage-median
%template{ "v-if" => "stage.isUserAllowed" }
%span{ "v-if" => "stage.value" }
{{ stage.value }}
%span.stage-empty{ "v-else" => true }
Not enough data
%template{ "v-else" => true }
%span.not-available
Not available
.section.stage-events
%template{ "v-if" => "isLoadingStage" }
= icon("spinner spin")
%template{ "v-if" => "currentStage && !currentStage.isUserAllowed" }
= render partial: "no_access"
%template{ "v-else" => true }
%template{ "v-if" => "isEmptyStage && !isLoadingStage" }
= render partial: "empty_stage"
%template{ "v-if" => "state.events.length && !isLoadingStage && !isEmptyStage" }
%component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.events" }
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= render "projects/issues/head" = render "projects/issues/head"
%div{ class: container_class } %div{ class: container_class }
.detail-page-header .detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) } .status-box{ class: status_box_class(@milestone) }
- if @milestone.closed? - if @milestone.closed?
Closed Closed
...@@ -12,13 +12,14 @@ ...@@ -12,13 +12,14 @@
Past due Past due
- else - else
Open Open
.header-text-content
%span.identifier %span.identifier
Milestone ##{@milestone.iid} Milestone ##{@milestone.iid}
- if @milestone.expires_at - if @milestone.expires_at
%span.creator %span.creator
&middot; &middot;
= @milestone.expires_at = @milestone.expires_at
.pull-right .milestone-buttons
- if can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
- if @milestone.active? - if @milestone.active?
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
......
<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path>
</svg>
This diff is collapsed.
<svg width="46px" height="54px" viewBox="227 0 46 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="0" y="20" width="46" height="34" rx="8"></rect>
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="34" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<path d="M29,16 C29,8.2680135 22.7319865,2 15,2 C7.2680135,2 1,8.2680135 1,16" id="path-3"></path>
<mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="28" height="14" fill="white">
<use xlink:href="#path-3"></use>
</mask>
</defs>
<g id="locker" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(227.000000, 0.000000)">
<g id="Group-8">
<use id="Rectangle-14" stroke="#B5A7DD" mask="url(#mask-2)" stroke-width="6" xlink:href="#path-1"></use>
<g id="Group-7" transform="translate(8.000000, 0.000000)">
<use id="Oval-3" stroke="#B5A7DD" mask="url(#mask-4)" stroke-width="6" xlink:href="#path-3"></use>
<rect id="Rectangle-13" fill="#B5A7DD" x="1" y="16" width="3" height="6"></rect>
<rect id="Rectangle-13-Copy" fill="#B5A7DD" x="26" y="16" width="3" height="6"></rect>
</g>
<path d="M25,37.4648712 C26.1956027,36.7732524 27,35.4805647 27,34 C27,31.790861 25.209139,30 23,30 C20.790861,30 19,31.790861 19,34 C19,35.4805647 19.8043973,36.7732524 21,37.4648712 L21,41.0026083 C21,42.1041422 21.8954305,43 23,43 C24.1122704,43 25,42.1057373 25,41.0026083 L25,37.4648712 Z" id="Combined-Shape" fill="#6B4FBB"></path>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="211 0 78 36" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<circle id="a" cx="5" cy="31" r="5"/>
<mask id="e" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#a"/>
</mask>
<circle id="b" cx="29" cy="14" r="5"/>
<mask id="f" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#b"/>
</mask>
<circle id="c" cx="53" cy="24" r="5"/>
<mask id="g" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#c"/>
</mask>
<circle id="d" cx="73" cy="5" r="5"/>
<mask id="h" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#d"/>
</mask>
</defs>
<g fill="none" fill-rule="evenodd" transform="translate(211)">
<path stroke="#B5A7DD" stroke-width="2" d="M5 31l24-17 26 10L73 5" stroke-linecap="round" stroke-dasharray="3 6"/>
<use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#e)" xlink:href="#a"/>
<use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#f)" xlink:href="#b"/>
<use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#g)" xlink:href="#c"/>
<use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#h)" xlink:href="#d"/>
</g>
</svg>
...@@ -3,6 +3,28 @@ ...@@ -3,6 +3,28 @@
.context.prepend-top-default .context.prepend-top-default
.milestone-summary .milestone-summary
%h4 Progress %h4 Progress
<<<<<<< HEAD
- remaining_days = milestone_remaining_days(milestone)
- if remaining_days
%span.milestone-stat
%span.remaining-days= remaining_days
- total_weight = milestone.issues_visible_to_user(current_user).sum(:weight)
- unless total_weight.zero?
%span.milestone-stat
Total weight:
%strong= total_weight
%span.pull-right.tab-issues-buttons
- if project && can?(current_user, :create_issue, project)
= link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do
New Issue
= link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped"
%span.pull-right.tab-merge-requests-buttons.hidden
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped"
=======
.milestone-stats-and-buttons
.milestone-stats
%span.milestone-stat.with-drilldown
%strong= milestone.issues_visible_to_user(current_user).size %strong= milestone.issues_visible_to_user(current_user).size
issues: issues:
%span.milestone-stat %span.milestone-stat
...@@ -10,6 +32,7 @@ ...@@ -10,6 +32,7 @@
open and open and
%strong= milestone.issues_visible_to_user(current_user).closed.size %strong= milestone.issues_visible_to_user(current_user).closed.size
closed closed
%span.milestone-stat.with-drilldown
%strong= milestone.merge_requests.size %strong= milestone.merge_requests.size
merge requests: merge requests:
%span.milestone-stat %span.milestone-stat
...@@ -20,22 +43,17 @@ ...@@ -20,22 +43,17 @@
%span.milestone-stat %span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}% %strong== #{milestone.percent_complete(current_user)}%
complete complete
- remaining_days = milestone_remaining_days(milestone)
- if remaining_days
%span.milestone-stat
%span.remaining-days= remaining_days
- total_weight = milestone.issues_visible_to_user(current_user).sum(:weight)
- unless total_weight.zero?
%span.milestone-stat %span.milestone-stat
Total weight: %span.remaining-days= milestone_remaining_days(milestone)
%strong= total_weight >>>>>>> ce/master
%span.pull-right.tab-issues-buttons
.milestone-progress-buttons
%span.tab-issues-buttons
- if project && can?(current_user, :create_issue, project) - if project && can?(current_user, :create_issue, project)
= link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
New Issue New Issue
= link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped" = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
%span.pull-right.tab-merge-requests-buttons.hidden %span.tab-merge-requests-buttons.hidden
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped" = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
= milestone_progress_bar(milestone) = milestone_progress_bar(milestone)
---
title: Show events per stage on Cycle Analytics page
merge_request: 23449
author:
module Gitlab module Gitlab
module ChatCommands module ChatCommands
class Deploy < BaseCommand class Deploy < BaseCommand
include Gitlab::Routing.url_helpers
def self.match(text) def self.match(text)
/\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text) /\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text)
end end
...@@ -25,7 +27,7 @@ module Gitlab ...@@ -25,7 +27,7 @@ module Gitlab
return unless actions.present? return unless actions.present?
if actions.one? if actions.one?
actions.first.play(current_user) play!(from, to, actions.first)
else else
Result.new(:error, 'Too many actions defined') Result.new(:error, 'Too many actions defined')
end end
...@@ -33,12 +35,23 @@ module Gitlab ...@@ -33,12 +35,23 @@ module Gitlab
private private
def play!(from, to, action)
new_action = action.play(current_user)
Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
end
def find_actions(from, to) def find_actions(from, to)
environment = project.environments.find_by(name: from) environment = project.environments.find_by(name: from)
return unless environment return unless environment
environment.actions_for(to).select(&:starts_environment?) environment.actions_for(to).select(&:starts_environment?)
end end
def url(subject)
polymorphic_url(
[ subject.project.namespace.becomes(Namespace), subject.project, subject ])
end
end end
end end
end end
...@@ -49,8 +49,13 @@ module Mattermost ...@@ -49,8 +49,13 @@ module Mattermost
private private
def show_result(result) def show_result(result)
case result.type
when :success
in_channel_response(result.message)
else
ephemeral_response(result.message) ephemeral_response(result.message)
end end
end
def not_found def not_found
ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
......
...@@ -74,7 +74,7 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -74,7 +74,7 @@ describe Gitlab::ChatCommands::Command, service: true do
end end
it 'returns action' do it 'returns action' do
expect(subject[:text]).to include(manual.name) expect(subject[:text]).to include('Deployment from staging to production started')
expect(subject[:response_type]).to be(:in_channel) expect(subject[:response_type]).to be(:in_channel)
end end
......
...@@ -36,8 +36,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do ...@@ -36,8 +36,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production') create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
end end
it 'returns action' do it 'returns success result' do
expect(subject).to eq(manual1) expect(subject.type).to eq(:success)
expect(subject.message).to include('Deployment from staging to production started')
end end
context 'when duplicate action exists' do context 'when duplicate action exists' do
...@@ -46,7 +47,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do ...@@ -46,7 +47,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end end
it 'returns error' do it 'returns error' do
expect(subject.message).to eq('Too many actions defined') expect(subject.type).to eq(:error)
expect(subject.message).to include('Too many actions defined')
end end
end end
...@@ -57,8 +59,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do ...@@ -57,8 +59,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
name: 'teardown', environment: 'production') name: 'teardown', environment: 'production')
end end
it 'returns error' do it 'returns success result' do
expect(subject).to eq(manual1) expect(subject.type).to eq(:success)
expect(subject.message).to include('Deployment from staging to production started')
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment