Commit 311342ae authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '344918-replace-the-runner-active-state-with-a-new-filter-paused' into 'master'

Replace runners 'active' filters with 'paused'

See merge request gitlab-org/gitlab!83865
parents 9a228147 65f2450e
......@@ -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 = {
[PARAM_KEY_STATUS]: [STATUS_NEVER_CONTACTED],
};
return setUrlParams(updatedParams, url, false, true, true);
const status = params[PARAM_KEY_STATUS]?.[0] || null;
switch (status) {
case STATUS_NOT_CONNECTED:
return updateUrlParams(url, {
[PARAM_KEY_STATUS]: [STATUS_NEVER_CONTACTED],
});
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;
}
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