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 { ...@@ -9,4 +9,6 @@ fragment RunnerDetailsShared on CiRunner {
description description
maximumTimeout maximumTimeout
tagList tagList
createdAt
status(legacyMode: null)
} }
...@@ -2,19 +2,16 @@ ...@@ -2,19 +2,16 @@
import createFlash from '~/flash'; import createFlash from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import { sprintf } from '~/locale'; import RunnerHeader from '../components/runner_header.vue';
import RunnerTypeAlert from '../components/runner_type_alert.vue';
import RunnerTypeBadge from '../components/runner_type_badge.vue';
import RunnerUpdateForm from '../components/runner_update_form.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 getRunnerQuery from '../graphql/get_runner.query.graphql';
import { captureException } from '../sentry_utils'; import { captureException } from '../sentry_utils';
export default { export default {
name: 'RunnerDetailsApp', name: 'RunnerDetailsApp',
components: { components: {
RunnerTypeAlert, RunnerHeader,
RunnerTypeBadge,
RunnerUpdateForm, RunnerUpdateForm,
}, },
props: { props: {
...@@ -43,11 +40,6 @@ export default { ...@@ -43,11 +40,6 @@ export default {
}, },
}, },
}, },
computed: {
pageTitle() {
return sprintf(I18N_DETAILS_TITLE, { runner_id: this.runnerId });
},
},
errorCaptured(error) { errorCaptured(error) {
this.reportToSentry(error); this.reportToSentry(error);
}, },
...@@ -60,12 +52,7 @@ export default { ...@@ -60,12 +52,7 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<h2 class="page-title"> <runner-header v-if="runner" :runner="runner" />
{{ pageTitle }} <runner-type-badge v-if="runner" :type="runner.runnerType" />
</h2>
<runner-type-alert v-if="runner" :type="runner.runnerType" />
<runner-update-form :runner="runner" class="gl-my-5" /> <runner-update-form :runner="runner" class="gl-my-5" />
</div> </div>
</template> </template>
...@@ -876,6 +876,9 @@ msgstr "" ...@@ -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." 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 "" msgstr ""
msgid "%{runner} created %{timeago}"
msgstr ""
msgid "%{scope} results for term '%{term}'" msgid "%{scope} results for term '%{term}'"
msgstr "" msgstr ""
...@@ -30738,9 +30741,6 @@ msgstr "" ...@@ -30738,9 +30741,6 @@ msgstr ""
msgid "Runners|This runner has never contacted this instance" msgid "Runners|This runner has never contacted this instance"
msgstr "" msgstr ""
msgid "Runners|This runner is associated with one or more projects."
msgstr ""
msgid "Runners|This runner is associated with specific projects." msgid "Runners|This runner is associated with specific projects."
msgstr "" msgstr ""
......
...@@ -468,9 +468,9 @@ RSpec.describe "Admin Runners" do ...@@ -468,9 +468,9 @@ RSpec.describe "Admin Runners" do
end end
end end
describe 'runner page title', :js do describe 'runner header', :js do
it 'contains the runner id' do it 'contains the runner status, type and id' do
expect(find('.page-title')).to have_content("Runner ##{runner.id}") expect(page).to have_content("never contacted shared Runner ##{runner.id} created")
end end
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', () => { ...@@ -127,7 +127,7 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait(); await submitFormAndWait();
// Some fields are not submitted // Some fields are not submitted
const { ipAddress, runnerType, ...submitted } = mockRunner; const { ipAddress, runnerType, createdAt, status, ...submitted } = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted); expectToHaveSubmittedRunnerContaining(submitted);
}); });
......
...@@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; 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 getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
import RunnerDetailsApp from '~/runner/runner_details/runner_details_app.vue'; import RunnerDetailsApp from '~/runner/runner_details/runner_details_app.vue';
import { captureException } from '~/runner/sentry_utils'; import { captureException } from '~/runner/sentry_utils';
...@@ -25,7 +25,7 @@ describe('RunnerDetailsApp', () => { ...@@ -25,7 +25,7 @@ describe('RunnerDetailsApp', () => {
let wrapper; let wrapper;
let mockRunnerQuery; let mockRunnerQuery;
const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge); const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(RunnerDetailsApp, { wrapper = mountFn(RunnerDetailsApp, {
...@@ -40,7 +40,7 @@ describe('RunnerDetailsApp', () => { ...@@ -40,7 +40,7 @@ describe('RunnerDetailsApp', () => {
return waitForPromises(); return waitForPromises();
}; };
beforeEach(async () => { beforeEach(() => {
mockRunnerQuery = jest.fn().mockResolvedValue(runnerData); mockRunnerQuery = jest.fn().mockResolvedValue(runnerData);
}); });
...@@ -56,15 +56,16 @@ describe('RunnerDetailsApp', () => { ...@@ -56,15 +56,16 @@ describe('RunnerDetailsApp', () => {
}); });
it('displays the runner id', async () => { 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 }); 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', () => { describe('When there is an error', () => {
...@@ -73,14 +74,14 @@ describe('RunnerDetailsApp', () => { ...@@ -73,14 +74,14 @@ describe('RunnerDetailsApp', () => {
await createComponentWithApollo(); await createComponentWithApollo();
}); });
it('error is reported to sentry', async () => { it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({ expect(captureException).toHaveBeenCalledWith({
error: new Error('Network error: Error!'), error: new Error('Network error: Error!'),
component: 'RunnerDetailsApp', component: 'RunnerDetailsApp',
}); });
}); });
it('error is shown to the user', async () => { it('error is shown to the user', () => {
expect(createFlash).toHaveBeenCalled(); 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