Commit a83f2072 authored by Miguel Rincon's avatar Miguel Rincon

Add runner counts to group runners page

This change improves the group runners page with total counts for
each tab and removal of the "Instance" tab.
parent 3736bc11
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import { searchValidator } from '~/runner/runner_search_utils';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
I18N_ALL_TYPES,
I18N_INSTANCE_TYPE,
I18N_GROUP_TYPE,
I18N_PROJECT_TYPE,
} from '../constants';
const tabs = [
{
title: s__('Runners|All'),
runnerType: null,
},
{
title: s__('Runners|Instance'),
runnerType: INSTANCE_TYPE,
},
{
title: s__('Runners|Group'),
runnerType: GROUP_TYPE,
},
{
title: s__('Runners|Project'),
runnerType: PROJECT_TYPE,
},
];
const I18N_TAB_TITLES = {
[INSTANCE_TYPE]: I18N_INSTANCE_TYPE,
[GROUP_TYPE]: I18N_GROUP_TYPE,
[PROJECT_TYPE]: I18N_PROJECT_TYPE,
};
export default {
components: {
......@@ -29,12 +23,34 @@ export default {
GlTab,
},
props: {
runnerTypes: {
type: Array,
required: false,
default: () => [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE],
},
value: {
type: Object,
required: true,
validator: searchValidator,
},
},
computed: {
tabs() {
const tabs = this.runnerTypes.map((runnerType) => ({
title: I18N_TAB_TITLES[runnerType],
runnerType,
}));
// Always add a "All" tab that resets filters
return [
{
title: I18N_ALL_TYPES,
runnerType: null,
},
...tabs,
];
},
},
methods: {
onTabSelected({ runnerType }) {
this.$emit('input', {
......@@ -47,13 +63,12 @@ export default {
return runnerType === this.value.runnerType;
},
},
tabs,
};
</script>
<template>
<gl-tabs v-bind="$attrs" data-testid="runner-type-tabs">
<gl-tab
v-for="tab in $options.tabs"
v-for="tab in tabs"
:key="`${tab.runnerType}`"
:active="isTabActive(tab)"
@click="onTabSelected(tab)"
......
......@@ -2,12 +2,16 @@ import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
export const RUNNER_JOB_COUNT_LIMIT = 1000;
export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
// Type
export const I18N_ALL_TYPES = s__('Runners|All');
export const I18N_INSTANCE_TYPE = s__('Runners|Instance');
export const I18N_GROUP_TYPE = s__('Runners|Group');
export const I18N_PROJECT_TYPE = s__('Runners|Project');
export const I18N_INSTANCE_RUNNER_DESCRIPTION = s__('Runners|Available to all projects');
export const I18N_GROUP_RUNNER_DESCRIPTION = s__(
'Runners|Available to all projects and subgroups in the group',
......
<script>
import { GlLink } from '@gitlab/ui';
import { GlBadge, GlLink } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, s__ } from '~/locale';
import { formatNumber } from '~/locale';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
......@@ -18,7 +18,7 @@ import {
I18N_FETCH_ERROR,
GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE,
GROUP_RUNNER_COUNT_LIMIT,
PROJECT_TYPE,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
......@@ -46,6 +46,7 @@ const runnersCountSmartQuery = {
export default {
name: 'GroupRunnersApp',
components: {
GlBadge,
GlLink,
RegistrationDropdown,
RunnerFilteredSearchBar,
......@@ -131,6 +132,33 @@ export default {
};
},
},
allRunnersCount: {
...runnersCountSmartQuery,
variables() {
return {
...this.countVariables,
type: null,
};
},
},
groupRunnersCount: {
...runnersCountSmartQuery,
variables() {
return {
...this.countVariables,
type: GROUP_TYPE,
};
},
},
projectRunnersCount: {
...runnersCountSmartQuery,
variables() {
return {
...this.countVariables,
type: PROJECT_TYPE,
};
},
},
},
computed: {
variables() {
......@@ -139,23 +167,17 @@ export default {
groupFullPath: this.groupFullPath,
};
},
countVariables() {
// Exclude pagination variables, leave only filters variables
const { sort, before, last, after, first, ...countVariables } = this.variables;
return countVariables;
},
runnersLoading() {
return this.$apollo.queries.runners.loading;
},
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
groupRunnersCount() {
if (this.groupRunnersLimitedCount > GROUP_RUNNER_COUNT_LIMIT) {
return `${formatNumber(GROUP_RUNNER_COUNT_LIMIT)}+`;
}
return formatNumber(this.groupRunnersLimitedCount);
},
runnerCountMessage() {
return sprintf(s__('Runners|Runners in this group: %{groupRunnersCount}'), {
groupRunnersCount: this.groupRunnersCount,
});
},
searchTokens() {
return [statusTokenConfig];
},
......@@ -179,10 +201,31 @@ export default {
this.reportToSentry(error);
},
methods: {
tabCount({ runnerType }) {
let count;
switch (runnerType) {
case null:
count = this.allRunnersCount;
break;
case GROUP_TYPE:
count = this.groupRunnersCount;
break;
case PROJECT_TYPE:
count = this.projectRunnersCount;
break;
default:
return null;
}
if (typeof count === 'number') {
return formatNumber(count);
}
return null;
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
},
TABS_RUNNER_TYPES: [GROUP_TYPE, PROJECT_TYPE],
GROUP_TYPE,
};
</script>
......@@ -198,9 +241,17 @@ export default {
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
v-model="search"
:runner-types="$options.TABS_RUNNER_TYPES"
content-class="gl-display-none"
nav-class="gl-border-none!"
/>
>
<template #title="{ tab }">
{{ tab.title }}
<gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm">
{{ tabCount(tab) }}
</gl-badge>
</template>
</runner-type-tabs>
<registration-dropdown
class="gl-ml-auto"
......
- page_title s_('Runners|Runners')
%h2.page-title
= s_('Runners|Group Runners')
#js-group-runners{ data: group_runners_data_attributes(@group).merge( { group_runners_limited_count: @group_runners_limited_count } ) }
......@@ -30810,9 +30810,6 @@ msgstr ""
msgid "Runners|Group"
msgstr ""
msgid "Runners|Group Runners"
msgstr ""
msgid "Runners|IP Address"
msgstr ""
......@@ -30945,9 +30942,6 @@ msgstr ""
msgid "Runners|Runners"
msgstr ""
msgid "Runners|Runners in this group: %{groupRunnersCount}"
msgstr ""
msgid "Runners|Runs untagged jobs"
msgstr ""
......
import { GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' };
......@@ -13,6 +13,7 @@ describe('RunnerTypeTabs', () => {
findTabs()
.filter((tab) => tab.attributes('active') === 'true')
.at(0);
const getTabsTitles = () => findTabs().wrappers.map((tab) => tab.text());
const createComponent = ({ props, ...options } = {}) => {
wrapper = shallowMount(RunnerTypeTabs, {
......@@ -35,13 +36,18 @@ describe('RunnerTypeTabs', () => {
wrapper.destroy();
});
it('Renders options to filter runners', () => {
expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
'All',
'Instance',
'Group',
'Project',
]);
it('Renders all options to filter runners by default', () => {
expect(getTabsTitles()).toEqual(['All', 'Instance', 'Group', 'Project']);
});
it('Renders fewer options to filter runners', () => {
createComponent({
props: {
runnerTypes: [GROUP_TYPE, PROJECT_TYPE],
},
});
expect(getTabsTitles()).toEqual(['All', 'Group', 'Project']);
});
it('"All" is selected by default', () => {
......
import Vue, { nextTick } from 'vue';
import { GlLink } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
extendedWrapper,
shallowMountExtended,
mountExtended,
} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
......@@ -23,6 +26,7 @@ import {
DEFAULT_SORT,
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
PARAM_KEY_STATUS,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
......@@ -54,6 +58,7 @@ describe('GroupRunnersApp', () => {
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationPrev = () =>
......@@ -62,7 +67,12 @@ describe('GroupRunnersApp', () => {
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
const mockCountQueryResult = (count) =>
Promise.resolve({
data: { group: { id: groupRunnersCountData.data.group.id, runners: { count } } },
});
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const handlers = [
[getGroupRunnersQuery, mockGroupRunnersQuery],
[getGroupRunnersCountQuery, mockGroupRunnersCountQuery],
......@@ -90,7 +100,7 @@ describe('GroupRunnersApp', () => {
});
it('shows total runner counts', async () => {
createComponent({ mountFn: mount });
createComponent({ mountFn: mountExtended });
await waitForPromises();
......@@ -101,6 +111,44 @@ describe('GroupRunnersApp', () => {
expect(stats).toMatch('Stale runners 2');
});
it('shows the runner tabs with a runner count for each type', async () => {
mockGroupRunnersCountQuery.mockImplementation(({ type }) => {
switch (type) {
case GROUP_TYPE:
return mockCountQueryResult(2);
case PROJECT_TYPE:
return mockCountQueryResult(1);
default:
return mockCountQueryResult(4);
}
});
createComponent({ mountFn: mountExtended });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText('All 4 Group 2 Project 1');
});
it('shows the runner tabs with a formatted runner count', async () => {
mockGroupRunnersCountQuery.mockImplementation(({ type }) => {
switch (type) {
case GROUP_TYPE:
return mockCountQueryResult(2000);
case PROJECT_TYPE:
return mockCountQueryResult(1000);
default:
return mockCountQueryResult(3000);
}
});
createComponent({ mountFn: mountExtended });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
'All 3,000 Group 2,000 Project 1,000',
);
});
it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
......@@ -115,7 +163,7 @@ describe('GroupRunnersApp', () => {
const { webUrl, node } = groupRunnersData.data.group.runners.edges[0];
const { id, shortSha } = node;
createComponent({ mountFn: mount });
createComponent({ mountFn: mountExtended });
await waitForPromises();
......@@ -135,7 +183,7 @@ describe('GroupRunnersApp', () => {
});
it('sets tokens in the filtered search', () => {
createComponent({ mountFn: mount });
createComponent({ mountFn: mountExtended });
const tokens = findFilteredSearch().props('tokens');
......@@ -250,7 +298,7 @@ describe('GroupRunnersApp', () => {
beforeEach(() => {
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersDataPaginated);
createComponent({ mountFn: mount });
createComponent({ mountFn: mountExtended });
});
it('more pages can be selected', () => {
......
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