Commit a0f54d10 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '19819-add-group-runners-list' into 'master'

Display list of group runners

See merge request gitlab-org/gitlab!66526
parents deec7383 61508b55
#import "~/runner/graphql/runner_node.fragment.graphql"
query getGroupRunners($groupFullPath: ID!) {
group(fullPath: $groupFullPath) {
runners(membership: DESCENDANTS) {
nodes {
...RunnerNode
}
}
}
}
<script> <script>
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue'; import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue'; import RunnerTypeHelp from '../components/runner_type_help.vue';
import { GROUP_TYPE } from '../constants'; import { I18N_FETCH_ERROR, GROUP_TYPE } from '../constants';
import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql';
import { captureException } from '../sentry_utils';
export default { export default {
name: 'GroupRunnersApp',
components: { components: {
RunnerList,
RunnerManualSetupHelp, RunnerManualSetupHelp,
RunnerTypeHelp, RunnerTypeHelp,
}, },
...@@ -13,6 +20,61 @@ export default { ...@@ -13,6 +20,61 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
groupFullPath: {
type: String,
required: true,
},
},
data() {
return {
runners: {
items: [],
},
};
},
apollo: {
runners: {
query: getGroupRunnersQuery,
// Runners can be updated by users directly in this list.
// A "cache and network" policy prevents outdated filtered
// results.
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
variables() {
return this.variables;
},
update(data) {
const { runners } = data?.group;
return {
items: runners?.nodes || [],
};
},
error(error) {
createFlash({ message: I18N_FETCH_ERROR });
this.reportToSentry(error);
},
},
},
computed: {
variables() {
return {
groupFullPath: this.groupFullPath,
};
},
runnersLoading() {
return this.$apollo.queries.runners.loading;
},
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
},
errorCaptured(error) {
this.reportToSentry(error);
},
methods: {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
}, },
GROUP_TYPE, GROUP_TYPE,
}; };
...@@ -31,5 +93,12 @@ export default { ...@@ -31,5 +93,12 @@ export default {
/> />
</div> </div>
</div> </div>
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
</div>
<template v-else>
<runner-list :runners="runners.items" :loading="runnersLoading" />
</template>
</div> </div>
</template> </template>
...@@ -12,7 +12,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => { ...@@ -12,7 +12,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
return null; return null;
} }
const { registrationToken, groupId } = el.dataset; const { registrationToken, runnerInstallHelpPage, groupId, groupFullPath } = el.dataset;
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient( defaultClient: createDefaultClient(
...@@ -27,12 +27,14 @@ export const initGroupRunners = (selector = '#js-group-runners') => { ...@@ -27,12 +27,14 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
el, el,
apolloProvider, apolloProvider,
provide: { provide: {
runnerInstallHelpPage,
groupId, groupId,
}, },
render(h) { render(h) {
return h(GroupRunnersApp, { return h(GroupRunnersApp, {
props: { props: {
registrationToken, registrationToken,
groupFullPath,
}, },
}); });
}, },
......
...@@ -3,4 +3,4 @@ ...@@ -3,4 +3,4 @@
%h2.page-title %h2.page-title
= s_('Runners|Group Runners') = s_('Runners|Group Runners')
#js-group-runners{ data: { registration_token: @group.runners_token, group_id: @group.id } } #js-group-runners{ data: { registration_token: @group.runners_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', group_id: @group.id, group_full_path: @group.full_path } }
...@@ -14,6 +14,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -14,6 +14,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') } let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') } let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') } let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
query_path = 'runner/graphql/' query_path = 'runner/graphql/'
...@@ -27,14 +28,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -27,14 +28,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
remove_repository(project) remove_repository(project)
end end
describe GraphQL::Query, type: :request 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
...@@ -55,6 +56,11 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -55,6 +56,11 @@ 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
...@@ -67,4 +73,26 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -67,4 +73,26 @@ 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_query_name = 'get_group_runners.query.graphql'
let_it_be(:group_owner) { create(:user) }
before do
group.add_owner(group_owner)
end
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
end
it "#{fixtures_path}#{get_group_runners_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
...@@ -78,7 +78,7 @@ describe('AdminRunnersApp', () => { ...@@ -78,7 +78,7 @@ describe('AdminRunnersApp', () => {
}); });
it('shows the runners list', () => { it('shows the runners list', () => {
expect(runnersData.data.runners.nodes).toMatchObject(findRunnerList().props('runners')); expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes);
}); });
it('requests the runners with no filters', () => { it('requests the runners with no filters', () => {
......
...@@ -56,7 +56,7 @@ describe('RunnerList', () => { ...@@ -56,7 +56,7 @@ describe('RunnerList', () => {
}); });
it('Displays a list of runners', () => { it('Displays a list of runners', () => {
expect(findRows()).toHaveLength(3); expect(findRows()).toHaveLength(4);
expect(findSkeletonLoader().exists()).toBe(false); expect(findSkeletonLoader().exists()).toBe(false);
}); });
......
import { shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue'; import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
import RunnerTypeHelp from '~/runner/components/runner_type_help.vue'; import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue'; import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
import { groupRunnersData } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC'; const mockRegistrationToken = 'AABBCC';
describe('GroupRunnersApp', () => { describe('GroupRunnersApp', () => {
let wrapper; let wrapper;
let mockGroupRunnersQuery;
const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp); const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp); const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const createComponent = ({ mountFn = shallowMount } = {}) => { const createComponent = ({ mountFn = shallowMount } = {}) => {
const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]];
wrapper = mountFn(GroupRunnersApp, { wrapper = mountFn(GroupRunnersApp, {
localVue,
apolloProvider: createMockApollo(handlers),
propsData: { propsData: {
registrationToken: mockRegistrationToken, registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
}, },
}); });
}; };
beforeEach(() => { beforeEach(async () => {
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData);
createComponent(); createComponent();
await waitForPromises();
}); });
it('shows the runner type help', () => { it('shows the runner type help', () => {
...@@ -31,4 +51,8 @@ describe('GroupRunnersApp', () => { ...@@ -31,4 +51,8 @@ describe('GroupRunnersApp', () => {
expect(findRunnerManualSetupHelp().exists()).toBe(true); expect(findRunnerManualSetupHelp().exists()).toBe(true);
expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken); expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
}); });
it('shows the runners list', () => {
expect(findRunnerList().props('runners')).toEqual(groupRunnersData.data.group.runners.nodes);
});
}); });
const runnerFixture = (filename) => getJSONFixture(`graphql/runner/${filename}`);
// Fixtures generated by: spec/frontend/fixtures/runner.rb // Fixtures generated by: spec/frontend/fixtures/runner.rb
export const runnersData = getJSONFixture('graphql/runner/get_runners.query.graphql.json');
export const runnersDataPaginated = getJSONFixture( // Admin queries
'graphql/runner/get_runners.query.graphql.paginated.json', export const runnersData = runnerFixture('get_runners.query.graphql.json');
); export const runnersDataPaginated = runnerFixture('get_runners.query.graphql.paginated.json');
export const runnerData = getJSONFixture('graphql/runner/get_runner.query.graphql.json'); export const runnerData = runnerFixture('get_runner.query.graphql.json');
// Group queries
export const groupRunnersData = runnerFixture('get_group_runners.query.graphql.json');
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