Commit 0bd3371e authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'jivanvl-add-ci-minutes-usage-app' into 'master'

Create CI minutes usage page

See merge request gitlab-org/gitlab!66779
parents 3487b4db eddd5e0b
<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
@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 otherStorageCounter from 'ee/other_storage_counter';
import storageCounter from 'ee/storage_counter'; import storageCounter from 'ee/storage_counter';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
...@@ -23,3 +24,5 @@ if (document.querySelector('#js-other-storage-counter-app')) { ...@@ -23,3 +24,5 @@ if (document.querySelector('#js-other-storage-counter-app')) {
hashedTabs: true, hashedTabs: true,
}); });
} }
ciMinutesUsage();
...@@ -38,6 +38,8 @@ ...@@ -38,6 +38,8 @@
= render 'namespaces/pipelines_quota/extra_shared_runners_minutes_quota', namespace: namespace = render 'namespaces/pipelines_quota/extra_shared_runners_minutes_quota', namespace: namespace
.js-ci-minutes-usage
%table.table.pipeline-project-metrics %table.table.pipeline-project-metrics
%thead %thead
%tr %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,
},
],
},
},
],
},
},
};
...@@ -35632,6 +35632,12 @@ msgstr "" ...@@ -35632,6 +35632,12 @@ msgstr ""
msgid "UsageQuota|Buy additional minutes" msgid "UsageQuota|Buy additional minutes"
msgstr "" msgstr ""
msgid "UsageQuota|CI minutes usage by month"
msgstr ""
msgid "UsageQuota|CI minutes usage by project"
msgstr ""
msgid "UsageQuota|Current period usage" msgid "UsageQuota|Current period usage"
msgstr "" 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