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