Commit 1351c794 authored by Miguel Rincon's avatar Miguel Rincon

Add offline, stale count to runner admins section

This change adds a offline, stale runners count to the admin page,
in order to help admins get more information about their system at
a glance.

Changelog: changed
parent 829106c0
...@@ -9,7 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo ...@@ -9,7 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue'; import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue'; import RunnerName from '../components/runner_name.vue';
import RunnerOnlineStat from '../components/stat/runner_online_stat.vue'; import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue'; import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue'; import RunnerTypeTabs from '../components/runner_type_tabs.vue';
...@@ -20,6 +20,9 @@ import { ...@@ -20,6 +20,9 @@ import {
INSTANCE_TYPE, INSTANCE_TYPE,
GROUP_TYPE, GROUP_TYPE,
PROJECT_TYPE, PROJECT_TYPE,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
I18N_FETCH_ERROR, I18N_FETCH_ERROR,
} from '../constants'; } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql'; import getRunnersQuery from '../graphql/get_runners.query.graphql';
...@@ -51,7 +54,7 @@ export default { ...@@ -51,7 +54,7 @@ export default {
RunnerFilteredSearchBar, RunnerFilteredSearchBar,
RunnerList, RunnerList,
RunnerName, RunnerName,
RunnerOnlineStat, RunnerStats,
RunnerPagination, RunnerPagination,
RunnerTypeTabs, RunnerTypeTabs,
}, },
...@@ -60,10 +63,6 @@ export default { ...@@ -60,10 +63,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
activeRunnersCount: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -130,6 +129,30 @@ export default { ...@@ -130,6 +129,30 @@ export default {
}; };
}, },
}, },
onlineRunnersTotal: {
...runnersCountSmartQuery,
variables() {
return {
status: STATUS_ONLINE,
};
},
},
offlineRunnersTotal: {
...runnersCountSmartQuery,
variables() {
return {
status: STATUS_OFFLINE,
};
},
},
staleRunnersTotal: {
...runnersCountSmartQuery,
variables() {
return {
status: STATUS_STALE,
};
},
},
}, },
computed: { computed: {
variables() { variables() {
...@@ -205,7 +228,11 @@ export default { ...@@ -205,7 +228,11 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<runner-online-stat class="gl-py-6 gl-px-5" :value="activeRunnersCount" /> <runner-stats
:online-runners-count="onlineRunnersTotal"
:offline-runners-count="offlineRunnersTotal"
:stale-runners-count="staleRunnersTotal"
/>
<div <div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0" class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
......
...@@ -25,9 +25,7 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { ...@@ -25,9 +25,7 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
return null; return null;
} }
// TODO `activeRunnersCount` should be implemented using a GraphQL API const { runnerInstallHelpPage, registrationToken } = el.dataset;
// https://gitlab.com/gitlab-org/gitlab/-/issues/333806
const { runnerInstallHelpPage, registrationToken, activeRunnersCount } = el.dataset;
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(),
...@@ -43,10 +41,6 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { ...@@ -43,10 +41,6 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
return h(AdminRunnersApp, { return h(AdminRunnersApp, {
props: { props: {
registrationToken, registrationToken,
// Runner counts are returned as formatted
// strings, we do not use `parseInt`.
activeRunnersCount,
}, },
}); });
}, },
......
<script>
import { GlSingleStat } from '@gitlab/ui/dist/charts';
export default {
components: {
GlSingleStat,
},
};
</script>
<template>
<gl-single-stat
v-bind="$attrs"
variant="success"
:title="s__('Runners|Online Runners')"
:meta-text="s__('Runners|online')"
/>
</template>
<script>
import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants';
import RunnerStatusStat from './runner_status_stat.vue';
export default {
components: {
RunnerStatusStat,
},
props: {
onlineRunnersCount: {
type: Number,
required: false,
default: null,
},
offlineRunnersCount: {
type: Number,
required: false,
default: null,
},
staleRunnersCount: {
type: Number,
required: false,
default: null,
},
},
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
};
</script>
<template>
<div class="gl-display-flex gl-py-6">
<runner-status-stat
class="gl-px-5"
:status="$options.STATUS_ONLINE"
:value="onlineRunnersCount"
/>
<runner-status-stat
class="gl-px-5"
:status="$options.STATUS_OFFLINE"
:value="offlineRunnersCount"
/>
<runner-status-stat
class="gl-px-5"
:status="$options.STATUS_STALE"
:value="staleRunnersCount"
/>
</div>
</template>
<script>
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { s__, formatNumber } from '~/locale';
import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants';
export default {
components: {
GlSingleStat,
},
props: {
value: {
type: Number,
required: false,
default: null,
},
status: {
type: String,
required: true,
},
},
computed: {
formattedValue() {
if (typeof this.value === 'number') {
return formatNumber(this.value);
}
return '-';
},
stat() {
switch (this.status) {
case STATUS_ONLINE:
return {
variant: 'success',
title: s__('Runners|Online runners'),
metaText: s__('Runners|online'),
};
case STATUS_OFFLINE:
return {
variant: 'muted',
title: s__('Runners|Offline runners'),
metaText: s__('Runners|offline'),
};
case STATUS_STALE:
return {
variant: 'warning',
title: s__('Runners|Stale runners'),
metaText: s__('Runners|stale'),
};
default:
return {
title: s__('Runners|Runners'),
};
}
},
},
};
</script>
<template>
<gl-single-stat
v-if="stat"
:value="formattedValue"
:variant="stat.variant"
:title="stat.title"
:meta-text="stat.metaText"
/>
</template>
...@@ -13,7 +13,7 @@ query getGroupRunners( ...@@ -13,7 +13,7 @@ query getGroupRunners(
$sort: CiRunnerSort $sort: CiRunnerSort
) { ) {
group(fullPath: $groupFullPath) { group(fullPath: $groupFullPath) {
id id # Apollo required
runners( runners(
membership: DESCENDANTS membership: DESCENDANTS
before: $before before: $before
......
query getGroupRunnersCount(
$groupFullPath: ID!
$status: CiRunnerStatus
$type: CiRunnerType
$tagList: [String!]
$search: String
) {
group(fullPath: $groupFullPath) {
id # Apollo required
runners(
membership: DESCENDANTS
status: $status
type: $type
tagList: $tagList
search: $search
) {
count
}
}
}
...@@ -9,7 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo ...@@ -9,7 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue'; import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue'; import RunnerName from '../components/runner_name.vue';
import RunnerOnlineStat from '../components/stat/runner_online_stat.vue'; import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue'; import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue'; import RunnerTypeTabs from '../components/runner_type_tabs.vue';
...@@ -19,8 +19,12 @@ import { ...@@ -19,8 +19,12 @@ import {
GROUP_FILTERED_SEARCH_NAMESPACE, GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE, GROUP_TYPE,
GROUP_RUNNER_COUNT_LIMIT, GROUP_RUNNER_COUNT_LIMIT,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
} from '../constants'; } from '../constants';
import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql'; import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql';
import getGroupRunnersCountQuery from '../graphql/get_group_runners_count.query.graphql';
import { import {
fromUrlQueryToSearch, fromUrlQueryToSearch,
fromSearchToUrl, fromSearchToUrl,
...@@ -28,6 +32,17 @@ import { ...@@ -28,6 +32,17 @@ import {
} from '../runner_search_utils'; } from '../runner_search_utils';
import { captureException } from '../sentry_utils'; import { captureException } from '../sentry_utils';
const runnersCountSmartQuery = {
query: getGroupRunnersCountQuery,
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
update(data) {
return data?.group?.runners?.count;
},
error(error) {
this.reportToSentry(error);
},
};
export default { export default {
name: 'GroupRunnersApp', name: 'GroupRunnersApp',
components: { components: {
...@@ -36,7 +51,7 @@ export default { ...@@ -36,7 +51,7 @@ export default {
RunnerFilteredSearchBar, RunnerFilteredSearchBar,
RunnerList, RunnerList,
RunnerName, RunnerName,
RunnerOnlineStat, RunnerStats,
RunnerPagination, RunnerPagination,
RunnerTypeTabs, RunnerTypeTabs,
}, },
...@@ -89,6 +104,33 @@ export default { ...@@ -89,6 +104,33 @@ export default {
this.reportToSentry(error); this.reportToSentry(error);
}, },
}, },
onlineRunnersTotal: {
...runnersCountSmartQuery,
variables() {
return {
groupFullPath: this.groupFullPath,
status: STATUS_ONLINE,
};
},
},
offlineRunnersTotal: {
...runnersCountSmartQuery,
variables() {
return {
groupFullPath: this.groupFullPath,
status: STATUS_OFFLINE,
};
},
},
staleRunnersTotal: {
...runnersCountSmartQuery,
variables() {
return {
groupFullPath: this.groupFullPath,
status: STATUS_STALE,
};
},
},
}, },
computed: { computed: {
variables() { variables() {
...@@ -147,7 +189,11 @@ export default { ...@@ -147,7 +189,11 @@ export default {
<template> <template>
<div> <div>
<runner-online-stat class="gl-py-6 gl-px-5" :value="groupRunnersCount" /> <runner-stats
:online-runners-count="onlineRunnersTotal"
:offline-runners-count="offlineRunnersTotal"
:stale-runners-count="staleRunnersTotal"
/>
<div class="gl-display-flex gl-align-items-center"> <div class="gl-display-flex gl-align-items-center">
<runner-type-tabs <runner-type-tabs
......
...@@ -65,10 +65,7 @@ module Ci ...@@ -65,10 +65,7 @@ module Ci
# Runner install help page is external, located at # Runner install help page is external, located at
# https://gitlab.com/gitlab-org/gitlab-runner # https://gitlab.com/gitlab-org/gitlab-runner
runner_install_help_page: 'https://docs.gitlab.com/runner/install/', runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
registration_token: Gitlab::CurrentSettings.runners_registration_token, registration_token: Gitlab::CurrentSettings.runners_registration_token
# Runner counts are returned as formatted strings
active_runners_count: Ci::Runner.online.count.to_s
} }
end end
......
...@@ -30726,10 +30726,13 @@ msgstr "" ...@@ -30726,10 +30726,13 @@ msgstr ""
msgid "Runners|Offline" msgid "Runners|Offline"
msgstr "" msgstr ""
msgid "Runners|Offline runners"
msgstr ""
msgid "Runners|Online" msgid "Runners|Online"
msgstr "" msgstr ""
msgid "Runners|Online Runners" msgid "Runners|Online runners"
msgstr "" msgstr ""
msgid "Runners|Paused" msgid "Runners|Paused"
...@@ -30825,6 +30828,9 @@ msgstr "" ...@@ -30825,6 +30828,9 @@ msgstr ""
msgid "Runners|Stale" msgid "Runners|Stale"
msgstr "" msgstr ""
msgid "Runners|Stale runners"
msgstr ""
msgid "Runners|Status" msgid "Runners|Status"
msgstr "" msgstr ""
......
...@@ -21,12 +21,16 @@ RSpec.describe "Admin Runners" do ...@@ -21,12 +21,16 @@ RSpec.describe "Admin Runners" do
context "when there are runners" do context "when there are runners" do
it 'has all necessary texts' do it 'has all necessary texts' do
create(:ci_runner, :instance, contacted_at: Time.now) create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: Time.now)
create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.week.ago)
create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.year.ago)
visit admin_runners_path visit admin_runners_path
expect(page).to have_text "Register an instance runner" expect(page).to have_text "Register an instance runner"
expect(page).to have_text "Online Runners 1" expect(page).to have_text "Online runners 1"
expect(page).to have_text "Offline runners 2"
expect(page).to have_text "Stale runners 1"
end end
it 'with an instance runner shows an instance badge' do it 'with an instance runner shows an instance badge' do
...@@ -387,7 +391,11 @@ RSpec.describe "Admin Runners" do ...@@ -387,7 +391,11 @@ RSpec.describe "Admin Runners" do
it 'has all necessary texts including no runner message' do it 'has all necessary texts including no runner message' do
expect(page).to have_text "Register an instance runner" expect(page).to have_text "Register an instance runner"
expect(page).to have_text "Online Runners 0"
expect(page).to have_text "Online runners 0"
expect(page).to have_text "Offline runners 0"
expect(page).to have_text "Stale runners 0"
expect(page).to have_text 'No runners found' expect(page).to have_text 'No runners found'
end end
......
...@@ -24,14 +24,15 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -24,14 +24,15 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
remove_repository(project) remove_repository(project)
end end
describe GraphQL::Query, type: :request do describe do
get_runners_query_name = 'get_runners.query.graphql'
before do before do
sign_in(admin) sign_in(admin)
enable_admin_mode!(admin) enable_admin_mode!(admin)
end end
describe GraphQL::Query, type: :request do
get_runners_query_name = 'get_runners.query.graphql'
let_it_be(:query) do let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_runners_query_name}") get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
end end
...@@ -52,11 +53,6 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -52,11 +53,6 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
describe GraphQL::Query, type: :request do describe GraphQL::Query, type: :request do
get_runners_count_query_name = 'get_runners_count.query.graphql' get_runners_count_query_name = 'get_runners_count.query.graphql'
before do
sign_in(admin)
enable_admin_mode!(admin)
end
let_it_be(:query) do let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}") get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}")
end end
...@@ -71,11 +67,6 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -71,11 +67,6 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
describe GraphQL::Query, type: :request do describe GraphQL::Query, type: :request do
get_runner_query_name = 'get_runner.query.graphql' get_runner_query_name = 'get_runner.query.graphql'
before do
sign_in(admin)
enable_admin_mode!(admin)
end
let_it_be(:query) do let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_runner_query_name}") get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
end end
...@@ -88,16 +79,18 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -88,16 +79,18 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty expect_graphql_errors_to_be_empty
end end
end end
end
describe GraphQL::Query, type: :request do describe do
get_group_runners_query_name = 'get_group_runners.query.graphql'
let_it_be(:group_owner) { create(:user) } let_it_be(:group_owner) { create(:user) }
before do before do
group.add_owner(group_owner) group.add_owner(group_owner)
end end
describe GraphQL::Query, type: :request do
get_group_runners_query_name = 'get_group_runners.query.graphql'
let_it_be(:query) do let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}") get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
end end
...@@ -119,4 +112,21 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -119,4 +112,21 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty expect_graphql_errors_to_be_empty
end end
end end
describe GraphQL::Query, type: :request do
get_group_runners_count_query_name = 'get_group_runners_count.query.graphql'
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_group_runners_count_query_name}")
end
it "#{fixtures_path}#{get_group_runners_count_query_name}.json" do
post_graphql(query, current_user: group_owner, variables: {
groupFullPath: group.full_path
})
expect_graphql_errors_to_be_empty
end
end
end
end end
...@@ -13,6 +13,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue'; ...@@ -13,6 +13,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue'; import RunnerList from '~/runner/components/runner_list.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue';
...@@ -37,7 +38,6 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered ...@@ -37,7 +38,6 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data'; import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockActiveRunnersCount = '2';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/runner/sentry_utils'); jest.mock('~/runner/sentry_utils');
...@@ -54,6 +54,7 @@ describe('AdminRunnersApp', () => { ...@@ -54,6 +54,7 @@ describe('AdminRunnersApp', () => {
let mockRunnersQuery; let mockRunnersQuery;
let mockRunnersCountQuery; let mockRunnersCountQuery;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs); const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList); const findRunnerList = () => wrapper.findComponent(RunnerList);
...@@ -70,15 +71,16 @@ describe('AdminRunnersApp', () => { ...@@ -70,15 +71,16 @@ describe('AdminRunnersApp', () => {
[getRunnersCountQuery, mockRunnersCountQuery], [getRunnersCountQuery, mockRunnersCountQuery],
]; ];
wrapper = mountFn(AdminRunnersApp, { wrapper = extendedWrapper(
mountFn(AdminRunnersApp, {
localVue, localVue,
apolloProvider: createMockApollo(handlers), apolloProvider: createMockApollo(handlers),
propsData: { propsData: {
registrationToken: mockRegistrationToken, registrationToken: mockRegistrationToken,
activeRunnersCount: mockActiveRunnersCount,
...props, ...props,
}, },
}); }),
);
}; };
beforeEach(async () => { beforeEach(async () => {
...@@ -95,6 +97,18 @@ describe('AdminRunnersApp', () => { ...@@ -95,6 +97,18 @@ describe('AdminRunnersApp', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('shows total runner counts', async () => {
createComponent({ mountFn: mount });
await waitForPromises();
const stats = findRunnerStats().text();
expect(stats).toMatch('Online runners 4');
expect(stats).toMatch('Offline runners 4');
expect(stats).toMatch('Stale runners 4');
});
it('shows the runner tabs with a runner count for each type', async () => { it('shows the runner tabs with a runner count for each type', async () => {
mockRunnersCountQuery.mockImplementation(({ type }) => { mockRunnersCountQuery.mockImplementation(({ type }) => {
let count; let count;
...@@ -198,12 +212,6 @@ describe('AdminRunnersApp', () => { ...@@ -198,12 +212,6 @@ describe('AdminRunnersApp', () => {
]); ]);
}); });
it('shows the active runner count', () => {
createComponent({ mountFn: mount });
expect(wrapper.text()).toMatch(new RegExp(`Online Runners ${mockActiveRunnersCount}`));
});
describe('when a filter is preselected', () => { describe('when a filter is preselected', () => {
beforeEach(async () => { beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`); setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
......
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount, mount } from '@vue/test-utils';
import RunnerOnlineBadge from '~/runner/components/stat/runner_online_stat.vue';
describe('RunnerOnlineBadge', () => {
let wrapper;
const findSingleStat = () => wrapper.findComponent(GlSingleStat);
const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
wrapper = mountFn(RunnerOnlineBadge, {
propsData: {
value: '99',
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('Uses a success appearance', () => {
createComponent({}, shallowMount);
expect(findSingleStat().props('variant')).toBe('success');
});
it('Renders a value', () => {
createComponent({}, mount);
expect(wrapper.text()).toMatch(new RegExp(`Online Runners 99\\s+online`));
});
});
import { shallowMount, mount } from '@vue/test-utils';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
describe('RunnerStats', () => {
let wrapper;
const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(RunnerStats, {
propsData: {
onlineRunnersCount: 3,
offlineRunnersCount: 2,
staleRunnersCount: 1,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('Displays all the stats', () => {
createComponent({ mountFn: mount });
const stats = wrapper.text();
expect(stats).toMatch('Online runners 3');
expect(stats).toMatch('Offline runners 2');
expect(stats).toMatch('Stale runners 1');
});
it.each`
i | status
${0} | ${STATUS_ONLINE}
${1} | ${STATUS_OFFLINE}
${2} | ${STATUS_STALE}
`('Displays status types at index $i', ({ i, status }) => {
createComponent();
expect(findRunnerStatusStatAt(i).props('status')).toBe(status);
});
});
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount, mount } from '@vue/test-utils';
import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
describe('RunnerStatusStat', () => {
let wrapper;
const findSingleStat = () => wrapper.findComponent(GlSingleStat);
const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
wrapper = mountFn(RunnerStatusStat, {
propsData: {
status: STATUS_ONLINE,
value: 99,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
status | variant | title | badge
${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'}
${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'}
${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'}
`('Renders a stat for status "$status"', ({ status, variant, title, badge }) => {
beforeEach(() => {
createComponent({ props: { status } }, mount);
});
it('Renders text', () => {
expect(wrapper.text()).toMatch(new RegExp(`${title} 99\\s+${badge}`));
});
it(`Uses variant ${variant}`, () => {
expect(findSingleStat().props('variant')).toBe(variant);
});
});
it('Formats stat number', () => {
createComponent({ props: { value: 1000 } }, mount);
expect(wrapper.text()).toMatch('Online runners 1,000');
});
it('Shows a null result', () => {
createComponent({ props: { value: null } }, mount);
expect(wrapper.text()).toMatch('Online runners -');
});
it('Shows an undefined result', () => {
createComponent({ props: { value: undefined } }, mount);
expect(wrapper.text()).toMatch('Online runners -');
});
it('Shows result for an unknown status', () => {
createComponent({ props: { status: 'UNKNOWN' } }, mount);
expect(wrapper.text()).toMatch('Runners 99');
});
});
...@@ -12,6 +12,7 @@ import { updateHistory } from '~/lib/utils/url_utility'; ...@@ -12,6 +12,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue'; import RunnerList from '~/runner/components/runner_list.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue';
...@@ -26,10 +27,11 @@ import { ...@@ -26,10 +27,11 @@ import {
RUNNER_PAGE_SIZE, RUNNER_PAGE_SIZE,
} from '~/runner/constants'; } from '~/runner/constants';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql'; import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import getGroupRunnersCountQuery from '~/runner/graphql/get_group_runners_count.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue'; import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
import { captureException } from '~/runner/sentry_utils'; import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { groupRunnersData, groupRunnersDataPaginated } from '../mock_data'; import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -48,7 +50,9 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -48,7 +50,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('GroupRunnersApp', () => { describe('GroupRunnersApp', () => {
let wrapper; let wrapper;
let mockGroupRunnersQuery; let mockGroupRunnersQuery;
let mockGroupRunnersCountQuery;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerList = () => wrapper.findComponent(RunnerList); const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
...@@ -59,7 +63,10 @@ describe('GroupRunnersApp', () => { ...@@ -59,7 +63,10 @@ describe('GroupRunnersApp', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]]; const handlers = [
[getGroupRunnersQuery, mockGroupRunnersQuery],
[getGroupRunnersCountQuery, mockGroupRunnersCountQuery],
];
wrapper = mountFn(GroupRunnersApp, { wrapper = mountFn(GroupRunnersApp, {
localVue, localVue,
...@@ -77,11 +84,24 @@ describe('GroupRunnersApp', () => { ...@@ -77,11 +84,24 @@ describe('GroupRunnersApp', () => {
setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`); setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`);
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData); mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData);
mockGroupRunnersCountQuery = jest.fn().mockResolvedValue(groupRunnersCountData);
createComponent(); createComponent();
await waitForPromises(); await waitForPromises();
}); });
it('shows total runner counts', async () => {
createComponent({ mountFn: mount });
await waitForPromises();
const stats = findRunnerStats().text();
expect(stats).toMatch('Online runners 2');
expect(stats).toMatch('Offline runners 2');
expect(stats).toMatch('Stale runners 2');
});
it('shows the runner setup instructions', () => { it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken); expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE); expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
...@@ -129,28 +149,6 @@ describe('GroupRunnersApp', () => { ...@@ -129,28 +149,6 @@ describe('GroupRunnersApp', () => {
); );
}); });
describe('shows the active runner count', () => {
const expectedOnlineCount = (count) => new RegExp(`Online Runners ${count}`);
it('with a regular value', () => {
createComponent({ mountFn: mount });
expect(wrapper.text()).toMatch(expectedOnlineCount(mockGroupRunnersLimitedCount));
});
it('at the limit', () => {
createComponent({ props: { groupRunnersLimitedCount: 1000 }, mountFn: mount });
expect(wrapper.text()).toMatch(expectedOnlineCount('1,000'));
});
it('over the limit', () => {
createComponent({ props: { groupRunnersLimitedCount: 1001 }, mountFn: mount });
expect(wrapper.text()).toMatch(expectedOnlineCount('1,000\\+'));
});
});
describe('when a filter is preselected', () => { describe('when a filter is preselected', () => {
beforeEach(async () => { beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`); setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
......
...@@ -8,6 +8,7 @@ import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.js ...@@ -8,6 +8,7 @@ import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.js
// Group queries // Group queries
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json'; import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runners_count.query.graphql.json';
import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json'; import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json';
export { export {
...@@ -16,5 +17,6 @@ export { ...@@ -16,5 +17,6 @@ export {
runnersDataPaginated, runnersDataPaginated,
runnersData, runnersData,
groupRunnersData, groupRunnersData,
groupRunnersCountData,
groupRunnersDataPaginated, groupRunnersDataPaginated,
}; };
...@@ -79,8 +79,7 @@ RSpec.describe Ci::RunnersHelper do ...@@ -79,8 +79,7 @@ RSpec.describe Ci::RunnersHelper do
it 'returns the data in format' do it 'returns the data in format' do
expect(helper.admin_runners_data_attributes).to eq({ expect(helper.admin_runners_data_attributes).to eq({
runner_install_help_page: 'https://docs.gitlab.com/runner/install/', runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
registration_token: Gitlab::CurrentSettings.runners_registration_token, registration_token: Gitlab::CurrentSettings.runners_registration_token
active_runners_count: '0'
}) })
end end
end end
......
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