Commit f3cf2fee authored by Miguel Rincon's avatar Miguel Rincon

Update look and feel of runner heading

This change updates the looks and feel of the runner heading according
to new designs.

This update adds the date in which the runner was created to this
heading.

Changelog: changed
parent a2aa5348
<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 ""
......@@ -30738,9 +30741,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