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
198cf31f
Commit
198cf31f
authored
Mar 02, 2020
by
Miguel Rincon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add infinite scrolling to env logs
Integrates logs infinite scroll using pagination.
parent
c9f50b1d
Changes
21
Show whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
770 additions
and
544 deletions
+770
-544
app/assets/javascripts/api.js
app/assets/javascripts/api.js
+0
-35
app/assets/javascripts/logs/components/environment_logs.vue
app/assets/javascripts/logs/components/environment_logs.vue
+77
-21
app/assets/javascripts/logs/components/log_control_buttons.vue
...ssets/javascripts/logs/components/log_control_buttons.vue
+28
-29
app/assets/javascripts/logs/stores/actions.js
app/assets/javascripts/logs/stores/actions.js
+60
-26
app/assets/javascripts/logs/stores/getters.js
app/assets/javascripts/logs/stores/getters.js
+5
-5
app/assets/javascripts/logs/stores/mutation_types.js
app/assets/javascripts/logs/stores/mutation_types.js
+3
-0
app/assets/javascripts/logs/stores/mutations.js
app/assets/javascripts/logs/stores/mutations.js
+42
-10
app/assets/javascripts/logs/stores/state.js
app/assets/javascripts/logs/stores/state.js
+11
-2
app/assets/javascripts/logs/utils.js
app/assets/javascripts/logs/utils.js
+5
-0
app/assets/stylesheets/framework/mixins.scss
app/assets/stylesheets/framework/mixins.scss
+0
-1
app/assets/stylesheets/pages/builds.scss
app/assets/stylesheets/pages/builds.scss
+19
-1
changelogs/unreleased/198050-frontend-pagination-in-log-explorer.yml
...unreleased/198050-frontend-pagination-in-log-explorer.yml
+5
-0
doc/user/project/clusters/kubernetes_pod_logs.md
doc/user/project/clusters/kubernetes_pod_logs.md
+3
-1
ee/spec/features/projects/environments_pod_logs_spec.rb
ee/spec/features/projects/environments_pod_logs_spec.rb
+0
-47
ee/spec/frontend/api_spec.js
ee/spec/frontend/api_spec.js
+0
-71
locale/gitlab.pot
locale/gitlab.pot
+9
-0
spec/frontend/logs/components/environment_logs_spec.js
spec/frontend/logs/components/environment_logs_spec.js
+152
-58
spec/frontend/logs/components/log_control_buttons_spec.js
spec/frontend/logs/components/log_control_buttons_spec.js
+17
-33
spec/frontend/logs/mock_data.js
spec/frontend/logs/mock_data.js
+19
-51
spec/frontend/logs/stores/actions_spec.js
spec/frontend/logs/stores/actions_spec.js
+203
-129
spec/frontend/logs/stores/mutations_spec.js
spec/frontend/logs/stores/mutations_spec.js
+112
-24
No files found.
app/assets/javascripts/api.js
View file @
198cf31f
...
...
@@ -492,41 +492,6 @@ const Api = {
buildUrl
(
url
)
{
return
joinPaths
(
gon
.
relative_url_root
||
''
,
url
.
replace
(
'
:version
'
,
gon
.
api_version
));
},
/**
* Returns pods logs for an environment with an optional pod and container
*
* @param {Object} params
* @param {Object} param.environment - Environment object
* @param {string=} params.podName - Pod name, if not set the backend assumes a default one
* @param {string=} params.containerName - Container name, if not set the backend assumes a default one
* @param {string=} params.start - Starting date to query the logs in ISO format
* @param {string=} params.end - Ending date to query the logs in ISO format
* @returns {Promise} Axios promise for the result of a GET request of logs
*/
getPodLogs
({
environment
,
podName
,
containerName
,
search
,
start
,
end
})
{
const
url
=
this
.
buildUrl
(
environment
.
logs_api_path
);
const
params
=
{};
if
(
podName
)
{
params
.
pod_name
=
podName
;
}
if
(
containerName
)
{
params
.
container_name
=
containerName
;
}
if
(
search
)
{
params
.
search
=
search
;
}
if
(
start
)
{
params
.
start
=
start
;
}
if
(
end
)
{
params
.
end
=
end
;
}
return
axios
.
get
(
url
,
{
params
});
},
};
export
default
Api
;
app/assets/javascripts/logs/components/environment_logs.vue
View file @
198cf31f
<
script
>
import
{
throttle
}
from
'
lodash
'
;
import
{
mapActions
,
mapState
,
mapGetters
}
from
'
vuex
'
;
import
{
GlDropdown
,
GlDropdownItem
,
GlFormGroup
,
GlSearchBoxByClick
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
GlSprintf
,
GlAlert
,
GlDropdown
,
GlDropdownItem
,
GlFormGroup
,
GlSearchBoxByClick
,
GlInfiniteScroll
,
}
from
'
@gitlab/ui
'
;
import
DateTimePicker
from
'
~/vue_shared/components/date_time_picker/date_time_picker.vue
'
;
import
{
scrollDown
}
from
'
~/lib/utils/scroll_utils
'
;
import
LogControlButtons
from
'
./log_control_buttons.vue
'
;
import
{
timeRanges
,
defaultTimeRange
}
from
'
~/monitoring/constants
'
;
import
{
timeRangeFromUrl
}
from
'
~/monitoring/utils
'
;
import
{
formatDate
}
from
'
../utils
'
;
export
default
{
components
:
{
GlSprintf
,
GlAlert
,
GlDropdown
,
GlDropdownItem
,
GlFormGroup
,
GlSearchBoxByClick
,
GlInfiniteScroll
,
DateTimePicker
,
LogControlButtons
,
},
filters
:
{
formatDate
,
},
props
:
{
environmentName
:
{
type
:
String
,
...
...
@@ -39,11 +53,13 @@ export default {
required
:
true
,
},
},
traceHeight
:
600
,
data
()
{
return
{
searchQuery
:
''
,
timeRanges
,
isElasticStackCalloutDismissed
:
false
,
scrollDownButtonDisabled
:
true
,
};
},
computed
:
{
...
...
@@ -52,7 +68,7 @@ export default {
timeRangeModel
:
{
get
()
{
return
this
.
timeRange
.
current
;
return
this
.
timeRange
.
selected
;
},
set
(
val
)
{
this
.
setTimeRange
(
val
);
...
...
@@ -60,7 +76,7 @@ export default {
},
showLoader
()
{
return
this
.
logs
.
isLoading
||
!
this
.
logs
.
isComplete
;
return
this
.
logs
.
isLoading
;
},
advancedFeaturesEnabled
()
{
const
environment
=
this
.
environments
.
options
.
find
(
...
...
@@ -75,16 +91,6 @@ export default {
return
!
this
.
isElasticStackCalloutDismissed
&&
this
.
disableAdvancedControls
;
},
},
watch
:
{
trace
(
val
)
{
this
.
$nextTick
(()
=>
{
if
(
val
)
{
scrollDown
();
}
this
.
$refs
.
scrollButtons
.
update
();
});
},
},
mounted
()
{
this
.
setInitData
({
timeRange
:
timeRangeFromUrl
()
||
defaultTimeRange
,
...
...
@@ -102,12 +108,26 @@ export default {
'
showPodLogs
'
,
'
showEnvironment
'
,
'
fetchEnvironments
'
,
'
fetchMoreLogsPrepend
'
,
]),
topReached
()
{
if
(
!
this
.
logs
.
isLoading
)
{
this
.
fetchMoreLogsPrepend
();
}
},
scrollDown
()
{
this
.
$refs
.
infiniteScroll
.
scrollDown
();
},
scroll
:
throttle
(
function
scrollThrottled
({
target
=
{}
})
{
const
{
scrollTop
=
0
,
clientHeight
=
0
,
scrollHeight
=
0
}
=
target
;
this
.
scrollDownButtonDisabled
=
scrollTop
+
clientHeight
===
scrollHeight
;
},
200
),
},
};
</
script
>
<
template
>
<div
class=
"
build-page-pod-logs
mt-3"
>
<div
class=
"
environment-logs-viewer
mt-3"
>
<gl-alert
v-if=
"shouldShowElasticStackCallout"
class=
"mb-3 js-elasticsearch-alert"
...
...
@@ -209,14 +229,50 @@ export default {
<log-control-buttons
ref=
"scrollButtons"
class=
"controllers align-self-end mb-1"
:scroll-down-button-disabled=
"scrollDownButtonDisabled"
@
refresh=
"showPodLogs(pods.current)"
@
scrollDown=
"scrollDown"
/>
</div>
<pre
class=
"build-trace js-log-trace"
><code
class=
"bash js-build-output"
>
{{
trace
}}
<div
v-if=
"showLoader"
class=
"build-loader-animation js-build-loader-animation"
>
<gl-infinite-scroll
ref=
"infiniteScroll"
class=
"log-lines"
:style=
"
{ height: `${$options.traceHeight}px` }"
:max-list-height="$options.traceHeight"
:fetched-items="logs.lines.length"
@topReached="topReached"
@scroll="scroll"
>
<template
#items
>
<pre
class=
"build-trace js-log-trace"
><code
class=
"bash js-build-output"
><div
v-if=
"showLoader"
class=
"build-loader-animation js-build-loader-animation"
>
<div
class=
"dot"
></div>
<div
class=
"dot"
></div>
<div
class=
"dot"
></div>
</div></code></pre>
</div>
{{
trace
}}
</code></pre>
</
template
>
<
template
#default
><div></div
></
template
>
</gl-infinite-scroll>
<div
ref=
"logFooter"
class=
"log-footer py-2 px-3"
>
<gl-sprintf
:message=
"s__('Environments|Logs from %{start} to %{end}.')"
>
<
template
#start
>
{{
timeRange
.
current
.
start
|
formatDate
}}
</
template
>
<
template
#end
>
{{
timeRange
.
current
.
end
|
formatDate
}}
</
template
>
</gl-sprintf>
<gl-sprintf
v-if=
"!logs.isComplete"
:message=
"s__('Environments|Currently showing %{fetched} results.')"
>
<
template
#fetched
>
{{
logs
.
lines
.
length
}}
</
template
>
</gl-sprintf>
<
template
v-else
>
{{
s__
(
'
Environments|Currently showing all results.
'
)
}}
</
template
>
</div>
</div>
</template>
app/assets/javascripts/logs/components/log_control_buttons.vue
View file @
198cf31f
<
script
>
import
{
GlButton
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
canScroll
,
isScrolledToTop
,
isScrolledToBottom
,
scrollDown
,
scrollUp
,
}
from
'
~/lib/utils/scroll_utils
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
...
...
@@ -17,32 +10,34 @@ export default {
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
scrollUpButtonDisabled
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
scrollDownButtonDisabled
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
data
()
{
return
{
scroll
ToTopEnabled
:
false
,
scroll
ToBottomEnabled
:
false
,
scroll
UpAvailable
:
Boolean
(
this
.
$listeners
.
scrollUp
)
,
scroll
DownAvailable
:
Boolean
(
this
.
$listeners
.
scrollDown
)
,
};
},
created
()
{
window
.
addEventListener
(
'
scroll
'
,
this
.
update
);
},
destroyed
()
{
window
.
removeEventListener
(
'
scroll
'
,
this
.
update
);
},
methods
:
{
/**
* Checks if page can be scrolled and updates
* enabled/disabled state of buttons accordingly
*/
update
()
{
this
.
scrollToTopEnabled
=
canScroll
()
&&
!
isScrolledToTop
();
this
.
scrollToBottomEnabled
=
canScroll
()
&&
!
isScrolledToBottom
();
},
handleRefreshClick
()
{
this
.
$emit
(
'
refresh
'
);
},
scrollUp
,
scrollDown
,
handleScrollUp
()
{
this
.
$emit
(
'
scrollUp
'
);
},
handleScrollDown
()
{
this
.
$emit
(
'
scrollDown
'
);
},
},
};
</
script
>
...
...
@@ -50,6 +45,7 @@ export default {
<
template
>
<div>
<div
v-if=
"scrollUpAvailable"
v-gl-tooltip
class=
"controllers-buttons"
:title=
"__('Scroll to top')"
...
...
@@ -59,13 +55,15 @@ export default {
id=
"scroll-to-top"
class=
"btn-blank js-scroll-to-top"
:aria-label=
"__('Scroll to top')"
:disabled=
"
!scrollToTopEn
abled"
@
click=
"
s
crollUp()"
:disabled=
"
scrollUpButtonDis
abled"
@
click=
"
handleS
crollUp()"
><icon
name=
"scroll_up"
/></gl-button>
</div>
<div
v-if=
"scrollDownAvailable"
v-gl-tooltip
:disabled=
"scrollUpButtonDisabled"
class=
"controllers-buttons"
:title=
"__('Scroll to bottom')"
aria-labelledby=
"scroll-to-bottom"
...
...
@@ -74,8 +72,9 @@ export default {
id=
"scroll-to-bottom"
class=
"btn-blank js-scroll-to-bottom"
:aria-label=
"__('Scroll to bottom')"
:disabled=
"!scrollToBottomEnabled"
@
click=
"scrollDown()"
:v-if=
"scrollDownAvailable"
:disabled=
"scrollDownButtonDisabled"
@
click=
"handleScrollDown()"
><icon
name=
"scroll_down"
/></gl-button>
</div>
...
...
app/assets/javascripts/logs/stores/actions.js
View file @
198cf31f
import
Api
from
'
~/api
'
;
import
{
backOff
}
from
'
~/lib/utils/common_utils
'
;
import
httpStatusCodes
from
'
~/lib/utils/http_status
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
...
...
@@ -16,9 +15,10 @@ const flashLogsError = () => {
flash
(
s__
(
'
Metrics|There was an error fetching the logs, please try again
'
));
};
const
request
LogsUntilData
=
params
=>
const
request
UntilData
=
(
url
,
params
)
=>
backOff
((
next
,
stop
)
=>
{
Api
.
getPodLogs
(
params
)
axios
.
get
(
url
,
{
params
})
.
then
(
res
=>
{
if
(
res
.
status
===
httpStatusCodes
.
ACCEPTED
)
{
next
();
...
...
@@ -31,10 +31,36 @@ const requestLogsUntilData = params =>
});
});
const
requestLogsUntilData
=
state
=>
{
const
params
=
{};
const
{
logs_api_path
}
=
state
.
environments
.
options
.
find
(
({
name
})
=>
name
===
state
.
environments
.
current
,
);
if
(
state
.
pods
.
current
)
{
params
.
pod_name
=
state
.
pods
.
current
;
}
if
(
state
.
search
)
{
params
.
search
=
state
.
search
;
}
if
(
state
.
timeRange
.
current
)
{
try
{
const
{
start
,
end
}
=
convertToFixedRange
(
state
.
timeRange
.
current
);
params
.
start
=
start
;
params
.
end
=
end
;
}
catch
{
flashTimeRangeWarning
();
}
}
if
(
state
.
logs
.
cursor
)
{
params
.
cursor
=
state
.
logs
.
cursor
;
}
return
requestUntilData
(
logs_api_path
,
params
);
};
export
const
setInitData
=
({
commit
},
{
timeRange
,
environmentName
,
podName
})
=>
{
if
(
timeRange
)
{
commit
(
types
.
SET_TIME_RANGE
,
timeRange
);
}
commit
(
types
.
SET_PROJECT_ENVIRONMENT
,
environmentName
);
commit
(
types
.
SET_CURRENT_POD_NAME
,
podName
);
};
...
...
@@ -60,10 +86,15 @@ export const showEnvironment = ({ dispatch, commit }, environmentName) => {
dispatch
(
'
fetchLogs
'
);
};
/**
* Fetch environments data and initial logs
* @param {Object} store
* @param {String} environmentsPath
*/
export
const
fetchEnvironments
=
({
commit
,
dispatch
},
environmentsPath
)
=>
{
commit
(
types
.
REQUEST_ENVIRONMENTS_DATA
);
axios
return
axios
.
get
(
environmentsPath
)
.
then
(({
data
})
=>
{
commit
(
types
.
RECEIVE_ENVIRONMENTS_DATA_SUCCESS
,
data
.
environments
);
...
...
@@ -76,32 +107,16 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
};
export
const
fetchLogs
=
({
commit
,
state
})
=>
{
const
params
=
{
environment
:
state
.
environments
.
options
.
find
(({
name
})
=>
name
===
state
.
environments
.
current
),
podName
:
state
.
pods
.
current
,
search
:
state
.
search
,
};
if
(
state
.
timeRange
.
current
)
{
try
{
const
{
start
,
end
}
=
convertToFixedRange
(
state
.
timeRange
.
current
);
params
.
start
=
start
;
params
.
end
=
end
;
}
catch
{
flashTimeRangeWarning
();
}
}
commit
(
types
.
REQUEST_PODS_DATA
);
commit
(
types
.
REQUEST_LOGS_DATA
);
return
requestLogsUntilData
(
params
)
return
requestLogsUntilData
(
state
)
.
then
(({
data
})
=>
{
const
{
pod_name
,
pods
,
logs
}
=
data
;
const
{
pod_name
,
pods
,
logs
,
cursor
}
=
data
;
commit
(
types
.
SET_CURRENT_POD_NAME
,
pod_name
);
commit
(
types
.
RECEIVE_PODS_DATA_SUCCESS
,
pods
);
commit
(
types
.
RECEIVE_LOGS_DATA_SUCCESS
,
logs
);
commit
(
types
.
RECEIVE_LOGS_DATA_SUCCESS
,
{
logs
,
cursor
}
);
})
.
catch
(()
=>
{
commit
(
types
.
RECEIVE_PODS_DATA_ERROR
);
...
...
@@ -110,5 +125,24 @@ export const fetchLogs = ({ commit, state }) => {
});
};
export
const
fetchMoreLogsPrepend
=
({
commit
,
state
})
=>
{
if
(
state
.
logs
.
isComplete
)
{
// return when all logs are loaded
return
Promise
.
resolve
();
}
commit
(
types
.
REQUEST_LOGS_DATA_PREPEND
);
return
requestLogsUntilData
(
state
)
.
then
(({
data
})
=>
{
const
{
logs
,
cursor
}
=
data
;
commit
(
types
.
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
,
{
logs
,
cursor
});
})
.
catch
(()
=>
{
commit
(
types
.
RECEIVE_LOGS_DATA_PREPEND_ERROR
);
flashLogsError
();
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
app/assets/javascripts/logs/stores/getters.js
View file @
198cf31f
import
dateFormat
from
'
dateformat
'
;
import
{
formatDate
}
from
'
../utils
'
;
export
const
trace
=
state
=>
state
.
logs
.
lines
.
map
(
item
=>
[
dateFormat
(
item
.
timestamp
,
'
UTC:mmm dd HH:MM:ss.l"Z"
'
),
item
.
message
].
join
(
'
|
'
))
.
join
(
'
\n
'
);
const
mapTrace
=
({
timestamp
=
null
,
message
=
''
})
=>
[
timestamp
?
formatDate
(
timestamp
)
:
''
,
message
].
join
(
'
|
'
);
export
const
trace
=
state
=>
state
.
logs
.
lines
.
map
(
mapTrace
)
.
join
(
'
\n
'
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
app/assets/javascripts/logs/stores/mutation_types.js
View file @
198cf31f
...
...
@@ -10,6 +10,9 @@ export const RECEIVE_ENVIRONMENTS_DATA_ERROR = 'RECEIVE_ENVIRONMENTS_DATA_ERROR'
export
const
REQUEST_LOGS_DATA
=
'
REQUEST_LOGS_DATA
'
;
export
const
RECEIVE_LOGS_DATA_SUCCESS
=
'
RECEIVE_LOGS_DATA_SUCCESS
'
;
export
const
RECEIVE_LOGS_DATA_ERROR
=
'
RECEIVE_LOGS_DATA_ERROR
'
;
export
const
REQUEST_LOGS_DATA_PREPEND
=
'
REQUEST_LOGS_DATA_PREPEND
'
;
export
const
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
=
'
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
'
;
export
const
RECEIVE_LOGS_DATA_PREPEND_ERROR
=
'
RECEIVE_LOGS_DATA_PREPEND_ERROR
'
;
export
const
REQUEST_PODS_DATA
=
'
REQUEST_PODS_DATA
'
;
export
const
RECEIVE_PODS_DATA_SUCCESS
=
'
RECEIVE_PODS_DATA_SUCCESS
'
;
...
...
app/assets/javascripts/logs/stores/mutations.js
View file @
198cf31f
import
*
as
types
from
'
./mutation_types
'
;
import
{
convertToFixedRange
}
from
'
~/lib/utils/datetime_range
'
;
const
mapLine
=
({
timestamp
,
message
})
=>
({
timestamp
,
message
,
});
export
default
{
/
** Search data */
/
/ Search Data
[
types
.
SET_SEARCH
](
state
,
searchQuery
)
{
state
.
search
=
searchQuery
;
},
/
** Time Range data */
/
/ Time Range Data
[
types
.
SET_TIME_RANGE
](
state
,
timeRange
)
{
state
.
timeRange
.
current
=
timeRange
;
state
.
timeRange
.
selected
=
timeRange
;
state
.
timeRange
.
current
=
convertToFixedRange
(
timeRange
);
},
/
** Environments data */
/
/ Environments Data
[
types
.
SET_PROJECT_ENVIRONMENT
](
state
,
environmentName
)
{
state
.
environments
.
current
=
environmentName
;
},
...
...
@@ -28,24 +35,49 @@ export default {
state
.
environments
.
isLoading
=
false
;
},
/
** Logs data */
/
/ Logs data
[
types
.
REQUEST_LOGS_DATA
](
state
)
{
state
.
timeRange
.
current
=
convertToFixedRange
(
state
.
timeRange
.
selected
);
state
.
logs
.
lines
=
[];
state
.
logs
.
isLoading
=
true
;
// start pagination from the beginning
state
.
logs
.
cursor
=
null
;
state
.
logs
.
isComplete
=
false
;
},
[
types
.
RECEIVE_LOGS_DATA_SUCCESS
](
state
,
lines
)
{
state
.
logs
.
lines
=
l
ines
;
[
types
.
RECEIVE_LOGS_DATA_SUCCESS
](
state
,
{
logs
=
[],
cursor
}
)
{
state
.
logs
.
lines
=
l
ogs
.
map
(
mapLine
)
;
state
.
logs
.
isLoading
=
false
;
state
.
logs
.
cursor
=
cursor
;
if
(
!
cursor
)
{
state
.
logs
.
isComplete
=
true
;
}
},
[
types
.
RECEIVE_LOGS_DATA_ERROR
](
state
)
{
state
.
logs
.
lines
=
[];
state
.
logs
.
isLoading
=
false
;
},
[
types
.
REQUEST_LOGS_DATA_PREPEND
](
state
)
{
state
.
logs
.
isLoading
=
true
;
},
[
types
.
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
](
state
,
{
logs
=
[],
cursor
})
{
const
lines
=
logs
.
map
(
mapLine
);
state
.
logs
.
lines
=
lines
.
concat
(
state
.
logs
.
lines
);
state
.
logs
.
isLoading
=
false
;
state
.
logs
.
cursor
=
cursor
;
if
(
!
cursor
)
{
state
.
logs
.
isComplete
=
true
;
}
},
[
types
.
RECEIVE_LOGS_DATA_PREPEND_ERROR
](
state
)
{
state
.
logs
.
isLoading
=
false
;
},
/
** Pods data */
/
/ Pods data
[
types
.
SET_CURRENT_POD_NAME
](
state
,
podName
)
{
state
.
pods
.
current
=
podName
;
},
...
...
app/assets/javascripts/logs/stores/state.js
View file @
198cf31f
import
{
timeRanges
,
defaultTimeRange
}
from
'
~/monitoring/constants
'
;
import
{
convertToFixedRange
}
from
'
~/lib/utils/datetime_range
'
;
export
default
()
=>
({
/**
...
...
@@ -11,7 +12,10 @@ export default () => ({
*/
timeRange
:
{
options
:
timeRanges
,
current
:
defaultTimeRange
,
// Selected time range, can be fixed or relative
selected
:
defaultTimeRange
,
// Current time range, must be fixed
current
:
convertToFixedRange
(
defaultTimeRange
),
},
/**
...
...
@@ -29,7 +33,12 @@ export default () => ({
logs
:
{
lines
:
[],
isLoading
:
false
,
isComplete
:
true
,
/**
* Logs `cursor` represents the current pagination position,
* Should be sent in next batch (page) of logs to be fetched
*/
cursor
:
null
,
isComplete
:
false
,
},
/**
...
...
app/assets/javascripts/logs/utils.js
View file @
198cf31f
import
{
secondsToMilliseconds
}
from
'
~/lib/utils/datetime_utility
'
;
import
dateFormat
from
'
dateformat
'
;
const
dateFormatMask
=
'
UTC:mmm dd HH:MM:ss.l"Z"
'
;
/**
* Returns a time range (`start`, `end`) where `start` is the
...
...
@@ -20,4 +23,6 @@ export const getTimeRange = (seconds = 0) => {
};
};
export
const
formatDate
=
timestamp
=>
dateFormat
(
timestamp
,
dateFormatMask
);
export
default
{};
app/assets/stylesheets/framework/mixins.scss
View file @
198cf31f
...
...
@@ -257,7 +257,6 @@
width
:
15px
;
height
:
15px
;
display
:
$svg-display
;
fill
:
$gl-text-color
;
top
:
$svg-top
;
}
...
...
app/assets/stylesheets/pages/builds.scss
View file @
198cf31f
...
...
@@ -358,17 +358,30 @@
}
}
.
build-page-pod-logs
{
.
environment-logs-viewer
{
.build-trace-container
{
position
:
relative
;
}
.log-lines
,
.gl-infinite-scroll-container
{
// makes scrollbar visible by creating contrast
background
:
$black
;
}
.gl-infinite-scroll-legend
{
margin
:
0
;
}
.build-trace
{
@include
build-trace
();
margin
:
0
;
}
.top-bar
{
@include
build-trace-top-bar
(
$gl-line-height
*
5
);
position
:
relative
;
top
:
0
;
.dropdown-menu-toggle
{
width
:
200px
;
...
...
@@ -395,4 +408,9 @@
.build-loader-animation
{
@include
build-loader-animation
;
}
.log-footer
{
color
:
$white-normal
;
background-color
:
$gray-900
;
}
}
changelogs/unreleased/198050-frontend-pagination-in-log-explorer.yml
0 → 100644
View file @
198cf31f
---
title
:
More logs entries are loaded when logs are scrolled to the top
merge_request
:
26254
author
:
type
:
added
doc/user/project/clusters/kubernetes_pod_logs.md
View file @
198cf31f
...
...
@@ -46,13 +46,15 @@ Logs can be displayed by clicking on a specific pod from [Deploy Boards](../depl
### Logs view
The logs view
will contain the last 500 lines for a pod, and has control to filter through
:
The logs view
lets you filter the logs by
:
-
Pods.
-
[
From GitLab 12.4
](
https://gitlab.com/gitlab-org/gitlab/issues/5769
)
, environments.
-
[
From GitLab 12.7
](
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21656
)
,
[
full text search
](
#full-text-search
)
.
-
[
From GitLab 12.8
](
https://gitlab.com/gitlab-org/gitlab/issues/197879
)
, dates.
Loading more than 500 log lines is possible from
[
GitLab 12.9
](
https://gitlab.com/gitlab-org/gitlab/-/issues/198050
)
onwards.
Support for pods with multiple containers is coming
[
in a future release
](
https://gitlab.com/gitlab-org/gitlab/issues/13404
)
.
Support for historical data is coming
[
in a future release
](
https://gitlab.com/gitlab-org/gitlab/issues/196191
)
.
...
...
ee/spec/features/projects/environments_pod_logs_spec.rb
View file @
198cf31f
...
...
@@ -5,8 +5,6 @@ require 'spec_helper'
describe
'Environment > Pod Logs'
,
:js
do
include
KubernetesHelpers
SCROLL_DISTANCE
=
400
let
(
:pod_names
)
{
%w(kube-pod)
}
let
(
:pod_name
)
{
pod_names
.
first
}
let
(
:project
)
{
create
(
:project
,
:repository
)
}
...
...
@@ -62,49 +60,4 @@ describe 'Environment > Pod Logs', :js do
expect
(
page
).
to
have_content
(
"Dec 13 14:04:22.123Z | Log 1 Dec 13 14:04:23.123Z | Log 2 Dec 13 14:04:24.123Z | Log 3"
)
end
end
context
'with perf bar enabled'
do
before
do
allow
(
Gitlab
::
PerformanceBar
).
to
receive
(
:enabled_for_request?
).
and_return
(
true
)
end
it
'log header sticks to top'
do
load_and_scroll_down
expect
(
log_header_top
).
to
eq
(
navbar_height
+
perf_bar_height
)
end
end
context
'with perf bar disabled'
do
it
'log header sticks to top'
do
load_and_scroll_down
expect
(
log_header_top
).
to
eq
(
navbar_height
)
end
end
def
load_and_scroll_down
visit
project_logs_path
(
environment
.
project
,
environment_name:
environment
.
name
,
pod_name:
pod_name
)
wait_for_requests
scroll_down_build_log
end
def
scroll_down_build_log
page
.
execute_script
(
"$('.js-build-output').height('200vh')"
)
page
.
execute_script
(
"window.scrollTo(0,
#{
SCROLL_DISTANCE
}
)"
)
end
def
perf_bar_height
page
.
evaluate_script
(
"$('#js-peek').height()"
).
to_i
end
def
navbar_height
page
.
evaluate_script
(
"$('.js-navbar').height()"
).
to_i
end
def
log_header_top
page
.
evaluate_script
(
"$('.js-top-bar').offset().top"
)
-
SCROLL_DISTANCE
end
end
ee/spec/frontend/api_spec.js
View file @
198cf31f
...
...
@@ -165,77 +165,6 @@ describe('Api', () => {
});
});
describe
(
'
getPodLogs
'
,
()
=>
{
const
projectPath
=
'
/root/test-project
'
;
const
podName
=
'
pod
'
;
const
containerName
=
'
container
'
;
const
search
=
'
foo +bar
'
;
const
expectedUrl
=
'
/gitlab/dummy_api_path.json
'
;
const
environment
=
{
enable_advanced_logs_querying
:
false
,
project_path
:
projectPath
,
logs_api_path
:
'
/dummy_api_path.json
'
,
};
const
getRequest
=
()
=>
mock
.
history
.
get
[
0
];
beforeEach
(()
=>
{
mock
.
onAny
().
reply
(
200
);
});
afterEach
(()
=>
{
mock
.
reset
();
});
it
(
'
calls `axios.get` with pod_name and container_name
'
,
done
=>
{
Api
.
getPodLogs
({
environment
,
podName
,
containerName
})
.
then
(()
=>
{
expect
(
getRequest
().
url
).
toBe
(
expectedUrl
);
expect
(
getRequest
().
params
).
toEqual
({
pod_name
:
podName
,
container_name
:
containerName
,
});
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
calls `axios.get` without pod_name and container_name
'
,
done
=>
{
Api
.
getPodLogs
({
environment
})
.
then
(()
=>
{
expect
(
getRequest
().
url
).
toBe
(
expectedUrl
);
expect
(
getRequest
().
params
).
toEqual
({});
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
calls `axios.get` with pod_name
'
,
done
=>
{
Api
.
getPodLogs
({
environment
,
podName
})
.
then
(()
=>
{
expect
(
getRequest
().
url
).
toBe
(
expectedUrl
);
expect
(
getRequest
().
params
).
toEqual
({
pod_name
:
podName
,
});
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
calls `axios.get` with pod_name and search
'
,
done
=>
{
Api
.
getPodLogs
({
environment
,
podName
,
search
})
.
then
(()
=>
{
expect
(
getRequest
().
url
).
toBe
(
expectedUrl
);
expect
(
getRequest
().
params
).
toEqual
({
pod_name
:
podName
,
search
,
});
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
describe
(
'
packages
'
,
()
=>
{
const
projectId
=
'
project_a
'
;
const
packageId
=
'
package_b
'
;
...
...
locale/gitlab.pot
View file @
198cf31f
...
...
@@ -7648,6 +7648,12 @@ msgstr ""
msgid "Environments|Commit"
msgstr ""
msgid "Environments|Currently showing %{fetched} results."
msgstr ""
msgid "Environments|Currently showing all results."
msgstr ""
msgid "Environments|Deploy to..."
msgstr ""
...
...
@@ -7681,6 +7687,9 @@ msgstr ""
msgid "Environments|Logs from"
msgstr ""
msgid "Environments|Logs from %{start} to %{end}."
msgstr ""
msgid "Environments|New environment"
msgstr ""
...
...
spec/frontend/logs/components/environment_logs_spec.js
View file @
198cf31f
import
Vue
from
'
vue
'
;
import
{
GlDropdown
,
GlDropdownItem
,
GlSearchBoxByClick
}
from
'
@gitlab/ui
'
;
import
{
Gl
Sprintf
,
Gl
Dropdown
,
GlDropdownItem
,
GlSearchBoxByClick
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
DateTimePicker
from
'
~/vue_shared/components/date_time_picker/date_time_picker.vue
'
;
import
EnvironmentLogs
from
'
~/logs/components/environment_logs.vue
'
;
...
...
@@ -20,9 +20,18 @@ import {
jest
.
mock
(
'
~/lib/utils/scroll_utils
'
);
const
module
=
'
environmentLogs
'
;
jest
.
mock
(
'
lodash/throttle
'
,
()
=>
jest
.
fn
(
func
=>
{
return
func
;
}),
);
describe
(
'
EnvironmentLogs
'
,
()
=>
{
let
EnvironmentLogsComponent
;
let
store
;
let
dispatch
;
let
wrapper
;
let
state
;
...
...
@@ -32,14 +41,6 @@ describe('EnvironmentLogs', () => {
clusterApplicationsDocumentationPath
:
mockDocumentationPath
,
};
const
actionMocks
=
{
setInitData
:
jest
.
fn
(),
setSearch
:
jest
.
fn
(),
showPodLogs
:
jest
.
fn
(),
showEnvironment
:
jest
.
fn
(),
fetchEnvironments
:
jest
.
fn
(),
};
const
updateControlBtnsMock
=
jest
.
fn
();
const
findEnvironmentsDropdown
=
()
=>
wrapper
.
find
(
'
.js-environments-dropdown
'
);
...
...
@@ -47,24 +48,25 @@ describe('EnvironmentLogs', () => {
const
findSearchBar
=
()
=>
wrapper
.
find
(
'
.js-logs-search
'
);
const
findTimeRangePicker
=
()
=>
wrapper
.
find
({
ref
:
'
dateTimePicker
'
});
const
findInfoAlert
=
()
=>
wrapper
.
find
(
'
.js-elasticsearch-alert
'
);
const
findLogControlButtons
=
()
=>
wrapper
.
find
({
name
:
'
log-control-buttons-stub
'
});
const
findInfiniteScroll
=
()
=>
wrapper
.
find
({
ref
:
'
infiniteScroll
'
});
const
findLogTrace
=
()
=>
wrapper
.
find
(
'
.js-log-trace
'
);
const
findLogFooter
=
()
=>
wrapper
.
find
({
ref
:
'
logFooter
'
});
const
getInfiniteScrollAttr
=
attr
=>
parseInt
(
findInfiniteScroll
().
attributes
(
attr
),
10
);
const
mockSetInitData
=
()
=>
{
state
.
pods
.
options
=
mockPods
;
state
.
environments
.
current
=
mockEnvName
;
[
state
.
pods
.
current
]
=
state
.
pods
.
options
;
state
.
logs
.
isComplete
=
false
;
state
.
logs
.
lines
=
mockLogsResult
;
state
.
logs
.
lines
=
[];
};
const
mockShowPodLogs
=
podName
=>
{
const
mockShowPodLogs
=
()
=>
{
state
.
pods
.
options
=
mockPods
;
[
state
.
pods
.
current
]
=
podName
;
[
state
.
pods
.
current
]
=
mockPods
;
state
.
logs
.
isComplete
=
false
;
state
.
logs
.
lines
=
mockLogsResult
;
};
...
...
@@ -83,10 +85,21 @@ describe('EnvironmentLogs', () => {
methods
:
{
update
:
updateControlBtnsMock
,
},
props
:
{
scrollDownButtonDisabled
:
false
,
},
},
methods
:
{
...
actionMocks
,
GlInfiniteScroll
:
{
name
:
'
gl-infinite-scroll
'
,
template
:
`
<div>
<slot name="header"></slot>
<slot name="items"></slot>
<slot></slot>
</div>
`
,
},
GlSprintf
,
},
});
};
...
...
@@ -95,12 +108,14 @@ describe('EnvironmentLogs', () => {
store
=
createStore
();
state
=
store
.
state
.
environmentLogs
;
EnvironmentLogsComponent
=
Vue
.
extend
(
EnvironmentLogs
);
jest
.
spyOn
(
store
,
'
dispatch
'
).
mockResolvedValue
();
dispatch
=
store
.
dispatch
;
});
afterEach
(()
=>
{
actionMocks
.
setInitData
.
mockReset
();
actionMocks
.
showPodLogs
.
mockReset
();
actionMocks
.
fetchEnvironments
.
mockReset
();
store
.
dispatch
.
mockReset
();
if
(
wrapper
)
{
wrapper
.
destroy
();
...
...
@@ -124,14 +139,14 @@ describe('EnvironmentLogs', () => {
expect
(
findTimeRangePicker
().
is
(
DateTimePicker
)).
toBe
(
true
);
// log trace
expect
(
findLogTrace
().
isEmpty
()).
toBe
(
false
);
expect
(
findInfiniteScroll
().
exists
()).
toBe
(
true
);
expect
(
findLogTrace
().
exists
()).
toBe
(
true
);
});
it
(
'
mounted inits data
'
,
()
=>
{
initWrapper
();
expect
(
actionMocks
.
setInitData
).
toHaveBeenCalledTimes
(
1
);
expect
(
actionMocks
.
setInitData
).
toHaveBeenLastCalledWith
({
expect
(
dispatch
).
toHaveBeenCalledWith
(
`
${
module
}
/setInitData`
,
{
timeRange
:
expect
.
objectContaining
({
default
:
true
,
}),
...
...
@@ -139,18 +154,15 @@ describe('EnvironmentLogs', () => {
podName
:
null
,
});
expect
(
actionMocks
.
fetchEnvironments
).
toHaveBeenCalledTimes
(
1
);
expect
(
actionMocks
.
fetchEnvironments
).
toHaveBeenLastCalledWith
(
mockEnvironmentsEndpoint
);
expect
(
dispatch
).
toHaveBeenCalledWith
(
`
${
module
}
/fetchEnvironments`
,
mockEnvironmentsEndpoint
);
});
describe
(
'
loading state
'
,
()
=>
{
beforeEach
(()
=>
{
state
.
pods
.
options
=
[];
state
.
logs
=
{
lines
:
[],
isLoading
:
true
,
};
state
.
logs
.
lines
=
[];
state
.
logs
.
isLoading
=
true
;
state
.
environments
=
{
options
:
[],
...
...
@@ -183,6 +195,18 @@ describe('EnvironmentLogs', () => {
expect
(
updateControlBtnsMock
).
not
.
toHaveBeenCalled
();
});
it
(
'
shows an infinite scroll with height and no content
'
,
()
=>
{
expect
(
getInfiniteScrollAttr
(
'
max-list-height
'
)).
toBeGreaterThan
(
0
);
expect
(
getInfiniteScrollAttr
(
'
fetched-items
'
)).
toBe
(
0
);
});
it
(
'
shows an infinite scroll container with equal height and max-height
'
,
()
=>
{
const
height
=
getInfiniteScrollAttr
(
'
max-list-height
'
);
expect
(
height
).
toEqual
(
expect
.
any
(
Number
));
expect
(
findInfiniteScroll
().
attributes
(
'
style
'
)).
toMatch
(
`height:
${
height
}
px;`
);
});
it
(
'
shows a logs trace
'
,
()
=>
{
expect
(
findLogTrace
().
text
()).
toBe
(
''
);
expect
(
...
...
@@ -193,14 +217,12 @@ describe('EnvironmentLogs', () => {
});
});
describe
(
'
legacy
environment
'
,
()
=>
{
describe
(
'
k8s
environment
'
,
()
=>
{
beforeEach
(()
=>
{
state
.
pods
.
options
=
[];
state
.
logs
=
{
lines
:
[],
isLoading
:
false
,
};
state
.
logs
.
lines
=
[];
state
.
logs
.
isLoading
=
false
;
state
.
environments
=
{
options
:
mockEnvironments
,
...
...
@@ -226,9 +248,16 @@ describe('EnvironmentLogs', () => {
describe
(
'
state with data
'
,
()
=>
{
beforeEach
(()
=>
{
actionMocks
.
setInitData
.
mockImplementation
(
mockSetInitData
);
actionMocks
.
showPodLogs
.
mockImplementation
(
mockShowPodLogs
);
actionMocks
.
fetchEnvironments
.
mockImplementation
(
mockFetchEnvs
);
dispatch
.
mockImplementation
(
actionName
=>
{
if
(
actionName
===
`
${
module
}
/setInitData`
)
{
mockSetInitData
();
}
else
if
(
actionName
===
`
${
module
}
/showPodLogs`
)
{
mockShowPodLogs
();
}
else
if
(
actionName
===
`
${
module
}
/fetchEnvironments`
)
{
mockFetchEnvs
();
mockShowPodLogs
();
}
});
initWrapper
();
});
...
...
@@ -236,10 +265,6 @@ describe('EnvironmentLogs', () => {
afterEach
(()
=>
{
scrollDown
.
mockReset
();
updateControlBtnsMock
.
mockReset
();
actionMocks
.
setInitData
.
mockReset
();
actionMocks
.
showPodLogs
.
mockReset
();
actionMocks
.
fetchEnvironments
.
mockReset
();
});
it
(
'
displays an enabled search bar
'
,
()
=>
{
...
...
@@ -249,8 +274,8 @@ describe('EnvironmentLogs', () => {
findSearchBar
().
vm
.
$emit
(
'
input
'
,
mockSearch
);
findSearchBar
().
vm
.
$emit
(
'
submit
'
);
expect
(
actionMocks
.
setSearch
).
toHaveBeenCalledTimes
(
1
);
expect
(
actionMocks
.
setSearch
).
toHaveBeenCalledWith
(
mockSearch
);
expect
(
dispatch
).
toHaveBeenCalledWith
(
`
${
module
}
/setInitData`
,
expect
.
any
(
Object
)
);
expect
(
dispatch
).
toHaveBeenCalledWith
(
`
${
module
}
/setSearch`
,
mockSearch
);
});
it
(
'
displays an enabled time window dropdown
'
,
()
=>
{
...
...
@@ -282,18 +307,21 @@ describe('EnvironmentLogs', () => {
});
});
it
(
'
shows infinite scroll with height and no content
'
,
()
=>
{
expect
(
getInfiniteScrollAttr
(
'
max-list-height
'
)).
toBeGreaterThan
(
0
);
expect
(
getInfiniteScrollAttr
(
'
fetched-items
'
)).
toBe
(
mockTrace
.
length
);
});
it
(
'
populates logs trace
'
,
()
=>
{
const
trace
=
findLogTrace
();
expect
(
trace
.
text
().
split
(
'
\n
'
).
length
).
toBe
(
mockTrace
.
length
);
expect
(
trace
.
text
().
split
(
'
\n
'
)).
toEqual
(
mockTrace
);
});
it
(
'
update control buttons state
'
,
()
=>
{
expect
(
updateControlBtnsMock
).
toHaveBeenCalledTimes
(
1
);
});
it
(
'
populates footer
'
,
()
=>
{
const
footer
=
findLogFooter
().
text
();
it
(
'
scrolls to bottom when loaded
'
,
()
=>
{
expect
(
scrollDown
).
toHaveBeenCalledTimes
(
1
);
expect
(
footer
).
toContain
(
`
${
mockLogsResult
.
length
}
results`
);
});
describe
(
'
when user clicks
'
,
()
=>
{
...
...
@@ -301,33 +329,99 @@ describe('EnvironmentLogs', () => {
const
items
=
findEnvironmentsDropdown
().
findAll
(
GlDropdownItem
);
const
index
=
1
;
// any env
expect
(
actionMocks
.
showEnvironment
).
toHaveBeenCalledTimes
(
0
);
expect
(
dispatch
).
not
.
toHaveBeenCalledWith
(
`
${
module
}
/showEnvironment`
,
expect
.
anything
()
);
items
.
at
(
index
).
vm
.
$emit
(
'
click
'
);
expect
(
actionMocks
.
showEnvironment
).
toHaveBeenCalledTimes
(
1
);
expect
(
actionMocks
.
showEnvironment
).
toHaveBeenLastCalledWith
(
mockEnvironments
[
index
].
name
);
expect
(
dispatch
).
toHaveBeenCalledWith
(
`
${
module
}
/showEnvironment`
,
mockEnvironments
[
index
].
name
,
);
});
it
(
'
pod name, trace is refreshed
'
,
()
=>
{
const
items
=
findPodsDropdown
().
findAll
(
GlDropdownItem
);
const
index
=
2
;
// any pod
expect
(
actionMocks
.
showPodLogs
).
toHaveBeenCalledTimes
(
0
);
expect
(
dispatch
).
not
.
toHaveBeenCalledWith
(
`
${
module
}
/showPodLogs`
,
expect
.
anything
()
);
items
.
at
(
index
).
vm
.
$emit
(
'
click
'
);
expect
(
actionMocks
.
showPodLogs
).
toHaveBeenCalledTimes
(
1
);
expect
(
actionMocks
.
showPodLogs
).
toHaveBeenLastCalledWith
(
mockPods
[
index
]);
expect
(
dispatch
).
toHaveBeenCalledWith
(
`
${
module
}
/showPodLogs`
,
mockPods
[
index
]);
});
it
(
'
refresh button, trace is refreshed
'
,
()
=>
{
expect
(
actionMocks
.
showPodLogs
).
toHaveBeenCalledTimes
(
0
);
expect
(
dispatch
).
not
.
toHaveBeenCalledWith
(
`
${
module
}
/showPodLogs`
,
expect
.
anything
()
);
findLogControlButtons
().
vm
.
$emit
(
'
refresh
'
);
expect
(
actionMocks
.
showPodLogs
).
toHaveBeenCalledTimes
(
1
);
expect
(
actionMocks
.
showPodLogs
).
toHaveBeenLastCalledWith
(
mockPodName
);
expect
(
dispatch
).
toHaveBeenCalledWith
(
`
${
module
}
/showPodLogs`
,
mockPodName
);
});
});
});
describe
(
'
listeners
'
,
()
=>
{
beforeEach
(()
=>
{
initWrapper
();
});
it
(
'
attaches listeners in components
'
,
()
=>
{
expect
(
findInfiniteScroll
().
vm
.
$listeners
).
toEqual
({
topReached
:
expect
.
any
(
Function
),
scroll
:
expect
.
any
(
Function
),
});
});
it
(
'
`topReached` when not loading
'
,
()
=>
{
expect
(
store
.
dispatch
).
not
.
toHaveBeenCalledWith
(
`
${
module
}
/fetchMoreLogsPrepend`
,
undefined
);
findInfiniteScroll
().
vm
.
$emit
(
'
topReached
'
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
`
${
module
}
/fetchMoreLogsPrepend`
,
undefined
);
});
it
(
'
`topReached` does not fetches more logs when already loading
'
,
()
=>
{
state
.
logs
.
isLoading
=
true
;
findInfiniteScroll
().
vm
.
$emit
(
'
topReached
'
);
expect
(
store
.
dispatch
).
not
.
toHaveBeenCalledWith
(
`
${
module
}
/fetchMoreLogsPrepend`
,
undefined
);
});
it
(
'
`topReached` fetches more logs
'
,
()
=>
{
state
.
logs
.
isLoading
=
true
;
findInfiniteScroll
().
vm
.
$emit
(
'
topReached
'
);
expect
(
store
.
dispatch
).
not
.
toHaveBeenCalledWith
(
`
${
module
}
/fetchMoreLogsPrepend`
,
undefined
);
});
it
(
'
`scroll` on a scrollable target results in enabled scroll buttons
'
,
()
=>
{
const
target
=
{
scrollTop
:
10
,
clientHeight
:
10
,
scrollHeight
:
21
};
state
.
logs
.
isLoading
=
true
;
findInfiniteScroll
().
vm
.
$emit
(
'
scroll
'
,
{
target
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
findLogControlButtons
().
props
(
'
scrollDownButtonDisabled
'
)).
toEqual
(
false
);
});
});
it
(
'
`scroll` on a non-scrollable target in disabled scroll buttons
'
,
()
=>
{
const
target
=
{
scrollTop
:
10
,
clientHeight
:
10
,
scrollHeight
:
20
};
state
.
logs
.
isLoading
=
true
;
findInfiniteScroll
().
vm
.
$emit
(
'
scroll
'
,
{
target
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
findLogControlButtons
().
props
(
'
scrollDownButtonDisabled
'
)).
toEqual
(
true
);
});
});
it
(
'
`scroll` on no target results in disabled scroll buttons
'
,
()
=>
{
state
.
logs
.
isLoading
=
true
;
findInfiniteScroll
().
vm
.
$emit
(
'
scroll
'
,
{
target
:
undefined
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
findLogControlButtons
().
props
(
'
scrollDownButtonDisabled
'
)).
toEqual
(
true
);
});
});
});
...
...
spec/frontend/logs/components/log_control_buttons_spec.js
View file @
198cf31f
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlButton
}
from
'
@gitlab/ui
'
;
import
LogControlButtons
from
'
~/logs/components/log_control_buttons.vue
'
;
import
{
canScroll
,
isScrolledToTop
,
isScrolledToBottom
,
scrollDown
,
scrollUp
,
}
from
'
~/lib/utils/scroll_utils
'
;
jest
.
mock
(
'
~/lib/utils/scroll_utils
'
);
describe
(
'
LogControlButtons
'
,
()
=>
{
let
wrapper
;
...
...
@@ -18,8 +9,14 @@ describe('LogControlButtons', () => {
const
findScrollToBottom
=
()
=>
wrapper
.
find
(
'
.js-scroll-to-bottom
'
);
const
findRefreshBtn
=
()
=>
wrapper
.
find
(
'
.js-refresh-log
'
);
const
initWrapper
=
()
=>
{
wrapper
=
shallowMount
(
LogControlButtons
);
const
initWrapper
=
opts
=>
{
wrapper
=
shallowMount
(
LogControlButtons
,
{
listeners
:
{
scrollUp
:
()
=>
{},
scrollDown
:
()
=>
{},
},
...
opts
,
});
};
afterEach
(()
=>
{
...
...
@@ -55,27 +52,16 @@ describe('LogControlButtons', () => {
describe
(
'
when scrolling actions are enabled
'
,
()
=>
{
beforeEach
(()
=>
{
// mock scrolled to the middle of a long page
canScroll
.
mockReturnValue
(
true
);
isScrolledToBottom
.
mockReturnValue
(
false
);
isScrolledToTop
.
mockReturnValue
(
false
);
initWrapper
();
wrapper
.
vm
.
update
();
return
wrapper
.
vm
.
$nextTick
();
});
afterEach
(()
=>
{
canScroll
.
mockReset
();
isScrolledToTop
.
mockReset
();
isScrolledToBottom
.
mockReset
();
});
it
(
'
click on "scroll to top" scrolls up
'
,
()
=>
{
expect
(
findScrollToTop
().
is
(
'
[disabled]
'
)).
toBe
(
false
);
findScrollToTop
().
vm
.
$emit
(
'
click
'
);
expect
(
scrollUp
).
toHaveBeenCalledTimes
(
1
);
expect
(
wrapper
.
emitted
(
'
scrollUp
'
)).
toHaveLength
(
1
);
});
it
(
'
click on "scroll to bottom" scrolls down
'
,
()
=>
{
...
...
@@ -83,25 +69,23 @@ describe('LogControlButtons', () => {
findScrollToBottom
().
vm
.
$emit
(
'
click
'
);
expect
(
scrollDown
).
toHaveBeenCalledTimes
(
1
);
// plus one time when trace was loaded
expect
(
wrapper
.
emitted
(
'
scrollDown
'
)).
toHaveLength
(
1
);
});
});
describe
(
'
when scrolling actions are disabled
'
,
()
=>
{
beforeEach
(()
=>
{
// mock a short page without a scrollbar
canScroll
.
mockReturnValue
(
false
);
isScrolledToBottom
.
mockReturnValue
(
true
);
isScrolledToTop
.
mockReturnValue
(
true
);
initWrapper
();
initWrapper
({
listeners
:
{}
});
return
wrapper
.
vm
.
$nextTick
();
});
it
(
'
buttons are disabled
'
,
()
=>
{
wrapper
.
vm
.
update
();
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
findScrollToTop
().
is
(
'
[disabled]
'
)).
toBe
(
true
);
expect
(
findScrollToBottom
().
is
(
'
[disabled]
'
)).
toBe
(
true
);
expect
(
findScrollToTop
().
exists
()).
toBe
(
false
);
expect
(
findScrollToBottom
().
exists
()).
toBe
(
false
);
// This should be enabled when gitlab-ui contains:
// https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1149
// expect(findScrollToBottom().is('[disabled]')).toBe(true);
});
});
});
...
...
spec/frontend/logs/mock_data.js
View file @
198cf31f
export
const
mockProjectPath
=
'
root/autodevops-deploy
'
;
const
mockProjectPath
=
'
root/autodevops-deploy
'
;
export
const
mockEnvName
=
'
production
'
;
export
const
mockEnvironmentsEndpoint
=
`
${
mockProjectPath
}
/environments.json`
;
export
const
mockEnvId
=
'
99
'
;
export
const
mockDocumentationPath
=
'
/documentation.md
'
;
export
const
mockLogsEndpoint
=
'
/dummy_logs_path.json
'
;
export
const
mockCursor
=
'
MOCK_CURSOR
'
;
export
const
mockNextCursor
=
'
MOCK_NEXT_CURSOR
'
;
const
makeMockEnvironment
=
(
id
,
name
,
advancedQuerying
)
=>
({
id
,
project_path
:
mockProjectPath
,
name
,
logs_api_path
:
'
/dummy_logs_path.json
'
,
logs_api_path
:
mockLogsEndpoint
,
enable_advanced_logs_querying
:
advancedQuerying
,
});
...
...
@@ -28,58 +32,22 @@ export const mockPods = [
];
export
const
mockLogsResult
=
[
{
timestamp
:
'
2019-12-13T13:43:18.2760123Z
'
,
message
:
'
10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13
'
,
},
{
timestamp
:
'
2019-12-13T13:43:18.2760123Z
'
,
message
:
'
- -> /
'
},
{
timestamp
:
'
2019-12-13T13:43:26.8420123Z
'
,
message
:
'
10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13
'
,
},
{
timestamp
:
'
2019-12-13T13:43:26.8420123Z
'
,
message
:
'
- -> /
'
},
{
timestamp
:
'
2019-12-13T13:43:28.3710123Z
'
,
message
:
'
10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13
'
,
},
{
timestamp
:
'
2019-12-13T13:43:28.3710123Z
'
,
message
:
'
- -> /
'
},
{
timestamp
:
'
2019-12-13T13:43:36.8860123Z
'
,
message
:
'
10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13
'
,
},
{
timestamp
:
'
2019-12-13T13:43:36.8860123Z
'
,
message
:
'
- -> /
'
},
{
timestamp
:
'
2019-12-13T13:43:38.4000123Z
'
,
message
:
'
10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13
'
,
},
{
timestamp
:
'
2019-12-13T13:43:38.4000123Z
'
,
message
:
'
- -> /
'
},
{
timestamp
:
'
2019-12-13T13:43:46.8420123Z
'
,
message
:
'
10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13
'
,
},
{
timestamp
:
'
2019-12-13T13:43:46.8430123Z
'
,
message
:
'
- -> /
'
},
{
timestamp
:
'
2019-12-13T13:43:48.3240123Z
'
,
message
:
'
10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13
'
,
},
{
timestamp
:
'
2019-12-13T13:43:48.3250123Z
'
,
message
:
'
- -> /
'
},
{
timestamp
:
'
2019-12-13T13:43:18.2760123Z
'
,
message
:
'
Log 1
'
},
{
timestamp
:
'
2019-12-13T13:43:18.2760123Z
'
,
message
:
'
Log 2
'
},
{
timestamp
:
'
2019-12-13T13:43:26.8420123Z
'
,
message
:
'
Log 3
'
},
];
export
const
mockTrace
=
[
'
Dec 13 13:43:18.276Z | 10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13
'
,
'
Dec 13 13:43:18.276Z | - -> /
'
,
'
Dec 13 13:43:26.842Z | 10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13
'
,
'
Dec 13 13:43:26.842Z | - -> /
'
,
'
Dec 13 13:43:28.371Z | 10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13
'
,
'
Dec 13 13:43:28.371Z | - -> /
'
,
'
Dec 13 13:43:36.886Z | 10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13
'
,
'
Dec 13 13:43:36.886Z | - -> /
'
,
'
Dec 13 13:43:38.400Z | 10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13
'
,
'
Dec 13 13:43:38.400Z | - -> /
'
,
'
Dec 13 13:43:46.842Z | 10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13
'
,
'
Dec 13 13:43:46.843Z | - -> /
'
,
'
Dec 13 13:43:48.324Z | 10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13
'
,
'
Dec 13 13:43:48.325Z | - -> /
'
,
'
Dec 13 13:43:18.276Z | Log 1
'
,
'
Dec 13 13:43:18.276Z | Log 2
'
,
'
Dec 13 13:43:26.842Z | Log 3
'
,
];
export
const
mockResponse
=
{
pod_name
:
mockPodName
,
pods
:
mockPods
,
logs
:
mockLogsResult
,
cursor
:
mockNextCursor
,
};
export
const
mockSearch
=
'
foo +bar
'
;
spec/frontend/logs/stores/actions_spec.js
View file @
198cf31f
...
...
@@ -10,6 +10,7 @@ import {
showPodLogs
,
fetchEnvironments
,
fetchLogs
,
fetchMoreLogsPrepend
,
}
from
'
~/logs/stores/actions
'
;
import
{
defaultTimeRange
}
from
'
~/monitoring/constants
'
;
...
...
@@ -18,7 +19,6 @@ import axios from '~/lib/utils/axios_utils';
import
flash
from
'
~/flash
'
;
import
{
mockProjectPath
,
mockPodName
,
mockEnvironmentsEndpoint
,
mockEnvironments
,
...
...
@@ -26,6 +26,10 @@ import {
mockLogsResult
,
mockEnvName
,
mockSearch
,
mockLogsEndpoint
,
mockResponse
,
mockCursor
,
mockNextCursor
,
}
from
'
../mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
...
...
@@ -52,6 +56,8 @@ describe('Logs Store actions', () => {
let
state
;
let
mock
;
const
latestGetParams
=
()
=>
mock
.
history
.
get
[
mock
.
history
.
get
.
length
-
1
].
params
;
convertToFixedRange
.
mockImplementation
(
range
=>
{
if
(
range
===
defaultTimeRange
)
{
return
{
...
mockDefaultRange
};
...
...
@@ -75,10 +81,16 @@ describe('Logs Store actions', () => {
describe
(
'
setInitData
'
,
()
=>
{
it
(
'
should commit environment and pod name mutation
'
,
()
=>
testAction
(
setInitData
,
{
environmentName
:
mockEnvName
,
podName
:
mockPodName
},
state
,
[
testAction
(
setInitData
,
{
timeRange
:
mockFixedRange
,
environmentName
:
mockEnvName
,
podName
:
mockPodName
},
state
,
[
{
type
:
types
.
SET_TIME_RANGE
,
payload
:
mockFixedRange
},
{
type
:
types
.
SET_PROJECT_ENVIRONMENT
,
payload
:
mockEnvName
},
{
type
:
types
.
SET_CURRENT_POD_NAME
,
payload
:
mockPodName
},
]));
],
));
});
describe
(
'
setSearch
'
,
()
=>
{
...
...
@@ -140,183 +152,245 @@ describe('Logs Store actions', () => {
});
});
describe
(
'
fetchLogs
'
,
()
=>
{
describe
(
'
when the backend responds succesfully
'
,
()
=>
{
let
expectedMutations
;
let
expectedActions
;
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
mockLogsEndpoint
).
reply
(
200
,
mockResponse
);
mock
.
onGet
(
mockLogsEndpoint
).
replyOnce
(
202
);
// mock reactive cache
state
.
environments
.
options
=
mockEnvironments
;
state
.
environments
.
current
=
mockEnvName
;
});
afterEach
(()
=>
{
mock
.
reset
();
});
describe
(
'
fetchLogs
'
,
()
=>
{
beforeEach
(()
=>
{
expectedMutations
=
[
{
type
:
types
.
REQUEST_PODS_DATA
},
{
type
:
types
.
REQUEST_LOGS_DATA
},
{
type
:
types
.
SET_CURRENT_POD_NAME
,
payload
:
mockPodName
},
{
type
:
types
.
RECEIVE_PODS_DATA_SUCCESS
,
payload
:
mockPods
},
{
type
:
types
.
RECEIVE_LOGS_DATA_SUCCESS
,
payload
:
{
logs
:
mockLogsResult
,
cursor
:
mockNextCursor
},
},
];
expectedActions
=
[];
});
it
(
'
should commit logs and pod data when there is pod name defined
'
,
()
=>
{
state
.
environments
.
options
=
mockEnvironments
;
state
.
environments
.
current
=
mockEnvName
;
state
.
pods
.
current
=
mockPodName
;
const
endpoint
=
'
/dummy_logs_path.json
'
;
return
testAction
(
fetchLogs
,
null
,
state
,
expectedMutations
,
expectedActions
,
()
=>
{
expect
(
latestGetParams
()).
toMatchObject
({
pod_name
:
mockPodName
,
});
});
});
it
(
'
should commit logs and pod data when there is pod name defined and a non-default date range
'
,
()
=>
{
state
.
pods
.
current
=
mockPodName
;
state
.
timeRange
.
current
=
mockFixedRange
;
state
.
logs
.
cursor
=
mockCursor
;
mock
.
onGet
(
endpoint
,
{
params
:
{
return
testAction
(
fetchLogs
,
null
,
state
,
expectedMutations
,
expectedActions
,
()
=>
{
expect
(
latestGetParams
()).
toEqual
({
pod_name
:
mockPodName
,
...
mockDefaultRange
,
},
})
.
reply
(
200
,
{
start
:
mockFixedRange
.
start
,
end
:
mockFixedRange
.
end
,
cursor
:
mockCursor
,
});
});
});
it
(
'
should commit logs and pod data when there is pod name and search and a faulty date range
'
,
()
=>
{
state
.
pods
.
current
=
mockPodName
;
state
.
search
=
mockSearch
;
state
.
timeRange
.
current
=
'
INVALID_TIME_RANGE
'
;
return
testAction
(
fetchLogs
,
null
,
state
,
expectedMutations
,
expectedActions
,
()
=>
{
expect
(
latestGetParams
()).
toEqual
({
pod_name
:
mockPodName
,
pods
:
mockPods
,
logs
:
mockLogsResult
,
search
:
mockSearch
,
});
// Warning about time ranges was issued
expect
(
flash
).
toHaveBeenCalledTimes
(
1
);
expect
(
flash
).
toHaveBeenCalledWith
(
expect
.
any
(
String
),
'
warning
'
);
});
});
mock
.
onGet
(
endpoint
).
replyOnce
(
202
);
// mock reactive cache
it
(
'
should commit logs and pod data when no pod name defined
'
,
()
=>
{
state
.
timeRange
.
current
=
mockDefaultRange
;
return
testAction
(
fetchLogs
,
null
,
state
,
expectedMutations
,
expectedActions
,
()
=>
{
expect
(
latestGetParams
()).
toEqual
({});
});
});
});
describe
(
'
fetchMoreLogsPrepend
'
,
()
=>
{
beforeEach
(()
=>
{
expectedMutations
=
[
{
type
:
types
.
REQUEST_LOGS_DATA_PREPEND
},
{
type
:
types
.
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
,
payload
:
{
logs
:
mockLogsResult
,
cursor
:
mockNextCursor
},
},
];
expectedActions
=
[];
});
it
(
'
should commit logs and pod data when there is pod name defined
'
,
()
=>
{
state
.
pods
.
current
=
mockPodName
;
expectedActions
=
[];
return
testAction
(
fetchLogs
,
fetchMoreLogsPrepend
,
null
,
state
,
[
{
type
:
types
.
REQUEST_PODS_DATA
},
{
type
:
types
.
REQUEST_LOGS_DATA
},
{
type
:
types
.
SET_CURRENT_POD_NAME
,
payload
:
mockPodName
},
{
type
:
types
.
RECEIVE_PODS_DATA_SUCCESS
,
payload
:
mockPods
},
{
type
:
types
.
RECEIVE_LOGS_DATA_SUCCESS
,
payload
:
mockLogsResult
},
],
[],
expectedMutations
,
expectedActions
,
()
=>
{
expect
(
latestGetParams
()).
toMatchObject
({
pod_name
:
mockPodName
,
});
},
);
});
it
(
'
should commit logs and pod data when there is pod name defined and a non-default date range
'
,
()
=>
{
state
.
projectPath
=
mockProjectPath
;
state
.
environments
.
options
=
mockEnvironments
;
state
.
environments
.
current
=
mockEnvName
;
state
.
pods
.
current
=
mockPodName
;
state
.
timeRange
.
current
=
mockFixedRange
;
state
.
logs
.
cursor
=
mockCursor
;
const
endpoint
=
'
/dummy_logs_path.json
'
;
mock
.
onGet
(
endpoint
,
{
params
:
{
return
testAction
(
fetchMoreLogsPrepend
,
null
,
state
,
expectedMutations
,
expectedActions
,
()
=>
{
expect
(
latestGetParams
()).
toEqual
({
pod_name
:
mockPodName
,
start
:
mockFixedRange
.
start
,
end
:
mockFixedRange
.
end
,
},
})
.
reply
(
200
,
{
pod_name
:
mockPodName
,
pods
:
mockPods
,
logs
:
mockLogsResult
,
cursor
:
mockCursor
,
});
return
testAction
(
fetchLogs
,
null
,
state
,
[
{
type
:
types
.
REQUEST_PODS_DATA
},
{
type
:
types
.
REQUEST_LOGS_DATA
},
{
type
:
types
.
SET_CURRENT_POD_NAME
,
payload
:
mockPodName
},
{
type
:
types
.
RECEIVE_PODS_DATA_SUCCESS
,
payload
:
mockPods
},
{
type
:
types
.
RECEIVE_LOGS_DATA_SUCCESS
,
payload
:
mockLogsResult
},
],
[],
},
);
});
it
(
'
should commit logs and pod data when there is pod name and search and a faulty date range
'
,
()
=>
{
state
.
environments
.
options
=
mockEnvironments
;
state
.
environments
.
current
=
mockEnvName
;
state
.
pods
.
current
=
mockPodName
;
state
.
search
=
mockSearch
;
state
.
timeRange
.
current
=
'
INVALID_TIME_RANGE
'
;
const
endpoint
=
'
/dummy_logs_path.json
'
;
mock
.
onGet
(
endpoint
,
{
params
:
{
return
testAction
(
fetchMoreLogsPrepend
,
null
,
state
,
expectedMutations
,
expectedActions
,
()
=>
{
expect
(
latestGetParams
()).
toEqual
({
pod_name
:
mockPodName
,
search
:
mockSearch
,
});
// Warning about time ranges was issued
expect
(
flash
).
toHaveBeenCalledTimes
(
1
);
expect
(
flash
).
toHaveBeenCalledWith
(
expect
.
any
(
String
),
'
warning
'
);
},
})
.
reply
(
200
,
{
pod_name
:
mockPodName
,
pods
:
mockPods
,
logs
:
mockLogsResult
,
);
});
mock
.
onGet
(
endpoint
).
replyOnce
(
202
);
// mock reactive cache
it
(
'
should commit logs and pod data when no pod name defined
'
,
()
=>
{
state
.
timeRange
.
current
=
mockDefaultRange
;
return
testAction
(
fetchLogs
,
fetchMoreLogsPrepend
,
null
,
state
,
[
{
type
:
types
.
REQUEST_PODS_DATA
},
{
type
:
types
.
REQUEST_LOGS_DATA
},
{
type
:
types
.
SET_CURRENT_POD_NAME
,
payload
:
mockPodName
},
{
type
:
types
.
RECEIVE_PODS_DATA_SUCCESS
,
payload
:
mockPods
},
{
type
:
types
.
RECEIVE_LOGS_DATA_SUCCESS
,
payload
:
mockLogsResult
},
],
[],
expectedMutations
,
expectedActions
,
()
=>
{
// Warning about time ranges was issued
expect
(
flash
).
toHaveBeenCalledTimes
(
1
);
expect
(
flash
).
toHaveBeenCalledWith
(
expect
.
any
(
String
),
'
warning
'
);
expect
(
latestGetParams
()).
toEqual
({});
},
);
});
it
(
'
should commit logs and pod data when no pod name defined
'
,
done
=>
{
state
.
environments
.
options
=
mockEnvironments
;
state
.
environments
.
current
=
mockEnvName
;
it
(
'
should not commit logs or pod data when it has reached the end
'
,
()
=>
{
state
.
logs
.
isComplete
=
true
;
state
.
logs
.
cursor
=
null
;
const
endpoint
=
'
/dummy_logs_path.json
'
;
return
testAction
(
fetchMoreLogsPrepend
,
null
,
state
,
[],
// no mutations done
[],
// no actions dispatched
()
=>
{
expect
(
mock
.
history
.
get
).
toHaveLength
(
0
);
},
);
});
});
});
mock
.
onGet
(
endpoint
,
{
params
:
{
...
mockDefaultRange
}
}).
reply
(
200
,
{
pod_name
:
mockPodName
,
pods
:
mockPods
,
logs
:
mockLogsResult
,
describe
(
'
when the backend responds with an error
'
,
()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
mockLogsEndpoint
).
reply
(
500
);
});
mock
.
onGet
(
endpoint
).
replyOnce
(
202
);
// mock reactive cache
testAction
(
afterEach
(()
=>
{
mock
.
reset
();
});
it
(
'
fetchLogs should commit logs and pod errors
'
,
()
=>
{
state
.
environments
.
options
=
mockEnvironments
;
state
.
environments
.
current
=
mockEnvName
;
return
testAction
(
fetchLogs
,
null
,
state
,
[
{
type
:
types
.
REQUEST_PODS_DATA
},
{
type
:
types
.
REQUEST_LOGS_DATA
},
{
type
:
types
.
SET_CURRENT_POD_NAME
,
payload
:
mockPodName
},
{
type
:
types
.
RECEIVE_PODS_DATA_SUCCESS
,
payload
:
mockPods
},
{
type
:
types
.
RECEIVE_LOGS_DATA_SUCCESS
,
payload
:
mockLogsResult
},
{
type
:
types
.
RECEIVE_PODS_DATA_ERROR
},
{
type
:
types
.
RECEIVE_LOGS_DATA_ERROR
},
],
[],
done
,
()
=>
{
expect
(
mock
.
history
.
get
[
0
].
url
).
toBe
(
mockLogsEndpoint
);
},
);
});
it
(
'
should commit logs and pod errors when backend fail
s
'
,
()
=>
{
it
(
'
fetchMoreLogsPrepend should commit logs and pod error
s
'
,
()
=>
{
state
.
environments
.
options
=
mockEnvironments
;
state
.
environments
.
current
=
mockEnvName
;
const
endpoint
=
`/
${
mockProjectPath
}
/-/logs/elasticsearch.json?environment_name=
${
mockEnvName
}
`
;
mock
.
onGet
(
endpoint
).
replyOnce
(
500
);
return
testAction
(
fetch
Logs
,
fetch
MoreLogsPrepend
,
null
,
state
,
[
{
type
:
types
.
REQUEST_PODS_DATA
},
{
type
:
types
.
REQUEST_LOGS_DATA
},
{
type
:
types
.
RECEIVE_PODS_DATA_ERROR
},
{
type
:
types
.
RECEIVE_LOGS_DATA_ERROR
},
{
type
:
types
.
REQUEST_LOGS_DATA_PREPEND
},
{
type
:
types
.
RECEIVE_LOGS_DATA_PREPEND_ERROR
},
],
[],
()
=>
{
expect
(
flash
).
toHaveBeenCalledTimes
(
1
);
expect
(
mock
.
history
.
get
[
0
].
url
).
toBe
(
mockLogsEndpoint
);
},
);
});
...
...
spec/frontend/logs/stores/mutations_spec.js
View file @
198cf31f
...
...
@@ -9,6 +9,8 @@ import {
mockPodName
,
mockLogsResult
,
mockSearch
,
mockCursor
,
mockNextCursor
,
}
from
'
../mock_data
'
;
describe
(
'
Logs Store Mutations
'
,
()
=>
{
...
...
@@ -73,27 +75,47 @@ describe('Logs Store Mutations', () => {
it
(
'
starts loading for logs
'
,
()
=>
{
mutations
[
types
.
REQUEST_LOGS_DATA
](
state
);
expect
(
state
.
logs
).
toEqual
(
expect
.
objectContaining
({
expect
(
state
.
timeRange
.
current
).
toEqual
({
start
:
expect
.
any
(
String
),
end
:
expect
.
any
(
String
),
});
expect
(
state
.
logs
).
toEqual
({
lines
:
[],
cursor
:
null
,
isLoading
:
true
,
isComplete
:
false
,
}),
);
});
});
});
describe
(
'
RECEIVE_LOGS_DATA_SUCCESS
'
,
()
=>
{
it
(
'
receives logs lines
'
,
()
=>
{
mutations
[
types
.
RECEIVE_LOGS_DATA_SUCCESS
](
state
,
mockLogsResult
);
it
(
'
receives logs lines and cursor
'
,
()
=>
{
mutations
[
types
.
RECEIVE_LOGS_DATA_SUCCESS
](
state
,
{
logs
:
mockLogsResult
,
cursor
:
mockCursor
,
});
expect
(
state
.
logs
).
toEqual
(
expect
.
objectContaining
({
expect
(
state
.
logs
).
toEqual
({
lines
:
mockLogsResult
,
isLoading
:
false
,
cursor
:
mockCursor
,
isComplete
:
false
,
});
});
it
(
'
receives logs lines and a null cursor to indicate the end
'
,
()
=>
{
mutations
[
types
.
RECEIVE_LOGS_DATA_SUCCESS
](
state
,
{
logs
:
mockLogsResult
,
cursor
:
null
,
});
expect
(
state
.
logs
).
toEqual
({
lines
:
mockLogsResult
,
isLoading
:
false
,
cursor
:
null
,
isComplete
:
true
,
}),
);
});
});
});
...
...
@@ -101,13 +123,77 @@ describe('Logs Store Mutations', () => {
it
(
'
receives log data error and stops loading
'
,
()
=>
{
mutations
[
types
.
RECEIVE_LOGS_DATA_ERROR
](
state
);
expect
(
state
.
logs
).
toEqual
(
expect
.
objectContaining
({
expect
(
state
.
logs
).
toEqual
({
lines
:
[],
isLoading
:
false
,
cursor
:
null
,
isComplete
:
false
,
});
});
});
describe
(
'
REQUEST_LOGS_DATA_PREPEND
'
,
()
=>
{
it
(
'
receives logs lines and cursor
'
,
()
=>
{
mutations
[
types
.
REQUEST_LOGS_DATA_PREPEND
](
state
);
expect
(
state
.
logs
.
isLoading
).
toBe
(
true
);
});
});
describe
(
'
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
'
,
()
=>
{
it
(
'
receives logs lines and cursor
'
,
()
=>
{
mutations
[
types
.
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
](
state
,
{
logs
:
mockLogsResult
,
cursor
:
mockCursor
,
});
expect
(
state
.
logs
).
toEqual
({
lines
:
mockLogsResult
,
isLoading
:
false
,
cursor
:
mockCursor
,
isComplete
:
false
,
});
});
it
(
'
receives additional logs lines and a new cursor
'
,
()
=>
{
mutations
[
types
.
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
](
state
,
{
logs
:
mockLogsResult
,
cursor
:
mockCursor
,
});
mutations
[
types
.
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
](
state
,
{
logs
:
mockLogsResult
,
cursor
:
mockNextCursor
,
});
expect
(
state
.
logs
).
toEqual
({
lines
:
[...
mockLogsResult
,
...
mockLogsResult
],
isLoading
:
false
,
cursor
:
mockNextCursor
,
isComplete
:
false
,
});
});
it
(
'
receives logs lines and a null cursor to indicate is complete
'
,
()
=>
{
mutations
[
types
.
RECEIVE_LOGS_DATA_PREPEND_SUCCESS
](
state
,
{
logs
:
mockLogsResult
,
cursor
:
null
,
});
expect
(
state
.
logs
).
toEqual
({
lines
:
mockLogsResult
,
isLoading
:
false
,
cursor
:
null
,
isComplete
:
true
,
}),
);
});
});
});
describe
(
'
RECEIVE_LOGS_DATA_PREPEND_ERROR
'
,
()
=>
{
it
(
'
receives logs lines and cursor
'
,
()
=>
{
mutations
[
types
.
RECEIVE_LOGS_DATA_PREPEND_ERROR
](
state
);
expect
(
state
.
logs
.
isLoading
).
toBe
(
false
);
});
});
...
...
@@ -121,6 +207,7 @@ describe('Logs Store Mutations', () => {
describe
(
'
SET_TIME_RANGE
'
,
()
=>
{
it
(
'
sets a default range
'
,
()
=>
{
expect
(
state
.
timeRange
.
selected
).
toEqual
(
expect
.
any
(
Object
));
expect
(
state
.
timeRange
.
current
).
toEqual
(
expect
.
any
(
Object
));
});
...
...
@@ -131,12 +218,13 @@ describe('Logs Store Mutations', () => {
};
mutations
[
types
.
SET_TIME_RANGE
](
state
,
mockRange
);
expect
(
state
.
timeRange
.
selected
).
toEqual
(
mockRange
);
expect
(
state
.
timeRange
.
current
).
toEqual
(
mockRange
);
});
});
describe
(
'
REQUEST_PODS_DATA
'
,
()
=>
{
it
(
'
receives
log data error and stops loading
'
,
()
=>
{
it
(
'
receives
pods data
'
,
()
=>
{
mutations
[
types
.
REQUEST_PODS_DATA
](
state
);
expect
(
state
.
pods
).
toEqual
(
...
...
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