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
e1d4deb2
Commit
e1d4deb2
authored
May 24, 2018
by
Filipa Lacerda
Committed by
Dmitriy Zaporozhets
May 25, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Disables jupyter install button while ingress is not installed
Includes juptyer hostname in the post request Adds tests
parent
b3cf1530
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
212 additions
and
72 deletions
+212
-72
clusters_bundle.js
app/assets/javascripts/clusters/clusters_bundle.js
+3
-2
application_row.vue
...ssets/javascripts/clusters/components/application_row.vue
+15
-2
applications.vue
app/assets/javascripts/clusters/components/applications.vue
+11
-44
clusters_service.js
app/assets/javascripts/clusters/services/clusters_service.js
+2
-9
clusters_store.js
app/assets/javascripts/clusters/stores/clusters_store.js
+1
-1
clusters_bundle_spec.js
spec/javascripts/clusters/clusters_bundle_spec.js
+25
-7
application_row_spec.js
spec/javascripts/clusters/components/application_row_spec.js
+31
-1
applications_spec.js
spec/javascripts/clusters/components/applications_spec.js
+71
-6
mock_data.js
spec/javascripts/clusters/services/mock_data.js
+35
-0
clusters_store_spec.js
spec/javascripts/clusters/stores/clusters_store_spec.js
+18
-0
No files found.
app/assets/javascripts/clusters/clusters_bundle.js
View file @
e1d4deb2
...
...
@@ -211,11 +211,12 @@ export default class Clusters {
}
}
installApplication
(
appId
)
{
installApplication
(
data
)
{
const
appId
=
data
.
id
;
this
.
store
.
updateAppProperty
(
appId
,
'requestStatus'
,
REQUEST_LOADING
);
this
.
store
.
updateAppProperty
(
appId
,
'requestReason'
,
null
);
this
.
service
.
installApplication
(
appId
)
this
.
service
.
installApplication
(
appId
,
data
.
params
)
.
then
(()
=>
{
this
.
store
.
updateAppProperty
(
appId
,
'requestStatus'
,
REQUEST_SUCCESS
);
})
...
...
app/assets/javascripts/clusters/components/application_row.vue
View file @
e1d4deb2
...
...
@@ -52,6 +52,16 @@
type
:
String
,
required
:
false
,
},
disableInstallButton
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
installApplicationRequestParams
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
},
computed
:
{
rowJsClass
()
{
...
...
@@ -67,7 +77,7 @@
// Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return
(
this
.
status
!==
APPLICATION_INSTALLABLE
return
this
.
disableInstallButton
||
(
this
.
status
!==
APPLICATION_INSTALLABLE
&&
this
.
status
!==
APPLICATION_ERROR
)
||
this
.
requestStatus
===
REQUEST_LOADING
||
this
.
requestStatus
===
REQUEST_SUCCESS
;
...
...
@@ -109,7 +119,10 @@
},
methods
:
{
installClicked
()
{
eventHub
.
$emit
(
'installApplication'
,
this
.
id
);
eventHub
.
$emit
(
'installApplication'
,
{
id
:
this
.
id
,
params
:
this
.
installApplicationRequestParams
,
});
},
},
};
...
...
app/assets/javascripts/clusters/components/applications.vue
View file @
e1d4deb2
...
...
@@ -37,11 +37,6 @@ export default {
default
:
''
,
},
},
data
()
{
return
{
jupyterSuggestHostnameValue
:
''
,
};
},
computed
:
{
generalApplicationDescription
()
{
return
sprintf
(
...
...
@@ -132,14 +127,6 @@ export default {
jupyterHostname
()
{
return
this
.
applications
.
jupyter
.
hostname
;
},
jupyterSuggestHostname
()
{
return
`jupyter.
${
this
.
applications
.
ingress
.
externalIp
}
.xip.io`
;
},
},
watch
:
{
jupyterSuggestHostname
()
{
this
.
jupyterSuggestHostnameValue
=
this
.
jupyterSuggestHostname
;
},
},
};
</
script
>
...
...
@@ -305,6 +292,8 @@ export default {
:status-reason=
"applications.jupyter.statusReason"
:request-status=
"applications.jupyter.requestStatus"
:request-reason=
"applications.jupyter.requestReason"
:disable-install-button=
"!ingressInstalled"
:install-application-request-params=
"{ hostname: applications.jupyter.hostname }"
>
<div
slot=
"description"
>
<p>
...
...
@@ -314,45 +303,23 @@ export default {
notebooks to a class of students, a corporate data science group,
or a scientific research group.`) }}
</p>
<
template
v-if=
"jupyterInstalled"
>
<div
class=
"form-group"
>
<label
for=
"jupyter-hostname"
>
{{
s__
(
'ClusterIntegration|Jupyter Hostname'
)
}}
</label>
<div
v-if=
"jupyterHostname"
class=
"input-group"
>
<input
type=
"text"
id=
"jupyter-hostname"
class=
"form-control js-hostname"
:value=
"jupyterHostname"
readonly
/>
<span
class=
"input-group-btn"
>
<clipboard-button
:text=
"jupyterHostname"
:title=
"s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
class=
"js-clipboard-btn"
/>
</span>
</div>
</div>
</
template
>
<
template
v-else-if=
"ingressInstalled"
>
<
template
v-if=
"ingressInstalled"
>
<div
class=
"form-group"
>
<label
for=
"jupyter-hostname"
>
{{
s__
(
'ClusterIntegration|Jupyter Hostname'
)
}}
</label>
<div
class=
"input-group"
>
<input
type=
"text"
id=
"jupyter-hostname"
class=
"form-control js-hostname"
v-model=
"jupyterSuggestHostnameValue"
v-model=
"applications.jupyter.hostname"
:readonly=
"jupyterInstalled"
/>
<span
class=
"input-group-btn"
>
<span
class=
"input-group-btn"
>
<clipboard-button
:text=
"jupyterHostname"
:title=
"s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
...
...
@@ -361,7 +328,7 @@ export default {
</span>
</div>
</div>
<p>
<p
v-if=
"ingressInstalled"
>
{{
s__
(
`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`
)
}}
<a
...
...
app/assets/javascripts/clusters/services/clusters_service.js
View file @
e1d4deb2
import
axios
from
'../../lib/utils/axios_utils'
;
import
{
JUPYTER
}
from
'../constants'
;
export
default
class
ClusterService
{
constructor
(
options
=
{})
{
...
...
@@ -17,14 +16,8 @@ export default class ClusterService {
return
axios
.
get
(
this
.
options
.
endpoint
);
}
installApplication
(
appId
)
{
const
data
=
{};
if
(
appId
===
JUPYTER
)
{
data
.
hostname
=
document
.
getElementById
(
'jupyter-hostname'
).
value
;
}
return
axios
.
post
(
this
.
appInstallEndpointMap
[
appId
],
data
);
installApplication
(
appId
,
params
)
{
return
axios
.
post
(
this
.
appInstallEndpointMap
[
appId
],
params
);
}
static
updateCluster
(
endpoint
,
data
)
{
...
...
app/assets/javascripts/clusters/stores/clusters_store.js
View file @
e1d4deb2
...
...
@@ -92,7 +92,7 @@ export default class ClusterStore {
if
(
appId
===
INGRESS
)
{
this
.
state
.
applications
.
ingress
.
externalIp
=
serverAppEntry
.
external_ip
;
}
else
if
(
appId
===
JUPYTER
)
{
this
.
state
.
applications
.
jupyter
.
hostname
=
serverAppEntry
.
hostname
;
this
.
state
.
applications
.
jupyter
.
hostname
=
serverAppEntry
.
hostname
||
this
.
state
.
applications
.
ingress
.
externalIp
?
`jupyter.
${
this
.
state
.
applications
.
ingress
.
externalIp
}
.xip.io`
:
''
;
}
});
}
...
...
spec/javascripts/clusters/clusters_bundle_spec.js
View file @
e1d4deb2
...
...
@@ -207,11 +207,11 @@ describe('Clusters', () => {
spyOn
(
cluster
.
service
,
'installApplication'
).
and
.
returnValue
(
Promise
.
resolve
());
expect
(
cluster
.
store
.
state
.
applications
.
helm
.
requestStatus
).
toEqual
(
null
);
cluster
.
installApplication
(
'helm'
);
cluster
.
installApplication
(
{
id
:
'helm'
}
);
expect
(
cluster
.
store
.
state
.
applications
.
helm
.
requestStatus
).
toEqual
(
REQUEST_LOADING
);
expect
(
cluster
.
store
.
state
.
applications
.
helm
.
requestReason
).
toEqual
(
null
);
expect
(
cluster
.
service
.
installApplication
).
toHaveBeenCalledWith
(
'helm'
);
expect
(
cluster
.
service
.
installApplication
).
toHaveBeenCalledWith
(
'helm'
,
undefined
);
getSetTimeoutPromise
()
.
then
(()
=>
{
...
...
@@ -226,11 +226,11 @@ describe('Clusters', () => {
spyOn
(
cluster
.
service
,
'installApplication'
).
and
.
returnValue
(
Promise
.
resolve
());
expect
(
cluster
.
store
.
state
.
applications
.
ingress
.
requestStatus
).
toEqual
(
null
);
cluster
.
installApplication
(
'ingress'
);
cluster
.
installApplication
(
{
id
:
'ingress'
}
);
expect
(
cluster
.
store
.
state
.
applications
.
ingress
.
requestStatus
).
toEqual
(
REQUEST_LOADING
);
expect
(
cluster
.
store
.
state
.
applications
.
ingress
.
requestReason
).
toEqual
(
null
);
expect
(
cluster
.
service
.
installApplication
).
toHaveBeenCalledWith
(
'ingress'
);
expect
(
cluster
.
service
.
installApplication
).
toHaveBeenCalledWith
(
'ingress'
,
undefined
);
getSetTimeoutPromise
()
.
then
(()
=>
{
...
...
@@ -245,11 +245,11 @@ describe('Clusters', () => {
spyOn
(
cluster
.
service
,
'installApplication'
).
and
.
returnValue
(
Promise
.
resolve
());
expect
(
cluster
.
store
.
state
.
applications
.
runner
.
requestStatus
).
toEqual
(
null
);
cluster
.
installApplication
(
'runner'
);
cluster
.
installApplication
(
{
id
:
'runner'
}
);
expect
(
cluster
.
store
.
state
.
applications
.
runner
.
requestStatus
).
toEqual
(
REQUEST_LOADING
);
expect
(
cluster
.
store
.
state
.
applications
.
runner
.
requestReason
).
toEqual
(
null
);
expect
(
cluster
.
service
.
installApplication
).
toHaveBeenCalledWith
(
'runner'
);
expect
(
cluster
.
service
.
installApplication
).
toHaveBeenCalledWith
(
'runner'
,
undefined
);
getSetTimeoutPromise
()
.
then
(()
=>
{
...
...
@@ -260,11 +260,29 @@ describe('Clusters', () => {
.
catch
(
done
.
fail
);
});
it
(
'tries to install jupyter'
,
(
done
)
=>
{
spyOn
(
cluster
.
service
,
'installApplication'
).
and
.
returnValue
(
Promise
.
resolve
());
expect
(
cluster
.
store
.
state
.
applications
.
jupyter
.
requestStatus
).
toEqual
(
null
);
cluster
.
installApplication
({
id
:
'jupyter'
,
params
:
{
hostname
:
cluster
.
store
.
state
.
applications
.
jupyter
.
hostname
}
});
expect
(
cluster
.
store
.
state
.
applications
.
jupyter
.
requestStatus
).
toEqual
(
REQUEST_LOADING
);
expect
(
cluster
.
store
.
state
.
applications
.
jupyter
.
requestReason
).
toEqual
(
null
);
expect
(
cluster
.
service
.
installApplication
).
toHaveBeenCalledWith
(
'jupyter'
,
{
hostname
:
cluster
.
store
.
state
.
applications
.
jupyter
.
hostname
});
getSetTimeoutPromise
()
.
then
(()
=>
{
expect
(
cluster
.
store
.
state
.
applications
.
jupyter
.
requestStatus
).
toEqual
(
REQUEST_SUCCESS
);
expect
(
cluster
.
store
.
state
.
applications
.
jupyter
.
requestReason
).
toEqual
(
null
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'sets error request status when the request fails'
,
(
done
)
=>
{
spyOn
(
cluster
.
service
,
'installApplication'
).
and
.
returnValue
(
Promise
.
reject
(
new
Error
(
'STUBBED ERROR'
)));
expect
(
cluster
.
store
.
state
.
applications
.
helm
.
requestStatus
).
toEqual
(
null
);
cluster
.
installApplication
(
'helm'
);
cluster
.
installApplication
(
{
id
:
'helm'
}
);
expect
(
cluster
.
store
.
state
.
applications
.
helm
.
requestStatus
).
toEqual
(
REQUEST_LOADING
);
expect
(
cluster
.
store
.
state
.
applications
.
helm
.
requestReason
).
toEqual
(
null
);
...
...
spec/javascripts/clusters/components/application_row_spec.js
View file @
e1d4deb2
...
...
@@ -174,7 +174,27 @@ describe('Application Row', () => {
installButton
.
click
();
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'installApplication'
,
DEFAULT_APPLICATION_STATE
.
id
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'installApplication'
,
{
id
:
DEFAULT_APPLICATION_STATE
.
id
,
params
:
{},
});
});
it
(
'clicking install button when installApplicationRequestParams are provided emits event'
,
()
=>
{
spyOn
(
eventHub
,
'$emit'
);
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_INSTALLABLE
,
installApplicationRequestParams
:
{
hostname
:
'jupyter'
},
});
const
installButton
=
vm
.
$el
.
querySelector
(
'.js-cluster-application-install-button'
);
installButton
.
click
();
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'installApplication'
,
{
id
:
DEFAULT_APPLICATION_STATE
.
id
,
params
:
{
hostname
:
'jupyter'
},
});
});
it
(
'clicking disabled install button emits nothing'
,
()
=>
{
...
...
@@ -191,6 +211,16 @@ describe('Application Row', () => {
expect
(
eventHub
.
$emit
).
not
.
toHaveBeenCalled
();
});
it
(
'is disabled when disableInstallButton prop is provided'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_INSTALLING
,
disableInstallButton
:
true
,
});
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
});
});
describe
(
'Error block'
,
()
=>
{
...
...
spec/javascripts/clusters/components/applications_spec.js
View file @
e1d4deb2
...
...
@@ -22,6 +22,7 @@ describe('Applications', () => {
ingress
:
{
title
:
'Ingress'
},
runner
:
{
title
:
'GitLab Runner'
},
prometheus
:
{
title
:
'Prometheus'
},
jupyter
:
{
title
:
'JupyterHub'
},
},
});
});
...
...
@@ -41,6 +42,10 @@ describe('Applications', () => {
it
(
'renders a row for GitLab Runner'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'.js-cluster-application-row-runner'
)).
toBeDefined
();
});
it
(
'renders a row for Jupyter'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'.js-cluster-application-row-jupyter'
)).
not
.
toBe
(
null
);
});
});
describe
(
'Ingress application'
,
()
=>
{
...
...
@@ -57,12 +62,11 @@ describe('Applications', () => {
helm
:
{
title
:
'Helm Tiller'
},
runner
:
{
title
:
'GitLab Runner'
},
prometheus
:
{
title
:
'Prometheus'
},
jupyter
:
{
title
:
'JupyterHub'
,
hostname
:
''
},
},
});
expect
(
vm
.
$el
.
querySelector
(
'.js-ip-address'
).
value
,
).
toEqual
(
'0.0.0.0'
);
expect
(
vm
.
$el
.
querySelector
(
'.js-ip-address'
).
value
).
toEqual
(
'0.0.0.0'
);
expect
(
vm
.
$el
.
querySelector
(
'.js-clipboard-btn'
).
getAttribute
(
'data-clipboard-text'
),
...
...
@@ -81,12 +85,11 @@ describe('Applications', () => {
helm
:
{
title
:
'Helm Tiller'
},
runner
:
{
title
:
'GitLab Runner'
},
prometheus
:
{
title
:
'Prometheus'
},
jupyter
:
{
title
:
'JupyterHub'
,
hostname
:
''
},
},
});
expect
(
vm
.
$el
.
querySelector
(
'.js-ip-address'
).
value
,
).
toEqual
(
'?'
);
expect
(
vm
.
$el
.
querySelector
(
'.js-ip-address'
).
value
).
toEqual
(
'?'
);
expect
(
vm
.
$el
.
querySelector
(
'.js-no-ip-message'
)).
not
.
toBe
(
null
);
});
...
...
@@ -101,6 +104,7 @@ describe('Applications', () => {
ingress
:
{
title
:
'Ingress'
},
runner
:
{
title
:
'GitLab Runner'
},
prometheus
:
{
title
:
'Prometheus'
},
jupyter
:
{
title
:
'JupyterHub'
,
hostname
:
''
},
},
});
...
...
@@ -108,5 +112,66 @@ describe('Applications', () => {
expect
(
vm
.
$el
.
querySelector
(
'.js-ip-address'
)).
toBe
(
null
);
});
});
describe
(
'Jupyter application'
,
()
=>
{
describe
(
'with ingress installed & jupyter not installed'
,
()
=>
{
it
(
'renders hostname active input'
,
()
=>
{
vm
=
mountComponent
(
Applications
,
{
applications
:
{
helm
:
{
title
:
'Helm Tiller'
,
status
:
'installed'
},
ingress
:
{
title
:
'Ingress'
,
status
:
'installed'
,
externalIp
:
'1.1.1.1'
},
runner
:
{
title
:
'GitLab Runner'
},
prometheus
:
{
title
:
'Prometheus'
},
jupyter
:
{
title
:
'JupyterHub'
,
hostname
:
''
},
},
});
expect
(
vm
.
$el
.
querySelector
(
'.js-hostname'
).
getAttribute
(
'readonly'
)).
toEqual
(
null
);
});
describe
(
'with ingress & jupyter installed'
,
()
=>
{
it
(
'renders readonly input'
,
()
=>
{
vm
=
mountComponent
(
Applications
,
{
applications
:
{
helm
:
{
title
:
'Helm Tiller'
,
status
:
'installed'
},
ingress
:
{
title
:
'Ingress'
,
status
:
'installed'
,
externalIp
:
'1.1.1.1'
},
runner
:
{
title
:
'GitLab Runner'
},
prometheus
:
{
title
:
'Prometheus'
},
jupyter
:
{
title
:
'JupyterHub'
,
status
:
'installed'
,
hostname
:
''
},
},
});
expect
(
vm
.
$el
.
querySelector
(
'.js-hostname'
).
getAttribute
(
'readonly'
)).
toEqual
(
'readonly'
);
});
});
});
describe
(
'without ingress installed'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
mountComponent
(
Applications
,
{
applications
:
{
helm
:
{
title
:
'Helm Tiller'
},
ingress
:
{
title
:
'Ingress'
},
runner
:
{
title
:
'GitLab Runner'
},
prometheus
:
{
title
:
'Prometheus'
},
jupyter
:
{
title
:
'JupyterHub'
},
},
});
});
it
(
'does not render input'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'.js-hostname'
)).
toBe
(
null
);
});
it
(
'renders disabled install button'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'.js-cluster-application-row-jupyter .js-cluster-application-install-button'
,
)
.
getAttribute
(
'disabled'
),
).
toEqual
(
'disabled'
);
});
});
});
});
});
spec/javascripts/clusters/services/mock_data.js
View file @
e1d4deb2
import
{
APPLICATION_INSTALLED
,
APPLICATION_INSTALLABLE
,
APPLICATION_INSTALLING
,
APPLICATION_ERROR
,
...
...
@@ -28,6 +29,39 @@ const CLUSTERS_MOCK_DATA = {
name
:
'prometheus'
,
status
:
APPLICATION_ERROR
,
status_reason
:
'Cannot connect'
,
},
{
name
:
'jupyter'
,
status
:
APPLICATION_INSTALLING
,
status_reason
:
'Cannot connect'
,
}],
},
},
'/gitlab-org/gitlab-shell/clusters/2/status.json'
:
{
data
:
{
status
:
'errored'
,
status_reason
:
'Failed to request to CloudPlatform.'
,
applications
:
[{
name
:
'helm'
,
status
:
APPLICATION_INSTALLED
,
status_reason
:
null
,
},
{
name
:
'ingress'
,
status
:
APPLICATION_INSTALLED
,
status_reason
:
'Cannot connect'
,
external_ip
:
'1.1.1.1'
,
},
{
name
:
'runner'
,
status
:
APPLICATION_INSTALLING
,
status_reason
:
null
,
},
{
name
:
'prometheus'
,
status
:
APPLICATION_ERROR
,
status_reason
:
'Cannot connect'
,
},
{
name
:
'jupyter'
,
status
:
APPLICATION_INSTALLABLE
,
status_reason
:
'Cannot connect'
,
}],
},
},
...
...
@@ -37,6 +71,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress'
:
{
},
'/gitlab-org/gitlab-shell/clusters/1/applications/runner'
:
{
},
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus'
:
{
},
'/gitlab-org/gitlab-shell/clusters/1/applications/jupyter'
:
{
},
},
};
...
...
spec/javascripts/clusters/stores/clusters_store_spec.js
View file @
e1d4deb2
...
...
@@ -91,8 +91,26 @@ describe('Clusters Store', () => {
requestStatus
:
null
,
requestReason
:
null
,
},
jupyter
:
{
title
:
'JupyterHub'
,
status
:
mockResponseData
.
applications
[
4
].
status
,
statusReason
:
mockResponseData
.
applications
[
4
].
status_reason
,
requestStatus
:
null
,
requestReason
:
null
,
hostname
:
''
,
},
},
});
});
it
(
'sets default hostname for jupyter when ingress has a ip address'
,
()
=>
{
const
mockResponseData
=
CLUSTERS_MOCK_DATA
.
GET
[
'/gitlab-org/gitlab-shell/clusters/2/status.json'
].
data
;
store
.
updateStateFromServer
(
mockResponseData
);
expect
(
store
.
state
.
applications
.
jupyter
.
hostname
,
).
toEqual
(
`jupyter.
${
store
.
state
.
applications
.
ingress
.
externalIp
}
.xip.io`
);
});
});
});
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