Commit 837754d9 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '347856-groups-in-runner-view' into 'master'

Display group related to a runner

See merge request gitlab-org/gitlab!78680
parents 9df0ef28 62443d4a
<script>
import { GlAvatar, GlLink } from '@gitlab/ui';
export default {
components: {
GlAvatar,
GlLink,
},
props: {
runner: {
type: Object,
required: true,
},
},
computed: {
groups() {
return this.runner.groups?.nodes || [];
},
},
};
</script>
<template>
<div class="gl-border-t-gray-100 gl-border-t-1 gl-border-t-solid">
<h3 class="gl-font-lg gl-my-5">{{ s__('Runners|Assigned Group') }}</h3>
<template v-if="groups.length">
<div v-for="group in groups" :key="group.id" class="gl-display-flex gl-align-items-center">
<gl-link
:href="group.webUrl"
data-testid="group-avatar"
class="gl-text-decoration-none! gl-mr-3"
>
<gl-avatar
shape="rect"
:entity-name="group.name"
:src="group.avatarUrl"
:alt="group.name"
:size="48"
/>
</gl-link>
<gl-link :href="group.webUrl" class="gl-font-lg gl-font-weight-bold gl-text-gray-900!">{{
group.fullName
}}</gl-link>
</div>
</template>
<span v-else class="gl-text-gray-500">{{ __('None') }}</span>
</div>
</template>
...@@ -3,8 +3,9 @@ import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui'; ...@@ -3,8 +3,9 @@ import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED } from '../constants'; import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE } from '../constants';
import RunnerDetail from './runner_detail.vue'; import RunnerDetail from './runner_detail.vue';
import RunnerDetailGroups from './runner_detail_groups.vue';
import RunnerTags from './runner_tags.vue'; import RunnerTags from './runner_tags.vue';
export default { export default {
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
GlTab, GlTab,
GlIntersperse, GlIntersperse,
RunnerDetail, RunnerDetail,
RunnerDetailGroups,
RunnerTags, RunnerTags,
TimeAgo, TimeAgo,
}, },
...@@ -43,6 +45,9 @@ export default { ...@@ -43,6 +45,9 @@ export default {
} }
return null; return null;
}, },
isGroupRunner() {
return this.runner?.runnerType === GROUP_TYPE;
},
}, },
ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_REF_PROTECTED,
}; };
...@@ -53,8 +58,9 @@ export default { ...@@ -53,8 +58,9 @@ export default {
<gl-tab> <gl-tab>
<template #title>{{ s__('Runners|Details') }}</template> <template #title>{{ s__('Runners|Details') }}</template>
<div v-if="runner" class="gl-py-4"> <template v-if="runner">
<dl> <div class="gl-pt-4">
<dl class="gl-mb-0">
<runner-detail :label="s__('Runners|Description')" :value="runner.description" /> <runner-detail :label="s__('Runners|Description')" :value="runner.description" />
<runner-detail <runner-detail
:label="s__('Runners|Last contact')" :label="s__('Runners|Last contact')"
...@@ -87,6 +93,9 @@ export default { ...@@ -87,6 +93,9 @@ export default {
</runner-detail> </runner-detail>
</dl> </dl>
</div> </div>
<runner-detail-groups v-if="isGroupRunner" :runner="runner" />
</template>
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>
</template> </template>
...@@ -18,4 +18,13 @@ fragment RunnerDetailsShared on CiRunner { ...@@ -18,4 +18,13 @@ fragment RunnerDetailsShared on CiRunner {
updateRunner updateRunner
deleteRunner deleteRunner
} }
groups {
nodes {
id
avatarUrl
name
fullName
webUrl
}
}
} }
...@@ -30750,6 +30750,9 @@ msgstr "" ...@@ -30750,6 +30750,9 @@ msgstr ""
msgid "Runners|Architecture" msgid "Runners|Architecture"
msgstr "" msgstr ""
msgid "Runners|Assigned Group"
msgstr ""
msgid "Runners|Associated with one or more projects" msgid "Runners|Associated with one or more projects"
msgstr "" msgstr ""
......
...@@ -78,6 +78,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do ...@@ -78,6 +78,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty expect_graphql_errors_to_be_empty
end end
it "#{fixtures_path}#{get_runner_query_name}.with_group.json" do
post_graphql(query, current_user: admin, variables: {
id: group_runner.to_global_id.to_s
})
expect_graphql_errors_to_be_empty
end
end end
end end
......
import { GlAvatar } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerDetailGroups from '~/runner/components/runner_detail_groups.vue';
import { runnerData, runnerWithGroupData } from '../mock_data';
const mockInstanceRunner = runnerData.data.runner;
const mockGroupRunner = runnerWithGroupData.data.runner;
const mockGroup = mockGroupRunner.groups.nodes[0];
describe('RunnerDetailGroups', () => {
let wrapper;
const findHeading = () => wrapper.find('h3');
const findGroupAvatar = () => wrapper.findByTestId('group-avatar');
const createComponent = ({ runner = mockGroupRunner, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerDetailGroups, {
propsData: {
runner,
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('Shows a heading', () => {
createComponent();
expect(findHeading().text()).toBe('Assigned Group');
});
describe('When there is group runner', () => {
beforeEach(() => {
createComponent();
});
it('Shows a group avatar', () => {
const avatar = findGroupAvatar();
expect(avatar.attributes('href')).toBe(mockGroup.webUrl);
expect(avatar.findComponent(GlAvatar).props()).toMatchObject({
alt: mockGroup.name,
entityName: mockGroup.name,
src: mockGroup.avatarUrl,
shape: 'rect',
size: 48,
});
});
it('Shows a group link', () => {
const groupFullName = wrapper.findByText(mockGroup.fullName);
expect(groupFullName.attributes('href')).toBe(mockGroup.webUrl);
});
});
describe('When there are no groups', () => {
beforeEach(() => {
createComponent({
runner: mockInstanceRunner,
});
});
it('Shows a "None" label', () => {
expect(wrapper.findByText('None').exists()).toBe(true);
});
});
});
...@@ -7,10 +7,12 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner ...@@ -7,10 +7,12 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner
import RunnerDetails from '~/runner/components/runner_details.vue'; import RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerDetail from '~/runner/components/runner_detail.vue'; import RunnerDetail from '~/runner/components/runner_detail.vue';
import RunnerDetailGroups from '~/runner/components/runner_detail_groups.vue';
import { runnerData } from '../mock_data'; import { runnerData, runnerWithGroupData } from '../mock_data';
const mockRunner = runnerData.data.runner; const mockRunner = runnerData.data.runner;
const mockGroupRunner = runnerWithGroupData.data.runner;
describe('RunnerDetails', () => { describe('RunnerDetails', () => {
let wrapper; let wrapper;
...@@ -33,13 +35,12 @@ describe('RunnerDetails', () => { ...@@ -33,13 +35,12 @@ describe('RunnerDetails', () => {
return ErrorWrapper(dtLabel); return ErrorWrapper(dtLabel);
}; };
const createComponent = ({ runner = {}, mountFn = shallowMountExtended } = {}) => { const findDetailGroups = () => wrapper.findComponent(RunnerDetailGroups);
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerDetails, { wrapper = mountFn(RunnerDetails, {
propsData: { propsData: {
runner: { ...props,
...mockRunner,
...runner,
},
}, },
stubs: { stubs: {
GlIntersperse, GlIntersperse,
...@@ -54,6 +55,16 @@ describe('RunnerDetails', () => { ...@@ -54,6 +55,16 @@ describe('RunnerDetails', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('when no runner is present, no contents are shown', () => {
createComponent({
props: {
runner: null,
},
});
expect(wrapper.text()).toBe('');
});
describe.each` describe.each`
field | runner | expectedValue field | runner | expectedValue
${'Description'} | ${{ description: 'My runner' }} | ${'My runner'} ${'Description'} | ${{ description: 'My runner' }} | ${'My runner'}
...@@ -75,7 +86,12 @@ describe('RunnerDetails', () => { ...@@ -75,7 +86,12 @@ describe('RunnerDetails', () => {
`('"$field" field', ({ field, runner, expectedValue }) => { `('"$field" field', ({ field, runner, expectedValue }) => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
runner, props: {
runner: {
...mockRunner,
...runner,
},
},
}); });
}); });
...@@ -87,7 +103,9 @@ describe('RunnerDetails', () => { ...@@ -87,7 +103,9 @@ describe('RunnerDetails', () => {
describe('"Tags" field', () => { describe('"Tags" field', () => {
it('displays expected value "tag-1 tag-2"', () => { it('displays expected value "tag-1 tag-2"', () => {
createComponent({ createComponent({
runner: { tagList: ['tag-1', 'tag-2'] }, props: {
runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] },
},
mountFn: mountExtended, mountFn: mountExtended,
}); });
...@@ -96,11 +114,27 @@ describe('RunnerDetails', () => { ...@@ -96,11 +114,27 @@ describe('RunnerDetails', () => {
it('displays "None" when runner has no tags', () => { it('displays "None" when runner has no tags', () => {
createComponent({ createComponent({
runner: { tagList: [] }, props: {
runner: { ...mockRunner, tagList: [] },
},
mountFn: mountExtended, mountFn: mountExtended,
}); });
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None'); expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None');
}); });
}); });
describe('Group runners', () => {
beforeEach(() => {
createComponent({
props: {
runner: mockGroupRunner,
},
});
});
it('Shows a group runner details', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
});
});
}); });
...@@ -130,6 +130,7 @@ describe('RunnerUpdateForm', () => { ...@@ -130,6 +130,7 @@ describe('RunnerUpdateForm', () => {
editAdminUrl, editAdminUrl,
contactedAt, contactedAt,
userPermissions, userPermissions,
groups,
version, version,
...submitted ...submitted
} = mockRunner; } = mockRunner;
......
...@@ -5,6 +5,7 @@ import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql. ...@@ -5,6 +5,7 @@ import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql.
import runnersCountData from 'test_fixtures/graphql/runner/get_runners_count.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';
import runnerWithGroupData from 'test_fixtures/graphql/runner/get_runner.query.graphql.with_group.json';
// 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';
...@@ -12,10 +13,11 @@ import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runner ...@@ -12,10 +13,11 @@ import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runner
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 {
runnerData, runnersData,
runnersCountData, runnersCountData,
runnersDataPaginated, runnersDataPaginated,
runnersData, runnerData,
runnerWithGroupData,
groupRunnersData, groupRunnersData,
groupRunnersCountData, groupRunnersCountData,
groupRunnersDataPaginated, groupRunnersDataPaginated,
......
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