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
1
Merge Requests
1
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
nexedi
gitlab-ce
Commits
3467ff1b
Commit
3467ff1b
authored
Apr 01, 2022
by
Sean Arnold
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add metrics image UI for Alerts
Changelog: added
parent
fc05549a
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
208 additions
and
14 deletions
+208
-14
app/assets/javascripts/api/alert_management_alerts_api.js
app/assets/javascripts/api/alert_management_alerts_api.js
+62
-0
app/assets/javascripts/api/projects_api.js
app/assets/javascripts/api/projects_api.js
+2
-0
app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
...pts/vue_shared/alert_details/components/alert_details.vue
+66
-8
app/assets/javascripts/vue_shared/alert_details/index.js
app/assets/javascripts/vue_shared/alert_details/index.js
+10
-1
app/assets/javascripts/vue_shared/alert_details/service.js
app/assets/javascripts/vue_shared/alert_details/service.js
+43
-0
app/helpers/projects/alert_management_helper.rb
app/helpers/projects/alert_management_helper.rb
+3
-2
app/views/projects/alert_management/details.html.haml
app/views/projects/alert_management/details.html.haml
+1
-1
spec/helpers/projects/alert_management_helper_spec.rb
spec/helpers/projects/alert_management_helper_spec.rb
+21
-2
No files found.
app/assets/javascripts/api/alert_management_alerts_api.js
0 → 100644
View file @
3467ff1b
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
buildApiUrl
}
from
'
~/api/api_utils
'
;
import
{
ContentTypeMultipartFormData
}
from
'
~/lib/utils/headers
'
;
const
ALERT_METRIC_IMAGES_PATH
=
'
/api/:version/projects/:id/alert_management_alerts/:alert_iid/metric_images
'
;
const
ALERT_SINGLE_METRIC_IMAGE_PATH
=
'
/api/:version/projects/:id/alert_management_alerts/:alert_iid/metric_images/:image_id
'
;
export
function
fetchAlertMetricImages
({
alertIid
,
id
})
{
const
metricImagesUrl
=
buildApiUrl
(
ALERT_METRIC_IMAGES_PATH
)
.
replace
(
'
:id
'
,
encodeURIComponent
(
id
))
.
replace
(
'
:alert_iid
'
,
encodeURIComponent
(
alertIid
));
return
axios
.
get
(
metricImagesUrl
);
}
export
function
uploadAlertMetricImage
({
alertIid
,
id
,
file
,
url
=
null
,
urlText
=
null
})
{
const
options
=
{
headers
:
{
...
ContentTypeMultipartFormData
}
};
const
metricImagesUrl
=
buildApiUrl
(
ALERT_METRIC_IMAGES_PATH
)
.
replace
(
'
:id
'
,
encodeURIComponent
(
id
))
.
replace
(
'
:alert_iid
'
,
encodeURIComponent
(
alertIid
));
// Construct multipart form data
const
formData
=
new
FormData
();
formData
.
append
(
'
file
'
,
file
);
if
(
url
)
{
formData
.
append
(
'
url
'
,
url
);
}
if
(
urlText
)
{
formData
.
append
(
'
url_text
'
,
urlText
);
}
return
axios
.
post
(
metricImagesUrl
,
formData
,
options
);
}
export
function
updateAlertMetricImage
({
alertIid
,
id
,
imageId
,
url
=
null
,
urlText
=
null
})
{
const
metricImagesUrl
=
buildApiUrl
(
ALERT_SINGLE_METRIC_IMAGE_PATH
)
.
replace
(
'
:id
'
,
encodeURIComponent
(
id
))
.
replace
(
'
:alert_iid
'
,
encodeURIComponent
(
alertIid
))
.
replace
(
'
:image_id
'
,
encodeURIComponent
(
imageId
));
// Construct multipart form data
const
formData
=
new
FormData
();
if
(
url
!=
null
)
{
formData
.
append
(
'
url
'
,
url
);
}
if
(
urlText
!=
null
)
{
formData
.
append
(
'
url_text
'
,
urlText
);
}
return
axios
.
put
(
metricImagesUrl
,
formData
);
}
export
function
deleteAlertMetricImage
({
alertIid
,
id
,
imageId
})
{
const
individualMetricImageUrl
=
buildApiUrl
(
ALERT_SINGLE_METRIC_IMAGE_PATH
)
.
replace
(
'
:id
'
,
encodeURIComponent
(
id
))
.
replace
(
'
:alert_iid
'
,
encodeURIComponent
(
alertIid
))
.
replace
(
'
:image_id
'
,
encodeURIComponent
(
imageId
));
return
axios
.
delete
(
individualMetricImageUrl
);
}
app/assets/javascripts/api/projects_api.js
View file @
3467ff1b
...
@@ -2,6 +2,8 @@ import { DEFAULT_PER_PAGE } from '~/api';
...
@@ -2,6 +2,8 @@ import { DEFAULT_PER_PAGE } from '~/api';
import
axios
from
'
../lib/utils/axios_utils
'
;
import
axios
from
'
../lib/utils/axios_utils
'
;
import
{
buildApiUrl
}
from
'
./api_utils
'
;
import
{
buildApiUrl
}
from
'
./api_utils
'
;
export
*
from
'
./alert_management_alerts_api
'
;
const
PROJECTS_PATH
=
'
/api/:version/projects.json
'
;
const
PROJECTS_PATH
=
'
/api/:version/projects.json
'
;
const
PROJECT_IMPORT_MEMBERS_PATH
=
'
/api/:version/projects/:id/import_project_members/:project_id
'
;
const
PROJECT_IMPORT_MEMBERS_PATH
=
'
/api/:version/projects/:id/import_project_members/:project_id
'
;
...
...
app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
View file @
3467ff1b
...
@@ -10,23 +10,27 @@ import {
...
@@ -10,23 +10,27 @@ import {
GlTab
,
GlTab
,
GlButton
,
GlButton
,
GlSafeHtmlDirective
,
GlSafeHtmlDirective
,
GlFormGroup
,
GlFormInput
,
GlModal
,
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
highlightCurrentUser
from
'
~/behaviors/markdown/highlight_current_user
'
;
import
highlightCurrentUser
from
'
~/behaviors/markdown/highlight_current_user
'
;
import
{
fetchPolicies
}
from
'
~/lib/graphql
'
;
import
{
fetchPolicies
}
from
'
~/lib/graphql
'
;
import
{
toggleContainerClasses
}
from
'
~/lib/utils/dom_utils
'
;
import
{
toggleContainerClasses
}
from
'
~/lib/utils/dom_utils
'
;
import
{
visitUrl
,
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
import
{
visitUrl
,
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
Tracking
from
'
~/tracking
'
;
import
Tracking
from
'
~/tracking
'
;
import
initUserPopovers
from
'
~/user_popovers
'
;
import
initUserPopovers
from
'
~/user_popovers
'
;
import
AlertDetailsTable
from
'
~/vue_shared/components/alert_details_table.vue
'
;
import
AlertDetailsTable
from
'
~/vue_shared/components/alert_details_table.vue
'
;
import
TimeAgoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
TimeAgoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
MetricImagesTab
from
'
~/vue_shared/components/metric_images/metric_images_tab.vue
'
;
import
{
PAGE_CONFIG
,
SEVERITY_LEVELS
}
from
'
../constants
'
;
import
{
PAGE_CONFIG
,
SEVERITY_LEVELS
}
from
'
../constants
'
;
import
createIssueMutation
from
'
../graphql/mutations/alert_issue_create.mutation.graphql
'
;
import
createIssueMutation
from
'
../graphql/mutations/alert_issue_create.mutation.graphql
'
;
import
toggleSidebarStatusMutation
from
'
../graphql/mutations/alert_sidebar_status.mutation.graphql
'
;
import
toggleSidebarStatusMutation
from
'
../graphql/mutations/alert_sidebar_status.mutation.graphql
'
;
import
alertQuery
from
'
../graphql/queries/alert_sidebar_details.query.graphql
'
;
import
alertQuery
from
'
../graphql/queries/alert_sidebar_details.query.graphql
'
;
import
sidebarStatusQuery
from
'
../graphql/queries/alert_sidebar_status.query.graphql
'
;
import
sidebarStatusQuery
from
'
../graphql/queries/alert_sidebar_status.query.graphql
'
;
import
AlertMetrics
from
'
./alert_metrics.vue
'
;
import
AlertSidebar
from
'
./alert_sidebar.vue
'
;
import
AlertSidebar
from
'
./alert_sidebar.vue
'
;
import
AlertSummaryRow
from
'
./alert_summary_row.vue
'
;
import
AlertSummaryRow
from
'
./alert_summary_row.vue
'
;
import
SystemNote
from
'
./system_notes/system_note.vue
'
;
import
SystemNote
from
'
./system_notes/system_note.vue
'
;
...
@@ -40,6 +44,15 @@ export default {
...
@@ -40,6 +44,15 @@ export default {
),
),
reportedAt
:
s__
(
'
AlertManagement|Reported %{when}
'
),
reportedAt
:
s__
(
'
AlertManagement|Reported %{when}
'
),
reportedAtWithTool
:
s__
(
'
AlertManagement|Reported %{when} by %{tool}
'
),
reportedAtWithTool
:
s__
(
'
AlertManagement|Reported %{when} by %{tool}
'
),
modalUpload
:
__
(
'
Upload
'
),
modalCancel
:
__
(
'
Cancel
'
),
modalTitle
:
s__
(
'
Incidents|Add image details
'
),
modalDescription
:
s__
(
"
Incidents|Add text or a link to display with your image. If you don't add either, the file name displays instead.
"
,
),
dropDescription
:
s__
(
'
Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the alert
'
,
),
},
},
directives
:
{
directives
:
{
SafeHtml
:
GlSafeHtmlDirective
,
SafeHtml
:
GlSafeHtmlDirective
,
...
@@ -70,11 +83,14 @@ export default {
...
@@ -70,11 +83,14 @@ export default {
GlSprintf
,
GlSprintf
,
GlTab
,
GlTab
,
GlTabs
,
GlTabs
,
GlFormGroup
,
GlFormInput
,
GlButton
,
GlButton
,
TimeAgoTooltip
,
TimeAgoTooltip
,
AlertSidebar
,
AlertSidebar
,
SystemNote
,
SystemNote
,
AlertMetrics
,
GlModal
,
MetricImagesTab
,
},
},
inject
:
{
inject
:
{
projectPath
:
{
projectPath
:
{
...
@@ -98,6 +114,9 @@ export default {
...
@@ -98,6 +114,9 @@ export default {
trackAlertsDetailsViewsOptions
:
{
trackAlertsDetailsViewsOptions
:
{
default
:
null
,
default
:
null
,
},
},
canUpdate
:
{
default
:
false
,
},
},
},
apollo
:
{
apollo
:
{
alert
:
{
alert
:
{
...
@@ -130,9 +149,25 @@ export default {
...
@@ -130,9 +149,25 @@ export default {
createIncidentError
:
''
,
createIncidentError
:
''
,
incidentCreationInProgress
:
false
,
incidentCreationInProgress
:
false
,
sidebarErrorMessage
:
''
,
sidebarErrorMessage
:
''
,
currentFiles
:
[],
modalVisible
:
false
,
modalUrl
:
''
,
modalUrlText
:
''
,
};
};
},
},
computed
:
{
computed
:
{
...
mapState
([
'
metricImages
'
,
'
isLoadingMetricImages
'
,
'
isUploadingImage
'
]),
actionPrimaryProps
()
{
return
{
text
:
this
.
$options
.
i18n
.
modalUpload
,
attributes
:
{
loading
:
this
.
isUploadingImage
,
disabled
:
this
.
isUploadingImage
,
category
:
'
primary
'
,
variant
:
'
confirm
'
,
},
};
},
loading
()
{
loading
()
{
return
this
.
$apollo
.
queries
.
alert
.
loading
;
return
this
.
$apollo
.
queries
.
alert
.
loading
;
},
},
...
@@ -179,6 +214,30 @@ export default {
...
@@ -179,6 +214,30 @@ export default {
});
});
},
},
methods
:
{
methods
:
{
clearInputs
()
{
this
.
modalVisible
=
false
;
this
.
modalUrl
=
''
;
this
.
modalUrlText
=
''
;
this
.
currentFile
=
false
;
},
openMetricDialog
(
files
)
{
this
.
modalVisible
=
true
;
this
.
currentFiles
=
files
;
},
async
onUpload
()
{
try
{
await
this
.
uploadImage
({
files
:
this
.
currentFiles
,
url
:
this
.
modalUrl
,
urlText
:
this
.
modalUrlText
,
});
// Error case handled within action
}
catch
(
error
)
{
throw
Error
(
error
);
}
finally
{
this
.
clearInputs
();
}
},
dismissError
()
{
dismissError
()
{
this
.
isErrorDismissed
=
true
;
this
.
isErrorDismissed
=
true
;
this
.
sidebarErrorMessage
=
''
;
this
.
sidebarErrorMessage
=
''
;
...
@@ -372,13 +431,12 @@ export default {
...
@@ -372,13 +431,12 @@ export default {
</alert-summary-row>
</alert-summary-row>
<alert-details-table
:alert=
"alert"
:loading=
"loading"
:statuses=
"statuses"
/>
<alert-details-table
:alert=
"alert"
:loading=
"loading"
:statuses=
"statuses"
/>
</gl-tab>
</gl-tab>
<gl-tab
<metric-images-tab
v-if=
"!isThreatMonitoringPage"
v-if=
"!isThreatMonitoringPage"
:
data-testid=
"$options.tabsConfig[1].id"
data-testid=
"$options.tabsConfig[1].id"
:title=
"$options.tabsConfig[1].title"
:title=
"$options.tabsConfig[1].title"
>
/>
<alert-metrics
:dashboard-url=
"alert.metricsDashboardUrl"
/>
</gl-tab>
<gl-tab
:data-testid=
"$options.tabsConfig[2].id"
:title=
"$options.tabsConfig[2].title"
>
<gl-tab
:data-testid=
"$options.tabsConfig[2].id"
:title=
"$options.tabsConfig[2].title"
>
<div
v-if=
"alert.notes.nodes.length > 0"
class=
"issuable-discussion"
>
<div
v-if=
"alert.notes.nodes.length > 0"
class=
"issuable-discussion"
>
<ul
class=
"notes main-notes-list timeline"
>
<ul
class=
"notes main-notes-list timeline"
>
...
...
app/assets/javascripts/vue_shared/alert_details/index.js
View file @
3467ff1b
...
@@ -3,6 +3,9 @@ import produce from 'immer';
...
@@ -3,6 +3,9 @@ import produce from 'immer';
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
createStore
from
'
~/vue_shared/components/metric_images/store
'
;
import
service
from
'
./service.js
'
;
import
AlertDetails
from
'
./components/alert_details.vue
'
;
import
AlertDetails
from
'
./components/alert_details.vue
'
;
import
{
PAGE_CONFIG
}
from
'
./constants
'
;
import
{
PAGE_CONFIG
}
from
'
./constants
'
;
import
sidebarStatusQuery
from
'
./graphql/queries/alert_sidebar_status.query.graphql
'
;
import
sidebarStatusQuery
from
'
./graphql/queries/alert_sidebar_status.query.graphql
'
;
...
@@ -12,7 +15,8 @@ Vue.use(VueApollo);
...
@@ -12,7 +15,8 @@ Vue.use(VueApollo);
export
default
(
selector
)
=>
{
export
default
(
selector
)
=>
{
const
domEl
=
document
.
querySelector
(
selector
);
const
domEl
=
document
.
querySelector
(
selector
);
const
{
alertId
,
projectPath
,
projectIssuesPath
,
projectId
,
page
}
=
domEl
.
dataset
;
const
{
alertId
,
projectPath
,
projectIssuesPath
,
projectId
,
page
,
canUpdate
}
=
domEl
.
dataset
;
const
iid
=
alertId
;
const
router
=
createRouter
();
const
router
=
createRouter
();
const
resolvers
=
{
const
resolvers
=
{
...
@@ -54,7 +58,9 @@ export default (selector) => {
...
@@ -54,7 +58,9 @@ export default (selector) => {
page
,
page
,
projectIssuesPath
,
projectIssuesPath
,
projectId
,
projectId
,
iid
,
statuses
:
PAGE_CONFIG
[
page
].
STATUSES
,
statuses
:
PAGE_CONFIG
[
page
].
STATUSES
,
canUpdate
:
parseBoolean
(
canUpdate
),
};
};
if
(
page
===
PAGE_CONFIG
.
OPERATIONS
.
TITLE
)
{
if
(
page
===
PAGE_CONFIG
.
OPERATIONS
.
TITLE
)
{
...
@@ -67,6 +73,8 @@ export default (selector) => {
...
@@ -67,6 +73,8 @@ export default (selector) => {
provide
.
isThreatMonitoringPage
=
true
;
provide
.
isThreatMonitoringPage
=
true
;
}
}
const
store
=
createStore
({},
service
);
// eslint-disable-next-line no-new
// eslint-disable-next-line no-new
new
Vue
({
new
Vue
({
el
:
selector
,
el
:
selector
,
...
@@ -74,6 +82,7 @@ export default (selector) => {
...
@@ -74,6 +82,7 @@ export default (selector) => {
components
:
{
components
:
{
AlertDetails
,
AlertDetails
,
},
},
store
,
provide
,
provide
,
apolloProvider
,
apolloProvider
,
router
,
router
,
...
...
app/assets/javascripts/vue_shared/alert_details/service.js
0 → 100644
View file @
3467ff1b
import
{
fetchAlertMetricImages
,
uploadAlertMetricImage
,
updateAlertMetricImage
,
deleteAlertMetricImage
,
}
from
'
~/rest_api
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
export
const
getMetricImages
=
async
(
payload
)
=>
{
payload
=
replaceModelIId
(
payload
);
const
response
=
await
fetchAlertMetricImages
(
payload
);
return
convertObjectPropsToCamelCase
(
response
.
data
,
{
deep
:
true
});
};
export
const
uploadMetricImage
=
async
(
payload
)
=>
{
payload
=
replaceModelIId
(
payload
);
const
response
=
await
uploadAlertMetricImage
(
payload
);
return
convertObjectPropsToCamelCase
(
response
.
data
);
};
export
const
updateMetricImage
=
async
(
payload
)
=>
{
payload
=
replaceModelIId
(
payload
);
const
response
=
await
updateAlertMetricImage
(
payload
);
return
convertObjectPropsToCamelCase
(
response
.
data
);
};
export
const
deleteMetricImage
=
async
(
payload
)
=>
{
payload
=
replaceModelIId
(
payload
);
const
response
=
await
deleteAlertMetricImage
(
payload
);
return
convertObjectPropsToCamelCase
(
response
.
data
);
};
function
replaceModelIId
(
payload
)
{
delete
Object
.
assign
(
payload
,
{
alertIid
:
payload
.
modelIid
}).
modelIid
;
return
payload
;
}
export
default
{
getMetricImages
,
uploadMetricImage
,
updateMetricImage
,
deleteMetricImage
,
};
app/helpers/projects/alert_management_helper.rb
View file @
3467ff1b
...
@@ -15,13 +15,14 @@ module Projects::AlertManagementHelper
...
@@ -15,13 +15,14 @@ module Projects::AlertManagementHelper
}
}
end
end
def
alert_management_detail_data
(
project
,
alert_id
)
def
alert_management_detail_data
(
current_user
,
project
,
alert_id
)
{
{
'alert-id'
=>
alert_id
,
'alert-id'
=>
alert_id
,
'project-path'
=>
project
.
full_path
,
'project-path'
=>
project
.
full_path
,
'project-id'
=>
project
.
id
,
'project-id'
=>
project
.
id
,
'project-issues-path'
=>
project_issues_path
(
project
),
'project-issues-path'
=>
project_issues_path
(
project
),
'page'
=>
'OPERATIONS'
'page'
=>
'OPERATIONS'
,
'can-update'
=>
can?
(
current_user
,
:update_alert_management_alert
,
project
).
to_s
}
}
end
end
...
...
app/views/projects/alert_management/details.html.haml
View file @
3467ff1b
...
@@ -2,4 +2,4 @@
...
@@ -2,4 +2,4 @@
-
page_title
s_
(
'AlertManagement|Alert detail'
)
-
page_title
s_
(
'AlertManagement|Alert detail'
)
-
add_page_specific_style
'page_bundles/alert_management_details'
-
add_page_specific_style
'page_bundles/alert_management_details'
#js-alert_details
{
data:
alert_management_detail_data
(
@project
,
@alert_id
)
}
#js-alert_details
{
data:
alert_management_detail_data
(
current_user
,
@project
,
@alert_id
)
}
spec/helpers/projects/alert_management_helper_spec.rb
View file @
3467ff1b
...
@@ -110,15 +110,34 @@ RSpec.describe Projects::AlertManagementHelper do
...
@@ -110,15 +110,34 @@ RSpec.describe Projects::AlertManagementHelper do
describe
'#alert_management_detail_data'
do
describe
'#alert_management_detail_data'
do
let
(
:alert_id
)
{
1
}
let
(
:alert_id
)
{
1
}
let
(
:issues_path
)
{
project_issues_path
(
project
)
}
let
(
:issues_path
)
{
project_issues_path
(
project
)
}
let
(
:can_update_alert
)
{
true
}
before
do
allow
(
helper
)
.
to
receive
(
:can?
)
.
with
(
current_user
,
:update_alert_management_alert
,
project
)
.
and_return
(
can_update_alert
)
end
it
'returns detail page configuration'
do
it
'returns detail page configuration'
do
expect
(
helper
.
alert_management_detail_data
(
project
,
alert_id
)).
to
eq
(
expect
(
helper
.
alert_management_detail_data
(
current_user
,
project
,
alert_id
)).
to
eq
(
'alert-id'
=>
alert_id
,
'alert-id'
=>
alert_id
,
'project-path'
=>
project_path
,
'project-path'
=>
project_path
,
'project-id'
=>
project_id
,
'project-id'
=>
project_id
,
'project-issues-path'
=>
issues_path
,
'project-issues-path'
=>
issues_path
,
'page'
=>
'OPERATIONS'
'page'
=>
'OPERATIONS'
,
'can-update'
=>
'true'
)
)
end
end
context
'when user cannot update alert'
do
let
(
:can_update_alert
)
{
false
}
it
'shows error tracking enablement as disabled'
do
expect
(
helper
.
alert_management_detail_data
(
current_user
,
project
,
alert_id
)).
to
include
(
'can-update'
=>
'false'
)
end
end
end
end
end
end
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