Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
e857908f
Commit
e857908f
authored
Jun 23, 2021
by
Kushal Pandya
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '329658-runner-tags-search' into 'master'
Search runners by tags See merge request gitlab-org/gitlab!63990
parents
c878b3d0
6aa03245
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
491 additions
and
83 deletions
+491
-83
app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
...ascripts/runner/components/runner_filtered_search_bar.vue
+60
-46
app/assets/javascripts/runner/components/runner_tag.vue
app/assets/javascripts/runner/components/runner_tag.vue
+27
-0
app/assets/javascripts/runner/components/runner_tags.vue
app/assets/javascripts/runner/components/runner_tags.vue
+3
-10
app/assets/javascripts/runner/components/search_tokens/tag_token.vue
...javascripts/runner/components/search_tokens/tag_token.vue
+91
-0
app/assets/javascripts/runner/constants.js
app/assets/javascripts/runner/constants.js
+6
-1
app/assets/javascripts/runner/graphql/get_runners.query.graphql
...sets/javascripts/runner/graphql/get_runners.query.graphql
+4
-2
app/assets/javascripts/runner/runner_list/runner_search_utils.js
...ets/javascripts/runner/runner_list/runner_search_utils.js
+11
-6
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/runner/components/runner_filtered_search_bar_spec.js
...tend/runner/components/runner_filtered_search_bar_spec.js
+10
-2
spec/frontend/runner/components/runner_tag_spec.js
spec/frontend/runner/components/runner_tag_spec.js
+45
-0
spec/frontend/runner/components/runner_tags_spec.js
spec/frontend/runner/components/runner_tags_spec.js
+2
-10
spec/frontend/runner/components/search_tokens/tag_token_spec.js
...rontend/runner/components/search_tokens/tag_token_spec.js
+188
-0
spec/frontend/runner/runner_list/runner_list_app_spec.js
spec/frontend/runner/runner_list/runner_list_app_spec.js
+5
-3
spec/frontend/runner/runner_list/runner_search_utils_spec.js
spec/frontend/runner/runner_list/runner_search_utils_spec.js
+36
-3
No files found.
app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
View file @
e857908f
<
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"
...
...
app/assets/javascripts/runner/components/runner_tag.vue
0 → 100644
View file @
e857908f
<
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
>
app/assets/javascripts/runner/components/runner_tags.vue
View file @
e857908f
<
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
>
app/assets/javascripts/runner/components/search_tokens/tag_token.vue
0 → 100644
View file @
e857908f
<
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>
app/assets/javascripts/runner/constants.js
View file @
e857908f
...
...
@@ -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
'
;
...
...
app/assets/javascripts/runner/graphql/get_runners.query.graphql
View file @
e857908f
...
...
@@ -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
{
...
...
app/assets/javascripts/runner/runner_list/runner_search_utils.js
View file @
e857908f
...
...
@@ -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
;
}
...
...
locale/gitlab.pot
View file @
e857908f
...
...
@@ -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 ""
...
...
spec/frontend/runner/components/runner_filtered_search_bar_spec.js
View file @
e857908f
...
...
@@ -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
,
}),
]);
});
...
...
spec/frontend/runner/components/runner_tag_spec.js
0 → 100644
View file @
e857908f
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
'
);
});
});
spec/frontend/runner/components/runner_tags_spec.js
View file @
e857908f
import
{
GlBadge
}
from
'
@gitlab/ui
'
;
import
{
shallowM
ount
}
from
'
@vue/test-utils
'
;
import
{
m
ount
}
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
=
shallowM
ount
(
RunnerTags
,
{
wrapper
=
m
ount
(
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
},
...
...
spec/frontend/runner/components/search_tokens/tag_token_spec.js
0 → 100644
View file @
e857908f
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
);
});
});
});
spec/frontend/runner/runner_list/runner_list_app_spec.js
View file @
e857908f
...
...
@@ -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
'
,
});
});
...
...
spec/frontend/runner/runner_list/runner_search_utils_spec.js
View file @
e857908f
...
...
@@ -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
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment