summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlfredo Sumaran <alfredo@gitlab.com>2016-10-20 18:24:36 -0500
committerAlfredo Sumaran <alfredo@gitlab.com>2016-11-09 00:14:14 -0500
commit0b82d2b596dadee922383c92e82532f9487737d5 (patch)
treea22097fb4526f8f2806259257b5d3015688ab926
parent6301f3712ce454209f172178f186231d8bc8f498 (diff)
downloadgitlab-ce-22458-cycle-analytics-2-frontend.tar.gz
Cycle analytics second iteration22458-cycle-analytics-2-frontend
- Vue app has been completely rewritten - New components - Basic CSS
-rw-r--r--.eslintrc5
-rw-r--r--app/assets/javascripts/cycle_analytics/components/item_build_component.js.es624
-rw-r--r--app/assets/javascripts/cycle_analytics/components/item_commit_component.js.es622
-rw-r--r--app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es623
-rw-r--r--app/assets/javascripts/cycle_analytics/components/item_merge_request_component.js.es623
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_button.js.es626
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es615
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es615
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es615
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es615
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es615
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es615
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es615
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics.js.es698
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js94
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es613
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es666
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss205
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml192
-rw-r--r--app/views/shared/icons/_delta.svg3
-rw-r--r--app/views/shared/icons/_down_arrow.svg3
22 files changed, 752 insertions, 154 deletions
diff --git a/.eslintrc b/.eslintrc
index fd26215b843..2cd05e9ba26 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -23,7 +23,8 @@
"spyOn": false,
"spyOnEvent": false,
"Turbolinks": false,
- "window": false
+ "window": false,
+ "Vue": false,
+ "Flash": false
}
}
-
diff --git a/app/assets/javascripts/cycle_analytics/components/item_build_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/item_build_component.js.es6
new file mode 100644
index 00000000000..d4c488dc3a8
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/item_build_component.js.es6
@@ -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 = {})));
diff --git a/app/assets/javascripts/cycle_analytics/components/item_commit_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/item_commit_component.js.es6
new file mode 100644
index 00000000000..344cb77d7cc
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/item_commit_component.js.es6
@@ -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 = {})));
diff --git a/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6
new file mode 100644
index 00000000000..f4c3d92bd56
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6
@@ -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 = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/item_merge_request_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/item_merge_request_component.js.es6
new file mode 100644
index 00000000000..488f6f901ff
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/item_merge_request_component.js.es6
@@ -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 = {})));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_button.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_button.js.es6
new file mode 100644
index 00000000000..f5594c1244d
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_button.js.es6
@@ -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 = {}));
+
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6
new file mode 100644
index 00000000000..bdc9617f463
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6
@@ -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 = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6
new file mode 100644
index 00000000000..e4da9294b53
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6
@@ -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 = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
new file mode 100644
index 00000000000..2dcc0ee9699
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
@@ -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 = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6
new file mode 100644
index 00000000000..fea2e1edacb
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6
@@ -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 = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6
new file mode 100644
index 00000000000..292f8ada3f4
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6
@@ -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 = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6
new file mode 100644
index 00000000000..2a4cf97386a
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6
@@ -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 = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6
new file mode 100644
index 00000000000..7e16ae67f66
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6
@@ -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 = {}));
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics.js.es6
deleted file mode 100644
index 331f0209888..00000000000
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics.js.es6
+++ /dev/null
@@ -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 = {}));
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index d7cec96d137..f076644b037 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -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');
+ },
+ },
});
});
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6
index b043dbfdbfb..e5a30109ca6 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6
@@ -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;
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
index 1715097bfb3..7c8461b85ae 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
@@ -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 = {}));
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index be2a7ceefff..89bbad1b5c4 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -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
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 572e1e7d558..09625d178c5 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -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;
+ }
+ }
+ }
}
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 247d612ba6f..06a6e24ac49 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -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" }
+
+%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 }}
- .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: 'item-build-component' }
+ %div
+ %p
+ %h5
+ build -
+ %a{:href => "build.url"}
+ {{ build.title }}
diff --git a/app/views/shared/icons/_delta.svg b/app/views/shared/icons/_delta.svg
new file mode 100644
index 00000000000..7c0c0d3999c
--- /dev/null
+++ b/app/views/shared/icons/_delta.svg
@@ -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>
diff --git a/app/views/shared/icons/_down_arrow.svg b/app/views/shared/icons/_down_arrow.svg
new file mode 100644
index 00000000000..123116f4ca9
--- /dev/null
+++ b/app/views/shared/icons/_down_arrow.svg
@@ -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> \ No newline at end of file