Commit cfe4d2f2 authored by Phil Hughes's avatar Phil Hughes

added tab component

parent 32f965b2
...@@ -6,7 +6,7 @@ import RepoTabs from './repo_tabs.vue'; ...@@ -6,7 +6,7 @@ import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue'; import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue'; import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.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; const originalStopCallback = Mousetrap.stopCallback;
...@@ -17,7 +17,7 @@ export default { ...@@ -17,7 +17,7 @@ export default {
IdeStatusBar, IdeStatusBar,
RepoEditor, RepoEditor,
FindFile, FindFile,
RightSidebar, RightPane,
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -125,7 +125,7 @@ export default { ...@@ -125,7 +125,7 @@ export default {
</div> </div>
</template> </template>
</div> </div>
<right-sidebar <right-pane
v-if="currentProjectId" v-if="currentProjectId"
/> />
</div> </div>
......
<script> <script>
import { mapActions, mapState } from 'vuex';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue'; 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 { export default {
directives: { directives: {
...@@ -9,8 +11,15 @@ export default { ...@@ -9,8 +11,15 @@ export default {
}, },
components: { components: {
Icon, Icon,
Pipelines, PipelinesList,
}, },
computed: {
...mapState(['rightPane']),
},
methods: {
...mapActions(['setRightPane']),
},
rightSidebarViews,
}; };
</script> </script>
...@@ -18,25 +27,31 @@ export default { ...@@ -18,25 +27,31 @@ export default {
<div <div
class="multi-file-commit-panel ide-right-sidebar" class="multi-file-commit-panel ide-right-sidebar"
> >
<div class="multi-file-commit-panel-inner"> <div
<pipelines /> class="multi-file-commit-panel-inner"
v-if="rightPane"
>
<keep-alive>
<component :is="rightPane" />
</keep-alive>
</div> </div>
<nav class="ide-activity-bar"> <nav class="ide-activity-bar">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li v-once> <li v-once>
<a <button
v-tooltip v-tooltip
data-container="body" data-container="body"
data-placement="left" data-placement="left"
:title="__('Pipelines')" :title="__('Pipelines')"
class="ide-sidebar-link" class="ide-sidebar-link"
href="a" type="button"
@click="setRightPane($options.rightSidebarViews.pipelines)"
> >
<icon <icon
:size="16" :size="16"
name="log" name="pipeline"
/> />
</a> </button>
</li> </li>
</ul> </ul>
</nav> </nav>
...@@ -55,6 +70,7 @@ export default { ...@@ -55,6 +70,7 @@ export default {
.ide-right-sidebar .multi-file-commit-panel-inner { .ide-right-sidebar .multi-file-commit-panel-inner {
width: 300px; width: 300px;
padding: 8px 16px;
background-color: #fff; background-color: #fff;
border-left: 1px solid #eaeaea; border-left: 1px solid #eaeaea;
} }
......
<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>
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue'; import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import JobsList from './jobs.vue';
export default { export default {
components: { components: {
LoadingIcon, LoadingIcon,
CiIcon, CiIcon,
JobsList,
}, },
computed: { computed: {
...mapGetters(['currentProject']),
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline']), ...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline']),
statusIcon() { statusIcon() {
return { return {
...@@ -34,13 +37,31 @@ export default { ...@@ -34,13 +37,31 @@ export default {
size="2" size="2"
/> />
<template v-else-if="latestPipeline"> <template v-else-if="latestPipeline">
<ci-icon <header
:status="statusIcon" class="ide-tree-header ide-pipeline-header"
/> >
#{{ latestPipeline.id }} <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> </template>
</div> </div>
</template> </template>
<style> <style>
.ide-pipeline-header .ci-status-icon {
display: flex;
}
</style> </style>
...@@ -20,3 +20,7 @@ export const viewerTypes = { ...@@ -20,3 +20,7 @@ export const viewerTypes = {
edit: 'editor', edit: 'editor',
diff: 'diff', diff: 'diff',
}; };
export const rightSidebarViews = {
pipelines: 'pipelines-list',
};
...@@ -169,6 +169,10 @@ export const burstUnusedSeal = ({ state, commit }) => { ...@@ -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/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
...@@ -5,3 +5,5 @@ export const failedJobs = state => ...@@ -5,3 +5,5 @@ export const failedJobs = state =>
(acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')), (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);
...@@ -33,6 +33,7 @@ export default { ...@@ -33,6 +33,7 @@ export default {
if (!stage) { if (!stage) {
stage = { stage = {
title: job.stage, title: job.stage,
isCollapsed: false,
jobs: [], jobs: [],
}; };
......
...@@ -65,3 +65,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW'; ...@@ -65,3 +65,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
...@@ -148,6 +148,9 @@ export default { ...@@ -148,6 +148,9 @@ export default {
unusedSeal: false, unusedSeal: false,
}); });
}, },
[types.SET_RIGHT_PANE](state, view) {
state.rightPane = state.rightPane === view ? null : view;
},
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
...fileMutations, ...fileMutations,
......
...@@ -23,4 +23,5 @@ export default () => ({ ...@@ -23,4 +23,5 @@ export default () => ({
currentActivityView: activityBarViews.edit, currentActivityView: activityBarViews.edit,
unusedSeal: true, unusedSeal: true,
fileFindVisible: false, fileFindVisible: false,
rightPane: null,
}); });
<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>
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]);
},
};
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();
});
});
});
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');
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment