BigW Consortium Gitlab
Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
G
gitlab-ce
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Forest Godfrey
gitlab-ce
Commits
b3309bb2
Commit
b3309bb2
authored
Feb 03, 2017
by
Filipa Lacerda
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Adjustments to receive new data schema
parent
efa05023
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
138 additions
and
447 deletions
+138
-447
environment.js.es6
...ts/javascripts/environments/components/environment.js.es6
+9
-26
environment_item.js.es6
...vascripts/environments/components/environment_item.js.es6
+0
-0
environments_store.js.es6
...javascripts/environments/stores/environments_store.js.es6
+14
-154
environments.scss
app/assets/stylesheets/pages/environments.scss
+9
-8
environment_item_spec.js.es6
spec/javascripts/environments/environment_item_spec.js.es6
+59
-78
environments_store_spec.js.es6
spec/javascripts/environments/environments_store_spec.js.es6
+4
-44
mock_data.js.es6
spec/javascripts/environments/mock_data.js.es6
+43
-137
No files found.
app/assets/javascripts/environments/components/environment.js.es6
View file @
b3309bb2
...
...
@@ -69,12 +69,10 @@ require('./environment_item');
* Toggles loading property.
*/
created() {
gl.environmentsService = new EnvironmentsService(this.endpoint);
const scope = this.$options.getQueryParameter('scope') || this.visibility;
const endpoint = `${this.endpoint}?scope=${scope}`;
const scope = this.$options.getQueryParameter('scope');
if (scope) {
this.store.storeVisibility(scope);
}
gl.environmentsService = new EnvironmentsService(endpoint);
this.isLoading = true;
...
...
@@ -82,6 +80,8 @@ require('./environment_item');
.then(resp => resp.json())
.then((json) => {
this.store.storeEnvironments(json);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
...
...
@@ -165,8 +165,7 @@ require('./environment_item');
</a>
</p>
<a
v-if="canCreateEnvironmentParsed"
<a v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
...
...
@@ -174,7 +173,7 @@ require('./environment_item');
</div>
<div class="table-holder"
v-if="!isLoading && state.
filteredE
nvironments.length > 0">
v-if="!isLoading && state.
e
nvironments.length > 0">
<table class="table ci-table environments">
<thead>
<tr>
...
...
@@ -187,31 +186,15 @@ require('./environment_item');
</tr>
</thead>
<tbody>
<template v-for="model in state.
filteredE
nvironments"
<template v-for="model in state.
e
nvironments"
v-bind:model="model">
<tr
is="environment-item"
<tr is="environment-item"
:model="model"
:toggleRow="toggleRow.bind(model)"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0"
is="environment-item"
v-for="children in model.children"
:model="children"
:toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</tr>
</template>
</tbody>
</table>
...
...
app/assets/javascripts/environments/components/environment_item.js.es6
View file @
b3309bb2
This diff is collapsed.
Click to expand it.
app/assets/javascripts/environments/stores/environments_store.js.es6
View file @
b3309bb2
...
...
@@ -10,181 +10,41 @@
this.state.environments = [];
this.state.stoppedCounter = 0;
this.state.availableCounter = 0;
this.state.visibility = 'available';
this.state.filteredEnvironments = [];
return this;
},
/**
* In order to display a tree view we need to modify the received
* data in to a tree structure based on `environment_type`
* sorted alphabetically.
* In each children a `vue-` property will be added. This property will be
* used to know if an item is a children mostly for css purposes. This is
* needed because the children row is a fragment instance and therfore does
* not accept non-prop attributes.
*
* Stores the received environments.
*
* @example
* it will transform this:
* [
* { name: "environment", environment_type: "review" },
* { name: "environment_1", environment_type: null }
* { name: "environment_2, environment_type: "review" }
* ]
* into this:
* [
* { name: "review", children:
* [
* { name: "environment", environment_type: "review", vue-isChildren: true},
* { name: "environment_2", environment_type: "review", vue-isChildren: true}
* ]
* },
* {name: "environment_1", environment_type: null}
* ]
* Each environment has the following schema
* { name: String, size: Number, latest: Object }
*
* If the `size` is bigger than 1, it means it should be rendered as a folder.
* In those cases we add `isFolder` key in order to render it properly.
*
* @param {Array} environments
List of environments.
* @returns {Array}
Tree structured array with the received environments.
* @param {Array} environments
* @returns {Array}
*/
storeEnvironments(environments = []) {
this.state.stoppedCounter = this.countByState(environments, 'stopped');
this.state.availableCounter = this.countByState(environments, 'available');
const environmentsTree = environments.reduce((acc, environment) => {
if (environment.environment_type !== null) {
const occurs = acc.filter(element => element.children &&
element.name === environment.environment_type);
environment['vue-isChildren'] = true;
if (occurs.length) {
acc[acc.indexOf(occurs[0])].children.push(environment);
acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
} else {
acc.push({
name: environment.environment_type,
children: [environment],
isOpen: false,
'vue-isChildren': environment['vue-isChildren'],
});
}
} else {
acc.push(environment);
}
return acc;
}, []).slice().sort(this.sortByName);
this.state.environments = environmentsTree;
this.filterEnvironmentsByVisibility(this.state.environments);
return environmentsTree;
},
storeVisibility(visibility) {
this.state.visibility = visibility;
},
/**
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*
* Given array of environments will return only
* the environments that match the state stored.
*
* @param {Array} array
* @return {Array}
*/
filterEnvironmentsByVisibility(arr) {
const filteredEnvironments = arr.map((item) => {
if (item.children) {
const filteredChildren = this.filterEnvironmentsByVisibility(
item.children,
).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
}
}
return this.filterState(this.state.visibility, item);
}).filter(Boolean);
this.state.filteredEnvironments = filteredEnvironments;
return filteredEnvironments;
},
/**
* Given the state and the environment,
* returns only if the environment state matches the one provided.
*
* @param {String} state
* @param {Object} environment
* @return {Object}
*/
filterState(state, environment) {
return environment.state === state && environment;
},
/**
* Toggles folder open property given the environment type.
*
* @param {String} envType
* @return {Array}
*/
toggleFolder(envType) {
const environments = this.state.environments;
const environmentsCopy = environments.map((env) => {
if (env['vue-isChildren'] && env.name === envType) {
env.isOpen = !env.isOpen;
const filteredEnvironments = environments.map((env) => {
if (env.size > 1) {
return Object.assign({}, env, { isFolder: true });
}
return env;
});
this.state.environments =
environmentsCopy
;
this.state.environments =
filteredEnvironments
;
return
environmentsCopy
;
return
filteredEnvironments
;
},
/**
* Given an array of environments, returns the number of environments
* that have the given state.
*
* @param {Array} environments
* @param {String} state
* @returns {Number}
*/
countByState(environments, state) {
return environments.filter(env => env.state === state).length;
storeCounts() {
//TODO
},
/**
* Sorts the two objects provided by their name.
*
* @param {Object} a
* @param {Object} b
* @returns {Number}
*/
sortByName(a, b) {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line
},
};
})();
app/assets/stylesheets/pages/environments.scss
View file @
b3309bb2
...
...
@@ -110,17 +110,19 @@
}
}
.children-row
.environment-name
{
margin-left
:
17px
;
margin-right
:
-17px
;
}
.folder-icon
{
padding
:
0
5px
0
0
;
margin-right
:
3px
;
color
:
$gl-text-color-secondary
;
.fa
:nth-child
(
1
)
{
margin-right
:
3px
;
}
}
.folder-name
{
cursor
:
pointer
;
text-decoration
:
none
;
color
:
$gl-text-color-secondary
;
}
}
...
...
@@ -135,4 +137,4 @@
margin-right
:
0
;
}
}
}
\ No newline at end of file
}
spec/javascripts/environments/environment_item_spec.js.es6
View file @
b3309bb2
...
...
@@ -14,33 +14,13 @@ describe('Environment item', () => {
beforeEach(() => {
mockItem = {
name: 'review',
children: [
{
name: 'review-app',
id: 1,
state: 'available',
external_url: '',
last_deployment: {},
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
},
{
name: 'production',
id: 2,
state: 'available',
external_url: '',
last_deployment: {},
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
},
],
size: 3
};
component = new window.gl.environmentsList.EnvironmentItem({
el: document.querySelector('tr#environment-row'),
propsData: {
model: mockItem,
toggleRow: () => {},
canCreateDeployment: false,
canReadEnvironment: true,
},
...
...
@@ -53,7 +33,7 @@ describe('Environment item', () => {
});
it('Should render the number of children in a badge', () => {
expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.
children.length
);
expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.
size
);
});
});
...
...
@@ -63,38 +43,23 @@ describe('Environment item', () => {
beforeEach(() => {
environment = {
id: 31,
name: 'production',
state: 'stopped',
external_url: 'http://external.com',
environment_type: null,
last_deployment: {
id: 66,
iid: 6,
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
ref_path: 'root/ci-folders/tree/master',
},
tag: true,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
size: 1,
latest: {
state: 'stopped',
external_url: 'http://external.com',
environment_type: null,
last_deployment: {
id: 66,
iid: 6,
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
ref_path: 'root/ci-folders/tree/master',
},
tag: true,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
...
...
@@ -102,34 +67,50 @@ describe('Environment item', () => {
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1279,
name: 'deploy',
build_path: '/root/ci-folders/builds/1279',
retry_path: '/root/ci-folders/builds/1279/retry',
created_at: '2016-11-29T18:11:58.430Z',
updated_at: '2016-11-29T18:11:58.430Z',
},
manual_actions: [
{
name: 'action',
play_path: '/play',
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
],
deployable: {
id: 1279,
name: 'deploy',
build_path: '/root/ci-folders/builds/1279',
retry_path: '/root/ci-folders/builds/1279/retry',
created_at: '2016-11-29T18:11:58.430Z',
updated_at: '2016-11-29T18:11:58.430Z',
},
manual_actions: [
{
name: 'action',
play_path: '/play',
},
],
},
'stop_action?': true,
environment_path: 'root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
},
'stop_action?': true,
environment_path: 'root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
};
component = new window.gl.environmentsList.EnvironmentItem({
el: document.querySelector('tr#environment-row'),
propsData: {
model: environment,
toggleRow: () => {},
canCreateDeployment: true,
canReadEnvironment: true,
},
...
...
@@ -144,7 +125,7 @@ describe('Environment item', () => {
it('should render deployment internal id', () => {
expect(
component.$el.querySelector('.deployment-column span').textContent,
).toContain(environment.last_deployment.iid);
).toContain(environment.la
test.la
st_deployment.iid);
expect(
component.$el.querySelector('.deployment-column span').textContent,
...
...
@@ -154,7 +135,7 @@ describe('Environment item', () => {
it('should render last deployment date', () => {
const timeagoInstance = new timeago(); // eslint-disable-line
const formatedDate = timeagoInstance.format(
environment.last_deployment.deployable.created_at,
environment.la
test.la
st_deployment.deployable.created_at,
);
expect(
...
...
@@ -166,7 +147,7 @@ describe('Environment item', () => {
it('should render user avatar with link to profile', () => {
expect(
component.$el.querySelector('.js-deploy-user-container').getAttribute('href'),
).toEqual(environment.last_deployment.user.web_url);
).toEqual(environment.la
test.la
st_deployment.user.web_url);
});
});
...
...
@@ -174,13 +155,13 @@ describe('Environment item', () => {
it('Should link to build url provided', () => {
expect(
component.$el.querySelector('.build-link').getAttribute('href'),
).toEqual(environment.last_deployment.deployable.build_path);
).toEqual(environment.la
test.la
st_deployment.deployable.build_path);
});
it('Should render deployable name and id', () => {
expect(
component.$el.querySelector('.build-link').getAttribute('href'),
).toEqual(environment.last_deployment.deployable.build_path);
).toEqual(environment.la
test.la
st_deployment.deployable.build_path);
});
});
...
...
spec/javascripts/environments/environments_store_spec.js.es6
View file @
b3309bb2
...
...
@@ -20,50 +20,10 @@ require('./mock_data');
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
});
it('should count stopped environments and save the count in the state', () => {
expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1);
});
it('should count available environments and save the count in the state', () => {
expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3);
});
it('should store environments with same environment_type as sibilings', () => {
expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3);
const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments
.filter(env => env.children && env.children.length > 0);
expect(parentFolder[0].children.length).toBe(2);
expect(parentFolder[0].children[0].environment_type).toBe('review');
expect(parentFolder[0].children[1].environment_type).toBe('review');
expect(parentFolder[0].children[0].name).toBe('test-environment');
expect(parentFolder[0].children[1].name).toBe('test-environment-1');
});
it('should sort the environments alphabetically', () => {
const { environments } = gl.environmentsList.EnvironmentsStore.state;
expect(environments[0].name).toBe('production');
expect(environments[1].name).toBe('review');
expect(environments[1].children[0].name).toBe('test-environment');
expect(environments[1].children[1].name).toBe('test-environment-1');
expect(environments[2].name).toBe('review_app');
});
});
describe('toggleFolder', () => {
beforeEach(() => {
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
});
it('should toggle the open property for the given environment', () => {
gl.environmentsList.EnvironmentsStore.toggleFolder('review');
const { environments } = gl.environmentsList.EnvironmentsStore.state;
const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review');
expect(environment[0].isOpen).toBe(true);
it('should store environments', () => {
expect(
gl.environmentsList.EnvironmentsStore.state.environments.length
).toBe(environmentsList.length);
});
});
});
...
...
spec/javascripts/environments/mock_data.js.es6
View file @
b3309bb2
const environmentsList = [
{
id: 31,
name: 'production',
state: 'available',
external_url: 'https://www.gitlab.com',
environment_type: null,
last_deployment: {
id: 64,
iid: 5,
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
},
tag: false,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
name: 'DEV',
size: 1,
latest: {
id: 7,
name: 'DEV',
state: 'available',
external_url: null,
environment_type: null,
last_deployment: null,
'stop_action?': false,
environment_path: '/root/review-app/environments/7',
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
},
'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
{
id: 32,
name: 'review_app',
state: 'stopped',
external_url: 'https://www.gitlab.com',
environment_type: null,
last_deployment: {
id: 64,
iid: 5,
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
},
tag: false,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
name: 'build',
size: 5,
latest: {
id: 12,
name: 'build/update-README',
state: 'available',
external_url: null,
environment_type: 'build',
last_deployment: null,
'stop_action?': false,
environment_path: '/root/review-app/environments/12',
stop_path: '/root/review-app/environments/12/stop',
created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z',
},
'stop_action?': false,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
{
id: 33,
name: 'test-environment',
state: 'available',
environment_type: 'review',
last_deployment: null,
'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
{
id: 34,
name: 'test-environment-1',
state: 'available',
environment_type: 'review',
last_deployment: null,
'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
];
window.environmentsList = environmentsList;
const environment = {
id: 4,
name: 'production',
state: 'available',
external_url: 'http://production.',
environment_type: null,
last_deployment: {},
'stop_action?': false,
environment_path: '/root/review-app/environments/4',
stop_path: '/root/review-app/environments/4/stop',
created_at: '2016-12-16T11:51:04.690Z',
updated_at: '2016-12-16T12:04:51.133Z',
name: 'DEV',
size: 1,
latest: {
id: 7,
name: 'DEV',
state: 'available',
external_url: null,
environment_type: null,
last_deployment: null,
'stop_action?': false,
environment_path: '/root/review-app/environments/7',
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
},
};
window.environment = environment;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment