Commit 927f165c authored by Jose Vargas's avatar Jose Vargas

Add shared runner tab to group CI/CD analytics

This adds a shared runner tab to the group CI/CD
analytics page, said tab contains an area chart
that shows how many minutes of shared runners
ahve been used per month

Changelog: changed
EE: true
parent 336ac172
......@@ -4,6 +4,7 @@ import DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_c
import LeadTimeCharts from 'ee/dora/components/lead_time_charts.vue';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import ReleaseStatsCard from './release_stats_card.vue';
import SharedRunnersUsage from './shared_runner_usage.vue';
export default {
name: 'CiCdAnalyticsApp',
......@@ -13,6 +14,7 @@ export default {
GlTab,
DeploymentFrequencyCharts,
LeadTimeCharts,
SharedRunnersUsage,
},
inject: {
shouldRenderDoraCharts: {
......@@ -33,6 +35,8 @@ export default {
tabsToShow.push('deployment-frequency', 'lead-time');
}
tabsToShow.push('shared-runner-usage');
return tabsToShow;
},
releaseStatsCardClasses() {
......@@ -73,6 +77,9 @@ export default {
<lead-time-charts />
</gl-tab>
</template>
<gl-tab :title="s__('CICDAnalytics|Shared runner usage')">
<shared-runners-usage />
</gl-tab>
</gl-tabs>
<release-stats-card v-else :class="releaseStatsCardClasses" />
</div>
......
<script>
import { GlIcon, GlPopover } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { formatDate } from '~/lib/utils/datetime_utility';
import { TYPE_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import getCiMinutesUsageByNamespace from '../graphql/ci_minutes.query.graphql';
export default {
components: {
GlIcon,
GlPopover,
GlAreaChart,
},
i18n: {
sharedRunnersUsage: s__('CICDAnalytics|Shared Runners Usage'),
xAxisLabel: __('Month'),
yAxisLabel: __('Minutes'),
seriesName: s__('UsageQuota|CI minutes usage by month'),
},
popoverOptions: {
triggers: 'hover',
placement: 'top',
content: s__(
'CICDAnalytics|Shared runner usage is the total runtime of all jobs that ran on shared runners',
),
title: s__('CICDAnalytics|What is shared runner usage?'),
},
inject: ['groupId'],
data() {
return {
ciMinutesUsage: [],
};
},
apollo: {
ciMinutesUsage: {
query: getCiMinutesUsageByNamespace,
variables() {
return {
namespaceId: convertToGraphQLId(TYPE_GROUP, this.groupId),
};
},
update(res) {
return res?.ciMinutesUsage?.nodes;
},
},
},
computed: {
chartOptions() {
return {
xAxis: {
name: this.$options.i18n.xAxisLabel,
type: 'category',
},
yAxis: {
name: this.$options.i18n.yAxisLabel,
},
};
},
minutesUsageDataByMonth() {
return this.ciMinutesUsage
.slice()
.sort((a, b) => {
return new Date(a.monthIso8601) - new Date(b.monthIso8601);
})
.map((cur) => [formatDate(cur.monthIso8601, 'mmm yyyy'), cur.minutes]);
},
isDataEmpty() {
return this.minutesUsageDataByMonth.length === 0;
},
chartData() {
return [
{
data: this.minutesUsageDataByMonth,
name: this.$options.i18n.seriesName,
},
];
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-align-items-center gl-mb-4">
<div class="gl-display-flex">
<h3 class="gl-mr-2 gl-my-0">{{ $options.i18n.sharedRunnersUsage }}</h3>
</div>
<div id="shared-runner-message-popover-container" class="gl-display-flex">
<span id="shared-runner-question">
<gl-icon class="gl-text-blue-500" name="question-o" />
</span>
<gl-popover
target="shared-runner-question"
container="shared-runner-message-popover-container"
v-bind="$options.popoverOptions"
/>
</div>
</div>
<gl-area-chart v-if="!isDataEmpty" :data="chartData" :option="chartOptions" />
</div>
</template>
query getCiMinutesUsageByNamespace($namespaceId: NamespaceID) {
ciMinutesUsage(namespaceId: $namespaceId) {
nodes {
month
monthIso8601
minutes
projects {
nodes {
name
minutes
}
}
}
}
}
......@@ -15,7 +15,7 @@ export default () => {
if (!el) return false;
const { fullPath } = el.dataset;
const { fullPath, groupId } = el.dataset;
const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts);
......@@ -25,6 +25,7 @@ export default () => {
provide: {
groupPath: fullPath,
shouldRenderDoraCharts,
groupId,
},
render: (createElement) => createElement(CiCdAnalyticsApp),
});
......
- page_title _("CI/CD Analytics")
#js-group-ci-cd-analytics-app{ data: { full_path: @group.full_path,
should_render_dora_charts: should_render_dora_charts.to_s } }
should_render_dora_charts: should_render_dora_charts.to_s, group_id: @group.id } }
......@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import CiCdAnalyticsApp from 'ee/analytics/group_ci_cd_analytics/components/app.vue';
import ReleaseStatsCard from 'ee/analytics/group_ci_cd_analytics/components/release_stats_card.vue';
import SharedRunnersUsage from 'ee/analytics/group_ci_cd_analytics/components/shared_runner_usage.vue';
import DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_charts.vue';
import LeadTimeCharts from 'ee/dora/components/lead_time_charts.vue';
import setWindowLocation from 'helpers/set_window_location_helper';
......@@ -44,10 +45,11 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => {
it('renders tabs in the correct order', () => {
expect(findGlTabs().exists()).toBe(true);
expect(findAllGlTabs().length).toBe(3);
expect(findAllGlTabs().length).toBe(4);
expect(findGlTabAtIndex(0).attributes('title')).toBe('Release statistics');
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency');
expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead time');
expect(findGlTabAtIndex(3).attributes('title')).toBe('Shared runner usage');
});
});
......@@ -56,13 +58,13 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => {
createComponent({ provide: { shouldRenderDoraCharts: false } });
});
it('does not render any tabs', () => {
expect(findGlTabs().exists()).toBe(false);
});
it('renders the release statistics component', () => {
expect(wrapper.findComponent(ReleaseStatsCard).exists()).toBe(true);
});
it('renders the shared runner usage component', () => {
expect(wrapper.findComponent(SharedRunnersUsage).exists()).toBe(true);
});
});
});
......
......@@ -11,3 +11,45 @@ export const groupReleaseStatsQueryResponse = {
},
},
};
export const ciMinutesUsageNamespace = {
data: {
ciMinutesUsage: {
nodes: [
{
month: 'December',
monthIso8601: '2021-12-01',
minutes: 110,
projects: { nodes: [], __typename: 'CiMinutesProjectMonthlyUsageConnection' },
__typename: 'CiMinutesNamespaceMonthlyUsage',
},
{
month: 'November',
monthIso8601: '2021-11-01',
minutes: 95,
projects: {
nodes: [
{ name: 'Html5 Boilerplate', minutes: 0, __typename: 'CiMinutesProjectMonthlyUsage' },
],
__typename: 'CiMinutesProjectMonthlyUsageConnection',
},
__typename: 'CiMinutesNamespaceMonthlyUsage',
},
{
month: 'October',
monthIso8601: '2021-10-01',
minutes: 85,
projects: { nodes: [], __typename: 'CiMinutesProjectMonthlyUsageConnection' },
__typename: 'CiMinutesNamespaceMonthlyUsage',
},
{
month: 'September',
monthIso8601: '2021-09-01',
minutes: 85,
projects: { nodes: [], __typename: 'CiMinutesProjectMonthlyUsageConnection' },
__typename: 'CiMinutesNamespaceMonthlyUsage',
},
],
},
},
};
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import SharedRunnerUsage from 'ee/analytics/group_ci_cd_analytics/components/shared_runner_usage.vue';
import getCiMinutesUsageByNamespace from 'ee/analytics/group_ci_cd_analytics/graphql/ci_minutes.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { ciMinutesUsageNamespace } from './mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Shared runner usage tab', () => {
let wrapper;
const createComponent = ({ isLoading = false, options } = {}) => {
const mockApolloLoading = {
mocks: {
$apollo: {
queries: {
ciMinutesUsage: {
loading: isLoading,
},
},
},
},
};
const mock = options?.apolloProvider ? {} : mockApolloLoading;
wrapper = shallowMount(SharedRunnerUsage, {
provide: {
groupId: '1',
},
...mock,
...options,
});
};
const createComponentWithApollo = () => {
const ciMinutesMock = jest.fn().mockResolvedValue(ciMinutesUsageNamespace);
const handlers = [[getCiMinutesUsageByNamespace, ciMinutesMock]];
const apolloProvider = createMockApollo(handlers);
createComponent({
options: {
localVue,
apolloProvider,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findAreaChart = () => wrapper.find(GlAreaChart);
describe('when the data has successfully loaded', () => {
beforeEach(() => {
createComponentWithApollo();
});
it('should display the chart', () => {
expect(findAreaChart().exists()).toBe(true);
});
});
describe('when the component is loading data', () => {
beforeEach(() => {
createComponent({ isLoading: true });
});
it('should not display the chart', () => {
expect(findAreaChart().exists()).toBe(false);
});
});
});
......@@ -6284,9 +6284,21 @@ msgstr ""
msgid "CICDAnalytics|Releases"
msgstr ""
msgid "CICDAnalytics|Shared Runners Usage"
msgstr ""
msgid "CICDAnalytics|Shared runner usage"
msgstr ""
msgid "CICDAnalytics|Shared runner usage is the total runtime of all jobs that ran on shared runners"
msgstr ""
msgid "CICDAnalytics|Something went wrong while fetching release statistics"
msgstr ""
msgid "CICDAnalytics|What is shared runner usage?"
msgstr ""
msgid "CICD|Add a %{base_domain_link_start}base domain%{link_end} to your %{kubernetes_cluster_link_start}Kubernetes cluster%{link_end} for your deployment strategy to work."
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