Commit e857908f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '329658-runner-tags-search' into 'master'

Search runners by tags

See merge request gitlab-org/gitlab!63990
parents c878b3d0 6aa03245
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import { __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/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';
import {
STATUS_ACTIVE,
STATUS_PAUSED,
......@@ -19,50 +19,9 @@ import {
CONTACTED_ASC,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
} from '../constants';
const searchTokens = [
{
icon: 'status',
title: __('Status'),
type: PARAM_KEY_STATUS,
token: GlFilteredSearchToken,
// TODO Get more than one value when GraphQL API supports OR for "status"
unique: true,
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') },
// Added extra quotes in this title to avoid splitting this value:
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
{ value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
],
// TODO In principle we could support more complex search rules,
// this can be added to a separate issue.
operators: OPERATOR_IS_ONLY,
},
{
icon: 'file-tree',
title: __('Type'),
type: PARAM_KEY_RUNNER_TYPE,
token: GlFilteredSearchToken,
// TODO Get more than one value when GraphQL API supports OR for "status"
unique: true,
options: [
{ value: INSTANCE_TYPE, title: s__('Runners|shared') },
{ value: GROUP_TYPE, title: s__('Runners|group') },
{ value: PROJECT_TYPE, title: s__('Runners|specific') },
],
// TODO We should support more complex search rules,
// search for multiple states (OR) or have NOT operators
operators: OPERATOR_IS_ONLY,
},
// TODO Support tags
];
import TagToken from './search_tokens/tag_token.vue';
const sortOptions = [
{
......@@ -95,6 +54,10 @@ export default {
return Array.isArray(val?.filters) && typeof val?.sort === 'string';
},
},
namespace: {
type: String,
required: true,
},
},
data() {
// filtered_search_bar_root.vue may mutate the inital
......@@ -106,6 +69,57 @@ export default {
initialSortBy: sort,
};
},
computed: {
searchTokens() {
return [
{
icon: 'status',
title: __('Status'),
type: PARAM_KEY_STATUS,
token: BaseToken,
unique: true,
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') },
// Added extra quotes in this title to avoid splitting this value:
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
{ value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
],
// TODO In principle we could support more complex search rules,
// this can be added to a separate issue.
operators: OPERATOR_IS_ONLY,
},
{
icon: 'file-tree',
title: __('Type'),
type: PARAM_KEY_RUNNER_TYPE,
token: BaseToken,
unique: true,
options: [
{ value: INSTANCE_TYPE, title: s__('Runners|shared') },
{ value: GROUP_TYPE, title: s__('Runners|group') },
{ value: PROJECT_TYPE, title: s__('Runners|specific') },
],
// TODO We should support more complex search rules,
// search for multiple states (OR) or have NOT operators
operators: OPERATOR_IS_ONLY,
},
{
icon: 'tag',
title: s__('Runners|Tags'),
type: PARAM_KEY_TAG,
token: TagToken,
recentTokenValuesStorageKey: `${this.namespace}-recent-tags`,
operators: OPERATOR_IS_ONLY,
},
];
},
},
methods: {
onFilter(filters) {
const { sort } = this.value;
......@@ -127,17 +141,17 @@ export default {
},
},
sortOptions,
searchTokens,
};
</script>
<template>
<filtered-search
v-bind="$attrs"
:namespace="namespace"
recent-searches-storage-key="runners-search"
:sort-options="$options.sortOptions"
:initial-filter-value="initialFilterValue"
:initial-sort-by="initialSortBy"
:tokens="$options.searchTokens"
:tokens="searchTokens"
:search-input-placeholder="__('Search or filter results...')"
@onFilter="onFilter"
@onSort="onSort"
......
<script>
import { GlBadge } from '@gitlab/ui';
import { RUNNER_TAG_BADGE_VARIANT } from '../constants';
export default {
components: {
GlBadge,
},
props: {
tag: {
type: String,
required: true,
},
size: {
type: String,
required: false,
default: 'md',
},
},
RUNNER_TAG_BADGE_VARIANT,
};
</script>
<template>
<gl-badge :size="size" :variant="$options.RUNNER_TAG_BADGE_VARIANT">
{{ tag }}
</gl-badge>
</template>
<script>
import { GlBadge } from '@gitlab/ui';
import RunnerTag from './runner_tag.vue';
export default {
components: {
GlBadge,
RunnerTag,
},
props: {
tagList: {
......@@ -16,18 +16,11 @@ export default {
required: false,
default: 'md',
},
variant: {
type: String,
required: false,
default: 'info',
},
},
};
</script>
<template>
<div>
<gl-badge v-for="tag in tagList" :key="tag" :size="size" :variant="variant">
{{ tag }}
</gl-badge>
<runner-tag v-for="tag in tagList" :key="tag" :tag="tag" :size="size" />
</div>
</template>
<script>
import { GlFilteredSearchSuggestion, GlToken } from '@gitlab/ui';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { RUNNER_TAG_BG_CLASS } from '../../constants';
export const TAG_SUGGESTIONS_PATH = '/admin/runners/tag_list.json';
export default {
components: {
BaseToken,
GlFilteredSearchSuggestion,
GlToken,
},
props: {
config: {
type: Object,
required: true,
},
},
data() {
return {
tags: [],
loading: false,
};
},
methods: {
fnCurrentTokenValue(data) {
// By default, values are transformed with `toLowerCase`
// however, runner tags are case sensitive.
return data;
},
getTagsOptions(search) {
// TODO This should be implemented via a GraphQL API
// The API should
// 1) scope to the rights of the user
// 2) stay up to date to the removal of old tags
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
return axios
.get(TAG_SUGGESTIONS_PATH, {
params: {
search,
},
})
.then(({ data }) => {
return data.map(({ id, name }) => ({ id, value: name, text: name }));
});
},
async fetchTags(searchTerm) {
this.loading = true;
try {
this.tags = await this.getTagsOptions(searchTerm);
} catch {
createFlash({
message: s__('Runners|Something went wrong while fetching the tags suggestions'),
});
} finally {
this.loading = false;
}
},
},
RUNNER_TAG_BG_CLASS,
};
</script>
<template>
<base-token
v-bind="$attrs"
:config="config"
:suggestions-loading="loading"
:suggestions="tags"
:fn-current-token-value="fnCurrentTokenValue"
:recent-suggestions-storage-key="config.recentTokenValuesStorageKey"
@fetch-suggestions="fetchTags"
v-on="$listeners"
>
<template #view-token="{ viewTokenProps: { listeners, inputValue, activeTokenValue } }">
<gl-token variant="search-value" :class="$options.RUNNER_TAG_BG_CLASS" v-on="listeners">
{{ activeTokenValue ? activeTokenValue.text : inputValue }}
</gl-token>
</template>
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion v-for="tag in suggestions" :key="tag.id" :value="tag.value">
{{ tag.text }}
</gl-filtered-search-suggestion>
</template>
</base-token>
</template>
......@@ -6,13 +6,18 @@ export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
export const RUNNER_ENTITY_TYPE = 'Ci::Runner';
export const RUNNER_TAG_BADGE_VARIANT = 'info';
export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
// Filtered search parameter names
// - Used for URL params names
// - GlFilteredSearch tokens type
export const PARAM_KEY_SEARCH = 'search';
export const PARAM_KEY_STATUS = 'status';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
export const PARAM_KEY_TAG = 'tag';
export const PARAM_KEY_SEARCH = 'search';
export const PARAM_KEY_SORT = 'sort';
export const PARAM_KEY_PAGE = 'page';
export const PARAM_KEY_AFTER = 'after';
......
......@@ -6,9 +6,10 @@ query getRunners(
$after: String
$first: Int
$last: Int
$search: String
$status: CiRunnerStatus
$type: CiRunnerType
$tagList: [String!]
$search: String
$sort: CiRunnerSort
) {
runners(
......@@ -16,9 +17,10 @@ query getRunners(
after: $after
first: $first
last: $last
search: $search
status: $status
type: $type
tagList: $tagList
search: $search
sort: $sort
) {
nodes {
......
......@@ -6,9 +6,10 @@ import {
prepareTokens,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import {
PARAM_KEY_SEARCH,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
PARAM_KEY_SEARCH,
PARAM_KEY_SORT,
PARAM_KEY_PAGE,
PARAM_KEY_AFTER,
......@@ -40,7 +41,7 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
return {
filters: prepareTokens(
urlQueryToFilter(query, {
filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE],
filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG],
filteredSearchTermKey: PARAM_KEY_SEARCH,
legacySpacesDecode: false,
}),
......@@ -56,15 +57,19 @@ export const fromSearchToUrl = (
) => {
const filterParams = {
// Defaults
[PARAM_KEY_SEARCH]: null,
[PARAM_KEY_STATUS]: [],
[PARAM_KEY_RUNNER_TYPE]: [],
[PARAM_KEY_TAG]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
};
if (!filterParams[PARAM_KEY_SEARCH]) {
filterParams[PARAM_KEY_SEARCH] = null;
}
const isDefaultSort = sort !== DEFAULT_SORT;
const isFirstPage = pagination?.page === 1;
const otherParams = {
......@@ -87,12 +92,12 @@ export const fromSearchToVariables = ({ filters = [], sort = null, pagination =
variables.search = queryObj[PARAM_KEY_SEARCH];
// TODO Get more than one value when GraphQL API supports OR for "status"
// TODO Get more than one value when GraphQL API supports OR for "status" or "runner_type"
[variables.status] = queryObj[PARAM_KEY_STATUS] || [];
// TODO Get more than one value when GraphQL API supports OR for "runner type"
[variables.type] = queryObj[PARAM_KEY_RUNNER_TYPE] || [];
variables.tagList = queryObj[PARAM_KEY_TAG];
if (sort) {
variables.sort = sort;
}
......
......@@ -28126,6 +28126,9 @@ msgstr ""
msgid "Runners|Show Runner installation instructions"
msgstr ""
msgid "Runners|Something went wrong while fetching the tags suggestions"
msgstr ""
msgid "Runners|Tags"
msgstr ""
......
......@@ -2,8 +2,10 @@ import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import { PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE } from '~/runner/constants';
import TagToken from '~/runner/components/search_tokens/tag_token.vue';
import { PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG } 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';
describe('RunnerList', () => {
let wrapper;
......@@ -23,13 +25,13 @@ describe('RunnerList', () => {
wrapper = extendedWrapper(
shallowMount(RunnerFilteredSearchBar, {
propsData: {
namespace: 'runners',
value: {
filters: [],
sort: mockDefaultSort,
},
...props,
},
attrs: { namespace: 'runners' },
stubs: {
FilteredSearch,
GlFilteredSearch,
......@@ -65,12 +67,18 @@ describe('RunnerList', () => {
expect(findFilteredSearch().props('tokens')).toEqual([
expect.objectContaining({
type: PARAM_KEY_STATUS,
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,
}),
]);
});
......
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTag from '~/runner/components/runner_tag.vue';
describe('RunnerTag', () => {
let wrapper;
const findBadge = () => wrapper.findComponent(GlBadge);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerTag, {
propsData: {
tag: 'tag1',
...props,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('Displays tag text', () => {
expect(wrapper.text()).toBe('tag1');
});
it('Displays tags with correct style', () => {
expect(findBadge().props()).toMatchObject({
size: 'md',
variant: 'info',
});
});
it('Displays tags with small size', () => {
createComponent({
props: { size: 'sm' },
});
expect(findBadge().props('size')).toBe('sm');
});
});
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import RunnerTags from '~/runner/components/runner_tags.vue';
describe('RunnerTags', () => {
......@@ -9,7 +9,7 @@ describe('RunnerTags', () => {
const findBadgesAt = (i = 0) => wrapper.findAllComponents(GlBadge).at(i);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerTags, {
wrapper = mount(RunnerTags, {
propsData: {
tagList: ['tag1', 'tag2'],
...props,
......@@ -45,14 +45,6 @@ describe('RunnerTags', () => {
expect(findBadge().props('size')).toBe('sm');
});
it('Displays tags with a variant', () => {
createComponent({
props: { variant: 'warning' },
});
expect(findBadge().props('variant')).toBe('warning');
});
it('Is empty when there are no tags', () => {
createComponent({
props: { tagList: null },
......
import { GlFilteredSearchSuggestion, GlLoadingIcon, GlToken } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/runner/components/search_tokens/tag_token.vue';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
jest.mock('~/flash');
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
...jest.requireActual('~/vue_shared/components/filtered_search_bar/filtered_search_utils'),
getRecentlyUsedSuggestions: jest.fn(),
}));
const mockStorageKey = 'stored-recent-tags';
const mockTags = [
{ id: 1, name: 'linux' },
{ id: 2, name: 'windows' },
{ id: 3, name: 'mac' },
];
const mockTagsFiltered = [mockTags[0]];
const mockSearchTerm = mockTags[0].name;
const GlFilteredSearchTokenStub = {
template: `<div>
<slot name="view-token"></slot>
<slot name="suggestions"></slot>
</div>`,
};
const mockTagTokenConfig = {
icon: 'tag',
title: 'Tags',
type: 'tag',
token: TagToken,
recentTokenValuesStorageKey: mockStorageKey,
operators: OPERATOR_IS_ONLY,
};
describe('TagToken', () => {
let mock;
let wrapper;
const createComponent = (props = {}) => {
wrapper = mount(TagToken, {
propsData: {
config: mockTagTokenConfig,
value: { data: '' },
active: false,
...props,
},
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
filteredSearchSuggestionListInstance: {
register: jest.fn(),
unregister: jest.fn(),
},
},
stubs: {
GlFilteredSearchToken: GlFilteredSearchTokenStub,
},
});
};
const findGlFilteredSearchSuggestions = () =>
wrapper.findAllComponents(GlFilteredSearchSuggestion);
const findGlFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchTokenStub);
const findToken = () => wrapper.findComponent(GlToken);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(200, mockTags);
mock
.onGet(TAG_SUGGESTIONS_PATH, { params: { search: mockSearchTerm } })
.reply(200, mockTagsFiltered);
getRecentlyUsedSuggestions.mockReturnValue([]);
createComponent();
await waitForPromises();
});
afterEach(() => {
getRecentlyUsedSuggestions.mockReset();
wrapper.destroy();
});
describe('when the tags token is displayed', () => {
it('requests tags suggestions', () => {
expect(mock.history.get[0].params).toEqual({ search: '' });
});
it('displays tags suggestions', () => {
mockTags.forEach(({ name }, i) => {
expect(findGlFilteredSearchSuggestions().at(i).text()).toBe(name);
});
});
});
describe('when suggestions are stored', () => {
const storedSuggestions = [{ id: 4, value: 'docker', text: 'docker' }];
beforeEach(async () => {
getRecentlyUsedSuggestions.mockReturnValue(storedSuggestions);
createComponent();
await waitForPromises();
});
it('suggestions are loaded from a correct key', () => {
expect(getRecentlyUsedSuggestions).toHaveBeenCalledWith(mockStorageKey);
});
it('displays stored tags suggestions', () => {
expect(findGlFilteredSearchSuggestions()).toHaveLength(
mockTags.length + storedSuggestions.length,
);
expect(findGlFilteredSearchSuggestions().at(0).text()).toBe(storedSuggestions[0].text);
});
});
describe('when the users filters suggestions', () => {
beforeEach(async () => {
findGlFilteredSearchToken().vm.$emit('input', { data: mockSearchTerm });
jest.runAllTimers();
});
it('requests filtered tags suggestions', async () => {
await waitForPromises();
expect(mock.history.get[1].params).toEqual({ search: mockSearchTerm });
});
it('shows the loading icon', async () => {
await nextTick();
expect(findGlLoadingIcon().exists()).toBe(true);
});
it('displays filtered tags suggestions', async () => {
await waitForPromises();
expect(findGlFilteredSearchSuggestions()).toHaveLength(mockTagsFiltered.length);
expect(findGlFilteredSearchSuggestions().at(0).text()).toBe(mockTagsFiltered[0].name);
});
});
describe('when suggestions cannot be loaded', () => {
beforeEach(async () => {
mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(500);
createComponent();
await waitForPromises();
});
it('error is shown', async () => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith({ message: expect.any(String) });
});
});
describe('when the user selects a value', () => {
beforeEach(async () => {
createComponent({ value: { data: mockTags[0].name } });
findGlFilteredSearchToken().vm.$emit('select');
await waitForPromises();
});
it('selected tag is displayed', async () => {
expect(findToken().exists()).toBe(true);
});
});
});
......@@ -64,7 +64,7 @@ describe('RunnerListApp', () => {
};
const setQuery = (query) => {
window.location.href = `${TEST_HOST}/admin/runners/${query}`;
window.location.href = `${TEST_HOST}/admin/runners?${query}`;
window.location.search = query;
};
......@@ -119,7 +119,7 @@ describe('RunnerListApp', () => {
describe('when a filter is preselected', () => {
beforeEach(async () => {
window.location.search = `?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`;
setQuery(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
createComponentWithApollo();
await waitForPromises();
......@@ -130,6 +130,7 @@ describe('RunnerListApp', () => {
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',
pagination: { page: 1 },
......@@ -140,6 +141,7 @@ describe('RunnerListApp', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: STATUS_ACTIVE,
type: INSTANCE_TYPE,
tagList: ['tag1'],
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
......@@ -157,7 +159,7 @@ describe('RunnerListApp', () => {
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[]=ACTIVE&sort=CREATED_ASC',
});
});
......
......@@ -98,6 +98,37 @@ describe('search_params.js', () => {
first: RUNNER_PAGE_SIZE,
},
},
{
name: 'a tag',
urlQuery: '?tag[]=tag-1',
search: {
filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC',
},
graphqlVariables: {
tagList: ['tag-1'],
first: 20,
sort: 'CREATED_DESC',
},
},
{
name: 'two tags',
urlQuery: '?tag[]=tag-1&tag[]=tag-2',
search: {
filters: [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
pagination: { page: 1 },
sort: 'CREATED_DESC',
},
graphqlVariables: {
tagList: ['tag-1', 'tag-2'],
first: 20,
sort: 'CREATED_DESC',
},
},
{
name: 'the next page',
urlQuery: '?page=2&after=AFTER_CURSOR',
......@@ -115,14 +146,15 @@ describe('search_params.js', () => {
graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE },
},
{
name:
'the next page filtered by multiple status, a single instance type and a non default sort',
name: 'the next page filtered by a status, an instance type, tags and a non default sort',
urlQuery:
'?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
'?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
search: {
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: '=' } },
],
pagination: { page: 2, after: 'AFTER_CURSOR' },
sort: 'CREATED_ASC',
......@@ -130,6 +162,7 @@ describe('search_params.js', () => {
graphqlVariables: {
status: 'ACTIVE',
type: 'INSTANCE_TYPE',
tagList: ['tag-1', 'tag-2'],
sort: 'CREATED_ASC',
after: 'AFTER_CURSOR',
first: RUNNER_PAGE_SIZE,
......
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