diff --git a/src/components/base/progress_badge/progress_badge.md b/src/components/base/progress_badge/progress_badge.md new file mode 100644 index 0000000000000000000000000000000000000000..1cab06d5ad4c8abdc8a5a4a06b6ac5e132802f0f --- /dev/null +++ b/src/components/base/progress_badge/progress_badge.md @@ -0,0 +1,44 @@ +## Usage + +Signify the progress of something in a condensed way using a badge that +displays it as a fraction of a circle. + +```html + +``` + +You can optionally provide a `warning-at-percent` prop. If you set it to 60, +the badge will appear in a warning state once the count exceeds 60% of the +total. You can also provide it as a fraction (`0.6`). + +```html + +``` + +Similarly, you can provide a `danger-at-percent` prop that will make the badge +appear in a danger state once the count exceeds that amount of the total. + +```html + +``` + +If you don't provide a value for `warning-at-percent` and `danger-at-percent`, +the badge will appear in its default state even when `count >= total`. + +You can provide both warning states and danger states. If both are set to the +same value, the danger state takes precedence. + +Since you can provide both fractions and integers, a value of `1` will be +treated as 100%. diff --git a/src/components/base/progress_badge/progress_badge.scss b/src/components/base/progress_badge/progress_badge.scss new file mode 100644 index 0000000000000000000000000000000000000000..2c68d9481a45f51022e8bb2b6fa85a4bbfd59c5e --- /dev/null +++ b/src/components/base/progress_badge/progress_badge.scss @@ -0,0 +1,76 @@ +.gl-progress-badge { + @apply gl-inline-flex gl-items-center gl-p-1 gl-decoration-0; + + border-radius: 9999px; + + .gl-progress-badge-chart { + @apply gl-size-5; + + .gl-progress-badge-chart-filled, .gl-progress-badge-chart-remaining { + fill: none; + stroke-width: 7; + } + } + + .gl-progress-badge-content { + @apply gl-px-1 gl-text-sm; + } +} + +@mixin gl-progress-badge-variant( + $variant, + $background-color, + $empty-chart-line-color, + $filled-chart-line-color, + $filled-chart-line-color-darkmode, + $text-color +) { + .gl-progress-badge.gl-progress-badge-#{$variant} { + background-color: $background-color; + + .gl-progress-badge-content{ + color: $text-color; + } + + .gl-progress-badge-chart { + .gl-progress-badge-chart-filled { + stroke: $filled-chart-line-color; + } + + .gl-progress-badge-chart-remaining { + stroke: $empty-chart-line-color + } + } + + .gl-dark .gl-progress-badge-chart .gl-progress-badge-chart-filled { + stroke: $filled-chart-line-color-darkmode; + } + } +} + +@include gl-progress-badge-variant( + $variant: default, + $background-color: var(--gl-badge-muted-background-color-default), + $empty-chart-line-color: var(--gl-color-alpha-dark-24), + $filled-chart-line-color: var(--gl-color-blue-500), + $filled-chart-line-color-darkmode: var(--gl-color-blue-800), + $text-color: var(--gl-badge-muted-text-color-default), +); + +@include gl-progress-badge-variant( + $variant: warning, + $background-color: var(--gl-badge-warning-background-color-default), + $empty-chart-line-color: var(--gl-color-alpha-dark-16), + $filled-chart-line-color: var(--gl-badge-warning-icon-color-default), + $filled-chart-line-color-darkmode: var(--gl-badge-warning-icon-color-default), + $text-color: var(--gl-badge-warning-text-color-default), +); + +@include gl-progress-badge-variant( + $variant: danger, + $background-color: var(--gl-badge-danger-background-color-default), + $empty-chart-line-color: var(--gl-color-alpha-dark-16), + $filled-chart-line-color: var(--gl-badge-danger-icon-color-default), + $filled-chart-line-color-darkmode: var(--gl-badge-danger-icon-color-default), + $text-color: var(--gl-badge-danger-text-color-default), +); diff --git a/src/components/base/progress_badge/progress_badge.spec.js b/src/components/base/progress_badge/progress_badge.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7b1230e64e95c67b0d1116a93712c17cc7a3463a --- /dev/null +++ b/src/components/base/progress_badge/progress_badge.spec.js @@ -0,0 +1,104 @@ +import { mount } from '@vue/test-utils'; +import ProgressBadge from './progress_badge.vue'; + +describe('ProgressBadge', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = mount(ProgressBadge, { + propsData: props, + }); + }; + + const getSvg = () => wrapper.get('svg'); + const getChartFilledLine = () => getSvg().get('.gl-progress-badge-chart-filled'); + const getChartEmptyLine = () => getSvg().get('.gl-progress-badge-chart-remaining'); + const getContent = () => wrapper.get('.gl-progress-badge-content'); + + describe('svg structure', () => { + beforeEach(() => { + createComponent({ + count: 10, + total: 99, + }); + }); + + it('uses the correct path configuration that allows using percentage values in stroke-dasharray', () => { + const expectedPath = + 'M19.5 3.6 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831'; + expect(getChartFilledLine().attributes('d')).toEqual(expectedPath); + expect(getChartEmptyLine().attributes('d')).toEqual(expectedPath); + }); + }); + + describe.each` + scenario | count | total | warnAtPercent | dangerAtPercent | expectedVariantClass | expectedContent | expectedFilledStroke | expectedEmptyStroke + ${'default'} | ${20} | ${100} | ${undefined} | ${undefined} | ${'gl-progress-badge-default'} | ${'20/100'} | ${'20, 100'} | ${'0, 20, 100'} + ${'default almost filled, no warning'} | ${599} | ${600} | ${undefined} | ${undefined} | ${'gl-progress-badge-default'} | ${'599/600'} | ${'100, 100'} | ${'0, 100, 100'} + ${'warning'} | ${7} | ${12} | ${50} | ${undefined} | ${'gl-progress-badge-warning'} | ${'7/12'} | ${'59, 100'} | ${'0, 59, 100'} + ${'warning level as fraction'} | ${126} | ${222} | ${0.5} | ${undefined} | ${'gl-progress-badge-warning'} | ${'126/222'} | ${'57, 100'} | ${'0, 57, 100'} + ${'danger'} | ${901} | ${1001} | ${undefined} | ${90} | ${'gl-progress-badge-danger'} | ${'901/1001'} | ${'91, 100'} | ${'0, 91, 100'} + ${'danger level as fraction'} | ${19} | ${20} | ${undefined} | ${0.94} | ${'gl-progress-badge-danger'} | ${'19/20'} | ${'95, 100'} | ${'0, 95, 100'} + ${'negative count'} | ${-20} | ${100} | ${undefined} | ${undefined} | ${'gl-progress-badge-default'} | ${'-20/100'} | ${'0, 100'} | ${'0, 0, 100'} + ${'count > total'} | ${84} | ${67} | ${undefined} | ${undefined} | ${'gl-progress-badge-default'} | ${'84/67'} | ${'100, 100'} | ${'0, 100, 100'} + ${'total is 0'} | ${9262} | ${0} | ${undefined} | ${undefined} | ${'gl-progress-badge-default'} | ${'9262/0'} | ${'100, 100'} | ${'0, 100, 100'} + ${'warning level is 0'} | ${0} | ${10} | ${0} | ${undefined} | ${'gl-progress-badge-warning'} | ${'0/10'} | ${'0, 100'} | ${'0, 0, 100'} + ${'danger level is 0'} | ${0} | ${10} | ${undefined} | ${0} | ${'gl-progress-badge-danger'} | ${'0/10'} | ${'0, 100'} | ${'0, 0, 100'} + ${'warning level == danger level'} | ${60} | ${100} | ${60} | ${60} | ${'gl-progress-badge-danger'} | ${'60/100'} | ${'60, 100'} | ${'0, 60, 100'} + ${'warning level = 1 will be treated as 100%'} | ${10} | ${100} | ${1} | ${undefined} | ${'gl-progress-badge-default'} | ${'10/100'} | ${'10, 100'} | ${'0, 10, 100'} + ${'danger level = 1 will be treated as 100%'} | ${10} | ${100} | ${5} | ${1} | ${'gl-progress-badge-warning'} | ${'10/100'} | ${'10, 100'} | ${'0, 10, 100'} + `( + '$scenario', + ({ + count, + total, + warnAtPercent, + dangerAtPercent, + expectedVariantClass, + expectedContent, + expectedFilledStroke, + expectedEmptyStroke, + }) => { + beforeEach(() => { + createComponent({ count, total, warnAtPercent, dangerAtPercent }); + }); + + it('renders the correct wrapper classes', () => { + expect(wrapper.classes()).toContain('gl-progress-badge'); + expect(wrapper.classes()).toContain(expectedVariantClass); + }); + + it('renders the percentage as a filled dasharray', () => { + expect(getChartFilledLine().attributes('stroke-dasharray')).toEqual(expectedFilledStroke); + }); + + it('renders the remaining percentage as dasharray', () => { + expect(getChartEmptyLine().attributes('stroke-dasharray')).toEqual(expectedEmptyStroke); + }); + + it('renders the expected content', () => { + expect(getContent().text()).toBe(expectedContent); + }); + } + ); + + describe('validators', () => { + it.each([-Infinity, -1, 100.000001, 101, Infinity, 'invalid'])( + 'logs a warning when warnAtPercent is %d', + (value) => { + createComponent({ count: 1, total: 10, warnAtPercent: value }); + + expect(wrapper).toHaveLoggedVueWarnings(); + } + ); + + it.each([-Infinity, -1, 100.000001, 101, Infinity, 'invalid'])( + 'logs a warning when dangerAtPercent is %d', + (value) => { + createComponent({ count: 1, total: 10, dangerAtPercent: value }); + + expect(wrapper).toHaveLoggedVueWarnings(); + } + ); + }); +}); diff --git a/src/components/base/progress_badge/progress_badge.stories.js b/src/components/base/progress_badge/progress_badge.stories.js new file mode 100644 index 0000000000000000000000000000000000000000..340d249b5a5d2e5a699233997cc2de6b50002fbe --- /dev/null +++ b/src/components/base/progress_badge/progress_badge.stories.js @@ -0,0 +1,55 @@ +import GlProgressBadge from './progress_badge.vue'; +import readme from './progress_badge.md'; + +const generateProps = ({ + count = 33, + total = 100, + warnAtPercent = 60, + dangerAtPercent = 90, +} = {}) => ({ + count, + total, + warnAtPercent, + dangerAtPercent, +}); + +const Template = (args) => ({ + components: { GlProgressBadge }, + props: Object.keys(args), + template: ``, +}); + +export const Default = Template.bind({}); +Default.args = generateProps(); + +export const Warning = () => ({ + components: { GlProgressBadge }, + data: () => ({ + total: 100, + count: 65, + }), + template: ``, +}); +Warning.parameters = { controls: { disable: true } }; + +export const Danger = () => ({ + components: { GlProgressBadge }, + data: () => ({ + total: 100, + count: 90, + }), + template: ``, +}); +Danger.parameters = { controls: { disable: true } }; + +export default { + title: 'base/progress-badge', + component: GlProgressBadge, + parameters: { + docs: { + description: { + component: readme, + }, + }, + }, +}; diff --git a/src/components/base/progress_badge/progress_badge.vue b/src/components/base/progress_badge/progress_badge.vue new file mode 100644 index 0000000000000000000000000000000000000000..c9fdfee8521c12bb6c882305bb2fff741360d6c4 --- /dev/null +++ b/src/components/base/progress_badge/progress_badge.vue @@ -0,0 +1,105 @@ + + + diff --git a/src/scss/components.scss b/src/scss/components.scss index 5e9bc5c29a7be761a2d6c6ebca3ff03a780ae62d..e5ab1db22a9249a564b1baf9f7242574b366c6c3 100644 --- a/src/scss/components.scss +++ b/src/scss/components.scss @@ -55,6 +55,7 @@ @import '../components/base/pagination/pagination'; @import '../components/base/path/path'; @import '../components/base/popover/popover'; +@import '../components/base/progress_badge/progress_badge'; @import '../components/base/progress_bar/progress_bar'; @import '../components/base/search_box_by_type/search_box_by_type'; @import '../components/base/search_box_by_click/search_box_by_click'; diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-badge-danger-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-badge-danger-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..25987382041691ae65cc5e88cc0ed69d3fac4e6c Binary files /dev/null and b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-badge-danger-1-snap.png differ diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-badge-default-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-badge-default-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..6f1b8be35f5f0327c1070661ea165dee670001c3 Binary files /dev/null and b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-badge-default-1-snap.png differ diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-badge-warning-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-badge-warning-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..047d78aa88f9aa6f74e5e9d0c18f4624f43f5e06 Binary files /dev/null and b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-badge-warning-1-snap.png differ