diff options
author | Phil Hughes <me@iamphill.com> | 2019-06-24 12:51:40 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2019-06-24 12:52:00 +0100 |
commit | 039117deacf730286d6e1b0b6bc116d62c3b9c31 (patch) | |
tree | 98a86c75bcfe12161e4817100cb568eaf51b6cbd | |
parent | 88c8d177f835983a0a47796529906c69376d159d (diff) | |
download | gitlab-ce-039117deacf730286d6e1b0b6bc116d62c3b9c31.tar.gz |
Render branch divergance graph with Vue
10 files changed, 366 insertions, 54 deletions
diff --git a/app/assets/javascripts/branches/components/divergence_graph.vue b/app/assets/javascripts/branches/components/divergence_graph.vue new file mode 100644 index 00000000000..36fff370ea1 --- /dev/null +++ b/app/assets/javascripts/branches/components/divergence_graph.vue @@ -0,0 +1,72 @@ +<script> +import { sprintf, __ } from '~/locale'; +import GraphBar from './graph_bar.vue'; +import { MAX_COMMIT_COUNT } from '../constants'; + +export default { + components: { + GraphBar, + }, + props: { + defaultBranch: { + type: String, + required: true, + }, + distance: { + type: Number, + required: false, + default: null, + }, + aheadCount: { + type: Number, + required: true, + }, + behindCount: { + type: Number, + required: true, + }, + maxCommits: { + type: Number, + required: true, + }, + }, + computed: { + title() { + if (this.distance) { + return sprintf( + __('More than %{number_commits_distance} commits different with %{default_branch}'), + { + number_commits_distance: + this.distance >= MAX_COMMIT_COUNT ? `${MAX_COMMIT_COUNT - 1}+` : this.distance, + default_branch: this.defaultBranch, + }, + ); + } + + return sprintf( + __( + '%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead', + ), + { + number_commits_behind: this.behindCount, + number_commits_ahead: this.aheadCount, + default_branch: this.defaultBranch, + }, + ); + }, + }, +}; +</script> + +<template> + <div :title="title" class="divergence-graph px-2 d-none d-md-block"> + <template v-if="distance"> + <graph-bar :count="distance" :max-commits="maxCommits" position="full" /> + </template> + <template v-else> + <graph-bar :count="behindCount" :max-commits="maxCommits" position="left" /> + <div class="graph-separator pull-left mt-1"></div> + <graph-bar :count="aheadCount" :max-commits="maxCommits" position="right" /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/branches/components/graph_bar.vue b/app/assets/javascripts/branches/components/graph_bar.vue new file mode 100644 index 00000000000..83da41ca097 --- /dev/null +++ b/app/assets/javascripts/branches/components/graph_bar.vue @@ -0,0 +1,69 @@ +<script> +import { SIDES, MAX_COMMIT_COUNT } from '../constants'; + +export default { + props: { + position: { + type: String, + required: true, + }, + count: { + type: Number, + required: true, + }, + maxCommits: { + type: Number, + required: true, + }, + }, + computed: { + label() { + if (this.count >= MAX_COMMIT_COUNT) { + return `${MAX_COMMIT_COUNT - 1}+`; + } + + return this.count; + }, + barGraphWidthFactor() { + return this.maxCommits > 0 ? 100 / this.maxCommits : 0; + }, + style() { + return { + width: `${this.count * this.barGraphWidthFactor}%`, + }; + }, + isFullWidth() { + return this.position === SIDES.full; + }, + isLeftSide() { + return this.position === SIDES.left; + }, + roundedClass() { + if (this.isFullWidth) return 'rounded'; + + return `rounded-${this.position}`; + }, + textAlignmentClass() { + if (this.isFullWidth) return 'text-center'; + + return `text-${this.isLeftSide ? SIDES.right : SIDES.left}`; + }, + positionSideClass() { + return `position-${this.isLeftSide ? SIDES.right : SIDES.left}-0`; + }, + }, +}; +</script> + +<template> + <div :class="{ full: isFullWidth }" class="position-relative pull-left pt-1 graph-side h-100"> + <div + :style="style" + :class="[roundedClass, positionSideClass]" + class="position-absolute bar js-graph-bar" + ></div> + <span :class="textAlignmentClass" class="d-block pt-1 pr-1 count js-graph-count"> + {{ label }} + </span> + </div> +</template> diff --git a/app/assets/javascripts/branches/constants.js b/app/assets/javascripts/branches/constants.js new file mode 100644 index 00000000000..16949e662d2 --- /dev/null +++ b/app/assets/javascripts/branches/constants.js @@ -0,0 +1,6 @@ +export const SIDES = { + full: 'full', + left: 'left', + right: 'right', +}; +export const MAX_COMMIT_COUNT = 1000; diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js new file mode 100644 index 00000000000..670e8e9eb60 --- /dev/null +++ b/app/assets/javascripts/branches/divergence_graph.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import DivergenceGraph from './components/divergence_graph.vue'; + +export default () => { + document.querySelectorAll('.js-branch-divergence-graph').forEach(el => { + const { distance, aheadCount, behindCount, defaultBranch, maxCommits } = el.dataset; + + return new Vue({ + el, + render(h) { + return h(DivergenceGraph, { + props: { + defaultBranch, + distance: distance ? parseInt(distance, 10) : null, + aheadCount: parseInt(aheadCount, 10), + behindCount: parseInt(behindCount, 10), + maxCommits: parseInt(maxCommits, 10), + }, + }); + }, + }); + }); +}; diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index 8fa266a37ce..29de3b7806c 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -1,7 +1,9 @@ import AjaxLoadingSpinner from '~/ajax_loading_spinner'; import DeleteModal from '~/branches/branches_delete_modal'; +import initDiverganceGraph from '~/branches/divergence_graph'; document.addEventListener('DOMContentLoaded', () => { AjaxLoadingSpinner.init(); new DeleteModal(); // eslint-disable-line no-new + initDiverganceGraph(); }); diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss index ce0622b3d48..e1715b8e1bf 100644 --- a/app/assets/stylesheets/pages/branches.scss +++ b/app/assets/stylesheets/pages/branches.scss @@ -14,62 +14,26 @@ $graph-side-width: 80px; $graph-separator-width: 1px; - padding: 0 6px; - .graph-side { - position: relative; width: $graph-side-width; - height: 22px; - padding: 5px 0 13px; - float: left; &.full { width: $graph-side-width * 2 + $graph-separator-width; - display: flex; - justify-content: center; } .bar { - position: absolute; height: 4px; background-color: $gl-gray-200; } - .bar-behind { - right: 0; - border-radius: 3px 0 0 3px; - } - - .bar-ahead { - left: 0; - border-radius: 0 3px 3px 0; - } - .count { - padding-top: 6px; - padding-bottom: 0; font-size: 12px; - color: $gl-text-color; - display: block; - } - - .count-behind { - padding-right: 4px; - text-align: right; - } - - .count-ahead { - padding-left: 4px; - text-align: left; } } .graph-separator { - position: relative; width: $graph-separator-width; height: 18px; - margin: 5px 0 0; - float: left; background-color: $gl-gray-200; } } diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index a5eaae2dff4..dbc14245b4b 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,6 +1,5 @@ - merged = local_assigns.fetch(:merged, false) - commit = @repository.commit(branch.dereferenced_target) -- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 - diverging_commit_counts = @repository.diverging_commit_counts(branch) - number_commits_distance = diverging_commit_counts[:distance] - number_commits_behind = diverging_commit_counts[:behind] @@ -31,23 +30,7 @@ = s_('Branches|Cant find HEAD commit for this branch') - if branch.name != @repository.root_ref - - if number_commits_distance.nil? - .divergence-graph.d-none.d-md-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), - default_branch: @repository.root_ref, - number_commits_ahead: diverging_count_label(number_commits_ahead) } } - .graph-side - .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } - %span.count.count-behind= diverging_count_label(number_commits_behind) - .graph-separator - .graph-side - .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } - %span.count.count-ahead= diverging_count_label(number_commits_ahead) - - else - .divergence-graph.d-none.d-md-block{ title: s_('More than %{number_commits_distance} commits different with %{default_branch}') % { number_commits_distance: diverging_count_label(number_commits_distance), - default_branch: @repository.root_ref} } - .graph-side.full - .bar{ style: "width: #{number_commits_distance * bar_graph_width_factor}%" } - %span.count= diverging_count_label(number_commits_distance) + .js-branch-divergence-graph{ data: { distance: number_commits_distance, ahead_count: number_commits_ahead, behind_count: number_commits_behind, default_branch: @repository.root_ref, max_commits: @max_commits } } .controls.d-none.d-md-block< - if merge_project && create_mr_button?(@repository.root_ref, branch.name) diff --git a/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap b/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap new file mode 100644 index 00000000000..511c027dbc2 --- /dev/null +++ b/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Branch divergence graph component renders ahead and behind count 1`] = ` +<div + class="divergence-graph px-2 d-none d-md-block" + title="10 commits behind master, 10 commits ahead" +> + <graphbar-stub + count="10" + maxcommits="100" + position="left" + /> + + <div + class="graph-separator pull-left mt-1" + /> + + <graphbar-stub + count="10" + maxcommits="100" + position="right" + /> +</div> +`; + +exports[`Branch divergence graph component renders distance count 1`] = ` +<div + class="divergence-graph px-2 d-none d-md-block" + title="More than 900 commits different with master" +> + <graphbar-stub + count="900" + maxcommits="100" + position="full" + /> +</div> +`; diff --git a/spec/frontend/branches/components/divergence_graph_spec.js b/spec/frontend/branches/components/divergence_graph_spec.js new file mode 100644 index 00000000000..b54b2ceb233 --- /dev/null +++ b/spec/frontend/branches/components/divergence_graph_spec.js @@ -0,0 +1,67 @@ +import { shallowMount } from '@vue/test-utils'; +import DivergenceGraph from '~/branches/components/divergence_graph.vue'; +import GraphBar from '~/branches/components/graph_bar.vue'; + +let vm; + +function factory(propsData = {}) { + vm = shallowMount(DivergenceGraph, { propsData }); +} + +describe('Branch divergence graph component', () => { + afterEach(() => { + vm.destroy(); + }); + + it('renders ahead and behind count', () => { + factory({ + defaultBranch: 'master', + aheadCount: 10, + behindCount: 10, + maxCommits: 100, + }); + + expect(vm.findAll(GraphBar).length).toBe(2); + expect(vm.element).toMatchSnapshot(); + }); + + it('sets title for ahead and behind count', () => { + factory({ + defaultBranch: 'master', + aheadCount: 10, + behindCount: 10, + maxCommits: 100, + }); + + expect(vm.attributes('title')).toBe('10 commits behind master, 10 commits ahead'); + }); + + it('renders distance count', () => { + factory({ + defaultBranch: 'master', + aheadCount: 0, + behindCount: 0, + distance: 900, + maxCommits: 100, + }); + + expect(vm.findAll(GraphBar).length).toBe(1); + expect(vm.element).toMatchSnapshot(); + }); + + it.each` + distance | titleText + ${900} | ${'900'} + ${1100} | ${'999+'} + `('sets title for $distance as $titleText', ({ distance, titleText }) => { + factory({ + defaultBranch: 'master', + aheadCount: 0, + behindCount: 0, + distance, + maxCommits: 100, + }); + + expect(vm.attributes('title')).toBe(`More than ${titleText} commits different with master`); + }); +}); diff --git a/spec/frontend/branches/components/graph_bar_spec.js b/spec/frontend/branches/components/graph_bar_spec.js new file mode 100644 index 00000000000..61c051b49c6 --- /dev/null +++ b/spec/frontend/branches/components/graph_bar_spec.js @@ -0,0 +1,89 @@ +import { shallowMount } from '@vue/test-utils'; +import GraphBar from '~/branches/components/graph_bar.vue'; + +let vm; + +function factory(propsData = {}) { + vm = shallowMount(GraphBar, { propsData }); +} + +describe('Branch divergence graph bar component', () => { + afterEach(() => { + vm.destroy(); + }); + + it.each` + position | positionClass + ${'left'} | ${'position-right-0'} + ${'right'} | ${'position-left-0'} + ${'full'} | ${'position-left-0'} + `( + 'sets position class as $positionClass for position $position', + ({ position, positionClass }) => { + factory({ + position, + count: 10, + maxCommits: 100, + }); + + expect(vm.find('.js-graph-bar').classes()).toContain(positionClass); + }, + ); + + it.each` + position | textAlignmentClass + ${'left'} | ${'text-right'} + ${'right'} | ${'text-left'} + ${'full'} | ${'text-center'} + `( + 'sets text alignment class as $textAlignmentClass for position $position', + ({ position, textAlignmentClass }) => { + factory({ + position, + count: 10, + maxCommits: 100, + }); + + expect(vm.find('.js-graph-count').classes()).toContain(textAlignmentClass); + }, + ); + + it.each` + position | roundedClass + ${'left'} | ${'rounded-left'} + ${'right'} | ${'rounded-right'} + ${'full'} | ${'rounded'} + `('sets rounded class as $roundedClass for position $position', ({ position, roundedClass }) => { + factory({ + position, + count: 10, + maxCommits: 100, + }); + + expect(vm.find('.js-graph-bar').classes()).toContain(roundedClass); + }); + + it.each` + count | label + ${100} | ${'100'} + ${1000} | ${'999+'} + `('renders label as $roundedClass for $count', ({ count, label }) => { + factory({ + position: 'left', + count, + maxCommits: 1000, + }); + + expect(vm.find('.js-graph-count').text()).toContain(label); + }); + + it('sets width of bar', () => { + factory({ + position: 'left', + count: 100, + maxCommits: 1000, + }); + + expect(vm.find('.js-graph-bar').attributes('style')).toEqual('width: 10%;'); + }); +}); |