diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..edc5ffb7b77e2711ed0feb356b0f441ba28a263e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js @@ -0,0 +1,6 @@ +export const DEFAULT_RX = 0.4; +export const DEFAULT_BAR_WIDTH = 6; +export const DEFAULT_LABEL_WIDTH = 4; +export const DEFAULT_LABEL_HEIGHT = 5; +export const BAR_HEIGHTS = [5, 7, 9, 14, 21, 35, 50, 80]; +export const GRID_YS = [30, 60, 90]; diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue new file mode 100644 index 0000000000000000000000000000000000000000..306fa61780f74587328e1cd393079c606c983235 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue @@ -0,0 +1,95 @@ +<script> +import { GlSkeletonLoader } from '@gitlab/ui'; + +import { + DEFAULT_RX, + DEFAULT_BAR_WIDTH, + DEFAULT_LABEL_WIDTH, + DEFAULT_LABEL_HEIGHT, + BAR_HEIGHTS, + GRID_YS, +} from './constants'; + +export default { + components: { + GlSkeletonLoader, + }, + props: { + barWidth: { + type: Number, + default: DEFAULT_BAR_WIDTH, + required: false, + }, + labelWidth: { + type: Number, + default: DEFAULT_LABEL_WIDTH, + required: false, + }, + labelHeight: { + type: Number, + default: DEFAULT_LABEL_HEIGHT, + required: false, + }, + rx: { + type: Number, + default: DEFAULT_RX, + required: false, + }, + // skeleton-loader will generate a unique key if not defined + uniqueKey: { + type: String, + default: undefined, + required: false, + }, + }, + computed: { + labelCentering() { + return (this.barWidth - this.labelWidth) / 2; + }, + }, + methods: { + getBarXPosition(index) { + const numberOfBars = this.$options.BAR_HEIGHTS.length; + const numberOfSpaces = numberOfBars + 1; + const spaceBetweenBars = (100 - numberOfSpaces * this.barWidth) / numberOfBars; + + return (0.5 + index) * (this.barWidth + spaceBetweenBars); + }, + }, + BAR_HEIGHTS, + GRID_YS, +}; +</script> +<template> + <gl-skeleton-loader :unique-key="uniqueKey"> + <rect + v-for="(y, index) in $options.GRID_YS" + :key="`grid-${index}`" + data-testid="skeleton-chart-grid" + x="0" + :y="`${y}%`" + width="100%" + height="1px" + /> + <rect + v-for="(height, index) in $options.BAR_HEIGHTS" + :key="`bar-${index}`" + data-testid="skeleton-chart-bar" + :x="`${getBarXPosition(index)}%`" + :y="`${90 - height}%`" + :width="`${barWidth}%`" + :height="`${height}%`" + :rx="`${rx}%`" + /> + <rect + v-for="(height, index) in $options.BAR_HEIGHTS" + :key="`label-${index}`" + data-testid="skeleton-chart-label" + :x="`${labelCentering + getBarXPosition(index)}%`" + :y="`${100 - labelHeight}%`" + :width="`${labelWidth}%`" + :height="`${labelHeight}%`" + :rx="`${rx}%`" + /> + </gl-skeleton-loader> +</template> diff --git a/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap similarity index 100% rename from spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap rename to spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..103b53cb2800fce5dbe34974372103f410e8fb6a --- /dev/null +++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap @@ -0,0 +1,324 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Resizable Skeleton Loader default setup renders the bars, labels, and grid with correct position, size, and rx percentages 1`] = ` +<gl-skeleton-loader-stub + baseurl="" + height="130" + preserveaspectratio="xMidYMid meet" + width="400" +> + <rect + data-testid="skeleton-chart-grid" + height="1px" + width="100%" + x="0" + y="30%" + /> + <rect + data-testid="skeleton-chart-grid" + height="1px" + width="100%" + x="0" + y="60%" + /> + <rect + data-testid="skeleton-chart-grid" + height="1px" + width="100%" + x="0" + y="90%" + /> + + <rect + data-testid="skeleton-chart-bar" + height="5%" + rx="0.4%" + width="6%" + x="5.875%" + y="85%" + /> + <rect + data-testid="skeleton-chart-bar" + height="7%" + rx="0.4%" + width="6%" + x="17.625%" + y="83%" + /> + <rect + data-testid="skeleton-chart-bar" + height="9%" + rx="0.4%" + width="6%" + x="29.375%" + y="81%" + /> + <rect + data-testid="skeleton-chart-bar" + height="14%" + rx="0.4%" + width="6%" + x="41.125%" + y="76%" + /> + <rect + data-testid="skeleton-chart-bar" + height="21%" + rx="0.4%" + width="6%" + x="52.875%" + y="69%" + /> + <rect + data-testid="skeleton-chart-bar" + height="35%" + rx="0.4%" + width="6%" + x="64.625%" + y="55%" + /> + <rect + data-testid="skeleton-chart-bar" + height="50%" + rx="0.4%" + width="6%" + x="76.375%" + y="40%" + /> + <rect + data-testid="skeleton-chart-bar" + height="80%" + rx="0.4%" + width="6%" + x="88.125%" + y="10%" + /> + + <rect + data-testid="skeleton-chart-label" + height="5%" + rx="0.4%" + width="4%" + x="6.875%" + y="95%" + /> + <rect + data-testid="skeleton-chart-label" + height="5%" + rx="0.4%" + width="4%" + x="18.625%" + y="95%" + /> + <rect + data-testid="skeleton-chart-label" + height="5%" + rx="0.4%" + width="4%" + x="30.375%" + y="95%" + /> + <rect + data-testid="skeleton-chart-label" + height="5%" + rx="0.4%" + width="4%" + x="42.125%" + y="95%" + /> + <rect + data-testid="skeleton-chart-label" + height="5%" + rx="0.4%" + width="4%" + x="53.875%" + y="95%" + /> + <rect + data-testid="skeleton-chart-label" + height="5%" + rx="0.4%" + width="4%" + x="65.625%" + y="95%" + /> + <rect + data-testid="skeleton-chart-label" + height="5%" + rx="0.4%" + width="4%" + x="77.375%" + y="95%" + /> + <rect + data-testid="skeleton-chart-label" + height="5%" + rx="0.4%" + width="4%" + x="89.125%" + y="95%" + /> +</gl-skeleton-loader-stub> +`; + +exports[`Resizable Skeleton Loader with custom settings renders the correct position, and size percentages for bars and labels with different settings 1`] = ` +<gl-skeleton-loader-stub + baseurl="" + height="130" + preserveaspectratio="xMidYMid meet" + uniquekey="" + width="400" +> + <rect + data-testid="skeleton-chart-grid" + height="1px" + width="100%" + x="0" + y="30%" + /> + <rect + data-testid="skeleton-chart-grid" + height="1px" + width="100%" + x="0" + y="60%" + /> + <rect + data-testid="skeleton-chart-grid" + height="1px" + width="100%" + x="0" + y="90%" + /> + + <rect + data-testid="skeleton-chart-bar" + height="5%" + rx="0.6%" + width="3%" + x="6.0625%" + y="85%" + /> + <rect + data-testid="skeleton-chart-bar" + height="7%" + rx="0.6%" + width="3%" + x="18.1875%" + y="83%" + /> + <rect + data-testid="skeleton-chart-bar" + height="9%" + rx="0.6%" + width="3%" + x="30.3125%" + y="81%" + /> + <rect + data-testid="skeleton-chart-bar" + height="14%" + rx="0.6%" + width="3%" + x="42.4375%" + y="76%" + /> + <rect + data-testid="skeleton-chart-bar" + height="21%" + rx="0.6%" + width="3%" + x="54.5625%" + y="69%" + /> + <rect + data-testid="skeleton-chart-bar" + height="35%" + rx="0.6%" + width="3%" + x="66.6875%" + y="55%" + /> + <rect + data-testid="skeleton-chart-bar" + height="50%" + rx="0.6%" + width="3%" + x="78.8125%" + y="40%" + /> + <rect + data-testid="skeleton-chart-bar" + height="80%" + rx="0.6%" + width="3%" + x="90.9375%" + y="10%" + /> + + <rect + data-testid="skeleton-chart-label" + height="2%" + rx="0.6%" + width="7%" + x="4.0625%" + y="98%" + /> + <rect + data-testid="skeleton-chart-label" + height="2%" + rx="0.6%" + width="7%" + x="16.1875%" + y="98%" + /> + <rect + data-testid="skeleton-chart-label" + height="2%" + rx="0.6%" + width="7%" + x="28.3125%" + y="98%" + /> + <rect + data-testid="skeleton-chart-label" + height="2%" + rx="0.6%" + width="7%" + x="40.4375%" + y="98%" + /> + <rect + data-testid="skeleton-chart-label" + height="2%" + rx="0.6%" + width="7%" + x="52.5625%" + y="98%" + /> + <rect + data-testid="skeleton-chart-label" + height="2%" + rx="0.6%" + width="7%" + x="64.6875%" + y="98%" + /> + <rect + data-testid="skeleton-chart-label" + height="2%" + rx="0.6%" + width="7%" + x="76.8125%" + y="98%" + /> + <rect + data-testid="skeleton-chart-label" + height="2%" + rx="0.6%" + width="7%" + x="88.9375%" + y="98%" + /> +</gl-skeleton-loader-stub> +`; diff --git a/spec/frontend/vue_shared/components/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js similarity index 100% rename from spec/frontend/vue_shared/components/resizable_chart_container_spec.js rename to spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js diff --git a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7facd02e5960a593531e4e445ed2e9145af58e0a --- /dev/null +++ b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js @@ -0,0 +1,55 @@ +import { shallowMount } from '@vue/test-utils'; +import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; + +describe('Resizable Skeleton Loader', () => { + let wrapper; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(ChartSkeletonLoader, { + propsData, + }); + }; + + const verifyElementsPresence = () => { + const gridItems = wrapper.findAll('[data-testid="skeleton-chart-grid"]').wrappers; + const barItems = wrapper.findAll('[data-testid="skeleton-chart-bar"]').wrappers; + const labelItems = wrapper.findAll('[data-testid="skeleton-chart-label"]').wrappers; + expect(gridItems.length).toBe(3); + expect(barItems.length).toBe(8); + expect(labelItems.length).toBe(8); + }; + + afterEach(() => { + if (wrapper?.destroy) { + wrapper.destroy(); + } + }); + + describe('default setup', () => { + beforeEach(() => { + createComponent({ uniqueKey: null }); + }); + + it('renders the bars, labels, and grid with correct position, size, and rx percentages', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders the correct number of grid items, bars, and labels', () => { + verifyElementsPresence(); + }); + }); + + describe('with custom settings', () => { + beforeEach(() => { + createComponent({ uniqueKey: '', rx: 0.6, barWidth: 3, labelWidth: 7, labelHeight: 2 }); + }); + + it('renders the correct position, and size percentages for bars and labels with different settings', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders the correct number of grid items, bars, and labels', () => { + verifyElementsPresence(); + }); + }); +});