Cycle analytics second iteration
- Vue app has been completely rewritten - New components - Basic CSS
This commit is contained in:
parent
6f824b156f
commit
10282283b0
|
@ -23,7 +23,8 @@
|
|||
"spyOn": false,
|
||||
"spyOnEvent": false,
|
||||
"Turbolinks": false,
|
||||
"window": false
|
||||
"window": false,
|
||||
"Vue": false,
|
||||
"Flash": false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {})));
|
|
@ -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 = {})));
|
|
@ -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 = {}));
|
|
@ -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 = {})));
|
|
@ -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 = {}));
|
||||
|
|
@ -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 = {}));
|
|
@ -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 = {}));
|
|
@ -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 = {}));
|
|
@ -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 = {}));
|
|
@ -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 = {}));
|
|
@ -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 = {}));
|
|
@ -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 = {}));
|
|
@ -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 = {}));
|
|
@ -2,24 +2,48 @@
|
|||
//= 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 cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
|
||||
const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
|
||||
requestPath: cycleAnalyticsEl.dataset.requestPath
|
||||
})
|
||||
requestPath: cycleAnalyticsEl.dataset.requestPath,
|
||||
});
|
||||
|
||||
gl.cycleAnalyticsApp = new Vue({
|
||||
el: '#cycle-analytics',
|
||||
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() {
|
||||
this.fetchCycleAnalyticsData();
|
||||
},
|
||||
methods: {
|
||||
handleError(data) {
|
||||
handleError() {
|
||||
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() {
|
||||
const $dropdown = $('.js-ca-dropdown');
|
||||
|
@ -28,30 +52,66 @@ $(() => {
|
|||
$dropdown.find('li a').off('click').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
const value = $target.data('value');
|
||||
this.startDate = $target.data('value');
|
||||
|
||||
$label.text($target.text().trim());
|
||||
this.fetchCycleAnalyticsData({ startDate: value });
|
||||
this.fetchCycleAnalyticsData({ startDate: this.startDate });
|
||||
});
|
||||
},
|
||||
fetchCycleAnalyticsData(options) {
|
||||
options = options || { startDate: 30 };
|
||||
const fetchOptions = options || { startDate: this.startDate };
|
||||
|
||||
cycleAnalyticsStore.setLoadingState(true);
|
||||
this.isLoading = true;
|
||||
|
||||
cycleAnalyticsService
|
||||
.fetchCycleAnalyticsData(options)
|
||||
.then((response) => {
|
||||
.fetchCycleAnalyticsData(fetchOptions)
|
||||
.done((response) => {
|
||||
cycleAnalyticsStore.setCycleAnalyticsData(response);
|
||||
this.selectDefaultStage();
|
||||
this.initDropdown();
|
||||
})
|
||||
.fail(() => {
|
||||
this.handleError(data);
|
||||
.error(() => {
|
||||
this.handleError();
|
||||
})
|
||||
.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');
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -3,11 +3,54 @@
|
|||
|
||||
global.cycleAnalytics.CycleAnalyticsStore = {
|
||||
state: {
|
||||
isLoading: true,
|
||||
hasError: false,
|
||||
summary: '',
|
||||
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) {
|
||||
this.state = Object.assign(this.state, this.decorateData(data));
|
||||
|
@ -35,7 +78,22 @@
|
|||
},
|
||||
setErrorState(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 = {}));
|
||||
|
|
|
@ -160,6 +160,7 @@ $settings-icon-size: 18px;
|
|||
$provider-btn-group-border: #e5e5e5;
|
||||
$provider-btn-not-active-color: #4688f1;
|
||||
$link-underline-blue: #4a8bee;
|
||||
$active-item-blue: #4a8bee;
|
||||
$layout-link-gray: #7e7c7c;
|
||||
$todo-alert-blue: #428bca;
|
||||
$btn-side-margin: 10px;
|
||||
|
@ -283,6 +284,9 @@ $calendar-unselectable-bg: $gray-light;
|
|||
*/
|
||||
$cycle-analytics-box-padding: 30px;
|
||||
$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
|
||||
|
|
|
@ -1,10 +1,56 @@
|
|||
#cycle-analytics {
|
||||
margin: 24px auto 0;
|
||||
max-width: 800px;
|
||||
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 {
|
||||
padding: 24px 0;
|
||||
border-bottom: none;
|
||||
|
@ -35,23 +81,16 @@
|
|||
}
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
|
||||
@media (max-width: $screen-sm-min) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
top: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.bordered-box {
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-default;
|
||||
|
||||
}
|
||||
|
||||
.content-list {
|
||||
|
@ -141,4 +180,152 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,17 @@
|
|||
- page_title "Cycle Analytics"
|
||||
|
||||
- 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"
|
||||
|
||||
#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"}
|
||||
= icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()")
|
||||
.bordered-box.landing.content-block{"v-if" => "!isOverviewDialogDismissed"}
|
||||
= icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()")
|
||||
.row
|
||||
.col-sm-3.col-xs-12.svg-container
|
||||
= 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.
|
||||
|
||||
= link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
|
||||
|
||||
= icon("spinner spin", "v-show" => "isLoading")
|
||||
|
||||
.wrapper{"v-show" => "!isLoading && !hasError"}
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
Pipeline Health
|
||||
|
||||
.content-block
|
||||
.container-fluid
|
||||
.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}}
|
||||
%p.text {{item.title}}
|
||||
|
||||
.col-sm-3.col-xs-12.column
|
||||
.dropdown.inline.js-ca-dropdown
|
||||
%button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
|
||||
|
@ -42,22 +41,167 @@
|
|||
%i.fa.fa-chevron-down
|
||||
%ul.dropdown-menu.dropdown-menu-align-right
|
||||
%li
|
||||
%a{'href' => "#", 'data-value' => '30'}
|
||||
%a{ "href" => "#", "data-value" => "30" }
|
||||
Last 30 days
|
||||
%li
|
||||
%a{'href' => "#", 'data-value' => '90'}
|
||||
%a{ "href" => "#", "data-value" => "90" }
|
||||
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
|
||||
%ul.content-list
|
||||
%li{"v-for" => "item in analytics.stats"}
|
||||
.container-fluid
|
||||
.row
|
||||
.col-xs-8.title-col
|
||||
%p.title
|
||||
{{item.title}}
|
||||
%p.text
|
||||
{{item.description}}
|
||||
.col-xs-4.value-col
|
||||
%span
|
||||
{{item.value}}
|
||||
%script{ type: 'text/x-template', id: 'stage-issue-component' }
|
||||
%div
|
||||
.events-description
|
||||
Time before an issue get scheluded
|
||||
%ul.stage-event-list
|
||||
%li.stage-event-item{ "v-for" => "issue in items" }
|
||||
%item-issue-component{ ":issue" => "issue" }
|
||||
|
||||
%script{ type: 'text/x-template', id: 'stage-plan-component' }
|
||||
%div
|
||||
.events-description
|
||||
Time before an issue starts implementation
|
||||
%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 }}
|
||||
|
|
|
@ -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 |
|
@ -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 |
Loading…
Reference in New Issue