diff options
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'); + }); + }); +}); |