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
13cde9ae
Commit
13cde9ae
authored
Mar 09, 2020
by
Savas Vedova
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add button to export CSV data
Implement small improvements as well
parent
58b17cee
Changes
11
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
306 additions
and
59 deletions
+306
-59
ee/app/assets/javascripts/security_dashboard/components/csv_export_button.vue
...ripts/security_dashboard/components/csv_export_button.vue
+111
-0
ee/app/assets/javascripts/security_dashboard/components/first_class_project_security_dashboard.vue
...ard/components/first_class_project_security_dashboard.vue
+10
-0
ee/app/assets/javascripts/security_dashboard/components/project_security_dashboard.vue
...urity_dashboard/components/project_security_dashboard.vue
+6
-6
ee/app/assets/javascripts/security_dashboard/first_class_init.js
...assets/javascripts/security_dashboard/first_class_init.js
+1
-0
ee/app/assets/javascripts/security_dashboard/project_init.js
ee/app/assets/javascripts/security_dashboard/project_init.js
+19
-50
ee/changelogs/unreleased/197494-export-vulnerability-findings-as-csv.yml
...nreleased/197494-export-vulnerability-findings-as-csv.yml
+5
-0
ee/spec/frontend/security_dashboard/components/__snapshots__/project_security_dashboard_spec.js.snap
...nts/__snapshots__/project_security_dashboard_spec.js.snap
+2
-2
ee/spec/frontend/security_dashboard/components/csv_export_button_spec.js
...d/security_dashboard/components/csv_export_button_spec.js
+137
-0
ee/spec/frontend/security_dashboard/components/first_class_project_security_dashboard_spec.js
...components/first_class_project_security_dashboard_spec.js
+9
-0
ee/spec/frontend/security_dashboard/components/project_security_dashboard_spec.js
...y_dashboard/components/project_security_dashboard_spec.js
+0
-1
locale/gitlab.pot
locale/gitlab.pot
+6
-0
No files found.
ee/app/assets/javascripts/security_dashboard/components/csv_export_button.vue
0 → 100644
View file @
13cde9ae
<
script
>
import
{
GlPopover
,
GlIcon
,
GlLink
,
GlNewButton
,
GlTooltipDirective
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
createFlash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
pollUntilComplete
from
'
~/lib/utils/poll_until_complete
'
;
export
const
STORAGE_KEY
=
'
vulnerability_csv_export_popover_dismissed
'
;
export
default
{
components
:
{
GlIcon
,
GlNewButton
,
GlPopover
,
GlLink
,
GlLoadingIcon
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
vulnerabilitiesExportEndpoint
:
{
type
:
String
,
required
:
true
,
},
},
data
:
()
=>
({
isPreparingCsvExport
:
false
,
showPopover
:
localStorage
.
getItem
(
STORAGE_KEY
)
!==
'
true
'
,
}),
methods
:
{
closePopover
()
{
this
.
showPopover
=
false
;
try
{
localStorage
.
setItem
(
STORAGE_KEY
,
'
true
'
);
}
catch
(
e
)
{
// Ignore the error - this is just a safety measure.
}
},
initiateCsvExport
()
{
this
.
isPreparingCsvExport
=
true
;
this
.
closePopover
();
axios
.
post
(
this
.
vulnerabilitiesExportEndpoint
)
.
then
(({
data
})
=>
pollUntilComplete
(
data
.
_links
.
self
))
.
then
(({
data
})
=>
{
const
anchor
=
document
.
createElement
(
'
a
'
);
anchor
.
download
=
''
;
anchor
.
href
=
data
.
_links
.
download
;
anchor
.
click
();
})
.
catch
(()
=>
{
createFlash
(
s__
(
'
SecurityDashboard|There was an error while generating the report.
'
));
})
.
finally
(()
=>
{
this
.
isPreparingCsvExport
=
false
;
});
},
},
};
</
script
>
<
template
>
<gl-new-button
ref=
"csvExportButton"
v-gl-tooltip
.
hover
class=
"align-self-center"
:title=
"__('Export as CSV')"
:loading=
"isPreparingCsvExport"
@
click=
"initiateCsvExport"
>
<gl-icon
v-if=
"!isPreparingCsvExport"
ref=
"exportIcon"
name=
"export"
class=
"mr-0 position-top-0"
/>
<gl-loading-icon
v-else
/>
<gl-popover
ref=
"popover"
:target=
"() => $refs.csvExportButton.$el"
:show=
"showPopover"
placement=
"left"
triggers=
"manual"
>
<p
class=
"gl-font-size-14"
>
{{
__
(
'
You can now export your security dashboard to a CSV report.
'
)
}}
</p>
<gl-link
ref=
"popoverExternalLink"
target=
"_blank"
href=
"https://gitlab.com/gitlab-org/gitlab/issues/197111"
class=
"d-flex align-items-center mb-3"
>
{{
__
(
'
More information and share feedback
'
)
}}
<gl-icon
name=
"external-link"
:size=
"12"
class=
"ml-1"
/>
</gl-link>
<gl-new-button
ref=
"popoverButton"
class=
"w-100"
@
click=
"closePopover"
>
{{
__
(
'
Got it!
'
)
}}
</gl-new-button>
</gl-popover>
</gl-new-button>
</
template
>
ee/app/assets/javascripts/security_dashboard/components/first_class_project_security_dashboard.vue
View file @
13cde9ae
...
...
@@ -4,6 +4,7 @@ import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/
import
SecurityDashboardLayout
from
'
ee/security_dashboard/components/security_dashboard_layout.vue
'
;
import
VulnerabilitiesCountList
from
'
ee/security_dashboard/components/vulnerability_count_list.vue
'
;
import
Filters
from
'
ee/security_dashboard/components/first_class_vulnerability_filters.vue
'
;
import
CsvExportButton
from
'
./csv_export_button.vue
'
;
export
default
{
components
:
{
...
...
@@ -11,6 +12,7 @@ export default {
ReportsNotConfigured
,
SecurityDashboardLayout
,
VulnerabilitiesCountList
,
CsvExportButton
,
Filters
,
},
props
:
{
...
...
@@ -37,6 +39,10 @@ export default {
required
:
false
,
default
:
false
,
},
vulnerabilitiesExportEndpoint
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
...
...
@@ -56,6 +62,10 @@ export default {
<template
v-if=
"hasPipelineData"
>
<security-dashboard-layout>
<template
#header
>
<div
class=
"mt-4 d-flex"
>
<h4
class=
"flex-grow mt-0 mb-0"
>
{{
__
(
'
Vulnerabilities
'
)
}}
</h4>
<csv-export-button
:vulnerabilities-export-endpoint=
"vulnerabilitiesExportEndpoint"
/>
</div>
<vulnerabilities-count-list
:project-full-path=
"projectFullPath"
/>
<filters
@
filterChange=
"handleFilterChange"
/>
</
template
>
...
...
ee/app/assets/javascripts/security_dashboard/components/project_security_dashboard.vue
View file @
13cde9ae
<
script
>
import
{
isUndefined
}
from
'
lodash
'
;
import
{
GlEmptyState
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
GlEmptyState
,
GlSprintf
,
GlLink
}
from
'
@gitlab/ui
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
UserAvatarLink
from
'
~/vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
TimeagoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
...
...
@@ -8,10 +8,10 @@ import ReportsNotConfigured from './empty_states/reports_not_configured.vue';
import
SecurityDashboard
from
'
./security_dashboard_vuex.vue
'
;
export
default
{
name
:
'
ProjectSecurityDashboard
'
,
components
:
{
GlEmptyState
,
GlSprintf
,
GlLink
,
Icon
,
ReportsNotConfigured
,
SecurityDashboard
,
...
...
@@ -94,7 +94,7 @@ export default {
"
>
<template
#pipelineLink
>
<
a
:href=
"pipeline.path"
>
#
{{
pipeline
.
id
}}
</a
>
<
gl-link
:href=
"pipeline.path"
>
#
{{
pipeline
.
id
}}
</gl-link
>
</
template
>
<
template
#timeago
>
<timeago-tooltip
:time=
"pipeline.created"
/>
...
...
@@ -113,14 +113,14 @@ export default {
</span>
<span
class=
"js-security-dashboard-right pull-right"
>
<icon
name=
"branch"
/>
<
a
:href=
"branch.path"
class=
"monospace"
>
{{ branch.id }}
</a
>
<
gl-link
:href=
"branch.path"
class=
"monospace"
>
{{ branch.id }}
</gl-link
>
<span
class=
"text-muted prepend-left-5 append-right-5"
>
·
</span>
<icon
name=
"commit"
/>
<
a
:href=
"commit.path"
class=
"monospace"
>
{{ commit.id }}
</a
>
<
gl-link
:href=
"commit.path"
class=
"monospace"
>
{{ commit.id }}
</gl-link
>
</span>
</div>
</div>
<h4
class=
"mt-4 mb-3"
>
{{ __('Vulnerabilities') }}
</h4>
<h4>
{{ __('Vulnerabilities') }}
</h4>
<security-dashboard
:lock-to-project=
"project"
:vulnerabilities-endpoint=
"vulnerabilitiesEndpoint"
...
...
ee/app/assets/javascripts/security_dashboard/first_class_init.js
View file @
13cde9ae
...
...
@@ -41,6 +41,7 @@ export default (
if
(
dashboardType
===
DASHBOARD_TYPES
.
PROJECT
)
{
component
=
FirstClassProjectSecurityDashboard
;
props
.
projectFullPath
=
el
.
dataset
.
projectFullPath
;
props
.
vulnerabilitiesExportEndpoint
=
el
.
dataset
.
vulnerabilitiesExportEndpoint
;
}
else
if
(
dashboardType
===
DASHBOARD_TYPES
.
GROUP
)
{
component
=
FirstClassGroupSecurityDashboard
;
props
.
groupFullPath
=
el
.
dataset
.
groupFullPath
;
...
...
ee/app/assets/javascripts/security_dashboard/project_init.js
View file @
13cde9ae
...
...
@@ -8,67 +8,36 @@ import { parseBoolean } from '~/lib/utils/common_utils';
export
default
()
=>
{
const
securityTab
=
document
.
getElementById
(
'
js-security-report-app
'
);
const
{
commitId
,
commitPath
,
dashboardDocumentation
,
emptyStateSvgPath
,
hasPipelineData
,
pipelineCreated
,
pipelineId
,
pipelinePath
,
projectId
,
projectName
,
refId
,
refPath
,
securityDashboardHelpPath
,
userAvatarPath
,
userName
,
userPath
,
vulnerabilitiesEndpoint
,
vulnerabilitiesSummaryEndpoint
,
vulnerabilityFeedbackHelpPath
,
}
=
securityTab
.
dataset
;
const
parsedPipelineId
=
parseInt
(
pipelineId
,
10
);
const
parsedHasPipelineData
=
parseBoolean
(
hasPipelineData
);
let
props
=
{
dashboardDocumentation
,
emptyStateSvgPath
,
hasPipelineData
:
parsedHasPipelineData
,
securityDashboardHelpPath
,
vulnerabilitiesEndpoint
,
vulnerabilitiesSummaryEndpoint
,
vulnerabilityFeedbackHelpPath
,
const
props
=
{
...
securityTab
.
dataset
,
hasPipelineData
:
parseBoolean
(
securityTab
.
dataset
.
hasPipelineData
),
};
if
(
parsedHasPipelineData
)
{
props
=
{
...
props
,
if
(
props
.
hasPipelineData
)
{
Object
.
assign
(
props
,
{
project
:
{
id
:
projectId
,
name
:
projectName
,
id
:
pro
ps
.
pro
jectId
,
name
:
pro
ps
.
pro
jectName
,
},
triggeredBy
:
{
avatarPath
:
userAvatarPath
,
name
:
userName
,
path
:
userPath
,
avatarPath
:
props
.
userAvatarPath
,
name
:
props
.
userName
,
path
:
props
.
userPath
,
},
pipeline
:
{
id
:
parse
dPipelineId
,
created
:
pipelineCreated
,
path
:
pipelinePath
,
id
:
parse
Int
(
props
.
pipelineId
,
10
)
,
created
:
p
rops
.
p
ipelineCreated
,
path
:
p
rops
.
p
ipelinePath
,
},
commit
:
{
id
:
commitId
,
path
:
commitPath
,
id
:
props
.
commitId
,
path
:
props
.
commitPath
,
},
branch
:
{
id
:
refId
,
path
:
refPath
,
id
:
props
.
refId
,
path
:
props
.
refPath
,
},
};
}
)
;
}
const
router
=
createRouter
();
...
...
ee/changelogs/unreleased/197494-export-vulnerability-findings-as-csv.yml
0 → 100644
View file @
13cde9ae
---
title
:
Add a button to export vulnerabilities in CSV format.
merge_request
:
26838
author
:
type
:
added
ee/spec/frontend/security_dashboard/components/__snapshots__/project_security_dashboard_spec.js.snap
View file @
13cde9ae
...
...
@@ -27,7 +27,7 @@ exports[`Project Security Dashboard component Headline renders renders branch an
</svg>
<a
class="monospace"
class="
gl-link
monospace"
href="http://test.host/branch"
>
master
...
...
@@ -49,7 +49,7 @@ exports[`Project Security Dashboard component Headline renders renders branch an
</svg>
<a
class="monospace"
class="
gl-link
monospace"
href="http://test.host/commit"
>
1234adf
...
...
ee/spec/frontend/security_dashboard/components/csv_export_button_spec.js
0 → 100644
View file @
13cde9ae
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlLoadingIcon
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
statusCodes
from
'
~/lib/utils/http_status
'
;
import
createFlash
from
'
~/flash
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
CsvExportButton
,
{
STORAGE_KEY
,
}
from
'
ee/security_dashboard/components/csv_export_button.vue
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
jest
.
mock
(
'
~/flash
'
);
const
vulnerabilitiesExportEndpoint
=
`
${
TEST_HOST
}
/vulnerability_findings.csv`
;
describe
(
'
Csv Button Export
'
,
()
=>
{
let
mock
;
let
wrapper
;
const
issueUrl
=
'
https://gitlab.com/gitlab-org/gitlab/issues/197111
'
;
const
findPopoverExternalLink
=
()
=>
wrapper
.
find
({
ref
:
'
popoverExternalLink
'
});
const
findPopoverButton
=
()
=>
wrapper
.
find
({
ref
:
'
popoverButton
'
});
const
findPopover
=
()
=>
wrapper
.
find
({
ref
:
'
popover
'
});
const
findCsvExportButton
=
()
=>
wrapper
.
find
({
ref
:
'
csvExportButton
'
});
const
findLoadingIcon
=
()
=>
wrapper
.
find
(
GlLoadingIcon
);
const
findExportIcon
=
()
=>
wrapper
.
find
({
ref
:
'
exportIcon
'
});
const
createComponent
=
()
=>
{
return
shallowMount
(
CsvExportButton
,
{
propsData
:
{
vulnerabilitiesExportEndpoint
,
},
stubs
:
{
GlIcon
,
GlLoadingIcon
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
localStorage
.
removeItem
(
STORAGE_KEY
);
});
describe
(
'
when the user sees the button for the first time
'
,
()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
wrapper
=
createComponent
();
});
it
(
'
renders correctly
'
,
()
=>
{
expect
(
findPopoverExternalLink
().
attributes
(
'
href
'
)).
toBe
(
issueUrl
);
expect
(
wrapper
.
text
()).
toContain
(
'
More information and share feedback
'
);
expect
(
wrapper
.
text
()).
toContain
(
'
You can now export your security dashboard to a CSV report.
'
,
);
});
it
(
'
is a link that initiates the download and polls until the file is ready, and then opens the download in a new tab.
'
,
()
=>
{
const
downloadAnchor
=
document
.
createElement
(
'
a
'
);
const
statusLink
=
'
/poll/until/complete
'
;
const
downloadLink
=
'
/link/to/download
'
;
let
spy
;
mock
.
onPost
(
vulnerabilitiesExportEndpoint
)
.
reply
(
statusCodes
.
ACCEPTED
,
{
_links
:
{
self
:
statusLink
}
});
mock
.
onGet
(
statusLink
).
reply
(()
=>
{
// We need to mock it at this stage because vue internally uses
// document.createElement to mount the elements.
spy
=
jest
.
spyOn
(
document
,
'
createElement
'
).
mockImplementationOnce
(()
=>
{
downloadAnchor
.
click
=
jest
.
fn
();
return
downloadAnchor
;
});
return
[
statusCodes
.
OK
,
{
_links
:
{
download
:
downloadLink
}
}];
});
findCsvExportButton
().
vm
.
$emit
(
'
click
'
);
return
axios
.
waitForAll
().
then
(()
=>
{
expect
(
spy
).
toHaveBeenCalledWith
(
'
a
'
);
expect
(
downloadAnchor
.
href
).
toContain
(
downloadLink
);
expect
(
downloadAnchor
.
hasAttribute
(
'
download
'
)).
toBe
(
true
);
expect
(
downloadAnchor
.
click
).
toHaveBeenCalled
();
});
});
it
(
'
shows the flash error when backend fails to generate the export
'
,
()
=>
{
mock
.
onPost
(
vulnerabilitiesExportEndpoint
).
reply
(
statusCodes
.
NOT_FOUND
,
{});
findCsvExportButton
().
vm
.
$emit
(
'
click
'
);
return
axios
.
waitForAll
().
then
(()
=>
{
expect
(
createFlash
).
toHaveBeenCalledWith
(
'
There was an error while generating the report.
'
);
});
});
it
(
'
displays the export icon when not loading and the loading icon when loading
'
,
()
=>
{
expect
(
findExportIcon
().
props
(
'
name
'
)).
toBe
(
'
export
'
);
expect
(
findLoadingIcon
().
exists
()).
toBe
(
false
);
wrapper
.
setData
({
isPreparingCsvExport
:
true
,
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
findExportIcon
().
exists
()).
toBe
(
false
);
expect
(
findLoadingIcon
().
exists
()).
toBe
(
true
);
});
});
it
(
'
displays the popover by default
'
,
()
=>
{
expect
(
findPopover
().
attributes
(
'
show
'
)).
toBeTruthy
();
});
it
(
'
closes the popover when the button is clicked
'
,
()
=>
{
const
button
=
findPopoverButton
();
expect
(
button
.
text
().
trim
()).
toBe
(
'
Got it!
'
);
button
.
vm
.
$emit
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
findPopover
().
attributes
(
'
show
'
)).
toBeFalsy
();
});
});
});
describe
(
'
when user closed the popover before
'
,
()
=>
{
beforeEach
(()
=>
{
localStorage
.
setItem
(
STORAGE_KEY
,
'
true
'
);
wrapper
=
createComponent
();
});
it
(
'
does not display the popover anymore
'
,
()
=>
{
expect
(
findPopover
().
attributes
(
'
show
'
)).
toBeFalsy
();
});
});
});
ee/spec/frontend/security_dashboard/components/first_class_project_security_dashboard_spec.js
View file @
13cde9ae
...
...
@@ -4,12 +4,14 @@ import Filters from 'ee/security_dashboard/components/first_class_vulnerability_
import
SecurityDashboardLayout
from
'
ee/security_dashboard/components/security_dashboard_layout.vue
'
;
import
ProjectVulnerabilitiesApp
from
'
ee/vulnerabilities/components/project_vulnerabilities_app.vue
'
;
import
ReportsNotConfigured
from
'
ee/security_dashboard/components/empty_states/reports_not_configured.vue
'
;
import
CsvExportButton
from
'
ee/security_dashboard/components/csv_export_button.vue
'
;
const
props
=
{
dashboardDocumentation
:
'
/help/docs
'
,
emptyStateSvgPath
:
'
/svgs/empty/svg
'
,
projectFullPath
:
'
/group/project
'
,
securityDashboardHelpPath
:
'
/security/dashboard/help-path
'
,
vulnerabilitiesExportEndpoint
:
'
/vulnerabilities/exports
'
,
};
const
filters
=
{
foo
:
'
bar
'
};
...
...
@@ -19,6 +21,7 @@ describe('First class Project Security Dashboard component', () => {
const
findFilters
=
()
=>
wrapper
.
find
(
Filters
);
const
findVulnerabilities
=
()
=>
wrapper
.
find
(
ProjectVulnerabilitiesApp
);
const
findUnconfiguredState
=
()
=>
wrapper
.
find
(
ReportsNotConfigured
);
const
findCsvExportButton
=
()
=>
wrapper
.
find
(
CsvExportButton
);
const
createComponent
=
options
=>
{
wrapper
=
shallowMount
(
FirstClassProjectSecurityDashboard
,
{
...
...
@@ -59,6 +62,12 @@ describe('First class Project Security Dashboard component', () => {
it
(
'
does not display the unconfigured state
'
,
()
=>
{
expect
(
findUnconfiguredState
().
exists
()).
toBe
(
false
);
});
it
(
'
should display the csv export button
'
,
()
=>
{
expect
(
findCsvExportButton
().
props
(
'
vulnerabilitiesExportEndpoint
'
)).
toEqual
(
props
.
vulnerabilitiesExportEndpoint
,
);
});
});
describe
(
'
with filter data
'
,
()
=>
{
...
...
ee/spec/frontend/security_dashboard/components/project_security_dashboard_spec.js
View file @
13cde9ae
...
...
@@ -2,7 +2,6 @@ import { mount } from '@vue/test-utils';
import
{
GlEmptyState
}
from
'
@gitlab/ui
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
ProjectSecurityDashboard
from
'
ee/security_dashboard/components/project_security_dashboard.vue
'
;
import
createStore
from
'
ee/security_dashboard/store
'
;
import
{
trimText
}
from
'
helpers/text_helper
'
;
...
...
locale/gitlab.pot
View file @
13cde9ae
...
...
@@ -17951,6 +17951,9 @@ msgstr ""
msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
msgid "SecurityDashboard|There was an error while generating the report."
msgstr ""
msgid "SecurityDashboard|Unable to add %{invalidProjects}"
msgstr ""
...
...
@@ -23402,6 +23405,9 @@ msgstr ""
msgid "You can move around the graph by using the arrow keys."
msgstr ""
msgid "You can now export your security dashboard to a CSV report."
msgstr ""
msgid "You can now submit a merge request to get this change into the original branch."
msgstr ""
...
...
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