Commit 2f27901f authored by Miguel Rincon's avatar Miguel Rincon

Filter runner type via tabs

This change allows users to filter their runners by type using
tabs directly.

Changelog: changed
parent f02c40eb
......@@ -10,10 +10,10 @@ import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vu
import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
import { typeTokenConfig } from '../components/search_tokens/type_token_config';
import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
import {
......@@ -32,6 +32,7 @@ export default {
RunnerList,
RunnerName,
RunnerPagination,
RunnerTypeTabs,
},
props: {
activeRunnersCount: {
......@@ -94,7 +95,6 @@ export default {
searchTokens() {
return [
statusTokenConfig,
typeTokenConfig,
{
...tagTokenConfig,
recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
......@@ -128,7 +128,13 @@ export default {
</script>
<template>
<div>
<div class="gl-py-3 gl-display-flex">
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
v-model="search"
content-class="gl-display-none"
nav-class="gl-border-none!"
/>
<registration-dropdown
class="gl-ml-auto"
:registration-token="registrationToken"
......
......@@ -2,6 +2,7 @@
import { cloneDeep } from 'lodash';
import { __ } from '~/locale';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { searchValidator } from '~/runner/runner_search_utils';
import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants';
const sortOptions = [
......@@ -31,9 +32,12 @@ export default {
value: {
type: Object,
required: true,
validator(val) {
return Array.isArray(val?.filters) && typeof val?.sort === 'string';
},
validator: searchValidator,
},
tokens: {
type: Array,
required: false,
default: () => [],
},
namespace: {
type: String,
......@@ -43,7 +47,7 @@ export default {
data() {
// filtered_search_bar_root.vue may mutate the inital
// filters. Use `cloneDeep` to prevent those mutations
// from affecting this component
// from affecting this component
const { filters, sort } = cloneDeep(this.value);
return {
initialFilterValue: filters,
......@@ -52,19 +56,17 @@ export default {
},
methods: {
onFilter(filters) {
const { sort } = this.value;
// Apply new filters, from page 1
this.$emit('input', {
...this.value,
filters,
sort,
pagination: { page: 1 },
});
},
onSort(sort) {
const { filters } = this.value;
// Apply new sort, from page 1
this.$emit('input', {
filters,
...this.value,
sort,
pagination: { page: 1 },
});
......@@ -74,13 +76,16 @@ export default {
};
</script>
<template>
<div>
<div
class="gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1"
>
<filtered-search
v-bind="$attrs"
:namespace="namespace"
recent-searches-storage-key="runners-search"
:sort-options="$options.sortOptions"
:initial-filter-value="initialFilterValue"
:tokens="tokens"
:initial-sort-by="initialSortBy"
:search-input-placeholder="__('Search or filter results...')"
data-testid="runners-filtered-search"
......
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import { searchValidator } from '~/runner/runner_search_utils';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
const tabs = [
{
title: s__('Runners|All'),
runnerType: null,
},
{
title: s__('Runners|Instance'),
runnerType: INSTANCE_TYPE,
},
{
title: s__('Runners|Group'),
runnerType: GROUP_TYPE,
},
{
title: s__('Runners|Project'),
runnerType: PROJECT_TYPE,
},
];
export default {
components: {
GlTabs,
GlTab,
},
props: {
value: {
type: Object,
required: true,
validator: searchValidator,
},
},
methods: {
onTabSelected({ runnerType }) {
this.$emit('input', {
...this.value,
runnerType,
pagination: { page: 1 },
});
},
isTabActive({ runnerType }) {
return runnerType === this.value.runnerType;
},
},
tabs,
};
</script>
<template>
<gl-tabs v-bind="$attrs">
<gl-tab
v-for="tab in $options.tabs"
:key="`${tab.runnerType}`"
:active="isTabActive(tab)"
:title="tab.title"
@click="onTabSelected(tab)"
/>
</gl-tabs>
</template>
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 { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, PARAM_KEY_RUNNER_TYPE } from '../../constants';
export const typeTokenConfig = {
icon: 'file-tree',
title: __('Type'),
type: PARAM_KEY_RUNNER_TYPE,
token: BaseToken,
unique: true,
options: [
{ value: INSTANCE_TYPE, title: s__('Runners|instance') },
{ value: GROUP_TYPE, title: s__('Runners|group') },
{ value: PROJECT_TYPE, title: s__('Runners|project') },
],
// TODO We should support more complex search rules,
// search for multiple states (OR) or have NOT operators
operators: OPERATOR_IS_ONLY,
};
......@@ -10,9 +10,9 @@ import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vu
import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { typeTokenConfig } from '../components/search_tokens/type_token_config';
import {
I18N_FETCH_ERROR,
GROUP_FILTERED_SEARCH_NAMESPACE,
......@@ -36,6 +36,7 @@ export default {
RunnerList,
RunnerName,
RunnerPagination,
RunnerTypeTabs,
},
props: {
registrationToken: {
......@@ -112,7 +113,7 @@ export default {
});
},
searchTokens() {
return [statusTokenConfig, typeTokenConfig];
return [statusTokenConfig];
},
filteredSearchNamespace() {
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
......@@ -144,7 +145,13 @@ export default {
<template>
<div>
<div class="gl-py-3 gl-display-flex">
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
v-model="search"
content-class="gl-display-none"
nav-class="gl-border-none!"
/>
<registration-dropdown
class="gl-ml-auto"
:registration-token="registrationToken"
......
......@@ -18,6 +18,50 @@ import {
RUNNER_PAGE_SIZE,
} from './constants';
/**
* The filters and sorting of the runners are built around
* an object called "search" that contains the current state
* of search in the UI. For example:
*
* ```
* const search = {
* // The current tab
* runnerType: 'INSTANCE_TYPE',
*
* // Filters in the search bar
* filters: [
* { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
* { type: 'filtered-search-term', value: { data: '' } },
* ],
*
* // Current sorting value
* sort: 'CREATED_DESC',
*
* // Pagination information
* pagination: { page: 1 },
* };
* ```
*
* An object in this format can be used to generate URLs
* with the search parameters or by runner components
* a input using a v-model.
*
* @module runner_search_utils
*/
/**
* Validates a search value
* @param {Object} search
* @returns {boolean} True if the value follows the search format.
*/
export const searchValidator = ({ runnerType, filters, sort }) => {
return (
(runnerType === null || typeof runnerType === 'string') &&
Array.isArray(filters) &&
typeof sort === 'string'
);
};
const getPaginationFromParams = (params) => {
const page = parseInt(params[PARAM_KEY_PAGE], 10);
const after = params[PARAM_KEY_AFTER];
......@@ -35,13 +79,20 @@ const getPaginationFromParams = (params) => {
};
};
/**
* Takes a URL query and transforms it into a "search" object
* @param {String?} query
* @returns {Object} A search object
*/
export const fromUrlQueryToSearch = (query = window.location.search) => {
const params = queryToObject(query, { gatherArrays: true });
const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null;
return {
runnerType,
filters: prepareTokens(
urlQueryToFilter(query, {
filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG],
filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_TAG],
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
),
......@@ -50,8 +101,15 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
};
};
/**
* Takes a "search" object and transforms it into a URL.
*
* @param {Object} search
* @param {String} url
* @returns {String} New URL for the page
*/
export const fromSearchToUrl = (
{ filters = [], sort = null, pagination = {} },
{ runnerType = null, filters = [], sort = null, pagination = {} },
url = window.location.href,
) => {
const filterParams = {
......@@ -65,6 +123,10 @@ export const fromSearchToUrl = (
}),
};
if (runnerType) {
filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType];
}
if (!filterParams[PARAM_KEY_SEARCH]) {
filterParams[PARAM_KEY_SEARCH] = null;
}
......@@ -82,21 +144,31 @@ export const fromSearchToUrl = (
return setUrlParams({ ...filterParams, ...otherParams }, url, false, true, true);
};
export const fromSearchToVariables = ({ filters = [], sort = null, pagination = {} } = {}) => {
/**
* Takes a "search" object and transforms it into variables for runner a GraphQL query.
*
* @param {Object} search
* @returns {Object} Hash of filter values
*/
export const fromSearchToVariables = ({
runnerType = null,
filters = [],
sort = null,
pagination = {},
} = {}) => {
const variables = {};
const queryObj = filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
});
variables.search = queryObj[PARAM_KEY_SEARCH];
// TODO Get more than one value when GraphQL API supports OR for "status" or "runner_type"
[variables.status] = queryObj[PARAM_KEY_STATUS] || [];
[variables.type] = queryObj[PARAM_KEY_RUNNER_TYPE] || [];
variables.search = queryObj[PARAM_KEY_SEARCH];
variables.tagList = queryObj[PARAM_KEY_TAG];
if (runnerType) {
variables.type = runnerType;
}
if (sort) {
variables.sort = sort;
}
......
......@@ -29639,6 +29639,9 @@ msgstr ""
msgid "Runners|Active"
msgstr ""
msgid "Runners|All"
msgstr ""
msgid "Runners|Amazon Linux 2 Docker HA with manual scaling and optional scheduling. %{percentage} spot."
msgstr ""
......@@ -29693,6 +29696,9 @@ msgstr ""
msgid "Runners|For each solution, you will choose a capacity. 1 enables warm HA through Auto Scaling group re-spawn. 2 enables hot HA because the service is available even when a node is lost. 3 or more enables hot HA and manual scaling of runner fleet."
msgstr ""
msgid "Runners|Group"
msgstr ""
msgid "Runners|Group Runners"
msgstr ""
......@@ -29705,6 +29711,9 @@ msgstr ""
msgid "Runners|Install a runner"
msgstr ""
msgid "Runners|Instance"
msgstr ""
msgid "Runners|Last contact"
msgstr ""
......@@ -29747,6 +29756,9 @@ msgstr ""
msgid "Runners|Platform"
msgstr ""
msgid "Runners|Project"
msgstr ""
msgid "Runners|Property Name"
msgstr ""
......@@ -29897,9 +29909,6 @@ msgstr ""
msgid "Runners|group"
msgstr ""
msgid "Runners|instance"
msgstr ""
msgid "Runners|locked"
msgstr ""
......@@ -29915,9 +29924,6 @@ msgstr ""
msgid "Runners|paused"
msgstr ""
msgid "Runners|project"
msgstr ""
msgid "Runners|shared"
msgstr ""
......
......@@ -66,6 +66,13 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
end
it 'runner type can be selected' do
expect(page).to have_link('All')
expect(page).to have_link('Instance')
expect(page).to have_link('Group')
expect(page).to have_link('Project')
end
it 'shows runners' do
expect(page).to have_content("runner-foo")
expect(page).to have_content("runner-bar")
......@@ -155,13 +162,21 @@ RSpec.describe "Admin Runners" do
create(:ci_runner, :group, description: 'runner-group', groups: [group])
end
it 'shows correct runner when type matches' do
visit admin_runners_path
expect(page).to have_link('All', class: 'active')
end
it 'shows correct runner when type matches' do
visit admin_runners_path
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
input_filtered_search_filter_is_only('Type', 'project')
click_on 'Project'
expect(page).to have_link('Project', class: 'active')
expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
......@@ -170,7 +185,9 @@ RSpec.describe "Admin Runners" do
it 'shows no runner when type does not match' do
visit admin_runners_path
input_filtered_search_filter_is_only('Type', 'instance')
click_on 'Instance'
expect(page).to have_link('Instance', class: 'active')
expect(page).not_to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
......@@ -183,7 +200,7 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
input_filtered_search_filter_is_only('Type', 'project')
click_on 'Project'
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-2-project'
......@@ -195,6 +212,24 @@ RSpec.describe "Admin Runners" do
expect(page).not_to have_content 'runner-2-project'
expect(page).not_to have_content 'runner-group'
end
it 'maintains the same filter when switching between runner types' do
create(:ci_runner, :project, description: 'runner-paused-project', active: false, projects: [project])
visit admin_runners_path
input_filtered_search_filter_is_only('Status', 'Active')
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
expect(page).not_to have_content 'runner-paused-project'
click_on 'Project'
expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
expect(page).not_to have_content 'runner-paused-project'
end
end
describe 'filter by tag' do
......
......@@ -22,7 +22,6 @@ import {
DEFAULT_SORT,
INSTANCE_TYPE,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
......@@ -126,10 +125,6 @@ describe('AdminRunnersApp', () => {
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_RUNNER_TYPE,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_TAG,
recentTokenValuesStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
......@@ -155,9 +150,9 @@ describe('AdminRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
filters: [
{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
{ type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
{ type: 'tag', value: { data: 'tag1', operator: '=' } },
],
sort: 'CREATED_DESC',
......@@ -179,6 +174,7 @@ describe('AdminRunnersApp', () => {
describe('when a filter is selected by the user', () => {
beforeEach(() => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: CREATED_ASC,
});
......
......@@ -5,13 +5,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 { typeTokenConfig } from '~/runner/components/search_tokens/type_token_config';
import {
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
STATUS_ACTIVE,
} from '~/runner/constants';
import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ACTIVE, 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';
......@@ -31,6 +25,11 @@ describe('RunnerList', () => {
];
const mockActiveRunnersCount = 2;
const expectToHaveLastEmittedInput = (value) => {
const inputs = wrapper.emitted('input');
expect(inputs[inputs.length - 1][0]).toEqual(value);
};
const createComponent = ({ props = {}, options = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(RunnerFilteredSearchBar, {
......@@ -38,6 +37,7 @@ describe('RunnerList', () => {
namespace: 'runners',
tokens: [],
value: {
runnerType: null,
filters: [],
sort: mockDefaultSort,
},
......@@ -86,7 +86,7 @@ describe('RunnerList', () => {
it('sets tokens to the filtered search', () => {
createComponent({
props: {
tokens: [statusTokenConfig, typeTokenConfig, tagTokenConfig],
tokens: [statusTokenConfig, tagTokenConfig],
},
});
......@@ -96,11 +96,6 @@ describe('RunnerList', () => {
token: BaseToken,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_RUNNER_TYPE,
token: BaseToken,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_TAG,
token: TagToken,
......@@ -123,6 +118,7 @@ describe('RunnerList', () => {
createComponent({
props: {
value: {
runnerType: INSTANCE_TYPE,
sort: mockOtherSort,
filters: mockFilters,
},
......@@ -142,30 +138,40 @@ describe('RunnerList', () => {
.text(),
).toEqual('Last contact');
});
it('when the user sets a filter, the "search" preserves the other filters', () => {
findGlFilteredSearch().vm.$emit('input', mockFilters);
findGlFilteredSearch().vm.$emit('submit');
expectToHaveLastEmittedInput({
runnerType: INSTANCE_TYPE,
filters: mockFilters,
sort: mockOtherSort,
pagination: { page: 1 },
});
});
});
it('when the user sets a filter, the "search" is emitted with filters', () => {
findGlFilteredSearch().vm.$emit('input', mockFilters);
findGlFilteredSearch().vm.$emit('submit');
expect(wrapper.emitted('input')[0]).toEqual([
{
filters: mockFilters,
sort: mockDefaultSort,
pagination: { page: 1 },
},
]);
expectToHaveLastEmittedInput({
runnerType: null,
filters: mockFilters,
sort: mockDefaultSort,
pagination: { page: 1 },
});
});
it('when the user sets a sorting method, the "search" is emitted with the sort', () => {
findSortOptions().at(1).vm.$emit('click');
expect(wrapper.emitted('input')[0]).toEqual([
{
filters: [],
sort: mockOtherSort,
pagination: { page: 1 },
},
]);
expectToHaveLastEmittedInput({
runnerType: null,
filters: [],
sort: mockOtherSort,
pagination: { page: 1 },
});
});
});
import { GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' };
describe('RunnerTypeTabs', () => {
let wrapper;
const findTabs = () => wrapper.findAll(GlTab);
const findActiveTab = () =>
findTabs()
.filter((tab) => tab.attributes('active') === 'true')
.at(0);
const createComponent = ({ value = mockSearch } = {}) => {
wrapper = shallowMount(RunnerTypeTabs, {
propsData: {
value,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('Renders options to filter runners', () => {
expect(findTabs().wrappers.map((tab) => tab.attributes('title'))).toEqual([
'All',
'Instance',
'Group',
'Project',
]);
});
it('"All" is selected by default', () => {
expect(findActiveTab().attributes('title')).toBe('All');
});
it('Another tab can be preselected by the user', () => {
createComponent({
value: {
...mockSearch,
runnerType: INSTANCE_TYPE,
},
});
expect(findActiveTab().attributes('title')).toBe('Instance');
});
describe('When the user selects a tab', () => {
const emittedValue = () => wrapper.emitted('input')[0][0];
beforeEach(() => {
findTabs().at(2).vm.$emit('click');
});
it(`Runner type is emitted`, () => {
expect(emittedValue()).toEqual({
...mockSearch,
runnerType: GROUP_TYPE,
});
});
it('Runner type is selected', async () => {
const newValue = emittedValue();
await wrapper.setProps({ value: newValue });
expect(findActiveTab().attributes('title')).toBe('Group');
});
});
});
import { nextTick } from 'vue';
import { GlLink } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
......@@ -21,7 +22,6 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
......@@ -88,9 +88,8 @@ describe('GroupRunnersApp', () => {
});
it('shows the runners list', () => {
expect(findRunnerList().props('runners')).toEqual(
groupRunnersData.data.group.runners.edges.map(({ node }) => node),
);
const runners = findRunnerList().props('runners');
expect(runners).toEqual(groupRunnersData.data.group.runners.edges.map(({ node }) => node));
});
it('runner item links to the runner group page', async () => {
......@@ -119,16 +118,15 @@ describe('GroupRunnersApp', () => {
it('sets tokens in the filtered search', () => {
createComponent({ mountFn: mount });
expect(findFilteredSearch().props('tokens')).toEqual([
const tokens = findFilteredSearch().props('tokens');
expect(tokens).toHaveLength(1);
expect(tokens[0]).toEqual(
expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_RUNNER_TYPE,
options: expect.any(Array),
}),
]);
);
});
describe('shows the active runner count', () => {
......@@ -163,10 +161,8 @@ describe('GroupRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
filters: [
{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
{ type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
],
runnerType: INSTANCE_TYPE,
filters: [{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: 'CREATED_DESC',
pagination: { page: 1 },
});
......@@ -184,11 +180,14 @@ describe('GroupRunnersApp', () => {
});
describe('when a filter is selected by the user', () => {
beforeEach(() => {
beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: CREATED_ASC,
});
await nextTick();
});
it('updates the browser url', () => {
......
import { RUNNER_PAGE_SIZE } from '~/runner/constants';
import {
searchValidator,
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
......@@ -10,13 +11,14 @@ describe('search_params.js', () => {
{
name: 'a default query',
urlQuery: '',
search: { filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
},
{
name: 'a single status',
urlQuery: '?status[]=ACTIVE',
search: {
runnerType: null,
filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC',
......@@ -27,6 +29,7 @@ describe('search_params.js', () => {
name: 'a single term text search',
urlQuery: '?search=something',
search: {
runnerType: null,
filters: [
{
type: 'filtered-search-term',
......@@ -42,6 +45,7 @@ describe('search_params.js', () => {
name: 'a two terms text search',
urlQuery: '?search=something+else',
search: {
runnerType: null,
filters: [
{
type: 'filtered-search-term',
......@@ -61,7 +65,8 @@ describe('search_params.js', () => {
name: 'single instance type',
urlQuery: '?runner_type[]=INSTANCE_TYPE',
search: {
filters: [{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } }],
runnerType: 'INSTANCE_TYPE',
filters: [],
pagination: { page: 1 },
sort: 'CREATED_DESC',
},
......@@ -71,6 +76,7 @@ describe('search_params.js', () => {
name: 'multiple runner status',
urlQuery: '?status[]=ACTIVE&status[]=PAUSED',
search: {
runnerType: null,
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'status', value: { data: 'PAUSED', operator: '=' } },
......@@ -84,10 +90,8 @@ describe('search_params.js', () => {
name: 'multiple status, a single instance type and a non default sort',
urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
search: {
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
],
runnerType: 'INSTANCE_TYPE',
filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_ASC',
},
......@@ -102,6 +106,7 @@ describe('search_params.js', () => {
name: 'a tag',
urlQuery: '?tag[]=tag-1',
search: {
runnerType: null,
filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC',
......@@ -116,6 +121,7 @@ describe('search_params.js', () => {
name: 'two tags',
urlQuery: '?tag[]=tag-1&tag[]=tag-2',
search: {
runnerType: null,
filters: [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
......@@ -132,13 +138,19 @@ describe('search_params.js', () => {
{
name: 'the next page',
urlQuery: '?page=2&after=AFTER_CURSOR',
search: { filters: [], pagination: { page: 2, after: 'AFTER_CURSOR' }, sort: 'CREATED_DESC' },
search: {
runnerType: null,
filters: [],
pagination: { page: 2, after: 'AFTER_CURSOR' },
sort: 'CREATED_DESC',
},
graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE },
},
{
name: 'the previous page',
urlQuery: '?page=2&before=BEFORE_CURSOR',
search: {
runnerType: null,
filters: [],
pagination: { page: 2, before: 'BEFORE_CURSOR' },
sort: 'CREATED_DESC',
......@@ -150,9 +162,9 @@ describe('search_params.js', () => {
urlQuery:
'?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
search: {
runnerType: 'INSTANCE_TYPE',
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
......@@ -170,6 +182,14 @@ describe('search_params.js', () => {
},
];
describe('searchValidator', () => {
examples.forEach(({ name, search }) => {
it(`Validates ${name} as a search object`, () => {
expect(searchValidator(search)).toBe(true);
});
});
});
describe('fromUrlQueryToSearch', () => {
examples.forEach(({ name, urlQuery, search }) => {
it(`Converts ${name} to a search object`, () => {
......
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