From f95d7eddcd2173c97de2b4504675c28969c5a38d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 19 Oct 2016 03:17:33 -0500 Subject: [PATCH 01/53] Load cycle analytics related JS filed only when needed --- .../{cycle_analytics_bundle.js.es6 => cycle_analytics.js.es6} | 0 .../javascripts/cycle_analytics/cycle_analytics_bundle.js | 2 ++ 2 files changed, 2 insertions(+) rename app/assets/javascripts/cycle_analytics/{cycle_analytics_bundle.js.es6 => cycle_analytics.js.es6} (100%) create mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics.js.es6 similarity index 100% rename from app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 rename to app/assets/javascripts/cycle_analytics/cycle_analytics.js.es6 diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js new file mode 100644 index 00000000000..d618daef524 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -0,0 +1,2 @@ +//= require vue +//= require_tree . From 6f824b156fcc881aa4f3341a2fef119d43058073 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 19 Oct 2016 23:34:16 -0500 Subject: [PATCH 02/53] Improve first implementation --- .../cycle_analytics/cycle_analytics_bundle.js | 55 +++++++++++++++++++ .../cycle_analytics_service.js.es6 | 28 ++++++++++ .../cycle_analytics_store.js.es6 | 41 ++++++++++++++ app/assets/javascripts/dispatcher.js.es6 | 3 - 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index d618daef524..d7cec96d137 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -1,2 +1,57 @@ //= require vue //= require_tree . + +$(() => { + + const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); + const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore; + const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({ + requestPath: cycleAnalyticsEl.dataset.requestPath + }) + + gl.cycleAnalyticsApp = new Vue({ + el: '#cycle-analytics', + name: 'CycleAnalytics', + data: cycleAnalyticsStore.state, + created() { + this.fetchCycleAnalyticsData(); + }, + methods: { + handleError(data) { + cycleAnalyticsStore.setErrorState(true); + new Flash('There was an error while fetching cycle analytics data.'); + }, + 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.fetchCycleAnalyticsData({ startDate: value }); + }); + }, + fetchCycleAnalyticsData(options) { + options = options || { startDate: 30 }; + + cycleAnalyticsStore.setLoadingState(true); + + cycleAnalyticsService + .fetchCycleAnalyticsData(options) + .then((response) => { + cycleAnalyticsStore.setCycleAnalyticsData(response); + this.initDropdown(); + }) + .fail(() => { + this.handleError(data); + }) + .always(() => { + cycleAnalyticsStore.setLoadingState(false); + }); + } + } + }); +}); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 new file mode 100644 index 00000000000..b043dbfdbfb --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 @@ -0,0 +1,28 @@ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + class CycleAnalyticsService { + constructor(options) { + this.requestPath = options.requestPath; + } + + fetchCycleAnalyticsData(options) { + options = options || { startDate: 30 }; + + return $.ajax({ + url: this.requestPath, + method: 'GET', + dataType: 'json', + contentType: 'application/json', + data: { + cycle_analytics: { + start_date: options.startDate + } + } + }); + } + }; + + global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 new file mode 100644 index 00000000000..1715097bfb3 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -0,0 +1,41 @@ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.CycleAnalyticsStore = { + state: { + isLoading: true, + hasError: false, + summary: '', + stats: '', + analytics: '' + }, + setCycleAnalyticsData(data) { + this.state = Object.assign(this.state, this.decorateData(data)); + }, + decorateData(data) { + let newData = {}; + + newData.stats = data.stats || []; + newData.summary = data.summary || []; + + newData.summary.forEach((item) => { + item.value = item.value || '-'; + }); + + newData.stats.forEach((item) => { + item.value = item.value || '- - -'; + }); + + newData.analytics = data; + + return newData; + }, + setLoadingState(state) { + this.state.isLoading = state; + }, + setErrorState(state) { + this.state.hasError = state; + } + }; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 756a24cc0fc..2867b3d1c28 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -208,9 +208,6 @@ new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); break; - case 'projects:cycle_analytics:show': - new gl.CycleAnalytics(); - break; } switch (path.first()) { case 'admin': From 10282283b04b6516542cb2fe86e33b886d17b129 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 20 Oct 2016 18:24:36 -0500 Subject: [PATCH 03/53] Cycle analytics second iteration - Vue app has been completely rewritten - New components - Basic CSS --- .eslintrc | 5 +- .../components/item_build_component.js.es6 | 24 ++ .../components/item_commit_component.js.es6 | 22 ++ .../components/item_issue_component.js.es6 | 23 ++ .../item_merge_request_component.js.es6 | 23 ++ .../components/stage_button.js.es6 | 26 +++ .../components/stage_code_component.js.es6 | 15 ++ .../components/stage_issue_component.js.es6 | 15 ++ .../components/stage_plan_component.js.es6 | 15 ++ .../stage_production_component.js.es6 | 15 ++ .../components/stage_review_component.js.es6 | 15 ++ .../components/stage_staging_component.js.es6 | 15 ++ .../components/stage_test_component.js.es6 | 15 ++ .../cycle_analytics/cycle_analytics.js.es6 | 98 --------- .../cycle_analytics/cycle_analytics_bundle.js | 94 ++++++-- .../cycle_analytics_service.js.es6 | 13 ++ .../cycle_analytics_store.js.es6 | 66 +++++- .../stylesheets/framework/variables.scss | 4 + .../stylesheets/pages/cycle_analytics.scss | 205 +++++++++++++++++- .../projects/cycle_analytics/show.html.haml | 192 ++++++++++++++-- app/views/shared/icons/_delta.svg | 3 + app/views/shared/icons/_down_arrow.svg | 3 + 22 files changed, 752 insertions(+), 154 deletions(-) create mode 100644 app/assets/javascripts/cycle_analytics/components/item_build_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/item_commit_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/item_merge_request_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_button.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 delete mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics.js.es6 create mode 100644 app/views/shared/icons/_delta.svg create mode 100644 app/views/shared/icons/_down_arrow.svg 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(() => { - 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'); + }, + }, }); }); 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 92226f7432e..750d99ebabe 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" } - .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 }} 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 @@ + + + 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 @@ + + + \ No newline at end of file From b0d2dffc0dcaec59973afc87432935b0df320aeb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Nov 2016 01:02:06 -0500 Subject: [PATCH 04/53] Move ItemIssueComponent template to component JS file --- .../components/item_issue_component.js.es6 | 37 ++++++++++++++++++- .../projects/cycle_analytics/show.html.haml | 24 ------------ 2 files changed, 36 insertions(+), 25 deletions(-) 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 index f4c3d92bd56..14542dbb958 100644 --- a/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 @@ -18,6 +18,41 @@ template: '#item-issue-component', props: { issue: Object, - } + }, + template: ` + +
+ + {{ issue.totalTime.hours }} + hr + + + {{ issue.totalTime.minutes }} + mins + +
+ `, }); })(window.gl || (window.gl = {})); diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 06a6e24ac49..aa72a801938 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -152,30 +152,6 @@ %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 From 1fc9b7aea867b9de2ccb5a8ca5fb8c546cc6c322 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Nov 2016 01:17:20 -0500 Subject: [PATCH 05/53] Move StageIssueComponent template to component in JS file --- .../components/stage_issue_component.js.es6 | 15 +++++++++++++-- app/views/projects/cycle_analytics/show.html.haml | 8 -------- 2 files changed, 13 insertions(+), 10 deletions(-) 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 index e4da9294b53..12d1d7d2324 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 @@ -3,13 +3,24 @@ global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics.StageIssueComponent = Vue.extend({ - template: '#stage-issue-component', components: { 'item-issue-component': gl.cycleAnalytics.ItemIssueComponent, }, props: { items: Array, - } + }, + template: ` +
+
+ Time before an issue get scheluded +
+
    +
  • + +
  • +
+
+ `, }); })(window.gl || (window.gl = {})); diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index aa72a801938..5536198eab8 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -94,14 +94,6 @@ %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 From fe72c3fea31f34ddfffbf9aea59da60e8509eb0d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Nov 2016 01:19:42 -0500 Subject: [PATCH 06/53] Move StagePlanComponent template to component in JS file --- .../components/stage_plan_component.js.es6 | 15 +++++++++++++-- app/views/projects/cycle_analytics/show.html.haml | 8 -------- 2 files changed, 13 insertions(+), 10 deletions(-) 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 index 2dcc0ee9699..5b83bec58b6 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 @@ -3,13 +3,24 @@ global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics.StagePlanComponent = Vue.extend({ - template: '#stage-plan-component', components: { 'item-commit-component': gl.cycleAnalytics.ItemCommitComponent, }, props: { items: Array, - } + }, + template: ` +
+
+ Time before an issue starts implementation +
+
    +
  • + +
  • +
+
+ `, }); })(window.gl || (window.gl = {})); diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 5536198eab8..bb4854d88c5 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -94,14 +94,6 @@ %template{ "v-if" => "state.items.length && !isLoadingStage && !isEmptyStage" } %component{ ":is" => "currentStage.component", ":items" => "state.items" } -%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 From f8c572a2f1ccc808cec63aee3a091930c81992fd Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Nov 2016 02:15:50 -0500 Subject: [PATCH 07/53] Apply styling to ItemCommitComponent --- .../components/item_commit_component.js.es6 | 31 +++++++++++++++++-- .../components/stage_plan_component.js.es6 | 4 +-- .../stylesheets/pages/cycle_analytics.scss | 18 +++++++++++ .../projects/cycle_analytics/show.html.haml | 15 --------- 4 files changed, 49 insertions(+), 19 deletions(-) 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 index 344cb77d7cc..5a8d62ce87d 100644 --- a/app/assets/javascripts/cycle_analytics/components/item_commit_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/item_commit_component.js.es6 @@ -14,9 +14,36 @@ */ global.cycleAnalytics.ItemCommitComponent = Vue.extend({ - template: '#item-commit-component', props: { commit: Object, - } + }, + template: ` +
+
+ {{ commit.title }} +
+ + First + + + + + + {{ commit.hash }} + pushed by + {{ commit.author }} + +
+
+ + {{ commit.totalTime.hours }} + hr + + + {{ commit.totalTime.minutes }} + mins + +
+ `, }); }(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 index 5b83bec58b6..394990d4bb3 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 @@ -14,8 +14,8 @@
Time before an issue starts implementation
-
    -
  • +
      +
    diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 09625d178c5..dec2595b50d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -327,5 +327,23 @@ color: $gl-text-color; } } + + .commit-author-link { + color: $gl-dark-link-color; + } + + // Custom CSS for components + .item-conmmit-component { + .commit-icon { + position: relative; + top: 3px; + left: 1px; + display: inline-block; + + svg { + float: left; + } + } + } } } diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index bb4854d88c5..ea3ae13b6d8 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -119,7 +119,6 @@ %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 @@ -136,20 +135,6 @@ %li{ "v-for" => "issue in items" } %item-issue-component{ ":issue" => "issue" } -%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 From d70a1283a59832072779ddf9ef94e96fd6ef2e00 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Nov 2016 02:25:11 -0500 Subject: [PATCH 08/53] Fix text styles on ItemIssueComponent --- .../components/item_issue_component.js.es6 | 28 +++++++++---------- .../stylesheets/pages/cycle_analytics.scss | 8 +++++- 2 files changed, 21 insertions(+), 15 deletions(-) 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 index 14542dbb958..b1caf9906fd 100644 --- a/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 @@ -27,21 +27,21 @@ {{ issue.title }} - - #{{issue.id}} - - - Opened - - {{ issue.datetime }} + + #{{issue.id}} - - - by - - {{ issue.author }} - - + + Opened + + {{ issue.datetime }} + + + + by + + {{ issue.author }} + +
    diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index dec2595b50d..9f267675af9 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -328,7 +328,13 @@ } } - .commit-author-link { + .issue-date { + color: $gl-text-color; + } + + .issue-link, + .commit-author-link, + .issue-author-link { color: $gl-dark-link-color; } From 76421f6682232675a92ca292197718f80f4cd1c1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 15 Nov 2016 14:10:31 -0500 Subject: [PATCH 09/53] Add missing hasError to data object for vue2 compatibility --- app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index f076644b037..8a28db90508 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -18,6 +18,7 @@ $(() => { isLoading: false, isLoadingStage: false, isEmptyStage: false, + hasError: false, startDate: 30, isEmptyDialogDismissed: Cookies.get(EMPTY_DIALOG_COOKIE), isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), From 795ce9dc4a980129495f4b874fcf9842daa2b699 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 15 Nov 2016 14:10:47 -0500 Subject: [PATCH 10/53] Wrap components with a single element for Vue 2 compatibility --- .../components/item_commit_component.js.es6 | 52 ++++++++-------- .../components/item_issue_component.js.es6 | 62 ++++++++++--------- 2 files changed, 59 insertions(+), 55 deletions(-) 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 index 5a8d62ce87d..6713b1fb970 100644 --- a/app/assets/javascripts/cycle_analytics/components/item_commit_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/item_commit_component.js.es6 @@ -18,32 +18,34 @@ commit: Object, }, template: ` -
    -
    - {{ commit.title }} -
    - - First - - - - +
    +
    +
    + {{ commit.title }} +
    + + First + + + + + + {{ commit.hash }} + pushed by + {{ commit.author }} - {{ commit.hash }} - pushed by - {{ commit.author }} - -
    -
    - - {{ commit.totalTime.hours }} - hr - - - {{ commit.totalTime.minutes }} - mins - -
    +
    +
    + + {{ commit.totalTime.hours }} + hr + + + {{ commit.totalTime.minutes }} + mins + +
    +
    `, }); }(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 index b1caf9906fd..bd59124104c 100644 --- a/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 @@ -20,38 +20,40 @@ issue: Object, }, template: ` - -
    - - {{ issue.totalTime.hours }} - hr - - - {{ issue.totalTime.minutes }} - mins - + +
    +
    + + {{ issue.totalTime.hours }} + hr + + + {{ issue.totalTime.minutes }} + mins + +
    `, }); From f9cfc87c7ddac2393cdb8a150412ec37f2e04c56 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 15 Nov 2016 15:19:41 -0500 Subject: [PATCH 11/53] Move templates to its corresponding component --- .../components/item_build_component.js.es6 | 14 ++++- .../item_merge_request_component.js.es6 | 14 ++++- .../components/stage_code_component.js.es6 | 15 ++++- .../stage_production_component.js.es6 | 15 ++++- .../components/stage_review_component.js.es6 | 15 ++++- .../components/stage_staging_component.js.es6 | 14 ++++- .../components/stage_test_component.js.es6 | 15 ++++- .../projects/cycle_analytics/show.html.haml | 57 ------------------- 8 files changed, 89 insertions(+), 70 deletions(-) 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 index d4c488dc3a8..5f8ff683860 100644 --- a/app/assets/javascripts/cycle_analytics/components/item_build_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/item_build_component.js.es6 @@ -16,9 +16,19 @@ */ global.cycleAnalytics.ItemBuildComponent = Vue.extend({ - template: '#item-build-component', props: { build: Object, - } + }, + template: ` + + `, }); }(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 index 488f6f901ff..0fd0767a3d0 100644 --- 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 @@ -15,9 +15,19 @@ */ global.cycleAnalytics.ItemMergeRequestComponent = Vue.extend({ - template: '#item-merge-request-component', props: { mergeRequest: Object, - } + }, + template: ` + + `, }); }(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 index bdc9617f463..45bd7c7b9e7 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 @@ -3,13 +3,24 @@ global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics.StageCodeComponent = Vue.extend({ - template: '#stage-code-component', components: { 'item-merge-request-component': gl.cycleAnalytics.ItemMergeRequestComponent, }, props: { items: Array, - } + }, + template: ` +
    +
    + Time spent coding +
    +
      +
    • + +
    • +
    +
    + `, }); })(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 index fea2e1edacb..0a6650d5c10 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 @@ -3,13 +3,24 @@ global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics.StageProductionComponent = Vue.extend({ - template: '#stage-production-component', components: { 'item-issue-component': gl.cycleAnalytics.ItemIssueComponent, }, props: { items: Array, - } + }, + template: ` +
    +
    + The total time taken from idea to production +
    +
      +
    • + +
    • +
    +
    + ` }); })(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 index 292f8ada3f4..b52ecbb21f3 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 @@ -3,13 +3,24 @@ global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics.StageReviewComponent = Vue.extend({ - template: '#stage-review-component', components: { 'item-merge-request-component': gl.cycleAnalytics.ItemMergeRequestComponent, }, props: { items: Array, - } + }, + template: ` +
    +
    + The time taken to review the code +
    +
      +
    • + +
    • +
    +
    + `, }); })(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 index 2a4cf97386a..c07f556ed84 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 @@ -9,7 +9,19 @@ }, props: { items: Array, - } + }, + template: ` +
    +
    + The time taken in staging +
    +
      +
    • + +
    • +
    +
    + `, }); })(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 index 7e16ae67f66..e3057c6a507 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 @@ -3,13 +3,24 @@ global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics.StageTestComponent = Vue.extend({ - template: '#stage-test-component', components: { 'item-build-component': gl.cycleAnalytics.ItemBuildComponent, }, props: { items: Array, - } + }, + template: ` +
    +
    + The time taken to build and test the application +
    +
      +
    • + +
    • +
    +
    + `, }); })(window.gl || (window.gl = {})); diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index ea3ae13b6d8..a4255aceb2a 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -93,60 +93,3 @@ %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-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-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 }} From 20efd43f1670ed9bca9912a2fa2ee70b12a24fdf Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 15 Nov 2016 17:15:30 -0500 Subject: [PATCH 12/53] Set stage descriptions dynamically --- .../cycle_analytics/components/stage_code_component.js.es6 | 3 ++- .../components/stage_issue_component.js.es6 | 3 ++- .../cycle_analytics/components/stage_plan_component.js.es6 | 3 ++- .../components/stage_production_component.js.es6 | 3 ++- .../cycle_analytics/components/stage_test_component.js.es6 | 3 ++- .../cycle_analytics/cycle_analytics_store.js.es6 | 7 +++++++ app/views/projects/cycle_analytics/show.html.haml | 2 +- 7 files changed, 18 insertions(+), 6 deletions(-) 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 index 45bd7c7b9e7..0a61ac797dd 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 @@ -8,11 +8,12 @@ }, props: { items: Array, + stage: Object, }, template: `
    - Time spent coding + {{ stage.shortDescription }}
    • 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 index 12d1d7d2324..16b392138a6 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 @@ -8,11 +8,12 @@ }, props: { items: Array, + stage: Object, }, template: `
      - Time before an issue get scheluded + {{ stage.shortDescription }}
      • 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 index 394990d4bb3..87b189adacb 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 @@ -8,11 +8,12 @@ }, props: { items: Array, + stage: Object, }, template: `
        - Time before an issue starts implementation + {{ stage.shortDescription }}
        • 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 index 0a6650d5c10..15477f957fd 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 @@ -8,11 +8,12 @@ }, props: { items: Array, + stage: Object, }, template: `
          - The total time taken from idea to production + {{ stage.shortDescription }}
          • 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 index e3057c6a507..b09fd8c8a6f 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 @@ -8,11 +8,12 @@ }, props: { items: Array, + stage: Object, }, template: `
            - The time taken to build and test the application + {{ stage.shortDescription }}
            • 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 7c8461b85ae..50e079c034a 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -13,42 +13,49 @@ active: false, component: 'stage-issue-component', legendTitle: 'Related Issues', + shortDescription: 'Time before an issue get scheduled', }, { name:'Plan', active: false, component: 'stage-plan-component', legendTitle: 'Related Commits', + shortDescription: 'Time before an issue starts implementation', }, { name:'Code', active: false, component: 'stage-code-component', legendTitle: 'Related Merge Requests', + shortDescription: 'Time spent coding', }, { name:'Test', active: false, component: 'stage-test-component', legendTitle: 'Relative Builds Trigger by Commits', + shortDescription: 'The time taken to build and test the application', }, { name:'Review', active: false, component: 'stage-review-component', legendTitle: 'Relative Merged Requests', + shortDescription: 'The time taken to review the code', }, { name:'Staging', active: false, component: 'stage-staging-component', legendTitle: 'Relative Deployed Builds', + shortDescription: 'The time taken in staging', }, { name:'Production', active: false, component: 'stage-production-component', legendTitle: 'Related Issues', + shortDescription: 'The total time taken from idea to production', } ], }, diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index a4255aceb2a..8da0fbba783 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -92,4 +92,4 @@ %template{ "v-if" => "isEmptyStage" } %p No results %template{ "v-if" => "state.items.length && !isLoadingStage && !isEmptyStage" } - %component{ ":is" => "currentStage.component", ":items" => "state.items" } + %component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.items" } From bbd1c788621e3e186f37c7d43d16915f68c3576a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 15 Nov 2016 18:38:48 -0500 Subject: [PATCH 13/53] Show friendly message when stage has no data --- .../stylesheets/pages/cycle_analytics.scss | 19 +++++++++++++ .../cycle_analytics/_empty_stage.html.haml | 12 +++++++++ .../projects/cycle_analytics/show.html.haml | 2 +- app/views/shared/icons/_icon_no_data.svg | 27 +++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 app/views/projects/cycle_analytics/_empty_stage.html.haml create mode 100644 app/views/shared/icons/_icon_no_data.svg diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 9f267675af9..04bca466eed 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -352,4 +352,23 @@ } } } + + .empty-stage { + text-align: center; + width: 75%; + margin: 0 auto; + padding-top: 130px; + color: $gl-text-color-light; + + .icon-no-data { + height: 36px; + width: 78px; + display: inline-block; + margin-bottom: 20px; + } + + h4 { + color: $gl-text-color; + } + } } diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml new file mode 100644 index 00000000000..f839ec9c018 --- /dev/null +++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml @@ -0,0 +1,12 @@ +.empty-stage-container + .empty-stage + .icon-no-data + = render "shared/icons/icon_no_data.svg" + %h4 We don’t have enough data to show this stage. + %p + The test phase measures the median time to run the entire pipeline for that project. + It’s related to the time GitLab CI takes to run every job for the commits pushed + to that merge request defined in the previous stage. + Learn more about the + %a{ :href => "https://docs.gitlab.com/ce/user/project/cycle_analytics.html" } + expected workflow and calculations. diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 8da0fbba783..195b10e0779 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -90,6 +90,6 @@ %template{ "v-if" => "isLoadingStage" } = icon("spinner spin", "v-show" => "isLoadingStage") %template{ "v-if" => "isEmptyStage" } - %p No results + = render partial: "empty_stage" %template{ "v-if" => "state.items.length && !isLoadingStage && !isEmptyStage" } %component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.items" } diff --git a/app/views/shared/icons/_icon_no_data.svg b/app/views/shared/icons/_icon_no_data.svg new file mode 100644 index 00000000000..ced8653b88c --- /dev/null +++ b/app/views/shared/icons/_icon_no_data.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + From ee6b991f0b16e55009288b72a46204c23b08654b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 16 Nov 2016 13:45:23 -0500 Subject: [PATCH 14/53] Remove delta column and use stage data from response --- .../cycle_analytics_service.js.es6 | 2 +- .../cycle_analytics_store.js.es6 | 60 ++----------------- .../stylesheets/pages/cycle_analytics.scss | 17 +----- .../projects/cycle_analytics/show.html.haml | 14 +---- app/views/shared/icons/_down_arrow.svg | 3 - 5 files changed, 12 insertions(+), 84 deletions(-) delete mode 100644 app/views/shared/icons/_down_arrow.svg 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 e5a30109ca6..788bd14028c 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 @@ -28,7 +28,7 @@ startDate, } = options; - return $.get(`http://localhost:8000/${stage.name.toLowerCase()}.json`, { + return $.get(`http://localhost:8000/${stage.title.toLowerCase()}.json`, { cycle_analytics: { start_date: options.startDate } 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 50e079c034a..bef741c879a 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -7,57 +7,7 @@ stats: '', analytics: '', items: [], - stages:[ - { - name:'Issue', - active: false, - component: 'stage-issue-component', - legendTitle: 'Related Issues', - shortDescription: 'Time before an issue get scheduled', - }, - { - name:'Plan', - active: false, - component: 'stage-plan-component', - legendTitle: 'Related Commits', - shortDescription: 'Time before an issue starts implementation', - }, - { - name:'Code', - active: false, - component: 'stage-code-component', - legendTitle: 'Related Merge Requests', - shortDescription: 'Time spent coding', - }, - { - name:'Test', - active: false, - component: 'stage-test-component', - legendTitle: 'Relative Builds Trigger by Commits', - shortDescription: 'The time taken to build and test the application', - }, - { - name:'Review', - active: false, - component: 'stage-review-component', - legendTitle: 'Relative Merged Requests', - shortDescription: 'The time taken to review the code', - }, - { - name:'Staging', - active: false, - component: 'stage-staging-component', - legendTitle: 'Relative Deployed Builds', - shortDescription: 'The time taken in staging', - }, - { - name:'Production', - active: false, - component: 'stage-production-component', - legendTitle: 'Related Issues', - shortDescription: 'The total time taken from idea to production', - } - ], + stages:[], }, setCycleAnalyticsData(data) { this.state = Object.assign(this.state, this.decorateData(data)); @@ -65,18 +15,20 @@ decorateData(data) { let newData = {}; - newData.stats = data.stats || []; + newData.stages = data.stats || []; newData.summary = data.summary || []; newData.summary.forEach((item) => { item.value = item.value || '-'; }); - newData.stats.forEach((item) => { + newData.stages.forEach((item) => { item.value = item.value || '- - -'; + item.active = false; + item.component = `stage-${item.title.toLowerCase()}-component`; }); - newData.analytics = data; + newData.analytics = data; return newData; }, diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 04bca466eed..a862deb4251 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -22,7 +22,7 @@ } .stage-header { - width: 16%; + width: 28%; padding-left: $gl-padding; } @@ -30,10 +30,6 @@ width: 12%; } - .delta-header { - width: 12%; - } - .event-header { width: 45%; padding-left: $gl-padding; @@ -239,21 +235,12 @@ float: left; &.stage-name { - width: 40%; + width: 70%; } &.stage-median { width: 30%; } - - &.stage-delta { - width: 30%; - - .stage-direction { - float: right; - padding-right: $gl-padding; - } - } } .stage-name { diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 195b10e0779..1984499b8d5 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -31,7 +31,7 @@ .content-block .container-fluid .row - .col-sm-3.col-xs-12.column{"v-for" => "item in state.analytics.summary"} + .col-sm-3.col-xs-12.column{"v-for" => "item in state.summary"} %h3.header {{item.value}} %p.text {{item.title}} .col-sm-3.col-xs-12.column @@ -58,10 +58,6 @@ %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' }} @@ -79,13 +75,9 @@ ":on-stage-click" => "selectStage" } %li.stage-nav-item{ ":class" => "classObject", "@click" => "onClick(stage)" } .stage-name - {{stage.name}} + {{ stage.title }} .stage-median - 20 hrs 21 mins - .stage-delta - + 20 days - %span.stage-direction - = render "shared/icons/down_arrow.svg" + {{ stage.value }} .section.stage-events %template{ "v-if" => "isLoadingStage" } = icon("spinner spin", "v-show" => "isLoadingStage") diff --git a/app/views/shared/icons/_down_arrow.svg b/app/views/shared/icons/_down_arrow.svg deleted file mode 100644 index 123116f4ca9..00000000000 --- a/app/views/shared/icons/_down_arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file From 06fd2879ee4f5936e0be9744cbd208079f2542bd Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 16 Nov 2016 13:57:32 -0500 Subject: [PATCH 15/53] Provide stage legend on server response --- .../projects/cycle_analytics_controller.rb | 17 +++++++++-------- .../projects/cycle_analytics/show.html.haml | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 4caec91efe7..2fb496053a5 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -29,15 +29,15 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController def generate_cycle_analytics_data stats_values = [] - cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"], - [:plan, "Plan", "Time before an issue starts implementation"], - [:code, "Code", "Time until first merge request"], - [:test, "Test", "Total test time for all commits/merges"], - [:review, "Review", "Time between merge request creation and merge/close"], - [:staging, "Staging", "From merge request merge until deploy to production"], - [:production, "Production", "From issue creation until deploy to production"]] + cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"], + [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"], + [:code, "Code", "Related Merge Requests", "Time until first merge request"], + [:test, "Test", "Relative Builds Trigger by Commits", "Total test time for all commits/merges"], + [:review, "Review", "Relative Merged Requests", "Time between merge request creation and merge/close"], + [:staging, "Staging", "Relative Deployed Builds", "From merge request merge until deploy to production"], + [:production, "Production", "Related Issues", "From issue creation until deploy to production"]] - stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)| + stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)| value = @cycle_analytics.send(stage_method).presence stats_values << value.abs if value @@ -45,6 +45,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController stats << { title: stage_text, description: stage_description, + legend: stage_legend, value: value && !value.zero? ? distance_of_time_in_words(value) : nil } diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 1984499b8d5..cf496b46e4f 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -60,7 +60,7 @@ %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.event-header %span.stage-name - {{ currentStage ? currentStage.legendTitle : 'Related Issues' }} + {{ currentStage ? currentStage.legend : 'Related Issues' }} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The collection of events added to the data gathered for that stage.", "aria-hidden" => "true" } %li.total-time-header %span.stage-name From fe5ae3b0af46cfd5f370fb9d111c8b0f2cd1e0d4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 16 Nov 2016 16:07:47 -0500 Subject: [PATCH 16/53] Adde TotalTimeComponent as global component --- .../components/item_issue_component.js.es6 | 9 +----- .../components/total_time_component.js.es6 | 29 +++++++++++++++++++ .../cycle_analytics/cycle_analytics_bundle.js | 3 ++ 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 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 index bd59124104c..57e20b2aa4e 100644 --- a/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 @@ -45,14 +45,7 @@
            - - {{ issue.totalTime.hours }} - hr - - - {{ issue.totalTime.minutes }} - mins - +
          `, diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 new file mode 100644 index 00000000000..9d87400dfd8 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 @@ -0,0 +1,29 @@ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.TotalTimeComponent = Vue.extend({ + props: { + time: Object, + }, + template: ` + + + {{ time.days }} + {{ time.days === 1 ? 'day' : 'days' }} + + + {{ time.hours }} + hr + + + {{ time.mins }} + mins + + + {{ time.seconds }} + s + + + `, + }); +})(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 8a28db90508..05038c3c500 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -115,4 +115,7 @@ $(() => { }, }, }); + + // Register global components + Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); }); From 2748a01a3a0dc6c6e946c41c68ec866bc87e8f8b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 16 Nov 2016 16:11:27 -0500 Subject: [PATCH 17/53] Use stage description form endpoint response --- .../components/item_issue_component.js.es6 | 5 ++--- .../components/stage_code_component.js.es6 | 2 +- .../components/stage_issue_component.js.es6 | 2 +- .../components/stage_plan_component.js.es6 | 2 +- .../stage_production_component.js.es6 | 2 +- .../components/stage_review_component.js.es6 | 3 ++- .../components/stage_staging_component.js.es6 | 3 ++- .../components/stage_test_component.js.es6 | 2 +- .../cycle_analytics/cycle_analytics_bundle.js | 6 +++--- .../cycle_analytics_store.js.es6 | 17 ++++++++++++++--- .../projects/cycle_analytics_controller.rb | 10 +++++----- .../projects/cycle_analytics/show.html.haml | 4 ++-- 12 files changed, 35 insertions(+), 23 deletions(-) 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 index 57e20b2aa4e..e73b2db1ba8 100644 --- a/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/item_issue_component.js.es6 @@ -15,7 +15,6 @@ */ global.cycleAnalytics.ItemIssueComponent = Vue.extend({ - template: '#item-issue-component', props: { issue: Object, }, @@ -39,8 +38,8 @@ by - - {{ issue.author }} + + {{ issue.author.name }}
        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 index 0a61ac797dd..b070d22af8a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 @@ -13,7 +13,7 @@ template: `
        - {{ stage.shortDescription }} + {{ stage.description }}