diff --git a/ee/app/assets/javascripts/usage_quotas/usage_billing/components/app.vue b/ee/app/assets/javascripts/usage_quotas/usage_billing/components/app.vue index a21423da1d1909d6eeec91626dd9ffad1f3192db..e1e3e5707d5bb59cd3163f0170f7210d8a6a6818 100644 --- a/ee/app/assets/javascripts/usage_quotas/usage_billing/components/app.vue +++ b/ee/app/assets/javascripts/usage_quotas/usage_billing/components/app.vue @@ -6,6 +6,7 @@ import axios from '~/lib/utils/axios_utils'; import { captureException } from '~/sentry/sentry_browser_wrapper'; import PageHeading from '~/vue_shared/components/page_heading.vue'; import PurchaseCommitmentCard from './purchase_commitment_card.vue'; +import UsageTrendsChart from './usage_trends_chart.vue'; export default { name: 'UsageBillingApp', @@ -16,6 +17,7 @@ export default { GlTab, PageHeading, PurchaseCommitmentCard, + UsageTrendsChart, }, data() { return { @@ -24,6 +26,32 @@ export default { subscriptionData: null, }; }, + computed: { + gitlabUnitsUsage() { + return this.subscriptionData.gitlabUnitsUsage; + }, + trend() { + return ( + this.gitlabUnitsUsage.poolUsage?.usageTrend || this.gitlabUnitsUsage.seatUsage?.usageTrend + ); + }, + dailyUsage() { + return ( + this.gitlabUnitsUsage.poolUsage?.dailyUsage || this.gitlabUnitsUsage.seatUsage?.dailyUsage + ); + }, + dailyPeak() { + return ( + this.gitlabUnitsUsage.poolUsage?.peakUsage || this.gitlabUnitsUsage.seatUsage?.peakUsage + ); + }, + dailyAverage() { + return ( + this.gitlabUnitsUsage.poolUsage?.dailyAverage || + this.gitlabUnitsUsage.seatUsage?.dailyAverage + ); + }, + }, async mounted() { await this.fetchUsageData(); }, @@ -99,7 +127,14 @@ export default { - {{ s__('UsageBilling|Usage trends') }} + {{ s__('UsageBilling|Usage by user') }} diff --git a/ee/app/assets/javascripts/usage_quotas/usage_billing/components/usage_trends_chart.vue b/ee/app/assets/javascripts/usage_quotas/usage_billing/components/usage_trends_chart.vue new file mode 100644 index 0000000000000000000000000000000000000000..9ad53076e6a6ff5b6a63050d9a34fe5f9cf9d8c3 --- /dev/null +++ b/ee/app/assets/javascripts/usage_quotas/usage_billing/components/usage_trends_chart.vue @@ -0,0 +1,132 @@ + + diff --git a/ee/spec/frontend/usage_quotas/usage_billing/components/app_spec.js b/ee/spec/frontend/usage_quotas/usage_billing/components/app_spec.js index 6e59402a5cf7bd5a084e908e25ba93d13fc23f6f..6587ebcae3ae0e086171fef6a602d779e1c8d053 100644 --- a/ee/spec/frontend/usage_quotas/usage_billing/components/app_spec.js +++ b/ee/spec/frontend/usage_quotas/usage_billing/components/app_spec.js @@ -6,6 +6,8 @@ import waitForPromises from 'helpers/wait_for_promises'; import { logError } from '~/lib/logger'; import axios from '~/lib/utils/axios_utils'; import { captureException } from '~/sentry/sentry_browser_wrapper'; +import PurchaseCommitmentCard from 'ee/usage_quotas/usage_billing/components/purchase_commitment_card.vue'; +import UsageTrendsChart from 'ee/usage_quotas/usage_billing/components/usage_trends_chart.vue'; import { mockUsageDataWithPool } from '../mock_data'; jest.mock('~/lib/logger'); @@ -62,12 +64,25 @@ describe('UsageBillingApp', () => { ); }); + it('renders purchase-commitment-card', () => { + expect(wrapper.findComponent(PurchaseCommitmentCard).exists()).toBe(true); + }); + it('renders the correct tabs', () => { const tabs = findTabs(); expect(tabs.at(0).attributes('title')).toBe('Usage trends'); expect(tabs.at(1).attributes('title')).toBe('Usage by user'); }); + + it('renders usage trends chart with correct props', () => { + expect(wrapper.findComponent(UsageTrendsChart).props()).toMatchObject({ + monthStartDate: '2024-01-01', + monthEndDate: '2024-01-31', + trend: 0.12, + }); + expect(wrapper.findComponent(UsageTrendsChart).props('usageData')).toHaveLength(30); + }); }); describe('error state', () => { diff --git a/ee/spec/frontend/usage_quotas/usage_billing/components/usage_trends_chart_spec.js b/ee/spec/frontend/usage_quotas/usage_billing/components/usage_trends_chart_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..a449415b6f9e399cd22b9cd764bd1a68ab34047f --- /dev/null +++ b/ee/spec/frontend/usage_quotas/usage_billing/components/usage_trends_chart_spec.js @@ -0,0 +1,86 @@ +import { GlAreaChart } from '@gitlab/ui/src/charts'; +import { GlBadge } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import UsageTrendsChart from 'ee/usage_quotas/usage_billing/components/usage_trends_chart.vue'; + +describe('UsageTrendsChart', () => { + let wrapper; + + const defaultProps = { + usageData: [ + ['2025-07-01', '1000'], + ['2025-07-02', '200'], + ], + monthStartDate: '2025-07-01', + monthEndDate: '2025-07-31', + }; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(UsageTrendsChart, { + propsData: { ...defaultProps, ...props }, + }); + }; + + describe('rendering elements', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders formatted month date range', () => { + expect(wrapper.findByTestId('chart-heading').text()).toBe('Jul 1 – 31, 2025'); + }); + + it('passes the correct `option` prop to the gl-area-chart', () => { + expect(wrapper.findComponent(GlAreaChart).props('option')).toMatchObject({ + xAxis: { name: 'Date', type: 'category' }, + yAxis: { name: 'Tokens' }, + }); + }); + + it('passes the correct chartData to the gl-area-chart', () => { + expect(wrapper.findComponent(GlAreaChart).props('data')).toMatchObject([ + { + name: 'Daily usage', + data: [ + ['2025-07-01', '1000'], + ['2025-07-02', '200'], + ], + }, + ]); + }); + }); + + describe('trend changes', () => { + it.each` + trend | badgeVariant | badgeIcon + ${0.9} | ${'success'} | ${'trend-up'} + ${0} | ${'danger'} | ${'trend-down'} + ${0.2} | ${'neutral'} | ${'trend-static'} + `( + 'pass the correct variant and icon to the badge when trend = $trend', + ({ trend, badgeVariant, badgeIcon }) => { + createComponent({ trend }); + + expect(wrapper.findComponent(GlBadge).props()).toMatchObject({ + variant: badgeVariant, + icon: badgeIcon, + }); + }, + ); + + it.each` + trend | className + ${0.9} | ${'gl-text-green-500'} + ${0} | ${'gl-text-red-500'} + ${0.2} | ${''} + `( + 'pass the correct class to the usage trend title when trend = $trend', + ({ trend, className }) => { + createComponent({ trend }); + + const title = wrapper.findByTestId('usage-trend-title'); + expect(title.attributes('class')).toContain(className); + }, + ); + }); +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index aad31452156b17e8865aecdccd1f89b4c6b0b6ad..ee2eda13d245c6586239afff23052ca605f96b22 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -69085,21 +69085,48 @@ msgstr "" msgid "UsageBilling|An error occurred while fetching data" msgstr "" +msgid "UsageBilling|Current month" +msgstr "" + msgid "UsageBilling|Current month usage" msgstr "" +msgid "UsageBilling|Custom dates" +msgstr "" + +msgid "UsageBilling|Daily average use" +msgstr "" + +msgid "UsageBilling|Daily usage" +msgstr "" + msgid "UsageBilling|Increase monthly commitment" msgstr "" +msgid "UsageBilling|Last 3 months" +msgstr "" + +msgid "UsageBilling|Last month" +msgstr "" + +msgid "UsageBilling|Peak daily use" +msgstr "" + msgid "UsageBilling|Purchase a monthly commitment" msgstr "" +msgid "UsageBilling|Tokens" +msgstr "" + msgid "UsageBilling|Usage Billing" msgstr "" msgid "UsageBilling|Usage by user" msgstr "" +msgid "UsageBilling|Usage trend" +msgstr "" + msgid "UsageBilling|Usage trends" msgstr ""