diff options
author | Phil Hughes <me@iamphill.com> | 2018-01-15 14:22:31 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-01-15 14:22:31 +0000 |
commit | abae33a8ec5965a236f2e15ce6e6b6dc88d88537 (patch) | |
tree | ee3e008b5733f23d39e29c9b82b30aae6fc76485 | |
parent | f730a7ac96a9672ca9e63889e5d89b3c273047f1 (diff) | |
parent | ab68111f233bc4f5ccd6eed2ccca83fc92bc0016 (diff) | |
download | gitlab-ce-abae33a8ec5965a236f2e15ce6e6b6dc88d88537.tar.gz |
Merge branch '41856-backport-stacked-progress-bar' into 'master'
Backport StackedProgressBar component from GitLab EE
Closes #41856
See merge request gitlab-org/gitlab-ce!16457
4 files changed, 259 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue new file mode 100644 index 00000000000..86f06c8d266 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -0,0 +1,127 @@ +<script> +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + directives: { + tooltip, + }, + props: { + cssClass: { + type: String, + required: false, + default: '', + }, + successLabel: { + type: String, + required: true, + }, + failureLabel: { + type: String, + required: true, + }, + neutralLabel: { + type: String, + required: true, + }, + successCount: { + type: Number, + required: true, + }, + failureCount: { + type: Number, + required: true, + }, + totalCount: { + type: Number, + required: true, + }, + }, + computed: { + neutralCount() { + return this.totalCount - this.successCount - this.failureCount; + }, + successPercent() { + return this.getPercent(this.successCount); + }, + successBarStyle() { + return this.barStyle(this.successPercent); + }, + successTooltip() { + return this.getTooltip(this.successLabel, this.successCount); + }, + failurePercent() { + return this.getPercent(this.failureCount); + }, + failureBarStyle() { + return this.barStyle(this.failurePercent); + }, + failureTooltip() { + return this.getTooltip(this.failureLabel, this.failureCount); + }, + neutralPercent() { + return this.getPercent(this.neutralCount); + }, + neutralBarStyle() { + return this.barStyle(this.neutralPercent); + }, + neutralTooltip() { + return this.getTooltip(this.neutralLabel, this.neutralCount); + }, + }, + methods: { + getPercent(count) { + return Math.ceil((count / this.totalCount) * 100); + }, + barStyle(percent) { + return `width: ${percent}%;`; + }, + getTooltip(label, count) { + return `${label}: ${count}`; + }, + }, +}; +</script> + +<template> + <div + class="stacked-progress-bar" + :class="cssClass" + > + <span + v-if="!totalCount" + class="status-unavailable" + > + {{ __("Not available") }} + </span> + <span + v-tooltip + v-if="successPercent" + class="status-green" + data-placement="bottom" + :title="successTooltip" + :style="successBarStyle" + > + {{ successPercent }}% + </span> + <span + v-tooltip + v-if="neutralPercent" + class="status-neutral" + data-placement="bottom" + :title="neutralTooltip" + :style="neutralBarStyle" + > + {{ neutralPercent }}% + </span> + <span + v-tooltip + v-if="failurePercent" + class="status-red" + data-placement="bottom" + :title="failureTooltip" + :style="failureBarStyle" + > + {{ failurePercent }}% + </span> + </div> +</template> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 43b16d3cf7d..cff47ea76ec 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -59,3 +59,4 @@ @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive_tables"; +@import "framework/stacked-progress-bar"; diff --git a/app/assets/stylesheets/framework/stacked-progress-bar.scss b/app/assets/stylesheets/framework/stacked-progress-bar.scss new file mode 100644 index 00000000000..4869cda73e5 --- /dev/null +++ b/app/assets/stylesheets/framework/stacked-progress-bar.scss @@ -0,0 +1,54 @@ +.stacked-progress-bar { + display: flex; + height: 16px; + border-radius: 10px; + overflow: hidden; + background-color: $theme-gray-100; + + .status-unavailable, + .status-green, + .status-neutral, + .status-red, { + height: 100%; + min-width: 25px; + padding: 0 5px; + font-size: $tooltip-font-size; + font-weight: normal; + color: $white-light; + line-height: 16px; + + &:hover { + cursor: pointer; + } + } + + .status-unavailable { + padding: 0 10px; + color: $theme-gray-700; + } + + .status-green { + background-color: $green-500; + + &:hover { + background-color: $green-600; + } + } + + .status-neutral { + background-color: $theme-gray-200; + color: $gl-gray-dark; + + &:hover { + background-color: $theme-gray-300; + } + } + + .status-red { + background-color: $red-500; + + &:hover { + background-color: $red-600; + } + } +} diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js new file mode 100644 index 00000000000..6940b04573e --- /dev/null +++ b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js @@ -0,0 +1,77 @@ +import Vue from 'vue'; + +import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (config) => { + const Component = Vue.extend(stackedProgressBarComponent); + const defaultConfig = Object.assign({}, { + successLabel: 'Synced', + failureLabel: 'Failed', + neutralLabel: 'Out of sync', + successCount: 10, + failureCount: 5, + totalCount: 20, + }, config); + + return mountComponent(Component, defaultConfig); +}; + +describe('StackedProgressBarComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('neutralCount', () => { + it('returns neutralCount based on totalCount, successCount and failureCount', () => { + expect(vm.neutralCount).toBe(5); // 20 - 10 - 5 + }); + }); + }); + + describe('methods', () => { + describe('getPercent', () => { + it('returns percentage from provided count based on `totalCount`', () => { + expect(vm.getPercent(10)).toBe(50); + }); + }); + + describe('barStyle', () => { + it('returns style string based on percentage provided', () => { + expect(vm.barStyle(50)).toBe('width: 50%;'); + }); + }); + + describe('getTooltip', () => { + it('returns label string based on label and count provided', () => { + expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10'); + }); + }); + }); + + describe('template', () => { + it('renders container element', () => { + expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy(); + }); + + it('renders empty state when count is unavailable', () => { + const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 }); + expect(vmX.$el.querySelectorAll('.status-unavailable').length).not.toBe(0); + vmX.$destroy(); + }); + + it('renders bar elements when count is available', () => { + expect(vm.$el.querySelectorAll('.status-green').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-neutral').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-red').length).not.toBe(0); + }); + }); +}); |