summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2017-06-01 20:38:46 +0000
committerKamil Trzciński <ayufan@ayufan.eu>2017-06-01 20:38:46 +0000
commit8d131eb85ddc1ee6cd4a27830b2607617d767f28 (patch)
treeec42e6d1ea049de36c21de8b3017988963bd5289
parentb92e3d746466667642fd99d1ed72b481488d1a9f (diff)
parent696b0395116f9f973c919b281a1a5df023f22084 (diff)
downloadgitlab-ce-8d131eb85ddc1ee6cd4a27830b2607617d767f28.tar.gz
Merge branch 'zj-realtime-env-list' into 'master'
Realtime env list Closes #31701 See merge request !11333
-rw-r--r--app/assets/javascripts/environments/components/environment.vue95
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue100
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js17
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js3
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js6
-rw-r--r--app/controllers/projects/environments_controller.rb2
-rw-r--r--app/models/deployment.rb5
-rw-r--r--app/models/environment.rb16
-rw-r--r--changelogs/unreleased/zj-realtime-env-list.yml4
-rw-r--r--lib/gitlab/etag_caching/router.rb8
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb5
-rw-r--r--spec/javascripts/environments/environments_store_spec.js9
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb11
-rw-r--r--spec/models/deployment_spec.rb13
-rw-r--r--spec/models/environment_spec.rb22
15 files changed, 259 insertions, 57 deletions
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
index d4e13f3c84a..86d8fe89010 100644
--- a/app/assets/javascripts/environments/components/environment.vue
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -1,5 +1,6 @@
<script>
/* global Flash */
+import Visibility from 'visibilityjs';
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
@@ -7,6 +8,8 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
+import Poll from '../../lib/utils/poll';
+import environmentsMixin from '../mixins/environments_mixin';
export default {
@@ -16,6 +19,10 @@ export default {
loadingIcon,
},
+ mixins: [
+ environmentsMixin,
+ ],
+
data() {
const environmentsData = document.querySelector('#environments-list-view').dataset;
const store = new EnvironmentsStore();
@@ -35,6 +42,7 @@ export default {
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
+ isMakingRequest: false,
// Pagination Properties,
paginationInformation: {},
@@ -65,17 +73,43 @@ export default {
* Toggles loading property.
*/
created() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
+
this.service = new EnvironmentsService(this.endpoint);
- this.fetchEnvironments();
+ const poll = new Poll({
+ resource: this.service,
+ method: 'get',
+ data: { scope, page },
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: (isMakingRequest) => {
+ this.isMakingRequest = isMakingRequest;
+
+ // We need to verify if any folder is open to also fecth it
+ this.openFolders = this.store.getOpenFolders();
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ poll.restart();
+ } else {
+ poll.stop();
+ }
+ });
- eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder);
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
- eventHub.$off('refreshEnvironments');
eventHub.$off('toggleFolder');
eventHub.$off('postAction');
},
@@ -104,29 +138,13 @@ export default {
fetchEnvironments() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
this.isLoading = true;
- return this.service.get(scope, pageNumber)
- .then(resp => ({
- headers: resp.headers,
- body: resp.json(),
- }))
- .then((response) => {
- this.store.storeAvailableCount(response.body.available_count);
- this.store.storeStoppedCount(response.body.stopped_count);
- this.store.storeEnvironments(response.body.environments);
- this.store.setPagination(response.headers);
- })
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.');
- });
+ return this.service.get({ scope, page })
+ .then(this.successCallback)
+ .catch(this.errorCallback);
},
fetchChildEnvironments(folder, folderUrl) {
@@ -146,9 +164,34 @@ export default {
},
postAction(endpoint) {
- this.service.postAction(endpoint)
- .then(() => this.fetchEnvironments())
- .catch(() => new Flash('An error occured while making the request.'));
+ if (!this.isMakingRequest) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => this.fetchEnvironments())
+ .catch(() => new Flash('An error occured while making the request.'));
+ }
+ },
+
+ successCallback(resp) {
+ this.saveData(resp);
+
+ // If folders are open while polling we need to open them again
+ if (this.openFolders.length) {
+ this.openFolders.map((folder) => {
+ // TODO - Move this to the backend
+ const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
+
+ this.store.updateFolder(folder, 'isOpen', true);
+ return this.fetchChildEnvironments(folder, folderUrl);
+ });
+ }
+ },
+
+ errorCallback() {
+ this.isLoading = false;
+ // eslint-disable-next-line no-new
+ new Flash('An error occurred while fetching the environments.');
},
},
};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index bd161c8a379..925503a01c4 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,12 +1,15 @@
<script>
/* global Flash */
+import Visibility from 'visibilityjs';
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
+import Poll from '../../lib/utils/poll';
+import eventHub from '../event_hub';
+import environmentsMixin from '../mixins/environments_mixin';
import '../../lib/utils/common_utils';
-import '../../vue_shared/vue_resource_interceptor';
export default {
components: {
@@ -15,6 +18,10 @@ export default {
loadingIcon,
},
+ mixins: [
+ environmentsMixin,
+ ],
+
data() {
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
const store = new EnvironmentsStore();
@@ -76,33 +83,39 @@ export default {
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
-
- const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
-
- this.service = new EnvironmentsService(endpoint);
-
- this.isLoading = true;
-
- return this.service.get()
- .then(resp => ({
- headers: resp.headers,
- body: resp.json(),
- }))
- .then((response) => {
- this.store.storeAvailableCount(response.body.available_count);
- this.store.storeStoppedCount(response.body.stopped_count);
- this.store.storeEnvironments(response.body.environments);
- this.store.setPagination(response.headers);
- })
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.', 'alert');
- });
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ this.service = new EnvironmentsService(this.endpoint);
+
+ const poll = new Poll({
+ resource: this.service,
+ method: 'get',
+ data: { scope, page },
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: (isMakingRequest) => {
+ this.isMakingRequest = isMakingRequest;
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ poll.restart();
+ } else {
+ poll.stop();
+ }
+ });
+
+ eventHub.$on('postAction', this.postAction);
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('postAction');
},
methods: {
@@ -117,6 +130,37 @@ export default {
gl.utils.visitUrl(param);
return param;
},
+
+ fetchEnvironments() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ this.isLoading = true;
+
+ return this.service.get({ scope, page })
+ .then(this.successCallback)
+ .catch(this.errorCallback);
+ },
+
+ successCallback(resp) {
+ this.saveData(resp);
+ },
+
+ errorCallback() {
+ this.isLoading = false;
+ // eslint-disable-next-line no-new
+ new Flash('An error occurred while fetching the environments.');
+ },
+
+ postAction(endpoint) {
+ if (!this.isMakingRequest) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => this.fetchEnvironments())
+ .catch(() => new Flash('An error occured while making the request.'));
+ }
+ },
},
};
</script>
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
new file mode 100644
index 00000000000..25b24fbd6dc
--- /dev/null
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -0,0 +1,17 @@
+export default {
+ methods: {
+ saveData(resp) {
+ const response = {
+ headers: resp.headers,
+ body: resp.json(),
+ };
+
+ this.isLoading = false;
+
+ this.store.storeAvailableCount(response.body.available_count);
+ this.store.storeStoppedCount(response.body.stopped_count);
+ this.store.storeEnvironments(response.body.environments);
+ this.store.setPagination(response.headers);
+ },
+ },
+};
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 8adb53ea86d..03ab74b3338 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -10,7 +10,8 @@ export default class EnvironmentsService {
this.folderResults = 3;
}
- get(scope, page) {
+ get(options = {}) {
+ const { scope, page } = options;
return this.environments.get({ scope, page });
}
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 158e7922e3c..8a2f6a473de 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -153,4 +153,10 @@ export default class EnvironmentsStore {
return updatedEnvironments;
}
+ getOpenFolders() {
+ const environments = this.state.environments;
+
+ return environments.filter(env => env.isFolder && env.isOpen);
+ }
+
}
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index efe83776834..4630f451445 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -15,6 +15,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+
render json: {
environments: EnvironmentSerializer
.new(project: @project, current_user: @current_user)
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 216cec751e3..304179c0a97 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -12,6 +12,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true
after_create :create_ref
+ after_create :invalidate_cache
def commit
project.commit(sha)
@@ -33,6 +34,10 @@ class Deployment < ActiveRecord::Base
project.repository.create_ref(ref, ref_path)
end
+ def invalidate_cache
+ environment.expire_etag_cache
+ end
+
def manual_actions
@manual_actions ||= deployable.try(:other_actions)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 61572d8d69a..6211a5c1e63 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -57,6 +57,10 @@ class Environment < ActiveRecord::Base
state :available
state :stopped
+
+ after_transition do |environment|
+ environment.expire_etag_cache
+ end
end
def predefined_variables
@@ -196,6 +200,18 @@ class Environment < ActiveRecord::Base
[external_url, public_path].join('/')
end
+ def expire_etag_cache
+ Gitlab::EtagCaching::Store.new.tap do |store|
+ store.touch(etag_cache_key)
+ end
+ end
+
+ def etag_cache_key
+ Gitlab::Routing.url_helpers.namespace_project_environments_path(
+ project.namespace,
+ project)
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/changelogs/unreleased/zj-realtime-env-list.yml b/changelogs/unreleased/zj-realtime-env-list.yml
new file mode 100644
index 00000000000..6460d17edc9
--- /dev/null
+++ b/changelogs/unreleased/zj-realtime-env-list.yml
@@ -0,0 +1,4 @@
+---
+title: Make environment table realtime
+merge_request: 11333
+author:
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index d137cc1bae6..2f9d8bfc266 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -9,9 +9,11 @@ module Gitlab
# - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route
# - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
- commit pipelines merge_requests new].freeze
+ commit pipelines merge_requests new
+ environments].freeze
RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape)))
+
ROUTES = [
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z),
@@ -40,6 +42,10 @@ module Gitlab
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines/\d+\.json\z),
'project_pipeline'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/environments\.json\z),
+ 'environments'
)
].freeze
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index de13f17012b..f6840578145 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -57,6 +57,11 @@ describe Projects::EnvironmentsController do
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
+
+ it 'sets the polling interval header' do
+ expect(response).to have_http_status(:ok)
+ expect(response.headers['Poll-Interval']).to eq("3000")
+ end
end
context 'when requesting stopped environments scope' do
diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js
index f617c4bdffe..6e855530b21 100644
--- a/spec/javascripts/environments/environments_store_spec.js
+++ b/spec/javascripts/environments/environments_store_spec.js
@@ -123,4 +123,13 @@ describe('Store', () => {
expect(store.state.paginationInformation).toEqual(expectedResult);
});
});
+
+ describe('getOpenFolders', () => {
+ it('should return open folder', () => {
+ store.storeEnvironments(serverData);
+
+ store.toggleFolder(store.state.environments[1]);
+ expect(store.getOpenFolders()[0]).toEqual(store.state.environments[1]);
+ });
+ });
});
diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
index 46a238b17f4..0418fc0a1e2 100644
--- a/spec/lib/gitlab/etag_caching/router_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -77,6 +77,17 @@ describe Gitlab::EtagCaching::Router do
expect(result).to be_blank
end
+ it 'matches the environments path' do
+ env = build_env(
+ '/my-group/my-project/environments.json'
+ )
+
+ result = described_class.match(env)
+ expect(result).to be_present
+
+ expect(result.name).to eq 'environments'
+ end
+
it 'matches pipeline#show endpoint' do
env = build_env(
'/my-group/my-project/pipelines/2.json'
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 4bda7d4314a..6f0d2db23c7 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -16,6 +16,19 @@ describe Deployment, models: true do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+ describe 'after_create callbacks' do
+ let(:environment) { create(:environment) }
+ let(:store) { Gitlab::EtagCaching::Store.new }
+
+ it 'invalidates the environment etag cache' do
+ old_value = store.get(environment.etag_cache_key)
+
+ create(:deployment, environment: environment)
+
+ expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ end
+ end
+
describe '#includes_commit?' do
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 9fbe19b04d5..fe69c8e351d 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Environment, models: true do
- let(:project) { create(:empty_project) }
+ set(:project) { create(:empty_project) }
subject(:environment) { create(:environment, project: project) }
it { is_expected.to belong_to(:project) }
@@ -34,6 +34,26 @@ describe Environment, models: true do
end
end
+ describe 'state machine' do
+ it 'invalidates the cache after a change' do
+ expect(environment).to receive(:expire_etag_cache)
+
+ environment.stop
+ end
+ end
+
+ describe '#expire_etag_cache' do
+ let(:store) { Gitlab::EtagCaching::Store.new }
+
+ it 'changes the cached value' do
+ old_value = store.get(environment.etag_cache_key)
+
+ environment.stop
+
+ expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")