Commit 10282283 authored by Alfredo Sumaran's avatar Alfredo Sumaran

Cycle analytics second iteration

- Vue app has been completely rewritten
- New components
- Basic CSS
parent 6f824b15
......@@ -23,7 +23,8 @@
"spyOn": false,
"spyOnEvent": false,
"Turbolinks": false,
"window": false
"window": false,
"Vue": false,
"Flash": false
}
}
((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 = {})));
((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 = {})));
((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 = {}));
((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 = {})));
((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 = {}));
((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 = {}));
((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 = {}));
((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 = {}));
((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 = {}));
((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 = {}));
((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 = {}));
((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 = {}));
/* 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(() => {
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(() => {
cycleAnalyticsStore.setLoadingState(false);
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
......
#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 {