Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
N
nexpie-grafana-theme
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kornkitt Poolsup
nexpie-grafana-theme
Commits
f9281742
Unverified
Commit
f9281742
authored
Nov 19, 2020
by
Hugo Häggmark
Committed by
GitHub
Nov 19, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
CloudMonitoring: Support request cancellation properly (#28847)
parent
294770f4
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
205 additions
and
145 deletions
+205
-145
public/app/plugins/datasource/cloud-monitoring/api.test.ts
+40
-36
public/app/plugins/datasource/cloud-monitoring/api.ts
+46
-32
public/app/plugins/datasource/cloud-monitoring/datasource.ts
+104
-77
public/app/plugins/datasource/cloud-monitoring/specs/datasource.test.ts
+0
-0
public/test/helpers/createFetchResponse.ts
+15
-0
No files found.
public/app/plugins/datasource/cloud-monitoring/api.test.ts
View file @
f9281742
import
{
of
}
from
'rxjs'
;
import
Api
from
'./api'
;
import
Api
from
'./api'
;
import
{
backendSrv
}
from
'app/core/services/backend_srv'
;
// will use the version in __mocks__
import
{
backendSrv
}
from
'app/core/services/backend_srv'
;
// will use the version in __mocks__
import
{
SelectableValue
}
from
'@grafana/data
'
;
import
{
createFetchResponse
}
from
'test/helpers/createFetchResponse
'
;
jest
.
mock
(
'@grafana/runtime'
,
()
=>
({
jest
.
mock
(
'@grafana/runtime'
,
()
=>
({
...((
jest
.
requireActual
(
'@grafana/runtime'
)
as
unknown
)
as
object
),
...((
jest
.
requireActual
(
'@grafana/runtime'
)
as
unknown
)
as
object
),
...
@@ -12,58 +14,60 @@ const response = [
...
@@ -12,58 +14,60 @@ const response = [
{
label
:
'test2'
,
value
:
'test2'
},
{
label
:
'test2'
,
value
:
'test2'
},
];
];
describe
(
'api'
,
()
=>
{
type
Args
=
{
path
?:
string
;
options
?:
any
;
response
?:
any
;
cache
?:
any
};
const
datasourceRequestMock
=
jest
.
spyOn
(
backendSrv
,
'datasourceRequest'
);
beforeEach
(()
=>
{
async
function
getTestContext
({
path
=
'some-resource'
,
options
=
{},
response
=
{},
cache
}:
Args
=
{})
{
datasourceRequestMock
.
mockImplementation
((
options
:
any
)
=>
{
jest
.
clearAllMocks
();
const
data
=
{
[
options
.
url
.
match
(
/
([^\/]
*
)\/
*$/
)[
1
]]:
response
};
return
Promise
.
resolve
({
data
,
status
:
200
});
const
fetchMock
=
jest
.
spyOn
(
backendSrv
,
'fetch'
);
});
fetchMock
.
mockImplementation
((
options
:
any
)
=>
{
const
data
=
{
[
options
.
url
.
match
(
/
([^\/]
*
)\/
*$/
)[
1
]]:
response
};
return
of
(
createFetchResponse
(
data
));
});
});
const
api
=
new
Api
(
'/cloudmonitoring/'
);
if
(
cache
)
{
api
.
cache
[
path
]
=
cache
;
}
const
res
=
await
api
.
get
(
path
,
options
);
return
{
res
,
api
,
fetchMock
};
}
describe
(
'api'
,
()
=>
{
describe
(
'when resource was cached'
,
()
=>
{
describe
(
'when resource was cached'
,
()
=>
{
let
api
:
Api
;
it
(
'should return cached value and not load from source'
,
async
()
=>
{
let
res
:
Array
<
SelectableValue
<
string
>>
;
const
path
=
'some-resource'
;
beforeEach
(
async
()
=>
{
const
{
res
,
api
,
fetchMock
}
=
await
getTestContext
({
path
,
cache
:
response
});
api
=
new
Api
(
'/cloudmonitoring/'
);
api
.
cache
[
'some-resource'
]
=
response
;
res
=
await
api
.
get
(
'some-resource'
);
});
it
(
'should return cached value and not load from source'
,
()
=>
{
expect
(
res
).
toEqual
(
response
);
expect
(
res
).
toEqual
(
response
);
expect
(
api
.
cache
[
'some-resource'
]).
toEqual
(
response
);
expect
(
api
.
cache
[
path
]).
toEqual
(
response
);
expect
(
datasourceRequest
Mock
).
not
.
toHaveBeenCalled
();
expect
(
fetch
Mock
).
not
.
toHaveBeenCalled
();
});
});
});
});
describe
(
'when resource was not cached'
,
()
=>
{
describe
(
'when resource was not cached'
,
()
=>
{
let
api
:
Api
;
it
(
'should return from source and not from cache'
,
async
()
=>
{
let
res
:
Array
<
SelectableValue
<
string
>>
;
const
path
=
'some-resource'
;
beforeEach
(
async
()
=>
{
const
{
res
,
api
,
fetchMock
}
=
await
getTestContext
({
path
,
response
});
api
=
new
Api
(
'/cloudmonitoring/'
);
res
=
await
api
.
get
(
'some-resource'
);
});
it
(
'should return cached value and not load from source'
,
()
=>
{
expect
(
res
).
toEqual
(
response
);
expect
(
res
).
toEqual
(
response
);
expect
(
api
.
cache
[
'some-resource'
]).
toEqual
(
response
);
expect
(
api
.
cache
[
path
]).
toEqual
(
response
);
expect
(
datasourceRequest
Mock
).
toHaveBeenCalled
();
expect
(
fetch
Mock
).
toHaveBeenCalled
();
});
});
});
});
describe
(
'when cache should be bypassed'
,
()
=>
{
describe
(
'when cache should be bypassed'
,
()
=>
{
let
api
:
Api
;
it
(
'should return from source and not from cache'
,
async
()
=>
{
let
res
:
Array
<
SelectableValue
<
string
>>
;
const
options
=
{
useCache
:
false
};
beforeEach
(
async
()
=>
{
const
path
=
'some-resource'
;
api
=
new
Api
(
'/cloudmonitoring/'
);
const
{
res
,
fetchMock
}
=
await
getTestContext
({
path
,
response
,
cache
:
response
,
options
});
api
.
cache
[
'some-resource'
]
=
response
;
res
=
await
api
.
get
(
'some-resource'
,
{
useCache
:
false
});
});
it
(
'should return cached value and not load from source'
,
()
=>
{
expect
(
res
).
toEqual
(
response
);
expect
(
res
).
toEqual
(
response
);
expect
(
datasourceRequest
Mock
).
toHaveBeenCalled
();
expect
(
fetch
Mock
).
toHaveBeenCalled
();
});
});
});
});
});
});
public/app/plugins/datasource/cloud-monitoring/api.ts
View file @
f9281742
import
appEvents
from
'app/core/app_event
s'
;
import
{
Observable
,
of
}
from
'rxj
s'
;
import
{
CoreEvents
}
from
'app/type
s'
;
import
{
catchError
,
map
}
from
'rxjs/operator
s'
;
import
{
SelectableValue
}
from
'@grafana/data'
;
import
{
SelectableValue
}
from
'@grafana/data'
;
import
{
getBackendSrv
}
from
'@grafana/runtime'
;
import
{
FetchResponse
,
getBackendSrv
}
from
'@grafana/runtime'
;
import
appEvents
from
'app/core/app_events'
;
import
{
CoreEvents
}
from
'app/types'
;
import
{
formatCloudMonitoringError
}
from
'./functions'
;
import
{
formatCloudMonitoringError
}
from
'./functions'
;
import
{
MetricDescriptor
}
from
'./types'
;
import
{
MetricDescriptor
}
from
'./types'
;
export
interface
PostResponse
{
results
:
Record
<
string
,
any
>
;
}
interface
Options
{
interface
Options
{
responseMap
?:
(
res
:
any
)
=>
SelectableValue
<
string
>
|
MetricDescriptor
;
responseMap
?:
(
res
:
any
)
=>
SelectableValue
<
string
>
|
MetricDescriptor
;
baseUrl
?:
string
;
baseUrl
?:
string
;
...
@@ -25,48 +31,56 @@ export default class Api {
...
@@ -25,48 +31,56 @@ export default class Api {
};
};
}
}
async
get
(
path
:
string
,
options
?:
Options
):
Promise
<
Array
<
SelectableValue
<
string
>>
|
MetricDescriptor
[]
>
{
get
(
path
:
string
,
options
?:
Options
):
Promise
<
Array
<
SelectableValue
<
string
>>
|
MetricDescriptor
[]
>
{
try
{
const
{
useCache
,
responseMap
,
baseUrl
}
=
{
...
this
.
defaultOptions
,
...
options
};
const
{
useCache
,
responseMap
,
baseUrl
}
=
{
...
this
.
defaultOptions
,
...
options
};
if
(
useCache
&&
this
.
cache
[
path
])
{
if
(
useCache
&&
this
.
cache
[
path
])
{
return
this
.
cache
[
path
]
;
return
Promise
.
resolve
(
this
.
cache
[
path
])
;
}
}
const
response
=
await
getBackendSrv
().
datasourceRequest
({
return
getBackendSrv
()
.
fetch
<
Record
<
string
,
any
>>
({
url
:
baseUrl
+
path
,
url
:
baseUrl
+
path
,
method
:
'GET'
,
method
:
'GET'
,
});
})
.
pipe
(
const
responsePropName
=
path
.
match
(
/
([^\/]
*
)\/
*$/
)
!
[
1
];
map
(
response
=>
{
let
res
=
[];
const
responsePropName
=
path
.
match
(
/
([^\/]
*
)\/
*$/
)
!
[
1
];
if
(
response
&&
response
.
data
&&
response
.
data
[
responsePropName
])
{
let
res
=
[];
res
=
response
.
data
[
responsePropName
].
map
(
responseMap
);
if
(
response
&&
response
.
data
&&
response
.
data
[
responsePropName
])
{
}
res
=
response
.
data
[
responsePropName
].
map
(
responseMap
);
}
if
(
useCache
)
{
if
(
useCache
)
{
this
.
cache
[
path
]
=
res
;
this
.
cache
[
path
]
=
res
;
}
}
return
res
;
return
res
;
}
catch
(
error
)
{
}),
appEvents
.
emit
(
CoreEvents
.
dsRequestError
,
{
error
:
{
data
:
{
error
:
formatCloudMonitoringError
(
error
)
}
}
});
catchError
(
error
=>
{
return
[];
appEvents
.
emit
(
CoreEvents
.
dsRequestError
,
{
}
error
:
{
data
:
{
error
:
formatCloudMonitoringError
(
error
)
}
},
});
return
of
([]);
})
)
.
toPromise
();
}
}
async
post
(
data
:
{
[
key
:
string
]:
any
})
{
post
(
data
:
Record
<
string
,
any
>
):
Observable
<
FetchResponse
<
PostResponse
>>
{
return
getBackendSrv
().
datasourceRequest
({
return
getBackendSrv
().
fetch
<
PostResponse
>
({
url
:
'/api/tsdb/query'
,
url
:
'/api/tsdb/query'
,
method
:
'POST'
,
method
:
'POST'
,
data
,
data
,
});
});
}
}
async
test
(
projectName
:
string
)
{
test
(
projectName
:
string
)
{
return
getBackendSrv
().
datasourceRequest
({
return
getBackendSrv
()
url
:
`
${
this
.
baseUrl
}${
projectName
}
/metricDescriptors`
,
.
fetch
<
any
>
({
method
:
'GET'
,
url
:
`
${
this
.
baseUrl
}${
projectName
}
/metricDescriptors`
,
});
method
:
'GET'
,
})
.
toPromise
();
}
}
}
}
public/app/plugins/datasource/cloud-monitoring/datasource.ts
View file @
f9281742
...
@@ -14,8 +14,10 @@ import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
...
@@ -14,8 +14,10 @@ import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import
{
CloudMonitoringOptions
,
CloudMonitoringQuery
,
Filter
,
MetricDescriptor
,
QueryType
}
from
'./types'
;
import
{
CloudMonitoringOptions
,
CloudMonitoringQuery
,
Filter
,
MetricDescriptor
,
QueryType
}
from
'./types'
;
import
{
cloudMonitoringUnitMappings
}
from
'./constants'
;
import
{
cloudMonitoringUnitMappings
}
from
'./constants'
;
import
API
from
'./api'
;
import
API
,
{
PostResponse
}
from
'./api'
;
import
{
CloudMonitoringVariableSupport
}
from
'./variables'
;
import
{
CloudMonitoringVariableSupport
}
from
'./variables'
;
import
{
catchError
,
map
,
mergeMap
}
from
'rxjs/operators'
;
import
{
from
,
Observable
,
of
,
throwError
}
from
'rxjs'
;
export
default
class
CloudMonitoringDatasource
extends
DataSourceApi
<
CloudMonitoringQuery
,
CloudMonitoringOptions
>
{
export
default
class
CloudMonitoringDatasource
extends
DataSourceApi
<
CloudMonitoringQuery
,
CloudMonitoringOptions
>
{
api
:
API
;
api
:
API
;
...
@@ -37,45 +39,52 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
...
@@ -37,45 +39,52 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
return
this
.
templateSrv
.
getVariables
().
map
(
v
=>
`$
${
v
.
name
}
`
);
return
this
.
templateSrv
.
getVariables
().
map
(
v
=>
`$
${
v
.
name
}
`
);
}
}
async
query
(
options
:
DataQueryRequest
<
CloudMonitoringQuery
>
):
Promise
<
DataQueryResponseData
>
{
query
(
options
:
DataQueryRequest
<
CloudMonitoringQuery
>
):
Observable
<
DataQueryResponseData
>
{
const
result
:
DataQueryResponseData
[]
=
[];
return
this
.
getTimeSeries
(
options
).
pipe
(
const
data
=
await
this
.
getTimeSeries
(
options
);
map
(
data
=>
{
if
(
data
.
results
)
{
if
(
!
data
.
results
)
{
Object
.
values
(
data
.
results
).
forEach
((
queryRes
:
any
)
=>
{
return
{
data
:
[]
};
if
(
!
queryRes
.
series
)
{
return
;
}
}
const
unit
=
this
.
resolvePanelUnitFromTargets
(
options
.
targets
);
queryRes
.
series
.
forEach
((
series
:
any
)
=>
{
const
result
:
DataQueryResponseData
[]
=
[];
let
timeSerie
:
any
=
{
const
values
=
Object
.
values
(
data
.
results
);
target
:
series
.
name
,
for
(
const
queryRes
of
values
)
{
datapoints
:
series
.
points
,
if
(
!
queryRes
.
series
)
{
refId
:
queryRes
.
refId
,
continue
;
meta
:
queryRes
.
meta
,
};
if
(
unit
)
{
timeSerie
=
{
...
timeSerie
,
unit
};
}
}
const
df
=
toDataFrame
(
timeSerie
);
const
unit
=
this
.
resolvePanelUnitFromTargets
(
options
.
targets
);
for
(
const
field
of
df
.
fields
)
{
if
(
queryRes
.
meta
?.
deepLink
&&
queryRes
.
meta
?.
deepLink
.
length
>
0
)
{
for
(
const
series
of
queryRes
.
series
)
{
field
.
config
.
links
=
[
let
timeSerie
:
any
=
{
{
target
:
series
.
name
,
url
:
queryRes
.
meta
?.
deepLink
,
datapoints
:
series
.
points
,
title
:
'View in Metrics Explorer'
,
refId
:
queryRes
.
refId
,
targetBlank
:
true
,
meta
:
queryRes
.
meta
,
},
};
];
if
(
unit
)
{
timeSerie
=
{
...
timeSerie
,
unit
};
}
}
const
df
=
toDataFrame
(
timeSerie
);
for
(
const
field
of
df
.
fields
)
{
if
(
queryRes
.
meta
?.
deepLink
&&
queryRes
.
meta
?.
deepLink
.
length
>
0
)
{
field
.
config
.
links
=
[
{
url
:
queryRes
.
meta
?.
deepLink
,
title
:
'View in Metrics Explorer'
,
targetBlank
:
true
,
},
];
}
}
result
.
push
(
df
);
}
}
result
.
push
(
df
);
}
});
});
return
{
data
:
result
};
return
{
data
:
result
};
})
}
else
{
);
return
{
data
:
[]
};
}
}
}
async
annotationQuery
(
options
:
any
)
{
async
annotationQuery
(
options
:
any
)
{
...
@@ -101,47 +110,57 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
...
@@ -101,47 +110,57 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
},
},
];
];
const
{
data
}
=
await
this
.
api
.
post
({
return
this
.
api
from
:
options
.
range
.
from
.
valueOf
().
toString
(),
.
post
({
to
:
options
.
range
.
to
.
valueOf
().
toString
(),
from
:
options
.
range
.
from
.
valueOf
().
toString
(),
queries
,
to
:
options
.
range
.
to
.
valueOf
().
toString
(),
});
queries
,
})
const
results
=
data
.
results
[
'annotationQuery'
].
tables
[
0
].
rows
.
map
((
v
:
any
)
=>
{
.
pipe
(
return
{
map
(({
data
})
=>
{
annotation
:
annotation
,
const
results
=
data
.
results
[
'annotationQuery'
].
tables
[
0
].
rows
.
map
((
v
:
any
)
=>
{
time
:
Date
.
parse
(
v
[
0
]),
return
{
title
:
v
[
1
],
annotation
:
annotation
,
tags
:
[],
time
:
Date
.
parse
(
v
[
0
]),
text
:
v
[
3
],
title
:
v
[
1
],
}
as
any
;
tags
:
[],
});
text
:
v
[
3
],
}
as
any
;
return
results
;
});
return
results
;
})
)
.
toPromise
();
}
}
async
getTimeSeries
(
options
:
DataQueryRequest
<
CloudMonitoringQuery
>
)
{
getTimeSeries
(
options
:
DataQueryRequest
<
CloudMonitoringQuery
>
):
Observable
<
PostResponse
>
{
await
this
.
ensureGCEDefaultProject
();
const
queries
=
options
.
targets
const
queries
=
options
.
targets
.
map
(
this
.
migrateQuery
)
.
map
(
this
.
migrateQuery
)
.
filter
(
this
.
shouldRunQuery
)
.
filter
(
this
.
shouldRunQuery
)
.
map
(
q
=>
this
.
prepareTimeSeriesQuery
(
q
,
options
.
scopedVars
))
.
map
(
q
=>
this
.
prepareTimeSeriesQuery
(
q
,
options
.
scopedVars
))
.
map
(
q
=>
({
...
q
,
intervalMs
:
options
.
intervalMs
,
type
:
'timeSeriesQuery'
}));
.
map
(
q
=>
({
...
q
,
intervalMs
:
options
.
intervalMs
,
type
:
'timeSeriesQuery'
}));
if
(
queries
.
length
>
0
)
{
if
(
!
queries
.
length
)
{
const
{
data
}
=
await
this
.
api
.
post
({
return
of
({
results
:
[]
});
from
:
options
.
range
.
from
.
valueOf
().
toString
(),
to
:
options
.
range
.
to
.
valueOf
().
toString
(),
queries
,
});
return
data
;
}
else
{
return
{
results
:
[]
};
}
}
return
from
(
this
.
ensureGCEDefaultProject
()).
pipe
(
mergeMap
(()
=>
{
return
this
.
api
.
post
({
from
:
options
.
range
.
from
.
valueOf
().
toString
(),
to
:
options
.
range
.
to
.
valueOf
().
toString
(),
queries
,
});
}),
map
(({
data
})
=>
{
return
data
;
})
);
}
}
async
getLabels
(
metricType
:
string
,
refId
:
string
,
projectName
:
string
,
groupBys
?:
string
[])
{
async
getLabels
(
metricType
:
string
,
refId
:
string
,
projectName
:
string
,
groupBys
?:
string
[])
{
const
response
=
await
this
.
getTimeSeries
({
return
this
.
getTimeSeries
({
targets
:
[
targets
:
[
{
{
refId
,
refId
,
...
@@ -157,9 +176,14 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
...
@@ -157,9 +176,14 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
},
},
],
],
range
:
this
.
timeSrv
.
timeRange
(),
range
:
this
.
timeSrv
.
timeRange
(),
}
as
DataQueryRequest
<
CloudMonitoringQuery
>
);
}
as
DataQueryRequest
<
CloudMonitoringQuery
>
)
const
result
=
response
.
results
[
refId
];
.
pipe
(
return
result
&&
result
.
meta
?
result
.
meta
.
labels
:
{};
map
(
response
=>
{
const
result
=
response
.
results
[
refId
];
return
result
&&
result
.
meta
?
result
.
meta
.
labels
:
{};
})
)
.
toPromise
();
}
}
async
testDatasource
()
{
async
testDatasource
()
{
...
@@ -205,14 +229,17 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
...
@@ -205,14 +229,17 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
},
},
],
],
})
})
.
then
(({
data
})
=>
{
.
pipe
(
return
data
&&
data
.
results
&&
data
.
results
.
getGCEDefaultProject
&&
data
.
results
.
getGCEDefaultProject
.
meta
map
(({
data
})
=>
{
?
data
.
results
.
getGCEDefaultProject
.
meta
.
defaultProject
return
data
&&
data
.
results
&&
data
.
results
.
getGCEDefaultProject
&&
data
.
results
.
getGCEDefaultProject
.
meta
:
''
;
?
data
.
results
.
getGCEDefaultProject
.
meta
.
defaultProject
})
:
''
;
.
catch
(
err
=>
{
}),
throw
err
.
data
.
error
;
catchError
(
err
=>
{
});
return
throwError
(
err
.
data
.
error
);
})
)
.
toPromise
();
}
}
getDefaultProject
():
string
{
getDefaultProject
():
string
{
...
@@ -272,7 +299,7 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
...
@@ -272,7 +299,7 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
});
});
}
}
async
getProjects
()
{
getProjects
()
{
return
this
.
api
.
get
(
`projects`
,
{
return
this
.
api
.
get
(
`projects`
,
{
responseMap
:
({
projectId
,
name
}:
{
projectId
:
string
;
name
:
string
})
=>
({
responseMap
:
({
projectId
,
name
}:
{
projectId
:
string
;
name
:
string
})
=>
({
value
:
projectId
,
value
:
projectId
,
...
...
public/app/plugins/datasource/cloud-monitoring/specs/datasource.test.ts
View file @
f9281742
This diff is collapsed.
Click to expand it.
public/test/helpers/createFetchResponse.ts
0 → 100644
View file @
f9281742
import
{
FetchResponse
}
from
'@grafana/runtime'
;
export
function
createFetchResponse
<
T
>
(
data
:
T
):
FetchResponse
<
T
>
{
return
{
data
,
status
:
200
,
url
:
'http://localhost:3000/api/query'
,
config
:
{
url
:
'http://localhost:3000/api/query'
},
type
:
'basic'
,
statusText
:
'Ok'
,
redirected
:
false
,
headers
:
({}
as
unknown
)
as
Headers
,
ok
:
true
,
};
}
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