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
055a7b97
Commit
055a7b97
authored
Aug 08, 2019
by
Simon Knox
Committed by
Mike Greiling
Aug 08, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Simplify getTimeDiff function
Pass keyname instead of translated string
parent
eec1ed52
Changes
11
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
141 additions
and
72 deletions
+141
-72
app/assets/javascripts/lib/utils/url_utility.js
app/assets/javascripts/lib/utils/url_utility.js
+2
-2
app/assets/javascripts/monitoring/components/dashboard.vue
app/assets/javascripts/monitoring/components/dashboard.vue
+19
-15
app/assets/javascripts/monitoring/components/embed.vue
app/assets/javascripts/monitoring/components/embed.vue
+13
-5
app/assets/javascripts/monitoring/constants.js
app/assets/javascripts/monitoring/constants.js
+15
-7
app/assets/javascripts/monitoring/stores/actions.js
app/assets/javascripts/monitoring/stores/actions.js
+1
-1
app/assets/javascripts/monitoring/utils.js
app/assets/javascripts/monitoring/utils.js
+17
-28
changelogs/unreleased/62973-specify-time-frame-in-shareable-link-for-embedding-metrics.yml
...fy-time-frame-in-shareable-link-for-embedding-metrics.yml
+5
-0
spec/frontend/lib/utils/url_utility_spec.js
spec/frontend/lib/utils/url_utility_spec.js
+35
-0
spec/javascripts/monitoring/dashboard_spec.js
spec/javascripts/monitoring/dashboard_spec.js
+13
-3
spec/javascripts/monitoring/store/actions_spec.js
spec/javascripts/monitoring/store/actions_spec.js
+2
-2
spec/javascripts/monitoring/utils_spec.js
spec/javascripts/monitoring/utils_spec.js
+19
-9
No files found.
app/assets/javascripts/lib/utils/url_utility.js
View file @
055a7b97
...
@@ -2,8 +2,8 @@ import { join as joinPaths } from 'path';
...
@@ -2,8 +2,8 @@ import { join as joinPaths } from 'path';
// Returns an array containing the value(s) of the
// Returns an array containing the value(s) of the
// of the key passed as an argument
// of the key passed as an argument
export
function
getParameterValues
(
sParam
)
{
export
function
getParameterValues
(
sParam
,
url
=
window
.
location
)
{
const
sPageURL
=
decodeURIComponent
(
window
.
location
.
search
.
substring
(
1
));
const
sPageURL
=
decodeURIComponent
(
new
URL
(
url
)
.
search
.
substring
(
1
));
return
sPageURL
.
split
(
'
&
'
).
reduce
((
acc
,
urlParam
)
=>
{
return
sPageURL
.
split
(
'
&
'
).
reduce
((
acc
,
urlParam
)
=>
{
const
sParameterName
=
urlParam
.
split
(
'
=
'
);
const
sParameterName
=
urlParam
.
split
(
'
=
'
);
...
...
app/assets/javascripts/monitoring/components/dashboard.vue
View file @
055a7b97
...
@@ -18,8 +18,8 @@ import MonitorSingleStatChart from './charts/single_stat.vue';
...
@@ -18,8 +18,8 @@ import MonitorSingleStatChart from './charts/single_stat.vue';
import
PanelType
from
'
./panel_type.vue
'
;
import
PanelType
from
'
./panel_type.vue
'
;
import
GraphGroup
from
'
./graph_group.vue
'
;
import
GraphGroup
from
'
./graph_group.vue
'
;
import
EmptyState
from
'
./empty_state.vue
'
;
import
EmptyState
from
'
./empty_state.vue
'
;
import
{
sidebarAnimationDuration
,
timeWindows
,
timeWindowsKeyNames
}
from
'
../constants
'
;
import
{
sidebarAnimationDuration
,
timeWindows
}
from
'
../constants
'
;
import
{
getTimeDiff
}
from
'
../utils
'
;
import
{
getTimeDiff
,
getTimeWindow
}
from
'
../utils
'
;
let
sidebarMutationObserver
;
let
sidebarMutationObserver
;
...
@@ -147,6 +147,7 @@ export default {
...
@@ -147,6 +147,7 @@ export default {
selectedTimeWindow
:
''
,
selectedTimeWindow
:
''
,
selectedTimeWindowKey
:
''
,
selectedTimeWindowKey
:
''
,
formIsValid
:
null
,
formIsValid
:
null
,
timeWindows
:
{},
};
};
},
},
computed
:
{
computed
:
{
...
@@ -184,17 +185,6 @@ export default {
...
@@ -184,17 +185,6 @@ export default {
currentDashboard
:
this
.
currentDashboard
,
currentDashboard
:
this
.
currentDashboard
,
projectPath
:
this
.
projectPath
,
projectPath
:
this
.
projectPath
,
});
});
this
.
timeWindows
=
timeWindows
;
this
.
selectedTimeWindowKey
=
_
.
escape
(
getParameterValues
(
'
time_window
'
)[
0
])
||
timeWindowsKeyNames
.
eightHours
;
// Set default time window if the selectedTimeWindowKey is bogus
if
(
!
Object
.
keys
(
this
.
timeWindows
).
includes
(
this
.
selectedTimeWindowKey
))
{
this
.
selectedTimeWindowKey
=
timeWindowsKeyNames
.
eightHours
;
}
this
.
selectedTimeWindow
=
this
.
timeWindows
[
this
.
selectedTimeWindowKey
];
},
},
beforeDestroy
()
{
beforeDestroy
()
{
if
(
sidebarMutationObserver
)
{
if
(
sidebarMutationObserver
)
{
...
@@ -205,7 +195,20 @@ export default {
...
@@ -205,7 +195,20 @@ export default {
if
(
!
this
.
hasMetrics
)
{
if
(
!
this
.
hasMetrics
)
{
this
.
setGettingStartedEmptyState
();
this
.
setGettingStartedEmptyState
();
}
else
{
}
else
{
this
.
fetchData
(
getTimeDiff
(
this
.
selectedTimeWindow
));
const
defaultRange
=
getTimeDiff
();
const
start
=
getParameterValues
(
'
start
'
)[
0
]
||
defaultRange
.
start
;
const
end
=
getParameterValues
(
'
end
'
)[
0
]
||
defaultRange
.
end
;
const
range
=
{
start
,
end
,
};
this
.
timeWindows
=
timeWindows
;
this
.
selectedTimeWindowKey
=
getTimeWindow
(
range
);
this
.
selectedTimeWindow
=
this
.
timeWindows
[
this
.
selectedTimeWindowKey
];
this
.
fetchData
(
range
);
sidebarMutationObserver
=
new
MutationObserver
(
this
.
onSidebarMutation
);
sidebarMutationObserver
=
new
MutationObserver
(
this
.
onSidebarMutation
);
sidebarMutationObserver
.
observe
(
document
.
querySelector
(
'
.layout-page
'
),
{
sidebarMutationObserver
.
observe
(
document
.
querySelector
(
'
.layout-page
'
),
{
...
@@ -259,7 +262,8 @@ export default {
...
@@ -259,7 +262,8 @@ export default {
return
this
.
timeWindows
[
key
]
===
this
.
selectedTimeWindow
;
return
this
.
timeWindows
[
key
]
===
this
.
selectedTimeWindow
;
},
},
setTimeWindowParameter
(
key
)
{
setTimeWindowParameter
(
key
)
{
return
`?time_window=
${
key
}
`
;
const
{
start
,
end
}
=
getTimeDiff
(
key
);
return
`?start=
${
encodeURIComponent
(
start
)}
&end=
${
encodeURIComponent
(
end
)}
`
;
},
},
groupHasData
(
group
)
{
groupHasData
(
group
)
{
return
this
.
chartsWithData
(
group
.
metrics
).
length
>
0
;
return
this
.
chartsWithData
(
group
.
metrics
).
length
>
0
;
...
...
app/assets/javascripts/monitoring/components/embed.vue
View file @
055a7b97
<
script
>
<
script
>
import
{
mapActions
,
mapState
}
from
'
vuex
'
;
import
{
mapActions
,
mapState
}
from
'
vuex
'
;
import
{
getParameterValues
,
removeParams
}
from
'
~/lib/utils/url_utility
'
;
import
GraphGroup
from
'
./graph_group.vue
'
;
import
GraphGroup
from
'
./graph_group.vue
'
;
import
MonitorAreaChart
from
'
./charts/area.vue
'
;
import
MonitorAreaChart
from
'
./charts/area.vue
'
;
import
{
sidebarAnimationDuration
,
timeWindowsKeyNames
,
timeWindows
}
from
'
../constants
'
;
import
{
sidebarAnimationDuration
}
from
'
../constants
'
;
import
{
getTimeDiff
}
from
'
../utils
'
;
import
{
getTimeDiff
}
from
'
../utils
'
;
let
sidebarMutationObserver
;
let
sidebarMutationObserver
;
...
@@ -19,10 +20,17 @@ export default {
...
@@ -19,10 +20,17 @@ export default {
},
},
},
},
data
()
{
data
()
{
const
defaultRange
=
getTimeDiff
();
const
start
=
getParameterValues
(
'
start
'
,
this
.
dashboardUrl
)[
0
]
||
defaultRange
.
start
;
const
end
=
getParameterValues
(
'
end
'
,
this
.
dashboardUrl
)[
0
]
||
defaultRange
.
end
;
const
params
=
{
start
,
end
,
};
return
{
return
{
params
:
{
params
,
...
getTimeDiff
(
timeWindows
[
timeWindowsKeyNames
.
eightHours
]),
},
elWidth
:
0
,
elWidth
:
0
,
};
};
},
},
...
@@ -73,7 +81,7 @@ export default {
...
@@ -73,7 +81,7 @@ export default {
prometheusEndpointEnabled
:
true
,
prometheusEndpointEnabled
:
true
,
});
});
this
.
setEndpoints
({
this
.
setEndpoints
({
dashboardEndpoint
:
this
.
dashboardUrl
,
dashboardEndpoint
:
removeParams
([
'
start
'
,
'
end
'
],
this
.
dashboardUrl
)
,
});
});
this
.
setShowErrorBanner
(
false
);
this
.
setShowErrorBanner
(
false
);
},
},
...
...
app/assets/javascripts/monitoring/constants.js
View file @
055a7b97
...
@@ -21,11 +21,19 @@ export const timeWindows = {
...
@@ -21,11 +21,19 @@ export const timeWindows = {
oneWeek
:
__
(
'
1 week
'
),
oneWeek
:
__
(
'
1 week
'
),
};
};
export
const
timeWindowsKeyNames
=
{
export
const
secondsIn
=
{
thirtyMinutes
:
'
thirtyMinutes
'
,
thirtyMinutes
:
60
*
30
,
threeHours
:
'
threeHours
'
,
threeHours
:
60
*
60
*
3
,
eightHours
:
'
eightHours
'
,
eightHours
:
60
*
60
*
8
,
oneDay
:
'
oneDay
'
,
oneDay
:
60
*
60
*
24
*
1
,
threeDays
:
'
threeDays
'
,
threeDays
:
60
*
60
*
24
*
3
,
oneWeek
:
'
oneWeek
'
,
oneWeek
:
60
*
60
*
24
*
7
*
1
,
};
};
export
const
timeWindowsKeyNames
=
Object
.
keys
(
secondsIn
).
reduce
(
(
otherTimeWindows
,
timeWindow
)
=>
({
...
otherTimeWindows
,
[
timeWindow
]:
timeWindow
,
}),
{},
);
app/assets/javascripts/monitoring/stores/actions.js
View file @
055a7b97
...
@@ -151,7 +151,7 @@ function fetchPrometheusResult(prometheusEndpoint, params) {
...
@@ -151,7 +151,7 @@ function fetchPrometheusResult(prometheusEndpoint, params) {
*/
*/
export
const
fetchPrometheusMetric
=
({
commit
},
{
metric
,
params
})
=>
{
export
const
fetchPrometheusMetric
=
({
commit
},
{
metric
,
params
})
=>
{
const
{
start
,
end
}
=
params
;
const
{
start
,
end
}
=
params
;
const
timeDiff
=
end
-
start
;
const
timeDiff
=
(
new
Date
(
end
)
-
new
Date
(
start
))
/
1000
;
const
minStep
=
60
;
const
minStep
=
60
;
const
queryDataPoints
=
600
;
const
queryDataPoints
=
600
;
...
...
app/assets/javascripts/monitoring/utils.js
View file @
055a7b97
import
{
timeWindow
s
}
from
'
./constants
'
;
import
{
secondsIn
,
timeWindowsKeyName
s
}
from
'
./constants
'
;
/**
export
const
getTimeDiff
=
timeWindow
=>
{
* method that converts a predetermined time window to minutes
const
end
=
Math
.
floor
(
Date
.
now
()
/
1000
);
// convert milliseconds to seconds
* defaults to 8 hours as the default option
const
difference
=
secondsIn
[
timeWindow
]
||
secondsIn
.
eightHours
;
* @param {String} timeWindow - The time window to convert to minutes
const
start
=
end
-
difference
;
* @returns {number} The time window in minutes
*/
const
getTimeDifferenceSeconds
=
timeWindow
=>
{
switch
(
timeWindow
)
{
case
timeWindows
.
thirtyMinutes
:
return
60
*
30
;
case
timeWindows
.
threeHours
:
return
60
*
60
*
3
;
case
timeWindows
.
oneDay
:
return
60
*
60
*
24
*
1
;
case
timeWindows
.
threeDays
:
return
60
*
60
*
24
*
3
;
case
timeWindows
.
oneWeek
:
return
60
*
60
*
24
*
7
*
1
;
default
:
return
60
*
60
*
8
;
}
};
export
const
getTimeDiff
=
selectedTimeWindow
=>
{
return
{
const
end
=
Date
.
now
()
/
1000
;
// convert milliseconds to seconds
start
:
new
Date
(
start
*
1000
).
toISOString
(),
const
start
=
end
-
getTimeDifferenceSeconds
(
selectedTimeWindow
);
end
:
new
Date
(
end
*
1000
).
toISOString
(),
};
return
{
start
,
end
};
};
};
export
const
getTimeWindow
=
({
start
,
end
})
=>
Object
.
entries
(
secondsIn
).
reduce
((
acc
,
[
timeRange
,
value
])
=>
{
if
(
end
-
start
===
value
)
{
return
timeRange
;
}
return
acc
;
},
timeWindowsKeyNames
.
eightHours
);
/**
/**
* This method is used to validate if the graph data format for a chart component
* This method is used to validate if the graph data format for a chart component
* that needs a time series as a response from a prometheus query (query_range) is
* that needs a time series as a response from a prometheus query (query_range) is
...
...
changelogs/unreleased/62973-specify-time-frame-in-shareable-link-for-embedding-metrics.yml
0 → 100644
View file @
055a7b97
---
title
:
Allow links to metrics dashboard at a specific time
merge_request
:
31283
author
:
type
:
added
spec/frontend/lib/utils/url_utility_spec.js
View file @
055a7b97
...
@@ -34,6 +34,41 @@ describe('URL utility', () => {
...
@@ -34,6 +34,41 @@ describe('URL utility', () => {
});
});
});
});
describe
(
'
getParameterValues
'
,
()
=>
{
beforeEach
(()
=>
{
setWindowLocation
({
href
:
'
https://gitlab.com?test=passing&multiple=1&multiple=2
'
,
// make our fake location act like real window.location.toString
// URL() (used in getParameterValues) does this if passed an object
toString
()
{
return
this
.
href
;
},
});
});
it
(
'
returns empty array for no params
'
,
()
=>
{
expect
(
urlUtils
.
getParameterValues
()).
toEqual
([]);
});
it
(
'
returns empty array for non-matching params
'
,
()
=>
{
expect
(
urlUtils
.
getParameterValues
(
'
notFound
'
)).
toEqual
([]);
});
it
(
'
returns single match
'
,
()
=>
{
expect
(
urlUtils
.
getParameterValues
(
'
test
'
)).
toEqual
([
'
passing
'
]);
});
it
(
'
returns multiple matches
'
,
()
=>
{
expect
(
urlUtils
.
getParameterValues
(
'
multiple
'
)).
toEqual
([
'
1
'
,
'
2
'
]);
});
it
(
'
accepts url as second arg
'
,
()
=>
{
const
url
=
'
https://gitlab.com?everything=works
'
;
expect
(
urlUtils
.
getParameterValues
(
'
everything
'
,
url
)).
toEqual
([
'
works
'
]);
expect
(
urlUtils
.
getParameterValues
(
'
test
'
,
url
)).
toEqual
([]);
});
});
describe
(
'
mergeUrlParams
'
,
()
=>
{
describe
(
'
mergeUrlParams
'
,
()
=>
{
it
(
'
adds w
'
,
()
=>
{
it
(
'
adds w
'
,
()
=>
{
expect
(
urlUtils
.
mergeUrlParams
({
w
:
1
},
'
#frag
'
)).
toBe
(
'
?w=1#frag
'
);
expect
(
urlUtils
.
mergeUrlParams
({
w
:
1
},
'
#frag
'
)).
toBe
(
'
?w=1#frag
'
);
...
...
spec/javascripts/monitoring/dashboard_spec.js
View file @
055a7b97
...
@@ -307,7 +307,7 @@ describe('Dashboard', () => {
...
@@ -307,7 +307,7 @@ describe('Dashboard', () => {
});
});
spyOn
(
component
.
$store
,
'
dispatch
'
).
and
.
stub
();
spyOn
(
component
.
$store
,
'
dispatch
'
).
and
.
stub
();
const
getTimeDiffSpy
=
spyOnDependency
(
Dashboard
,
'
getTimeDiff
'
);
const
getTimeDiffSpy
=
spyOnDependency
(
Dashboard
,
'
getTimeDiff
'
)
.
and
.
callThrough
()
;
component
.
$store
.
commit
(
component
.
$store
.
commit
(
`monitoringDashboard/
${
types
.
RECEIVE_ENVIRONMENTS_DATA_SUCCESS
}
`
,
`monitoringDashboard/
${
types
.
RECEIVE_ENVIRONMENTS_DATA_SUCCESS
}
`
,
...
@@ -319,7 +319,7 @@ describe('Dashboard', () => {
...
@@ -319,7 +319,7 @@ describe('Dashboard', () => {
Vue
.
nextTick
()
Vue
.
nextTick
()
.
then
(()
=>
{
.
then
(()
=>
{
expect
(
component
.
$store
.
dispatch
).
toHaveBeenCalled
();
expect
(
component
.
$store
.
dispatch
).
toHaveBeenCalled
();
expect
(
getTimeDiffSpy
).
toHaveBeenCalled
With
(
component
.
selectedTimeWindow
);
expect
(
getTimeDiffSpy
).
toHaveBeenCalled
(
);
done
();
done
();
})
})
...
@@ -327,7 +327,17 @@ describe('Dashboard', () => {
...
@@ -327,7 +327,17 @@ describe('Dashboard', () => {
});
});
it
(
'
shows a specific time window selected from the url params
'
,
done
=>
{
it
(
'
shows a specific time window selected from the url params
'
,
done
=>
{
spyOnDependency
(
Dashboard
,
'
getParameterValues
'
).
and
.
returnValue
([
'
thirtyMinutes
'
]);
const
start
=
1564439536
;
const
end
=
1564441336
;
spyOnDependency
(
Dashboard
,
'
getTimeDiff
'
).
and
.
returnValue
({
start
,
end
,
});
spyOnDependency
(
Dashboard
,
'
getParameterValues
'
).
and
.
callFake
(
param
=>
{
if
(
param
===
'
start
'
)
return
[
start
];
if
(
param
===
'
end
'
)
return
[
end
];
return
[];
});
component
=
new
DashboardComponent
({
component
=
new
DashboardComponent
({
el
:
document
.
querySelector
(
'
.prometheus-graphs
'
),
el
:
document
.
querySelector
(
'
.prometheus-graphs
'
),
...
...
spec/javascripts/monitoring/store/actions_spec.js
View file @
055a7b97
...
@@ -313,8 +313,8 @@ describe('Monitoring store actions', () => {
...
@@ -313,8 +313,8 @@ describe('Monitoring store actions', () => {
it
(
'
commits prometheus query result
'
,
done
=>
{
it
(
'
commits prometheus query result
'
,
done
=>
{
const
commit
=
jasmine
.
createSpy
();
const
commit
=
jasmine
.
createSpy
();
const
params
=
{
const
params
=
{
start
:
'
1557216349.469
'
,
start
:
'
2019-08-06T12:40:02.184Z
'
,
end
:
'
1557218149.469
'
,
end
:
'
2019-08-06T20:40:02.184Z
'
,
};
};
const
metric
=
metricsDashboardResponse
.
dashboard
.
panel_groups
[
0
].
panels
[
0
].
metrics
[
0
];
const
metric
=
metricsDashboardResponse
.
dashboard
.
panel_groups
[
0
].
panels
[
0
].
metrics
[
0
];
const
state
=
storeState
();
const
state
=
storeState
();
...
...
spec/javascripts/monitoring/utils_spec.js
View file @
055a7b97
...
@@ -3,28 +3,38 @@ import { timeWindows } from '~/monitoring/constants';
...
@@ -3,28 +3,38 @@ import { timeWindows } from '~/monitoring/constants';
import
{
graphDataPrometheusQuery
,
graphDataPrometheusQueryRange
}
from
'
./mock_data
'
;
import
{
graphDataPrometheusQuery
,
graphDataPrometheusQueryRange
}
from
'
./mock_data
'
;
describe
(
'
getTimeDiff
'
,
()
=>
{
describe
(
'
getTimeDiff
'
,
()
=>
{
function
secondsBetween
({
start
,
end
})
{
return
(
new
Date
(
end
)
-
new
Date
(
start
))
/
1000
;
}
function
minutesBetween
(
timeRange
)
{
return
secondsBetween
(
timeRange
)
/
60
;
}
function
hoursBetween
(
timeRange
)
{
return
minutesBetween
(
timeRange
)
/
60
;
}
it
(
'
defaults to an 8 hour (28800s) difference
'
,
()
=>
{
it
(
'
defaults to an 8 hour (28800s) difference
'
,
()
=>
{
const
params
=
getTimeDiff
();
const
params
=
getTimeDiff
();
expect
(
params
.
end
-
params
.
start
).
toEqual
(
28800
);
expect
(
hoursBetween
(
params
)).
toEqual
(
8
);
});
});
it
(
'
accepts time window as an argument
'
,
()
=>
{
it
(
'
accepts time window as an argument
'
,
()
=>
{
const
params
=
getTimeDiff
(
timeWindows
.
thirtyMinutes
);
const
params
=
getTimeDiff
(
'
thirtyMinutes
'
);
expect
(
params
.
end
-
params
.
start
).
not
.
toEqual
(
2880
0
);
expect
(
minutesBetween
(
params
)).
toEqual
(
3
0
);
});
});
it
(
'
returns a value for every defined time window
'
,
()
=>
{
it
(
'
returns a value for every defined time window
'
,
()
=>
{
const
nonDefaultWindows
=
Object
.
keys
(
timeWindows
).
filter
(
window
=>
window
!==
'
eightHours
'
);
const
nonDefaultWindows
=
Object
.
keys
(
timeWindows
).
filter
(
window
=>
window
!==
'
eightHours
'
);
nonDefaultWindows
.
forEach
(
window
=>
{
nonDefaultWindows
.
forEach
(
timeWindow
=>
{
const
params
=
getTimeDiff
(
timeWindows
[
window
]);
const
params
=
getTimeDiff
(
timeWindow
);
const
diff
=
params
.
end
-
params
.
start
;
// Ensure we're not returning the default, 28800 (the # of seconds in 8 hrs)
// Ensure we're not returning the default
expect
(
diff
).
not
.
toEqual
(
28800
);
expect
(
hoursBetween
(
params
)).
not
.
toEqual
(
8
);
expect
(
typeof
diff
).
toEqual
(
'
number
'
);
});
});
});
});
});
});
...
...
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