diff options
author | Jacob Schatz <jschatz@gitlab.com> | 2016-11-22 00:16:13 +0000 |
---|---|---|
committer | Jacob Schatz <jschatz@gitlab.com> | 2016-11-22 00:16:13 +0000 |
commit | d17f5068118a0c86fcd39b3576161b2408596e2d (patch) | |
tree | b5123636c1cc941997d5e6050b9788141bd67046 /app/assets | |
parent | 843ae9b26e6d307c6b2e140e904b77565a649674 (diff) | |
parent | c0b6370e3e8f395acc6ca44c89834cd2eec620d8 (diff) | |
download | gitlab-ce-d17f5068118a0c86fcd39b3576161b2408596e2d.tar.gz |
Merge branch '23449-cycle-analytics-2-frontend' into 'master'
Cycle analytics second iteration frontend
## Are there points in the code the reviewer needs to double check?
Mostly typos and code guidelines.
## Why was this MR needed?
This implements the frontend part of !6859 for #23449
## Screenshots
**Initial view**
![Screen_Shot_2016-11-21_at_5.28.43_PM](/uploads/ff9cfa9c9d6c2da28c24e03e384f89af/Screen_Shot_2016-11-21_at_5.28.43_PM.png)
**Cycle Analytics with data**
![Screen_Shot_2016-11-21_at_2.09.24_PM](/uploads/18d9786c090bdd554cf786c879543302/Screen_Shot_2016-11-21_at_2.09.24_PM.png)
**User doesn't have access for a stage**
![Screen_Shot_2016-11-21_at_2.11.01_PM](/uploads/d1ea76a63f2de2224954b5f40038c488/Screen_Shot_2016-11-21_at_2.11.01_PM.png)
## Does this MR meet the acceptance criteria?
- [x] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added
- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
- [ ] API support added
- Tests
- [ ] Added for this feature/bug
- [ ] All builds are passing
- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [ ] Branch has no merge conflicts with `master` (if it does - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
## What are the relevant issue numbers?
Closes #23449
See merge request !7366
Diffstat (limited to 'app/assets')
17 files changed, 952 insertions, 106 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 new file mode 100644 index 00000000000..520cee7738b --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 @@ -0,0 +1,43 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageCodeComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="mergeRequest in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <h5 class="item-title merge-merquest-title"> + <a :href="mergeRequest.url"> + {{ mergeRequest.title }} + </a> + </h5> + <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + · + <span> + Opened + <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + </span> + <span> + by + <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + </span> + </div> + <div class="item-time"> + <total-time :time="mergeRequest.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(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..3bb01c67206 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 @@ -0,0 +1,45 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageIssueComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="issue in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="issue.author.avatarUrl"> + <h5 class="item-title issue-title"> + <a class="issue-title" :href="issue.url"> + {{ issue.title }} + </a> + </h5> + <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + · + <span> + Opened + <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + </span> + <span> + by + <a :href="issue.author.webUrl" class="issue-author-link"> + {{ issue.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="issue.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(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..b568ab62a69 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 @@ -0,0 +1,42 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StagePlanComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="commit in items" class="stage-event-item"> + <div class="item-details item-conmmit-component"> + <img class="avatar" :src="commit.author.avatarUrl"> + <h5 class="item-title commit-title"> + <a :href="commit.commitUrl"> + {{ commit.title }} + </a> + </h5> + <span> + First + <span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span> + <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> + pushed by + <a :href="commit.author.webUrl" class="commit-author-link"> + {{ commit.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="commit.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(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..a6b6d817a82 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 @@ -0,0 +1,45 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageProductionComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="issue in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="issue.author.avatarUrl"> + <h5 class="item-title issue-title"> + <a class="issue-title" :href="issue.url"> + {{ issue.title }} + </a> + </h5> + <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + · + <span> + Opened + <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + </span> + <span> + by + <a :href="issue.author.webUrl" class="issue-author-link"> + {{ issue.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="issue.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(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..9e819c1d420 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 @@ -0,0 +1,55 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageReviewComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="mergeRequest in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <h5 class="item-title merge-merquest-title"> + <a :href="mergeRequest.url"> + {{ mergeRequest.title }} + </a> + </h5> + <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + · + <span> + Opened + <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + </span> + <span> + by + <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + </span> + <template v-if="mergeRequest.state === 'closed'"> + <span class="merge-request-state"> + <i class="fa fa-ban"></i> + {{ mergeRequest.state.toUpperCase() }} + </span> + </template> + <template v-else> + <span class="merge-request-branch" v-if="mergeRequest.branch"> + <i class= "fa fa-code-fork"></i> + <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> + </span> + </template> + </div> + <div class="item-time"> + <total-time :time="mergeRequest.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(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..b30c3a31010 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 @@ -0,0 +1,42 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageStagingComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="build in items" class="stage-event-item item-build-component"> + <div class="item-details"> + <img class="avatar" :src="build.author.avatarUrl"> + <h5 class="item-title"> + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> + <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span> + <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="build-date">{{ build.date }}</a> + by + <a :href="build.author.webUrl" class="issue-author-link"> + {{ build.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(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..c54d6b6ee37 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 @@ -0,0 +1,42 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageTestComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="build in items" class="stage-event-item item-build-component"> + <div class="item-details"> + <h5 class="item-title"> + <span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span> + <a :href="build.url" class="item-build-name">{{ build.name }}</a> + · + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> + <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span> + <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="issue-date"> + {{ build.date }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); 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..8403fbeaab5 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 @@ -0,0 +1,18 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.TotalTimeComponent = Vue.extend({ + props: { + time: Object, + }, + template: ` + <span class="total-time"> + <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template> + <template v-if="time.hours">{{ time.hours }} <span>hr</span></template> + <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template> + <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template> + </span> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index 331f0209888..f1ddd139c48 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -1,98 +1,121 @@ -/* 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(); - } +//= require_tree ./svg +//= require_tree . + +$(() => { + const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; + 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: { + state: cycleAnalyticsStore.state, + isLoading: false, + isLoadingStage: false, + isEmptyStage: false, + hasError: false, + startDate: 30, + isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), + }, + computed: { + currentStage() { + return cycleAnalyticsStore.currentActiveStage(); + }, + }, + components: { + '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() { + cycleAnalyticsStore.setErrorState(true); + return 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); + this.startDate = $target.data('value'); + + $label.text($target.text().trim()); + this.fetchCycleAnalyticsData({ startDate: this.startDate }); + }); + }, + fetchCycleAnalyticsData(options) { + const fetchOptions = options || { startDate: this.startDate }; + + this.isLoading = true; + + cycleAnalyticsService + .fetchCycleAnalyticsData(fetchOptions) + .done((response) => { + cycleAnalyticsStore.setCycleAnalyticsData(response); + this.selectDefaultStage(); + this.initDropdown(); + }) + .error(() => { + this.handleError(); + }) + .always(() => { + this.isLoading = false; + }); + }, + selectDefaultStage() { + const stage = this.state.stages.first(); + this.selectStage(stage); + }, + selectStage(stage) { + if (this.isLoadingStage) return; + if (this.currentStage === stage) return; + + if (!stage.isUserAllowed) { + cycleAnalyticsStore.setActiveStage(stage); + return; } - }); - } - - 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 = {})); + this.isLoadingStage = true; + cycleAnalyticsStore.setStageEvents([]); + cycleAnalyticsStore.setActiveStage(stage); + + cycleAnalyticsService + .fetchStageData({ + stage, + startDate: this.startDate, + }) + .done((response) => { + this.isEmptyStage = !response.events.length; + cycleAnalyticsStore.setStageEvents(response.events); + }) + .error(() => { + this.isEmptyStage = true; + }) + .always(() => { + this.isLoadingStage = false; + }); + }, + dismissOverviewDialog() { + this.isOverviewDialogDismissed = true; + Cookies.set(OVERVIEW_DIALOG_COOKIE, '1'); + }, + }, + }); + + // Register global components + Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); +}); 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..9f74b14c4b9 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 @@ -0,0 +1,41 @@ +/* eslint-disable no-param-reassign */ +((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, + }, + }, + }); + } + + fetchStageData(options) { + const { + stage, + startDate, + } = options; + + return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, { + cycle_analytics: { + start_date: 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..9b905874167 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -0,0 +1,90 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + const EMPTY_STAGE_TEXTS = { + issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', + plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', + code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', + test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', + review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', + staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', + production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', + }; + + global.cycleAnalytics.CycleAnalyticsStore = { + state: { + summary: '', + stats: '', + analytics: '', + events: [], + stages: [], + }, + setCycleAnalyticsData(data) { + this.state = Object.assign(this.state, this.decorateData(data)); + }, + decorateData(data) { + const newData = {}; + + newData.stages = data.stats || []; + newData.summary = data.summary || []; + + newData.summary.forEach((item) => { + item.value = item.value || '-'; + }); + + newData.stages.forEach((item) => { + const stageName = item.title.toLowerCase(); + item.active = false; + item.isUserAllowed = data.permissions[stageName]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageName]; + item.component = `stage-${stageName}-component`; + }); + newData.analytics = data; + return newData; + }, + setLoadingState(state) { + this.state.isLoading = state; + }, + setErrorState(state) { + this.state.hasError = state; + }, + deactivateAllStages() { + this.state.stages.forEach((stage) => { + stage.active = false; + }); + }, + setActiveStage(stage) { + this.deactivateAllStages(); + stage.active = true; + }, + setStageEvents(events) { + this.state.events = this.decorateEvents(events); + }, + decorateEvents(events) { + const newEvents = events; + + newEvents.forEach((item) => { + item.totalTime = item.total_time; + item.author.webUrl = item.author.web_url; + item.author.avatarUrl = item.author.avatar_url; + + if (item.created_at) item.createdAt = item.created_at; + if (item.short_sha) item.shortSha = item.short_sha; + if (item.commit_url) item.commitUrl = item.commit_url; + + delete item.author.web_url; + delete item.author.avatar_url; + delete item.total_time; + delete item.created_at; + delete item.short_sha; + delete item.commit_url; + }); + + return newEvents; + }, + currentActiveStage() { + return this.state.stages.find(stage => stage.active); + }, + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 new file mode 100644 index 00000000000..5d486bcaf66 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 @@ -0,0 +1,7 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; + + global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>'; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 new file mode 100644 index 00000000000..661bf9e9f1c --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 @@ -0,0 +1,7 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; + + global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>'; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 new file mode 100644 index 00000000000..2208c27a619 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 @@ -0,0 +1,7 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; + + global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>'; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index ab4858dca32..c2d4670b7e9 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': 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..498a8f68e49 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -1,10 +1,53 @@ #cycle-analytics { + max-width: 1000px; 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: 28%; + padding-left: $gl-padding; + } + .median-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 +78,20 @@ } &:last-child { - text-align: right; - @media (max-width: $screen-sm-min) { text-align: center; } } } + } - .dropdown { - top: 13px; - } + .js-ca-dropdown { + top: $gl-padding-top; } .bordered-box { border: 1px solid $border-color; border-radius: $border-radius-default; - } .content-list { @@ -141,4 +181,302 @@ 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: 1px solid transparent; + border-bottom: 1px solid transparent; + border-right: 1px solid $border-color; + background-color: $gray-light; + cursor: default; + + &.active { + background-color: transparent; + border-right-color: transparent; + border-top-color: $border-color; + border-bottom-color: $border-color; + box-shadow: inset 2px 0 0 0 $active-item-blue; + + .stage-name { + font-weight: 600; + } + } + + &:hover:not(.active) { + background-color: $gray-lightest; + box-shadow: inset 2px 0 0 0 $border-color; + } + + &:first-child { + border-top: none; + } + + &:last-child { + border-bottom: none; + } + + .stage-nav-item-cell { + float: left; + + &.stage-name { + width: 70%; + } + + &.stage-median { + width: 30%; + } + } + + .stage-name { + padding-left: 16px; + } + + .stage-empty, + .not-available { + color: $gl-text-color-light; + } + } + } + + .stage-panel-container { + width: 100%; + overflow: auto; + } + + .stage-panel { + min-width: 968px; + + .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; + border-bottom: 1px solid $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; + + &.issue-title, + &.commit-title, + &.merge-merquest-title { + max-width: 100%; + display: block; + @include text-overflow(); + + a { + color: $gl-dark-link-color; + } + } + } + + .item-time { + width: 25%; + text-align: right; + } + + .total-time { + font-size: $cycle-analytics-big-font; + color: $cycle-analytics-dark-text; + + span { + color: $gl-text-color; + font-size: $gl-font-size; + } + } + + .issue-date, + .build-date { + color: $gl-text-color; + } + + .issue-link, + .commit-author-link, + .issue-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; + } + } + } + + .merge-request-branch { + a { + max-width: 180px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: inline-block; + vertical-align: bottom; + } + } + } + + // Custom Styles for stage items + .item-build-component { + + .item-title { + .icon-build-status { + float: left; + margin-right: 5px; + position: relative; + top: 2px; + } + + .item-build-name { + color: $gl-title-color; + } + + .pipeline-id { + color: $gl-title-color; + padding: 0 3px 0 0; + } + + .branch-name { + color: $black; + display: inline-block; + max-width: 180px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + line-height: 1.3; + vertical-align: top; + } + + .short-sha { + color: $gl-link-color; + line-height: 1.3; + vertical-align: top; + font-weight: normal; + } + + .fa { + color: $gl-text-color-light; + font-size: $code_font_size; + } + } + } + + .empty-stage, + .no-access-stage { + text-align: center; + width: 75%; + margin: 0 auto; + padding-top: 130px; + color: $gl-text-color-light; + + h4 { + color: $gl-text-color; + } + } + + .empty-stage { + .icon-no-data { + height: 36px; + width: 78px; + display: inline-block; + margin-bottom: 20px; + } + } + + .no-access-stage { + .icon-lock { + height: 36px; + width: 78px; + display: inline-block; + margin-bottom: 20px; + } + } +} + +.cycle-analytics-overview { + padding-top: 100px; + + .overview-details { + display: flex; + align-items: center; + } + + .overview-image { + text-align: right; + } + + .overview-icon { + svg { + width: 365px; + height: 227px; + } + } } |