Commit 82e179e8 authored by Miguel Rincon's avatar Miguel Rincon

Add stale runners filters and badge

This change allows administration users to identify stale runners in
their instance and filter them in the search.

Changelog: added
parent 733ba47f
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import RunnerContactedStateBadge from '../runner_contacted_state_badge.vue';
import RunnerStatusBadge from '../runner_status_badge.vue';
import RunnerPausedBadge from '../runner_paused_badge.vue';
import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants';
export default {
components: {
RunnerContactedStateBadge,
RunnerStatusBadge,
RunnerPausedBadge,
},
directives: {
......@@ -25,16 +23,12 @@ export default {
return !this.runner.active;
},
},
i18n: {
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_PAUSED_RUNNER_DESCRIPTION,
},
};
</script>
<template>
<div>
<runner-contacted-state-badge :runner="runner" size="sm" />
<runner-status-badge :runner="runner" size="sm" />
<runner-paused-badge v-if="paused" size="sm" />
</div>
</template>
<script>
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import { getTimeago } from '~/lib/utils/datetime_utility';
import {
I18N_ONLINE_RUNNER_DESCRIPTION,
I18N_OFFLINE_RUNNER_DESCRIPTION,
I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION,
I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION,
I18N_STALE_RUNNER_DESCRIPTION,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_NOT_CONNECTED,
STATUS_OFFLINE,
STATUS_STALE,
} from '../constants';
export default {
......@@ -29,31 +31,38 @@ export default {
if (this.runner.contactedAt) {
return getTimeago().format(this.runner.contactedAt);
}
return null;
// Prevent "just now" from being rendered, in case data is missing.
return __('n/a');
},
badge() {
switch (this.runner.status) {
switch (this.runner?.status) {
case STATUS_ONLINE:
return {
variant: 'success',
label: s__('Runners|online'),
tooltip: sprintf(I18N_ONLINE_RUNNER_DESCRIPTION, {
tooltip: sprintf(I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION, {
timeAgo: this.contactedAtTimeAgo,
}),
};
case STATUS_NOT_CONNECTED:
return {
variant: 'muted',
label: s__('Runners|not connected'),
tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
};
case STATUS_OFFLINE:
return {
variant: 'muted',
label: s__('Runners|offline'),
tooltip: sprintf(I18N_OFFLINE_RUNNER_DESCRIPTION, {
tooltip: sprintf(I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION, {
timeAgo: this.contactedAtTimeAgo,
}),
};
case STATUS_NOT_CONNECTED:
case STATUS_STALE:
return {
variant: 'muted',
label: s__('Runners|not connected'),
tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
variant: 'warning',
label: s__('Runners|stale'),
tooltip: I18N_STALE_RUNNER_DESCRIPTION,
};
default:
return null;
......
......@@ -7,6 +7,7 @@ import {
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_NOT_CONNECTED,
STATUS_STALE,
PARAM_KEY_STATUS,
} from '../../constants';
......@@ -16,6 +17,7 @@ const options = [
{ value: STATUS_ONLINE, title: s__('Runners|Online') },
{ value: STATUS_OFFLINE, title: s__('Runners|Offline') },
{ value: STATUS_NOT_CONNECTED, title: s__('Runners|Not connected') },
{ value: STATUS_STALE, title: s__('Runners|Stale') },
];
export const statusTokenConfig = {
......
......@@ -14,15 +14,18 @@ export const I18N_GROUP_RUNNER_DESCRIPTION = s__(
export const I18N_PROJECT_RUNNER_DESCRIPTION = s__('Runners|Associated with one or more projects');
// Status
export const I18N_ONLINE_RUNNER_DESCRIPTION = s__(
export const I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION = s__(
'Runners|Runner is online; last contact was %{timeAgo}',
);
export const I18N_OFFLINE_RUNNER_DESCRIPTION = s__(
'Runners|No recent contact from this runner; last contact was %{timeAgo}',
);
export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__(
'Runners|This runner has never connected to this instance',
);
export const I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION = s__(
'Runners|No recent contact from this runner; last contact was %{timeAgo}',
);
export const I18N_STALE_RUNNER_DESCRIPTION = s__(
'Runners|No contact from this runner in over 3 months',
);
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
......@@ -54,9 +57,11 @@ export const PROJECT_TYPE = 'PROJECT_TYPE';
export const STATUS_ACTIVE = 'ACTIVE';
export const STATUS_PAUSED = 'PAUSED';
export const STATUS_ONLINE = 'ONLINE';
export const STATUS_OFFLINE = 'OFFLINE';
export const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
export const STATUS_OFFLINE = 'OFFLINE';
export const STATUS_STALE = 'STALE';
// CiRunnerAccessLevel
......
......@@ -10,5 +10,5 @@ fragment RunnerNode on CiRunner {
locked
tagList
contactedAt
status
status(legacyMode: null)
}
......@@ -30106,6 +30106,9 @@ msgstr ""
msgid "Runners|New runner, has not connected yet"
msgstr ""
msgid "Runners|No contact from this runner in over 3 months"
msgstr ""
msgid "Runners|No recent contact from this runner; last contact was %{timeAgo}"
msgstr ""
......@@ -30211,6 +30214,9 @@ msgstr ""
msgid "Runners|Something went wrong while fetching the tags suggestions"
msgstr ""
msgid "Runners|Stale"
msgstr ""
msgid "Runners|Status"
msgstr ""
......@@ -30307,6 +30313,9 @@ msgstr ""
msgid "Runners|specific"
msgstr ""
msgid "Runners|stale"
msgstr ""
msgid "Running"
msgstr ""
......
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerContactedStateBadge from '~/runner/components/runner_contacted_state_badge.vue';
import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_NOT_CONNECTED } from '~/runner/constants';
import {
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
STATUS_NOT_CONNECTED,
} from '~/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
......@@ -10,14 +15,14 @@ describe('RunnerTypeBadge', () => {
const findBadge = () => wrapper.findComponent(GlBadge);
const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
const createComponent = ({ runner = {} } = {}) => {
wrapper = shallowMount(RunnerContactedStateBadge, {
const createComponent = (props = {}) => {
wrapper = shallowMount(RunnerStatusBadge, {
propsData: {
runner: {
contactedAt: '2021-01-01T00:00:00Z',
contactedAt: '2020-12-31T23:59:00Z',
status: STATUS_ONLINE,
...runner,
},
...props,
},
directives: {
GlTooltip: createMockDirective(),
......@@ -27,6 +32,7 @@ describe('RunnerTypeBadge', () => {
beforeEach(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date('2021-01-01T00:00:00Z'));
});
afterEach(() => {
......@@ -36,8 +42,6 @@ describe('RunnerTypeBadge', () => {
});
it('renders online state', () => {
jest.setSystemTime(new Date('2021-01-01T00:01:00Z'));
createComponent();
expect(wrapper.text()).toBe('online');
......@@ -45,11 +49,23 @@ describe('RunnerTypeBadge', () => {
expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago');
});
it('renders offline state', () => {
jest.setSystemTime(new Date('2021-01-02T00:00:00Z'));
it('renders not connected state', () => {
createComponent({
runner: {
contactedAt: null,
status: STATUS_NOT_CONNECTED,
},
});
expect(wrapper.text()).toBe('not connected');
expect(findBadge().props('variant')).toBe('muted');
expect(getTooltip().value).toMatch('This runner has never connected');
});
it('renders offline state', () => {
createComponent({
runner: {
contactedAt: '2020-12-31T00:00:00Z',
status: STATUS_OFFLINE,
},
});
......@@ -61,26 +77,40 @@ describe('RunnerTypeBadge', () => {
);
});
it('renders not connected state', () => {
it('renders stale state', () => {
createComponent({
runner: {
contactedAt: null,
status: STATUS_NOT_CONNECTED,
contactedAt: '2020-01-01T00:00:00Z',
status: STATUS_STALE,
},
});
expect(wrapper.text()).toBe('not connected');
expect(findBadge().props('variant')).toBe('muted');
expect(getTooltip().value).toMatch('This runner has never connected');
expect(wrapper.text()).toBe('stale');
expect(findBadge().props('variant')).toBe('warning');
expect(getTooltip().value).toBe('No contact from this runner in over 3 months');
});
it('does not fail when data is missing', () => {
createComponent({
runner: {
status: null,
},
describe('does not fail when data is missing', () => {
it('contacted_at is missing', () => {
createComponent({
runner: {
contactedAt: null,
status: STATUS_ONLINE,
},
});
expect(wrapper.text()).toBe('online');
expect(getTooltip().value).toBe('Runner is online; last contact was n/a');
});
expect(wrapper.text()).toBe('');
it('status is missing', () => {
createComponent({
runner: {
status: null,
},
});
expect(wrapper.text()).toBe('');
});
});
});
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