diff options
author | Luke Bennett <lbennett@gitlab.com> | 2018-06-07 05:56:41 +0000 |
---|---|---|
committer | Luke Bennett <lbennett@gitlab.com> | 2018-06-07 05:56:41 +0000 |
commit | a97f4ec3615595694b11676484e9ac1ba5524a9e (patch) | |
tree | c82ad96af00a0cdd898f26a8e1da5a9bdae14819 /app/assets | |
parent | 119b128ec8415a074a73b73a7878717779c6e0f3 (diff) | |
parent | 760b12dc6b3a927c918855e2ee85a1c0e6bddb73 (diff) | |
download | gitlab-ce-a97f4ec3615595694b11676484e9ac1ba5524a9e.tar.gz |
Merge branch 'master' into '39549-label-list-page-redesign-with-draggable-labels'
# Conflicts:
# app/views/projects/labels/index.html.haml
Diffstat (limited to 'app/assets')
53 files changed, 1048 insertions, 216 deletions
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue index 0c9ec3b00f0..99fa2465a84 100644 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -11,17 +11,20 @@ export default { }, computed: { ...mapGetters(['currentMergeRequest']), - ...mapState(['viewer']), + ...mapState(['viewer', 'currentMergeRequestId']), showLatestChangesText() { - return !this.currentMergeRequest || this.viewer === viewerTypes.diff; + return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; }, showMergeRequestText() { - return this.currentMergeRequest && this.viewer === viewerTypes.mr; + return this.currentMergeRequestId && this.viewer === viewerTypes.mr; + }, + mergeRequestId() { + return `!${this.currentMergeRequest.iid}`; }, }, mounted() { this.$nextTick(() => { - this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff); + this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); }); }, methods: { @@ -54,7 +57,11 @@ export default { </template> <template v-else-if="showMergeRequestText"> {{ __('Merge request') }} - (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>) + (<a + v-if="currentMergeRequest" + :href="currentMergeRequest.web_url" + v-text="mergeRequestId" + ></a>) </template> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 3f980203911..1dc2170edde 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,4 +1,5 @@ <script> +import $ from 'jquery'; import { mapState, mapGetters } from 'vuex'; import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; import Icon from '~/vue_shared/components/icon.vue'; @@ -13,6 +14,7 @@ import CommitSection from './repo_commit_section.vue'; import CommitForm from './commit_sidebar/form.vue'; import IdeReview from './ide_review.vue'; import SuccessMessage from './commit_sidebar/success_message.vue'; +import MergeRequestDropdown from './merge_requests/dropdown.vue'; import { activityBarViews } from '../constants'; export default { @@ -32,10 +34,12 @@ export default { CommitForm, IdeReview, SuccessMessage, + MergeRequestDropdown, }, data() { return { showTooltip: false, + showMergeRequestsDropdown: false, }; }, computed: { @@ -46,6 +50,7 @@ export default { 'changedFiles', 'stagedFiles', 'lastCommitMsg', + 'currentMergeRequestId', ]), ...mapGetters(['currentProject', 'someUncommitedChanges']), showSuccessMessage() { @@ -61,9 +66,39 @@ export default { watch: { currentBranchId() { this.$nextTick(() => { + if (!this.$refs.branchId) return; + this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth; }); }, + loading() { + this.$nextTick(() => { + this.addDropdownListeners(); + }); + }, + }, + mounted() { + this.addDropdownListeners(); + }, + beforeDestroy() { + $(this.$refs.mergeRequestDropdown) + .off('show.bs.dropdown') + .off('hide.bs.dropdown'); + }, + methods: { + addDropdownListeners() { + if (!this.$refs.mergeRequestDropdown) return; + + $(this.$refs.mergeRequestDropdown) + .on('show.bs.dropdown', () => { + this.toggleMergeRequestDropdown(); + }).on('hide.bs.dropdown', () => { + this.toggleMergeRequestDropdown(); + }); + }, + toggleMergeRequestDropdown() { + this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown; + }, }, }; </script> @@ -88,9 +123,13 @@ export default { </div> </template> <template v-else> - <div class="context-header ide-context-header"> - <a - :href="currentProject.web_url" + <div + class="context-header ide-context-header dropdown" + ref="mergeRequestDropdown" + > + <button + type="button" + data-toggle="dropdown" > <div v-if="currentProject.avatar_url" @@ -114,19 +153,41 @@ export default { <div class="sidebar-context-title"> {{ currentProject.name }} </div> - <div - class="sidebar-context-title ide-sidebar-branch-title" - ref="branchId" - v-tooltip - :title="branchTooltipTitle" - > - <icon - name="branch" - css-classes="append-right-5" - />{{ currentBranchId }} + <div class="d-flex"> + <div + v-if="currentBranchId" + class="sidebar-context-title ide-sidebar-branch-title" + ref="branchId" + v-tooltip + :title="branchTooltipTitle" + > + <icon + name="branch" + css-classes="append-right-5" + />{{ currentBranchId }} + </div> + <div + v-if="currentMergeRequestId" + class="sidebar-context-title ide-sidebar-branch-title" + :class="{ + 'prepend-left-8': currentBranchId + }" + > + <icon + name="git-merge" + css-classes="append-right-5" + />!{{ currentMergeRequestId }} + </div> </div> </div> - </a> + <icon + class="ml-auto" + name="chevron-down" + /> + </button> + <merge-request-dropdown + :show="showMergeRequestsDropdown" + /> </div> <div class="multi-file-commit-panel-inner-scroll"> <component diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 368a2995ed9..e40f137d998 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -35,9 +35,7 @@ export default { }, watch: { lastCommit() { - if (!this.isPollingInitialized) { - this.initPipelinePolling(); - } + this.initPipelinePolling(); }, }, mounted() { @@ -47,9 +45,8 @@ export default { if (this.intervalId) { clearInterval(this.intervalId); } - if (this.isPollingInitialized) { - this.stopPipelinePolling(); - } + + this.stopPipelinePolling(); }, methods: { ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), @@ -59,8 +56,9 @@ export default { }, 1000); }, initPipelinePolling() { - this.fetchLatestPipeline(); - this.isPollingInitialized = true; + if (this.lastCommit) { + this.fetchLatestPipeline(); + } }, commitAgeUpdate() { if (this.lastCommit) { diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue new file mode 100644 index 00000000000..4d234a36fe5 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -0,0 +1,136 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import _ from 'underscore'; +import { __ } from '../../../locale'; +import tooltip from '../../../vue_shared/directives/tooltip'; +import Icon from '../../../vue_shared/components/icon.vue'; +import ScrollButton from './detail/scroll_button.vue'; +import JobDescription from './detail/description.vue'; + +const scrollPositions = { + top: 0, + bottom: 1, +}; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + ScrollButton, + JobDescription, + }, + data() { + return { + scrollPos: scrollPositions.top, + }; + }, + computed: { + ...mapState('pipelines', ['detailJob']), + isScrolledToBottom() { + return this.scrollPos === scrollPositions.bottom; + }, + isScrolledToTop() { + return this.scrollPos === scrollPositions.top; + }, + jobOutput() { + return this.detailJob.output || __('No messages were logged'); + }, + }, + mounted() { + this.getTrace(); + }, + methods: { + ...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']), + scrollDown() { + if (this.$refs.buildTrace) { + this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight); + } + }, + scrollUp() { + if (this.$refs.buildTrace) { + this.$refs.buildTrace.scrollTo(0, 0); + } + }, + scrollBuildLog: _.throttle(function buildLogScrollDebounce() { + const { scrollTop } = this.$refs.buildTrace; + const { offsetHeight, scrollHeight } = this.$refs.buildTrace; + + if (scrollTop + offsetHeight === scrollHeight) { + this.scrollPos = scrollPositions.bottom; + } else if (scrollTop === 0) { + this.scrollPos = scrollPositions.top; + } else { + this.scrollPos = ''; + } + }), + getTrace() { + return this.fetchJobTrace().then(() => this.scrollDown()); + }, + }, +}; +</script> + +<template> + <div class="ide-pipeline build-page d-flex flex-column flex-fill"> + <header class="ide-job-header d-flex align-items-center"> + <button + class="btn btn-default btn-sm d-flex" + @click="setDetailJob(null)" + > + <icon + name="chevron-left" + /> + {{ __('View jobs') }} + </button> + </header> + <div class="top-bar d-flex border-left-0"> + <job-description + :job="detailJob" + /> + <div class="controllers ml-auto"> + <a + v-tooltip + :title="__('Show complete raw log')" + data-placement="top" + data-container="body" + class="controllers-buttons" + :href="detailJob.rawPath" + target="_blank" + > + <i + aria-hidden="true" + class="fa fa-file-text-o" + ></i> + </a> + <scroll-button + direction="up" + :disabled="isScrolledToTop" + @click="scrollUp" + /> + <scroll-button + direction="down" + :disabled="isScrolledToBottom" + @click="scrollDown" + /> + </div> + </div> + <pre + class="build-trace mb-0 h-100" + ref="buildTrace" + @scroll="scrollBuildLog" + > + <code + class="bash" + v-html="jobOutput" + > + </code> + <div + v-show="detailJob.isLoading" + class="build-loader-animation" + > + </div> + </pre> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue new file mode 100644 index 00000000000..def6bac3157 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue @@ -0,0 +1,47 @@ +<script> +import Icon from '../../../../vue_shared/components/icon.vue'; +import CiIcon from '../../../../vue_shared/components/ci_icon.vue'; + +export default { + components: { + Icon, + CiIcon, + }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { + jobId() { + return `#${this.job.id}`; + }, + }, +}; +</script> + +<template> + <div class="d-flex align-items-center"> + <ci-icon + class="d-flex" + :status="job.status" + :borderless="true" + :size="24" + /> + <span class="prepend-left-8"> + {{ job.name }} + <a + :href="job.path" + target="_blank" + class="ide-external-link" + > + {{ jobId }} + <icon + name="external-link" + :size="12" + /> + </a> + </span> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue new file mode 100644 index 00000000000..4e19e6e9c84 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue @@ -0,0 +1,66 @@ +<script> +import { __ } from '../../../../locale'; +import Icon from '../../../../vue_shared/components/icon.vue'; +import tooltip from '../../../../vue_shared/directives/tooltip'; + +const directions = { + up: 'up', + down: 'down', +}; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + }, + props: { + direction: { + type: String, + required: true, + validator(value) { + return Object.keys(directions).includes(value); + }, + }, + disabled: { + type: Boolean, + required: true, + }, + }, + computed: { + tooltipTitle() { + return this.direction === directions.up ? __('Scroll to top') : __('Scroll to bottom'); + }, + iconName() { + return `scroll_${this.direction}`; + }, + }, + methods: { + clickedScroll() { + this.$emit('click'); + }, + }, +}; +</script> + +<template> + <div + v-tooltip + class="controllers-buttons" + data-container="body" + data-placement="top" + :title="tooltipTitle" + > + <button + class="btn-scroll btn-transparent btn-blank" + type="button" + :disabled="disabled" + @click="clickedScroll" + > + <icon + :name="iconName" + /> + </button> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue index c33936021d4..c8e621504f0 100644 --- a/app/assets/javascripts/ide/components/jobs/item.vue +++ b/app/assets/javascripts/ide/components/jobs/item.vue @@ -1,11 +1,9 @@ <script> -import Icon from '../../../vue_shared/components/icon.vue'; -import CiIcon from '../../../vue_shared/components/ci_icon.vue'; +import JobDescription from './detail/description.vue'; export default { components: { - Icon, - CiIcon, + JobDescription, }, props: { job: { @@ -18,29 +16,29 @@ export default { return `#${this.job.id}`; }, }, + methods: { + clickViewLog() { + this.$emit('clickViewLog', this.job); + }, + }, }; </script> <template> <div class="ide-job-item"> - <ci-icon - :status="job.status" - :borderless="true" - :size="24" + <job-description + class="append-right-default" + :job="job" /> - <span class="prepend-left-8"> - {{ job.name }} - <a - :href="job.path" - target="_blank" - class="ide-external-link" + <div class="ml-auto align-self-center"> + <button + v-if="job.started" + type="button" + class="btn btn-default btn-sm" + @click="clickViewLog" > - {{ jobId }} - <icon - name="external-link" - :size="12" - /> - </a> - </span> + {{ __('View log') }} + </button> + </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue index bdd0364c9b9..3b16b860ecd 100644 --- a/app/assets/javascripts/ide/components/jobs/list.vue +++ b/app/assets/javascripts/ide/components/jobs/list.vue @@ -19,7 +19,7 @@ export default { }, }, methods: { - ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']), + ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed', 'setDetailJob']), }, }; </script> @@ -38,6 +38,7 @@ export default { :stage="stage" @fetch="fetchJobs" @toggleCollapsed="toggleStageCollapsed" + @clickViewLog="setDetailJob" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue index 5b24bb1f5a7..b1428f885fb 100644 --- a/app/assets/javascripts/ide/components/jobs/stage.vue +++ b/app/assets/javascripts/ide/components/jobs/stage.vue @@ -48,6 +48,9 @@ export default { toggleCollapsed() { this.$emit('toggleCollapsed', this.stage.id); }, + clickViewLog(job) { + this.$emit('clickViewLog', job); + }, }, }; </script> @@ -101,6 +104,7 @@ export default { v-for="job in stage.jobs" :key="job.id" :job="job" + @clickViewLog="clickViewLog" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue new file mode 100644 index 00000000000..8cc8345db2e --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue @@ -0,0 +1,63 @@ +<script> +import { mapGetters } from 'vuex'; +import Tabs from '../../../vue_shared/components/tabs/tabs'; +import Tab from '../../../vue_shared/components/tabs/tab.vue'; +import List from './list.vue'; + +export default { + components: { + Tabs, + Tab, + List, + }, + props: { + show: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapGetters('mergeRequests', ['assignedData', 'createdData']), + createdMergeRequestLength() { + return this.createdData.mergeRequests.length; + }, + assignedMergeRequestLength() { + return this.assignedData.mergeRequests.length; + }, + }, +}; +</script> + +<template> + <div class="dropdown-menu ide-merge-requests-dropdown p-0"> + <tabs + v-if="show" + stop-propagation + > + <tab active> + <template slot="title"> + {{ __('Created by me') }} + <span class="badge badge-pill"> + {{ createdMergeRequestLength }} + </span> + </template> + <list + type="created" + :empty-text="__('You have not created any merge requests')" + /> + </tab> + <tab> + <template slot="title"> + {{ __('Assigned to me') }} + <span class="badge badge-pill"> + {{ assignedMergeRequestLength }} + </span> + </template> + <list + type="assigned" + :empty-text="__('You do not have any assigned merge requests')" + /> + </tab> + </tabs> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue new file mode 100644 index 00000000000..b50fc8a3dbb --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/item.vue @@ -0,0 +1,63 @@ +<script> +import Icon from '../../../vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + item: { + type: Object, + required: true, + }, + currentId: { + type: String, + required: true, + }, + currentProjectId: { + type: String, + required: true, + }, + }, + computed: { + isActive() { + return ( + this.item.iid === parseInt(this.currentId, 10) && + this.currentProjectId === this.item.projectPathWithNamespace + ); + }, + pathWithID() { + return `${this.item.projectPathWithNamespace}!${this.item.iid}`; + }, + }, + methods: { + clickItem() { + this.$emit('click', this.item); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="btn-link d-flex align-items-center" + @click="clickItem" + > + <span class="d-flex append-right-default ide-merge-request-current-icon"> + <icon + v-if="isActive" + name="mobile-issue-close" + :size="18" + /> + </span> + <span> + <strong> + {{ item.title }} + </strong> + <span class="ide-merge-request-project-path d-block mt-1"> + {{ pathWithID }} + </span> + </span> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue new file mode 100644 index 00000000000..5896e3a147d --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -0,0 +1,132 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import _ from 'underscore'; +import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import Item from './item.vue'; + +export default { + components: { + LoadingIcon, + Item, + }, + props: { + type: { + type: String, + required: true, + }, + emptyText: { + type: String, + required: true, + }, + }, + data() { + return { + search: '', + }; + }, + computed: { + ...mapGetters('mergeRequests', ['getData']), + ...mapState(['currentMergeRequestId', 'currentProjectId']), + data() { + return this.getData(this.type); + }, + isLoading() { + return this.data.isLoading; + }, + mergeRequests() { + return this.data.mergeRequests; + }, + hasMergeRequests() { + return this.mergeRequests.length !== 0; + }, + hasNoSearchResults() { + return this.search !== '' && !this.hasMergeRequests; + }, + }, + watch: { + isLoading: { + handler: 'focusSearch', + }, + }, + mounted() { + this.loadMergeRequests(); + }, + methods: { + ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']), + loadMergeRequests() { + this.fetchMergeRequests({ type: this.type, search: this.search }); + }, + viewMergeRequest(item) { + this.openMergeRequest({ + projectPath: item.projectPathWithNamespace, + id: item.iid, + }); + }, + searchMergeRequests: _.debounce(function debounceSearch() { + this.loadMergeRequests(); + }, 250), + focusSearch() { + if (!this.isLoading) { + this.$nextTick(() => { + this.$refs.searchInput.focus(); + }); + } + }, + }, +}; +</script> + +<template> + <div> + <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom"> + <input + type="search" + class="dropdown-input-field" + :placeholder="__('Search merge requests')" + v-model="search" + @input="searchMergeRequests" + ref="searchInput" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + ></i> + </div> + <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> + <loading-icon + class="mt-3 mb-3 align-self-center ml-auto mr-auto" + v-if="isLoading" + size="2" + /> + <ul + v-else + class="mb-3 w-100" + > + <template v-if="hasMergeRequests"> + <li + v-for="item in mergeRequests" + :key="item.id" + > + <item + :item="item" + :current-id="currentMergeRequestId" + :current-project-id="currentProjectId" + @click="viewMergeRequest" + /> + </li> + </template> + <li + v-else + class="ide-merge-requests-empty d-flex align-items-center justify-content-center" + > + <template v-if="hasNoSearchResults"> + {{ __('No merge requests found') }} + </template> + <template v-else> + {{ emptyText }} + </template> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 703c4a70cfa..aafd6a15a78 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -4,6 +4,7 @@ import tooltip from '../../../vue_shared/directives/tooltip'; import Icon from '../../../vue_shared/components/icon.vue'; import { rightSidebarViews } from '../../constants'; import PipelinesList from '../pipelines/list.vue'; +import JobsDetail from '../jobs/detail.vue'; export default { directives: { @@ -12,9 +13,16 @@ export default { components: { Icon, PipelinesList, + JobsDetail, }, computed: { ...mapState(['rightPane']), + pipelinesActive() { + return ( + this.rightPane === rightSidebarViews.pipelines || + this.rightPane === rightSidebarViews.jobsDetail + ); + }, }, methods: { ...mapActions(['setRightPane']), @@ -48,7 +56,7 @@ export default { :title="__('Pipelines')" class="ide-sidebar-link is-right" :class="{ - active: rightPane === $options.rightSidebarViews.pipelines + active: pipelinesActive }" type="button" @click="clickTab($event, $options.rightSidebarViews.pipelines)" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 33cd20caf52..65886c02b92 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -23,4 +23,5 @@ export const viewerTypes = { export const rightSidebarViews = { pipelines: 'pipelines-list', + jobsDetail: 'jobs-detail', }; diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 5ec9bd661bb..edb20ff96fc 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -17,9 +17,7 @@ export const getMergeRequestData = ( mergeRequestId, mergeRequest: data, }); - if (!state.currentMergeRequestId) { - commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId); - } + commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId); resolve(data); }) .catch(() => { diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 46af47d2f81..0b99bce4a8e 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -13,8 +13,7 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force .then(data => { commit(types.TOGGLE_LOADING, { entry: state }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); - if (!state.currentProjectId) - commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); + commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); resolve(data); }) .catch(() => { diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js index d3050183bd3..5beb8fac71f 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js @@ -1,25 +1,42 @@ import { __ } from '../../../../locale'; import Api from '../../../../api'; import flash from '../../../../flash'; +import router from '../../../ide_router'; +import { scopes } from './constants'; import * as types from './mutation_types'; +import * as rootTypes from '../../mutation_types'; -export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS); -export const receiveMergeRequestsError = ({ commit }) => { +export const requestMergeRequests = ({ commit }, type) => + commit(types.REQUEST_MERGE_REQUESTS, type); +export const receiveMergeRequestsError = ({ commit }, type) => { flash(__('Error loading merge requests.')); - commit(types.RECEIVE_MERGE_REQUESTS_ERROR); + commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type); }; -export const receiveMergeRequestsSuccess = ({ commit }, data) => - commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data); +export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) => + commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data }); -export const fetchMergeRequests = ({ dispatch, state: { scope, state } }, search = '') => { - dispatch('requestMergeRequests'); - dispatch('resetMergeRequests'); +export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => { + const scope = scopes[type]; + dispatch('requestMergeRequests', type); + dispatch('resetMergeRequests', type); Api.mergeRequests({ scope, state, search }) - .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data)) - .catch(() => dispatch('receiveMergeRequestsError')); + .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data })) + .catch(() => dispatch('receiveMergeRequestsError', type)); }; -export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS); +export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type); + +export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => { + commit(rootTypes.CLEAR_PROJECTS, null, { root: true }); + commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true }); + commit(rootTypes.RESET_OPEN_FILES, null, { root: true }); + dispatch('pipelines/stopPipelinePolling', null, { root: true }); + dispatch('pipelines/clearEtagPoll', null, { root: true }); + dispatch('pipelines/resetLatestPipeline', null, { root: true }); + dispatch('setCurrentBranchId', '', { root: true }); + + router.push(`/project/${projectPath}/merge_requests/${id}`); +}; export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js index 64b7763f257..a7085c7d04c 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js @@ -1,6 +1,6 @@ export const scopes = { - assignedToMe: 'assigned-to-me', - createdByMe: 'created-by-me', + assigned: 'assigned-to-me', + created: 'created-by-me', }; export const states = { diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js new file mode 100644 index 00000000000..8e2b234be8d --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js @@ -0,0 +1,4 @@ +export const getData = state => type => state[type]; + +export const assignedData = state => state.assigned; +export const createdData = state => state.created; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js index 04e7e0f08f1..2e6dfb420f4 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js @@ -1,5 +1,6 @@ import state from './state'; import * as actions from './actions'; +import * as getters from './getters'; import mutations from './mutations'; export default { @@ -7,4 +8,5 @@ export default { state: state(), actions, mutations, + getters, }; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js index 98102a68e08..971da0806bd 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js @@ -2,15 +2,15 @@ import * as types from './mutation_types'; export default { - [types.REQUEST_MERGE_REQUESTS](state) { - state.isLoading = true; + [types.REQUEST_MERGE_REQUESTS](state, type) { + state[type].isLoading = true; }, - [types.RECEIVE_MERGE_REQUESTS_ERROR](state) { - state.isLoading = false; + [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) { + state[type].isLoading = false; }, - [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) { - state.isLoading = false; - state.mergeRequests = data.map(mergeRequest => ({ + [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) { + state[type].isLoading = false; + state[type].mergeRequests = data.map(mergeRequest => ({ id: mergeRequest.id, iid: mergeRequest.iid, title: mergeRequest.title, @@ -20,7 +20,7 @@ export default { .replace(`/merge_requests/${mergeRequest.iid}`, ''), })); }, - [types.RESET_MERGE_REQUESTS](state) { - state.mergeRequests = []; + [types.RESET_MERGE_REQUESTS](state, type) { + state[type].mergeRequests = []; }, }; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js index 2947b686c1c..57eb6b04283 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js @@ -1,8 +1,13 @@ -import { scopes, states } from './constants'; +import { states } from './constants'; export default () => ({ - isLoading: false, - mergeRequests: [], - scope: scopes.assignedToMe, + created: { + isLoading: false, + mergeRequests: [], + }, + assigned: { + isLoading: false, + mergeRequests: [], + }, state: states.opened, }); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 1ebe487263b..0a4ea80c4c1 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -4,6 +4,7 @@ import { __ } from '../../../../locale'; import flash from '../../../../flash'; import Poll from '../../../../lib/utils/poll'; import service from '../../../services'; +import { rightSidebarViews } from '../../../constants'; import * as types from './mutation_types'; let eTagPoll; @@ -77,4 +78,31 @@ export const fetchJobs = ({ dispatch }, stage) => { export const toggleStageCollapsed = ({ commit }, stageId) => commit(types.TOGGLE_STAGE_COLLAPSE, stageId); +export const setDetailJob = ({ commit, dispatch }, job) => { + commit(types.SET_DETAIL_JOB, job); + dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { + root: true, + }); +}; + +export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE); +export const receiveJobTraceError = ({ commit }) => { + flash(__('Error fetching job trace')); + commit(types.RECEIVE_JOB_TRACE_ERROR); +}; +export const receiveJobTraceSuccess = ({ commit }, data) => + commit(types.RECEIVE_JOB_TRACE_SUCCESS, data); + +export const fetchJobTrace = ({ dispatch, state }) => { + dispatch('requestJobTrace'); + + return axios + .get(`${state.detailJob.path}/trace`, { params: { format: 'json' } }) + .then(({ data }) => dispatch('receiveJobTraceSuccess', data)) + .catch(() => dispatch('receiveJobTraceError')); +}; + +export const resetLatestPipeline = ({ commit }) => + commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null); + export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js index 3ddc8409c5b..f4c36b9d96f 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js @@ -7,3 +7,9 @@ export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE'; + +export const SET_DETAIL_JOB = 'SET_DETAIL_JOB'; + +export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE'; +export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR'; +export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js index 745797e1ee5..5a2213bbe89 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -63,4 +63,17 @@ export default { isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed, })); }, + [types.SET_DETAIL_JOB](state, job) { + state.detailJob = { ...job }; + }, + [types.REQUEST_JOB_TRACE](state) { + state.detailJob.isLoading = true; + }, + [types.RECEIVE_JOB_TRACE_ERROR](state) { + state.detailJob.isLoading = false; + }, + [types.RECEIVE_JOB_TRACE_SUCCESS](state, data) { + state.detailJob.isLoading = false; + state.detailJob.output = data.html; + }, }; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js index 0f83b315fff..8651e267b53 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js @@ -3,4 +3,5 @@ export default () => ({ isLoadingJobs: false, latestPipeline: null, stages: [], + detailJob: null, }); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js index 9f4b0d7d726..a6caca2d2dc 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js @@ -4,4 +4,8 @@ export const normalizeJob = job => ({ name: job.name, status: job.status, path: job.build_path, + rawPath: `${job.build_path}/raw`, + started: job.started, + output: '', + isLoading: false, }); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index fbfb92105d6..99b315ac4db 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -68,3 +68,6 @@ export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const SET_RIGHT_PANE = 'SET_RIGHT_PANE'; + +export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; +export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index eeaa7cb0ec3..48f1da4eccf 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -157,6 +157,12 @@ export default { [types.SET_LINKS](state, links) { Object.assign(state, { links }); }, + [types.CLEAR_PROJECTS](state) { + Object.assign(state, { projects: {}, trees: {} }); + }, + [types.RESET_OPEN_FILES](state) { + Object.assign(state, { openFiles: [] }); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 52455885248..f9ff0722c01 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -67,7 +67,15 @@ class ImporterStatus { false, )); }) - .catch(() => flash(__('An error occurred while importing project'))); + .catch((error) => { + let details = error; + + if (error.response && error.response.data && error.response.data.errors) { + details = error.response.data.errors; + } + + flash(__(`An error occurred while importing project: ${details}`)); + }); } autoUpdate() { diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index 09cca1dc7d9..5c5a6e01848 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import stickyMonitor from './lib/utils/sticky'; +import { stickyMonitor } from './lib/utils/sticky'; export default (stickyTop) => { stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index 611e8200b4d..fc13f467675 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,14 +1,17 @@ import $ from 'jquery'; import _ from 'underscore'; -import StickyFill from 'stickyfilljs'; +import { polyfillSticky } from './lib/utils/sticky'; import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; import { numberToHumanSize } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; +import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils'; +import LogOutputBehaviours from './lib/utils/logoutput_behaviours'; -export default class Job { +export default class Job extends LogOutputBehaviours { constructor(options) { + super(); this.timeout = null; this.state = null; this.fetchingStatusFavicon = false; @@ -29,10 +32,6 @@ export default class Job { this.$buildTraceOutput = $('.js-build-output'); this.$topBar = $('.js-top-bar'); - // Scroll controllers - this.$scrollTopBtn = $('.js-scroll-up'); - this.$scrollBottomBtn = $('.js-scroll-down'); - clearTimeout(this.timeout); this.initSidebar(); @@ -48,23 +47,14 @@ export default class Job { .off('click', '.stage-item') .on('click', '.stage-item', this.updateDropdown); - // add event listeners to the scroll buttons - this.$scrollTopBtn - .off('click') - .on('click', this.scrollToTop.bind(this)); - - this.$scrollBottomBtn - .off('click') - .on('click', this.scrollToBottom.bind(this)); - this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.$window .off('scroll') .on('scroll', () => { - if (!this.isScrolledToBottom()) { + if (!isScrolledToBottom()) { this.toggleScrollAnimation(false); - } else if (this.isScrolledToBottom() && !this.isLogComplete) { + } else if (isScrolledToBottom() && !this.isLogComplete) { this.toggleScrollAnimation(true); } this.scrollThrottled(); @@ -80,70 +70,11 @@ export default class Job { } initAffixTopArea() { - /** - If the browser does not support position sticky, it returns the position as static. - If the browser does support sticky, then we allow the browser to handle it, if not - then we use a polyfill - */ - if (this.$topBar.css('position') !== 'static') return; - - StickyFill.add(this.$topBar); - } - - // eslint-disable-next-line class-methods-use-this - canScroll() { - return $(document).height() > $(window).height(); - } - - toggleScroll() { - const $document = $(document); - const currentPosition = $document.scrollTop(); - const scrollHeight = $document.height(); - - const windowHeight = $(window).height(); - if (this.canScroll()) { - if (currentPosition > 0 && - (scrollHeight - currentPosition !== windowHeight)) { - // User is in the middle of the log - - this.toggleDisableButton(this.$scrollTopBtn, false); - this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (currentPosition === 0) { - // User is at Top of Log - - this.toggleDisableButton(this.$scrollTopBtn, true); - this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (this.isScrolledToBottom()) { - // User is at the bottom of the build log. - - this.toggleDisableButton(this.$scrollTopBtn, false); - this.toggleDisableButton(this.$scrollBottomBtn, true); - } - } else { - this.toggleDisableButton(this.$scrollTopBtn, true); - this.toggleDisableButton(this.$scrollBottomBtn, true); - } - } - // eslint-disable-next-line class-methods-use-this - isScrolledToBottom() { - const $document = $(document); - - const currentPosition = $document.scrollTop(); - const scrollHeight = $document.height(); - - const windowHeight = $(window).height(); - - return scrollHeight - currentPosition === windowHeight; - } - - // eslint-disable-next-line class-methods-use-this - scrollDown() { - const $document = $(document); - $document.scrollTop($document.height()); + polyfillSticky(this.$topBar); } scrollToBottom() { - this.scrollDown(); + scrollDown(); this.hasBeenScrolled = true; this.toggleScroll(); } @@ -154,12 +85,6 @@ export default class Job { this.toggleScroll(); } - // eslint-disable-next-line class-methods-use-this - toggleDisableButton($button, disable) { - if (disable && $button.prop('disabled')) return; - $button.prop('disabled', disable); - } - toggleScrollAnimation(toggle) { this.$scrollBottomBtn.toggleClass('animate', toggle); } @@ -191,7 +116,7 @@ export default class Job { this.state = log.state; } - this.isScrollInBottom = this.isScrolledToBottom(); + this.isScrollInBottom = isScrolledToBottom(); if (log.append) { this.$buildTraceOutput.append(log.html); @@ -231,7 +156,7 @@ export default class Job { }) .then(() => { if (this.isScrollInBottom) { - this.scrollDown(); + scrollDown(); } }) .then(() => this.toggleScroll()); diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index c1044f4cd42..5704d753277 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -42,6 +42,9 @@ export default { jobStarted() { return !this.job.started === false; }, + headerTime() { + return this.jobStarted ? this.job.started : this.job.created_at; + }, }, watch: { job() { @@ -73,7 +76,7 @@ export default { :status="status" item-name="Job" :item-id="job.id" - :time="job.created_at" + :time="headerTime" :user="job.user" :actions="actions" :has-sidebar-button="true" diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js new file mode 100644 index 00000000000..1bf99d935ef --- /dev/null +++ b/app/assets/javascripts/lib/utils/logoutput_behaviours.js @@ -0,0 +1,46 @@ +import $ from 'jquery'; +import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils'; + +export default class LogOutputBehaviours { + constructor() { + // Scroll buttons + this.$scrollTopBtn = $('.js-scroll-up'); + this.$scrollBottomBtn = $('.js-scroll-down'); + + this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this)); + this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this)); + } + + toggleScroll() { + const $document = $(document); + const currentPosition = $document.scrollTop(); + const scrollHeight = $document.height(); + + const windowHeight = $(window).height(); + if (canScroll()) { + if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) { + // User is in the middle of the log + + toggleDisableButton(this.$scrollTopBtn, false); + toggleDisableButton(this.$scrollBottomBtn, false); + } else if (currentPosition === 0) { + // User is at Top of Log + + toggleDisableButton(this.$scrollTopBtn, true); + toggleDisableButton(this.$scrollBottomBtn, false); + } else if (isScrolledToBottom()) { + // User is at the bottom of the build log. + + toggleDisableButton(this.$scrollTopBtn, false); + toggleDisableButton(this.$scrollBottomBtn, true); + } + } else { + toggleDisableButton(this.$scrollTopBtn, true); + toggleDisableButton(this.$scrollBottomBtn, true); + } + } + + toggleScrollAnimation(toggle) { + this.$scrollBottomBtn.toggleClass('animate', toggle); + } +} diff --git a/app/assets/javascripts/lib/utils/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js new file mode 100644 index 00000000000..9313b570863 --- /dev/null +++ b/app/assets/javascripts/lib/utils/scroll_utils.js @@ -0,0 +1,29 @@ +import $ from 'jquery'; + +export const canScroll = () => $(document).height() > $(window).height(); + +/** + * Checks if the entire page is scrolled down all the way to the bottom + */ +export const isScrolledToBottom = () => { + const $document = $(document); + + const currentPosition = $document.scrollTop(); + const scrollHeight = $document.height(); + + const windowHeight = $(window).height(); + + return scrollHeight - currentPosition === windowHeight; +}; + +export const scrollDown = () => { + const $document = $(document); + $document.scrollTop($document.height()); +}; + +export const toggleDisableButton = ($button, disable) => { + if (disable && $button.prop('disabled')) return; + $button.prop('disabled', disable); +}; + +export default {}; diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 098afcfa1b4..15a4dd62012 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -1,3 +1,5 @@ +import StickyFill from 'stickyfilljs'; + export const createPlaceholder = () => { const placeholder = document.createElement('div'); placeholder.classList.add('sticky-placeholder'); @@ -28,7 +30,16 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { } }; -export default (el, stickyTop, insertPlaceholder = true) => { +/** + * Create a listener that will toggle a 'is-stuck' class, based on the current scroll position. + * + * - If the current environment does not support `position: sticky`, do nothing. + * + * @param {HTMLElement} el The `position: sticky` element. + * @param {Number} stickyTop Used to determine when an element is stuck. + * @param {Boolean} insertPlaceholder Should a placeholder element be created when element is stuck? + */ +export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => { if (!el) return; if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return; @@ -37,3 +48,13 @@ export default (el, stickyTop, insertPlaceholder = true) => { passive: true, }); }; + +/** + * Polyfill the `position: sticky` behavior. + * + * - If the current environment supports `position: sticky`, do nothing. + * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement. + */ +export const polyfillSticky = (el) => { + StickyFill.add(el); +}; diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index f5572be5fbf..21934021852 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -174,7 +174,10 @@ export default { :tags-path="tagsPath" :show-legend="showLegend" :small-graph="forceSmallGraph" - /> + > + <!-- EE content --> + {{ null }} + </graph> </graph-group> </div> <empty-state diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index de6755e0414..503ee1ce3d1 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -232,9 +232,14 @@ export default { @mouseover="showFlagContent = true" @mouseleave="showFlagContent = false" > - <h5 class="text-center graph-title"> - {{ graphData.title }} - </h5> + <div class="prometheus-graph-header"> + <h5 class="prometheus-graph-title"> + {{ graphData.title }} + </h5> + <div class="prometheus-graph-widgets"> + <slot></slot> + </div> + </div> <div class="prometheus-svg-container" :style="paddingBottomRootSvg" diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue index ab7d2d41ece..6ed35c0a981 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -89,14 +89,13 @@ export default { <div> <div class="js-gcp-machine-type-dropdown dropdown" - :class="{ 'gl-show-field-errors': hasErrors }" > <dropdown-hidden-input :name="fieldName" :value="selectedMachineType" /> <dropdown-button - :class="{ 'gl-field-error-outline': hasErrors }" + :class="{ 'border-danger': hasErrors }" :is-disabled="isDisabled" :is-loading="isLoading" :toggle-text="toggleText" @@ -132,8 +131,11 @@ export default { </div> </div> <span - class="form-text text-muted" - :class="{ 'gl-field-error': hasErrors }" + class="form-text" + :class="{ + 'text-danger': hasErrors, + 'text-muted': !hasErrors + }" v-if="hasErrors" > {{ errorMessage }} diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue index 25350ef0fa9..542d4d21a22 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue @@ -147,7 +147,6 @@ export default { <div> <div class="js-gcp-project-id-dropdown dropdown" - :class="{ 'gl-show-field-errors': hasErrors }" > <dropdown-hidden-input :name="fieldName" @@ -155,7 +154,7 @@ export default { /> <dropdown-button :class="{ - 'gl-field-error-outline': hasErrors, + 'border-danger': hasErrors, 'read-only': hasOneProject }" :is-disabled="isDisabled" @@ -193,8 +192,11 @@ export default { </div> </div> <span - class="form-text text-muted" - :class="{ 'gl-field-error': hasErrors }" + class="form-text" + :class="{ + 'text-danger': hasErrors, + 'text-muted': !hasErrors + }" v-html="helpText" ></span> </div> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue index 8ee4eefcd91..bc28f8b5df4 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue @@ -63,14 +63,13 @@ export default { <div> <div class="js-gcp-zone-dropdown dropdown" - :class="{ 'gl-show-field-errors': hasErrors }" > <dropdown-hidden-input :name="fieldName" :value="selectedZone" /> <dropdown-button - :class="{ 'gl-field-error-outline': hasErrors }" + :class="{ 'border-danger': hasErrors }" :is-disabled="isDisabled" :is-loading="isLoading" :toggle-text="toggleText" @@ -106,8 +105,11 @@ export default { </div> </div> <span - class="form-text text-muted" - :class="{ 'gl-field-error': hasErrors }" + class="form-text" + :class="{ + 'text-danger': hasErrors, + 'text-muted': !hasErrors + }" v-if="hasErrors" > {{ errorMessage }} diff --git a/app/assets/javascripts/vue_shared/components/tabs/tab.vue b/app/assets/javascripts/vue_shared/components/tabs/tab.vue index 2a35d6bc151..9b2f46186ac 100644 --- a/app/assets/javascripts/vue_shared/components/tabs/tab.vue +++ b/app/assets/javascripts/vue_shared/components/tabs/tab.vue @@ -26,6 +26,11 @@ export default { created() { this.isTab = true; }, + updated() { + if (this.$parent) { + this.$parent.$forceUpdate(); + } + }, }; </script> diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js index 4362264caa5..9b9e4bb47bd 100644 --- a/app/assets/javascripts/vue_shared/components/tabs/tabs.js +++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js @@ -1,4 +1,11 @@ export default { + props: { + stopPropagation: { + type: Boolean, + required: false, + default: false, + }, + }, data() { return { currentIndex: 0, @@ -13,7 +20,12 @@ export default { this.tabs = this.$children.filter(child => child.isTab); this.currentIndex = this.tabs.findIndex(tab => tab.localActive); }, - setTab(index) { + setTab(e, index) { + if (this.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + this.tabs[this.currentIndex].localActive = false; this.tabs[index].localActive = true; @@ -36,7 +48,7 @@ export default { href: '#', }, on: { - click: () => this.setTab(i), + click: e => this.setTab(e, i), }, }, tab.$slots.title || tab.title, diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index 3785aaa43f0..79f580546c3 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -251,3 +251,13 @@ table { pre code { white-space: pre-wrap; } + +.alert-danger { + background-color: $red-500; + border-color: $red-500; + color: $white-light; + + h4 { + color: $white-light; + } +} diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 1a415e1b852..9cbaaa5dc8d 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -26,19 +26,25 @@ margin-right: 2px; width: $contextual-sidebar-width; - a { + > a, + > button { transition: padding $sidebar-transition-duration; font-weight: $gl-font-weight-bold; display: flex; + width: 100%; align-items: center; padding: 10px 16px 10px 10px; color: $gl-text-color; - } + background-color: transparent; + border: 0; + text-align: left; - &:hover, - a:hover { - background-color: $link-hover-background; - color: $gl-text-color; + &:hover, + &:focus { + background-color: $link-hover-background; + color: $gl-text-color; + outline: 0; + } } .avatar-container { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 0ee5748952a..551a7e852ae 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -299,6 +299,7 @@ height: 14px; width: 14px; vertical-align: middle; + margin-bottom: 4px; } .dropdown-toggle-text { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index a10bd1544c5..10c23f6c407 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -1,5 +1,6 @@ .table-holder { margin: 0; + overflow: auto; } table { diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss index d5cc78a6680..20394cc1e52 100644 --- a/app/assets/stylesheets/framework/toggle.scss +++ b/app/assets/stylesheets/framework/toggle.scss @@ -42,6 +42,10 @@ background: none; } + &:focus { + outline: none; + } + .toggle-icon { position: relative; display: block; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 1c3d312f7ac..b2416a3d5bc 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -282,9 +282,6 @@ box-shadow: 0 1px 2px $issue-boards-card-shadow; list-style: none; - // as a fallback, hide overflow content so that dragging and dropping still works - overflow: hidden; - &:not(:last-child) { margin-bottom: 5px; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 9ee02ca1d83..9213ccd4cdf 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -75,6 +75,7 @@ .top-bar { height: 35px; + min-height: 35px; background: $gray-light; border: 1px solid $border-color; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index cd0d67613c3..06f08ae2215 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -23,7 +23,6 @@ } .btn-group { - > a { color: $gl-text-color-secondary; } @@ -245,6 +244,7 @@ .prometheus-graph { flex: 1 0 auto; min-width: 450px; + max-width: 100%; padding: $gl-padding / 2; h5 { @@ -256,6 +256,17 @@ } } +.prometheus-graph-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: $gl-padding-8; + + h5 { + margin: 0; + } +} + .prometheus-graph-cursor { position: absolute; background: $theme-gray-600; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 3c74c5ed2b4..785df23a355 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -117,10 +117,6 @@ .prioritized-labels { margin-bottom: 30px; - h5 { - font-size: $gl-font-size; - } - .add-priority { display: none; color: $gray-light; @@ -135,10 +131,6 @@ } .other-labels { - h5 { - font-size: $gl-font-size; - } - .remove-priority { display: none; } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 2b3cc33c8ae..3c7edb0d4bb 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -458,14 +458,10 @@ width: auto; margin-right: 0; - a { + > a, + > button { height: 60px; } - - a:hover, - a:focus { - text-decoration: none; - } } .projects-sidebar { @@ -1135,6 +1131,11 @@ .avatar { flex: 0 0 40px; } + + .ide-merge-requests-dropdown.dropdown-menu { + width: 385px; + max-height: initial; + } } .ide-sidebar-project-title { @@ -1143,11 +1144,20 @@ .sidebar-context-title { white-space: nowrap; } + + .ide-sidebar-branch-title { + min-width: 50px; + } } .ide-external-link { + position: relative; + svg { display: none; + position: absolute; + top: 2px; + right: -$gl-padding; } &:hover, @@ -1178,6 +1188,8 @@ display: flex; flex-direction: column; height: 100%; + margin-top: -$grid-size; + margin-bottom: -$grid-size; .empty-state { margin-top: auto; @@ -1194,6 +1206,17 @@ margin: 0; } } + + .build-trace, + .top-bar { + margin-left: -$gl-padding; + } + + &.build-page .top-bar { + top: 0; + font-size: 12px; + border-top-right-radius: $border-radius-default; + } } .ide-pipeline-list { @@ -1202,7 +1225,7 @@ } .ide-pipeline-header { - min-height: 50px; + min-height: 55px; padding-left: $gl-padding; padding-right: $gl-padding; @@ -1222,8 +1245,7 @@ .ci-status-icon { display: flex; justify-content: center; - height: 20px; - margin-top: -2px; + min-width: 24px; overflow: hidden; } } @@ -1253,3 +1275,56 @@ overflow: hidden; text-overflow: ellipsis; } + +.ide-job-header { + min-height: 60px; +} + +.ide-merge-requests-dropdown { + .nav-links li { + width: 50%; + padding-left: 0; + padding-right: 0; + + a { + text-align: center; + + &:not(.active) { + background-color: $gray-light; + } + } + } + + .dropdown-input { + padding-left: $gl-padding; + padding-right: $gl-padding; + + .fa { + right: 26px; + } + } + + .btn-link { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + } +} + +.ide-merge-request-current-icon { + min-width: 18px; +} + +.ide-merge-requests-empty { + height: 230px; +} + +.ide-merge-requests-dropdown-content { + min-height: 230px; + max-height: 470px; +} + +.ide-merge-request-project-path { + font-size: 12px; + line-height: 16px; + color: $gl-text-color-secondary; +} |