Commit 12e82066 authored by Jose Vargas's avatar Jose Vargas

Create CI minutes usage page

This page lives inside the user preferences
page, allowing to visualize the CI minutes
usage by project or by month

Changelog: added

EE: true
parent abc7f820
<script>
import getCiMinutesUsage from '../graphql/queries/ci_minutes.graphql';
import MinutesUsageMonthChart from './minutes_usage_month_chart.vue';
import MinutesUsageProjectChart from './minutes_usage_project_chart.vue';
export default {
components: {
MinutesUsageMonthChart,
MinutesUsageProjectChart,
},
data() {
return {
ciMinutesUsage: [],
};
},
apollo: {
ciMinutesUsage: {
query: getCiMinutesUsage,
update(res) {
return res?.ciMinutesUsage?.nodes;
},
},
},
computed: {
minutesUsageDataByMonth() {
return this?.ciMinutesUsage.map((cur) => [cur.month, cur.minutes]);
},
},
};
</script>
<template>
<div class="gl-border-b-solid gl-border-gray-200 gl-border-b-1 gl-mb-3">
<minutes-usage-month-chart
class="gl-border-b-solid gl-border-gray-200 gl-border-b-1"
:minutes-usage-data="minutesUsageDataByMonth"
/>
<minutes-usage-project-chart :minutes-usage-data="ciMinutesUsage" />
</div>
</template>
<script>
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { USAGE_BY_MONTH, X_AXIS_MONTH_LABEL, X_AXIS_CATEGORY, Y_AXIS_LABEL } from '../constants';
export default {
USAGE_BY_MONTH,
X_AXIS_MONTH_LABEL,
X_AXIS_CATEGORY,
Y_AXIS_LABEL,
components: {
GlAreaChart,
},
props: {
minutesUsageData: {
type: Array,
required: true,
},
},
computed: {
chartOptions() {
return {
xAxis: {
name: this.$options.X_AXIS_MONTH_LABEL,
type: this.$options.X_AXIS_CATEGORY,
},
yAxis: {
name: this.$options.Y_AXIS_LABEL,
},
};
},
chartData() {
return [
{
data: this.minutesUsageData,
name: this.$options.USAGE_BY_MONTH,
},
];
},
isDataEmpty() {
return this.minutesUsageData.length === 0;
},
},
};
</script>
<template>
<div>
<h5>{{ $options.USAGE_BY_MONTH }}</h5>
<gl-area-chart v-if="!isDataEmpty" class="gl-mb-3" :data="chartData" :option="chartOptions" />
</div>
</template>
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { keyBy } from 'lodash';
import {
USAGE_BY_PROJECT,
X_AXIS_PROJECT_LABEL,
X_AXIS_CATEGORY,
Y_AXIS_LABEL,
} from '../constants';
export default {
USAGE_BY_PROJECT,
X_AXIS_PROJECT_LABEL,
X_AXIS_CATEGORY,
Y_AXIS_LABEL,
components: {
GlColumnChart,
GlDropdown,
GlDropdownItem,
},
props: {
minutesUsageData: {
type: Array,
required: true,
},
},
data() {
return {
selectedMonth: '',
};
},
computed: {
chartData() {
return [
{
data: this.getUsageDataSelectedMonth,
},
];
},
usageDataByMonth() {
return keyBy(this.minutesUsageData, 'month');
},
getUsageDataSelectedMonth() {
return this.usageDataByMonth[this.selectedMonth]?.projects?.nodes.map((cur) => [
cur.name,
cur.minutes,
]);
},
months() {
return this.minutesUsageData.map((cur) => cur.month);
},
isDataEmpty() {
return this.minutesUsageData.length === 0 && this.selectedMonth.length === 0;
},
},
watch: {
months() {
this.setFirstMonthDropdown();
},
},
mounted() {
if (!this.isDataEmpty) {
this.setFirstMonthDropdown();
}
},
methods: {
changeSelectedMonth(month) {
this.selectedMonth = month;
},
setFirstMonthDropdown() {
[this.selectedMonth] = this.months;
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-my-3">
<h5 class="gl-flex-grow-1">{{ $options.USAGE_BY_PROJECT }}</h5>
<gl-dropdown v-if="!isDataEmpty" :text="selectedMonth">
<gl-dropdown-item
v-for="(month, index) in months"
:key="index"
:is-checked="selectedMonth === month"
:is-check-item="true"
@click="changeSelectedMonth(month)"
>
{{ month }}
</gl-dropdown-item>
</gl-dropdown>
</div>
<gl-column-chart
v-if="!isDataEmpty"
class="gl-mb-3"
:bars="chartData"
:y-axis-title="$options.Y_AXIS_LABEL"
:x-axis-title="$options.X_AXIS_PROJECT_LABEL"
:x-axis-type="$options.X_AXIS_CATEGORY"
/>
</div>
</template>
import { __, s__ } from '~/locale';
// i18n
export const USAGE_BY_MONTH = s__('UsageQuota|CI minutes usage by month');
export const USAGE_BY_PROJECT = s__('UsageQuota|CI minutes usage by project');
export const X_AXIS_MONTH_LABEL = __('Month');
export const X_AXIS_PROJECT_LABEL = __('Projects');
export const Y_AXIS_LABEL = __('Minutes');
export const X_AXIS_CATEGORY = 'category';
query getCiMinutesUsage {
ciMinutesUsage {
nodes {
month
minutes
projects {
nodes {
name
minutes
}
}
}
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import CiMinutesUsageApp from './components/app.vue';
const mountCiMinutesUsageApp = (el) => {
Vue.use(VueApollo);
const defaultClient = createDefaultClient();
const apolloProvider = new VueApollo({
defaultClient,
});
return new Vue({
el,
apolloProvider,
name: 'CiMinutesUsageApp',
components: {
CiMinutesUsageApp,
},
render: (createElement) => createElement(CiMinutesUsageApp, {}),
});
};
export default () => {
const el = document.querySelector('.js-ci-minutes-usage');
return !el ? {} : mountCiMinutesUsageApp(el);
};
import ciMinutesUsage from 'ee/ci_minutes_usage';
import otherStorageCounter from 'ee/other_storage_counter';
import storageCounter from 'ee/storage_counter';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
......@@ -23,3 +24,5 @@ if (document.querySelector('#js-other-storage-counter-app')) {
hashedTabs: true,
});
}
ciMinutesUsage();
......@@ -38,6 +38,8 @@
= render 'namespaces/pipelines_quota/extra_shared_runners_minutes_quota', namespace: namespace
.js-ci-minutes-usage
%table.table.pipeline-project-metrics
%thead
%tr
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import CiMinutesUsageApp from 'ee/ci_minutes_usage/components/app.vue';
import MinutesUsageMonthChart from 'ee/ci_minutes_usage/components/minutes_usage_month_chart.vue';
import MinutesUsageProjectChart from 'ee/ci_minutes_usage/components/minutes_usage_project_chart.vue';
import ciMinutesUsage from 'ee/ci_minutes_usage/graphql/queries/ci_minutes.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { ciMinutesUsageMockData } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('CI minutes usage app', () => {
let wrapper;
function createMockApolloProvider() {
const requestHandlers = [[ciMinutesUsage, jest.fn().mockResolvedValue(ciMinutesUsageMockData)]];
return createMockApollo(requestHandlers);
}
function createComponent(options = {}) {
const { fakeApollo } = options;
return shallowMount(CiMinutesUsageApp, {
localVue,
apolloProvider: fakeApollo,
});
}
const findMinutesUsageMonthChart = () => wrapper.findComponent(MinutesUsageMonthChart);
const findMinutesUsageProjectChart = () => wrapper.findComponent(MinutesUsageProjectChart);
beforeEach(() => {
const fakeApollo = createMockApolloProvider();
wrapper = createComponent({ fakeApollo });
});
afterEach(() => {
wrapper.destroy();
});
it('should contain two charts', () => {
expect(findMinutesUsageMonthChart().exists()).toBe(true);
expect(findMinutesUsageProjectChart().exists()).toBe(true);
});
});
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MinutesUsageMonthChart from 'ee/ci_minutes_usage/components/minutes_usage_month_chart.vue';
import { ciMinutesUsageMockData } from '../mock_data';
describe('Minutes usage by month chart component', () => {
let wrapper;
const findAreaChart = () => wrapper.findComponent(GlAreaChart);
const createComponent = () => {
return shallowMount(MinutesUsageMonthChart, {
propsData: {
minutesUsageData: ciMinutesUsageMockData.data.ciMinutesUsage.nodes.map((cur) => [
cur.month,
cur.minutes,
]),
},
});
};
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders an area chart component', () => {
expect(findAreaChart().exists()).toBe(true);
});
});
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MinutesUsageProjectChart from 'ee/ci_minutes_usage/components/minutes_usage_project_chart.vue';
import { ciMinutesUsageMockData } from '../mock_data';
describe('Minutes usage by project chart component', () => {
let wrapper;
const findColumnChart = () => wrapper.findComponent(GlColumnChart);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const createComponent = () => {
return shallowMount(MinutesUsageProjectChart, {
propsData: {
minutesUsageData: ciMinutesUsageMockData.data.ciMinutesUsage.nodes,
},
});
};
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders a column chart component with axis legends', () => {
expect(findColumnChart().exists()).toBe(true);
expect(findColumnChart().props('xAxisTitle')).toBe('Projects');
expect(findColumnChart().props('yAxisTitle')).toBe('Minutes');
});
it('renders a dropdown component', () => {
expect(findDropdown().exists()).toBe(true);
expect(findDropdown().props('text')).toBe(
ciMinutesUsageMockData.data.ciMinutesUsage.nodes[0].month,
);
});
it('renders the same amount of dropdown components as the backend response', () => {
expect(findAllDropdownItems().length).toBe(
ciMinutesUsageMockData.data.ciMinutesUsage.nodes.length,
);
});
});
export const ciMinutesUsageMockData = {
data: {
ciMinutesUsage: {
nodes: [
{
month: 'June',
minutes: 5,
projects: {
nodes: [
{
name: 'devcafe-wp-theme',
minutes: 5,
},
],
},
},
],
},
},
};
......@@ -35617,6 +35617,12 @@ msgstr ""
msgid "UsageQuota|Buy additional minutes"
msgstr ""
msgid "UsageQuota|CI minutes usage by month"
msgstr ""
msgid "UsageQuota|CI minutes usage by project"
msgstr ""
msgid "UsageQuota|Current period usage"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment