Commit 058cca73 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-05-25

# Conflicts:
#	spec/javascripts/api_spec.js

[ci skip]
parents 191e25f8 4e257213
...@@ -22,6 +22,7 @@ const Api = { ...@@ -22,6 +22,7 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
pipelinesPath: '/api/:version/projects/:id/pipelines', pipelinesPath: '/api/:version/projects/:id/pipelines',
...@@ -168,6 +169,19 @@ const Api = { ...@@ -168,6 +169,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) { branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath) const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id)) .replace(':id', encodeURIComponent(id))
......
...@@ -123,8 +123,6 @@ export default { ...@@ -123,8 +123,6 @@ export default {
</template> </template>
</div> </div>
</div> </div>
<ide-status-bar <ide-status-bar :file="activeFile"/>
:file="activeFile"
/>
</article> </article>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; 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'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default { export default {
components: { components: {
icon, icon,
userAvatarImage, userAvatarImage,
CiIcon,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -27,8 +29,16 @@ export default { ...@@ -27,8 +29,16 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['currentBranchId', 'currentProjectId']),
...mapGetters(['currentProject', 'lastCommit']), ...mapGetters(['currentProject', 'lastCommit']),
}, },
watch: {
lastCommit() {
if (!this.isPollingInitialized) {
this.initPipelinePolling();
}
},
},
mounted() { mounted() {
this.startTimer(); this.startTimer();
}, },
...@@ -36,13 +46,21 @@ export default { ...@@ -36,13 +46,21 @@ export default {
if (this.intervalId) { if (this.intervalId) {
clearInterval(this.intervalId); clearInterval(this.intervalId);
} }
if (this.isPollingInitialized) {
this.stopPipelinePolling();
}
}, },
methods: { methods: {
...mapActions(['pipelinePoll', 'stopPipelinePolling']),
startTimer() { startTimer() {
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
this.commitAgeUpdate(); this.commitAgeUpdate();
}, 1000); }, 1000);
}, },
initPipelinePolling() {
this.pipelinePoll();
this.isPollingInitialized = true;
},
commitAgeUpdate() { commitAgeUpdate() {
if (this.lastCommit) { if (this.lastCommit) {
this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date); this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
...@@ -61,6 +79,23 @@ export default { ...@@ -61,6 +79,23 @@ export default {
class="ide-status-branch" class="ide-status-branch"
v-if="lastCommit && lastCommitFormatedAge" 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 <icon
name="commit" name="commit"
/> />
......
...@@ -75,4 +75,8 @@ export default { ...@@ -75,4 +75,8 @@ export default {
}, },
}); });
}, },
lastCommitPipelines({ getters }) {
const commitSha = getters.lastCommit.id;
return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha);
},
}; };
import Visibility from 'visibilityjs';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import Poll from '../../../lib/utils/poll';
let eTagPoll;
export const getProjectData = ( export const getProjectData = (
{ commit, state, dispatch }, { commit, state, dispatch },
...@@ -21,7 +26,7 @@ export const getProjectData = ( ...@@ -21,7 +26,7 @@ export const getProjectData = (
}) })
.catch(() => { .catch(() => {
flash( flash(
'Error loading project data. Please try again.', __('Error loading project data. Please try again.'),
'alert', 'alert',
document, document,
null, null,
...@@ -59,7 +64,7 @@ export const getBranchData = ( ...@@ -59,7 +64,7 @@ export const getBranchData = (
}) })
.catch(() => { .catch(() => {
flash( flash(
'Error loading branch data. Please try again.', __('Error loading branch data. Please try again.'),
'alert', 'alert',
document, document,
null, null,
...@@ -73,25 +78,74 @@ export const getBranchData = ( ...@@ -73,25 +78,74 @@ export const getBranchData = (
} }
}); });
export const refreshLastCommitData = ( export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) =>
{ commit, state, dispatch }, service
{ projectId, branchId } = {}, .getBranchData(projectId, branchId)
) => service .then(({ data }) => {
.getBranchData(projectId, branchId) commit(types.SET_BRANCH_COMMIT, {
.then(({ data }) => { projectId,
commit(types.SET_BRANCH_COMMIT, { branchId,
projectId, commit: data.commit,
branchId, });
commit: data.commit, })
.catch(() => {
flash(__('Error loading last commit.'), 'alert', document, null, false, true);
}); });
})
.catch(() => { export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => {
flash( if (data.pipelines && data.pipelines.length) {
'Error loading last commit.', const lastCommitHash =
'alert', state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id;
document, const lastCommitPipeline = data.pipelines.find(
null, pipeline => pipeline.commit.id === lastCommitHash,
false,
true,
); );
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'; ...@@ -23,6 +23,7 @@ export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT'; export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE';
// Tree mutation types // Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
......
...@@ -14,6 +14,10 @@ export default { ...@@ -14,6 +14,10 @@ export default {
treeId: `${projectPath}/${branchName}`, treeId: `${projectPath}/${branchName}`,
active: true, active: true,
workingReference: '', workingReference: '',
commit: {
...branch.commit,
pipeline: {},
},
}, },
}, },
}); });
...@@ -28,4 +32,9 @@ export default { ...@@ -28,4 +32,9 @@ export default {
commit, commit,
}); });
}, },
[types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) {
Object.assign(state.projects[projectId].branches[branchId].commit, {
pipeline,
});
},
}; };
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
</h4> </h4>
<p> <p>
{{ s__(`Pipelines|Continous Integration can help {{ s__(`Pipelines|Continuous Integration can help
catch bugs by running your tests automatically, catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver while Continuous Deployment can help you deliver
code to your product environment.`) }} code to your product environment.`) }}
......
...@@ -231,7 +231,7 @@ $row-hover: $blue-50; ...@@ -231,7 +231,7 @@ $row-hover: $blue-50;
$row-hover-border: $blue-200; $row-hover-border: $blue-200;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 40px; $header-height: 40px;
$ide-statusbar-height: 27px; $ide-statusbar-height: 25px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$limited-layout-width: 990px; $limited-layout-width: 990px;
$limited-layout-width-sm: 790px; $limited-layout-width-sm: 790px;
......
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$header-height});
margin-top: 0; margin-top: 0;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
padding-bottom: $ide-statusbar-height; padding-bottom: $ide-statusbar-height;
&.is-collapsed { &.is-collapsed {
...@@ -380,7 +379,7 @@ ...@@ -380,7 +379,7 @@
.ide-status-bar { .ide-status-bar {
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
padding: $gl-bar-padding $gl-padding; padding: 2px $gl-padding-8 0;
background: $white-light; background: $white-light;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
...@@ -391,12 +390,19 @@ ...@@ -391,12 +390,19 @@
left: 0; left: 0;
width: 100%; width: 100%;
font-size: 12px;
line-height: 22px;
* {
font-size: inherit;
}
> div + div { > div + div {
padding-left: $gl-padding; padding-left: $gl-padding;
} }
svg { svg {
vertical-align: middle; vertical-align: sub;
} }
} }
......
---
title: Add pipeline status to the status bar of the Web IDE
merge_request:
author:
type: added
...@@ -342,6 +342,7 @@ describe('Api', () => { ...@@ -342,6 +342,7 @@ describe('Api', () => {
}); });
}); });
<<<<<<< HEAD
describe('ldap_groups', () => { describe('ldap_groups', () => {
it('calls callback on completion', done => { it('calls callback on completion', done => {
const query = 'query'; const query = 'query';
...@@ -349,15 +350,29 @@ describe('Api', () => { ...@@ -349,15 +350,29 @@ describe('Api', () => {
const callback = jasmine.createSpy(); const callback = jasmine.createSpy();
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/ldap/${provider}/groups.json`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/ldap/${provider}/groups.json`;
=======
describe('commitPipelines', () => {
it('fetches pipelines for a given commit', done => {
const projectId = 'example/foobar';
const commitSha = 'abc123def';
const expectedUrl = `${dummyUrlRoot}/${projectId}/commit/${commitSha}/pipelines`;
>>>>>>> upstream/master
mock.onGet(expectedUrl).reply(200, [ mock.onGet(expectedUrl).reply(200, [
{ {
name: 'test', name: 'test',
}, },
]); ]);
<<<<<<< HEAD
Api.ldap_groups(query, provider, callback) Api.ldap_groups(query, provider, callback)
.then(response => { .then(response => {
expect(callback).toHaveBeenCalledWith(response); expect(callback).toHaveBeenCalledWith(response);
=======
Api.commitPipelines(projectId, commitSha)
.then(({ data }) => {
expect(data.length).toBe(1);
expect(data[0].name).toBe('test');
>>>>>>> upstream/master
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
......
...@@ -59,3 +59,37 @@ export const jobs = [ ...@@ -59,3 +59,37 @@ export const jobs = [
duration: 1, 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',
},
},
},
],
},
};
import { import Visibility from 'visibilityjs';
refreshLastCommitData, import MockAdapter from 'axios-mock-adapter';
} from '~/ide/stores/actions'; import { refreshLastCommitData, pollSuccessCallBack } from '~/ide/stores/actions';
import store from '~/ide/stores'; import store from '~/ide/stores';
import service from '~/ide/services'; import service from '~/ide/services';
import axios from '~/lib/utils/axios_utils';
import { fullPipelinesResponse } from '../../mock_data';
import { resetStore } from '../../helpers'; import { resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper'; import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store project actions', () => { 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(() => { beforeEach(() => {
store.state.projects.abcproject = {}; store.state.projects['abc/def'] = {};
}); });
afterEach(() => { afterEach(() => {
...@@ -17,18 +36,16 @@ describe('IDE store project actions', () => { ...@@ -17,18 +36,16 @@ describe('IDE store project actions', () => {
describe('refreshLastCommitData', () => { describe('refreshLastCommitData', () => {
beforeEach(() => { beforeEach(() => {
store.state.currentProjectId = 'abcproject'; store.state.currentProjectId = 'abc/def';
store.state.currentBranchId = 'master'; store.state.currentBranchId = 'master';
store.state.projects.abcproject = { store.state.projects['abc/def'] = {
id: 4,
branches: { branches: {
master: { master: {
commit: null, commit: null,
}, },
}, },
}; };
});
it('calls the service', done => {
spyOn(service, 'getBranchData').and.returnValue( spyOn(service, 'getBranchData').and.returnValue(
Promise.resolve({ Promise.resolve({
data: { data: {
...@@ -36,14 +53,16 @@ describe('IDE store project actions', () => { ...@@ -36,14 +53,16 @@ describe('IDE store project actions', () => {
}, },
}), }),
); );
});
it('calls the service', done => {
store store
.dispatch('refreshLastCommitData', { .dispatch('refreshLastCommitData', {
projectId: store.state.currentProjectId, projectId: store.state.currentProjectId,
branchId: store.state.currentBranchId, branchId: store.state.currentBranchId,
}) })
.then(() => { .then(() => {
expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master'); expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'master');
done(); done();
}) })
...@@ -53,16 +72,118 @@ describe('IDE store project actions', () => { ...@@ -53,16 +72,118 @@ describe('IDE store project actions', () => {
it('commits getBranchData', done => { it('commits getBranchData', done => {
testAction( testAction(
refreshLastCommitData, refreshLastCommitData,
{}, {
{}, projectId: store.state.currentProjectId,
[{ branchId: store.state.currentBranchId,
type: 'SET_BRANCH_COMMIT', },
payload: { store.state,
projectId: 'abcproject', [
branchId: 'master', {
commit: { id: '123' }, type: 'SET_BRANCH_COMMIT',
payload: {
projectId: 'abc/def',
branchId: 'master',
commit: { id: '123' },
},
},
], // 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 ], // mutations
[], // action [], // action
done, done,
); );
......
...@@ -37,4 +37,40 @@ describe('Multi-file store branch mutations', () => { ...@@ -37,4 +37,40 @@ describe('Multi-file store branch mutations', () => {
expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit'); 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',
);
});
});
}); });
...@@ -29,7 +29,7 @@ describe('Pipelines Empty State', () => { ...@@ -29,7 +29,7 @@ describe('Pipelines Empty State', () => {
expect( expect(
component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '), component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
).toContain('Continous Integration can help catch bugs by running your tests automatically,'); ).toContain('Continuous Integration can help catch bugs by running your tests automatically,');
expect( expect(
component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '), component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
......
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