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
6366e43a
Unverified
Commit
6366e43a
authored
Apr 03, 2020
by
Ryan McKinley
Committed by
GitHub
Apr 03, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Chore: swap [value,time] order when converting to DataFrame (#23206)
parent
e257536e
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
77 additions
and
68 deletions
+77
-68
packages/grafana-data/src/dataframe/processDataFrame.test.ts
+9
-7
packages/grafana-data/src/dataframe/processDataFrame.ts
+28
-21
public/app/features/explore/utils/ResultProcessor.test.ts
+5
-5
public/app/features/explore/utils/ResultProcessor.ts
+1
-1
public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
+7
-7
public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts
+20
-20
public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts
+4
-4
public/app/plugins/datasource/graphite/specs/datasource.test.ts
+1
-1
public/app/plugins/panel/graph/specs/__snapshots__/data_processor.test.ts.snap
+2
-2
No files found.
packages/grafana-data/src/dataframe/processDataFrame.test.ts
View file @
6366e43a
...
...
@@ -21,16 +21,17 @@ describe('toDataFrame', () => {
],
};
let
series
=
toDataFrame
(
input1
);
expect
(
series
.
fields
[
0
].
name
).
toBe
(
input1
.
target
);
expect
(
series
.
fields
[
1
].
name
).
toBe
(
input1
.
target
);
const
v0
=
series
.
fields
[
0
].
values
;
const
v1
=
series
.
fields
[
1
].
values
;
expect
(
v0
.
length
).
toEqual
(
2
);
expect
(
v0
.
get
(
0
)).
toEqual
(
1
);
expect
(
v0
.
get
(
1
)).
toEqual
(
2
);
expect
(
v1
.
length
).
toEqual
(
2
);
expect
(
v0
.
get
(
0
)).
toEqual
(
100
);
expect
(
v0
.
get
(
1
)).
toEqual
(
200
);
expect
(
v1
.
get
(
0
)).
toEqual
(
1
);
expect
(
v1
.
get
(
1
)).
toEqual
(
2
);
expect
(
v1
.
get
(
0
)).
toEqual
(
100
);
expect
(
v1
.
get
(
1
)).
toEqual
(
200
);
// Should fill a default name if target is empty
const
input2
=
{
...
...
@@ -42,7 +43,7 @@ describe('toDataFrame', () => {
],
};
series
=
toDataFrame
(
input2
);
expect
(
series
.
fields
[
0
].
name
).
toEqual
(
'Value'
);
expect
(
series
.
fields
[
1
].
name
).
toEqual
(
'Value'
);
});
it
(
'assumes TimeSeries values are numbers'
,
()
=>
{
...
...
@@ -54,7 +55,8 @@ describe('toDataFrame', () => {
],
};
const
data
=
toDataFrame
(
input1
);
expect
(
data
.
fields
[
0
].
type
).
toBe
(
FieldType
.
number
);
expect
(
data
.
fields
[
0
].
type
).
toBe
(
FieldType
.
time
);
expect
(
data
.
fields
[
1
].
type
).
toBe
(
FieldType
.
number
);
});
it
(
'keeps dataFrame unchanged'
,
()
=>
{
...
...
packages/grafana-data/src/dataframe/processDataFrame.ts
View file @
6366e43a
...
...
@@ -67,6 +67,12 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
const
fields
=
[
{
name
:
'Time'
,
type
:
FieldType
.
time
,
config
:
{},
values
:
new
ArrayVector
<
number
>
(
times
),
},
{
name
:
timeSeries
.
target
||
'Value'
,
type
:
FieldType
.
number
,
config
:
{
...
...
@@ -75,12 +81,6 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
values
:
new
ArrayVector
<
TimeSeriesValue
>
(
values
),
labels
:
timeSeries
.
tags
,
},
{
name
:
'Time'
,
type
:
FieldType
.
time
,
config
:
{},
values
:
new
ArrayVector
<
number
>
(
times
),
},
];
return
{
...
...
@@ -285,23 +285,22 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData =
const
rowCount
=
frame
.
length
;
const
rows
:
any
[][]
=
[];
for
(
let
i
=
0
;
i
<
rowCount
;
i
++
)
{
const
row
:
any
[]
=
[];
for
(
let
j
=
0
;
j
<
fields
.
length
;
j
++
)
{
row
.
push
(
fields
[
j
].
values
.
get
(
i
));
}
rows
.
push
(
row
);
}
if
(
fields
.
length
===
2
)
{
let
type
=
fields
[
1
].
type
;
if
(
!
type
)
{
type
=
guessFieldTypeForField
(
fields
[
1
])
||
FieldType
.
other
;
}
if
(
type
===
FieldType
.
time
)
{
const
{
timeField
,
timeIndex
}
=
getTimeField
(
frame
);
if
(
timeField
)
{
const
valueIndex
=
timeIndex
===
0
?
1
:
0
;
// Make sure it is [value,time]
for
(
let
i
=
0
;
i
<
rowCount
;
i
++
)
{
rows
.
push
([
fields
[
valueIndex
].
values
.
get
(
i
),
// value
fields
[
timeIndex
!
].
values
.
get
(
i
),
// time
]);
}
return
{
alias
:
fields
[
0
].
name
||
frame
.
name
,
target
:
fields
[
0
].
name
||
frame
.
name
,
alias
:
fields
[
valueIndex
].
name
||
frame
.
name
,
target
:
fields
[
valueIndex
].
name
||
frame
.
name
,
datapoints
:
rows
,
unit
:
fields
[
0
].
config
?
fields
[
0
].
config
.
unit
:
undefined
,
refId
:
frame
.
refId
,
...
...
@@ -310,6 +309,14 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData =
}
}
for
(
let
i
=
0
;
i
<
rowCount
;
i
++
)
{
const
row
:
any
[]
=
[];
for
(
let
j
=
0
;
j
<
fields
.
length
;
j
++
)
{
row
.
push
(
fields
[
j
].
values
.
get
(
i
));
}
rows
.
push
(
row
);
}
if
(
frame
.
meta
&&
frame
.
meta
.
json
)
{
return
{
alias
:
fields
[
0
].
name
||
frame
.
name
,
...
...
public/app/features/explore/utils/ResultProcessor.test.ts
View file @
6366e43a
...
...
@@ -23,8 +23,8 @@ const testContext = (options: any = {}) => {
name
:
'A-series'
,
refId
:
'A'
,
fields
:
[
{
name
:
'A-series'
,
type
:
FieldType
.
number
,
values
:
[
4
,
5
,
6
]
},
{
name
:
'time'
,
type
:
FieldType
.
time
,
values
:
[
100
,
200
,
300
]
},
{
name
:
'A-series'
,
type
:
FieldType
.
number
,
values
:
[
4
,
5
,
6
]
},
],
});
...
...
@@ -100,8 +100,8 @@ describe('ResultProcessor', () => {
describe
(
'when calling getGraphResult'
,
()
=>
{
it
(
'then it should return correct graph result'
,
()
=>
{
const
{
resultProcessor
,
dataFrames
}
=
testContext
();
const
timeField
=
dataFrames
[
0
].
fields
[
1
];
const
valueField
=
dataFrames
[
0
].
fields
[
0
];
const
timeField
=
dataFrames
[
0
].
fields
[
0
];
const
valueField
=
dataFrames
[
0
].
fields
[
1
];
const
theResult
=
resultProcessor
.
getGraphResult
();
expect
(
theResult
).
toEqual
([
...
...
@@ -164,8 +164,8 @@ describe('ResultProcessor', () => {
describe
(
'when calling getLogsResult'
,
()
=>
{
it
(
'then it should return correct logs result'
,
()
=>
{
const
{
resultProcessor
,
dataFrames
}
=
testContext
({
mode
:
ExploreMode
.
Logs
});
const
timeField
=
dataFrames
[
0
].
fields
[
1
];
const
valueField
=
dataFrames
[
0
].
fields
[
0
];
const
timeField
=
dataFrames
[
0
].
fields
[
0
];
const
valueField
=
dataFrames
[
0
].
fields
[
1
];
const
logsDataFrame
=
dataFrames
[
1
];
const
theResult
=
resultProcessor
.
getLogsResult
();
...
...
public/app/features/explore/utils/ResultProcessor.ts
View file @
6366e43a
...
...
@@ -114,7 +114,7 @@ export class ResultProcessor {
export
function
isTimeSeries
(
frame
:
DataFrame
):
boolean
{
if
(
frame
.
fields
.
length
===
2
)
{
if
(
frame
.
fields
[
1
].
type
===
FieldType
.
time
)
{
if
(
frame
.
fields
[
0
].
type
===
FieldType
.
time
)
{
return
true
;
}
}
...
...
public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
View file @
6366e43a
...
...
@@ -174,7 +174,7 @@ describe('CloudWatchDatasource', () => {
it
(
'should return series list'
,
done
=>
{
ctx
.
ds
.
query
(
query
).
then
((
result
:
any
)
=>
{
expect
(
result
.
data
[
0
].
name
).
toBe
(
response
.
results
.
A
.
series
[
0
].
name
);
expect
(
result
.
data
[
0
].
fields
[
0
].
values
.
buffer
[
0
]).
toBe
(
response
.
results
.
A
.
series
[
0
].
points
[
0
][
0
]);
expect
(
result
.
data
[
0
].
fields
[
1
].
values
.
buffer
[
0
]).
toBe
(
response
.
results
.
A
.
series
[
0
].
points
[
0
][
0
]);
done
();
});
});
...
...
@@ -191,8 +191,8 @@ describe('CloudWatchDatasource', () => {
response
.
results
[
'A'
].
meta
.
gmdMeta
=
[{
Expression
:
`REMOVE_EMPTY(SEARCH('some expression'))`
,
Period
:
'300'
}];
ctx
.
ds
.
query
(
query
).
then
((
result
:
any
)
=>
{
expect
(
result
.
data
[
0
].
name
).
toBe
(
response
.
results
.
A
.
series
[
0
].
name
);
expect
(
result
.
data
[
0
].
fields
[
0
].
config
.
links
[
0
].
title
).
toBe
(
'View in CloudWatch console'
);
expect
(
decodeURIComponent
(
result
.
data
[
0
].
fields
[
0
].
config
.
links
[
0
].
url
)).
toContain
(
expect
(
result
.
data
[
0
].
fields
[
1
].
config
.
links
[
0
].
title
).
toBe
(
'View in CloudWatch console'
);
expect
(
decodeURIComponent
(
result
.
data
[
0
].
fields
[
1
].
config
.
links
[
0
].
url
)).
toContain
(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'some expression\'))"}]}`
);
done
();
...
...
@@ -206,7 +206,7 @@ describe('CloudWatchDatasource', () => {
];
ctx
.
ds
.
query
(
query
).
then
((
result
:
any
)
=>
{
expect
(
result
.
data
[
0
].
name
).
toBe
(
response
.
results
.
A
.
series
[
0
].
name
);
expect
(
result
.
data
[
0
].
fields
[
0
].
config
.
links
[
0
].
title
).
toBe
(
'View in CloudWatch console'
);
expect
(
result
.
data
[
0
].
fields
[
1
].
config
.
links
[
0
].
title
).
toBe
(
'View in CloudWatch console'
);
expect
(
decodeURIComponent
(
result
.
data
[
0
].
fields
[
0
].
config
.
links
[
0
].
url
)).
toContain
(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'first expression\'))"},{"expression":"REMOVE_EMPTY(SEARCH(\'second expression\'))"}]}`
);
...
...
@@ -218,7 +218,7 @@ describe('CloudWatchDatasource', () => {
response
.
results
[
'A'
].
meta
.
gmdMeta
=
[{
Period
:
'300'
}];
ctx
.
ds
.
query
(
query
).
then
((
result
:
any
)
=>
{
expect
(
result
.
data
[
0
].
name
).
toBe
(
response
.
results
.
A
.
series
[
0
].
name
);
expect
(
result
.
data
[
0
].
fields
[
0
].
config
.
links
[
0
].
title
).
toBe
(
'View in CloudWatch console'
);
expect
(
result
.
data
[
0
].
fields
[
1
].
config
.
links
[
0
].
title
).
toBe
(
'View in CloudWatch console'
);
expect
(
decodeURIComponent
(
result
.
data
[
0
].
fields
[
0
].
config
.
links
[
0
].
url
)).
toContain
(
`region=us-east-1#metricsV2:graph={\"view\":\"timeSeries\",\"stacked\":false,\"title\":\"A\",\"start\":\"2016-12-31T15:00:00.000Z\",\"end\":\"2016-12-31T16:00:00.000Z\",\"region\":\"us-east-1\",\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-12345678\",{\"stat\":\"Average\",\"period\":\"300\"}]]}`
);
...
...
@@ -230,7 +230,7 @@ describe('CloudWatchDatasource', () => {
query
.
targets
[
0
].
expression
=
'a * 2'
;
response
.
results
[
'A'
].
meta
.
searchExpressions
=
[];
ctx
.
ds
.
query
(
query
).
then
((
result
:
any
)
=>
{
expect
(
result
.
data
[
0
].
fields
[
0
].
config
.
links
).
toBeUndefined
();
expect
(
result
.
data
[
0
].
fields
[
1
].
config
.
links
).
toBeUndefined
();
done
();
});
});
...
...
@@ -456,7 +456,7 @@ describe('CloudWatchDatasource', () => {
it
(
'should return series list'
,
done
=>
{
ctx
.
ds
.
query
(
query
).
then
((
result
:
any
)
=>
{
expect
(
result
.
data
[
0
].
name
).
toBe
(
response
.
results
.
A
.
series
[
0
].
name
);
expect
(
result
.
data
[
0
].
fields
[
0
].
values
.
buffer
[
0
]).
toBe
(
response
.
results
.
A
.
series
[
0
].
points
[
0
][
0
]);
expect
(
result
.
data
[
0
].
fields
[
1
].
values
.
buffer
[
0
]).
toBe
(
response
.
results
.
A
.
series
[
0
].
points
[
0
][
0
]);
done
();
});
});
...
...
public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts
View file @
6366e43a
...
...
@@ -177,8 +177,8 @@ describe('AppInsightsDatasource', () => {
const
data
=
results
.
data
[
0
]
as
DataFrame
;
expect
(
data
.
name
).
toEqual
(
'PrimaryResult'
);
expect
(
data
.
fields
[
0
].
values
.
length
).
toEqual
(
1
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
1558278660000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
2.2075
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
1558278660000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
2.2075
);
});
});
});
...
...
@@ -220,8 +220,8 @@ describe('AppInsightsDatasource', () => {
const
data
=
results
.
data
[
0
]
as
DataFrame
;
expect
(
data
.
name
).
toEqual
(
'paritionA'
);
expect
(
data
.
fields
[
0
].
values
.
length
).
toEqual
(
1
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
1558278660000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
2.2075
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
1558278660000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
2.2075
);
});
});
});
...
...
@@ -280,8 +280,8 @@ describe('AppInsightsDatasource', () => {
expect
(
results
.
data
.
length
).
toBe
(
1
);
const
data
=
results
.
data
[
0
]
as
DataFrame
;
expect
(
data
.
name
).
toEqual
(
'exceptions/server'
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
1558278660000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
2.2075
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
1558278660000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
2.2075
);
});
});
});
...
...
@@ -324,10 +324,10 @@ describe('AppInsightsDatasource', () => {
const
data
=
results
.
data
[
0
]
as
DataFrame
;
expect
(
data
.
name
).
toEqual
(
'exceptions/server'
);
expect
(
data
.
fields
[
0
].
values
.
length
).
toEqual
(
2
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
1504108800000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
3
);
expect
(
data
.
fields
[
1
].
values
.
get
(
1
)).
toEqual
(
1504112400000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
1
)).
toEqual
(
6
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
1504108800000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
3
);
expect
(
data
.
fields
[
0
].
values
.
get
(
1
)).
toEqual
(
1504112400000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
1
)).
toEqual
(
6
);
});
});
});
...
...
@@ -377,18 +377,18 @@ describe('AppInsightsDatasource', () => {
expect
(
results
.
data
.
length
).
toBe
(
2
);
let
data
=
results
.
data
[
0
]
as
DataFrame
;
expect
(
data
.
name
).
toEqual
(
'exceptions/server{client/city="Miami"}'
);
expect
(
data
.
fields
[
0
].
values
.
length
).
toEqual
(
2
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
1504108800000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
10
);
expect
(
data
.
fields
[
1
].
values
.
get
(
1
)).
toEqual
(
1504112400000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
1
)).
toEqual
(
20
);
expect
(
data
.
fields
[
1
].
values
.
length
).
toEqual
(
2
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
1504108800000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
10
);
expect
(
data
.
fields
[
0
].
values
.
get
(
1
)).
toEqual
(
1504112400000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
1
)).
toEqual
(
20
);
data
=
results
.
data
[
1
]
as
DataFrame
;
expect
(
data
.
name
).
toEqual
(
'exceptions/server{client/city="San Antonio"}'
);
expect
(
data
.
fields
[
0
].
values
.
length
).
toEqual
(
2
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
1504108800000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
1
);
expect
(
data
.
fields
[
1
].
values
.
get
(
1
)).
toEqual
(
1504112400000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
1
)).
toEqual
(
2
);
expect
(
data
.
fields
[
1
].
values
.
length
).
toEqual
(
2
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
1504108800000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
1
);
expect
(
data
.
fields
[
0
].
values
.
get
(
1
)).
toEqual
(
1504112400000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
1
)).
toEqual
(
2
);
});
});
});
...
...
public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts
View file @
6366e43a
...
...
@@ -138,10 +138,10 @@ describe('AzureMonitorDatasource', () => {
expect
(
results
.
data
.
length
).
toBe
(
1
);
const
data
=
results
.
data
[
0
]
as
DataFrame
;
expect
(
data
.
name
).
toEqual
(
'Percentage CPU'
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
1558278660000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
2.2075
);
expect
(
data
.
fields
[
1
].
values
.
get
(
1
)).
toEqual
(
1558278720000
);
expect
(
data
.
fields
[
0
].
values
.
get
(
1
)).
toEqual
(
2.29
);
expect
(
data
.
fields
[
0
].
values
.
get
(
0
)).
toEqual
(
1558278660000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
0
)).
toEqual
(
2.2075
);
expect
(
data
.
fields
[
0
].
values
.
get
(
1
)).
toEqual
(
1558278720000
);
expect
(
data
.
fields
[
1
].
values
.
get
(
1
)).
toEqual
(
2.29
);
});
});
});
...
...
public/app/plugins/datasource/graphite/specs/datasource.test.ts
View file @
6366e43a
...
...
@@ -165,7 +165,7 @@ describe('graphiteDatasource', () => {
});
it
(
'should convert to millisecond resolution'
,
()
=>
{
expect
(
results
.
data
[
0
].
fields
[
0
].
values
.
get
(
0
)).
toBe
(
10
);
expect
(
results
.
data
[
0
].
fields
[
1
].
values
.
get
(
0
)).
toBe
(
10
);
});
});
...
...
public/app/plugins/panel/graph/specs/__snapshots__/data_processor.test.ts.snap
View file @
6366e43a
...
...
@@ -24,7 +24,7 @@ Array [
1003,
],
],
"fieldIndex":
0
,
"fieldIndex":
1
,
"hasMsResolution": false,
"id": "Value",
"label": "Value",
...
...
@@ -232,7 +232,7 @@ Array [
1003,
],
],
"fieldIndex":
0
,
"fieldIndex":
1
,
"hasMsResolution": false,
"id": "Value",
"label": "Value",
...
...
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