Commit 1c7c73be authored by Peter Hegman's avatar Peter Hegman

Merge branch '347856-runner-edit-page-heading' into 'master'

Update look and feel of runner heading

See merge request gitlab-org/gitlab!77519
parents 8ac06b70 f3cf2fee
<script>
import { GlSprintf } from '@gitlab/ui';
import { sprintf } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { I18N_DETAILS_TITLE } from '../constants';
import RunnerTypeBadge from './runner_type_badge.vue';
import RunnerStatusBadge from './runner_status_badge.vue';
export default {
components: {
GlSprintf,
TimeAgo,
RunnerTypeBadge,
RunnerStatusBadge,
},
props: {
runner: {
type: Object,
required: true,
},
},
computed: {
paused() {
return !this.runner.active;
},
heading() {
const id = getIdFromGraphQLId(this.runner.id);
return sprintf(I18N_DETAILS_TITLE, { runner_id: id });
},
},
};
</script>
<template>
<div class="gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<runner-status-badge :runner="runner" />
<runner-type-badge v-if="runner" :type="runner.runnerType" />
<template v-if="runner.createdAt">
<gl-sprintf :message="__('%{runner} created %{timeago}')">
<template #runner>
<strong>{{ heading }}</strong>
</template>
<template #timeago>
<time-ago :time="runner.createdAt" />
</template>
</gl-sprintf>
</template>
<template v-else>
<strong>{{ heading }}</strong>
</template>
</div>
</template>
<script>
import { GlAlert, GlLink } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
const ALERT_DATA = {
[INSTANCE_TYPE]: {
message: s__(
'Runners|This runner is available to all groups and projects in your GitLab instance.',
),
anchor: 'shared-runners',
},
[GROUP_TYPE]: {
message: s__('Runners|This runner is available to all projects and subgroups in a group.'),
anchor: 'group-runners',
},
[PROJECT_TYPE]: {
message: s__('Runners|This runner is associated with one or more projects.'),
anchor: 'specific-runners',
},
};
export default {
components: {
GlAlert,
GlLink,
},
props: {
type: {
type: String,
required: false,
default: null,
validator(type) {
return Boolean(ALERT_DATA[type]);
},
},
},
computed: {
alert() {
return ALERT_DATA[this.type];
},
helpHref() {
return helpPagePath('ci/runners/runners_scope', { anchor: this.alert.anchor });
},
},
};
</script>
<template>
<gl-alert v-if="alert" variant="info" :dismissible="false">
{{ alert.message }}
<gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link>
</gl-alert>
</template>
......@@ -9,4 +9,6 @@ fragment RunnerDetailsShared on CiRunner {
description
maximumTimeout
tagList
createdAt
status(legacyMode: null)
}
......@@ -2,19 +2,16 @@
import createFlash from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { sprintf } from '~/locale';
import RunnerTypeAlert from '../components/runner_type_alert.vue';
import RunnerTypeBadge from '../components/runner_type_badge.vue';
import RunnerHeader from '../components/runner_header.vue';
import RunnerUpdateForm from '../components/runner_update_form.vue';
import { I18N_DETAILS_TITLE, I18N_FETCH_ERROR } from '../constants';
import { I18N_FETCH_ERROR } from '../constants';
import getRunnerQuery from '../graphql/get_runner.query.graphql';
import { captureException } from '../sentry_utils';
export default {
name: 'RunnerDetailsApp',
components: {
RunnerTypeAlert,
RunnerTypeBadge,
RunnerHeader,
RunnerUpdateForm,
},
props: {
......@@ -43,11 +40,6 @@ export default {
},
},
},
computed: {
pageTitle() {
return sprintf(I18N_DETAILS_TITLE, { runner_id: this.runnerId });
},
},
errorCaptured(error) {
this.reportToSentry(error);
},
......@@ -60,12 +52,7 @@ export default {
</script>
<template>
<div>
<h2 class="page-title">
{{ pageTitle }} <runner-type-badge v-if="runner" :type="runner.runnerType" />
</h2>
<runner-type-alert v-if="runner" :type="runner.runnerType" />
<runner-header v-if="runner" :runner="runner" />
<runner-update-form :runner="runner" class="gl-my-5" />
</div>
</template>
......@@ -876,6 +876,9 @@ msgstr ""
msgid "%{rotation} has been recalculated with the remaining participants. Please review the new setup for %{rotation}. It is recommended that you reach out to the current on-call responder to ensure continuity of on-call coverage."
msgstr ""
msgid "%{runner} created %{timeago}"
msgstr ""
msgid "%{scope} results for term '%{term}'"
msgstr ""
......@@ -30741,9 +30744,6 @@ msgstr ""
msgid "Runners|This runner has never contacted this instance"
msgstr ""
msgid "Runners|This runner is associated with one or more projects."
msgstr ""
msgid "Runners|This runner is associated with specific projects."
msgstr ""
......
......@@ -468,9 +468,9 @@ RSpec.describe "Admin Runners" do
end
end
describe 'runner page title', :js do
it 'contains the runner id' do
expect(find('.page-title')).to have_content("Runner ##{runner.id}")
describe 'runner header', :js do
it 'contains the runner status, type and id' do
expect(page).to have_content("never contacted shared Runner ##{runner.id} created")
end
end
......
import { GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerHeader from '~/runner/components/runner_header.vue';
import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
import { runnerData } from '../mock_data';
const mockRunner = runnerData.data.runner;
describe('RunnerHeader', () => {
let wrapper;
const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
const findRunnerStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
const findTimeAgo = () => wrapper.findComponent(TimeAgo);
const createComponent = ({ runner = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(RunnerHeader, {
propsData: {
runner: {
...mockRunner,
...runner,
},
},
stubs: {
GlSprintf,
TimeAgo,
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('displays the runner status', () => {
createComponent({
mountFn: mount,
runner: {
status: STATUS_ONLINE,
},
});
expect(findRunnerStatusBadge().text()).toContain(`online`);
});
it('displays the runner type', () => {
createComponent({
mountFn: mount,
runner: {
runnerType: GROUP_TYPE,
},
});
expect(findRunnerTypeBadge().text()).toContain(`group`);
});
it('displays the runner id', () => {
createComponent({
runner: {
id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
},
});
expect(wrapper.text()).toContain(`Runner #99`);
});
it('displays the runner creation time', () => {
createComponent();
expect(wrapper.text()).toMatch(/created .+/);
expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
});
it('does not display runner creation time if createdAt missing', () => {
createComponent({
runner: {
id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
createdAt: null,
},
});
expect(wrapper.text()).toContain(`Runner #99`);
expect(wrapper.text()).not.toMatch(/created .+/);
expect(findTimeAgo().exists()).toBe(false);
});
});
import { GlAlert, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeAlert from '~/runner/components/runner_type_alert.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
describe('RunnerTypeAlert', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerTypeAlert, {
propsData: {
type: INSTANCE_TYPE,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
type | exampleText | anchor
${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'}
${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'}
${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'}
`('When it is an $type level runner', ({ type, exampleText, anchor }) => {
beforeEach(() => {
createComponent({ props: { type } });
});
it('Describes runner type', () => {
expect(wrapper.text()).toMatch(exampleText);
});
it(`Shows an "info" variant`, () => {
expect(findAlert().props('variant')).toBe('info');
});
it(`Links to anchor "${anchor}"`, () => {
expect(findLink().attributes('href')).toBe(`/help/ci/runners/runners_scope${anchor}`);
});
});
describe('When runner type is not correct', () => {
it('Does not render content when type is missing', () => {
createComponent({ props: { type: undefined } });
expect(wrapper.html()).toBe('');
});
it('Validation fails for an incorrect type', () => {
expect(() => {
createComponent({ props: { type: 'NOT_A_TYPE' } });
}).toThrow();
});
});
});
......@@ -127,7 +127,7 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
// Some fields are not submitted
const { ipAddress, runnerType, ...submitted } = mockRunner;
const { ipAddress, runnerType, createdAt, status, ...submitted } = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted);
});
......
......@@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
import RunnerHeader from '~/runner/components/runner_header.vue';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
import RunnerDetailsApp from '~/runner/runner_details/runner_details_app.vue';
import { captureException } from '~/runner/sentry_utils';
......@@ -25,7 +25,7 @@ describe('RunnerDetailsApp', () => {
let wrapper;
let mockRunnerQuery;
const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(RunnerDetailsApp, {
......@@ -40,7 +40,7 @@ describe('RunnerDetailsApp', () => {
return waitForPromises();
};
beforeEach(async () => {
beforeEach(() => {
mockRunnerQuery = jest.fn().mockResolvedValue(runnerData);
});
......@@ -56,15 +56,16 @@ describe('RunnerDetailsApp', () => {
});
it('displays the runner id', async () => {
await createComponentWithApollo();
await createComponentWithApollo({ mountFn: mount });
expect(wrapper.text()).toContain(`Runner #${mockRunnerId}`);
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId} created`);
});
it('displays the runner type', async () => {
it('displays the runner type and status', async () => {
await createComponentWithApollo({ mountFn: mount });
expect(findRunnerTypeBadge().text()).toBe('shared');
expect(findRunnerHeader().text()).toContain(`never contacted`);
expect(findRunnerHeader().text()).toContain(`shared`);
});
describe('When there is an error', () => {
......@@ -73,14 +74,14 @@ describe('RunnerDetailsApp', () => {
await createComponentWithApollo();
});
it('error is reported to sentry', async () => {
it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Network error: Error!'),
component: 'RunnerDetailsApp',
});
});
it('error is shown to the user', async () => {
it('error is shown to the user', () => {
expect(createFlash).toHaveBeenCalled();
});
});
......
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