Cycle analytics second iteration

- Vue app has been completely rewritten
- New components
- Basic CSS
This commit is contained in:
Alfredo Sumaran 2016-10-20 18:24:36 -05:00
parent 6f824b156f
commit 10282283b0
22 changed files with 752 additions and 154 deletions

View File

@ -23,7 +23,8 @@
"spyOn": false, "spyOn": false,
"spyOnEvent": false, "spyOnEvent": false,
"Turbolinks": false, "Turbolinks": false,
"window": false "window": false,
"Vue": false,
"Flash": false
} }
} }

View File

@ -0,0 +1,24 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
/*
`build` prop should have
- Build name/title
- Build ID
- Build URL
- Build branch
- Build branch URL
- Build short SHA
- Build commit URL
- Build date
- Total time
*/
global.cycleAnalytics.ItemBuildComponent = Vue.extend({
template: '#item-build-component',
props: {
build: Object,
}
});
}(window.gl || (window.gl = {})));

View File

@ -0,0 +1,22 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
/*
`commit` prop should have
- Commit title
- Commit URL
- Commit Short SHA
- Commit author
- Commit author profile URL
- Commit author avatar URL
- Total time
*/
global.cycleAnalytics.ItemCommitComponent = Vue.extend({
template: '#item-commit-component',
props: {
commit: Object,
}
});
}(window.gl || (window.gl = {})));

View File

@ -0,0 +1,23 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
/*
`issue` prop should have
- Issue title
- Issue URL
- Issue ID
- Issue date created
- Issue author
- Issue author profile URL
- Issue author avatar URL
- Total time
*/
global.cycleAnalytics.ItemIssueComponent = Vue.extend({
template: '#item-issue-component',
props: {
issue: Object,
}
});
})(window.gl || (window.gl = {}));

View File

@ -0,0 +1,23 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
/*
`mergeRequest` prop should have
- MR title
- MR URL
- MR ID
- MR date opened
- MR author
- MR author profile URL
- MR author avatar URL
- Total time
*/
global.cycleAnalytics.ItemMergeRequestComponent = Vue.extend({
template: '#item-merge-request-component',
props: {
mergeRequest: Object,
}
});
}(window.gl || (window.gl = {})));

View File

@ -0,0 +1,26 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageButton = Vue.extend({
props: {
stage: Object,
onStageClick: Function
},
computed: {
classObject() {
return {
'active': this.stage.active
}
}
},
methods: {
onClick(stage) {
this.onStageClick(stage);
}
}
});
})(window.gl || (window.gl = {}));

View File

@ -0,0 +1,15 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageCodeComponent = Vue.extend({
template: '#stage-code-component',
components: {
'item-merge-request-component': gl.cycleAnalytics.ItemMergeRequestComponent,
},
props: {
items: Array,
}
});
})(window.gl || (window.gl = {}));

View File

@ -0,0 +1,15 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageIssueComponent = Vue.extend({
template: '#stage-issue-component',
components: {
'item-issue-component': gl.cycleAnalytics.ItemIssueComponent,
},
props: {
items: Array,
}
});
})(window.gl || (window.gl = {}));

View File

@ -0,0 +1,15 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StagePlanComponent = Vue.extend({
template: '#stage-plan-component',
components: {
'item-commit-component': gl.cycleAnalytics.ItemCommitComponent,
},
props: {
items: Array,
}
});
})(window.gl || (window.gl = {}));

View File

@ -0,0 +1,15 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageProductionComponent = Vue.extend({
template: '#stage-production-component',
components: {
'item-issue-component': gl.cycleAnalytics.ItemIssueComponent,
},
props: {
items: Array,
}
});
})(window.gl || (window.gl = {}));

View File

@ -0,0 +1,15 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageReviewComponent = Vue.extend({
template: '#stage-review-component',
components: {
'item-merge-request-component': gl.cycleAnalytics.ItemMergeRequestComponent,
},
props: {
items: Array,
}
});
})(window.gl || (window.gl = {}));

View File

@ -0,0 +1,15 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageStagingComponent = Vue.extend({
template: '#stage-staging-component',
components: {
'item-build-component': gl.cycleAnalytics.ItemBuildComponent,
},
props: {
items: Array,
}
});
})(window.gl || (window.gl = {}));

View File

@ -0,0 +1,15 @@
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageTestComponent = Vue.extend({
template: '#stage-test-component',
components: {
'item-build-component': gl.cycleAnalytics.ItemBuildComponent,
},
props: {
items: Array,
}
});
})(window.gl || (window.gl = {}));

View File

@ -1,98 +0,0 @@
/* eslint-disable */
//= require vue
((global) => {
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',
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: {
cycle_analytics: {
start_date: options.startDate
}
}
}).done((data) => {
this.decorateData(data);
this.initDropdown();
})
.error((data) => {
this.handleError(data);
})
.always(() => {
store.isLoading = false;
})
}
decorateData(data) {
data.summary = data.summary || [];
data.stats = data.stats || [];
data.summary.forEach((item) => {
item.value = item.value || '-';
});
data.stats.forEach((item) => {
item.value = item.value || '- - -';
});
store.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() {
const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label');
$dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
const value = $target.data('value');
$label.text($target.text().trim());
this.fetchData({ startDate: value });
})
}
}
})(window.gl || (window.gl = {}));

View File

@ -2,24 +2,48 @@
//= require_tree . //= require_tree .
$(() => { $(() => {
const EMPTY_DIALOG_COOKIE = 'ca_empty_dialog_dismissed';
const OVERVIEW_DIALOG_COOKIE = 'ca_overview_dialog_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore; const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({ const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath requestPath: cycleAnalyticsEl.dataset.requestPath,
}) });
gl.cycleAnalyticsApp = new Vue({ gl.cycleAnalyticsApp = new Vue({
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
data: cycleAnalyticsStore.state, data: {
state: cycleAnalyticsStore.state,
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
startDate: 30,
isEmptyDialogDismissed: Cookies.get(EMPTY_DIALOG_COOKIE),
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
},
computed: {
currentStage() {
return cycleAnalyticsStore.currentActiveStage();
},
},
components: {
'stage-button': gl.cycleAnalytics.StageButton,
'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
'stage-test-component': gl.cycleAnalytics.StageTestComponent,
'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
},
created() { created() {
this.fetchCycleAnalyticsData(); this.fetchCycleAnalyticsData();
}, },
methods: { methods: {
handleError(data) { handleError() {
cycleAnalyticsStore.setErrorState(true); cycleAnalyticsStore.setErrorState(true);
new Flash('There was an error while fetching cycle analytics data.'); return new Flash('There was an error while fetching cycle analytics data.');
}, },
initDropdown() { initDropdown() {
const $dropdown = $('.js-ca-dropdown'); const $dropdown = $('.js-ca-dropdown');
@ -28,30 +52,66 @@ $(() => {
$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.fetchCycleAnalyticsData({ startDate: value }); this.fetchCycleAnalyticsData({ startDate: this.startDate });
}); });
}, },
fetchCycleAnalyticsData(options) { fetchCycleAnalyticsData(options) {
options = options || { startDate: 30 }; const fetchOptions = options || { startDate: this.startDate };
cycleAnalyticsStore.setLoadingState(true); this.isLoading = true;
cycleAnalyticsService cycleAnalyticsService
.fetchCycleAnalyticsData(options) .fetchCycleAnalyticsData(fetchOptions)
.then((response) => { .done((response) => {
cycleAnalyticsStore.setCycleAnalyticsData(response); cycleAnalyticsStore.setCycleAnalyticsData(response);
this.selectDefaultStage();
this.initDropdown(); this.initDropdown();
}) })
.fail(() => { .error(() => {
this.handleError(data); this.handleError();
}) })
.always(() => { .always(() => {
cycleAnalyticsStore.setLoadingState(false); this.isLoading = false;
}); });
} },
} selectDefaultStage() {
this.selectStage(this.state.stages.first());
},
selectStage(stage) {
if (this.isLoadingStage) return;
if (this.currentStage === stage) return;
this.isLoadingStage = true;
cycleAnalyticsStore.setStageItems([]);
cycleAnalyticsStore.setActiveStage(stage);
cycleAnalyticsService
.fetchStageData({
stage,
startDate: this.startDate,
})
.done((response) => {
this.isEmptyStage = !response.items.length;
cycleAnalyticsStore.setStageItems(response.items);
})
.error(() => {
this.isEmptyStage = true;
})
.always(() => {
this.isLoadingStage = false;
});
},
dismissEmptyDialog() {
this.isEmptyDialogDismissed = true;
Cookies.set(EMPTY_DIALOG_COOKIE, '1');
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
},
},
}); });
}); });

View File

@ -21,6 +21,19 @@
} }
}); });
} }
fetchStageData(options) {
let {
stage,
startDate,
} = options;
return $.get(`http://localhost:8000/${stage.name.toLowerCase()}.json`, {
cycle_analytics: {
start_date: options.startDate
}
});
}
}; };
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService; global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;

View File

@ -3,11 +3,54 @@
global.cycleAnalytics.CycleAnalyticsStore = { global.cycleAnalytics.CycleAnalyticsStore = {
state: { state: {
isLoading: true,
hasError: false,
summary: '', summary: '',
stats: '', stats: '',
analytics: '' analytics: '',
items: [],
stages:[
{
name:'Issue',
active: false,
component: 'stage-issue-component',
legendTitle: 'Related Issues',
},
{
name:'Plan',
active: false,
component: 'stage-plan-component',
legendTitle: 'Related Commits',
},
{
name:'Code',
active: false,
component: 'stage-code-component',
legendTitle: 'Related Merge Requests',
},
{
name:'Test',
active: false,
component: 'stage-test-component',
legendTitle: 'Relative Builds Trigger by Commits',
},
{
name:'Review',
active: false,
component: 'stage-review-component',
legendTitle: 'Relative Merged Requests',
},
{
name:'Staging',
active: false,
component: 'stage-staging-component',
legendTitle: 'Relative Deployed Builds',
},
{
name:'Production',
active: false,
component: 'stage-production-component',
legendTitle: 'Related Issues',
}
],
}, },
setCycleAnalyticsData(data) { setCycleAnalyticsData(data) {
this.state = Object.assign(this.state, this.decorateData(data)); this.state = Object.assign(this.state, this.decorateData(data));
@ -35,7 +78,22 @@
}, },
setErrorState(state) { setErrorState(state) {
this.state.hasError = state; this.state.hasError = state;
} },
deactivateAllStages() {
this.state.stages.forEach(stage => {
stage.active = false;
});
},
setActiveStage(stage) {
this.deactivateAllStages();
stage.active = true;
},
setStageItems(items) {
this.state.items = items;
},
currentActiveStage() {
return this.state.stages.find(stage => stage.active);
},
}; };
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));

View File

@ -160,6 +160,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;
@ -283,6 +284,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

View File

@ -1,10 +1,56 @@
#cycle-analytics { #cycle-analytics {
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: 16%;
padding-left: $gl-padding;
}
.median-header {
width: 12%;
}
.delta-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 +81,16 @@
} }
&: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;
}
} }
.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 +180,152 @@
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: solid 1px transparent;
border-bottom: solid 1px transparent;
border-right: solid 1px $border-color;
background-color: $gray-light;
&.active {
background-color: transparent;
border-right-color: transparent;
border-top-color: $border-color;
border-bottom-color: $border-color;
box-shadow: inset 2px 0px 0px 0px $active-item-blue;
.stage-name {
font-weight: 600;
}
}
&:first-child {
border-top: none;
}
&:last-child {
border-bottom: none;
}
> div {
float: left;
&.stage-name {
width: 40%;
}
&.stage-median {
width: 30%;
}
&.stage-delta {
width: 30%;
.stage-direction {
float: right;
padding-right: $gl-padding;
}
}
}
.stage-name {
padding-left: 16px;
}
}
}
.stage-panel {
.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 $gl-padding;
border-bottom: solid 1px $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 0;
a {
color: $gl-dark-link-color;
max-width: 100%;
display: block;
@include text-overflow();
}
}
.item-time {
width: 25%;
text-align: right;
font-size: $cycle-analytics-big-font;
color: $cycle-analytics-dark-text;
abbr {
font-size: $gl-font-size;
color: $gl-text-color;
}
}
}
} }

View File

@ -2,14 +2,17 @@
- 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) } }
.empty-dialog-message{ "v-if" => "!isEmptyDialogDismissed" }
%p There is nothing happened
= icon("times", class: "dismiss-icon", "@click" => "dismissEmptyDialog()")
.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 +23,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.analytics.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 +41,167 @@
%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
.panel.panel-default.stage-panel
.panel-heading
%nav.col-headers
%ul
%li.stage-header
%span.stage-name
Stage
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" }
%li.median-header
%span.stage-name
Median
%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" }
%li.delta-header
%span.stage-name
= render "shared/icons/delta.svg"
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The difference between the previous and last measure, expressed as positive or negative values. E.g., if the previous value was 5 and the new value is 7, the delta is +2.", "aria-hidden" => "true" }
%li.event-header
%span.stage-name
{{ currentStage ? currentStage.legendTitle : '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
%stage-button{ "inline-template" => true,
"v-for" => "stage in state.stages",
":stage" => "stage",
":on-stage-click" => "selectStage" }
%li.stage-nav-item{ ":class" => "classObject", "@click" => "onClick(stage)" }
.stage-name
{{stage.name}}
.stage-median
20 hrs 21 mins
.stage-delta
+ 20 days
%span.stage-direction
= render "shared/icons/down_arrow.svg"
.section.stage-events
%template{ "v-if" => "isLoadingStage" }
= icon("spinner spin", "v-show" => "isLoadingStage")
%template{ "v-if" => "isEmptyStage" }
%p No results
%template{ "v-if" => "state.items.length && !isLoadingStage && !isEmptyStage" }
%component{ ":is" => "currentStage.component", ":items" => "state.items" }
.bordered-box %script{ type: 'text/x-template', id: 'stage-issue-component' }
%ul.content-list %div
%li{"v-for" => "item in analytics.stats"} .events-description
.container-fluid Time before an issue get scheluded
.row %ul.stage-event-list
.col-xs-8.title-col %li.stage-event-item{ "v-for" => "issue in items" }
%p.title %item-issue-component{ ":issue" => "issue" }
{{item.title}}
%p.text %script{ type: 'text/x-template', id: 'stage-plan-component' }
{{item.description}} %div
.col-xs-4.value-col .events-description
%span Time before an issue starts implementation
{{item.value}} %ul.event-list
%li.event-item{ "v-for" => "commit in items" }
%item-commit-component{ ":commit" => "commit" }
%script{ type: 'text/x-template', id: 'stage-code-component' }
%div
.events-description
Time spent coding
%ul
%li{ "v-for" => "mergeRequest in items" }
%item-merge-request-component{ ":merge-request" => "mergeRequest" }
%script{ type: 'text/x-template', id: 'stage-test-component' }
%div
.events-description
The time taken to build and test the application
%ul
%li{ "v-for" => "build in items" }
%item-build-component{ ":build" => "build" }
%script{ type: 'text/x-template', id: 'stage-review-component' }
%div
.events-description
The time taken to review the code
%ul
%li{ "v-for" => "mergeRequest in items" }
%item-merge-request-component{ ":merge-request" => "mergeRequest" }
%script{ type: 'text/x-template', id: 'stage-staging-component' }
%div
.events-description
The time taken in staging
%ul
%li{ "v-for" => "build in items" }
%item-build-component{ ":build" => "build" }
%script{ type: 'text/x-template', id: 'stage-production-component' }
%div
.events-description
The total time taken from idea to production
%ul
%li{ "v-for" => "issue in items" }
%item-issue-component{ ":issue" => "issue" }
%script{ type: 'text/x-template', id: 'item-issue-component' }
.item-details
%img.avatar{:src => "https://secure.gravatar.com/avatar/3731e7dd4f2b4fa8ae184c0a7519dd58?s=64&d=identicon"}/
%h5.item-title
%a{ :href => "issue.url" }
{{ issue.title }}
%a{ :href => "issue.url" }
= '#{{issue.id}}'
%span
Opened
%a{:href => "issue.url"}
{{ issue.datetime }}
%span
by
%a{:href => "issue.profile"}
{{ issue.author }}
.item-time
%span.hours{ "v-if" => "issue.totalTime.hours"}
{{ issue.totalTime.hours }}
%abbr{:title => "Hours"} hr
%span.minutes{ "v-if" => "issue.totalTime.minutes" }
{{ issue.totalTime.minutes }}
%abbr{:title => "Minutes"} mins
%script{ type: 'text/x-template', id: 'item-commit-component' }
%div
%p
%h5
%a{:href => "commit.url"}
{{ commit.title }}
%span
First
%a{:href => "#"}
{{ commit.hash }}
pushed by
%a{:href => "commit.profile"}
{{ commit.author }}
%script{ type: 'text/x-template', id: 'item-merge-request-component' }
%div
%p
%h5
merge request -
%a{:href => "mergeRequest.url"}
{{ mergeRequest.title }}
%script{ type: 'text/x-template', id: 'item-build-component' }
%div
%p
%h5
build -
%a{:href => "build.url"}
{{ build.title }}

View File

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 549 B

View File

@ -0,0 +1,3 @@
<svg width="9px" height="12px" viewBox="4 3 9 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M10,8.01971215 L10,13.022682 C10,13.5733266 9.55613518,14.0197122 9,14.0197122 C8.44771525,14.0197122 8,13.5666758 8,13.022682 L8,8.01971215 L5.99703014,8.01971215 C5.4463856,8.01971215 5.2749362,7.6760419 5.625,7.23846215 L8.375,3.80096215 C8.72017797,3.36948969 9.2749362,3.3633824 9.625,3.80096215 L12.375,7.23846215 C12.720178,7.66993461 12.5469637,8.01971215 12.0029699,8.01971215 L10,8.01971215 Z" id="Combined-Shape" stroke="none" fill="#31AF64" fill-rule="evenodd" transform="translate(8.998117, 8.747388) scale(1, -1) translate(-8.998117, -8.747388) "></path>
</svg>

After

Width:  |  Height:  |  Size: 729 B