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
bd6da5e9
Commit
bd6da5e9
authored
May 18, 2021
by
Miguel Rincon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add filtered search to runner list
Thi changes adds filtered search to runners.
parent
19d68686
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
624 additions
and
12 deletions
+624
-12
app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
...ascripts/runner/components/runner_filtered_search_bar.vue
+143
-0
app/assets/javascripts/runner/components/runner_list.vue
app/assets/javascripts/runner/components/runner_list.vue
+2
-2
app/assets/javascripts/runner/constants.js
app/assets/javascripts/runner/constants.js
+25
-0
app/assets/javascripts/runner/graphql/get_runners.query.graphql
...sets/javascripts/runner/graphql/get_runners.query.graphql
+2
-2
app/assets/javascripts/runner/runner_list/filtered_search_utils.js
...s/javascripts/runner/runner_list/filtered_search_utils.js
+72
-0
app/assets/javascripts/runner/runner_list/runner_list_app.vue
...assets/javascripts/runner/runner_list/runner_list_app.vue
+27
-0
locale/gitlab.pot
locale/gitlab.pot
+12
-0
spec/frontend/runner/components/runner_filtered_search_bar_spec.js
...tend/runner/components/runner_filtered_search_bar_spec.js
+135
-0
spec/frontend/runner/components/runner_list_spec.js
spec/frontend/runner/components/runner_list_spec.js
+18
-8
spec/frontend/runner/runner_list/filtered_search_utils_spec.js
...frontend/runner/runner_list/filtered_search_utils_spec.js
+98
-0
spec/frontend/runner/runner_list/runner_list_app_spec.js
spec/frontend/runner/runner_list/runner_list_app_spec.js
+90
-0
No files found.
app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
0 → 100644
View file @
bd6da5e9
<
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
{
STATUS_ACTIVE
,
STATUS_PAUSED
,
STATUS_ONLINE
,
STATUS_OFFLINE
,
STATUS_NOT_CONNECTED
,
INSTANCE_TYPE
,
GROUP_TYPE
,
PROJECT_TYPE
,
CREATED_DESC
,
CREATED_ASC
,
CONTACTED_DESC
,
CONTACTED_ASC
,
PARAM_KEY_STATUS
,
PARAM_KEY_RUNNER_TYPE
,
}
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
];
const
sortOptions
=
[
{
id
:
1
,
title
:
__
(
'
Created date
'
),
sortDirection
:
{
descending
:
CREATED_DESC
,
ascending
:
CREATED_ASC
,
},
},
{
id
:
2
,
title
:
__
(
'
Last contact
'
),
sortDirection
:
{
descending
:
CONTACTED_DESC
,
ascending
:
CONTACTED_ASC
,
},
},
];
export
default
{
components
:
{
FilteredSearch
,
},
props
:
{
value
:
{
type
:
Object
,
required
:
true
,
validator
(
val
)
{
return
Array
.
isArray
(
val
?.
filters
)
&&
typeof
val
?.
sort
===
'
string
'
;
},
},
},
data
()
{
// filtered_search_bar_root.vue may mutate the inital
// filters. Use `cloneDeep` to prevent those mutations
// from affecting this component
const
{
filters
,
sort
}
=
cloneDeep
(
this
.
value
);
return
{
initialFilterValue
:
filters
,
initialSortBy
:
sort
,
};
},
methods
:
{
onFilter
(
filters
)
{
const
{
sort
}
=
this
.
value
;
this
.
$emit
(
'
input
'
,
{
filters
,
sort
,
});
},
onSort
(
sort
)
{
const
{
filters
}
=
this
.
value
;
this
.
$emit
(
'
input
'
,
{
filters
,
sort
,
});
},
},
sortOptions
,
searchTokens
,
};
</
script
>
<
template
>
<filtered-search
v-bind=
"$attrs"
recent-searches-storage-key=
"runners-search"
:sort-options=
"$options.sortOptions"
:initial-filter-value=
"initialFilterValue"
:initial-sort-by=
"initialSortBy"
:tokens=
"$options.searchTokens"
:search-input-placeholder=
"__('Search or filter results...')"
@
onFilter=
"onFilter"
@
onSort=
"onSort"
/>
</
template
>
app/assets/javascripts/runner/components/runner_list.vue
View file @
bd6da5e9
...
...
@@ -95,8 +95,8 @@ export default {
stacked=
"md"
fixed
>
<template
#table-busy
>
<gl-skeleton-loader
/>
<template
v-if=
"!runners.length"
#table-busy
>
<gl-skeleton-loader
v-for=
"i in 4"
:key=
"i"
/>
</
template
>
<
template
#cell(type)=
"{ item }"
>
...
...
app/assets/javascripts/runner/constants.js
View file @
bd6da5e9
...
...
@@ -4,8 +4,33 @@ export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
export
const
RUNNER_ENTITY_TYPE
=
'
Ci::Runner
'
;
// Filtered search parameter names
// - Used for URL params names
// - GlFilteredSearch tokens type
export
const
PARAM_KEY_STATUS
=
'
status
'
;
export
const
PARAM_KEY_RUNNER_TYPE
=
'
runner_type
'
;
export
const
PARAM_KEY_SORT
=
'
sort
'
;
// CiRunnerType
export
const
INSTANCE_TYPE
=
'
INSTANCE_TYPE
'
;
export
const
GROUP_TYPE
=
'
GROUP_TYPE
'
;
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_OFFLINE
=
'
OFFLINE
'
;
export
const
STATUS_NOT_CONNECTED
=
'
NOT_CONNECTED
'
;
// CiRunnerSort
export
const
CREATED_DESC
=
'
CREATED_DESC
'
;
export
const
CREATED_ASC
=
'
CREATED_ASC
'
;
// TODO Add this to the API
export
const
CONTACTED_DESC
=
'
CONTACTED_DESC
'
;
// TODO Add this to the API
export
const
CONTACTED_ASC
=
'
CONTACTED_ASC
'
;
export
const
DEFAULT_SORT
=
CREATED_DESC
;
app/assets/javascripts/runner/graphql/get_runners.query.graphql
View file @
bd6da5e9
query
getRunners
{
runners
{
query
getRunners
(
$status
:
CiRunnerStatus
,
$type
:
CiRunnerType
,
$sort
:
CiRunnerSort
)
{
runners
(
status
:
$status
,
type
:
$type
,
sort
:
$sort
)
{
nodes
{
id
description
...
...
app/assets/javascripts/runner/runner_list/filtered_search_utils.js
0 → 100644
View file @
bd6da5e9
import
{
queryToObject
,
setUrlParams
}
from
'
~/lib/utils/url_utility
'
;
import
{
PARAM_KEY_STATUS
,
PARAM_KEY_RUNNER_TYPE
,
PARAM_KEY_SORT
,
DEFAULT_SORT
,
}
from
'
../constants
'
;
const
getValuesFromFilters
=
(
paramKey
,
filters
)
=>
{
return
filters
.
filter
(({
type
,
value
})
=>
type
===
paramKey
&&
value
.
operator
===
'
=
'
)
.
map
(({
value
})
=>
value
.
data
);
};
const
getFilterFromParams
=
(
paramKey
,
params
)
=>
{
const
value
=
params
[
paramKey
];
if
(
!
value
)
{
return
[];
}
const
values
=
Array
.
isArray
(
value
)
?
value
:
[
value
];
return
values
.
map
((
data
)
=>
{
return
{
type
:
paramKey
,
value
:
{
data
,
operator
:
'
=
'
,
},
};
});
};
export
const
fromUrlQueryToSearch
=
(
query
=
window
.
location
.
search
)
=>
{
const
params
=
queryToObject
(
query
,
{
gatherArrays
:
true
});
return
{
filters
:
[
...
getFilterFromParams
(
PARAM_KEY_STATUS
,
params
),
...
getFilterFromParams
(
PARAM_KEY_RUNNER_TYPE
,
params
),
],
sort
:
params
[
PARAM_KEY_SORT
]
||
DEFAULT_SORT
,
};
};
export
const
fromSearchToUrl
=
({
filters
=
[],
sort
=
null
},
url
=
window
.
location
.
href
)
=>
{
const
urlParams
=
{
[
PARAM_KEY_STATUS
]:
getValuesFromFilters
(
PARAM_KEY_STATUS
,
filters
),
[
PARAM_KEY_RUNNER_TYPE
]:
getValuesFromFilters
(
PARAM_KEY_RUNNER_TYPE
,
filters
),
};
if
(
sort
&&
sort
!==
DEFAULT_SORT
)
{
urlParams
[
PARAM_KEY_SORT
]
=
sort
;
}
return
setUrlParams
(
urlParams
,
url
,
false
,
true
,
true
);
};
export
const
fromSearchToVariables
=
({
filters
=
[],
sort
=
null
}
=
{})
=>
{
const
variables
=
{};
// TODO Get more than one value when GraphQL API supports OR for "status"
[
variables
.
status
]
=
getValuesFromFilters
(
PARAM_KEY_STATUS
,
filters
);
// TODO Get more than one value when GraphQL API supports OR for "runner type"
[
variables
.
type
]
=
getValuesFromFilters
(
PARAM_KEY_RUNNER_TYPE
,
filters
);
if
(
sort
)
{
variables
.
sort
=
sort
;
}
return
variables
;
};
app/assets/javascripts/runner/runner_list/runner_list_app.vue
View file @
bd6da5e9
<
script
>
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
{
updateHistory
}
from
'
~/lib/utils/url_utility
'
;
import
RunnerFilteredSearchBar
from
'
../components/runner_filtered_search_bar.vue
'
;
import
RunnerList
from
'
../components/runner_list.vue
'
;
import
RunnerManualSetupHelp
from
'
../components/runner_manual_setup_help.vue
'
;
import
RunnerTypeHelp
from
'
../components/runner_type_help.vue
'
;
import
getRunnersQuery
from
'
../graphql/get_runners.query.graphql
'
;
import
{
fromUrlQueryToSearch
,
fromSearchToUrl
,
fromSearchToVariables
,
}
from
'
./filtered_search_utils
'
;
export
default
{
components
:
{
RunnerFilteredSearchBar
,
RunnerList
,
RunnerManualSetupHelp
,
RunnerTypeHelp
,
...
...
@@ -23,12 +31,16 @@ export default {
},
data
()
{
return
{
search
:
fromUrlQueryToSearch
(),
runners
:
[],
};
},
apollo
:
{
runners
:
{
query
:
getRunnersQuery
,
variables
()
{
return
this
.
variables
;
},
update
({
runners
})
{
return
runners
?.
nodes
||
[];
},
...
...
@@ -38,6 +50,9 @@ export default {
},
},
computed
:
{
variables
()
{
return
fromSearchToVariables
(
this
.
search
);
},
runnersLoading
()
{
return
this
.
$apollo
.
queries
.
runners
.
loading
;
},
...
...
@@ -45,6 +60,16 @@ export default {
return
!
this
.
runnersLoading
&&
!
this
.
runners
.
length
;
},
},
watch
:
{
search
()
{
// TODO Implement back button reponse using onpopstate
updateHistory
({
url
:
fromSearchToUrl
(
this
.
search
),
title
:
document
.
title
,
});
},
},
errorCaptured
(
err
)
{
this
.
captureException
(
err
);
},
...
...
@@ -69,6 +94,8 @@ export default {
</div>
</div>
<runner-filtered-search-bar
v-model=
"search"
namespace=
"admin_runners"
/>
<div
v-if=
"noRunnersFound"
class=
"gl-text-center gl-p-5"
>
{{
__
(
'
No runners found
'
)
}}
</div>
...
...
locale/gitlab.pot
View file @
bd6da5e9
...
...
@@ -28355,6 +28355,18 @@ msgstr ""
msgid "Runners|New runner, has not connected yet"
msgstr ""
msgid "Runners|Not connected"
msgstr ""
msgid "Runners|Offline"
msgstr ""
msgid "Runners|Online"
msgstr ""
msgid "Runners|Paused"
msgstr ""
msgid "Runners|Platform"
msgstr ""
...
...
spec/frontend/runner/components/runner_filtered_search_bar_spec.js
0 → 100644
View file @
bd6da5e9
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
FilteredSearch
from
'
~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
'
;
describe
(
'
RunnerList
'
,
()
=>
{
let
wrapper
;
const
findFilteredSearch
=
()
=>
wrapper
.
findComponent
(
FilteredSearch
);
const
findGlFilteredSearch
=
()
=>
wrapper
.
findComponent
(
GlFilteredSearch
);
const
findSortOptions
=
()
=>
wrapper
.
findAllComponents
(
GlDropdownItem
);
const
mockDefaultSort
=
'
CREATED_DESC
'
;
const
mockOtherSort
=
'
CONTACTED_DESC
'
;
const
mockFilters
=
[
{
type
:
PARAM_KEY_STATUS
,
value
:
{
data
:
'
ACTIVE
'
,
operator
:
'
=
'
}
},
{
type
:
'
filtered-search-term
'
,
value
:
{
data
:
''
}
},
];
const
createComponent
=
({
props
=
{},
options
=
{}
}
=
{})
=>
{
wrapper
=
extendedWrapper
(
shallowMount
(
RunnerFilteredSearchBar
,
{
propsData
:
{
value
:
{
filters
:
[],
sort
:
mockDefaultSort
,
},
...
props
,
},
attrs
:
{
namespace
:
'
runners
'
},
stubs
:
{
FilteredSearch
,
GlFilteredSearch
,
GlDropdown
,
GlDropdownItem
,
},
...
options
,
}),
);
};
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
binds a namespace to the filtered search
'
,
()
=>
{
expect
(
findFilteredSearch
().
props
(
'
namespace
'
)).
toBe
(
'
runners
'
);
});
it
(
'
sets sorting options
'
,
()
=>
{
const
SORT_OPTIONS_COUNT
=
2
;
expect
(
findSortOptions
()).
toHaveLength
(
SORT_OPTIONS_COUNT
);
expect
(
findSortOptions
().
at
(
0
).
text
()).
toBe
(
'
Created date
'
);
expect
(
findSortOptions
().
at
(
1
).
text
()).
toBe
(
'
Last contact
'
);
});
it
(
'
sets tokens
'
,
()
=>
{
expect
(
findFilteredSearch
().
props
(
'
tokens
'
)).
toEqual
([
expect
.
objectContaining
({
type
:
PARAM_KEY_STATUS
,
options
:
expect
.
any
(
Array
),
}),
expect
.
objectContaining
({
type
:
PARAM_KEY_RUNNER_TYPE
,
options
:
expect
.
any
(
Array
),
}),
]);
});
it
(
'
fails validation for v-model with the wrong shape
'
,
()
=>
{
expect
(()
=>
{
createComponent
({
props
:
{
value
:
{
filters
:
'
wrong_filters
'
,
sort
:
'
sort
'
}
}
});
}).
toThrow
(
'
Invalid prop: custom validator check failed
'
);
expect
(()
=>
{
createComponent
({
props
:
{
value
:
{
sort
:
'
sort
'
}
}
});
}).
toThrow
(
'
Invalid prop: custom validator check failed
'
);
});
describe
(
'
when a search is preselected
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
props
:
{
value
:
{
sort
:
mockOtherSort
,
filters
:
mockFilters
,
},
},
});
});
it
(
'
filter values are shown
'
,
()
=>
{
expect
(
findGlFilteredSearch
().
props
(
'
value
'
)).
toEqual
(
mockFilters
);
});
it
(
'
sort option is selected
'
,
()
=>
{
expect
(
findSortOptions
()
.
filter
((
w
)
=>
w
.
props
(
'
isChecked
'
))
.
at
(
0
)
.
text
(),
).
toEqual
(
'
Last contact
'
);
});
});
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
,
},
]);
});
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
,
},
]);
});
});
spec/frontend/runner/components/runner_list_spec.js
View file @
bd6da5e9
import
{
GlLink
,
GlSkeletonLoader
}
from
'
@gitlab/ui
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
GlLink
,
Gl
Table
,
Gl
SkeletonLoader
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
RunnerList
from
'
~/runner/components/runner_list.vue
'
;
...
...
@@ -13,14 +13,15 @@ describe('RunnerList', () => {
const
findActiveRunnersMessage
=
()
=>
wrapper
.
findByTestId
(
'
active-runners-message
'
);
const
findSkeletonLoader
=
()
=>
wrapper
.
findComponent
(
GlSkeletonLoader
);
const
findTable
=
()
=>
wrapper
.
findComponent
(
GlTable
);
const
findHeaders
=
()
=>
wrapper
.
findAll
(
'
th
'
);
const
findRows
=
()
=>
wrapper
.
findAll
(
'
[data-testid^="runner-row-"]
'
);
const
findCell
=
({
row
=
0
,
fieldKey
})
=>
findRows
().
at
(
row
).
find
(
`[data-testid="td-
${
fieldKey
}
"]`
);
const
createComponent
=
({
props
=
{}
}
=
{})
=>
{
const
createComponent
=
({
props
=
{}
}
=
{}
,
mountFn
=
shallowMount
)
=>
{
wrapper
=
extendedWrapper
(
mount
(
RunnerList
,
{
mount
Fn
(
RunnerList
,
{
propsData
:
{
runners
:
mockRunners
,
activeRunnersCount
:
mockActiveRunnersCount
,
...
...
@@ -31,7 +32,7 @@ describe('RunnerList', () => {
};
beforeEach
(()
=>
{
createComponent
();
createComponent
(
{},
mount
);
});
afterEach
(()
=>
{
...
...
@@ -104,12 +105,21 @@ describe('RunnerList', () => {
});
describe
(
'
When data is loading
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
props
:
{
loading
:
true
}
});
it
(
'
shows a busy state
'
,
()
=>
{
createComponent
({
props
:
{
runners
:
[],
loading
:
true
}
});
expect
(
findTable
().
attributes
(
'
busy
'
)).
toBeTruthy
();
});
it
(
'
shows an skeleton loader
'
,
()
=>
{
it
(
'
when there are no runners, shows an skeleton loader
'
,
()
=>
{
createComponent
({
props
:
{
runners
:
[],
loading
:
true
}
},
mount
);
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
true
);
});
it
(
'
when there are runners, shows a busy indicator skeleton loader
'
,
()
=>
{
createComponent
({
props
:
{
loading
:
true
}
},
mount
);
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
false
);
});
});
});
spec/frontend/runner/runner_list/filtered_search_utils_spec.js
0 → 100644
View file @
bd6da5e9
import
{
fromUrlQueryToSearch
,
fromSearchToUrl
,
fromSearchToVariables
,
}
from
'
~/runner/runner_list/filtered_search_utils
'
;
describe
(
'
search_params.js
'
,
()
=>
{
const
examples
=
[
{
name
:
'
a default query
'
,
urlQuery
:
''
,
search
:
{
filters
:
[],
sort
:
'
CREATED_DESC
'
},
graphqlVariables
:
{
sort
:
'
CREATED_DESC
'
},
},
{
name
:
'
a single status
'
,
urlQuery
:
'
?status[]=ACTIVE
'
,
search
:
{
filters
:
[{
type
:
'
status
'
,
value
:
{
data
:
'
ACTIVE
'
,
operator
:
'
=
'
}
}],
sort
:
'
CREATED_DESC
'
,
},
graphqlVariables
:
{
status
:
'
ACTIVE
'
,
sort
:
'
CREATED_DESC
'
},
},
{
name
:
'
single instance type
'
,
urlQuery
:
'
?runner_type[]=INSTANCE_TYPE
'
,
search
:
{
filters
:
[{
type
:
'
runner_type
'
,
value
:
{
data
:
'
INSTANCE_TYPE
'
,
operator
:
'
=
'
}
}],
sort
:
'
CREATED_DESC
'
,
},
graphqlVariables
:
{
type
:
'
INSTANCE_TYPE
'
,
sort
:
'
CREATED_DESC
'
},
},
{
name
:
'
multiple runner status
'
,
urlQuery
:
'
?status[]=ACTIVE&status[]=PAUSED
'
,
search
:
{
filters
:
[
{
type
:
'
status
'
,
value
:
{
data
:
'
ACTIVE
'
,
operator
:
'
=
'
}
},
{
type
:
'
status
'
,
value
:
{
data
:
'
PAUSED
'
,
operator
:
'
=
'
}
},
],
sort
:
'
CREATED_DESC
'
,
},
graphqlVariables
:
{
status
:
'
ACTIVE
'
,
sort
:
'
CREATED_DESC
'
},
},
{
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
:
'
=
'
}
},
],
sort
:
'
CREATED_ASC
'
,
},
graphqlVariables
:
{
status
:
'
ACTIVE
'
,
type
:
'
INSTANCE_TYPE
'
,
sort
:
'
CREATED_ASC
'
},
},
];
describe
(
'
fromUrlQueryToSearch
'
,
()
=>
{
examples
.
forEach
(({
name
,
urlQuery
,
search
})
=>
{
it
(
`Converts
${
name
}
to a search object`
,
()
=>
{
expect
(
fromUrlQueryToSearch
(
urlQuery
)).
toEqual
(
search
);
});
});
});
describe
(
'
fromSearchToUrl
'
,
()
=>
{
examples
.
forEach
(({
name
,
urlQuery
,
search
})
=>
{
it
(
`Converts
${
name
}
to a url`
,
()
=>
{
expect
(
fromSearchToUrl
(
search
)).
toEqual
(
`http://test.host/
${
urlQuery
}
`
);
});
});
it
(
'
When a filtered search parameter is already present, it gets removed
'
,
()
=>
{
const
initialUrl
=
`http://test.host/?status[]=ACTIVE`
;
const
search
=
{
filters
:
[],
sort
:
'
CREATED_DESC
'
};
const
expectedUrl
=
`http://test.host/`
;
expect
(
fromSearchToUrl
(
search
,
initialUrl
)).
toEqual
(
expectedUrl
);
});
it
(
'
When unrelated search parameter is present, it does not get removed
'
,
()
=>
{
const
initialUrl
=
`http://test.host/?unrelated=UNRELATED&status[]=ACTIVE`
;
const
search
=
{
filters
:
[],
sort
:
'
CREATED_DESC
'
};
const
expectedUrl
=
`http://test.host/?unrelated=UNRELATED`
;
expect
(
fromSearchToUrl
(
search
,
initialUrl
)).
toEqual
(
expectedUrl
);
});
});
describe
(
'
fromSearchToVariables
'
,
()
=>
{
examples
.
forEach
(({
name
,
graphqlVariables
,
search
})
=>
{
it
(
`Converts
${
name
}
to a GraphQL query variables object`
,
()
=>
{
expect
(
fromSearchToVariables
(
search
)).
toEqual
(
graphqlVariables
);
});
});
});
});
spec/frontend/runner/runner_list/runner_list_app_spec.js
View file @
bd6da5e9
...
...
@@ -2,12 +2,22 @@ import * as Sentry from '@sentry/browser';
import
{
createLocalVue
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
updateHistory
}
from
'
~/lib/utils/url_utility
'
;
import
RunnerFilteredSearchBar
from
'
~/runner/components/runner_filtered_search_bar.vue
'
;
import
RunnerList
from
'
~/runner/components/runner_list.vue
'
;
import
RunnerManualSetupHelp
from
'
~/runner/components/runner_manual_setup_help.vue
'
;
import
RunnerTypeHelp
from
'
~/runner/components/runner_type_help.vue
'
;
import
{
CREATED_ASC
,
DEFAULT_SORT
,
INSTANCE_TYPE
,
PARAM_KEY_STATUS
,
STATUS_ACTIVE
,
}
from
'
~/runner/constants
'
;
import
getRunnersQuery
from
'
~/runner/graphql/get_runners.query.graphql
'
;
import
RunnerListApp
from
'
~/runner/runner_list/runner_list_app.vue
'
;
...
...
@@ -18,6 +28,10 @@ const mockActiveRunnersCount = 2;
const
mocKRunners
=
runnersData
.
data
.
runners
.
nodes
;
jest
.
mock
(
'
@sentry/browser
'
);
jest
.
mock
(
'
~/lib/utils/url_utility
'
,
()
=>
({
...
jest
.
requireActual
(
'
~/lib/utils/url_utility
'
),
updateHistory
:
jest
.
fn
(),
}));
const
localVue
=
createLocalVue
();
localVue
.
use
(
VueApollo
);
...
...
@@ -25,10 +39,12 @@ localVue.use(VueApollo);
describe
(
'
RunnerListApp
'
,
()
=>
{
let
wrapper
;
let
mockRunnersQuery
;
let
originalLocation
;
const
findRunnerTypeHelp
=
()
=>
wrapper
.
findComponent
(
RunnerTypeHelp
);
const
findRunnerManualSetupHelp
=
()
=>
wrapper
.
findComponent
(
RunnerManualSetupHelp
);
const
findRunnerList
=
()
=>
wrapper
.
findComponent
(
RunnerList
);
const
findRunnerFilteredSearchBar
=
()
=>
wrapper
.
findComponent
(
RunnerFilteredSearchBar
);
const
createComponentWithApollo
=
({
props
=
{},
mountFn
=
shallowMount
}
=
{})
=>
{
const
handlers
=
[[
getRunnersQuery
,
mockRunnersQuery
]];
...
...
@@ -44,7 +60,23 @@ describe('RunnerListApp', () => {
});
};
const
setQuery
=
(
query
)
=>
{
window
.
location
.
href
=
`
${
TEST_HOST
}
/admin/runners/
${
query
}
`
;
window
.
location
.
search
=
query
;
};
beforeAll
(()
=>
{
originalLocation
=
window
.
location
;
Object
.
defineProperty
(
window
,
'
location
'
,
{
writable
:
true
,
value
:
{
href
:
''
,
search
:
''
}
});
});
afterAll
(()
=>
{
window
.
location
=
originalLocation
;
});
beforeEach
(
async
()
=>
{
setQuery
(
''
);
Sentry
.
withScope
.
mockImplementation
((
fn
)
=>
{
const
scope
=
{
setTag
:
jest
.
fn
()
};
fn
(
scope
);
...
...
@@ -64,6 +96,14 @@ describe('RunnerListApp', () => {
expect
(
mocKRunners
).
toMatchObject
(
findRunnerList
().
props
(
'
runners
'
));
});
it
(
'
requests the runners with no filters
'
,
()
=>
{
expect
(
mockRunnersQuery
).
toHaveBeenLastCalledWith
({
status
:
undefined
,
type
:
undefined
,
sort
:
DEFAULT_SORT
,
});
});
it
(
'
shows the runner type help
'
,
()
=>
{
expect
(
findRunnerTypeHelp
().
exists
()).
toBe
(
true
);
});
...
...
@@ -73,6 +113,56 @@ describe('RunnerListApp', () => {
expect
(
findRunnerManualSetupHelp
().
props
(
'
registrationToken
'
)).
toBe
(
mockRegistrationToken
);
});
describe
(
'
when a filter is preselected
'
,
()
=>
{
beforeEach
(
async
()
=>
{
window
.
location
.
search
=
`?status[]=
${
STATUS_ACTIVE
}
&runner_type[]=
${
INSTANCE_TYPE
}
`
;
createComponentWithApollo
();
await
waitForPromises
();
});
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
:
'
=
'
}
},
],
sort
:
'
CREATED_DESC
'
,
});
});
it
(
'
requests the runners with filter parameters
'
,
()
=>
{
expect
(
mockRunnersQuery
).
toHaveBeenLastCalledWith
({
status
:
STATUS_ACTIVE
,
type
:
INSTANCE_TYPE
,
sort
:
DEFAULT_SORT
,
});
});
});
describe
(
'
when a filter is selected by the user
'
,
()
=>
{
beforeEach
(()
=>
{
findRunnerFilteredSearchBar
().
vm
.
$emit
(
'
input
'
,
{
filters
:
[{
type
:
PARAM_KEY_STATUS
,
value
:
{
data
:
'
ACTIVE
'
,
operator
:
'
=
'
}
}],
sort
:
CREATED_ASC
,
});
});
it
(
'
updates the browser url
'
,
()
=>
{
expect
(
updateHistory
).
toHaveBeenLastCalledWith
({
title
:
expect
.
any
(
String
),
url
:
'
http://test.host/admin/runners/?status[]=ACTIVE&sort=CREATED_ASC
'
,
});
});
it
(
'
requests the runners with filters
'
,
()
=>
{
expect
(
mockRunnersQuery
).
toHaveBeenLastCalledWith
({
status
:
STATUS_ACTIVE
,
sort
:
CREATED_ASC
,
});
});
});
describe
(
'
when no runners are found
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mockRunnersQuery
=
jest
.
fn
().
mockResolvedValue
({
data
:
{
runners
:
{
nodes
:
[]
}
}
});
...
...
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