Commit 65f2450e authored by Miguel Rincon's avatar Miguel Rincon

Replace runners 'active' filters with 'paused'

This change removes the active/paused values from the status filter
to search for a runner and replaces them with a new "Paused" filter
with values Yes or No.

This new filter is an equivalent way to make this search, users that
visit URLs that use the deprecated status filters are redirected.

Changelog: changed
parent f64be48c
......@@ -16,6 +16,7 @@ import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
import {
......@@ -178,6 +179,7 @@ export default {
},
searchTokens() {
return [
pausedTokenConfig,
statusTokenConfig,
{
...tagTokenConfig,
......
import { __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { PARAM_KEY_PAUSED } from '../../constants';
const options = [
{ value: 'true', title: __('Yes') },
{ value: 'false', title: __('No') },
];
export const pausedTokenConfig = {
icon: 'pause',
title: s__('Runners|Paused'),
type: PARAM_KEY_PAUSED,
token: BaseToken,
unique: true,
options: options.map(({ value, title }) => ({
value,
// Replace whitespace with a special character to avoid
// splitting this value.
// Replacing in each option, as translations may also
// contain spaces!
// see: https://gitlab.com/gitlab-org/gitlab/-/issues/344142
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
title: title.replace(' ', '\u00a0'),
})),
operators: OPERATOR_IS_ONLY,
};
......@@ -2,8 +2,6 @@ import { __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import {
STATUS_ACTIVE,
STATUS_PAUSED,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_NEVER_CONTACTED,
......@@ -12,8 +10,6 @@ import {
} from '../../constants';
const options = [
{ value: STATUS_ACTIVE, title: s__('Runners|Active') },
{ value: STATUS_PAUSED, title: s__('Runners|Paused') },
{ value: STATUS_ONLINE, title: s__('Runners|Online') },
{ value: STATUS_OFFLINE, title: s__('Runners|Offline') },
{ value: STATUS_NEVER_CONTACTED, title: s__('Runners|Never contacted') },
......
......@@ -95,6 +95,7 @@ export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
// - GlFilteredSearch tokens type
export const PARAM_KEY_STATUS = 'status';
export const PARAM_KEY_PAUSED = 'paused';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
export const PARAM_KEY_TAG = 'tag';
export const PARAM_KEY_SEARCH = 'search';
......@@ -112,9 +113,6 @@ export const PROJECT_TYPE = 'PROJECT_TYPE';
// CiRunnerStatus
export const STATUS_ACTIVE = 'ACTIVE';
export const STATUS_PAUSED = 'PAUSED';
export const STATUS_ONLINE = 'ONLINE';
export const STATUS_NEVER_CONTACTED = 'NEVER_CONTACTED';
export const STATUS_OFFLINE = 'OFFLINE';
......
......@@ -6,6 +6,7 @@ query getRunners(
$after: String
$first: Int
$last: Int
$paused: Boolean
$status: CiRunnerStatus
$type: CiRunnerType
$tagList: [String!]
......@@ -17,6 +18,7 @@ query getRunners(
after: $after
first: $first
last: $last
paused: $paused
status: $status
type: $type
tagList: $tagList
......
query getRunnersCount(
$paused: Boolean
$status: CiRunnerStatus
$type: CiRunnerType
$tagList: [String!]
$search: String
) {
runners(status: $status, type: $type, tagList: $tagList, search: $search) {
runners(paused: $paused, status: $status, type: $type, tagList: $tagList, search: $search) {
count
}
}
......@@ -7,6 +7,7 @@ query getGroupRunners(
$after: String
$first: Int
$last: Int
$paused: Boolean
$status: CiRunnerStatus
$type: CiRunnerType
$search: String
......@@ -20,6 +21,7 @@ query getGroupRunners(
after: $after
first: $first
last: $last
paused: $paused
status: $status
type: $type
search: $search
......
query getGroupRunnersCount(
$groupFullPath: ID!
$paused: Boolean
$status: CiRunnerStatus
$type: CiRunnerType
$tagList: [String!]
......@@ -9,6 +10,7 @@ query getGroupRunnersCount(
id # Apollo required
runners(
membership: DESCENDANTS
paused: $paused
status: $status
type: $type
tagList: $tagList
......
......@@ -14,6 +14,7 @@ import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import {
GROUP_FILTERED_SEARCH_NAMESPACE,
......@@ -189,7 +190,7 @@ export default {
return !this.runnersLoading && !this.runners.items.length;
},
searchTokens() {
return [statusTokenConfig];
return [pausedTokenConfig, statusTokenConfig];
},
filteredSearchNamespace() {
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
......
......@@ -5,7 +5,9 @@ import {
urlQueryToFilter,
prepareTokens,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import {
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
......@@ -83,6 +85,19 @@ const getPaginationFromParams = (params) => {
// Outdated URL parameters
const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
const STATUS_ACTIVE = 'ACTIVE';
const STATUS_PAUSED = 'PAUSED';
/**
* Replaces params into a URL
*
* @param {String} url - Original URL
* @param {Object} params - Query parameters to update
* @returns Updated URL
*/
const updateUrlParams = (url, params = {}) => {
return setUrlParams(params, url, false, true, true);
};
/**
* Returns an updated URL for old (or deprecated) admin runner URLs.
......@@ -98,14 +113,26 @@ export const updateOutdatedUrl = (url = window.location.href) => {
const params = queryToObject(query, { gatherArrays: true });
const runnerType = params[PARAM_KEY_STATUS]?.[0] || null;
if (runnerType === STATUS_NOT_CONNECTED) {
const updatedParams = {
const status = params[PARAM_KEY_STATUS]?.[0] || null;
switch (status) {
case STATUS_NOT_CONNECTED:
return updateUrlParams(url, {
[PARAM_KEY_STATUS]: [STATUS_NEVER_CONTACTED],
};
return setUrlParams(updatedParams, url, false, true, true);
}
});
case STATUS_ACTIVE:
return updateUrlParams(url, {
[PARAM_KEY_PAUSED]: ['false'],
[PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop!
});
case STATUS_PAUSED:
return updateUrlParams(url, {
[PARAM_KEY_PAUSED]: ['true'],
[PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop!
});
default:
return null;
}
};
/**
......@@ -121,7 +148,7 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
runnerType,
filters: prepareTokens(
urlQueryToFilter(query, {
filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_TAG],
filterNamesAllowList: [PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG],
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
),
......@@ -195,6 +222,12 @@ export const fromSearchToVariables = ({
filterVariables.search = queryObj[PARAM_KEY_SEARCH];
filterVariables.tagList = queryObj[PARAM_KEY_TAG];
if (queryObj[PARAM_KEY_PAUSED]) {
filterVariables.paused = parseBoolean(queryObj[PARAM_KEY_PAUSED]);
} else {
filterVariables.paused = undefined;
}
if (runnerType) {
filterVariables.type = runnerType;
}
......
......@@ -154,35 +154,69 @@ RSpec.describe "Admin Runners" do
end
end
describe 'filter by paused' do
before do
create(:ci_runner, :instance, description: 'runner-active')
create(:ci_runner, :instance, description: 'runner-paused', active: false)
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-active'
expect(page).to have_content 'runner-paused'
end
it 'shows paused runners' do
input_filtered_search_filter_is_only('Paused', 'Yes')
expect(page).to have_link('All 1')
expect(page).not_to have_content 'runner-active'
expect(page).to have_content 'runner-paused'
end
it 'shows active runners' do
input_filtered_search_filter_is_only('Paused', 'No')
expect(page).to have_link('All 1')
expect(page).to have_content 'runner-active'
expect(page).not_to have_content 'runner-paused'
end
end
describe 'filter by status' do
let!(:never_contacted) { create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil) }
before do
create(:ci_runner, :instance, description: 'runner-1', contacted_at: Time.zone.now)
create(:ci_runner, :instance, description: 'runner-2', contacted_at: Time.zone.now)
create(:ci_runner, :instance, description: 'runner-paused', active: false, contacted_at: Time.zone.now)
create(:ci_runner, :instance, description: 'runner-offline', contacted_at: 1.week.ago)
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 4')
expect(page).to have_content 'runner-1'
expect(page).to have_content 'runner-2'
expect(page).to have_content 'runner-paused'
expect(page).to have_content 'runner-offline'
expect(page).to have_content 'runner-never-contacted'
expect(page).to have_link('All 4')
end
it 'shows correct runner when status matches' do
input_filtered_search_filter_is_only('Status', 'Active')
input_filtered_search_filter_is_only('Status', 'Online')
expect(page).to have_link('All 3')
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-1'
expect(page).to have_content 'runner-2'
expect(page).to have_content 'runner-never-contacted'
expect(page).not_to have_content 'runner-paused'
expect(page).not_to have_content 'runner-offline'
expect(page).not_to have_content 'runner-never-contacted'
end
it 'shows no runner when status does not match' do
......@@ -194,15 +228,15 @@ RSpec.describe "Admin Runners" do
end
it 'shows correct runner when status is selected and search term is entered' do
input_filtered_search_filter_is_only('Status', 'Active')
input_filtered_search_filter_is_only('Status', 'Online')
input_filtered_search_keys('runner-1')
expect(page).to have_link('All 1')
expect(page).to have_content 'runner-1'
expect(page).not_to have_content 'runner-2'
expect(page).not_to have_content 'runner-offline'
expect(page).not_to have_content 'runner-never-contacted'
expect(page).not_to have_content 'runner-paused'
end
it 'shows correct runner when status filter is entered' do
......@@ -308,7 +342,7 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
input_filtered_search_filter_is_only('Status', 'Active')
input_filtered_search_filter_is_only('Paused', 'No')
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
......@@ -427,6 +461,18 @@ RSpec.describe "Admin Runners" do
expect(page).to have_current_path(admin_runners_path('status[]': 'NEVER_CONTACTED') )
end
it 'updates ACTIVE runner status to paused=false' do
visit admin_runners_path('status[]': 'ACTIVE')
expect(page).to have_current_path(admin_runners_path('paused[]': 'false') )
end
it 'updates PAUSED runner status to paused=true' do
visit admin_runners_path('status[]': 'PAUSED')
expect(page).to have_current_path(admin_runners_path('paused[]': 'true') )
end
end
describe 'runners registration' do
......
......@@ -31,9 +31,10 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
STATUS_ACTIVE,
STATUS_ONLINE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import adminRunnersQuery from '~/runner/graphql/list/admin_runners.query.graphql';
......@@ -242,6 +243,10 @@ describe('AdminRunnersApp', () => {
createComponent({ mountFn: mountExtended });
expect(findFilteredSearch().props('tokens')).toEqual([
expect.objectContaining({
type: PARAM_KEY_PAUSED,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
......@@ -297,7 +302,7 @@ describe('AdminRunnersApp', () => {
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
createComponent();
await waitForPromises();
......@@ -307,7 +312,7 @@ describe('AdminRunnersApp', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
filters: [
{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'tag', value: { data: 'tag1', operator: '=' } },
],
sort: 'CREATED_DESC',
......@@ -317,7 +322,7 @@ describe('AdminRunnersApp', () => {
it('requests the runners with filter parameters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: STATUS_ACTIVE,
status: STATUS_ONLINE,
type: INSTANCE_TYPE,
tagList: ['tag1'],
sort: DEFAULT_SORT,
......@@ -330,7 +335,7 @@ describe('AdminRunnersApp', () => {
beforeEach(() => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
});
......@@ -338,13 +343,13 @@ describe('AdminRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
url: 'http://test.host/admin/runners?status[]=ACTIVE&sort=CREATED_ASC',
url: 'http://test.host/admin/runners?status[]=ONLINE&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: STATUS_ACTIVE,
status: STATUS_ONLINE,
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
......
......@@ -4,7 +4,7 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_
import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
import TagToken from '~/runner/components/search_tokens/tag_token.vue';
import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config';
import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ACTIVE, INSTANCE_TYPE } from '~/runner/constants';
import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ONLINE, INSTANCE_TYPE } from '~/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
......@@ -18,7 +18,7 @@ describe('RunnerList', () => {
const mockDefaultSort = 'CREATED_DESC';
const mockOtherSort = 'CONTACTED_DESC';
const mockFilters = [
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } },
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'filtered-search-term', value: { data: '' } },
];
......
......@@ -28,8 +28,9 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
STATUS_ACTIVE,
STATUS_ONLINE,
RUNNER_PAGE_SIZE,
I18N_EDIT,
} from '~/runner/constants';
......@@ -188,13 +189,16 @@ describe('GroupRunnersApp', () => {
const tokens = findFilteredSearch().props('tokens');
expect(tokens).toHaveLength(1);
expect(tokens[0]).toEqual(
expect(tokens).toEqual([
expect.objectContaining({
type: PARAM_KEY_PAUSED,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
);
]);
});
describe('Single runner row', () => {
......@@ -253,7 +257,7 @@ describe('GroupRunnersApp', () => {
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}`);
createComponent();
await waitForPromises();
......@@ -262,7 +266,7 @@ describe('GroupRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
filters: [{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } }],
filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
sort: 'CREATED_DESC',
pagination: { page: 1 },
});
......@@ -271,7 +275,7 @@ describe('GroupRunnersApp', () => {
it('requests the runners with filter parameters', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ACTIVE,
status: STATUS_ONLINE,
type: INSTANCE_TYPE,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
......@@ -283,7 +287,7 @@ describe('GroupRunnersApp', () => {
beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
......@@ -293,14 +297,14 @@ describe('GroupRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
url: 'http://test.host/groups/group1/-/runners?status[]=ACTIVE&sort=CREATED_ASC',
url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ACTIVE,
status: STATUS_ONLINE,
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
......
......@@ -181,6 +181,28 @@ describe('search_params.js', () => {
first: RUNNER_PAGE_SIZE,
},
},
{
name: 'paused runners',
urlQuery: '?paused[]=true',
search: {
runnerType: null,
filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC',
},
graphqlVariables: { paused: true, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
},
{
name: 'active runners',
urlQuery: '?paused[]=false',
search: {
runnerType: null,
filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC',
},
graphqlVariables: { paused: false, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
},
];
describe('searchValidator', () => {
......@@ -197,14 +219,18 @@ describe('search_params.js', () => {
expect(updateOutdatedUrl('http://test.host/?a=b')).toBe(null);
});
it('returns updated url for updating NOT_CONNECTED to NEVER_CONTACTED', () => {
expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED')).toBe(
'http://test.host/admin/runners?status[]=NEVER_CONTACTED',
);
it.each`
query | updatedQuery
${'status[]=NOT_CONNECTED'} | ${'status[]=NEVER_CONTACTED'}
${'status[]=NOT_CONNECTED&a=b'} | ${'status[]=NEVER_CONTACTED&a=b'}
${'status[]=ACTIVE'} | ${'paused[]=false'}
${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'}
${'status[]=ACTIVE'} | ${'paused[]=false'}
${'status[]=PAUSED'} | ${'paused[]=true'}
`('updates "$query" to "$updatedQuery"', ({ query, updatedQuery }) => {
const mockUrl = 'http://test.host/admin/runners?';
expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED&a=b')).toBe(
'http://test.host/admin/runners?status[]=NEVER_CONTACTED&a=b',
);
expect(updateOutdatedUrl(`${mockUrl}${query}`)).toBe(`${mockUrl}${updatedQuery}`);
});
});
......
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