summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/api.js6
-rw-r--r--app/assets/javascripts/ide/components/ide.vue6
-rw-r--r--app/assets/javascripts/ide/components/jobs/item.vue58
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue38
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue127
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue78
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue104
-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/actions.js45
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js14
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js69
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js5
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tab.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tabs.js64
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss7
21 files changed, 656 insertions, 49 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index ce1069276ab..dbdc4de7986 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -25,7 +25,7 @@ const Api = {
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
pipelinesPath: '/api/:version/projects/:id/pipelines',
- pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs',
+ pipelineJobsPath: '/:project_path/pipelines/:id/builds.json',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -246,8 +246,8 @@ const Api = {
pipelineJobs(projectPath, pipelineId, params = {}) {
const url = Api.buildUrl(this.pipelineJobsPath)
- .replace(':id', encodeURIComponent(projectPath))
- .replace(':pipeline_id', pipelineId);
+ .replace(':project_path', projectPath)
+ .replace(':id', pipelineId);
return axios.get(url, { params });
},
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 0aaf5a112cb..c8bbdcced1a 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -6,6 +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 RightPane from './panes/right.vue';
const originalStopCallback = Mousetrap.stopCallback;
@@ -16,6 +17,7 @@ export default {
IdeStatusBar,
RepoEditor,
FindFile,
+ RightPane,
},
computed: {
...mapState([
@@ -25,6 +27,7 @@ export default {
'currentMergeRequestId',
'fileFindVisible',
'emptyStateSvgPath',
+ 'currentProjectId',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
@@ -122,6 +125,9 @@ export default {
</div>
</template>
</div>
+ <right-pane
+ v-if="currentProjectId"
+ />
</div>
<ide-status-bar :file="activeFile"/>
</article>
diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue
new file mode 100644
index 00000000000..a6ca629a358
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/item.vue
@@ -0,0 +1,58 @@
+<script>
+import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+
+export default {
+ components: {
+ CiIcon,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ jobId() {
+ return `#${this.job.id}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="ide-job-item">
+ <ci-icon
+ :status="job.status"
+ :borderless="true"
+ :size="24"
+ />
+ <span class="prepend-left-8">
+ {{ job.name }}
+ <a
+ :href="job.build_path"
+ target="_blank"
+ v-text="jobId"
+ >
+ </a>
+ </span>
+ </div>
+</template>
+
+<style scoped>
+.ide-job-item {
+ display: flex;
+ padding: 16px;
+}
+
+.ide-job-item:not(:last-child) {
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.ide-job-item .ci-status-icon {
+ display: flex;
+ justify-content: center;
+ height: 20px;
+ margin-top: -2px;
+ overflow: hidden;
+}
+</style>
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
new file mode 100644
index 00000000000..fd6bfdf86d0
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -0,0 +1,38 @@
+<script>
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Stage from './stage.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ Stage,
+ },
+ props: {
+ stages: {
+ type: Array,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <loading-icon
+ v-if="loading && !stages.length"
+ class="prepend-top-default"
+ size="2"
+ />
+ <template v-else>
+ <stage
+ v-for="stage in stages"
+ :key="stage.id"
+ :stage="stage"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
new file mode 100644
index 00000000000..7f1a0ed1218
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -0,0 +1,127 @@
+<script>
+import { mapActions } from 'vuex';
+import tooltip from '../../../vue_shared/directives/tooltip';
+import Icon from '../../../vue_shared/components/icon.vue';
+import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Item from './item.vue';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ CiIcon,
+ LoadingIcon,
+ Item,
+ },
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showTooltip: false,
+ };
+ },
+ computed: {
+ collapseIcon() {
+ return this.stage.isCollapsed ? 'angle-left' : 'angle-down';
+ },
+ showLoadingIcon() {
+ return this.stage.isLoading && !this.stage.jobs.length;
+ },
+ jobsCount() {
+ return this.stage.jobs.length;
+ },
+ },
+ created() {
+ this.fetchJobs(this.stage);
+ },
+ mounted() {
+ const { stageTitle } = this.$refs;
+
+ this.showTooltip = stageTitle.scrollWidth > stageTitle.offsetWidth;
+ },
+ methods: {
+ ...mapActions('pipelines', ['fetchJobs']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="panel panel-default prepend-top-default"
+ >
+ <div
+ class="panel-heading"
+ @click="() => stage.isCollapsed = !stage.isCollapsed"
+ >
+ <ci-icon
+ :status="stage.status"
+ :size="24"
+ />
+ <strong
+ v-tooltip="showTooltip"
+ :title="showTooltip ? stage.name : null"
+ data-container="body"
+ class="prepend-left-8 ide-stage-title"
+ ref="stageTitle"
+ >
+ {{ stage.name }}
+ </strong>
+ <div class="append-right-8">
+ <span class="badge">
+ {{ jobsCount }}
+ </span>
+ </div>
+ <icon
+ :name="collapseIcon"
+ css-classes="pull-right"
+ />
+ </div>
+ <div
+ class="panel-body"
+ v-show="!stage.isCollapsed"
+ >
+ <loading-icon
+ v-if="showLoadingIcon"
+ />
+ <template v-else>
+ <item
+ v-for="job in stage.jobs"
+ :key="job.id"
+ :job="job"
+ />
+ </template>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+.panel-heading {
+ display: flex;
+ cursor: pointer;
+}
+.panel-heading .ci-status-icon {
+ display: flex;
+ align-items: center;
+}
+
+.panel-heading .pull-right {
+ margin: auto 0 auto auto;
+}
+
+.panel-body {
+ padding: 0;
+}
+
+.ide-stage-title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+</style>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
new file mode 100644
index 00000000000..485412867f6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -0,0 +1,78 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+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';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ PipelinesList,
+ },
+ computed: {
+ ...mapState(['rightPane']),
+ },
+ methods: {
+ ...mapActions(['setRightPane']),
+ },
+ rightSidebarViews,
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel ide-right-sidebar"
+ >
+ <div
+ class="multi-file-commit-panel-inner"
+ v-if="rightPane"
+ >
+ <component :is="rightPane" />
+ </div>
+ <nav class="ide-activity-bar">
+ <ul class="list-unstyled">
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="__('Pipelines')"
+ class="ide-sidebar-link is-right"
+ :class="{
+ active: rightPane === $options.rightSidebarViews.pipelines
+ }"
+ type="button"
+ @click="setRightPane($options.rightSidebarViews.pipelines)"
+ >
+ <icon
+ :size="16"
+ name="pipeline"
+ />
+ </button>
+ </li>
+ </ul>
+ </nav>
+ </div>
+</template>
+
+<style>
+.ide-right-sidebar {
+ width: auto;
+ min-width: 60px;
+}
+
+.ide-right-sidebar .ide-activity-bar {
+ border-left: 1px solid #eaeaea;
+}
+
+.ide-right-sidebar .multi-file-commit-panel-inner {
+ width: 350px;
+ padding: 8px 16px;
+ background-color: #fff;
+ border-left: 1px solid #eaeaea;
+}
+</style>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
new file mode 100644
index 00000000000..8eed902d4e2
--- /dev/null
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -0,0 +1,104 @@
+<script>
+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 Tabs from '../../../vue_shared/components/tabs/tabs';
+import Tab from '../../../vue_shared/components/tabs/tab.vue';
+import JobsList from '../jobs/list.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ CiIcon,
+ Tabs,
+ Tab,
+ JobsList,
+ },
+ computed: {
+ ...mapGetters(['currentProject']),
+ ...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages']),
+ ...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
+ statusIcon() {
+ return {
+ group: this.latestPipeline.status,
+ icon: `status_${this.latestPipeline.status}`,
+ };
+ },
+ },
+ created() {
+ return this.fetchLatestPipeline().then(() => this.fetchStages());
+ },
+ methods: {
+ ...mapActions('pipelines', ['fetchLatestPipeline', 'fetchStages']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <loading-icon
+ v-if="isLoadingPipeline && !latestPipeline"
+ class="prepend-top-default"
+ size="2"
+ />
+ <template v-else-if="latestPipeline">
+ <header
+ class="ide-tree-header ide-pipeline-header"
+ >
+ <ci-icon
+ :status="statusIcon"
+ :size="24"
+ />
+ <span class="prepend-left-8">
+ <strong>
+ Pipeline
+ </strong>
+ <a
+ :href="currentProject.web_url + '/pipelines/' + latestPipeline.id"
+ target="_blank"
+ >
+ #{{ latestPipeline.id }}
+ </a>
+ </span>
+ </header>
+ <tabs>
+ <tab active>
+ <template slot="title">
+ Jobs
+ <span
+ v-if="!isLoadingJobs || jobsCount"
+ class="badge"
+ >
+ {{ jobsCount }}
+ </span>
+ </template>
+ <jobs-list
+ :loading="isLoadingJobs"
+ :stages="stages"
+ />
+ </tab>
+ <tab>
+ <template slot="title">
+ Failed Jobs
+ <span
+ v-if="!isLoadingJobs || failedJobsCount"
+ class="badge"
+ >
+ {{ failedJobsCount }}
+ </span>
+ </template>
+ <jobs-list
+ :loading="isLoadingJobs"
+ :stages="failedStages"
+ />
+ </tab>
+ </tabs>
+ </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/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 07f7b201f2e..b5d78b381ea 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -1,3 +1,4 @@
+import axios from 'axios';
import { __ } from '../../../../locale';
import Api from '../../../../api';
import flash from '../../../../flash';
@@ -21,29 +22,39 @@ export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => {
.catch(() => dispatch('receiveLatestPipelineError'));
};
-export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS);
-export const receiveJobsError = ({ commit }) => {
- flash(__('There was an error loading jobs'));
- commit(types.RECEIVE_JOBS_ERROR);
+export const requestStages = ({ commit }) => commit(types.REQUEST_STAGES);
+export const receiveStagesError = ({ commit }) => {
+ flash(__('There was an error loading job stages'));
+ commit(types.RECEIVE_STAGES_ERROR);
};
-export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data);
+export const receiveStagesSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_STAGES_SUCCESS, data);
+
+export const fetchStages = ({ dispatch, state, rootState }) => {
+ dispatch('requestStages');
-export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => {
- dispatch('requestJobs');
+ Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id)
+ .then(({ data }) => dispatch('receiveStagesSuccess', data))
+ .catch(() => dispatch('receiveStagesError'));
+};
- Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, {
- page,
- })
- .then(({ data, headers }) => {
- const nextPage = headers && headers['x-next-page'];
+export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
+export const receiveJobsError = ({ commit }, id) => {
+ flash(__('There was an error loading jobs'));
+ commit(types.RECEIVE_JOBS_ERROR, id);
+};
+export const receiveJobsSuccess = ({ commit }, { id, data }) =>
+ commit(types.RECEIVE_JOBS_SUCCESS, { id, data });
- dispatch('receiveJobsSuccess', data);
+export const fetchJobs = ({ dispatch }, stage) => {
+ dispatch('requestJobs', stage.id);
- if (nextPage) {
- dispatch('fetchJobs', nextPage);
- }
+ axios
+ .get(stage.dropdown_path)
+ .then(({ data }) => {
+ dispatch('receiveJobsSuccess', { id: stage.id, data });
})
- .catch(() => dispatch('receiveJobsError'));
+ .catch(() => dispatch('receiveJobsError', stage.id));
};
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
index d6c91f5b64d..95bed0f9050 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -1,7 +1,15 @@
export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
-export const failedJobs = state =>
+export const failedStages = state =>
+ state.stages.filter(stage => stage.status.text === 'failed').map(stage => ({
+ ...stage,
+ jobs: stage.jobs.filter(job => job.status.text === 'failed'),
+ }));
+
+export const failedJobsCount = state =>
state.stages.reduce(
- (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')),
- [],
+ (acc, stage) => acc + stage.jobs.filter(j => j.status.text === 'failed').length,
+ 0,
);
+
+export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);
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 6b5701670a6..0911b8ee6fb 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
@@ -2,6 +2,10 @@ export const REQUEST_LATEST_PIPELINE = 'REQUEST_LATEST_PIPELINE';
export const RECEIVE_LASTEST_PIPELINE_ERROR = 'RECEIVE_LASTEST_PIPELINE_ERROR';
export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCESS';
+export const REQUEST_STAGES = 'REQUEST_STAGES';
+export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR';
+export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS';
+
export const REQUEST_JOBS = 'REQUEST_JOBS';
export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index 2b16e57b386..0713aa4a3f5 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -18,36 +18,55 @@ export default {
};
}
},
- [types.REQUEST_JOBS](state) {
+ [types.REQUEST_STAGES](state) {
state.isLoadingJobs = true;
},
- [types.RECEIVE_JOBS_ERROR](state) {
+ [types.RECEIVE_STAGES_ERROR](state) {
state.isLoadingJobs = false;
},
- [types.RECEIVE_JOBS_SUCCESS](state, jobs) {
+ [types.RECEIVE_STAGES_SUCCESS](state, stages) {
state.isLoadingJobs = false;
- state.stages = jobs.reduce((acc, job) => {
- let stage = acc.find(s => s.title === job.stage);
-
- if (!stage) {
- stage = {
- title: job.stage,
- jobs: [],
- };
-
- acc.push(stage);
- }
-
- stage.jobs = stage.jobs.concat({
- id: job.id,
- name: job.name,
- status: job.status,
- stage: job.stage,
- duration: job.duration,
- });
-
- return acc;
- }, state.stages);
+ state.stages = stages.map((stage, i) => {
+ const foundStage = state.stages.find(s => s.id === i);
+ return {
+ ...stage,
+ id: i,
+ isCollapsed: foundStage ? foundStage.isCollapsed : false,
+ isLoading: foundStage ? foundStage.isLoading : false,
+ jobs: foundStage ? foundStage.jobs : [],
+ };
+ });
+ },
+ [types.REQUEST_JOBS](state, id) {
+ state.stages = state.stages.reduce(
+ (acc, stage) =>
+ acc.concat({
+ ...stage,
+ isLoading: id === stage.id ? true : stage.isLoading,
+ }),
+ [],
+ );
+ },
+ [types.RECEIVE_JOBS_ERROR](state, id) {
+ state.stages = state.stages.reduce(
+ (acc, stage) =>
+ acc.concat({
+ ...stage,
+ isLoading: id === stage.id ? false : stage.isLoading,
+ }),
+ [],
+ );
+ },
+ [types.RECEIVE_JOBS_SUCCESS](state, { id, data }) {
+ state.stages = state.stages.reduce(
+ (acc, stage) =>
+ acc.concat({
+ ...stage,
+ isLoading: id === stage.id ? false : stage.isLoading,
+ jobs: id === stage.id ? data.latest_statuses : stage.jobs,
+ }),
+ [],
+ );
},
};
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 0a3f8d031c4..4e8340b3e9e 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -66,3 +66,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..633c93ed0a8 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -148,6 +148,11 @@ export default {
unusedSeal: false,
});
},
+ [types.SET_RIGHT_PANE](state, view) {
+ Object.assign(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/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index fcab8f571dd..03f924ba99d 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -22,6 +22,8 @@ import Icon from '../../vue_shared/components/icon.vue';
* - Jobs show view header
* - Jobs show view sidebar
*/
+const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
+
export default {
components: {
Icon,
@@ -31,17 +33,36 @@ export default {
type: Object,
required: true,
},
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ validator(value) {
+ return validSizes.includes(value);
+ },
+ },
+ borderless: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
+ icon() {
+ return this.borderless ? `${this.status.icon}_borderless` : this.status.icon;
+ },
},
};
</script>
<template>
<span :class="cssClass">
- <icon :name="status.icon" />
+ <icon
+ :name="icon"
+ :size="size"
+ />
</span>
</template>
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..ac804c0ab3d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js
@@ -0,0 +1,64 @@
+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',
+ {
+ attrs: {
+ 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/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 0bbd6eb27c1..8df2906f906 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -189,6 +189,10 @@
&.active {
color: $color-700;
box-shadow: inset 3px 0 $color-700;
+
+ &.is-right {
+ box-shadow: inset -3px 0 $color-700;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 35985d3c8f3..4e8b5d3b1b7 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -909,6 +909,13 @@
width: 1px;
background: $white-light;
}
+
+ &.is-right {
+ &::after {
+ right: auto;
+ left: -1px;
+ }
+ }
}
}