summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
authorLuke Bennett <lbennett@gitlab.com>2018-06-07 05:56:41 +0000
committerLuke Bennett <lbennett@gitlab.com>2018-06-07 05:56:41 +0000
commita97f4ec3615595694b11676484e9ac1ba5524a9e (patch)
treec82ad96af00a0cdd898f26a8e1da5a9bdae14819 /app/assets/javascripts
parent119b128ec8415a074a73b73a7878717779c6e0f3 (diff)
parent760b12dc6b3a927c918855e2ee85a1c0e6bddb73 (diff)
downloadgitlab-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/javascripts')
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue17
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue89
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue14
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue136
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue47
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue66
-rw-r--r--app/assets/javascripts/ide/components/jobs/item.vue40
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue3
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue4
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/dropdown.vue63
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/item.vue63
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue132
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue10
-rw-r--r--app/assets/javascripts/ide/constants.js1
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js39
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/constants.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/getters.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js18
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/state.js13
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js28
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js13
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/utils.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js6
-rw-r--r--app/assets/javascripts/importer_status.js10
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js2
-rw-r--r--app/assets/javascripts/job.js97
-rw-r--r--app/assets/javascripts/jobs/components/header.vue5
-rw-r--r--app/assets/javascripts/lib/utils/logoutput_behaviours.js46
-rw-r--r--app/assets/javascripts/lib/utils/scroll_utils.js29
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js23
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue11
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue10
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue10
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tab.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tabs.js16
43 files changed, 923 insertions, 189 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,