Resolve "Show CI pipeline status in Web IDE"
This commit is contained in:
parent
de12348ee8
commit
c53890548e
|
@ -21,6 +21,7 @@ const Api = {
|
|||
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
|
||||
usersPath: '/api/:version/users.json',
|
||||
commitPath: '/api/:version/projects/:id/repository/commits',
|
||||
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
|
||||
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
|
||||
createBranchPath: '/api/:version/projects/:id/repository/branches',
|
||||
pipelinesPath: '/api/:version/projects/:id/pipelines',
|
||||
|
@ -166,6 +167,19 @@ const Api = {
|
|||
});
|
||||
},
|
||||
|
||||
commitPipelines(projectId, sha) {
|
||||
const encodedProjectId = projectId
|
||||
.split('/')
|
||||
.map(fragment => encodeURIComponent(fragment))
|
||||
.join('/');
|
||||
|
||||
const url = Api.buildUrl(Api.commitPipelinesPath)
|
||||
.replace(':project_id', encodedProjectId)
|
||||
.replace(':sha', encodeURIComponent(sha));
|
||||
|
||||
return axios.get(url);
|
||||
},
|
||||
|
||||
branchSingle(id, branch) {
|
||||
const url = Api.buildUrl(Api.branchSinglePath)
|
||||
.replace(':id', encodeURIComponent(id))
|
||||
|
|
|
@ -123,8 +123,6 @@ export default {
|
|||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ide-status-bar
|
||||
:file="activeFile"
|
||||
/>
|
||||
<ide-status-bar :file="activeFile"/>
|
||||
</article>
|
||||
</template>
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
import CiIcon from '../../vue_shared/components/ci_icon.vue';
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
userAvatarImage,
|
||||
CiIcon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
|
@ -27,8 +29,16 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId', 'currentProjectId']),
|
||||
...mapGetters(['currentProject', 'lastCommit']),
|
||||
},
|
||||
watch: {
|
||||
lastCommit() {
|
||||
if (!this.isPollingInitialized) {
|
||||
this.initPipelinePolling();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.startTimer();
|
||||
},
|
||||
|
@ -36,13 +46,21 @@ export default {
|
|||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
if (this.isPollingInitialized) {
|
||||
this.stopPipelinePolling();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['pipelinePoll', 'stopPipelinePolling']),
|
||||
startTimer() {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.commitAgeUpdate();
|
||||
}, 1000);
|
||||
},
|
||||
initPipelinePolling() {
|
||||
this.pipelinePoll();
|
||||
this.isPollingInitialized = true;
|
||||
},
|
||||
commitAgeUpdate() {
|
||||
if (this.lastCommit) {
|
||||
this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
|
||||
|
@ -61,6 +79,23 @@ export default {
|
|||
class="ide-status-branch"
|
||||
v-if="lastCommit && lastCommitFormatedAge"
|
||||
>
|
||||
<span
|
||||
class="ide-status-pipeline"
|
||||
v-if="lastCommit.pipeline && lastCommit.pipeline.details"
|
||||
>
|
||||
<ci-icon
|
||||
:status="lastCommit.pipeline.details.status"
|
||||
v-tooltip
|
||||
:title="lastCommit.pipeline.details.status.text"
|
||||
/>
|
||||
Pipeline
|
||||
<a
|
||||
class="monospace"
|
||||
:href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a>
|
||||
{{ lastCommit.pipeline.details.status.text }}
|
||||
for
|
||||
</span>
|
||||
|
||||
<icon
|
||||
name="commit"
|
||||
/>
|
||||
|
|
|
@ -75,4 +75,8 @@ export default {
|
|||
},
|
||||
});
|
||||
},
|
||||
lastCommitPipelines({ getters }) {
|
||||
const commitSha = getters.lastCommit.id;
|
||||
return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import flash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import Poll from '../../../lib/utils/poll';
|
||||
|
||||
let eTagPoll;
|
||||
|
||||
export const getProjectData = (
|
||||
{ commit, state, dispatch },
|
||||
|
@ -21,7 +26,7 @@ export const getProjectData = (
|
|||
})
|
||||
.catch(() => {
|
||||
flash(
|
||||
'Error loading project data. Please try again.',
|
||||
__('Error loading project data. Please try again.'),
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
|
@ -59,7 +64,7 @@ export const getBranchData = (
|
|||
})
|
||||
.catch(() => {
|
||||
flash(
|
||||
'Error loading branch data. Please try again.',
|
||||
__('Error loading branch data. Please try again.'),
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
|
@ -73,25 +78,74 @@ export const getBranchData = (
|
|||
}
|
||||
});
|
||||
|
||||
export const refreshLastCommitData = (
|
||||
{ commit, state, dispatch },
|
||||
{ projectId, branchId } = {},
|
||||
) => service
|
||||
.getBranchData(projectId, branchId)
|
||||
.then(({ data }) => {
|
||||
commit(types.SET_BRANCH_COMMIT, {
|
||||
projectId,
|
||||
branchId,
|
||||
commit: data.commit,
|
||||
export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) =>
|
||||
service
|
||||
.getBranchData(projectId, branchId)
|
||||
.then(({ data }) => {
|
||||
commit(types.SET_BRANCH_COMMIT, {
|
||||
projectId,
|
||||
branchId,
|
||||
commit: data.commit,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
flash(__('Error loading last commit.'), 'alert', document, null, false, true);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
flash(
|
||||
'Error loading last commit.',
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
|
||||
export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => {
|
||||
if (data.pipelines && data.pipelines.length) {
|
||||
const lastCommitHash =
|
||||
state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id;
|
||||
const lastCommitPipeline = data.pipelines.find(
|
||||
pipeline => pipeline.commit.id === lastCommitHash,
|
||||
);
|
||||
commit(types.SET_LAST_COMMIT_PIPELINE, {
|
||||
projectId: state.currentProjectId,
|
||||
branchId: state.currentBranchId,
|
||||
pipeline: lastCommitPipeline || {},
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const pipelinePoll = ({ getters, dispatch }) => {
|
||||
eTagPoll = new Poll({
|
||||
resource: service,
|
||||
method: 'lastCommitPipelines',
|
||||
data: {
|
||||
getters,
|
||||
},
|
||||
successCallback: ({ data }) => dispatch('pollSuccessCallBack', { data }),
|
||||
errorCallback: () => {
|
||||
flash(
|
||||
__('Something went wrong while fetching the latest pipeline status.'),
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
eTagPoll.makeRequest();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
eTagPoll.restart();
|
||||
} else {
|
||||
eTagPoll.stop();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const stopPipelinePolling = () => {
|
||||
eTagPoll.stop();
|
||||
};
|
||||
|
||||
export const restartPipelinePolling = () => {
|
||||
eTagPoll.restart();
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ export const SET_BRANCH = 'SET_BRANCH';
|
|||
export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
|
||||
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
|
||||
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
|
||||
export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE';
|
||||
|
||||
// Tree mutation types
|
||||
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
|
||||
|
|
|
@ -14,6 +14,10 @@ export default {
|
|||
treeId: `${projectPath}/${branchName}`,
|
||||
active: true,
|
||||
workingReference: '',
|
||||
commit: {
|
||||
...branch.commit,
|
||||
pipeline: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -28,4 +32,9 @@ export default {
|
|||
commit,
|
||||
});
|
||||
},
|
||||
[types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) {
|
||||
Object.assign(state.projects[projectId].branches[branchId].commit, {
|
||||
pipeline,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -230,7 +230,7 @@ $row-hover: $blue-50;
|
|||
$row-hover-border: $blue-200;
|
||||
$progress-color: #c0392b;
|
||||
$header-height: 40px;
|
||||
$ide-statusbar-height: 27px;
|
||||
$ide-statusbar-height: 25px;
|
||||
$fixed-layout-width: 1280px;
|
||||
$limited-layout-width: 990px;
|
||||
$limited-layout-width-sm: 790px;
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
height: calc(100vh - #{$header-height});
|
||||
margin-top: 0;
|
||||
border-top: 1px solid $white-dark;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
padding-bottom: $ide-statusbar-height;
|
||||
|
||||
&.is-collapsed {
|
||||
|
@ -380,7 +379,7 @@
|
|||
|
||||
.ide-status-bar {
|
||||
border-top: 1px solid $white-dark;
|
||||
padding: $gl-bar-padding $gl-padding;
|
||||
padding: 2px $gl-padding-8 0;
|
||||
background: $white-light;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -391,12 +390,19 @@
|
|||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
|
||||
* {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
> div + div {
|
||||
padding-left: $gl-padding;
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
vertical-align: sub;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add pipeline status to the status bar of the Web IDE
|
||||
merge_request:
|
||||
author:
|
||||
type: added
|
|
@ -341,4 +341,25 @@ describe('Api', () => {
|
|||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commitPipelines', () => {
|
||||
it('fetches pipelines for a given commit', done => {
|
||||
const projectId = 'example/foobar';
|
||||
const commitSha = 'abc123def';
|
||||
const expectedUrl = `${dummyUrlRoot}/${projectId}/commit/${commitSha}/pipelines`;
|
||||
mock.onGet(expectedUrl).reply(200, [
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
]);
|
||||
|
||||
Api.commitPipelines(projectId, commitSha)
|
||||
.then(({ data }) => {
|
||||
expect(data.length).toBe(1);
|
||||
expect(data[0].name).toBe('test');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -59,3 +59,37 @@ export const jobs = [
|
|||
duration: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const fullPipelinesResponse = {
|
||||
data: {
|
||||
count: {
|
||||
all: 2,
|
||||
},
|
||||
pipelines: [
|
||||
{
|
||||
id: '51',
|
||||
commit: {
|
||||
id: 'xxxxxxxxxxxxxxxxxxxx',
|
||||
},
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_failed',
|
||||
text: 'failed',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '50',
|
||||
commit: {
|
||||
id: 'abc123def456ghi789jkl',
|
||||
},
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_passed',
|
||||
text: 'passed',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,14 +1,33 @@
|
|||
import {
|
||||
refreshLastCommitData,
|
||||
} from '~/ide/stores/actions';
|
||||
import Visibility from 'visibilityjs';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { refreshLastCommitData, pollSuccessCallBack } from '~/ide/stores/actions';
|
||||
import store from '~/ide/stores';
|
||||
import service from '~/ide/services';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { fullPipelinesResponse } from '../../mock_data';
|
||||
import { resetStore } from '../../helpers';
|
||||
import testAction from '../../../helpers/vuex_action_helper';
|
||||
|
||||
describe('IDE store project actions', () => {
|
||||
const setProjectState = () => {
|
||||
store.state.currentProjectId = 'abc/def';
|
||||
store.state.currentBranchId = 'master';
|
||||
store.state.projects['abc/def'] = {
|
||||
id: 4,
|
||||
path_with_namespace: 'abc/def',
|
||||
branches: {
|
||||
master: {
|
||||
commit: {
|
||||
id: 'abc123def456ghi789jkl',
|
||||
title: 'example',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store.state.projects.abcproject = {};
|
||||
store.state.projects['abc/def'] = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -17,18 +36,16 @@ describe('IDE store project actions', () => {
|
|||
|
||||
describe('refreshLastCommitData', () => {
|
||||
beforeEach(() => {
|
||||
store.state.currentProjectId = 'abcproject';
|
||||
store.state.currentProjectId = 'abc/def';
|
||||
store.state.currentBranchId = 'master';
|
||||
store.state.projects.abcproject = {
|
||||
store.state.projects['abc/def'] = {
|
||||
id: 4,
|
||||
branches: {
|
||||
master: {
|
||||
commit: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('calls the service', done => {
|
||||
spyOn(service, 'getBranchData').and.returnValue(
|
||||
Promise.resolve({
|
||||
data: {
|
||||
|
@ -36,14 +53,16 @@ describe('IDE store project actions', () => {
|
|||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls the service', done => {
|
||||
store
|
||||
.dispatch('refreshLastCommitData', {
|
||||
projectId: store.state.currentProjectId,
|
||||
branchId: store.state.currentBranchId,
|
||||
})
|
||||
.then(() => {
|
||||
expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
|
||||
expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'master');
|
||||
|
||||
done();
|
||||
})
|
||||
|
@ -53,16 +72,118 @@ describe('IDE store project actions', () => {
|
|||
it('commits getBranchData', done => {
|
||||
testAction(
|
||||
refreshLastCommitData,
|
||||
{},
|
||||
{},
|
||||
[{
|
||||
type: 'SET_BRANCH_COMMIT',
|
||||
payload: {
|
||||
projectId: 'abcproject',
|
||||
branchId: 'master',
|
||||
commit: { id: '123' },
|
||||
{
|
||||
projectId: store.state.currentProjectId,
|
||||
branchId: store.state.currentBranchId,
|
||||
},
|
||||
store.state,
|
||||
[
|
||||
{
|
||||
type: 'SET_BRANCH_COMMIT',
|
||||
payload: {
|
||||
projectId: 'abc/def',
|
||||
branchId: 'master',
|
||||
commit: { id: '123' },
|
||||
},
|
||||
},
|
||||
}], // mutations
|
||||
], // mutations
|
||||
[
|
||||
{
|
||||
type: 'getLastCommitPipeline',
|
||||
payload: {
|
||||
projectId: 'abc/def',
|
||||
projectIdNumber: store.state.projects['abc/def'].id,
|
||||
branchId: 'master',
|
||||
},
|
||||
},
|
||||
], // action
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipelinePoll', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
setProjectState();
|
||||
jasmine.clock().install();
|
||||
mock = new MockAdapter(axios);
|
||||
mock
|
||||
.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
|
||||
.reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
mock.restore();
|
||||
store.dispatch('stopPipelinePolling');
|
||||
});
|
||||
|
||||
it('calls service periodically', done => {
|
||||
spyOn(axios, 'get').and.callThrough();
|
||||
spyOn(Visibility, 'hidden').and.returnValue(false);
|
||||
|
||||
store
|
||||
.dispatch('pipelinePoll')
|
||||
.then(() => {
|
||||
jasmine.clock().tick(1000);
|
||||
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
expect(axios.get.calls.count()).toBe(1);
|
||||
})
|
||||
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
|
||||
.then(() => {
|
||||
jasmine.clock().tick(10000);
|
||||
expect(axios.get.calls.count()).toBe(2);
|
||||
})
|
||||
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
|
||||
.then(() => {
|
||||
jasmine.clock().tick(10000);
|
||||
expect(axios.get.calls.count()).toBe(3);
|
||||
})
|
||||
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
|
||||
.then(() => {
|
||||
jasmine.clock().tick(10000);
|
||||
expect(axios.get.calls.count()).toBe(4);
|
||||
})
|
||||
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pollSuccessCallBack', () => {
|
||||
beforeEach(() => {
|
||||
setProjectState();
|
||||
});
|
||||
|
||||
it('commits correct pipeline', done => {
|
||||
testAction(
|
||||
pollSuccessCallBack,
|
||||
fullPipelinesResponse,
|
||||
store.state,
|
||||
[
|
||||
{
|
||||
type: 'SET_LAST_COMMIT_PIPELINE',
|
||||
payload: {
|
||||
projectId: 'abc/def',
|
||||
branchId: 'master',
|
||||
pipeline: {
|
||||
id: '50',
|
||||
commit: {
|
||||
id: 'abc123def456ghi789jkl',
|
||||
},
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_passed',
|
||||
text: 'passed',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
], // mutations
|
||||
[], // action
|
||||
done,
|
||||
);
|
||||
|
|
|
@ -37,4 +37,40 @@ describe('Multi-file store branch mutations', () => {
|
|||
expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_LAST_COMMIT_PIPELINE', () => {
|
||||
it('sets the pipeline for the last commit on current project', () => {
|
||||
localState.projects = {
|
||||
Example: {
|
||||
branches: {
|
||||
master: {
|
||||
commit: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mutations.SET_LAST_COMMIT_PIPELINE(localState, {
|
||||
projectId: 'Example',
|
||||
branchId: 'master',
|
||||
pipeline: {
|
||||
id: '50',
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_passed',
|
||||
text: 'passed',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(localState.projects.Example.branches.master.commit.pipeline.id).toBe('50');
|
||||
expect(localState.projects.Example.branches.master.commit.pipeline.details.status.text).toBe(
|
||||
'passed',
|
||||
);
|
||||
expect(localState.projects.Example.branches.master.commit.pipeline.details.status.icon).toBe(
|
||||
'status_passed',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue