Commit 77de7479 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch '347856-runner-utils-refactor' into 'master'

Refactor runner utils functions

See merge request gitlab-org/gitlab!80592
parents 061e77cb 72e773ef
...@@ -2,31 +2,14 @@ ...@@ -2,31 +2,14 @@
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatNumber, __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { RUNNER_JOB_COUNT_LIMIT } from '../constants'; import { formatJobCount, tableField } from '../utils';
import RunnerActionsCell from './cells/runner_actions_cell.vue'; import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue'; import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue'; import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerTags from './runner_tags.vue'; import RunnerTags from './runner_tags.vue';
const tableField = ({ key, label = '', thClasses = [] }) => {
return {
key,
label,
thClass: [
'gl-bg-transparent!',
'gl-border-b-solid!',
'gl-border-b-gray-100!',
'gl-border-b-1!',
...thClasses,
],
tdAttr: {
'data-testid': `td-${key}`,
},
};
};
export default { export default {
components: { components: {
GlTable, GlTable,
...@@ -54,10 +37,7 @@ export default { ...@@ -54,10 +37,7 @@ export default {
}, },
methods: { methods: {
formatJobCount(jobCount) { formatJobCount(jobCount) {
if (jobCount > RUNNER_JOB_COUNT_LIMIT) { return formatJobCount(jobCount);
return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
}
return formatNumber(jobCount);
}, },
runnerTrAttr(runner) { runnerTrAttr(runner) {
if (runner) { if (runner) {
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
I18N_FETCH_ERROR, I18N_FETCH_ERROR,
RUNNER_DETAILS_PROJECTS_PAGE_SIZE, RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
} from '../constants'; } from '../constants';
import { getPaginationVariables } from '../utils';
import { captureException } from '../sentry_utils'; import { captureException } from '../sentry_utils';
import RunnerAssignedItem from './runner_assigned_item.vue'; import RunnerAssignedItem from './runner_assigned_item.vue';
import RunnerPagination from './runner_pagination.vue'; import RunnerPagination from './runner_pagination.vue';
...@@ -62,19 +63,9 @@ export default { ...@@ -62,19 +63,9 @@ export default {
computed: { computed: {
variables() { variables() {
const { id } = this.runner; const { id } = this.runner;
const { before, after } = this.pagination;
if (before) {
return {
id,
before,
last: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
};
}
return { return {
id, id,
after, ...getPaginationVariables(this.pagination, RUNNER_DETAILS_PROJECTS_PAGE_SIZE),
first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
}; };
}, },
loading() { loading() {
......
...@@ -18,6 +18,7 @@ import { ...@@ -18,6 +18,7 @@ import {
RUNNER_PAGE_SIZE, RUNNER_PAGE_SIZE,
STATUS_NEVER_CONTACTED, STATUS_NEVER_CONTACTED,
} from './constants'; } from './constants';
import { getPaginationVariables } from './utils';
/** /**
* The filters and sorting of the runners are built around * The filters and sorting of the runners are built around
...@@ -184,30 +185,27 @@ export const fromSearchToVariables = ({ ...@@ -184,30 +185,27 @@ export const fromSearchToVariables = ({
sort = null, sort = null,
pagination = {}, pagination = {},
} = {}) => { } = {}) => {
const variables = {}; const filterVariables = {};
const queryObj = filterToQueryObject(processFilters(filters), { const queryObj = filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH, filteredSearchTermKey: PARAM_KEY_SEARCH,
}); });
[variables.status] = queryObj[PARAM_KEY_STATUS] || []; [filterVariables.status] = queryObj[PARAM_KEY_STATUS] || [];
variables.search = queryObj[PARAM_KEY_SEARCH]; filterVariables.search = queryObj[PARAM_KEY_SEARCH];
variables.tagList = queryObj[PARAM_KEY_TAG]; filterVariables.tagList = queryObj[PARAM_KEY_TAG];
if (runnerType) { if (runnerType) {
variables.type = runnerType; filterVariables.type = runnerType;
} }
if (sort) { if (sort) {
variables.sort = sort; filterVariables.sort = sort;
} }
if (pagination.before) { const paginationVariables = getPaginationVariables(pagination, RUNNER_PAGE_SIZE);
variables.before = pagination.before;
variables.last = RUNNER_PAGE_SIZE;
} else {
variables.after = pagination.after;
variables.first = RUNNER_PAGE_SIZE;
}
return variables; return {
...filterVariables,
...paginationVariables,
};
}; };
import { formatNumber } from '~/locale';
import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
import { RUNNER_JOB_COUNT_LIMIT } from './constants';
/**
* Formats a job count, limited to a max number
*
* @param {Number} jobCount
* @returns Formatted string
*/
export const formatJobCount = (jobCount) => {
if (typeof jobCount !== 'number') {
return '';
}
if (jobCount > RUNNER_JOB_COUNT_LIMIT) {
return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
}
return formatNumber(jobCount);
};
/**
* Returns a GlTable fields with a given key and label
*
* @param {Object} options
* @returns Field object to add to GlTable fields
*/
export const tableField = ({ key, label = '', thClasses = [] }) => {
return {
key,
label,
thClass: [DEFAULT_TH_CLASSES, ...thClasses],
tdAttr: {
'data-testid': `td-${key}`,
},
};
};
/**
* Returns variables for a GraphQL query that uses keyset
* pagination.
*
* https://docs.gitlab.com/ee/development/graphql_guide/pagination.html#keyset-pagination
*
* @param {Object} pagination - Contains before, after, page
* @param {Number} pageSize
* @returns Variables
*/
export const getPaginationVariables = (pagination, pageSize = 10) => {
const { before, after } = pagination;
// first + after: Next page
// Get the first N items after item X
if (after) {
return {
after,
first: pageSize,
};
}
// last + before: Prev page
// Get the first N items before item X, when you click on Prev
if (before) {
return {
before,
last: pageSize,
};
}
// first page
// Get the first N items
return { first: pageSize };
};
...@@ -7,7 +7,6 @@ import { createAlert } from '~/flash'; ...@@ -7,7 +7,6 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/runner/components/runner_header.vue'; import RunnerHeader from '~/runner/components/runner_header.vue';
import RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql'; import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
...@@ -30,7 +29,6 @@ describe('AdminRunnerShowApp', () => { ...@@ -30,7 +29,6 @@ describe('AdminRunnerShowApp', () => {
let mockRunnerQuery; let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton); const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton); const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
...@@ -80,8 +78,7 @@ describe('AdminRunnerShowApp', () => { ...@@ -80,8 +78,7 @@ describe('AdminRunnerShowApp', () => {
}); });
it('shows basic runner details', async () => { it('shows basic runner details', async () => {
const expected = `Details const expected = `Description Instance runner
Description Instance runner
Last contact Never contacted Last contact Never contacted
Version 1.0.0 Version 1.0.0
IP Address 127.0.0.1 IP Address 127.0.0.1
...@@ -89,7 +86,7 @@ describe('AdminRunnerShowApp', () => { ...@@ -89,7 +86,7 @@ describe('AdminRunnerShowApp', () => {
Maximum job timeout None Maximum job timeout None
Tags None`.replace(/\s+/g, ' '); Tags None`.replace(/\s+/g, ' ');
expect(findRunnerDetails().text()).toMatchInterpolatedText(expected); expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
}); });
describe('when runner cannot be updated', () => { describe('when runner cannot be updated', () => {
......
import { GlSprintf, GlIntersperse } from '@gitlab/ui'; import { GlSprintf, GlIntersperse } from '@gitlab/ui';
import { createWrapper, ErrorWrapper } from '@vue/test-utils'; import { createWrapper, ErrorWrapper } from '@vue/test-utils';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants'; import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
...@@ -8,6 +8,8 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner ...@@ -8,6 +8,8 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner
import RunnerDetails from '~/runner/components/runner_details.vue'; import RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerDetail from '~/runner/components/runner_detail.vue'; import RunnerDetail from '~/runner/components/runner_detail.vue';
import RunnerGroups from '~/runner/components/runner_groups.vue'; import RunnerGroups from '~/runner/components/runner_groups.vue';
import RunnerTags from '~/runner/components/runner_tags.vue';
import RunnerTag from '~/runner/components/runner_tag.vue';
import { runnerData, runnerWithGroupData } from '../mock_data'; import { runnerData, runnerWithGroupData } from '../mock_data';
...@@ -37,16 +39,14 @@ describe('RunnerDetails', () => { ...@@ -37,16 +39,14 @@ describe('RunnerDetails', () => {
const findDetailGroups = () => wrapper.findComponent(RunnerGroups); const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
wrapper = mountFn(RunnerDetails, { wrapper = mountFn(RunnerDetails, {
propsData: { propsData: {
...props, ...props,
}, },
stubs: { stubs: {
GlIntersperse,
GlSprintf,
TimeAgo,
RunnerDetail, RunnerDetail,
...stubs,
}, },
}); });
}; };
...@@ -65,76 +65,85 @@ describe('RunnerDetails', () => { ...@@ -65,76 +65,85 @@ describe('RunnerDetails', () => {
expect(wrapper.text()).toBe(''); expect(wrapper.text()).toBe('');
}); });
describe.each` describe('Details tab', () => {
field | runner | expectedValue describe.each`
${'Description'} | ${{ description: 'My runner' }} | ${'My runner'} field | runner | expectedValue
${'Description'} | ${{ description: null }} | ${'None'} ${'Description'} | ${{ description: 'My runner' }} | ${'My runner'}
${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'} ${'Description'} | ${{ description: null }} | ${'None'}
${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'} ${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'}
${'Version'} | ${{ version: '12.3' }} | ${'12.3'} ${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'}
${'Version'} | ${{ version: null }} | ${'None'} ${'Version'} | ${{ version: '12.3' }} | ${'12.3'}
${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'} ${'Version'} | ${{ version: null }} | ${'None'}
${'IP Address'} | ${{ ipAddress: null }} | ${'None'} ${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'} ${'IP Address'} | ${{ ipAddress: null }} | ${'None'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'} ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'} ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'} ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'}
${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'} ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'} ${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'} ${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'} ${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'}
`('"$field" field', ({ field, runner, expectedValue }) => { ${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'}
beforeEach(() => { `('"$field" field', ({ field, runner, expectedValue }) => {
createComponent({ beforeEach(() => {
props: { createComponent({
runner: { props: {
...mockRunner, runner: {
...runner, ...mockRunner,
...runner,
},
}, },
}, stubs: {
GlIntersperse,
GlSprintf,
TimeAgo,
},
});
}); });
});
it(`displays expected value "${expectedValue}"`, () => { it(`displays expected value "${expectedValue}"`, () => {
expect(findDd(field).text()).toBe(expectedValue); expect(findDd(field).text()).toBe(expectedValue);
});
}); });
});
describe('"Tags" field', () => { describe('"Tags" field', () => {
it('displays expected value "tag-1 tag-2"', () => { const stubs = { RunnerTags, RunnerTag };
createComponent({
props: {
runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] },
},
mountFn: mountExtended,
});
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2'); it('displays expected value "tag-1 tag-2"', () => {
}); createComponent({
props: {
runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] },
},
stubs,
});
it('displays "None" when runner has no tags', () => { expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2');
createComponent({
props: {
runner: { ...mockRunner, tagList: [] },
},
mountFn: mountExtended,
}); });
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None'); it('displays "None" when runner has no tags', () => {
}); createComponent({
}); props: {
runner: { ...mockRunner, tagList: [] },
},
stubs,
});
describe('Group runners', () => { expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None');
beforeEach(() => {
createComponent({
props: {
runner: mockGroupRunner,
},
}); });
}); });
it('Shows a group runner details', () => { describe('Group runners', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner); beforeEach(() => {
createComponent({
props: {
runner: mockGroupRunner,
},
});
});
it('Shows a group runner details', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
});
}); });
}); });
}); });
import { formatJobCount, tableField, getPaginationVariables } from '~/runner/utils';
describe('~/runner/utils', () => {
describe('formatJobCount', () => {
it('formats a number', () => {
expect(formatJobCount(1)).toBe('1');
expect(formatJobCount(99)).toBe('99');
});
it('formats a large count', () => {
expect(formatJobCount(1000)).toBe('1,000');
expect(formatJobCount(1001)).toBe('1,000+');
});
it('returns an empty string for non-numeric values', () => {
expect(formatJobCount(undefined)).toBe('');
expect(formatJobCount(null)).toBe('');
expect(formatJobCount('number')).toBe('');
});
});
describe('tableField', () => {
it('a field with options', () => {
expect(tableField({ key: 'name' })).toEqual({
key: 'name',
label: '',
tdAttr: { 'data-testid': 'td-name' },
thClass: expect.any(Array),
});
});
it('a field with a label', () => {
const label = 'A field name';
expect(tableField({ key: 'name', label })).toMatchObject({
label,
});
});
it('a field with custom classes', () => {
const mockClasses = ['foo', 'bar'];
expect(tableField({ thClasses: mockClasses })).toMatchObject({
thClass: expect.arrayContaining(mockClasses),
});
});
});
describe('getPaginationVariables', () => {
const after = 'AFTER_CURSOR';
const before = 'BEFORE_CURSOR';
it.each`
case | pagination | pageSize | variables
${'next page'} | ${{ after }} | ${undefined} | ${{ after, first: 10 }}
${'prev page'} | ${{ before }} | ${undefined} | ${{ before, last: 10 }}
${'first page'} | ${{}} | ${undefined} | ${{ first: 10 }}
${'next page with N items'} | ${{ after }} | ${20} | ${{ after, first: 20 }}
${'prev page with N items'} | ${{ before }} | ${20} | ${{ before, last: 20 }}
${'first page with N items'} | ${{}} | ${20} | ${{ first: 20 }}
`('navigates to $case', ({ pagination, pageSize, variables }) => {
expect(getPaginationVariables(pagination, pageSize)).toEqual(variables);
});
});
});
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