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
8d585766
Commit
8d585766
authored
Sep 28, 2016
by
Torkel Ödegaard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor(tsdb): changed tsdb time series model to use null.Float instead of pointers
parent
63caedb0
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
101 additions
and
89 deletions
+101
-89
pkg/services/alerting/conditions/evaluator.go
+14
-11
pkg/services/alerting/conditions/evaluator_test.go
+4
-2
pkg/services/alerting/conditions/query.go
+3
-3
pkg/services/alerting/conditions/query_test.go
+14
-19
pkg/services/alerting/conditions/reducer.go
+16
-15
pkg/services/alerting/conditions/reducer_test.go
+11
-14
pkg/tsdb/graphite/graphite.go
+1
-0
pkg/tsdb/graphite/types.go
+4
-2
pkg/tsdb/models.go
+23
-4
pkg/tsdb/prometheus/prometheus.go
+6
-8
pkg/tsdb/testdata/scenarios.go
+5
-11
No files found.
pkg/services/alerting/conditions/evaluator.go
View file @
8d585766
...
...
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/alerting"
"gopkg.in/guregu/null.v3"
)
var
(
...
...
@@ -13,13 +14,13 @@ var (
)
type
AlertEvaluator
interface
{
Eval
(
reducedValue
*
float64
)
bool
Eval
(
reducedValue
null
.
Float
)
bool
}
type
NoDataEvaluator
struct
{}
func
(
e
*
NoDataEvaluator
)
Eval
(
reducedValue
*
float64
)
bool
{
return
reducedValue
==
nil
func
(
e
*
NoDataEvaluator
)
Eval
(
reducedValue
null
.
Float
)
bool
{
return
reducedValue
.
Valid
==
false
}
type
ThresholdEvaluator
struct
{
...
...
@@ -43,16 +44,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
return
defaultEval
,
nil
}
func
(
e
*
ThresholdEvaluator
)
Eval
(
reducedValue
*
float64
)
bool
{
if
reducedValue
==
nil
{
func
(
e
*
ThresholdEvaluator
)
Eval
(
reducedValue
null
.
Float
)
bool
{
if
reducedValue
.
Valid
==
false
{
return
false
}
switch
e
.
Type
{
case
"gt"
:
return
*
reducedValue
>
e
.
Threshold
return
reducedValue
.
Float64
>
e
.
Threshold
case
"lt"
:
return
*
reducedValue
<
e
.
Threshold
return
reducedValue
.
Float64
<
e
.
Threshold
}
return
false
...
...
@@ -86,16 +87,18 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
return
rangedEval
,
nil
}
func
(
e
*
RangedEvaluator
)
Eval
(
reducedValue
*
float64
)
bool
{
if
reducedValue
==
nil
{
func
(
e
*
RangedEvaluator
)
Eval
(
reducedValue
null
.
Float
)
bool
{
if
reducedValue
.
Valid
==
false
{
return
false
}
floatValue
:=
reducedValue
.
Float64
switch
e
.
Type
{
case
"within_range"
:
return
(
e
.
Lower
<
*
reducedValue
&&
e
.
Upper
>
*
reducedValue
)
||
(
e
.
Upper
<
*
reducedValue
&&
e
.
Lower
>
*
reduced
Value
)
return
(
e
.
Lower
<
floatValue
&&
e
.
Upper
>
floatValue
)
||
(
e
.
Upper
<
floatValue
&&
e
.
Lower
>
float
Value
)
case
"outside_range"
:
return
(
e
.
Upper
<
*
reducedValue
&&
e
.
Lower
<
*
reducedValue
)
||
(
e
.
Upper
>
*
reducedValue
&&
e
.
Lower
>
*
reduced
Value
)
return
(
e
.
Upper
<
floatValue
&&
e
.
Lower
<
floatValue
)
||
(
e
.
Upper
>
floatValue
&&
e
.
Lower
>
float
Value
)
}
return
false
...
...
pkg/services/alerting/conditions/evaluator_test.go
View file @
8d585766
...
...
@@ -3,6 +3,8 @@ package conditions
import
(
"testing"
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/components/simplejson"
.
"github.com/smartystreets/goconvey/convey"
)
...
...
@@ -14,7 +16,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64)
evaluator
,
err
:=
NewAlertEvaluator
(
jsonModel
)
So
(
err
,
ShouldBeNil
)
return
evaluator
.
Eval
(
&
reducedValue
)
return
evaluator
.
Eval
(
null
.
FloatFrom
(
reducedValue
)
)
}
func
TestEvalutors
(
t
*
testing
.
T
)
{
...
...
@@ -51,6 +53,6 @@ func TestEvalutors(t *testing.T) {
evaluator
,
err
:=
NewAlertEvaluator
(
jsonModel
)
So
(
err
,
ShouldBeNil
)
So
(
evaluator
.
Eval
(
n
il
),
ShouldBeTrue
)
So
(
evaluator
.
Eval
(
n
ull
.
FloatFromPtr
(
nil
)
),
ShouldBeTrue
)
})
}
pkg/services/alerting/conditions/query.go
View file @
8d585766
...
...
@@ -46,21 +46,21 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
reducedValue
:=
c
.
Reducer
.
Reduce
(
series
)
evalMatch
:=
c
.
Evaluator
.
Eval
(
reducedValue
)
if
reducedValue
==
nil
{
if
reducedValue
.
Valid
==
false
{
emptySerieCount
++
continue
}
if
context
.
IsTestRun
{
context
.
Logs
=
append
(
context
.
Logs
,
&
alerting
.
ResultLogEntry
{
Message
:
fmt
.
Sprintf
(
"Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f"
,
c
.
Index
,
evalMatch
,
series
.
Name
,
*
reducedValue
),
Message
:
fmt
.
Sprintf
(
"Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f"
,
c
.
Index
,
evalMatch
,
series
.
Name
,
reducedValue
.
Float64
),
})
}
if
evalMatch
{
context
.
EvalMatches
=
append
(
context
.
EvalMatches
,
&
alerting
.
EvalMatch
{
Metric
:
series
.
Name
,
Value
:
*
reducedValue
,
Value
:
reducedValue
.
Float64
,
})
}
}
...
...
pkg/services/alerting/conditions/query_test.go
View file @
8d585766
...
...
@@ -3,6 +3,8 @@ package conditions
import
(
"testing"
null
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m
"github.com/grafana/grafana/pkg/models"
...
...
@@ -41,9 +43,8 @@ func TestQueryCondition(t *testing.T) {
})
Convey
(
"should fire when avg is above 100"
,
func
()
{
one
:=
float64
(
120
)
two
:=
float64
(
0
)
ctx
.
series
=
tsdb
.
TimeSeriesSlice
{
tsdb
.
NewTimeSeries
(
"test1"
,
[][
2
]
*
float64
{{
&
one
,
&
two
}})}
points
:=
tsdb
.
NewTimeSeriesPointsFromArgs
(
120
,
0
)
ctx
.
series
=
tsdb
.
TimeSeriesSlice
{
tsdb
.
NewTimeSeries
(
"test1"
,
points
)}
ctx
.
exec
()
So
(
ctx
.
result
.
Error
,
ShouldBeNil
)
...
...
@@ -51,9 +52,8 @@ func TestQueryCondition(t *testing.T) {
})
Convey
(
"Should not fire when avg is below 100"
,
func
()
{
one
:=
float64
(
90
)
two
:=
float64
(
0
)
ctx
.
series
=
tsdb
.
TimeSeriesSlice
{
tsdb
.
NewTimeSeries
(
"test1"
,
[][
2
]
*
float64
{{
&
one
,
&
two
}})}
points
:=
tsdb
.
NewTimeSeriesPointsFromArgs
(
90
,
0
)
ctx
.
series
=
tsdb
.
TimeSeriesSlice
{
tsdb
.
NewTimeSeries
(
"test1"
,
points
)}
ctx
.
exec
()
So
(
ctx
.
result
.
Error
,
ShouldBeNil
)
...
...
@@ -61,11 +61,9 @@ func TestQueryCondition(t *testing.T) {
})
Convey
(
"Should fire if only first serie matches"
,
func
()
{
one
:=
float64
(
120
)
two
:=
float64
(
0
)
ctx
.
series
=
tsdb
.
TimeSeriesSlice
{
tsdb
.
NewTimeSeries
(
"test1"
,
[][
2
]
*
float64
{{
&
one
,
&
two
}}
),
tsdb
.
NewTimeSeries
(
"test2"
,
[][
2
]
*
float64
{{
&
two
,
&
two
}}
),
tsdb
.
NewTimeSeries
(
"test1"
,
tsdb
.
NewTimeSeriesPointsFromArgs
(
120
,
0
)
),
tsdb
.
NewTimeSeries
(
"test2"
,
tsdb
.
NewTimeSeriesPointsFromArgs
(
0
,
0
)
),
}
ctx
.
exec
()
...
...
@@ -76,8 +74,8 @@ func TestQueryCondition(t *testing.T) {
Convey
(
"Empty series"
,
func
()
{
Convey
(
"Should set NoDataFound both series are empty"
,
func
()
{
ctx
.
series
=
tsdb
.
TimeSeriesSlice
{
tsdb
.
NewTimeSeries
(
"test1"
,
[][
2
]
*
float64
{}
),
tsdb
.
NewTimeSeries
(
"test2"
,
[][
2
]
*
float64
{}
),
tsdb
.
NewTimeSeries
(
"test1"
,
tsdb
.
NewTimeSeriesPointsFromArgs
()
),
tsdb
.
NewTimeSeries
(
"test2"
,
tsdb
.
NewTimeSeriesPointsFromArgs
()
),
}
ctx
.
exec
()
...
...
@@ -86,10 +84,9 @@ func TestQueryCondition(t *testing.T) {
})
Convey
(
"Should set NoDataFound both series contains null"
,
func
()
{
one
:=
float64
(
120
)
ctx
.
series
=
tsdb
.
TimeSeriesSlice
{
tsdb
.
NewTimeSeries
(
"test1"
,
[][
2
]
*
float64
{{
nil
,
&
one
}}),
tsdb
.
NewTimeSeries
(
"test2"
,
[][
2
]
*
float64
{{
nil
,
&
one
}}),
tsdb
.
NewTimeSeries
(
"test1"
,
tsdb
.
TimeSeriesPoints
{
tsdb
.
TimePoint
{
null
.
FloatFromPtr
(
nil
),
null
.
FloatFrom
(
0
)
}}),
tsdb
.
NewTimeSeries
(
"test2"
,
tsdb
.
TimeSeriesPoints
{
tsdb
.
TimePoint
{
null
.
FloatFromPtr
(
nil
),
null
.
FloatFrom
(
0
)
}}),
}
ctx
.
exec
()
...
...
@@ -98,11 +95,9 @@ func TestQueryCondition(t *testing.T) {
})
Convey
(
"Should not set NoDataFound if one serie is empty"
,
func
()
{
one
:=
float64
(
120
)
two
:=
float64
(
0
)
ctx
.
series
=
tsdb
.
TimeSeriesSlice
{
tsdb
.
NewTimeSeries
(
"test1"
,
[][
2
]
*
float64
{}
),
tsdb
.
NewTimeSeries
(
"test2"
,
[][
2
]
*
float64
{{
&
one
,
&
two
}}
),
tsdb
.
NewTimeSeries
(
"test1"
,
tsdb
.
NewTimeSeriesPointsFromArgs
()
),
tsdb
.
NewTimeSeries
(
"test2"
,
tsdb
.
NewTimeSeriesPointsFromArgs
(
120
,
0
)
),
}
ctx
.
exec
()
...
...
pkg/services/alerting/conditions/reducer.go
View file @
8d585766
...
...
@@ -4,19 +4,20 @@ import (
"math"
"github.com/grafana/grafana/pkg/tsdb"
"gopkg.in/guregu/null.v3"
)
type
QueryReducer
interface
{
Reduce
(
timeSeries
*
tsdb
.
TimeSeries
)
*
float64
Reduce
(
timeSeries
*
tsdb
.
TimeSeries
)
null
.
Float
}
type
SimpleReducer
struct
{
Type
string
}
func
(
s
*
SimpleReducer
)
Reduce
(
series
*
tsdb
.
TimeSeries
)
*
float64
{
func
(
s
*
SimpleReducer
)
Reduce
(
series
*
tsdb
.
TimeSeries
)
null
.
Float
{
if
len
(
series
.
Points
)
==
0
{
return
n
il
return
n
ull
.
FloatFromPtr
(
nil
)
}
value
:=
float64
(
0
)
...
...
@@ -25,36 +26,36 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
switch
s
.
Type
{
case
"avg"
:
for
_
,
point
:=
range
series
.
Points
{
if
point
[
0
]
!=
nil
{
value
+=
*
point
[
0
]
if
point
[
0
]
.
Valid
{
value
+=
point
[
0
]
.
Float64
allNull
=
false
}
}
value
=
value
/
float64
(
len
(
series
.
Points
))
case
"sum"
:
for
_
,
point
:=
range
series
.
Points
{
if
point
[
0
]
!=
nil
{
value
+=
*
point
[
0
]
if
point
[
0
]
.
Valid
{
value
+=
point
[
0
]
.
Float64
allNull
=
false
}
}
case
"min"
:
value
=
math
.
MaxFloat64
for
_
,
point
:=
range
series
.
Points
{
if
point
[
0
]
!=
nil
{
if
point
[
0
]
.
Valid
{
allNull
=
false
if
value
>
*
point
[
0
]
{
value
=
*
point
[
0
]
if
value
>
point
[
0
]
.
Float64
{
value
=
point
[
0
]
.
Float64
}
}
}
case
"max"
:
value
=
-
math
.
MaxFloat64
for
_
,
point
:=
range
series
.
Points
{
if
point
[
0
]
!=
nil
{
if
point
[
0
]
.
Valid
{
allNull
=
false
if
value
<
*
point
[
0
]
{
value
=
*
point
[
0
]
if
value
<
point
[
0
]
.
Float64
{
value
=
point
[
0
]
.
Float64
}
}
}
...
...
@@ -64,10 +65,10 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
}
if
allNull
{
return
n
il
return
n
ull
.
FloatFromPtr
(
nil
)
}
return
&
value
return
null
.
FloatFrom
(
value
)
}
func
NewSimpleReducer
(
typ
string
)
*
SimpleReducer
{
...
...
pkg/services/alerting/conditions/reducer_test.go
View file @
8d585766
...
...
@@ -10,44 +10,41 @@ import (
func
TestSimpleReducer
(
t
*
testing
.
T
)
{
Convey
(
"Test simple reducer by calculating"
,
t
,
func
()
{
Convey
(
"avg"
,
func
()
{
result
:=
*
testReducer
(
"avg"
,
1
,
2
,
3
)
result
:=
testReducer
(
"avg"
,
1
,
2
,
3
)
So
(
result
,
ShouldEqual
,
float64
(
2
))
})
Convey
(
"sum"
,
func
()
{
result
:=
*
testReducer
(
"sum"
,
1
,
2
,
3
)
result
:=
testReducer
(
"sum"
,
1
,
2
,
3
)
So
(
result
,
ShouldEqual
,
float64
(
6
))
})
Convey
(
"min"
,
func
()
{
result
:=
*
testReducer
(
"min"
,
3
,
2
,
1
)
result
:=
testReducer
(
"min"
,
3
,
2
,
1
)
So
(
result
,
ShouldEqual
,
float64
(
1
))
})
Convey
(
"max"
,
func
()
{
result
:=
*
testReducer
(
"max"
,
1
,
2
,
3
)
result
:=
testReducer
(
"max"
,
1
,
2
,
3
)
So
(
result
,
ShouldEqual
,
float64
(
3
))
})
Convey
(
"count"
,
func
()
{
result
:=
*
testReducer
(
"count"
,
1
,
2
,
3000
)
result
:=
testReducer
(
"count"
,
1
,
2
,
3000
)
So
(
result
,
ShouldEqual
,
float64
(
3
))
})
})
}
func
testReducer
(
typ
string
,
datapoints
...
float64
)
*
float64
{
func
testReducer
(
typ
string
,
datapoints
...
float64
)
float64
{
reducer
:=
NewSimpleReducer
(
typ
)
var
timeserie
[][
2
]
*
float64
dummieTimestamp
:=
float64
(
521452145
)
series
:=
&
tsdb
.
TimeSeries
{
Name
:
"test time serie"
,
}
for
idx
:=
range
datapoints
{
timeserie
=
append
(
timeserie
,
[
2
]
*
float64
{
&
datapoints
[
idx
],
&
dummieTimestamp
}
)
series
.
Points
=
append
(
series
.
Points
,
tsdb
.
NewTimePoint
(
datapoints
[
idx
],
1234134
)
)
}
tsdb
:=
&
tsdb
.
TimeSeries
{
Name
:
"test time serie"
,
Points
:
timeserie
,
}
return
reducer
.
Reduce
(
tsdb
)
return
reducer
.
Reduce
(
series
)
.
Float64
}
pkg/tsdb/graphite/graphite.go
View file @
8d585766
...
...
@@ -80,6 +80,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
result
.
QueryResults
=
make
(
map
[
string
]
*
tsdb
.
QueryResult
)
queryRes
:=
&
tsdb
.
QueryResult
{}
for
_
,
series
:=
range
data
{
queryRes
.
Series
=
append
(
queryRes
.
Series
,
&
tsdb
.
TimeSeries
{
Name
:
series
.
Target
,
...
...
pkg/tsdb/graphite/types.go
View file @
8d585766
package
graphite
import
"github.com/grafana/grafana/pkg/tsdb"
type
TargetResponseDTO
struct
{
Target
string
`json:"target"`
DataPoints
[][
2
]
*
float64
`json:"datapoints"`
Target
string
`json:"target"`
DataPoints
tsdb
.
TimeSeriesPoints
`json:"datapoints"`
}
pkg/tsdb/models.go
View file @
8d585766
package
tsdb
import
"github.com/grafana/grafana/pkg/components/simplejson"
import
(
"github.com/grafana/grafana/pkg/components/simplejson"
"gopkg.in/guregu/null.v3"
)
type
Query
struct
{
RefId
string
...
...
@@ -55,13 +58,29 @@ type QueryResult struct {
}
type
TimeSeries
struct
{
Name
string
`json:"name"`
Points
[][
2
]
*
float64
`json:"points"`
Name
string
`json:"name"`
Points
TimeSeriesPoints
`json:"points"`
}
type
TimePoint
[
2
]
null
.
Float
type
TimeSeriesPoints
[]
TimePoint
type
TimeSeriesSlice
[]
*
TimeSeries
func
NewTimeSeries
(
name
string
,
points
[][
2
]
*
float64
)
*
TimeSeries
{
func
NewTimePoint
(
value
float64
,
timestamp
float64
)
TimePoint
{
return
TimePoint
{
null
.
FloatFrom
(
value
),
null
.
FloatFrom
(
timestamp
)}
}
func
NewTimeSeriesPointsFromArgs
(
values
...
float64
)
TimeSeriesPoints
{
points
:=
make
(
TimeSeriesPoints
,
0
)
for
i
:=
0
;
i
<
len
(
values
);
i
+=
2
{
points
=
append
(
points
,
NewTimePoint
(
values
[
i
],
values
[
i
+
1
]))
}
return
points
}
func
NewTimeSeries
(
name
string
,
points
TimeSeriesPoints
)
*
TimeSeries
{
return
&
TimeSeries
{
Name
:
name
,
Points
:
points
,
...
...
pkg/tsdb/prometheus/prometheus.go
View file @
8d585766
...
...
@@ -140,17 +140,15 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
}
for
_
,
v
:=
range
data
{
var
points
[][
2
]
*
float64
series
:=
tsdb
.
TimeSeries
{
Name
:
formatLegend
(
v
.
Metric
,
query
),
}
for
_
,
k
:=
range
v
.
Values
{
timestamp
:=
float64
(
k
.
Timestamp
)
val
:=
float64
(
k
.
Value
)
points
=
append
(
points
,
[
2
]
*
float64
{
&
val
,
&
timestamp
})
series
.
Points
=
append
(
series
.
Points
,
tsdb
.
NewTimePoint
(
float64
(
k
.
Value
),
float64
(
k
.
Timestamp
.
Unix
()
*
1000
)))
}
queryRes
.
Series
=
append
(
queryRes
.
Series
,
&
tsdb
.
TimeSeries
{
Name
:
formatLegend
(
v
.
Metric
,
query
),
Points
:
points
,
})
queryRes
.
Series
=
append
(
queryRes
.
Series
,
&
series
)
}
queryResults
[
"A"
]
=
queryRes
...
...
pkg/tsdb/testdata/scenarios.go
View file @
8d585766
...
...
@@ -4,7 +4,6 @@ import (
"math/rand"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/tsdb"
)
...
...
@@ -21,7 +20,7 @@ var ScenarioRegistry map[string]*Scenario
func
init
()
{
ScenarioRegistry
=
make
(
map
[
string
]
*
Scenario
)
logger
:=
log
.
New
(
"tsdb.testdata"
)
//
logger := log.New("tsdb.testdata")
registerScenario
(
&
Scenario
{
Id
:
"random_walk"
,
...
...
@@ -33,13 +32,11 @@ func init() {
series
:=
newSeriesForQuery
(
query
)
points
:=
make
(
[][
2
]
*
float64
,
0
)
points
:=
make
(
tsdb
.
TimeSeriesPoints
,
0
)
walker
:=
rand
.
Float64
()
*
100
for
i
:=
int64
(
0
);
i
<
10000
&&
timeWalkerMs
<
to
;
i
++
{
timestamp
:=
float64
(
timeWalkerMs
)
val
:=
float64
(
walker
)
points
=
append
(
points
,
[
2
]
*
float64
{
&
val
,
&
timestamp
})
points
=
append
(
points
,
tsdb
.
NewTimePoint
(
walker
,
float64
(
timeWalkerMs
)))
walker
+=
rand
.
Float64
()
-
0.5
timeWalkerMs
+=
query
.
IntervalMs
...
...
@@ -72,12 +69,9 @@ func init() {
series
:=
newSeriesForQuery
(
query
)
outsideTime
:=
context
.
TimeRange
.
MustGetFrom
()
.
Add
(
-
1
*
time
.
Hour
)
.
Unix
()
*
1000
timestamp
:=
float64
(
outsideTime
)
logger
.
Info
(
"time"
,
"from"
,
timestamp
)
val
:=
float64
(
10
)
series
.
Points
=
append
(
series
.
Points
,
[
2
]
*
float64
{
&
val
,
&
timestamp
})
series
.
Points
=
append
(
series
.
Points
,
tsdb
.
NewTimePoint
(
10
,
float64
(
outsideTime
)))
queryRes
.
Series
=
append
(
queryRes
.
Series
,
series
)
return
queryRes
},
})
...
...
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