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
72247ff2
Commit
72247ff2
authored
Nov 21, 2019
by
Olena Horal-Koretska
Committed by
Phil Hughes
Nov 21, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Improve sparkline chart in MR widget deployment
Migrate to gitlab-ui sparkline chart
parent
2d0d3ef5
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
102 additions
and
332 deletions
+102
-332
app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
...ipts/vue_merge_request_widget/components/memory_usage.vue
+1
-7
app/assets/javascripts/vue_shared/components/memory_graph.vue
...assets/javascripts/vue_shared/components/memory_graph.vue
+22
-107
app/assets/stylesheets/framework/memory_graph.scss
app/assets/stylesheets/framework/memory_graph.scss
+2
-16
app/assets/stylesheets/pages/merge_requests.scss
app/assets/stylesheets/pages/merge_requests.scss
+0
-1
changelogs/unreleased/31391-update-sparkline-chart-deployment-widget.yml
...leased/31391-update-sparkline-chart-deployment-widget.yml
+5
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-3
spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
...shared/components/__snapshots__/memory_graph_spec.js.snap
+15
-0
spec/frontend/vue_shared/components/memory_graph_spec.js
spec/frontend/vue_shared/components/memory_graph_spec.js
+53
-0
spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
...s/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+1
-0
spec/javascripts/vue_shared/components/memory_graph_spec.js
spec/javascripts/vue_shared/components/memory_graph_spec.js
+0
-131
spec/javascripts/vue_shared/components/mock_data.js
spec/javascripts/vue_shared/components/mock_data.js
+0
-67
No files found.
app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
View file @
72247ff2
...
@@ -169,12 +169,6 @@ export default {
...
@@ -169,12 +169,6 @@ export default {
<p
v-if=
"shouldShowMetricsUnavailable"
class=
"usage-info js-usage-info usage-info-unavailable"
>
<p
v-if=
"shouldShowMetricsUnavailable"
class=
"usage-info js-usage-info usage-info-unavailable"
>
{{
s__
(
'
mrWidget|Deployment statistics are not available currently
'
)
}}
{{
s__
(
'
mrWidget|Deployment statistics are not available currently
'
)
}}
</p>
</p>
<memory-graph
<memory-graph
v-if=
"shouldShowMemoryGraph"
:metrics=
"memoryMetrics"
:height=
"25"
:width=
"110"
/>
v-if=
"shouldShowMemoryGraph"
:metrics=
"memoryMetrics"
:deployment-time=
"deploymentTime"
height=
"25"
width=
"100"
/>
</div>
</div>
</
template
>
</
template
>
app/assets/javascripts/vue_shared/components/memory_graph.vue
View file @
72247ff2
<
script
>
<
script
>
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
{
formatDate
,
secondsToMilliseconds
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
getTimeago
}
from
'
../../lib/utils/datetime_utility
'
;
import
{
GlSparklineChart
}
from
'
@gitlab/ui/dist/charts
'
;
export
default
{
export
default
{
name
:
'
MemoryGraph
'
,
name
:
'
MemoryGraph
'
,
components
:
{
GlSparklineChart
,
},
props
:
{
props
:
{
metrics
:
{
type
:
Array
,
required
:
true
},
metrics
:
{
type
:
Array
,
required
:
true
},
deploymentTime
:
{
type
:
Number
,
required
:
true
},
width
:
{
type
:
Number
,
required
:
true
},
width
:
{
type
:
String
,
required
:
true
},
height
:
{
type
:
Number
,
required
:
true
},
height
:
{
type
:
String
,
required
:
true
},
},
data
()
{
return
{
pathD
:
''
,
pathViewBox
:
''
,
dotX
:
''
,
dotY
:
''
,
};
},
},
computed
:
{
computed
:
{
getFormattedMedian
()
{
chartData
()
{
const
deployedSince
=
getTimeago
().
format
(
this
.
deploymentTime
*
1000
);
return
this
.
metrics
.
map
(([
x
,
y
])
=>
[
return
sprintf
(
__
(
'
Deployed %{deployedSince}
'
),
{
deployedSince
});
this
.
getFormattedDeploymentTime
(
x
),
this
.
getMemoryUsage
(
y
),
]);
},
},
},
},
mounted
()
{
this
.
renderGraph
(
this
.
deploymentTime
,
this
.
metrics
);
},
methods
:
{
methods
:
{
/**
getFormattedDeploymentTime
(
timestamp
)
{
* Returns metric value index in metrics array
return
formatDate
(
new
Date
(
secondsToMilliseconds
(
timestamp
)),
'
mmm dd yyyy HH:MM:s
'
);
* with timestamp closest to matching median
*/
getMedianMetricIndex
(
median
,
metrics
)
{
let
matchIndex
=
0
;
let
timestampDiff
=
0
;
let
smallestDiff
=
0
;
const
metricTimestamps
=
metrics
.
map
(
v
=>
v
[
0
]);
// Find metric timestamp which is closest to deploymentTime
timestampDiff
=
Math
.
abs
(
metricTimestamps
[
0
]
-
median
);
metricTimestamps
.
forEach
((
timestamp
,
index
)
=>
{
if
(
index
===
0
)
{
// Skip first element
return
;
}
smallestDiff
=
Math
.
abs
(
timestamp
-
median
);
if
(
smallestDiff
<
timestampDiff
)
{
matchIndex
=
index
;
timestampDiff
=
smallestDiff
;
}
});
return
matchIndex
;
},
},
getMemoryUsage
(
MBs
)
{
/**
return
Number
(
MBs
).
toFixed
(
2
);
* Get Graph Plotting values to render Line and Dot
*/
getGraphPlotValues
(
median
,
metrics
)
{
const
renderData
=
metrics
.
map
(
v
=>
v
[
1
]);
const
medianMetricIndex
=
this
.
getMedianMetricIndex
(
median
,
metrics
);
let
cx
=
0
;
let
cy
=
0
;
// Find Maximum and Minimum values from `renderData` array
const
maxMemory
=
Math
.
max
.
apply
(
null
,
renderData
);
const
minMemory
=
Math
.
min
.
apply
(
null
,
renderData
);
// Find difference between extreme ends
const
diff
=
maxMemory
-
minMemory
;
const
lineWidth
=
renderData
.
length
;
// Iterate over metrics values and perform following
// 1. Find x & y co-ords for deploymentTime's memory value
// 2. Return line path against maxMemory
const
linePath
=
renderData
.
map
((
y
,
x
)
=>
{
if
(
medianMetricIndex
===
x
)
{
cx
=
x
;
cy
=
maxMemory
-
y
;
}
return
`
${
x
}
${
maxMemory
-
y
}
`
;
});
return
{
pathD
:
linePath
,
pathViewBox
:
{
lineWidth
,
diff
,
},
dotX
:
cx
,
dotY
:
cy
,
};
},
/**
* Render Graph based on provided median and metrics values
*/
renderGraph
(
median
,
metrics
)
{
const
{
pathD
,
pathViewBox
,
dotX
,
dotY
}
=
this
.
getGraphPlotValues
(
median
,
metrics
);
// Set props and update graph on UI.
this
.
pathD
=
`M
${
pathD
}
`
;
this
.
pathViewBox
=
`0 0
${
pathViewBox
.
lineWidth
}
${
pathViewBox
.
diff
}
`
;
this
.
dotX
=
dotX
;
this
.
dotY
=
dotY
;
},
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div
class=
"memory-graph-container"
>
<div
class=
"memory-graph-container p-1"
:style=
"
{ width: `${width}px` }">
<svg
<gl-sparkline-chart
:title=
"getFormattedMedian"
:width=
"width"
:height=
"height"
:height=
"height"
class=
"has-tooltip"
:tooltip-label=
"__('MB')"
xmlns=
"http://www.w3.org/2000/svg"
:show-last-y-value=
"false"
>
:data=
"chartData"
<path
:d=
"pathD"
:viewBox=
"pathViewBox"
/>
/>
<circle
:cx=
"dotX"
:cy=
"dotY"
r=
"1.5"
transform=
"translate(0 -1)"
/>
</svg>
</div>
</div>
</
template
>
</
template
>
app/assets/stylesheets/framework/memory_graph.scss
View file @
72247ff2
.memory-graph-container
{
.memory-graph-container
{
svg
{
background
:
$white-light
;
background
:
$white-light
;
border
:
1px
solid
$gray-200
;
border
:
1px
solid
$gray-200
;
}
path
{
fill
:
none
;
stroke
:
$blue-500
;
stroke-width
:
2px
;
}
circle
{
stroke
:
$blue-700
;
fill
:
$blue-700
;
stroke-width
:
4px
;
}
}
}
app/assets/stylesheets/pages/merge_requests.scss
View file @
72247ff2
...
@@ -949,7 +949,6 @@
...
@@ -949,7 +949,6 @@
.deployment-info
{
.deployment-info
{
flex
:
1
;
flex
:
1
;
white-space
:
nowrap
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
text-overflow
:
ellipsis
;
min-width
:
100px
;
min-width
:
100px
;
...
...
changelogs/unreleased/31391-update-sparkline-chart-deployment-widget.yml
0 → 100644
View file @
72247ff2
---
title
:
Improve sparkline chart in MR widget deployment
merge_request
:
20085
author
:
type
:
other
locale/gitlab.pot
View file @
72247ff2
...
@@ -5692,9 +5692,6 @@ msgstr ""
...
@@ -5692,9 +5692,6 @@ msgstr ""
msgid "Deployed"
msgid "Deployed"
msgstr ""
msgstr ""
msgid "Deployed %{deployedSince}"
msgstr ""
msgid "Deployed to"
msgid "Deployed to"
msgstr ""
msgstr ""
...
@@ -10390,6 +10387,9 @@ msgstr ""
...
@@ -10390,6 +10387,9 @@ msgstr ""
msgid "Logs|To see the pod logs, deploy your code to an environment."
msgid "Logs|To see the pod logs, deploy your code to an environment."
msgstr ""
msgstr ""
msgid "MB"
msgstr ""
msgid "MD5"
msgid "MD5"
msgstr ""
msgstr ""
...
...
spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
0 → 100644
View file @
72247ff2
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MemoryGraph Render chart should draw container with chart 1`] = `
<div
class="memory-graph-container p-1"
style="width: 100px;"
>
<glsparklinechart-stub
data="Nov 12 2019 19:17:33,2.87,Nov 12 2019 19:18:33,2.78,Nov 12 2019 19:19:33,2.78,Nov 12 2019 19:20:33,3.01"
height="25"
tooltiplabel="MB"
variant="gray900"
/>
</div>
`;
spec/frontend/vue_shared/components/memory_graph_spec.js
0 → 100644
View file @
72247ff2
import
Vue
from
'
vue
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
MemoryGraph
from
'
~/vue_shared/components/memory_graph.vue
'
;
import
{
GlSparklineChart
}
from
'
@gitlab/ui/dist/charts
'
;
describe
(
'
MemoryGraph
'
,
()
=>
{
const
Component
=
Vue
.
extend
(
MemoryGraph
);
let
wrapper
;
const
metrics
=
[
[
1573586253.853
,
'
2.87
'
],
[
1573586313.853
,
'
2.77734375
'
],
[
1573586373.853
,
'
2.77734375
'
],
[
1573586433.853
,
'
3.0066964285714284
'
],
];
afterEach
(()
=>
{
wrapper
.
destroy
();
});
beforeEach
(()
=>
{
wrapper
=
shallowMount
(
Component
,
{
propsData
:
{
metrics
,
width
:
100
,
height
:
25
,
},
});
});
describe
(
'
chartData
'
,
()
=>
{
it
(
'
should calculate chartData
'
,
()
=>
{
expect
(
wrapper
.
vm
.
chartData
.
length
).
toEqual
(
metrics
.
length
);
});
it
(
'
should format date & MB values
'
,
()
=>
{
const
formattedData
=
[
[
'
Nov 12 2019 19:17:33
'
,
'
2.87
'
],
[
'
Nov 12 2019 19:18:33
'
,
'
2.78
'
],
[
'
Nov 12 2019 19:19:33
'
,
'
2.78
'
],
[
'
Nov 12 2019 19:20:33
'
,
'
3.01
'
],
];
expect
(
wrapper
.
vm
.
chartData
).
toEqual
(
formattedData
);
});
});
describe
(
'
Render chart
'
,
()
=>
{
it
(
'
should draw container with chart
'
,
()
=>
{
expect
(
wrapper
.
element
).
toMatchSnapshot
();
expect
(
wrapper
.
find
(
'
.memory-graph-container
'
).
exists
()).
toBe
(
true
);
expect
(
wrapper
.
find
(
GlSparklineChart
).
exists
()).
toBe
(
true
);
});
});
});
spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
View file @
72247ff2
...
@@ -185,6 +185,7 @@ describe('MemoryUsage', () => {
...
@@ -185,6 +185,7 @@ describe('MemoryUsage', () => {
vm
.
loadingMetrics
=
false
;
vm
.
loadingMetrics
=
false
;
vm
.
hasMetrics
=
true
;
vm
.
hasMetrics
=
true
;
vm
.
loadFailed
=
false
;
vm
.
loadFailed
=
false
;
vm
.
memoryMetrics
=
metricsMockData
.
metrics
.
memory_values
[
0
].
values
;
Vue
.
nextTick
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
el
.
querySelector
(
'
.memory-graph-container
'
)).
toBeDefined
();
expect
(
el
.
querySelector
(
'
.memory-graph-container
'
)).
toBeDefined
();
...
...
spec/javascripts/vue_shared/components/memory_graph_spec.js
deleted
100644 → 0
View file @
2d0d3ef5
import
Vue
from
'
vue
'
;
import
MemoryGraph
from
'
~/vue_shared/components/memory_graph.vue
'
;
import
{
mockMetrics
,
mockMedian
,
mockMedianIndex
}
from
'
./mock_data
'
;
const
defaultHeight
=
'
25
'
;
const
defaultWidth
=
'
100
'
;
const
createComponent
=
()
=>
{
const
Component
=
Vue
.
extend
(
MemoryGraph
);
return
new
Component
({
el
:
document
.
createElement
(
'
div
'
),
propsData
:
{
metrics
:
[],
deploymentTime
:
0
,
width
:
''
,
height
:
''
,
pathD
:
''
,
pathViewBox
:
''
,
dotX
:
''
,
dotY
:
''
,
},
});
};
describe
(
'
MemoryGraph
'
,
()
=>
{
let
vm
;
let
el
;
beforeEach
(()
=>
{
vm
=
createComponent
();
el
=
vm
.
$el
;
});
describe
(
'
data
'
,
()
=>
{
it
(
'
should have default data
'
,
()
=>
{
const
data
=
MemoryGraph
.
data
();
const
dataValidator
=
(
dataItem
,
expectedType
,
defaultVal
)
=>
{
expect
(
typeof
dataItem
).
toBe
(
expectedType
);
expect
(
dataItem
).
toBe
(
defaultVal
);
};
dataValidator
(
data
.
pathD
,
'
string
'
,
''
);
dataValidator
(
data
.
pathViewBox
,
'
string
'
,
''
);
dataValidator
(
data
.
dotX
,
'
string
'
,
''
);
dataValidator
(
data
.
dotY
,
'
string
'
,
''
);
});
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
getFormattedMedian
'
,
()
=>
{
it
(
'
should show human readable median value based on provided median timestamp
'
,
()
=>
{
vm
.
deploymentTime
=
mockMedian
;
const
formattedMedian
=
vm
.
getFormattedMedian
;
expect
(
formattedMedian
.
indexOf
(
'
Deployed
'
)).
toBeGreaterThan
(
-
1
);
expect
(
formattedMedian
.
indexOf
(
'
ago
'
)).
toBeGreaterThan
(
-
1
);
});
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
getMedianMetricIndex
'
,
()
=>
{
it
(
'
should return index of closest metric timestamp to that of median
'
,
()
=>
{
const
matchingIndex
=
vm
.
getMedianMetricIndex
(
mockMedian
,
mockMetrics
);
expect
(
matchingIndex
).
toBe
(
mockMedianIndex
);
});
});
describe
(
'
getGraphPlotValues
'
,
()
=>
{
it
(
'
should return Object containing values to plot graph
'
,
()
=>
{
const
plotValues
=
vm
.
getGraphPlotValues
(
mockMedian
,
mockMetrics
);
expect
(
plotValues
.
pathD
).
toBeDefined
();
expect
(
Array
.
isArray
(
plotValues
.
pathD
)).
toBeTruthy
();
expect
(
plotValues
.
pathViewBox
).
toBeDefined
();
expect
(
typeof
plotValues
.
pathViewBox
).
toBe
(
'
object
'
);
expect
(
plotValues
.
dotX
).
toBeDefined
();
expect
(
typeof
plotValues
.
dotX
).
toBe
(
'
number
'
);
expect
(
plotValues
.
dotY
).
toBeDefined
();
expect
(
typeof
plotValues
.
dotY
).
toBe
(
'
number
'
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
should render template elements correctly
'
,
()
=>
{
expect
(
el
.
classList
.
contains
(
'
memory-graph-container
'
)).
toBeTruthy
();
expect
(
el
.
querySelector
(
'
svg
'
)).
toBeDefined
();
});
it
(
'
should render graph when renderGraph is called internally
'
,
done
=>
{
const
{
pathD
,
pathViewBox
,
dotX
,
dotY
}
=
vm
.
getGraphPlotValues
(
mockMedian
,
mockMetrics
);
vm
.
height
=
defaultHeight
;
vm
.
width
=
defaultWidth
;
vm
.
pathD
=
`M
${
pathD
}
`
;
vm
.
pathViewBox
=
`0 0
${
pathViewBox
.
lineWidth
}
${
pathViewBox
.
diff
}
`
;
vm
.
dotX
=
dotX
;
vm
.
dotY
=
dotY
;
Vue
.
nextTick
(()
=>
{
const
svgEl
=
el
.
querySelector
(
'
svg
'
);
expect
(
svgEl
).
toBeDefined
();
expect
(
svgEl
.
getAttribute
(
'
height
'
)).
toBe
(
defaultHeight
);
expect
(
svgEl
.
getAttribute
(
'
width
'
)).
toBe
(
defaultWidth
);
const
pathEl
=
el
.
querySelector
(
'
path
'
);
expect
(
pathEl
).
toBeDefined
();
expect
(
pathEl
.
getAttribute
(
'
d
'
)).
toBe
(
`M
${
pathD
}
`
);
expect
(
pathEl
.
getAttribute
(
'
viewBox
'
)).
toBe
(
`0 0
${
pathViewBox
.
lineWidth
}
${
pathViewBox
.
diff
}
`
,
);
const
circleEl
=
el
.
querySelector
(
'
circle
'
);
expect
(
circleEl
).
toBeDefined
();
expect
(
circleEl
.
getAttribute
(
'
r
'
)).
toBe
(
'
1.5
'
);
expect
(
circleEl
.
getAttribute
(
'
transform
'
)).
toBe
(
'
translate(0 -1)
'
);
expect
(
circleEl
.
getAttribute
(
'
cx
'
)).
toBe
(
`
${
dotX
}
`
);
expect
(
circleEl
.
getAttribute
(
'
cy
'
)).
toBe
(
`
${
dotY
}
`
);
done
();
});
});
});
});
spec/javascripts/vue_shared/components/mock_data.js
deleted
100644 → 0
View file @
2d0d3ef5
export
const
mockMetrics
=
[
[
1493716685
,
'
4.30859375
'
],
[
1493716745
,
'
4.30859375
'
],
[
1493716805
,
'
4.30859375
'
],
[
1493716865
,
'
4.30859375
'
],
[
1493716925
,
'
4.30859375
'
],
[
1493716985
,
'
4.30859375
'
],
[
1493717045
,
'
4.30859375
'
],
[
1493717105
,
'
4.30859375
'
],
[
1493717165
,
'
4.30859375
'
],
[
1493717225
,
'
4.30859375
'
],
[
1493717285
,
'
4.30859375
'
],
[
1493717345
,
'
4.30859375
'
],
[
1493717405
,
'
4.30859375
'
],
[
1493717465
,
'
4.30859375
'
],
[
1493717525
,
'
4.30859375
'
],
[
1493717585
,
'
4.30859375
'
],
[
1493717645
,
'
4.30859375
'
],
[
1493717705
,
'
4.30859375
'
],
[
1493717765
,
'
4.30859375
'
],
[
1493717825
,
'
4.30859375
'
],
[
1493717885
,
'
4.30859375
'
],
[
1493717945
,
'
4.30859375
'
],
[
1493718005
,
'
4.30859375
'
],
[
1493718065
,
'
4.30859375
'
],
[
1493718125
,
'
4.30859375
'
],
[
1493718185
,
'
4.30859375
'
],
[
1493718245
,
'
4.30859375
'
],
[
1493718305
,
'
4.234375
'
],
[
1493718365
,
'
4.234375
'
],
[
1493718425
,
'
4.234375
'
],
[
1493718485
,
'
4.234375
'
],
[
1493718545
,
'
4.243489583333333
'
],
[
1493718605
,
'
4.2109375
'
],
[
1493718665
,
'
4.2109375
'
],
[
1493718725
,
'
4.2109375
'
],
[
1493718785
,
'
4.26171875
'
],
[
1493718845
,
'
4.26171875
'
],
[
1493718905
,
'
4.26171875
'
],
[
1493718965
,
'
4.26171875
'
],
[
1493719025
,
'
4.26171875
'
],
[
1493719085
,
'
4.26171875
'
],
[
1493719145
,
'
4.26171875
'
],
[
1493719205
,
'
4.26171875
'
],
[
1493719265
,
'
4.26171875
'
],
[
1493719325
,
'
4.26171875
'
],
[
1493719385
,
'
4.26171875
'
],
[
1493719445
,
'
4.26171875
'
],
[
1493719505
,
'
4.26171875
'
],
[
1493719565
,
'
4.26171875
'
],
[
1493719625
,
'
4.26171875
'
],
[
1493719685
,
'
4.26171875
'
],
[
1493719745
,
'
4.26171875
'
],
[
1493719805
,
'
4.26171875
'
],
[
1493719865
,
'
4.26171875
'
],
[
1493719925
,
'
4.26171875
'
],
[
1493719985
,
'
4.26171875
'
],
[
1493720045
,
'
4.26171875
'
],
[
1493720105
,
'
4.26171875
'
],
[
1493720165
,
'
4.26171875
'
],
[
1493720225
,
'
4.26171875
'
],
[
1493720285
,
'
4.26171875
'
],
];
export
const
mockMedian
=
1493718485
;
export
const
mockMedianIndex
=
30
;
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