diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-13 15:09:22 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-13 15:09:22 +0300 |
commit | 286fe61013674fe2d245ffc8d2233baf09923e70 (patch) | |
tree | 2037291f5863105e54e75be056b49f7d62007cae /app/assets/javascripts/logs | |
parent | 4cb5e5011abfe8d50ac3a7ebd0018c563c6d7af4 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/logs')
-rw-r--r-- | app/assets/javascripts/logs/components/environment_logs.vue | 98 | ||||
-rw-r--r-- | app/assets/javascripts/logs/components/log_control_buttons.vue | 57 | ||||
-rw-r--r-- | app/assets/javascripts/logs/stores/actions.js | 86 | ||||
-rw-r--r-- | app/assets/javascripts/logs/stores/getters.js | 10 | ||||
-rw-r--r-- | app/assets/javascripts/logs/stores/mutation_types.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/logs/stores/mutations.js | 52 | ||||
-rw-r--r-- | app/assets/javascripts/logs/stores/state.js | 13 | ||||
-rw-r--r-- | app/assets/javascripts/logs/utils.js | 5 |
8 files changed, 231 insertions, 93 deletions
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue index b94cd2bcec4..b0acd69bae0 100644 --- a/app/assets/javascripts/logs/components/environment_logs.vue +++ b/app/assets/javascripts/logs/components/environment_logs.vue @@ -1,23 +1,37 @@ <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"> - <div class="dot"></div> - <div class="dot"></div> - <div class="dot"></div> - </div></code></pre> + + <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>{{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> diff --git a/app/assets/javascripts/logs/components/log_control_buttons.vue b/app/assets/javascripts/logs/components/log_control_buttons.vue index d55c2f7cd4c..170d0474447 100644 --- a/app/assets/javascripts/logs/components/log_control_buttons.vue +++ b/app/assets/javascripts/logs/components/log_control_buttons.vue @@ -1,12 +1,5 @@ <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 { - scrollToTopEnabled: false, - scrollToBottomEnabled: false, + scrollUpAvailable: Boolean(this.$listeners.scrollUp), + scrollDownAvailable: 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="!scrollToTopEnabled" - @click="scrollUp()" + :disabled="scrollUpButtonDisabled" + @click="handleScrollUp()" ><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> diff --git a/app/assets/javascripts/logs/stores/actions.js b/app/assets/javascripts/logs/stores/actions.js index 89a896b9dec..4544ebdfec1 100644 --- a/app/assets/javascripts/logs/stores/actions.js +++ b/app/assets/javascripts/logs/stores/actions.js @@ -1,4 +1,3 @@ -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 requestLogsUntilData = params => +const requestUntilData = (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 => }); }); -export const setInitData = ({ commit }, { timeRange, environmentName, podName }) => { - if (timeRange) { - commit(types.SET_TIME_RANGE, timeRange); +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 }) => { + 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 () => {}; diff --git a/app/assets/javascripts/logs/stores/getters.js b/app/assets/javascripts/logs/stores/getters.js index c7dbb72ce3d..58f2dbf4835 100644 --- a/app/assets/javascripts/logs/stores/getters.js +++ b/app/assets/javascripts/logs/stores/getters.js @@ -1,9 +1,9 @@ -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 () => {}; diff --git a/app/assets/javascripts/logs/stores/mutation_types.js b/app/assets/javascripts/logs/stores/mutation_types.js index b8e70f95d92..5ff49135e41 100644 --- a/app/assets/javascripts/logs/stores/mutation_types.js +++ b/app/assets/javascripts/logs/stores/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/logs/stores/mutations.js b/app/assets/javascripts/logs/stores/mutations.js index ca31dd3bc20..d94d71cd25a 100644 --- a/app/assets/javascripts/logs/stores/mutations.js +++ b/app/assets/javascripts/logs/stores/mutations.js @@ -1,17 +1,24 @@ 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 = lines; + [types.RECEIVE_LOGS_DATA_SUCCESS](state, { logs = [], cursor }) { + state.logs.lines = logs.map(mapLine); state.logs.isLoading = false; - state.logs.isComplete = true; + state.logs.cursor = cursor; + + if (!cursor) { + state.logs.isComplete = true; + } }, [types.RECEIVE_LOGS_DATA_ERROR](state) { state.logs.lines = []; state.logs.isLoading = false; - state.logs.isComplete = true; }, - /** Pods data */ + [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 [types.SET_CURRENT_POD_NAME](state, podName) { state.pods.current = podName; }, diff --git a/app/assets/javascripts/logs/stores/state.js b/app/assets/javascripts/logs/stores/state.js index eaf1b1bdd93..e058f15b6b4 100644 --- a/app/assets/javascripts/logs/stores/state.js +++ b/app/assets/javascripts/logs/stores/state.js @@ -1,4 +1,5 @@ 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, }, /** diff --git a/app/assets/javascripts/logs/utils.js b/app/assets/javascripts/logs/utils.js index 668efee74e8..30213dbc130 100644 --- a/app/assets/javascripts/logs/utils.js +++ b/app/assets/javascripts/logs/utils.js @@ -1,4 +1,7 @@ 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 {}; |