Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Léo-Paul Géneau
gitlab-ce
Commits
f4fb0340
Commit
f4fb0340
authored
Nov 06, 2017
by
Eric Eastwood
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add FE tests for not_installable/scheduled and cluster banner rules
parent
895b6e5d
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
117 additions
and
34 deletions
+117
-34
app/assets/javascripts/clusters/clusters_bundle.js
app/assets/javascripts/clusters/clusters_bundle.js
+28
-20
app/assets/javascripts/clusters/components/application_row.vue
...ssets/javascripts/clusters/components/application_row.vue
+5
-1
app/assets/javascripts/clusters/constants.js
app/assets/javascripts/clusters/constants.js
+1
-1
app/assets/javascripts/clusters/services/clusters_service.js
app/assets/javascripts/clusters/services/clusters_service.js
+2
-0
spec/javascripts/clusters/clusters_bundle_spec.js
spec/javascripts/clusters/clusters_bundle_spec.js
+45
-3
spec/javascripts/clusters/components/application_row_spec.js
spec/javascripts/clusters/components/application_row_spec.js
+33
-9
spec/javascripts/clusters/stores/clusters_store_spec.js
spec/javascripts/clusters/stores/clusters_store_spec.js
+3
-0
No files found.
app/assets/javascripts/clusters/clusters_bundle.js
View file @
f4fb0340
...
@@ -28,6 +28,8 @@ export default class Clusters {
...
@@ -28,6 +28,8 @@ export default class Clusters {
const
{
const
{
statusPath
,
statusPath
,
installHelmPath
,
installHelmPath
,
installIngressPath
,
installRunnerPath
,
clusterStatus
,
clusterStatus
,
clusterStatusReason
,
clusterStatusReason
,
helpPath
,
helpPath
,
...
@@ -40,6 +42,8 @@ export default class Clusters {
...
@@ -40,6 +42,8 @@ export default class Clusters {
this
.
service
=
new
ClustersService
({
this
.
service
=
new
ClustersService
({
endpoint
:
statusPath
,
endpoint
:
statusPath
,
installHelmEndpoint
:
installHelmPath
,
installHelmEndpoint
:
installHelmPath
,
installIngresEndpoint
:
installIngressPath
,
installRunnerEndpoint
:
installRunnerPath
,
});
});
this
.
toggle
=
this
.
toggle
.
bind
(
this
);
this
.
toggle
=
this
.
toggle
.
bind
(
this
);
...
@@ -57,7 +61,7 @@ export default class Clusters {
...
@@ -57,7 +61,7 @@ export default class Clusters {
this
.
initApplications
();
this
.
initApplications
();
if
(
this
.
store
.
state
.
status
!==
'
created
'
)
{
if
(
this
.
store
.
state
.
status
!==
'
created
'
)
{
this
.
updateContainer
(
this
.
store
.
state
.
status
,
this
.
store
.
state
.
statusReason
);
this
.
updateContainer
(
null
,
this
.
store
.
state
.
status
,
this
.
store
.
state
.
statusReason
);
}
}
this
.
addListeners
();
this
.
addListeners
();
...
@@ -131,13 +135,13 @@ export default class Clusters {
...
@@ -131,13 +135,13 @@ export default class Clusters {
}
}
handleSuccess
(
data
)
{
handleSuccess
(
data
)
{
const
prevApplicationMap
=
Object
.
assign
({},
this
.
store
.
state
.
applications
);
const
prevStatus
=
this
.
store
.
state
.
status
;
const
prevStatus
=
this
.
store
.
state
.
status
;
const
prevApplicationMap
=
Object
.
assign
({},
this
.
store
.
state
.
applications
);
this
.
store
.
updateStateFromServer
(
data
.
data
);
this
.
store
.
updateStateFromServer
(
data
.
data
);
this
.
checkForNewInstalls
(
prevApplicationMap
,
this
.
store
.
state
.
applications
);
this
.
checkForNewInstalls
(
prevApplicationMap
,
this
.
store
.
state
.
applications
);
if
(
prevStatus
.
length
==
0
||
prevStatus
!==
this
.
store
.
state
.
status
)
{
this
.
updateContainer
(
prevStatus
,
this
.
store
.
state
.
status
,
this
.
store
.
state
.
statusReason
);
this
.
updateContainer
(
this
.
store
.
state
.
status
,
this
.
store
.
state
.
statusReason
);
}
}
}
toggle
()
{
toggle
()
{
...
@@ -168,8 +172,11 @@ export default class Clusters {
...
@@ -168,8 +172,11 @@ export default class Clusters {
}
}
}
}
updateContainer
(
status
,
error
)
{
updateContainer
(
prevStatus
,
status
,
error
)
{
this
.
hideAll
();
this
.
hideAll
();
// We poll all the time but only want the `created` banner to show when newly created
if
(
this
.
store
.
state
.
status
!==
'
created
'
||
prevStatus
!==
this
.
store
.
state
.
status
)
{
switch
(
status
)
{
switch
(
status
)
{
case
'
created
'
:
case
'
created
'
:
this
.
successContainer
.
classList
.
remove
(
'
hidden
'
);
this
.
successContainer
.
classList
.
remove
(
'
hidden
'
);
...
@@ -186,6 +193,7 @@ export default class Clusters {
...
@@ -186,6 +193,7 @@ export default class Clusters {
this
.
hideAll
();
this
.
hideAll
();
}
}
}
}
}
installApplication
(
appId
)
{
installApplication
(
appId
)
{
this
.
store
.
updateAppProperty
(
appId
,
'
requestStatus
'
,
REQUEST_LOADING
);
this
.
store
.
updateAppProperty
(
appId
,
'
requestStatus
'
,
REQUEST_LOADING
);
...
...
app/assets/javascripts/clusters/components/application_row.vue
View file @
f4fb0340
...
@@ -75,7 +75,11 @@ export default {
...
@@ -75,7 +75,11 @@ export default {
},
},
installButtonLabel
()
{
installButtonLabel
()
{
let
label
;
let
label
;
if
(
this
.
status
===
APPLICATION_INSTALLABLE
||
this
.
status
===
APPLICATION_ERROR
||
this
.
status
===
APPLICATION_NOT_INSTALLABLE
)
{
if
(
this
.
status
===
APPLICATION_NOT_INSTALLABLE
||
this
.
status
===
APPLICATION_INSTALLABLE
||
this
.
status
===
APPLICATION_ERROR
)
{
label
=
s__
(
'
ClusterIntegration|Install
'
);
label
=
s__
(
'
ClusterIntegration|Install
'
);
}
else
if
(
this
.
status
===
APPLICATION_SCHEDULED
||
this
.
status
===
APPLICATION_INSTALLING
)
{
}
else
if
(
this
.
status
===
APPLICATION_SCHEDULED
||
this
.
status
===
APPLICATION_INSTALLING
)
{
label
=
s__
(
'
ClusterIntegration|Installing
'
);
label
=
s__
(
'
ClusterIntegration|Installing
'
);
...
...
app/assets/javascripts/clusters/constants.js
View file @
f4fb0340
...
@@ -4,7 +4,7 @@ export const APPLICATION_INSTALLABLE = 'installable';
...
@@ -4,7 +4,7 @@ export const APPLICATION_INSTALLABLE = 'installable';
export
const
APPLICATION_SCHEDULED
=
'
scheduled
'
;
export
const
APPLICATION_SCHEDULED
=
'
scheduled
'
;
export
const
APPLICATION_INSTALLING
=
'
installing
'
;
export
const
APPLICATION_INSTALLING
=
'
installing
'
;
export
const
APPLICATION_INSTALLED
=
'
installed
'
;
export
const
APPLICATION_INSTALLED
=
'
installed
'
;
export
const
APPLICATION_ERROR
=
'
error
'
;
export
const
APPLICATION_ERROR
=
'
error
ed
'
;
// These are only used client-side
// These are only used client-side
export
const
REQUEST_LOADING
=
'
request-loading
'
;
export
const
REQUEST_LOADING
=
'
request-loading
'
;
...
...
app/assets/javascripts/clusters/services/clusters_service.js
View file @
f4fb0340
...
@@ -8,6 +8,8 @@ export default class ClusterService {
...
@@ -8,6 +8,8 @@ export default class ClusterService {
this
.
options
=
options
;
this
.
options
=
options
;
this
.
appInstallEndpointMap
=
{
this
.
appInstallEndpointMap
=
{
helm
:
this
.
options
.
installHelmEndpoint
,
helm
:
this
.
options
.
installHelmEndpoint
,
ingress
:
this
.
options
.
installIngressEndpoint
,
runner
:
this
.
options
.
installRunnerEndpoint
,
};
};
}
}
...
...
spec/javascripts/clusters/clusters_bundle_spec.js
View file @
f4fb0340
...
@@ -104,7 +104,21 @@ describe('Clusters', () => {
...
@@ -104,7 +104,21 @@ describe('Clusters', () => {
describe
(
'
updateContainer
'
,
()
=>
{
describe
(
'
updateContainer
'
,
()
=>
{
describe
(
'
when creating cluster
'
,
()
=>
{
describe
(
'
when creating cluster
'
,
()
=>
{
it
(
'
should show the creating container
'
,
()
=>
{
it
(
'
should show the creating container
'
,
()
=>
{
cluster
.
updateContainer
(
'
creating
'
);
cluster
.
updateContainer
(
null
,
'
creating
'
);
expect
(
cluster
.
creatingContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeFalsy
();
expect
(
cluster
.
successContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeTruthy
();
expect
(
cluster
.
errorContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeTruthy
();
});
it
(
'
should continue to show `creating` banner with subsequent updates of the same status
'
,
()
=>
{
cluster
.
updateContainer
(
'
creating
'
,
'
creating
'
);
expect
(
expect
(
cluster
.
creatingContainer
.
classList
.
contains
(
'
hidden
'
),
cluster
.
creatingContainer
.
classList
.
contains
(
'
hidden
'
),
...
@@ -120,7 +134,7 @@ describe('Clusters', () => {
...
@@ -120,7 +134,7 @@ describe('Clusters', () => {
describe
(
'
when cluster is created
'
,
()
=>
{
describe
(
'
when cluster is created
'
,
()
=>
{
it
(
'
should show the success container
'
,
()
=>
{
it
(
'
should show the success container
'
,
()
=>
{
cluster
.
updateContainer
(
'
created
'
);
cluster
.
updateContainer
(
null
,
'
created
'
);
expect
(
expect
(
cluster
.
creatingContainer
.
classList
.
contains
(
'
hidden
'
),
cluster
.
creatingContainer
.
classList
.
contains
(
'
hidden
'
),
...
@@ -132,11 +146,25 @@ describe('Clusters', () => {
...
@@ -132,11 +146,25 @@ describe('Clusters', () => {
cluster
.
errorContainer
.
classList
.
contains
(
'
hidden
'
),
cluster
.
errorContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeTruthy
();
).
toBeTruthy
();
});
});
it
(
'
should not show a banner when status is already `created`
'
,
()
=>
{
cluster
.
updateContainer
(
'
created
'
,
'
created
'
);
expect
(
cluster
.
creatingContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeTruthy
();
expect
(
cluster
.
successContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeTruthy
();
expect
(
cluster
.
errorContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeTruthy
();
});
});
});
describe
(
'
when cluster has error
'
,
()
=>
{
describe
(
'
when cluster has error
'
,
()
=>
{
it
(
'
should show the error container
'
,
()
=>
{
it
(
'
should show the error container
'
,
()
=>
{
cluster
.
updateContainer
(
'
errored
'
,
'
this is an error
'
);
cluster
.
updateContainer
(
null
,
'
errored
'
,
'
this is an error
'
);
expect
(
expect
(
cluster
.
creatingContainer
.
classList
.
contains
(
'
hidden
'
),
cluster
.
creatingContainer
.
classList
.
contains
(
'
hidden
'
),
...
@@ -152,6 +180,20 @@ describe('Clusters', () => {
...
@@ -152,6 +180,20 @@ describe('Clusters', () => {
cluster
.
errorReasonContainer
.
textContent
,
cluster
.
errorReasonContainer
.
textContent
,
).
toContain
(
'
this is an error
'
);
).
toContain
(
'
this is an error
'
);
});
});
it
(
'
should show `error` banner when previously `creating`
'
,
()
=>
{
cluster
.
updateContainer
(
'
creating
'
,
'
errored
'
);
expect
(
cluster
.
creatingContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeTruthy
();
expect
(
cluster
.
successContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeTruthy
();
expect
(
cluster
.
errorContainer
.
classList
.
contains
(
'
hidden
'
),
).
toBeFalsy
();
});
});
});
});
});
...
...
spec/javascripts/clusters/components/application_row_spec.js
View file @
f4fb0340
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
eventHub
from
'
~/clusters/event_hub
'
;
import
eventHub
from
'
~/clusters/event_hub
'
;
import
{
import
{
APPLICATION_NOT_INSTALLABLE
,
APPLICATION_SCHEDULED
,
APPLICATION_INSTALLABLE
,
APPLICATION_INSTALLABLE
,
APPLICATION_INSTALLING
,
APPLICATION_INSTALLING
,
APPLICATION_INSTALLED
,
APPLICATION_INSTALLED
,
...
@@ -60,7 +62,18 @@ describe('Application Row', () => {
...
@@ -60,7 +62,18 @@ describe('Application Row', () => {
expect
(
vm
.
installButtonLabel
).
toBeUndefined
();
expect
(
vm
.
installButtonLabel
).
toBeUndefined
();
});
});
it
(
'
has enabled "Install" when `status=installable`
'
,
()
=>
{
it
(
'
has disabled "Install" when APPLICATION_NOT_INSTALLABLE
'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_NOT_INSTALLABLE
,
});
expect
(
vm
.
installButtonLabel
).
toEqual
(
'
Install
'
);
expect
(
vm
.
installButtonLoading
).
toEqual
(
false
);
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
});
it
(
'
has enabled "Install" when APPLICATION_INSTALLABLE
'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_INSTALLABLE
,
status
:
APPLICATION_INSTALLABLE
,
...
@@ -71,7 +84,18 @@ describe('Application Row', () => {
...
@@ -71,7 +84,18 @@ describe('Application Row', () => {
expect
(
vm
.
installButtonDisabled
).
toEqual
(
false
);
expect
(
vm
.
installButtonDisabled
).
toEqual
(
false
);
});
});
it
(
'
has loading "Installing" when `status=installing`
'
,
()
=>
{
it
(
'
has loading "Installing" when APPLICATION_SCHEDULED
'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_SCHEDULED
,
});
expect
(
vm
.
installButtonLabel
).
toEqual
(
'
Installing
'
);
expect
(
vm
.
installButtonLoading
).
toEqual
(
true
);
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
});
it
(
'
has loading "Installing" when APPLICATION_INSTALLING
'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_INSTALLING
,
status
:
APPLICATION_INSTALLING
,
...
@@ -82,7 +106,7 @@ describe('Application Row', () => {
...
@@ -82,7 +106,7 @@ describe('Application Row', () => {
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
});
});
it
(
'
has disabled "Installed" when
`status=installed`
'
,
()
=>
{
it
(
'
has disabled "Installed" when
APPLICATION_INSTALLED
'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_INSTALLED
,
status
:
APPLICATION_INSTALLED
,
...
@@ -93,7 +117,7 @@ describe('Application Row', () => {
...
@@ -93,7 +117,7 @@ describe('Application Row', () => {
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
});
});
it
(
'
has disabled "Install" when
`status=error`
'
,
()
=>
{
it
(
'
has disabled "Install" when
APPLICATION_ERROR
'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_ERROR
,
status
:
APPLICATION_ERROR
,
...
@@ -104,7 +128,7 @@ describe('Application Row', () => {
...
@@ -104,7 +128,7 @@ describe('Application Row', () => {
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
});
});
it
(
'
has loading "Install" when
`requestStatus=loading`
'
,
()
=>
{
it
(
'
has loading "Install" when
REQUEST_LOADING
'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_INSTALLABLE
,
status
:
APPLICATION_INSTALLABLE
,
...
@@ -116,7 +140,7 @@ describe('Application Row', () => {
...
@@ -116,7 +140,7 @@ describe('Application Row', () => {
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
});
});
it
(
'
has disabled "Install" when
`requestStatus=success`
'
,
()
=>
{
it
(
'
has disabled "Install" when
REQUEST_SUCCESS
'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_INSTALLABLE
,
status
:
APPLICATION_INSTALLABLE
,
...
@@ -128,7 +152,7 @@ describe('Application Row', () => {
...
@@ -128,7 +152,7 @@ describe('Application Row', () => {
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
expect
(
vm
.
installButtonDisabled
).
toEqual
(
true
);
});
});
it
(
'
has enabled "Install" when
`requestStatus=error`
(so you can try installing again)
'
,
()
=>
{
it
(
'
has enabled "Install" when
REQUEST_FAILURE
(so you can try installing again)
'
,
()
=>
{
vm
=
mountComponent
(
ApplicationRow
,
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
...
DEFAULT_APPLICATION_STATE
,
status
:
APPLICATION_INSTALLABLE
,
status
:
APPLICATION_INSTALLABLE
,
...
@@ -181,7 +205,7 @@ describe('Application Row', () => {
...
@@ -181,7 +205,7 @@ describe('Application Row', () => {
expect
(
generalErrorMessage
).
toBeNull
();
expect
(
generalErrorMessage
).
toBeNull
();
});
});
it
(
'
shows status reason when
`status=error`
'
,
()
=>
{
it
(
'
shows status reason when
APPLICATION_ERROR
'
,
()
=>
{
const
statusReason
=
'
We broke it 0.0
'
;
const
statusReason
=
'
We broke it 0.0
'
;
vm
=
mountComponent
(
ApplicationRow
,
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
...
DEFAULT_APPLICATION_STATE
,
...
@@ -195,7 +219,7 @@ describe('Application Row', () => {
...
@@ -195,7 +219,7 @@ describe('Application Row', () => {
expect
(
statusErrorMessage
.
textContent
.
trim
()).
toEqual
(
statusReason
);
expect
(
statusErrorMessage
.
textContent
.
trim
()).
toEqual
(
statusReason
);
});
});
it
(
'
shows request reason when
`requestStatus=error`
'
,
()
=>
{
it
(
'
shows request reason when
REQUEST_FAILURE
'
,
()
=>
{
const
requestReason
=
'
We broke thre request 0.0
'
;
const
requestReason
=
'
We broke thre request 0.0
'
;
vm
=
mountComponent
(
ApplicationRow
,
{
vm
=
mountComponent
(
ApplicationRow
,
{
...
DEFAULT_APPLICATION_STATE
,
...
DEFAULT_APPLICATION_STATE
,
...
...
spec/javascripts/clusters/stores/clusters_store_spec.js
View file @
f4fb0340
...
@@ -62,18 +62,21 @@ describe('Clusters Store', () => {
...
@@ -62,18 +62,21 @@ describe('Clusters Store', () => {
statusReason
:
mockResponseData
.
status_reason
,
statusReason
:
mockResponseData
.
status_reason
,
applications
:
{
applications
:
{
helm
:
{
helm
:
{
title
:
'
Helm Tiller
'
,
status
:
mockResponseData
.
applications
[
0
].
status
,
status
:
mockResponseData
.
applications
[
0
].
status
,
statusReason
:
mockResponseData
.
applications
[
0
].
status_reason
,
statusReason
:
mockResponseData
.
applications
[
0
].
status_reason
,
requestStatus
:
null
,
requestStatus
:
null
,
requestReason
:
null
,
requestReason
:
null
,
},
},
ingress
:
{
ingress
:
{
title
:
'
Ingress
'
,
status
:
mockResponseData
.
applications
[
1
].
status
,
status
:
mockResponseData
.
applications
[
1
].
status
,
statusReason
:
mockResponseData
.
applications
[
1
].
status_reason
,
statusReason
:
mockResponseData
.
applications
[
1
].
status_reason
,
requestStatus
:
null
,
requestStatus
:
null
,
requestReason
:
null
,
requestReason
:
null
,
},
},
runner
:
{
runner
:
{
title
:
'
GitLab Runner
'
,
status
:
mockResponseData
.
applications
[
2
].
status
,
status
:
mockResponseData
.
applications
[
2
].
status
,
statusReason
:
mockResponseData
.
applications
[
2
].
status_reason
,
statusReason
:
mockResponseData
.
applications
[
2
].
status_reason
,
requestStatus
:
null
,
requestStatus
:
null
,
...
...
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