Commit ff2af63d authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '19819-fix-group-runners-tabs-counts' into 'master'

Add runner counts to group runners page

See merge request gitlab-org/gitlab!78711
parents f15fbc36 a83f2072
<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 } ) }
......@@ -30834,9 +30834,6 @@ msgstr ""
msgid "Runners|Group"
msgstr ""
msgid "Runners|Group Runners"
msgstr ""
msgid "Runners|IP Address"
msgstr ""
......@@ -30969,9 +30966,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