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';
import { s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
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 RunnerDetailGroups from './runner_detail_groups.vue';
import RunnerTags from './runner_tags.vue';
export default {
......@@ -13,6 +14,7 @@ export default {
GlTab,
GlIntersperse,
RunnerDetail,
RunnerDetailGroups,
RunnerTags,
TimeAgo,
},
......@@ -43,6 +45,9 @@ export default {
}
return null;
},
isGroupRunner() {
return this.runner?.runnerType === GROUP_TYPE;
},
},
ACCESS_LEVEL_REF_PROTECTED,
};
......@@ -53,40 +58,44 @@ export default {
<gl-tab>
<template #title>{{ s__('Runners|Details') }}</template>
<div v-if="runner" class="gl-py-4">
<dl>
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
<runner-detail
:label="s__('Runners|Last contact')"
:empty-value="s__('Runners|Never contacted')"
>
<template #value>
<time-ago v-if="runner.contactedAt" :time="runner.contactedAt" />
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Version')" :value="runner.version" />
<runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" />
<runner-detail :label="s__('Runners|Configuration')">
<template #value>
<gl-intersperse v-if="configTextProtected || configTextUntagged">
<span v-if="configTextProtected">{{ configTextProtected }}</span>
<span v-if="configTextUntagged">{{ configTextUntagged }}</span>
</gl-intersperse>
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
<runner-detail :label="s__('Runners|Tags')">
<template #value>
<runner-tags
v-if="runner.tagList && runner.tagList.length"
class="gl-vertical-align-middle"
:tag-list="runner.tagList"
size="sm"
/>
</template>
</runner-detail>
</dl>
</div>
<template v-if="runner">
<div class="gl-pt-4">
<dl class="gl-mb-0">
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
<runner-detail
:label="s__('Runners|Last contact')"
:empty-value="s__('Runners|Never contacted')"
>
<template #value>
<time-ago v-if="runner.contactedAt" :time="runner.contactedAt" />
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Version')" :value="runner.version" />
<runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" />
<runner-detail :label="s__('Runners|Configuration')">
<template #value>
<gl-intersperse v-if="configTextProtected || configTextUntagged">
<span v-if="configTextProtected">{{ configTextProtected }}</span>
<span v-if="configTextUntagged">{{ configTextUntagged }}</span>
</gl-intersperse>
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
<runner-detail :label="s__('Runners|Tags')">
<template #value>
<runner-tags
v-if="runner.tagList && runner.tagList.length"
class="gl-vertical-align-middle"
:tag-list="runner.tagList"
size="sm"
/>
</template>
</runner-detail>
</dl>
</div>
<runner-detail-groups v-if="isGroupRunner" :runner="runner" />
</template>
</gl-tab>
</gl-tabs>
</template>
......@@ -18,4 +18,13 @@ fragment RunnerDetailsShared on CiRunner {
updateRunner
deleteRunner
}
groups {
nodes {
id
avatarUrl
name
fullName
webUrl
}
}
}
......@@ -30750,6 +30750,9 @@ msgstr ""
msgid "Runners|Architecture"
msgstr ""
msgid "Runners|Assigned Group"
msgstr ""
msgid "Runners|Associated with one or more projects"
msgstr ""
......
......@@ -78,6 +78,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
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
......
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
import RunnerDetails from '~/runner/components/runner_details.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 mockGroupRunner = runnerWithGroupData.data.runner;
describe('RunnerDetails', () => {
let wrapper;
......@@ -33,13 +35,12 @@ describe('RunnerDetails', () => {
return ErrorWrapper(dtLabel);
};
const createComponent = ({ runner = {}, mountFn = shallowMountExtended } = {}) => {
const findDetailGroups = () => wrapper.findComponent(RunnerDetailGroups);
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerDetails, {
propsData: {
runner: {
...mockRunner,
...runner,
},
...props,
},
stubs: {
GlIntersperse,
......@@ -54,6 +55,16 @@ describe('RunnerDetails', () => {
wrapper.destroy();
});
it('when no runner is present, no contents are shown', () => {
createComponent({
props: {
runner: null,
},
});
expect(wrapper.text()).toBe('');
});
describe.each`
field | runner | expectedValue
${'Description'} | ${{ description: 'My runner' }} | ${'My runner'}
......@@ -75,7 +86,12 @@ describe('RunnerDetails', () => {
`('"$field" field', ({ field, runner, expectedValue }) => {
beforeEach(() => {
createComponent({
runner,
props: {
runner: {
...mockRunner,
...runner,
},
},
});
});
......@@ -87,7 +103,9 @@ describe('RunnerDetails', () => {
describe('"Tags" field', () => {
it('displays expected value "tag-1 tag-2"', () => {
createComponent({
runner: { tagList: ['tag-1', 'tag-2'] },
props: {
runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] },
},
mountFn: mountExtended,
});
......@@ -96,11 +114,27 @@ describe('RunnerDetails', () => {
it('displays "None" when runner has no tags', () => {
createComponent({
runner: { tagList: [] },
props: {
runner: { ...mockRunner, tagList: [] },
},
mountFn: mountExtended,
});
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', () => {
editAdminUrl,
contactedAt,
userPermissions,
groups,
version,
...submitted
} = mockRunner;
......
......@@ -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 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 runnerWithGroupData from 'test_fixtures/graphql/runner/get_runner.query.graphql.with_group.json';
// Group queries
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
import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json';
export {
runnerData,
runnersData,
runnersCountData,
runnersDataPaginated,
runnersData,
runnerData,
runnerWithGroupData,
groupRunnersData,
groupRunnersCountData,
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