Commit 5b2b42a4 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '5983-realtime-pipelines-table' into 'master'

Adds polling function to pipelines table

Closes #5983

See merge request !10210
parents b9027887 ae62fc05
...@@ -8,10 +8,8 @@ Vue.use(VueResource); ...@@ -8,10 +8,8 @@ Vue.use(VueResource);
/** /**
* Commits View > Pipelines Tab > Pipelines Table. * Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table.
* *
* Renders Pipelines table in pipelines tab in the commits show view. * Renders Pipelines table in pipelines tab in the commits show view.
* Renders Pipelines table in pipelines tab in the merge request show view.
*/ */
$(() => { $(() => {
...@@ -20,13 +18,14 @@ $(() => { ...@@ -20,13 +18,14 @@ $(() => {
gl.commits.pipelines = gl.commits.pipelines || {}; gl.commits.pipelines = gl.commits.pipelines || {};
if (gl.commits.PipelinesTableBundle) { if (gl.commits.PipelinesTableBundle) {
document.querySelector('#commit-pipeline-table-view').removeChild(this.pipelinesTableBundle.$el);
gl.commits.PipelinesTableBundle.$destroy(true); gl.commits.PipelinesTableBundle.$destroy(true);
} }
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount();
document.querySelector('#commit-pipeline-table-view').appendChild(gl.commits.pipelines.PipelinesTableBundle.$el);
} }
}); });
import Vue from 'vue'; import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
...@@ -7,6 +8,7 @@ import EmptyState from '../../vue_pipelines_index/components/empty_state'; ...@@ -7,6 +8,7 @@ import EmptyState from '../../vue_pipelines_index/components/empty_state';
import ErrorState from '../../vue_pipelines_index/components/error_state'; import ErrorState from '../../vue_pipelines_index/components/error_state';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor'; import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll';
/** /**
* *
...@@ -20,6 +22,7 @@ import '../../vue_shared/vue_resource_interceptor'; ...@@ -20,6 +22,7 @@ import '../../vue_shared/vue_resource_interceptor';
*/ */
export default Vue.component('pipelines-table', { export default Vue.component('pipelines-table', {
components: { components: {
'pipelines-table-component': PipelinesTableComponent, 'pipelines-table-component': PipelinesTableComponent,
'error-state': ErrorState, 'error-state': ErrorState,
...@@ -42,6 +45,7 @@ export default Vue.component('pipelines-table', { ...@@ -42,6 +45,7 @@ export default Vue.component('pipelines-table', {
state: store.state, state: store.state,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false,
}; };
}, },
...@@ -64,17 +68,41 @@ export default Vue.component('pipelines-table', { ...@@ -64,17 +68,41 @@ export default Vue.component('pipelines-table', {
* *
*/ */
beforeMount() { beforeMount() {
this.endpoint = this.$el.dataset.endpoint; const element = document.querySelector('#commit-pipeline-table-view');
this.helpPagePath = this.$el.dataset.helpPagePath;
this.endpoint = element.dataset.endpoint;
this.helpPagePath = element.dataset.helpPagePath;
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.fetchPipelines(); this.poll = new Poll({
resource: this.service,
method: 'getPipelines',
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() { beforeUpdate() {
if (this.state.pipelines.length && this.$children) { if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue); this.store.startTimeAgoLoops.call(this, Vue);
} }
}, },
...@@ -83,21 +111,35 @@ export default Vue.component('pipelines-table', { ...@@ -83,21 +111,35 @@ export default Vue.component('pipelines-table', {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
destroyed() {
this.poll.stop();
},
methods: { methods: {
fetchPipelines() { fetchPipelines() {
this.isLoading = true; this.isLoading = true;
return this.service.getPipelines() return this.service.getPipelines()
.then(response => response.json()) .then(response => this.successCallback(response))
.then((json) => { .catch(() => this.errorCallback());
},
successCallback(resp) {
const response = resp.json();
// depending of the endpoint the response can either bring a `pipelines` key or not. // depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = json.pipelines || json; const pipelines = response.pipelines || response;
this.store.storePipelines(pipelines); this.store.storePipelines(pipelines);
this.isLoading = false; this.isLoading = false;
}) },
.catch(() => {
errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
}); },
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
}, },
}, },
......
...@@ -65,7 +65,6 @@ export default class Poll { ...@@ -65,7 +65,6 @@ export default class Poll {
this.makeRequest(); this.makeRequest();
}, pollInterval); }, pollInterval);
} }
this.options.successCallback(response); this.options.successCallback(response);
} }
...@@ -76,8 +75,14 @@ export default class Poll { ...@@ -76,8 +75,14 @@ export default class Poll {
notificationCallback(true); notificationCallback(true);
return resource[method](data) return resource[method](data)
.then(response => this.checkConditions(response)) .then((response) => {
.catch(error => errorCallback(error)); this.checkConditions(response);
notificationCallback(false);
})
.catch((error) => {
notificationCallback(false);
errorCallback(error);
});
} }
/** /**
......
...@@ -90,6 +90,7 @@ import './flash'; ...@@ -90,6 +90,7 @@ import './flash';
.on('click', this.clickTab); .on('click', this.clickTab);
} }
// Used in tests
unbindEvents() { unbindEvents() {
$(document) $(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
...@@ -99,9 +100,11 @@ import './flash'; ...@@ -99,9 +100,11 @@ import './flash';
.off('click', this.clickTab); .off('click', this.clickTab);
} }
destroy() { destroyPipelinesView() {
this.unbindEvents();
if (this.commitPipelinesTable) { if (this.commitPipelinesTable) {
document.querySelector('#commit-pipeline-table-view')
.removeChild(this.commitPipelinesTable.$el);
this.commitPipelinesTable.$destroy(); this.commitPipelinesTable.$destroy();
} }
} }
...@@ -128,6 +131,7 @@ import './flash'; ...@@ -128,6 +131,7 @@ import './flash';
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView();
} else if (this.isDiffAction(action)) { } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if (Breakpoints.get().getBreakpointSize() !== 'lg') { if (Breakpoints.get().getBreakpointSize() !== 'lg') {
...@@ -136,12 +140,14 @@ import './flash'; ...@@ -136,12 +140,14 @@ import './flash';
if (this.diffViewType() === 'parallel') { if (this.diffViewType() === 'parallel') {
this.expandViewContainer(); this.expandViewContainer();
} }
this.destroyPipelinesView();
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
this.resetViewContainer(); this.resetViewContainer();
this.loadPipelines(); this.mountPipelinesView();
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView();
} }
if (this.setUrl) { if (this.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
...@@ -227,16 +233,12 @@ import './flash'; ...@@ -227,16 +233,12 @@ import './flash';
}); });
} }
loadPipelines() { mountPipelinesView() {
if (this.pipelinesLoaded) { this.commitPipelinesTable = new CommitPipelinesTable().$mount();
return; // $mount(el) replaces the el with the new rendered component. We need it in order to mount
} // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); document.querySelector('#commit-pipeline-table-view')
// Could already be mounted from the `pipelines_bundle` .appendChild(this.commitPipelinesTable.$el);
if (pipelineTableViewEl) {
this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl);
}
this.pipelinesLoaded = true;
} }
loadDiff(source) { loadDiff(source) {
......
import Vue from 'vue'; import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service'; import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
...@@ -7,6 +8,7 @@ import EmptyState from './components/empty_state'; ...@@ -7,6 +8,7 @@ import EmptyState from './components/empty_state';
import ErrorState from './components/error_state'; import ErrorState from './components/error_state';
import NavigationTabs from './components/navigation_tabs'; import NavigationTabs from './components/navigation_tabs';
import NavigationControls from './components/nav_controls'; import NavigationControls from './components/nav_controls';
import Poll from '../lib/utils/poll';
export default { export default {
props: { props: {
...@@ -47,6 +49,7 @@ export default { ...@@ -47,6 +49,7 @@ export default {
pagenum: 1, pagenum: 1,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false,
}; };
}, },
...@@ -120,18 +123,49 @@ export default { ...@@ -120,18 +123,49 @@ export default {
tagsPath: this.tagsPath, tagsPath: this.tagsPath,
}; };
}, },
pageParameter() {
return gl.utils.getParameterByName('page') || this.pagenum;
},
scopeParameter() {
return gl.utils.getParameterByName('scope') || this.apiScope;
},
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.fetchPipelines(); const poll = new Poll({
resource: this.service,
method: 'getPipelines',
data: { page: this.pageParameter, scope: this.scopeParameter },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() { beforeUpdate() {
if (this.state.pipelines.length && this.$children) { if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue); this.store.startTimeAgoLoops.call(this, Vue);
} }
}, },
...@@ -154,27 +188,35 @@ export default { ...@@ -154,27 +188,35 @@ export default {
}, },
fetchPipelines() { fetchPipelines() {
const pageNumber = gl.utils.getParameterByName('page') || this.pagenum; if (!this.isMakingRequest) {
const scope = gl.utils.getParameterByName('scope') || this.apiScope;
this.isLoading = true; this.isLoading = true;
return this.service.getPipelines(scope, pageNumber)
.then(resp => ({ this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
},
successCallback(resp) {
const response = {
headers: resp.headers, headers: resp.headers,
body: resp.json(), body: resp.json(),
})) };
.then((response) => {
this.store.storeCount(response.body.count); this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines); this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers); this.store.storePagination(response.headers);
})
.then(() => {
this.isLoading = false; this.isLoading = false;
}) },
.catch(() => {
errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
}); },
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
}, },
}, },
......
...@@ -26,7 +26,8 @@ export default class PipelinesService { ...@@ -26,7 +26,8 @@ export default class PipelinesService {
this.pipelines = Vue.resource(endpoint); this.pipelines = Vue.resource(endpoint);
} }
getPipelines(scope, page) { getPipelines(data = {}) {
const { scope, page } = data;
return this.pipelines.get({ scope, page }); return this.pipelines.get({ scope, page });
} }
......
...@@ -35,6 +35,8 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -35,6 +35,8 @@ class Projects::CommitController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: PipelineSerializer render json: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
.represent(@pipelines) .represent(@pipelines)
......
...@@ -233,6 +233,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -233,6 +233,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: PipelineSerializer render json: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
.represent(@pipelines) .represent(@pipelines)
...@@ -246,6 +248,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -246,6 +248,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.json do format.json do
define_pipelines_vars define_pipelines_vars
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
......
...@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
......
...@@ -88,6 +88,8 @@ module Ci ...@@ -88,6 +88,8 @@ module Ci
pipeline.run_after_commit do pipeline.run_after_commit do
PipelineHooksWorker.perform_async(id) PipelineHooksWorker.perform_async(id)
Ci::ExpirePipelineCacheService.new(project, nil)
.execute(pipeline)
end end
end end
......
module Ci
class ExpirePipelineCacheService < BaseService
attr_reader :pipeline
def execute(pipeline)
@pipeline = pipeline
store = Gitlab::EtagCaching::Store.new
store.touch(project_pipelines_path)
store.touch(commit_pipelines_path) if pipeline.commit
store.touch(new_merge_request_pipelines_path)
merge_requests_pipelines_paths.each { |path| store.touch(path) }
end
private
def project_pipelines_path
Gitlab::Routing.url_helpers.namespace_project_pipelines_path(
project.namespace,
project,
format: :json)
end
def commit_pipelines_path
Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path(
project.namespace,
project,
pipeline.commit.id,
format: :json)
end
def new_merge_request_pipelines_path
Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path(
project.namespace,
project,
format: :json)
end
def merge_requests_pipelines_paths
pipeline.merge_requests.collect do |merge_request|
Gitlab::Routing.url_helpers.pipelines_namespace_project_merge_request_path(
project.namespace,
project,
merge_request,
format: :json)
end
end
end
end
...@@ -10,6 +10,22 @@ module Gitlab ...@@ -10,6 +10,22 @@ module Gitlab
{ {
regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z), regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z),
name: 'issue_title' name: 'issue_title'
},
{
regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/pipelines\.json\z),
name: 'project_pipelines'
},
{
regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/commit/\s+/pipelines\.json\z),
name: 'commit_pipelines'
},
{
regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/new\.json\z),
name: 'new_merge_request_pipelines'
},
{
regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/\d+/pipelines\.json\z),
name: 'merge_request_pipelines'
} }
].freeze ].freeze
...@@ -65,7 +81,7 @@ module Gitlab ...@@ -65,7 +81,7 @@ module Gitlab
status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429 status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
[status_code, { 'ETag' => etag }, ['']] [status_code, { 'ETag' => etag }, []]
end end
def track_cache_miss(if_none_match, cached_value_present, route) def track_cache_miss(if_none_match, cached_value_present, route)
......
...@@ -42,7 +42,8 @@ require('vendor/jquery.scrollTo'); ...@@ -42,7 +42,8 @@ require('vendor/jquery.scrollTo');
}); });
afterEach(function () { afterEach(function () {
this.class.destroy(); this.class.unbindEvents();
this.class.destroyPipelinesView();
}); });
describe('#activateTab', function () { describe('#activateTab', function () {
...@@ -68,6 +69,7 @@ require('vendor/jquery.scrollTo'); ...@@ -68,6 +69,7 @@ require('vendor/jquery.scrollTo');
expect($('#diffs')).toHaveClass('active'); expect($('#diffs')).toHaveClass('active');
}); });
}); });
describe('#opensInNewTab', function () { describe('#opensInNewTab', function () {
var tabUrl; var tabUrl;
var windowTarget = '_blank'; var windowTarget = '_blank';
...@@ -119,6 +121,7 @@ require('vendor/jquery.scrollTo'); ...@@ -119,6 +121,7 @@ require('vendor/jquery.scrollTo');
stopImmediatePropagation: function () {} stopImmediatePropagation: function () {}
}); });
}); });
it('opens page tab in a new browser tab with Cmd+Click - Mac', function () { it('opens page tab in a new browser tab with Cmd+Click - Mac', function () {
spyOn(window, 'open').and.callFake(function (url, name) { spyOn(window, 'open').and.callFake(function (url, name) {
expect(url).toEqual(tabUrl); expect(url).toEqual(tabUrl);
...@@ -132,6 +135,7 @@ require('vendor/jquery.scrollTo'); ...@@ -132,6 +135,7 @@ require('vendor/jquery.scrollTo');
stopImmediatePropagation: function () {} stopImmediatePropagation: function () {}
}); });
}); });
it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () { it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () {
spyOn(window, 'open').and.callFake(function (url, name) { spyOn(window, 'open').and.callFake(function (url, name) {
expect(url).toEqual(tabUrl); expect(url).toEqual(tabUrl);
...@@ -152,6 +156,7 @@ require('vendor/jquery.scrollTo'); ...@@ -152,6 +156,7 @@ require('vendor/jquery.scrollTo');
spyOn($, 'ajax').and.callFake(function () {}); spyOn($, 'ajax').and.callFake(function () {});
this.subject = this.class.setCurrentAction; this.subject = this.class.setCurrentAction;
}); });
it('changes from commits', function () { it('changes from commits', function () {
setLocation({ setLocation({
pathname: '/foo/bar/merge_requests/1/commits' pathname: '/foo/bar/merge_requests/1/commits'
...@@ -159,13 +164,16 @@ require('vendor/jquery.scrollTo'); ...@@ -159,13 +164,16 @@ require('vendor/jquery.scrollTo');
expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1');
expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
}); });
it('changes from diffs', function () { it('changes from diffs', function () {
setLocation({ setLocation({
pathname: '/foo/bar/merge_requests/1/diffs' pathname: '/foo/bar/merge_requests/1/diffs'
}); });
expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1');
expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
}); });
it('changes from diffs.html', function () { it('changes from diffs.html', function () {
setLocation({ setLocation({
pathname: '/foo/bar/merge_requests/1/diffs.html' pathname: '/foo/bar/merge_requests/1/diffs.html'
...@@ -173,6 +181,7 @@ require('vendor/jquery.scrollTo'); ...@@ -173,6 +181,7 @@ require('vendor/jquery.scrollTo');
expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1');
expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
}); });
it('changes from notes', function () { it('changes from notes', function () {
setLocation({ setLocation({
pathname: '/foo/bar/merge_requests/1' pathname: '/foo/bar/merge_requests/1'
...@@ -180,6 +189,7 @@ require('vendor/jquery.scrollTo'); ...@@ -180,6 +189,7 @@ require('vendor/jquery.scrollTo');
expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
}); });
it('includes search parameters and hash string', function () { it('includes search parameters and hash string', function () {
setLocation({ setLocation({
pathname: '/foo/bar/merge_requests/1/diffs', pathname: '/foo/bar/merge_requests/1/diffs',
...@@ -188,6 +198,7 @@ require('vendor/jquery.scrollTo'); ...@@ -188,6 +198,7 @@ require('vendor/jquery.scrollTo');
}); });
expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35');
}); });
it('replaces the current history state', function () { it('replaces the current history state', function () {
var newState; var newState;
setLocation({ setLocation({
...@@ -200,6 +211,7 @@ require('vendor/jquery.scrollTo'); ...@@ -200,6 +211,7 @@ require('vendor/jquery.scrollTo');
}, document.title, newState); }, document.title, newState);
} }
}); });
it('treats "show" like "notes"', function () { it('treats "show" like "notes"', function () {
setLocation({ setLocation({
pathname: '/foo/bar/merge_requests/1/commits' pathname: '/foo/bar/merge_requests/1/commits'
...@@ -210,6 +222,7 @@ require('vendor/jquery.scrollTo'); ...@@ -210,6 +222,7 @@ require('vendor/jquery.scrollTo');
describe('#tabShown', () => { describe('#tabShown', () => {
beforeEach(function () { beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {});
loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
}); });
......
...@@ -91,6 +91,12 @@ describe Gitlab::EtagCaching::Middleware do ...@@ -91,6 +91,12 @@ describe Gitlab::EtagCaching::Middleware do
expect(status).to eq 304 expect(status).to eq 304
end end
it 'returns empty body' do
_, _, body = middleware.call(build_env(path, if_none_match))
expect(body).to be_empty
end
it 'tracks "etag_caching_cache_hit" event' do it 'tracks "etag_caching_cache_hit" event' do
expect(Gitlab::Metrics).to receive(:add_event) expect(Gitlab::Metrics).to receive(:add_event)
.with(:etag_caching_middleware_used, endpoint: 'issue_notes') .with(:etag_caching_middleware_used, endpoint: 'issue_notes')
......
...@@ -335,6 +335,14 @@ describe Ci::Pipeline, models: true do ...@@ -335,6 +335,14 @@ describe Ci::Pipeline, models: true do
end end
end end
describe 'pipeline ETag caching' do
it 'executes ExpirePipelinesCacheService' do
expect_any_instance_of(Ci::ExpirePipelineCacheService).to receive(:execute).with(pipeline)
pipeline.cancel
end
end
def create_build(name, queued_at = current, started_from = 0) def create_build(name, queued_at = current, started_from = 0)
create(:ci_build, create(:ci_build,
name: name, name: name,
......
require 'spec_helper'
describe Ci::ExpirePipelineCacheService, services: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
subject { described_class.new(project, user) }
describe '#execute' do
it 'invalidate Etag caching for project pipelines path' do
pipelines_path = "/#{project.full_path}/pipelines.json"
new_mr_pipelines_path = "/#{project.full_path}/merge_requests/new.json"
expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipelines_path)
expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(new_mr_pipelines_path)
subject.execute(pipeline)
end
end
end
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