summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/ide/components/ide.vue6
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue (renamed from app/assets/javascripts/ide/components/right_sidebar/index.vue)32
-rw-r--r--app/assets/javascripts/ide/components/pipelines/jobs.vue40
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue (renamed from app/assets/javascripts/ide/components/right_sidebar/pipelines.vue)31
-rw-r--r--app/assets/javascripts/ide/constants.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js3
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tab.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tabs.js62
-rw-r--r--spec/javascripts/vue_shared/components/tabs/tab_spec.js32
-rw-r--r--spec/javascripts/vue_shared/components/tabs/tabs_spec.js68
15 files changed, 314 insertions, 16 deletions
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index d61ed36a757..318e7aa5716 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -6,7 +6,7 @@ import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
-import RightSidebar from './right_sidebar/index.vue';
+import RightPane from './panes/right.vue';
const originalStopCallback = Mousetrap.stopCallback;
@@ -17,7 +17,7 @@ export default {
IdeStatusBar,
RepoEditor,
FindFile,
- RightSidebar,
+ RightPane,
},
computed: {
...mapState([
@@ -125,7 +125,7 @@ export default {
</div>
</template>
</div>
- <right-sidebar
+ <right-pane
v-if="currentProjectId"
/>
</div>
diff --git a/app/assets/javascripts/ide/components/right_sidebar/index.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 2417e3976aa..7ac79347225 100644
--- a/app/assets/javascripts/ide/components/right_sidebar/index.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -1,7 +1,9 @@
<script>
+import { mapActions, mapState } from 'vuex';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
-import Pipelines from './pipelines.vue';
+import { rightSidebarViews } from '../../constants';
+import PipelinesList from '../pipelines/list.vue';
export default {
directives: {
@@ -9,8 +11,15 @@ export default {
},
components: {
Icon,
- Pipelines,
+ PipelinesList,
},
+ computed: {
+ ...mapState(['rightPane']),
+ },
+ methods: {
+ ...mapActions(['setRightPane']),
+ },
+ rightSidebarViews,
};
</script>
@@ -18,25 +27,31 @@ export default {
<div
class="multi-file-commit-panel ide-right-sidebar"
>
- <div class="multi-file-commit-panel-inner">
- <pipelines />
+ <div
+ class="multi-file-commit-panel-inner"
+ v-if="rightPane"
+ >
+ <keep-alive>
+ <component :is="rightPane" />
+ </keep-alive>
</div>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li v-once>
- <a
+ <button
v-tooltip
data-container="body"
data-placement="left"
:title="__('Pipelines')"
class="ide-sidebar-link"
- href="a"
+ type="button"
+ @click="setRightPane($options.rightSidebarViews.pipelines)"
>
<icon
:size="16"
- name="log"
+ name="pipeline"
/>
- </a>
+ </button>
</li>
</ul>
</nav>
@@ -55,6 +70,7 @@ export default {
.ide-right-sidebar .multi-file-commit-panel-inner {
width: 300px;
+ padding: 8px 16px;
background-color: #fff;
border-left: 1px solid #eaeaea;
}
diff --git a/app/assets/javascripts/ide/components/pipelines/jobs.vue b/app/assets/javascripts/ide/components/pipelines/jobs.vue
new file mode 100644
index 00000000000..d69945b617c
--- /dev/null
+++ b/app/assets/javascripts/ide/components/pipelines/jobs.vue
@@ -0,0 +1,40 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import Tabs from '../../../vue_shared/components/tabs/tabs';
+import Tab from '../../../vue_shared/components/tabs/tab.vue';
+
+export default {
+ components: {
+ Tabs,
+ Tab,
+ },
+ computed: {
+ ...mapGetters('pipelines', ['jobsCount', 'failedJobs']),
+ },
+ mounted() {
+ this.fetchJobs();
+ },
+ methods: {
+ ...mapActions('pipelines', ['fetchJobs']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <tabs>
+ <tab active>
+ <template slot="title">
+ Jobs <span class="badge">{{ jobsCount }}</span>
+ </template>
+ List all jobs here
+ </tab>
+ <tab>
+ <template slot="title">
+ Failed Jobs <span class="badge">{{ failedJobs.length }}</span>
+ </template>
+ List all failed jobs here
+ </tab>
+ </tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/right_sidebar/pipelines.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 0ff78242e6a..e76ea0b50af 100644
--- a/app/assets/javascripts/ide/components/right_sidebar/pipelines.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,14 +1,17 @@
<script>
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+import JobsList from './jobs.vue';
export default {
components: {
LoadingIcon,
CiIcon,
+ JobsList,
},
computed: {
+ ...mapGetters(['currentProject']),
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline']),
statusIcon() {
return {
@@ -34,13 +37,31 @@ export default {
size="2"
/>
<template v-else-if="latestPipeline">
- <ci-icon
- :status="statusIcon"
- />
- #{{ latestPipeline.id }}
+ <header
+ class="ide-tree-header ide-pipeline-header"
+ >
+ <ci-icon
+ :status="statusIcon"
+ />
+ <span class="prepend-left-8">
+ <strong>
+ Pipeline
+ </strong>
+ <a
+ :href="currentProject.web_url + '/pipelines/' + latestPipeline.id"
+ target="_blank"
+ >
+ #{{ latestPipeline.id }}
+ </a>
+ </span>
+ </header>
+ <jobs-list />
</template>
</div>
</template>
<style>
+.ide-pipeline-header .ci-status-icon {
+ display: flex;
+}
</style>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 83fe22f40a4..33cd20caf52 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -20,3 +20,7 @@ export const viewerTypes = {
edit: 'editor',
diff: 'diff',
};
+
+export const rightSidebarViews = {
+ pipelines: 'pipelines-list',
+};
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 1a98b42761e..2b55a09f2a6 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -169,6 +169,10 @@ export const burstUnusedSeal = ({ state, commit }) => {
}
};
+export const setRightPane = ({ commit }, view) => {
+ commit(types.SET_RIGHT_PANE, view);
+};
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
index d6c91f5b64d..e1f55bcd933 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -5,3 +5,5 @@ export const failedJobs = state =>
(acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')),
[],
);
+
+export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index 2b16e57b386..d28d9ca776d 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -33,6 +33,7 @@ export default {
if (!stage) {
stage = {
title: job.stage,
+ isCollapsed: false,
jobs: [],
};
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index a3fb3232f1d..27e91573510 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -65,3 +65,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
+
+export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index a257e2ef025..375b4d8233d 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -148,6 +148,9 @@ export default {
unusedSeal: false,
});
},
+ [types.SET_RIGHT_PANE](state, view) {
+ state.rightPane = state.rightPane === view ? null : view;
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index e7411f16a4f..ef8d678dd43 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -23,4 +23,5 @@ export default () => ({
currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
+ rightPane: null,
});
diff --git a/app/assets/javascripts/vue_shared/components/tabs/tab.vue b/app/assets/javascripts/vue_shared/components/tabs/tab.vue
new file mode 100644
index 00000000000..2a35d6bc151
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tabs/tab.vue
@@ -0,0 +1,42 @@
+<script>
+export default {
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ active: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ // props can't be updated, so we map it to data where we can
+ localActive: this.active,
+ };
+ },
+ watch: {
+ active() {
+ this.localActive = this.active;
+ },
+ },
+ created() {
+ this.isTab = true;
+ },
+};
+</script>
+
+<template>
+ <div
+ class="tab-pane"
+ :class="{
+ active: localActive
+ }"
+ role="tabpanel"
+ >
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js
new file mode 100644
index 00000000000..3dff37b1c84
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js
@@ -0,0 +1,62 @@
+export default {
+ data() {
+ return {
+ currentIndex: 0,
+ tabs: [],
+ };
+ },
+ mounted() {
+ this.updateTabs();
+ },
+ methods: {
+ updateTabs() {
+ this.tabs = this.$children.filter(child => child.isTab);
+ this.currentIndex = this.tabs.findIndex(tab => tab.localActive);
+ },
+ setTab(index) {
+ this.tabs[this.currentIndex].localActive = false;
+ this.tabs[index].localActive = true;
+
+ this.currentIndex = index;
+ },
+ },
+ render(h) {
+ const navItems = this.tabs.map((tab, i) =>
+ h(
+ 'li',
+ {
+ key: i,
+ class: tab.localActive ? 'active' : null,
+ },
+ [
+ h(
+ 'a',
+ {
+ href: '#',
+ on: {
+ click: () => this.setTab(i),
+ },
+ },
+ tab.$slots.title || tab.title,
+ ),
+ ],
+ ),
+ );
+ const nav = h(
+ 'ul',
+ {
+ class: 'nav-links tab-links',
+ },
+ [navItems],
+ );
+ const content = h(
+ 'div',
+ {
+ class: ['tab-content'],
+ },
+ [this.$slots.default],
+ );
+
+ return h('div', {}, [[nav], content]);
+ },
+};
diff --git a/spec/javascripts/vue_shared/components/tabs/tab_spec.js b/spec/javascripts/vue_shared/components/tabs/tab_spec.js
new file mode 100644
index 00000000000..8437fe37738
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/tabs/tab_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tab component', () => {
+ const Component = Vue.extend(Tab);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component);
+ });
+
+ it('sets localActive to equal active', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.localActive).toBe(true);
+
+ done();
+ });
+ });
+
+ it('sets active class', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).toContain('active');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js b/spec/javascripts/vue_shared/components/tabs/tabs_spec.js
new file mode 100644
index 00000000000..07752329965
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/tabs/tabs_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import Tabs from '~/vue_shared/components/tabs/tabs';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tabs component', () => {
+ let vm;
+
+ beforeEach(done => {
+ vm = new Vue({
+ components: {
+ Tabs,
+ Tab,
+ },
+ template: `
+ <div>
+ <tabs>
+ <tab title="Testing" active>
+ First tab
+ </tab>
+ <tab>
+ <template slot="title">Test slot</template>
+ Second tab
+ </tab>
+ </tabs>
+ </div>
+ `,
+ }).$mount();
+
+ setTimeout(done);
+ });
+
+ describe('tab links', () => {
+ it('renders links for tabs', () => {
+ expect(vm.$el.querySelectorAll('a').length).toBe(2);
+ });
+
+ it('renders link titles from props', () => {
+ expect(vm.$el.querySelector('a').textContent).toContain('Testing');
+ });
+
+ it('renders link titles from slot', () => {
+ expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot');
+ });
+
+ it('renders active class', () => {
+ expect(vm.$el.querySelector('li').classList).toContain('active');
+ });
+
+ it('updates active class on click', done => {
+ vm.$el.querySelectorAll('a')[1].click();
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('li').classList).not.toContain('active');
+ expect(vm.$el.querySelectorAll('li')[1].classList).toContain('active');
+
+ done();
+ });
+ });
+ });
+
+ describe('content', () => {
+ it('renders content panes', () => {
+ expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2);
+ expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab');
+ expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab');
+ });
+ });
+});