diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-13 12:09:22 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-13 12:09:22 +0000 |
commit | 286fe61013674fe2d245ffc8d2233baf09923e70 (patch) | |
tree | 2037291f5863105e54e75be056b49f7d62007cae /app/assets/javascripts | |
parent | 4cb5e5011abfe8d50ac3a7ebd0018c563c6d7af4 (diff) | |
download | gitlab-ce-286fe61013674fe2d245ffc8d2233baf09923e70.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
19 files changed, 298 insertions, 281 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index c85e5b68f5f..dc6ea148047 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -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; diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue index 3398cd091ba..e618fb3daae 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -24,25 +24,19 @@ export default { discardModalTitle() { return sprintf(__('Discard changes to %{path}?'), { path: this.activeFile.path }); }, - actionButtonText() { - return this.activeFile.staged ? __('Unstage') : __('Stage'); - }, isStaged() { return !this.activeFile.changed && this.activeFile.staged; }, }, methods: { ...mapActions(['stageChange', 'unstageChange', 'discardFileChanges']), - actionButtonClicked() { - if (this.activeFile.staged) { - this.unstageChange(this.activeFile.path); - } else { - this.stageChange(this.activeFile.path); - } - }, showDiscardModal() { this.$refs.discardModal.show(); }, + discardChanges(path) { + this.unstageChange(path); + this.discardFileChanges(path); + }, }, }; </script> @@ -65,19 +59,7 @@ export default { class="btn btn-remove btn-inverted append-right-8" @click="showDiscardModal" > - {{ __('Discard') }} - </button> - <button - ref="actionButton" - :class="{ - 'btn-success': !isStaged, - 'btn-warning': isStaged, - }" - type="button" - class="btn btn-inverted" - @click="actionButtonClicked" - > - {{ actionButtonText }} + {{ __('Discard changes') }} </button> </div> <gl-modal @@ -87,7 +69,7 @@ export default { :ok-title="__('Discard changes')" :modal-id="discardModalId" :title="discardModalTitle" - @ok="discardFileChanges(activeFile.path)" + @ok="discardChanges(activeFile.path)" > {{ __("You will lose all changes you've made to this file. This action cannot be undone.") }} </gl-modal> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 5ec3fc4041b..f6ca728defc 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; -import { sprintf, __ } from '~/locale'; +import { n__, __ } from '~/locale'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; import CommitMessageField from './message_field.vue'; import Actions from './actions.vue'; @@ -26,15 +26,7 @@ export default { ...mapGetters(['hasChanges']), ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']), overviewText() { - return sprintf( - __( - '<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes', - ), - { - stagedFilesLength: this.stagedFiles.length, - changedFilesLength: this.changedFiles.length, - }, - ); + return n__('%d changed file', '%d changed files', this.stagedFiles.length); }, commitButtonText() { return this.stagedFiles.length ? __('Commit') : __('Stage & Commit'); @@ -125,7 +117,7 @@ export default { > {{ __('Commit…') }} </button> - <p class="text-center" v-html="overviewText"></p> + <p class="text-center bold">{{ overviewText }}</p> </div> <form v-if="!isCompact" ref="formEl" @submit.prevent.stop="commitChanges"> <transition name="fade"> <success-message v-show="lastCommitMsg" /> </transition> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index d9a385a9d31..2e273d45506 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -17,10 +17,6 @@ export default { tooltip, }, props: { - title: { - type: String, - required: true, - }, fileList: { type: Array, required: true, @@ -29,18 +25,6 @@ export default { type: String, required: true, }, - action: { - type: String, - required: true, - }, - actionBtnText: { - type: String, - required: true, - }, - actionBtnIcon: { - type: String, - required: true, - }, stagedList: { type: Boolean, required: false, @@ -63,9 +47,9 @@ export default { }, computed: { titleText() { - return sprintf(__('%{title} changes'), { - title: this.title, - }); + if (!this.title) return __('Changes'); + + return sprintf(__('%{title} changes'), { title: this.title }); }, filesLength() { return this.fileList.length; @@ -73,17 +57,16 @@ export default { }, methods: { ...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']), - actionBtnClicked() { - this[this.action](); - - $(this.$refs.actionBtn).tooltip('hide'); - }, openDiscardModal() { $('#discard-all-changes').modal('show'); }, + unstageAndDiscardAllChanges() { + this.unstageAllChanges(); + this.discardAllChanges(); + }, }, discardModalText: __( - "You will lose all the unstaged changes you've made in this project. This action cannot be undone.", + "You will lose all uncommitted changes you've made in this project. This action cannot be undone.", ), }; </script> @@ -96,24 +79,6 @@ export default { <strong> {{ titleText }} </strong> <div class="d-flex ml-auto"> <button - ref="actionBtn" - v-tooltip - :title="actionBtnText" - :aria-label="actionBtnText" - :disabled="!filesLength" - :class="{ - 'disabled-content': !filesLength, - }" - type="button" - class="d-flex ide-staged-action-btn p-0 border-0 align-items-center" - data-placement="bottom" - data-container="body" - data-boundary="viewport" - @click="actionBtnClicked" - > - <icon :name="actionBtnIcon" :size="16" class="ml-auto mr-auto" /> - </button> - <button v-if="!stagedList" v-tooltip :title="__('Discard all changes')" @@ -151,9 +116,9 @@ export default { v-if="!stagedList" id="discard-all-changes" :footer-primary-button-text="__('Discard all changes')" - :header-title-text="__('Discard all unstaged changes?')" + :header-title-text="__('Discard all changes?')" footer-primary-button-variant="danger" - @submit="discardAllChanges" + @submit="unstageAndDiscardAllChanges" > {{ $options.discardModalText }} </gl-modal> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 726e2b7e1fc..e49d96efe50 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -57,13 +57,7 @@ export default { }, }, methods: { - ...mapActions([ - 'discardFileChanges', - 'updateViewer', - 'openPendingTab', - 'unstageChange', - 'stageChange', - ]), + ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']), openFileInEditor() { if (this.file.type === 'tree') return null; @@ -76,13 +70,6 @@ export default { } }); }, - fileAction() { - if (this.file.staged) { - this.unstageChange(this.file.path); - } else { - this.stageChange(this.file.path); - } - }, }, }; </script> @@ -97,7 +84,6 @@ export default { }" class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0" role="button" - @dblclick="fileAction" @click="openFileInEditor" > <span class="multi-file-commit-list-file-path d-flex align-items-center"> diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index 3ef7d863bd5..32822a75772 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -1,6 +1,6 @@ <script> import { mapGetters } from 'vuex'; -import { n__, __, sprintf } from '~/locale'; +import { n__ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; @@ -49,16 +49,7 @@ export default { folderChangesTooltip() { if (this.changesCount === 0) return undefined; - if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) { - return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount); - } else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) { - return n__('%d staged change', '%d staged changes', this.folderStagedCount); - } - - return sprintf(__('%{staged} staged and %{unstaged} unstaged changes'), { - unstaged: this.folderUnstagedCount, - staged: this.folderStagedCount, - }); + return n__('%d changed file', '%d changed files', this.changesCount); }, showTreeChangesCount() { return this.isTree && this.changesCount > 0 && !this.file.opened; diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 62fb0b03975..b8dca2709c8 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -86,28 +86,12 @@ export default { </deprecated-modal> <template v-if="showStageUnstageArea"> <commit-files-list - :title="__('Unstaged')" - :key-prefix="$options.stageKeys.unstaged" - :file-list="changedFiles" - :action-btn-text="__('Stage all changes')" - :active-file-key="activeFileKey" - :empty-state-text="__('There are no unstaged changes')" - action="stageAllChanges" - action-btn-icon="stage-all" - class="is-first" - icon-name="unstaged" - /> - <commit-files-list - :title="__('Staged')" :key-prefix="$options.stageKeys.staged" :file-list="stagedFiles" - :action-btn-text="__('Unstage all changes')" - :staged-list="true" :active-file-key="activeFileKey" - :empty-state-text="__('There are no staged changes')" - action="unstageAllChanges" - action-btn-icon="unstage-all" - icon-name="staged" + :empty-state-text="__('There are no changes')" + class="is-first" + icon-name="unstaged" /> </template> <empty-state v-if="unusedSeal" /> 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 {}; diff --git a/app/assets/javascripts/pages/admin/sessions/index.js b/app/assets/javascripts/pages/admin/sessions/index.js new file mode 100644 index 00000000000..680ebd19a9f --- /dev/null +++ b/app/assets/javascripts/pages/admin/sessions/index.js @@ -0,0 +1 @@ +import '~/pages/sessions/index'; diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue index b9e80899e25..511b3cda9c8 100644 --- a/app/assets/javascripts/releases/components/app_index.vue +++ b/app/assets/javascripts/releases/components/app_index.vue @@ -1,11 +1,12 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlSkeletonLoading, GlEmptyState } from '@gitlab/ui'; +import { GlSkeletonLoading, GlEmptyState, GlLink } from '@gitlab/ui'; import { getParameterByName, historyPushState, buildUrlWithCurrentLocation, } from '~/lib/utils/common_utils'; +import { __ } from '~/locale'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import ReleaseBlock from './release_block.vue'; @@ -16,13 +17,14 @@ export default { GlEmptyState, ReleaseBlock, TablePagination, + GlLink, }, props: { projectId: { type: String, required: true, }, - documentationLink: { + documentationPath: { type: String, required: true, }, @@ -30,6 +32,11 @@ export default { type: String, required: true, }, + newReleasePath: { + type: String, + required: false, + default: '', + }, }, computed: { ...mapState('list', ['isLoading', 'releases', 'hasError', 'pageInfo']), @@ -39,6 +46,11 @@ export default { shouldRenderSuccessState() { return this.releases.length && !this.isLoading && !this.hasError; }, + emptyStateText() { + return __( + "Releases are based on Git tags and mark specific points in a project's development history. They can contain information about the type of changes and can also deliver binaries, like compiled versions of your software.", + ); + }, }, created() { this.fetchReleases({ @@ -56,7 +68,16 @@ export default { }; </script> <template> - <div class="prepend-top-default"> + <div class="flex flex-column mt-2"> + <gl-link + v-if="newReleasePath" + :href="newReleasePath" + :aria-describedby="shouldRenderEmptyState && 'releases-description'" + class="btn btn-success align-self-end mb-2 js-new-release-btn" + > + {{ __('New release') }} + </gl-link> + <gl-skeleton-loading v-if="isLoading" class="js-loading" /> <gl-empty-state @@ -64,14 +85,20 @@ export default { class="js-empty-state" :title="__('Getting started with releases')" :svg-path="illustrationPath" - :description=" - __( - 'Releases are based on Git tags and mark specific points in a project\'s development history. They can contain information about the type of changes and can also deliver binaries, like compiled versions of your software.', - ) - " - :primary-button-link="documentationLink" - :primary-button-text="__('Open Documentation')" - /> + > + <template #description> + <span id="releases-description"> + {{ emptyStateText }} + <gl-link + :href="documentationPath" + :aria-label="__('Releases documentation')" + target="_blank" + > + {{ __('More information') }} + </gl-link> + </span> + </template> + </gl-empty-state> <div v-else-if="shouldRenderSuccessState" class="js-success-state"> <release-block diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js index ad82d9a65d6..5f0bf3b6459 100644 --- a/app/assets/javascripts/releases/mount_index.js +++ b/app/assets/javascripts/releases/mount_index.js @@ -15,11 +15,7 @@ export default () => { }), render: h => h(ReleaseListApp, { - props: { - projectId: el.dataset.projectId, - documentationLink: el.dataset.documentationPath, - illustrationPath: el.dataset.illustrationPath, - }, + props: el.dataset, }), }); }; diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index 9ec99ac93d7..7acbe949151 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -1,7 +1,7 @@ <script> import { GlTooltipDirective } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; -import { __, sprintf } from '~/locale'; +import { __ } from '~/locale'; import { getCommitIconMap } from '~/ide/utils'; export default { @@ -51,17 +51,7 @@ export default { tooltipTitle() { if (!this.showTooltip || !this.file.changed) return undefined; - const type = this.file.tempFile ? 'addition' : 'modification'; - - if (this.file.staged) { - return sprintf(__('Staged %{type}'), { - type, - }); - } - - return sprintf(__('Unstaged %{type}'), { - type, - }); + return this.file.tempFile ? __('Added') : __('Modified'); }, showIcon() { return ( |