Commit a54e0346 authored by Peter Hegman's avatar Peter Hegman

Merge branch '345710-update-counters-runner--tabs' into 'master'

Update total count of runners for each type

See merge request gitlab-org/gitlab!77752
parents 82d27826 92512ba6
...@@ -22,6 +22,7 @@ import { ...@@ -22,6 +22,7 @@ import {
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';
import getRunnersCountQuery from '../graphql/get_runners_count.query.graphql';
import { import {
fromUrlQueryToSearch, fromUrlQueryToSearch,
fromSearchToUrl, fromSearchToUrl,
...@@ -29,6 +30,17 @@ import { ...@@ -29,6 +30,17 @@ import {
} from '../runner_search_utils'; } from '../runner_search_utils';
import { captureException } from '../sentry_utils'; import { captureException } from '../sentry_utils';
const runnersCountSmartQuery = {
query: getRunnersCountQuery,
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
update(data) {
return data?.runners?.count;
},
error(error) {
this.reportToSentry(error);
},
};
export default { export default {
name: 'AdminRunnersApp', name: 'AdminRunnersApp',
components: { components: {
...@@ -51,22 +63,6 @@ export default { ...@@ -51,22 +63,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
allRunnersCount: {
type: String,
required: true,
},
instanceRunnersCount: {
type: String,
required: true,
},
groupRunnersCount: {
type: String,
required: true,
},
projectRunnersCount: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -100,11 +96,49 @@ export default { ...@@ -100,11 +96,49 @@ export default {
this.reportToSentry(error); this.reportToSentry(error);
}, },
}, },
allRunnersCount: {
...runnersCountSmartQuery,
variables() {
return this.countVariables;
},
},
instanceRunnersCount: {
...runnersCountSmartQuery,
variables() {
return {
...this.countVariables,
type: INSTANCE_TYPE,
};
},
},
groupRunnersCount: {
...runnersCountSmartQuery,
variables() {
return {
...this.countVariables,
type: GROUP_TYPE,
};
},
},
projectRunnersCount: {
...runnersCountSmartQuery,
variables() {
return {
...this.countVariables,
type: PROJECT_TYPE,
};
},
},
}, },
computed: { computed: {
variables() { variables() {
return fromSearchToVariables(this.search); return fromSearchToVariables(this.search);
}, },
countVariables() {
// Exclude pagination variables, leave only filters variables
const { sort, before, last, after, first, ...countVariables } = this.variables;
return countVariables;
},
runnersLoading() { runnersLoading() {
return this.$apollo.queries.runners.loading; return this.$apollo.queries.runners.loading;
}, },
...@@ -125,7 +159,7 @@ export default { ...@@ -125,7 +159,7 @@ export default {
search: { search: {
deep: true, deep: true,
handler() { handler() {
// TODO Implement back button reponse using onpopstate // TODO Implement back button response using onpopstate
updateHistory({ updateHistory({
url: fromSearchToUrl(this.search), url: fromSearchToUrl(this.search),
title: document.title, title: document.title,
...@@ -174,7 +208,7 @@ export default { ...@@ -174,7 +208,7 @@ export default {
> >
<template #title="{ tab }"> <template #title="{ tab }">
{{ tab.title }} {{ tab.title }}
<gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm"> <gl-badge v-if="typeof tabCount(tab) == 'number'" class="gl-ml-1" size="sm">
{{ tabCount(tab) }} {{ tabCount(tab) }}
</gl-badge> </gl-badge>
</template> </template>
......
...@@ -27,16 +27,7 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { ...@@ -27,16 +27,7 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
// TODO `activeRunnersCount` should be implemented using a GraphQL API // TODO `activeRunnersCount` should be implemented using a GraphQL API
// https://gitlab.com/gitlab-org/gitlab/-/issues/333806 // https://gitlab.com/gitlab-org/gitlab/-/issues/333806
const { const { runnerInstallHelpPage, registrationToken, activeRunnersCount } = el.dataset;
runnerInstallHelpPage,
registrationToken,
activeRunnersCount,
allRunnersCount,
instanceRunnersCount,
groupRunnersCount,
projectRunnersCount,
} = el.dataset;
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(),
...@@ -53,13 +44,9 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { ...@@ -53,13 +44,9 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
props: { props: {
registrationToken, registrationToken,
// All runner counts are returned as formatted // Runner counts are returned as formatted
// strings, we do not use `parseInt`. // strings, we do not use `parseInt`.
activeRunnersCount, activeRunnersCount,
allRunnersCount,
instanceRunnersCount,
groupRunnersCount,
projectRunnersCount,
}, },
}); });
}, },
......
query getRunnersCount(
$status: CiRunnerStatus
$type: CiRunnerType
$tagList: [String!]
$search: String
) {
runners(status: $status, type: $type, tagList: $tagList, search: $search) {
count
}
}
...@@ -67,12 +67,8 @@ module Ci ...@@ -67,12 +67,8 @@ module Ci
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,
# All runner counts are returned as formatted strings # Runner counts are returned as formatted strings
active_runners_count: Ci::Runner.online.count.to_s, active_runners_count: Ci::Runner.online.count.to_s
all_runners_count: limited_counter_with_delimiter(Ci::Runner),
instance_runners_count: limited_counter_with_delimiter(Ci::Runner.instance_type),
group_runners_count: limited_counter_with_delimiter(Ci::Runner.group_type),
project_runners_count: limited_counter_with_delimiter(Ci::Runner.project_type)
} }
end end
......
...@@ -131,6 +131,9 @@ RSpec.describe "Admin Runners" do ...@@ -131,6 +131,9 @@ RSpec.describe "Admin Runners" do
it 'shows correct runner when description matches' do it 'shows correct runner when description matches' do
input_filtered_search_keys('runner-foo') input_filtered_search_keys('runner-foo')
expect(page).to have_link('All 1')
expect(page).to have_link('Instance 1')
expect(page).to have_content("runner-foo") expect(page).to have_content("runner-foo")
expect(page).not_to have_content("runner-bar") expect(page).not_to have_content("runner-bar")
end end
...@@ -138,73 +141,76 @@ RSpec.describe "Admin Runners" do ...@@ -138,73 +141,76 @@ RSpec.describe "Admin Runners" do
it 'shows no runner when description does not match' do it 'shows no runner when description does not match' do
input_filtered_search_keys('runner-baz') input_filtered_search_keys('runner-baz')
expect(page).to have_link('All 0')
expect(page).to have_link('Instance 0')
expect(page).to have_text 'No runners found' expect(page).to have_text 'No runners found'
end end
end end
describe 'filter by status' do describe 'filter by status' do
it 'shows correct runner when status matches' do let!(:never_contacted) { create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil) }
create(:ci_runner, :instance, description: 'runner-active', active: true)
create(:ci_runner, :instance, description: 'runner-paused', active: false) before do
create(:ci_runner, :instance, description: 'runner-1', contacted_at: Time.now)
create(:ci_runner, :instance, description: 'runner-2', contacted_at: Time.now)
create(:ci_runner, :instance, description: 'runner-paused', active: false, contacted_at: Time.now)
visit admin_runners_path visit admin_runners_path
end
expect(page).to have_content 'runner-active' it 'shows all runners' do
expect(page).to have_content 'runner-1'
expect(page).to have_content 'runner-2'
expect(page).to have_content 'runner-paused' expect(page).to have_content 'runner-paused'
expect(page).to have_content 'runner-never-contacted'
expect(page).to have_link('All 4')
end
it 'shows correct runner when status matches' do
input_filtered_search_filter_is_only('Status', 'Active') input_filtered_search_filter_is_only('Status', 'Active')
expect(page).to have_content 'runner-active' expect(page).to have_link('All 3')
expect(page).to have_content 'runner-1'
expect(page).to have_content 'runner-2'
expect(page).to have_content 'runner-never-contacted'
expect(page).not_to have_content 'runner-paused' expect(page).not_to have_content 'runner-paused'
end end
it 'shows no runner when status does not match' do it 'shows no runner when status does not match' do
create(:ci_runner, :instance, description: 'runner-active', active: true) input_filtered_search_filter_is_only('Status', 'Stale')
create(:ci_runner, :instance, description: 'runner-paused', active: false)
visit admin_runners_path expect(page).to have_link('All 0')
input_filtered_search_filter_is_only('Status', 'Online')
expect(page).not_to have_content 'runner-active'
expect(page).not_to have_content 'runner-paused'
expect(page).to have_text 'No runners found' expect(page).to have_text 'No runners found'
end end
it 'shows correct runner when status is selected and search term is entered' do it 'shows correct runner when status is selected and search term is entered' do
create(:ci_runner, :instance, description: 'runner-a-1', active: true)
create(:ci_runner, :instance, description: 'runner-a-2', active: false)
create(:ci_runner, :instance, description: 'runner-b-1', active: true)
visit admin_runners_path
input_filtered_search_filter_is_only('Status', 'Active') input_filtered_search_filter_is_only('Status', 'Active')
input_filtered_search_keys('runner-1')
expect(page).to have_content 'runner-a-1' expect(page).to have_link('All 1')
expect(page).to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2'
input_filtered_search_keys('runner-a')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-1'
expect(page).not_to have_content 'runner-b-1' expect(page).not_to have_content 'runner-2'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-never-contacted'
expect(page).not_to have_content 'runner-paused'
end end
it 'shows correct runner when status filter is entered' do it 'shows correct runner when status filter is entered' do
never_connected = create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil)
create(:ci_runner, :instance, description: 'runner-contacted', contacted_at: Time.now)
visit admin_runners_path
# use the string "Never" to avoid using space and trigger an early selection # use the string "Never" to avoid using space and trigger an early selection
input_filtered_search_filter_is_only('Status', 'Never') input_filtered_search_filter_is_only('Status', 'Never')
expect(page).to have_link('All 1')
expect(page).not_to have_content 'runner-1'
expect(page).not_to have_content 'runner-2'
expect(page).not_to have_content 'runner-paused'
expect(page).to have_content 'runner-never-contacted' expect(page).to have_content 'runner-never-contacted'
expect(page).not_to have_content 'runner-contacted'
within "[data-testid='runner-row-#{never_connected.id}']" do within "[data-testid='runner-row-#{never_contacted.id}']" do
expect(page).to have_selector '.badge', text: 'never contacted' expect(page).to have_selector '.badge', text: 'never contacted'
end end
end end
...@@ -219,6 +225,10 @@ RSpec.describe "Admin Runners" do ...@@ -219,6 +225,10 @@ RSpec.describe "Admin Runners" do
it '"All" tab is selected by default' do it '"All" tab is selected by default' do
visit admin_runners_path visit admin_runners_path
expect(page).to have_link('All 2')
expect(page).to have_link('Group 1')
expect(page).to have_link('Project 1')
page.within('[data-testid="runner-type-tabs"]') do page.within('[data-testid="runner-type-tabs"]') do
expect(page).to have_link('All', class: 'active') expect(page).to have_link('All', class: 'active')
end end
...@@ -380,6 +390,13 @@ RSpec.describe "Admin Runners" do ...@@ -380,6 +390,13 @@ RSpec.describe "Admin Runners" do
expect(page).to have_text "Online Runners 0" expect(page).to have_text "Online Runners 0"
expect(page).to have_text 'No runners found' expect(page).to have_text 'No runners found'
end end
it 'shows tabs with total counts equal to 0' do
expect(page).to have_link('All 0')
expect(page).to have_link('Instance 0')
expect(page).to have_link('Group 0')
expect(page).to have_link('Project 0')
end
end end
context "when visiting outdated URLs" do context "when visiting outdated URLs" do
...@@ -581,6 +598,8 @@ RSpec.describe "Admin Runners" do ...@@ -581,6 +598,8 @@ RSpec.describe "Admin Runners" do
page.find('input').send_keys(search_term) page.find('input').send_keys(search_term)
click_on 'Search' click_on 'Search'
end end
wait_for_requests
end end
def input_filtered_search_filter_is_only(filter, value) def input_filtered_search_filter_is_only(filter, value)
...@@ -597,5 +616,7 @@ RSpec.describe "Admin Runners" do ...@@ -597,5 +616,7 @@ RSpec.describe "Admin Runners" do
click_on 'Search' click_on 'Search'
end end
wait_for_requests
end end
end end
...@@ -49,6 +49,25 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -49,6 +49,25 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end end
end end
describe GraphQL::Query, type: :request do
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
get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}")
end
it "#{fixtures_path}#{get_runners_count_query_name}.json" do
post_graphql(query, current_user: admin, variables: {})
expect_graphql_errors_to_be_empty
end
end
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'
......
...@@ -22,23 +22,22 @@ import { ...@@ -22,23 +22,22 @@ import {
CREATED_DESC, CREATED_DESC,
DEFAULT_SORT, DEFAULT_SORT,
INSTANCE_TYPE, INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
PARAM_KEY_STATUS, PARAM_KEY_STATUS,
PARAM_KEY_TAG, PARAM_KEY_TAG,
STATUS_ACTIVE, STATUS_ACTIVE,
RUNNER_PAGE_SIZE, RUNNER_PAGE_SIZE,
} from '~/runner/constants'; } from '~/runner/constants';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql'; import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import getRunnersCountQuery from '~/runner/graphql/get_runners_count.query.graphql';
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 { runnersData, runnersDataPaginated } from '../mock_data'; import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockActiveRunnersCount = '2'; const mockActiveRunnersCount = '2';
const mockAllRunnersCount = '6';
const mockInstanceRunnersCount = '3';
const mockGroupRunnersCount = '2';
const mockProjectRunnersCount = '1';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/runner/sentry_utils'); jest.mock('~/runner/sentry_utils');
...@@ -53,6 +52,7 @@ localVue.use(VueApollo); ...@@ -53,6 +52,7 @@ localVue.use(VueApollo);
describe('AdminRunnersApp', () => { describe('AdminRunnersApp', () => {
let wrapper; let wrapper;
let mockRunnersQuery; let mockRunnersQuery;
let mockRunnersCountQuery;
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs); const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
...@@ -65,7 +65,10 @@ describe('AdminRunnersApp', () => { ...@@ -65,7 +65,10 @@ describe('AdminRunnersApp', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
const handlers = [[getRunnersQuery, mockRunnersQuery]]; const handlers = [
[getRunnersQuery, mockRunnersQuery],
[getRunnersCountQuery, mockRunnersCountQuery],
];
wrapper = mountFn(AdminRunnersApp, { wrapper = mountFn(AdminRunnersApp, {
localVue, localVue,
...@@ -73,10 +76,6 @@ describe('AdminRunnersApp', () => { ...@@ -73,10 +76,6 @@ describe('AdminRunnersApp', () => {
propsData: { propsData: {
registrationToken: mockRegistrationToken, registrationToken: mockRegistrationToken,
activeRunnersCount: mockActiveRunnersCount, activeRunnersCount: mockActiveRunnersCount,
allRunnersCount: mockAllRunnersCount,
instanceRunnersCount: mockInstanceRunnersCount,
groupRunnersCount: mockGroupRunnersCount,
projectRunnersCount: mockProjectRunnersCount,
...props, ...props,
}, },
}); });
...@@ -86,6 +85,19 @@ describe('AdminRunnersApp', () => { ...@@ -86,6 +85,19 @@ describe('AdminRunnersApp', () => {
setWindowLocation('/admin/runners'); setWindowLocation('/admin/runners');
mockRunnersQuery = jest.fn().mockResolvedValue(runnersData); mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
mockRunnersCountQuery = jest.fn().mockImplementation(({ type }) => {
const mockResponse = {
[INSTANCE_TYPE]: 3,
[GROUP_TYPE]: 2,
[PROJECT_TYPE]: 1,
};
if (mockResponse[type]) {
return Promise.resolve({
data: { runners: { count: mockResponse[type] } },
});
}
return Promise.resolve(runnersCountData);
});
createComponent(); createComponent();
await waitForPromises(); await waitForPromises();
}); });
...@@ -101,7 +113,7 @@ describe('AdminRunnersApp', () => { ...@@ -101,7 +113,7 @@ describe('AdminRunnersApp', () => {
await waitForPromises(); await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
`All ${mockAllRunnersCount} Instance ${mockInstanceRunnersCount} Group ${mockGroupRunnersCount} Project ${mockProjectRunnersCount}`, `All ${runnersCountData.data.runners.count} Instance 3 Group 2 Project 1`,
); );
}); });
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Admin queries // Admin queries
import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql.json'; import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql.json';
import runnersCountData from 'test_fixtures/graphql/runner/get_runners_count.query.graphql.json';
import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query.graphql.paginated.json'; import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query.graphql.paginated.json';
import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json'; import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json';
...@@ -11,6 +12,7 @@ import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_ru ...@@ -11,6 +12,7 @@ import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_ru
export { export {
runnerData, runnerData,
runnersCountData,
runnersDataPaginated, runnersDataPaginated,
runnersData, runnersData,
groupRunnersData, groupRunnersData,
......
...@@ -80,11 +80,7 @@ RSpec.describe Ci::RunnersHelper do ...@@ -80,11 +80,7 @@ RSpec.describe Ci::RunnersHelper 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', active_runners_count: '0'
all_runners_count: '2',
instance_runners_count: '1',
group_runners_count: '0',
project_runners_count: '1'
}) })
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