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
ad6b35cc
Commit
ad6b35cc
authored
Mar 22, 2022
by
Payton Burdette
Committed by
Jose Ivan Vargas
Mar 22, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Build jobs filtered search
Add jobs filtered search feature to the jobs page.
parent
8aaf26ce
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
494 additions
and
11 deletions
+494
-11
app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
.../jobs/components/filtered_search/jobs_filtered_search.vue
+42
-0
app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue
...bs/components/filtered_search/tokens/job_status_token.vue
+122
-0
app/assets/javascripts/jobs/components/table/constants.js
app/assets/javascripts/jobs/components/table/constants.js
+3
-0
app/assets/javascripts/jobs/components/table/jobs_table_app.vue
...sets/javascripts/jobs/components/table/jobs_table_app.vue
+61
-7
app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
...ets/javascripts/jobs/components/table/jobs_table_tabs.vue
+1
-1
app/controllers/projects/jobs_controller.rb
app/controllers/projects/jobs_controller.rb
+5
-0
config/feature_flags/development/jobs_table_vue_search.yml
config/feature_flags/development/jobs_table_vue_search.yml
+8
-0
locale/gitlab.pot
locale/gitlab.pot
+42
-0
spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
...s/components/filtered_search/jobs_filtered_search_spec.js
+49
-0
spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
...omponents/filtered_search/tokens/job_status_token_spec.js
+57
-0
spec/frontend/jobs/components/table/job_table_app_spec.js
spec/frontend/jobs/components/table/job_table_app_spec.js
+102
-3
spec/frontend/jobs/mock_data.js
spec/frontend/jobs/mock_data.js
+2
-0
No files found.
app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
0 → 100644
View file @
ad6b35cc
<
script
>
import
{
GlFilteredSearch
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
OPERATOR_IS_ONLY
}
from
'
~/vue_shared/components/filtered_search_bar/constants
'
;
import
JobStatusToken
from
'
./tokens/job_status_token.vue
'
;
export
default
{
tokenTypes
:
{
status
:
'
status
'
,
},
components
:
{
GlFilteredSearch
,
},
computed
:
{
tokens
()
{
return
[
{
type
:
this
.
$options
.
tokenTypes
.
status
,
icon
:
'
status
'
,
title
:
s__
(
'
Jobs|Status
'
),
unique
:
true
,
token
:
JobStatusToken
,
operators
:
OPERATOR_IS_ONLY
,
},
];
},
},
methods
:
{
onSubmit
(
filters
)
{
this
.
$emit
(
'
filterJobsBySearch
'
,
filters
);
},
},
};
</
script
>
<
template
>
<gl-filtered-search
:placeholder=
"s__('Jobs|Filter jobs')"
:available-tokens=
"tokens"
@
submit=
"onSubmit"
/>
</
template
>
app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue
0 → 100644
View file @
ad6b35cc
<
script
>
import
{
GlFilteredSearchToken
,
GlFilteredSearchSuggestion
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
export
default
{
components
:
{
GlFilteredSearchToken
,
GlFilteredSearchSuggestion
,
GlIcon
,
},
props
:
{
config
:
{
type
:
Object
,
required
:
true
,
},
value
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
statuses
()
{
return
[
{
class
:
'
ci-status-icon-canceled
'
,
icon
:
'
status_canceled
'
,
text
:
s__
(
'
Job|Canceled
'
),
value
:
'
CANCELED
'
,
},
{
class
:
'
ci-status-icon-created
'
,
icon
:
'
status_created
'
,
text
:
s__
(
'
Job|Created
'
),
value
:
'
CREATED
'
,
},
{
class
:
'
ci-status-icon-failed
'
,
icon
:
'
status_failed
'
,
text
:
s__
(
'
Job|Failed
'
),
value
:
'
FAILED
'
,
},
{
class
:
'
ci-status-icon-manual
'
,
icon
:
'
status_manual
'
,
text
:
s__
(
'
Job|Manual
'
),
value
:
'
MANUAL
'
,
},
{
class
:
'
ci-status-icon-success
'
,
icon
:
'
status_success
'
,
text
:
s__
(
'
Job|Passed
'
),
value
:
'
SUCCESS
'
,
},
{
class
:
'
ci-status-icon-pending
'
,
icon
:
'
status_pending
'
,
text
:
s__
(
'
Job|Pending
'
),
value
:
'
PENDING
'
,
},
{
class
:
'
ci-status-icon-preparing
'
,
icon
:
'
status_preparing
'
,
text
:
s__
(
'
Job|Preparing
'
),
value
:
'
PREPARING
'
,
},
{
class
:
'
ci-status-icon-running
'
,
icon
:
'
status_running
'
,
text
:
s__
(
'
Job|Running
'
),
value
:
'
RUNNING
'
,
},
{
class
:
'
ci-status-icon-scheduled
'
,
icon
:
'
status_scheduled
'
,
text
:
s__
(
'
Job|Scheduled
'
),
value
:
'
SCHEDULED
'
,
},
{
class
:
'
ci-status-icon-skipped
'
,
icon
:
'
status_skipped
'
,
text
:
s__
(
'
Job|Skipped
'
),
value
:
'
SKIPPED
'
,
},
{
class
:
'
ci-status-icon-waiting-for-resource
'
,
icon
:
'
status-waiting
'
,
text
:
s__
(
'
Job|Waiting for resource
'
),
value
:
'
WAITING_FOR_RESOURCE
'
,
},
];
},
findActiveStatus
()
{
return
this
.
statuses
.
find
((
status
)
=>
status
.
value
===
this
.
value
.
data
);
},
},
};
</
script
>
<
template
>
<gl-filtered-search-token
v-bind=
"
{ ...$props, ...$attrs }" v-on="$listeners">
<template
#view
>
<div
class=
"gl-display-flex gl-align-items-center"
>
<div
:class=
"findActiveStatus.class"
>
<gl-icon
:name=
"findActiveStatus.icon"
class=
"gl-mr-2 gl-display-block"
/>
</div>
<span>
{{
findActiveStatus
.
text
}}
</span>
</div>
</
template
>
<
template
#suggestions
>
<gl-filtered-search-suggestion
v-for=
"(status, index) in statuses"
:key=
"index"
:value=
"status.value"
>
<div
class=
"gl-display-flex"
:class=
"status.class"
>
<gl-icon
:name=
"status.icon"
class=
"gl-mr-3"
/>
<span>
{{
status
.
text
}}
</span>
</div>
</gl-filtered-search-suggestion>
</
template
>
</gl-filtered-search-token>
</template>
app/assets/javascripts/jobs/components/table/constants.js
View file @
ad6b35cc
...
@@ -4,6 +4,9 @@ import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
...
@@ -4,6 +4,9 @@ import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
/* Error constants */
/* Error constants */
export
const
POST_FAILURE
=
'
post_failure
'
;
export
const
POST_FAILURE
=
'
post_failure
'
;
export
const
DEFAULT
=
'
default
'
;
export
const
DEFAULT
=
'
default
'
;
export
const
RAW_TEXT_WARNING
=
s__
(
'
Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.
'
,
);
/* Job Status Constants */
/* Job Status Constants */
export
const
JOB_SCHEDULED
=
'
SCHEDULED
'
;
export
const
JOB_SCHEDULED
=
'
SCHEDULED
'
;
...
...
app/assets/javascripts/jobs/components/table/jobs_table_app.vue
View file @
ad6b35cc
<
script
>
<
script
>
import
{
GlAlert
,
GlSkeletonLoader
,
GlIntersectionObserver
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
,
GlSkeletonLoader
,
GlIntersectionObserver
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
import
createFlash
from
'
~/flash
'
;
import
glFeatureFlagMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
JobsFilteredSearch
from
'
../filtered_search/jobs_filtered_search.vue
'
;
import
eventHub
from
'
./event_hub
'
;
import
eventHub
from
'
./event_hub
'
;
import
GetJobs
from
'
./graphql/queries/get_jobs.query.graphql
'
;
import
GetJobs
from
'
./graphql/queries/get_jobs.query.graphql
'
;
import
JobsTable
from
'
./jobs_table.vue
'
;
import
JobsTable
from
'
./jobs_table.vue
'
;
import
JobsTableEmptyState
from
'
./jobs_table_empty_state.vue
'
;
import
JobsTableEmptyState
from
'
./jobs_table_empty_state.vue
'
;
import
JobsTableTabs
from
'
./jobs_table_tabs.vue
'
;
import
JobsTableTabs
from
'
./jobs_table_tabs.vue
'
;
import
{
RAW_TEXT_WARNING
}
from
'
./constants
'
;
export
default
{
export
default
{
i18n
:
{
i18n
:
{
errorMsg
:
__
(
'
There was an error fetching the jobs for your project.
'
),
errorMsg
:
__
(
'
There was an error fetching the jobs for your project.
'
),
loadingAriaLabel
:
__
(
'
Loading
'
),
loadingAriaLabel
:
__
(
'
Loading
'
),
},
},
filterSearchBoxStyles
:
'
gl-my-0 gl-p-5 gl-bg-gray-10 gl-text-gray-900 gl-border-gray-100 gl-border-b
'
,
components
:
{
components
:
{
GlAlert
,
GlAlert
,
GlSkeletonLoader
,
GlSkeletonLoader
,
JobsFilteredSearch
,
JobsTable
,
JobsTable
,
JobsTableEmptyState
,
JobsTableEmptyState
,
JobsTableTabs
,
JobsTableTabs
,
GlIntersectionObserver
,
GlIntersectionObserver
,
GlLoadingIcon
,
GlLoadingIcon
,
},
},
mixins
:
[
glFeatureFlagMixin
()],
inject
:
{
inject
:
{
fullPath
:
{
fullPath
:
{
default
:
''
,
default
:
''
,
...
@@ -54,19 +62,37 @@ export default {
...
@@ -54,19 +62,37 @@ export default {
hasError
:
false
,
hasError
:
false
,
isAlertDismissed
:
false
,
isAlertDismissed
:
false
,
scope
:
null
,
scope
:
null
,
firstLoad
:
true
,
infiniteScrollingTriggered
:
false
,
filterSearchTriggered
:
false
,
};
};
},
},
computed
:
{
computed
:
{
loading
()
{
return
this
.
$apollo
.
queries
.
jobs
.
loading
;
},
shouldShowAlert
()
{
shouldShowAlert
()
{
return
this
.
hasError
&&
!
this
.
isAlertDismissed
;
return
this
.
hasError
&&
!
this
.
isAlertDismissed
;
},
},
// Show when on All tab with no jobs
// Show only when not loading and filtered search has not been triggered
// So we don't show empty state when results are empty on a filtered search
showEmptyState
()
{
showEmptyState
()
{
return
this
.
jobs
.
list
.
length
===
0
&&
!
this
.
scope
;
return
(
this
.
jobs
.
list
.
length
===
0
&&
!
this
.
scope
&&
!
this
.
loading
&&
!
this
.
filterSearchTriggered
);
},
},
hasNextPage
()
{
hasNextPage
()
{
return
this
.
jobs
?.
pageInfo
?.
hasNextPage
;
return
this
.
jobs
?.
pageInfo
?.
hasNextPage
;
},
},
showLoadingSpinner
()
{
return
this
.
loading
&&
this
.
infiniteScrollingTriggered
;
},
showSkeletonLoader
()
{
return
this
.
loading
&&
!
this
.
showLoadingSpinner
;
},
showFilteredSearch
()
{
return
this
.
glFeatures
?.
jobsTableVueSearch
&&
!
this
.
scope
;
},
},
},
mounted
()
{
mounted
()
{
eventHub
.
$on
(
'
jobActionPerformed
'
,
this
.
handleJobAction
);
eventHub
.
$on
(
'
jobActionPerformed
'
,
this
.
handleJobAction
);
...
@@ -79,16 +105,38 @@ export default {
...
@@ -79,16 +105,38 @@ export default {
this
.
$apollo
.
queries
.
jobs
.
refetch
({
statuses
:
this
.
scope
});
this
.
$apollo
.
queries
.
jobs
.
refetch
({
statuses
:
this
.
scope
});
},
},
fetchJobsByStatus
(
scope
)
{
fetchJobsByStatus
(
scope
)
{
this
.
firstLoad
=
tru
e
;
this
.
infiniteScrollingTriggered
=
fals
e
;
this
.
scope
=
scope
;
this
.
scope
=
scope
;
this
.
$apollo
.
queries
.
jobs
.
refetch
({
statuses
:
scope
});
this
.
$apollo
.
queries
.
jobs
.
refetch
({
statuses
:
scope
});
},
},
filterJobsBySearch
(
filters
)
{
this
.
infiniteScrollingTriggered
=
false
;
this
.
filterSearchTriggered
=
true
;
// Eventually there will be more tokens available
// this code is written to scale for those tokens
filters
.
forEach
((
filter
)
=>
{
// Raw text input in filtered search does not have a type
// when a user enters raw text we alert them that it is
// not supported and we do not make an additional API call
if
(
!
filter
.
type
)
{
createFlash
({
message
:
RAW_TEXT_WARNING
,
type
:
'
warning
'
,
});
}
if
(
filter
.
type
===
'
status
'
)
{
this
.
$apollo
.
queries
.
jobs
.
refetch
({
statuses
:
filter
.
value
.
data
});
}
});
},
fetchMoreJobs
()
{
fetchMoreJobs
()
{
this
.
firstLoad
=
false
;
if
(
!
this
.
loading
)
{
this
.
infiniteScrollingTriggered
=
true
;
if
(
!
this
.
$apollo
.
queries
.
jobs
.
loading
)
{
this
.
$apollo
.
queries
.
jobs
.
fetchMore
({
this
.
$apollo
.
queries
.
jobs
.
fetchMore
({
variables
:
{
variables
:
{
fullPath
:
this
.
fullPath
,
fullPath
:
this
.
fullPath
,
...
@@ -115,7 +163,13 @@ export default {
...
@@ -115,7 +163,13 @@ export default {
<jobs-table-tabs
@
fetchJobsByStatus=
"fetchJobsByStatus"
/>
<jobs-table-tabs
@
fetchJobsByStatus=
"fetchJobsByStatus"
/>
<div
v-if=
"$apollo.loading && firstLoad"
class=
"gl-mt-5"
>
<jobs-filtered-search
v-if=
"showFilteredSearch"
:class=
"$options.filterSearchBoxStyles"
@
filterJobsBySearch=
"filterJobsBySearch"
/>
<div
v-if=
"showSkeletonLoader"
class=
"gl-mt-5"
>
<gl-skeleton-loader
:width=
"1248"
:height=
"73"
>
<gl-skeleton-loader
:width=
"1248"
:height=
"73"
>
<circle
cx=
"748.031"
cy=
"37.7193"
r=
"15.0307"
/>
<circle
cx=
"748.031"
cy=
"37.7193"
r=
"15.0307"
/>
<circle
cx=
"787.241"
cy=
"37.7193"
r=
"15.0307"
/>
<circle
cx=
"787.241"
cy=
"37.7193"
r=
"15.0307"
/>
...
@@ -138,7 +192,7 @@ export default {
...
@@ -138,7 +192,7 @@ export default {
<gl-intersection-observer
v-if=
"hasNextPage"
@
appear=
"fetchMoreJobs"
>
<gl-intersection-observer
v-if=
"hasNextPage"
@
appear=
"fetchMoreJobs"
>
<gl-loading-icon
<gl-loading-icon
v-if=
"
$apollo.loading
"
v-if=
"
showLoadingSpinner
"
size=
"md"
size=
"md"
:aria-label=
"$options.i18n.loadingAriaLabel"
:aria-label=
"$options.i18n.loadingAriaLabel"
/>
/>
...
...
app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
View file @
ad6b35cc
...
@@ -50,7 +50,7 @@ export default {
...
@@ -50,7 +50,7 @@ export default {
</
script
>
</
script
>
<
template
>
<
template
>
<gl-tabs
content-class=
"gl-p
b
-0"
>
<gl-tabs
content-class=
"gl-p
y
-0"
>
<gl-tab
<gl-tab
v-for=
"tab in tabs"
v-for=
"tab in tabs"
:key=
"tab.text"
:key=
"tab.text"
...
...
app/controllers/projects/jobs_controller.rb
View file @
ad6b35cc
...
@@ -18,6 +18,7 @@ class Projects::JobsController < Projects::ApplicationController
...
@@ -18,6 +18,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action
:authorize_create_proxy_build!
,
only: :proxy_websocket_authorize
before_action
:authorize_create_proxy_build!
,
only: :proxy_websocket_authorize
before_action
:verify_proxy_request!
,
only: :proxy_websocket_authorize
before_action
:verify_proxy_request!
,
only: :proxy_websocket_authorize
before_action
:push_jobs_table_vue
,
only:
[
:index
]
before_action
:push_jobs_table_vue
,
only:
[
:index
]
before_action
:push_jobs_table_vue_search
,
only:
[
:index
]
before_action
do
before_action
do
push_frontend_feature_flag
(
:infinitely_collapsible_sections
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:infinitely_collapsible_sections
,
@project
,
default_enabled: :yaml
)
...
@@ -269,4 +270,8 @@ class Projects::JobsController < Projects::ApplicationController
...
@@ -269,4 +270,8 @@ class Projects::JobsController < Projects::ApplicationController
def
push_jobs_table_vue
def
push_jobs_table_vue
push_frontend_feature_flag
(
:jobs_table_vue
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:jobs_table_vue
,
@project
,
default_enabled: :yaml
)
end
end
def
push_jobs_table_vue_search
push_frontend_feature_flag
(
:jobs_table_vue_search
,
@project
,
default_enabled: :yaml
)
end
end
end
config/feature_flags/development/jobs_table_vue_search.yml
0 → 100644
View file @
ad6b35cc
---
name
:
jobs_table_vue_search
introduced_by_url
:
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82539
rollout_issue_url
:
https://gitlab.com/gitlab-org/gitlab/-/issues/356007
milestone
:
'
14.10'
type
:
development
group
:
group::pipeline execution
default_enabled
:
false
locale/gitlab.pot
View file @
ad6b35cc
...
@@ -21372,6 +21372,9 @@ msgstr ""
...
@@ -21372,6 +21372,9 @@ msgstr ""
msgid "Jobs|Create CI/CD configuration file"
msgid "Jobs|Create CI/CD configuration file"
msgstr ""
msgstr ""
msgid "Jobs|Filter jobs"
msgstr ""
msgid "Jobs|Job is stuck. Check runners."
msgid "Jobs|Job is stuck. Check runners."
msgstr ""
msgstr ""
...
@@ -21381,6 +21384,12 @@ msgstr ""
...
@@ -21381,6 +21384,12 @@ msgstr ""
msgid "Jobs|No jobs to show"
msgid "Jobs|No jobs to show"
msgstr ""
msgstr ""
msgid "Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens."
msgstr ""
msgid "Jobs|Status"
msgstr ""
msgid "Jobs|Use jobs to automate your tasks"
msgid "Jobs|Use jobs to automate your tasks"
msgstr ""
msgstr ""
...
@@ -21408,15 +21417,24 @@ msgstr ""
...
@@ -21408,15 +21417,24 @@ msgstr ""
msgid "Job|Cancel"
msgid "Job|Cancel"
msgstr ""
msgstr ""
msgid "Job|Canceled"
msgstr ""
msgid "Job|Complete Raw"
msgid "Job|Complete Raw"
msgstr ""
msgstr ""
msgid "Job|Created"
msgstr ""
msgid "Job|Download"
msgid "Job|Download"
msgstr ""
msgstr ""
msgid "Job|Erase job log and artifacts"
msgid "Job|Erase job log and artifacts"
msgstr ""
msgstr ""
msgid "Job|Failed"
msgstr ""
msgid "Job|Finished at"
msgid "Job|Finished at"
msgstr ""
msgstr ""
...
@@ -21432,9 +21450,27 @@ msgstr ""
...
@@ -21432,9 +21450,27 @@ msgstr ""
msgid "Job|Keep"
msgid "Job|Keep"
msgstr ""
msgstr ""
msgid "Job|Manual"
msgstr ""
msgid "Job|Passed"
msgstr ""
msgid "Job|Pending"
msgstr ""
msgid "Job|Preparing"
msgstr ""
msgid "Job|Retry"
msgid "Job|Retry"
msgstr ""
msgstr ""
msgid "Job|Running"
msgstr ""
msgid "Job|Scheduled"
msgstr ""
msgid "Job|Scroll to bottom"
msgid "Job|Scroll to bottom"
msgstr ""
msgstr ""
...
@@ -21444,6 +21480,9 @@ msgstr ""
...
@@ -21444,6 +21480,9 @@ msgstr ""
msgid "Job|Show complete raw"
msgid "Job|Show complete raw"
msgstr ""
msgstr ""
msgid "Job|Skipped"
msgstr ""
msgid "Job|Status"
msgid "Job|Status"
msgstr ""
msgstr ""
...
@@ -21468,6 +21507,9 @@ msgstr ""
...
@@ -21468,6 +21507,9 @@ msgstr ""
msgid "Job|This job is stuck because you don't have any active runners that can run this job."
msgid "Job|This job is stuck because you don't have any active runners that can run this job."
msgstr ""
msgstr ""
msgid "Job|Waiting for resource"
msgstr ""
msgid "Job|allowed to fail"
msgid "Job|allowed to fail"
msgstr ""
msgstr ""
...
...
spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
0 → 100644
View file @
ad6b35cc
import
{
GlFilteredSearch
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
OPERATOR_IS_ONLY
}
from
'
~/vue_shared/components/filtered_search_bar/constants
'
;
import
JobsFilteredSearch
from
'
~/jobs/components/filtered_search/jobs_filtered_search.vue
'
;
import
{
mockFailedSearchToken
}
from
'
../../mock_data
'
;
describe
(
'
Jobs filtered search
'
,
()
=>
{
let
wrapper
;
const
findFilteredSearch
=
()
=>
wrapper
.
findComponent
(
GlFilteredSearch
);
const
getSearchToken
=
(
type
)
=>
findFilteredSearch
()
.
props
(
'
availableTokens
'
)
.
find
((
token
)
=>
token
.
type
===
type
);
const
findStatusToken
=
()
=>
getSearchToken
(
'
status
'
);
const
createComponent
=
()
=>
{
wrapper
=
shallowMount
(
JobsFilteredSearch
);
};
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
displays filtered search
'
,
()
=>
{
expect
(
findFilteredSearch
().
exists
()).
toBe
(
true
);
});
it
(
'
displays status token
'
,
()
=>
{
expect
(
findStatusToken
()).
toMatchObject
({
type
:
'
status
'
,
icon
:
'
status
'
,
title
:
'
Status
'
,
unique
:
true
,
operators
:
OPERATOR_IS_ONLY
,
});
});
it
(
'
emits filter token to parent component
'
,
()
=>
{
findFilteredSearch
().
vm
.
$emit
(
'
submit
'
,
mockFailedSearchToken
);
expect
(
wrapper
.
emitted
(
'
filterJobsBySearch
'
)).
toEqual
([[
mockFailedSearchToken
]]);
});
});
spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
0 → 100644
View file @
ad6b35cc
import
{
GlFilteredSearchToken
,
GlFilteredSearchSuggestion
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
stubComponent
}
from
'
helpers/stub_component
'
;
import
JobStatusToken
from
'
~/jobs/components/filtered_search/tokens/job_status_token.vue
'
;
describe
(
'
Job Status Token
'
,
()
=>
{
let
wrapper
;
const
findFilteredSearchToken
=
()
=>
wrapper
.
findComponent
(
GlFilteredSearchToken
);
const
findAllFilteredSearchSuggestions
=
()
=>
wrapper
.
findAllComponents
(
GlFilteredSearchSuggestion
);
const
findAllGlIcons
=
()
=>
wrapper
.
findAllComponents
(
GlIcon
);
const
defaultProps
=
{
config
:
{
type
:
'
status
'
,
icon
:
'
status
'
,
title
:
'
Status
'
,
unique
:
true
,
},
value
:
{
data
:
''
,
},
};
const
createComponent
=
()
=>
{
wrapper
=
shallowMount
(
JobStatusToken
,
{
propsData
:
{
...
defaultProps
,
},
stubs
:
{
GlFilteredSearchToken
:
stubComponent
(
GlFilteredSearchToken
,
{
template
:
`<div><slot name="suggestions"></slot></div>`
,
}),
},
});
};
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
passes config correctly
'
,
()
=>
{
expect
(
findFilteredSearchToken
().
props
(
'
config
'
)).
toEqual
(
defaultProps
.
config
);
});
it
(
'
renders all job statuses available
'
,
()
=>
{
const
expectedLength
=
11
;
expect
(
findAllFilteredSearchSuggestions
()).
toHaveLength
(
expectedLength
);
expect
(
findAllGlIcons
()).
toHaveLength
(
expectedLength
);
});
});
spec/frontend/jobs/components/table/job_table_app_spec.js
View file @
ad6b35cc
import
{
GlSkeletonLoader
,
GlAlert
,
GlEmptyState
,
GlIntersectionObserver
}
from
'
@gitlab/ui
'
;
import
{
GlSkeletonLoader
,
GlAlert
,
GlEmptyState
,
GlIntersectionObserver
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
{
s__
}
from
'
~/locale
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
getJobsQuery
from
'
~/jobs/components/table/graphql/queries/get_jobs.query.graphql
'
;
import
getJobsQuery
from
'
~/jobs/components/table/graphql/queries/get_jobs.query.graphql
'
;
import
JobsTable
from
'
~/jobs/components/table/jobs_table.vue
'
;
import
JobsTable
from
'
~/jobs/components/table/jobs_table.vue
'
;
import
JobsTableApp
from
'
~/jobs/components/table/jobs_table_app.vue
'
;
import
JobsTableApp
from
'
~/jobs/components/table/jobs_table_app.vue
'
;
import
JobsTableTabs
from
'
~/jobs/components/table/jobs_table_tabs.vue
'
;
import
JobsTableTabs
from
'
~/jobs/components/table/jobs_table_tabs.vue
'
;
import
{
mockJobsQueryResponse
,
mockJobsQueryEmptyResponse
}
from
'
../../mock_data
'
;
import
JobsFilteredSearch
from
'
~/jobs/components/filtered_search/jobs_filtered_search.vue
'
;
import
{
mockJobsQueryResponse
,
mockJobsQueryEmptyResponse
,
mockFailedSearchToken
,
}
from
'
../../mock_data
'
;
const
projectPath
=
'
gitlab-org/gitlab
'
;
const
projectPath
=
'
gitlab-org/gitlab
'
;
Vue
.
use
(
VueApollo
);
Vue
.
use
(
VueApollo
);
jest
.
mock
(
'
~/flash
'
);
describe
(
'
Job table app
'
,
()
=>
{
describe
(
'
Job table app
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
let
jobsTableVueSearch
=
true
;
const
successHandler
=
jest
.
fn
().
mockResolvedValue
(
mockJobsQueryResponse
);
const
successHandler
=
jest
.
fn
().
mockResolvedValue
(
mockJobsQueryResponse
);
const
failedHandler
=
jest
.
fn
().
mockRejectedValue
(
new
Error
(
'
GraphQL error
'
));
const
failedHandler
=
jest
.
fn
().
mockRejectedValue
(
new
Error
(
'
GraphQL error
'
));
const
emptyHandler
=
jest
.
fn
().
mockResolvedValue
(
mockJobsQueryEmptyResponse
);
const
emptyHandler
=
jest
.
fn
().
mockResolvedValue
(
mockJobsQueryEmptyResponse
);
const
findSkeletonLoader
=
()
=>
wrapper
.
findComponent
(
GlSkeletonLoader
);
const
findSkeletonLoader
=
()
=>
wrapper
.
findComponent
(
GlSkeletonLoader
);
const
findLoadingSpinner
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
const
findTable
=
()
=>
wrapper
.
findComponent
(
JobsTable
);
const
findTable
=
()
=>
wrapper
.
findComponent
(
JobsTable
);
const
findTabs
=
()
=>
wrapper
.
findComponent
(
JobsTableTabs
);
const
findTabs
=
()
=>
wrapper
.
findComponent
(
JobsTableTabs
);
const
findAlert
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
const
findAlert
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
const
findEmptyState
=
()
=>
wrapper
.
findComponent
(
GlEmptyState
);
const
findEmptyState
=
()
=>
wrapper
.
findComponent
(
GlEmptyState
);
const
findFilteredSearch
=
()
=>
wrapper
.
findComponent
(
JobsFilteredSearch
);
const
triggerInfiniteScroll
=
()
=>
const
triggerInfiniteScroll
=
()
=>
wrapper
.
findComponent
(
GlIntersectionObserver
).
vm
.
$emit
(
'
appear
'
);
wrapper
.
findComponent
(
GlIntersectionObserver
).
vm
.
$emit
(
'
appear
'
);
...
@@ -48,6 +66,7 @@ describe('Job table app', () => {
...
@@ -48,6 +66,7 @@ describe('Job table app', () => {
},
},
provide
:
{
provide
:
{
fullPath
:
projectPath
,
fullPath
:
projectPath
,
glFeatures
:
{
jobsTableVueSearch
},
},
},
apolloProvider
:
createMockApolloProvider
(
handler
),
apolloProvider
:
createMockApolloProvider
(
handler
),
});
});
...
@@ -58,11 +77,21 @@ describe('Job table app', () => {
...
@@ -58,11 +77,21 @@ describe('Job table app', () => {
});
});
describe
(
'
loading state
'
,
()
=>
{
describe
(
'
loading state
'
,
()
=>
{
it
(
'
should display skeleton loader when loading
'
,
()
=>
{
beforeEach
(
()
=>
{
createComponent
();
createComponent
();
});
it
(
'
should display skeleton loader when loading
'
,
()
=>
{
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
true
);
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
true
);
expect
(
findTable
().
exists
()).
toBe
(
false
);
expect
(
findTable
().
exists
()).
toBe
(
false
);
expect
(
findLoadingSpinner
().
exists
()).
toBe
(
false
);
});
it
(
'
when switching tabs only the skeleton loader should show
'
,
()
=>
{
findTabs
().
vm
.
$emit
(
'
fetchJobsByStatus
'
,
'
PENDING
'
);
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
true
);
expect
(
findLoadingSpinner
().
exists
()).
toBe
(
false
);
});
});
});
});
...
@@ -76,6 +105,7 @@ describe('Job table app', () => {
...
@@ -76,6 +105,7 @@ describe('Job table app', () => {
it
(
'
should display the jobs table with data
'
,
()
=>
{
it
(
'
should display the jobs table with data
'
,
()
=>
{
expect
(
findTable
().
exists
()).
toBe
(
true
);
expect
(
findTable
().
exists
()).
toBe
(
true
);
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
false
);
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
false
);
expect
(
findLoadingSpinner
().
exists
()).
toBe
(
false
);
});
});
it
(
'
should refetch jobs query on fetchJobsByStatus event
'
,
async
()
=>
{
it
(
'
should refetch jobs query on fetchJobsByStatus event
'
,
async
()
=>
{
...
@@ -98,8 +128,12 @@ describe('Job table app', () => {
...
@@ -98,8 +128,12 @@ describe('Job table app', () => {
});
});
it
(
'
handles infinite scrolling by calling fetch more
'
,
async
()
=>
{
it
(
'
handles infinite scrolling by calling fetch more
'
,
async
()
=>
{
expect
(
findLoadingSpinner
().
exists
()).
toBe
(
true
);
await
waitForPromises
();
await
waitForPromises
();
expect
(
findLoadingSpinner
().
exists
()).
toBe
(
false
);
expect
(
successHandler
).
toHaveBeenCalledWith
({
expect
(
successHandler
).
toHaveBeenCalledWith
({
after
:
'
eyJpZCI6IjIzMTcifQ
'
,
after
:
'
eyJpZCI6IjIzMTcifQ
'
,
fullPath
:
'
gitlab-org/gitlab
'
,
fullPath
:
'
gitlab-org/gitlab
'
,
...
@@ -137,4 +171,69 @@ describe('Job table app', () => {
...
@@ -137,4 +171,69 @@ describe('Job table app', () => {
expect
(
findTable
().
exists
()).
toBe
(
true
);
expect
(
findTable
().
exists
()).
toBe
(
true
);
});
});
});
});
describe
(
'
filtered search
'
,
()
=>
{
it
(
'
should display filtered search
'
,
()
=>
{
createComponent
();
expect
(
findFilteredSearch
().
exists
()).
toBe
(
true
);
});
// this test should be updated once BE supports tab and filtered search filtering
// https://gitlab.com/gitlab-org/gitlab/-/issues/356210
it
.
each
`
scope | shouldDisplay
${
null
}
|
${
true
}
${
'
PENDING
'
}
|
${
false
}
${
'
RUNNING
'
}
|
${
false
}
${[
'
FAILED
'
,
'
SUCCESS
'
,
'
CANCELED
'
]}
|
${
false
}
`
(
'
with tab scope $scope the filtered search displays $shouldDisplay
'
,
async
({
scope
,
shouldDisplay
})
=>
{
createComponent
();
await
findTabs
().
vm
.
$emit
(
'
fetchJobsByStatus
'
,
scope
);
expect
(
findFilteredSearch
().
exists
()).
toBe
(
shouldDisplay
);
},
);
it
(
'
refetches jobs query when filtering
'
,
async
()
=>
{
createComponent
();
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
.
queries
.
jobs
,
'
refetch
'
).
mockImplementation
(
jest
.
fn
());
expect
(
wrapper
.
vm
.
$apollo
.
queries
.
jobs
.
refetch
).
toHaveBeenCalledTimes
(
0
);
await
findFilteredSearch
().
vm
.
$emit
(
'
filterJobsBySearch
'
,
[
mockFailedSearchToken
]);
expect
(
wrapper
.
vm
.
$apollo
.
queries
.
jobs
.
refetch
).
toHaveBeenCalledTimes
(
1
);
});
it
(
'
shows raw text warning when user inputs raw text
'
,
async
()
=>
{
const
expectedWarning
=
{
message
:
s__
(
'
Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.
'
,
),
type
:
'
warning
'
,
};
createComponent
();
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
.
queries
.
jobs
,
'
refetch
'
).
mockImplementation
(
jest
.
fn
());
await
findFilteredSearch
().
vm
.
$emit
(
'
filterJobsBySearch
'
,
[
'
raw text
'
]);
expect
(
createFlash
).
toHaveBeenCalledWith
(
expectedWarning
);
expect
(
wrapper
.
vm
.
$apollo
.
queries
.
jobs
.
refetch
).
toHaveBeenCalledTimes
(
0
);
});
it
(
'
should not display filtered search
'
,
()
=>
{
jobsTableVueSearch
=
false
;
createComponent
();
expect
(
findFilteredSearch
().
exists
()).
toBe
(
false
);
});
});
});
});
spec/frontend/jobs/mock_data.js
View file @
ad6b35cc
...
@@ -1918,3 +1918,5 @@ export const CIJobConnectionExistingCache = {
...
@@ -1918,3 +1918,5 @@ export const CIJobConnectionExistingCache = {
],
],
statuses
:
'
PENDING
'
,
statuses
:
'
PENDING
'
,
};
};
export
const
mockFailedSearchToken
=
{
type
:
'
status
'
,
value
:
{
data
:
'
FAILED
'
,
operator
:
'
=
'
}
};
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