Skip to content

Commit fab66a5

Browse files
Merge pull request #21 from NeedleInAJayStack/feature/hisread-with-filter
feature: Adds "HisRead with filter" query
2 parents 6e95c80 + 9f67788 commit fab66a5

File tree

7 files changed

+95
-41
lines changed

7 files changed

+95
-41
lines changed

README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ This is a [Grafana](https://grafana.com/grafana/) plugin that supports direct co
55
and supports standard Haystack API operations as well as custom Axon execution, which is supported by
66
[SkySpark](https://skyfoundry.com/product) and [Haxall](https://haxall.io/).
77

8+
You can find more information on the
9+
[Haystack Grafana Plugin Page](https://grafana.com/grafana/plugins/needleinajaystack-haystack-datasource/)
10+
811
![Dashboard Example](src/img/screenshot/dashboard.png)
912

1013
## Installation
1114

12-
Long term, the goal is to get this into the Grafana plugin repository. Until then, I must manually sign the plugin
13-
for your particular Grafana URL. If interested, please contact me at [email protected].
15+
This data source plugin may be installed within the Grafana "Connect data" UI on compliant versions.
1416

1517
## Usage
1618

@@ -22,7 +24,3 @@ the instructions in the [plugin readme](./src/README.md)
2224
Contributions are very welcome! For details on how to develop this plugin, see the
2325
[development guide](./DEVELOPMENT_GUIDE.md).
2426

25-
## Continuing Work
26-
27-
- [ ] Publish plugin
28-
- [ ] Consider enabling multi-point hisRead (through filters)

pkg/plugin/datasource.go

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,15 @@ func (datasource *Datasource) QueryData(ctx context.Context, req *backend.QueryD
9696
}
9797

9898
type QueryModel struct {
99-
Type string `json:"type"`
100-
Nav *string `json:"nav"` // A zinc-encoded Ref or null
101-
Eval string `json:"eval"`
102-
HisRead string `json:"hisRead"`
103-
Read string `json:"read"`
99+
Type string `json:"type"`
100+
Nav *string `json:"nav"` // A zinc-encoded Ref or null
101+
Eval string `json:"eval"`
102+
HisRead string `json:"hisRead"`
103+
HisReadFilter string `json:"hisReadFilter"`
104+
Read string `json:"read"`
104105
}
105106

106107
func (datasource *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse {
107-
var response backend.DataResponse
108-
109108
// Unmarshal the JSON into our queryModel.
110109
var model QueryModel
111110

@@ -122,58 +121,92 @@ func (datasource *Datasource) query(ctx context.Context, pCtx backend.PluginCont
122121
"$__interval": haystack.NewNumber(query.Interval.Minutes(), "min").ToZinc(),
123122
}
124123

125-
var grid haystack.Grid
126124
switch model.Type {
125+
case "":
126+
// If no type is specified, just return an empty response.
127+
return responseFromGrids([]haystack.Grid{})
127128
case "ops":
128129
ops, err := datasource.ops()
129130
if err != nil {
130131
log.DefaultLogger.Error(err.Error())
131132
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Ops failure: %v", err.Error()))
132133
}
133-
grid = ops
134+
return responseFromGrids([]haystack.Grid{ops})
134135
case "nav":
135136
nav, err := datasource.nav(model.Nav)
136137
if err != nil {
137138
log.DefaultLogger.Error(err.Error())
138139
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Nav failure: %v", err.Error()))
139140
}
140-
grid = nav
141+
return responseFromGrids([]haystack.Grid{nav})
141142
case "eval":
142143
eval, err := datasource.eval(model.Eval, variables)
143144
if err != nil {
144145
log.DefaultLogger.Error(err.Error())
145146
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Eval failure: %v", err.Error()))
146147
}
147-
grid = eval
148+
return responseFromGrids([]haystack.Grid{eval})
148149
case "hisRead":
149-
hisRead, err := datasource.hisRead(model.HisRead, query.TimeRange)
150+
hisRead, err := datasource.hisRead(haystack.NewRef(model.HisRead, ""), query.TimeRange)
150151
if err != nil {
151152
log.DefaultLogger.Error(err.Error())
152153
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("HisRead failure: %v", err.Error()))
153154
}
154-
grid = hisRead
155+
return responseFromGrids([]haystack.Grid{hisRead})
156+
case "hisReadFilter":
157+
points, readErr := datasource.read(model.HisReadFilter, variables)
158+
if readErr != nil {
159+
log.DefaultLogger.Error(readErr.Error())
160+
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("HisReadFilter failure: %v", readErr.Error()))
161+
}
162+
163+
grids := []haystack.Grid{}
164+
for _, point := range points.Rows() {
165+
id := point.Get("id")
166+
var ref haystack.Ref
167+
switch id.(type) {
168+
case haystack.Ref:
169+
ref = id.(haystack.Ref)
170+
default:
171+
errMsg := fmt.Sprintf("id is not a ref: %v", id)
172+
log.DefaultLogger.Error(errMsg)
173+
return backend.ErrDataResponse(backend.StatusBadRequest, errMsg)
174+
}
175+
hisRead, err := datasource.hisRead(ref, query.TimeRange)
176+
if err != nil {
177+
log.DefaultLogger.Error(err.Error())
178+
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("HisReadFilter failure: %v", err.Error()))
179+
}
180+
grids = append(grids, hisRead)
181+
}
182+
return responseFromGrids(grids)
155183
case "read":
156184
read, err := datasource.read(model.Read, variables)
157185
if err != nil {
158186
log.DefaultLogger.Error(err.Error())
159187
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Read failure: %v", err.Error()))
160188
}
161-
grid = read
189+
return responseFromGrids([]haystack.Grid{read})
162190
default:
163-
log.DefaultLogger.Warn("No valid input, returning empty Grid")
164-
grid = haystack.EmptyGrid()
165-
}
166-
167-
frame, frameErr := dataFrameFromGrid(grid)
168-
if frameErr != nil {
169-
log.DefaultLogger.Error(frameErr.Error())
170-
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Frame conversion failure: %v", frameErr.Error()))
191+
warnMsg := fmt.Sprintf("Invalid type %s, returning empty Grid", model.Type)
192+
log.DefaultLogger.Warn(warnMsg)
193+
return backend.ErrDataResponse(backend.StatusBadRequest, warnMsg)
171194
}
195+
}
172196

173-
// add the frames to the response.
174-
response.Frames = append(response.Frames, frame)
175-
response.Status = backend.StatusOK
197+
func responseFromGrids(grids []haystack.Grid) backend.DataResponse {
198+
var response backend.DataResponse
199+
for _, grid := range grids {
200+
frame, frameErr := dataFrameFromGrid(grid)
201+
if frameErr != nil {
202+
log.DefaultLogger.Error(frameErr.Error())
203+
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Frame conversion failure: %v", frameErr.Error()))
204+
}
176205

206+
// add the frames to the response.
207+
response.Frames = append(response.Frames, frame)
208+
response.Status = backend.StatusOK
209+
}
177210
return response
178211
}
179212

@@ -220,14 +253,13 @@ func (datasource *Datasource) eval(expr string, variables map[string]string) (ha
220253
)
221254
}
222255

223-
func (datasource *Datasource) hisRead(id string, timeRange backend.TimeRange) (haystack.Grid, error) {
224-
ref := haystack.NewRef(id, "")
256+
func (datasource *Datasource) hisRead(id haystack.Ref, timeRange backend.TimeRange) (haystack.Grid, error) {
225257
start := haystack.NewDateTimeFromGo(timeRange.From.UTC())
226258
end := haystack.NewDateTimeFromGo(timeRange.To.UTC())
227259

228260
return datasource.withRetry(
229261
func() (haystack.Grid, error) {
230-
return datasource.client.HisReadAbsDateTime(ref, start, end)
262+
return datasource.client.HisReadAbsDateTime(id, start, end)
231263
},
232264
)
233265
}

src/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ Haystack query that should be performed. The supported queries are:
2424

2525
- Eval: Evaluate a free-form Axon expression. _Note: Not all Haystack servers support this functionality_
2626
- HisRead: Display the history of a single point over the selected time range.
27-
- Read: Display the records matching a filter. Since this is not timeseries data, it can only be viewed in Grafana's
27+
- HisRead via filter: Read multiple points using a filter, and display their histories over the selected time range.
28+
- Read: Display the records matching a filter. Since this is not timeseries data, it is best viewed in Grafana's
2829
"Table" view.
2930

3031
#### Variable Usage

src/components/HaystackQueryInput.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ export function HaystackQueryInput({ query, onChange }: HaystackQueryInputProps)
3838
/>
3939
</InlineField>
4040
);
41+
case "hisReadFilter":
42+
return (
43+
<InlineField>
44+
<Input
45+
width={width}
46+
prefix={<Icon name="filter" />}
47+
onChange={onQueryChange}
48+
value={query.hisReadFilter}
49+
placeholder={DEFAULT_QUERY.hisReadFilter}
50+
/>
51+
</InlineField>
52+
);
4153
case "read":
4254
return (
4355
<InlineField>

src/components/QueryEditor.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ export function QueryEditor({ datasource, query, onChange, onRunQuery }: Props)
1313
onChange({ ...query, type: newType });
1414
};
1515
const onQueryChange = (newQuery: string) => {
16-
if (query.type === "hisRead") {
17-
onChange({ ...query, hisRead: newQuery });
18-
} else if (query.type === "eval") {
16+
if (query.type === "eval") {
1917
onChange({ ...query, eval: newQuery });
18+
} else if (query.type === "hisRead") {
19+
onChange({ ...query, hisRead: newQuery });
20+
} else if (query.type === "hisReadFilter") {
21+
onChange({ ...query, hisReadFilter: newQuery });
2022
} else if (query.type === "read") {
2123
onChange({ ...query, read: newQuery });
2224
}

src/datasource.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ import {
2222
import { firstValueFrom } from 'rxjs';
2323

2424
export const queryTypes: QueryType[] = [
25-
{ label: 'Read', value: 'read', apiRequirements: ['read'], description: 'Read the records matched by a filter' },
26-
{ label: 'HisRead', value: 'hisRead', apiRequirements: ['hisRead'], description: 'Read the history of a point' },
2725
{ label: 'Eval', value: 'eval', apiRequirements: ['eval'], description: 'Evaluate an Axon expression' },
26+
{ label: 'HisRead', value: 'hisRead', apiRequirements: ['hisRead'], description: 'Read the history of a point' },
27+
{
28+
label: 'HisRead via filter',
29+
value: 'hisReadFilter',
30+
apiRequirements: ['read', 'hisRead'],
31+
description: 'Read the history of points found using a filter',
32+
},
33+
{ label: 'Read', value: 'read', apiRequirements: ['read'], description: 'Read the records matched by a filter' },
2834
];
2935

3036
export class DataSource extends DataSourceWithBackend<HaystackQuery, HaystackDataSourceOptions> {

src/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface HaystackQuery extends DataQuery {
44
type: string; // Defines the type of query that should be executed
55
eval: string;
66
hisRead: string;
7+
hisReadFilter: string;
78
read: string;
89
}
910

@@ -12,6 +13,7 @@ export class OpsQuery implements HaystackQuery {
1213
type = 'ops';
1314
eval = '';
1415
hisRead = '';
16+
hisReadFilter = '';
1517
read = '';
1618

1719
refId: string;
@@ -34,7 +36,8 @@ export const DEFAULT_QUERY: Partial<HaystackQuery> = {
3436
type: 'eval',
3537
eval: '[{ts: $__timeRange_start, v0: 0}, {ts: $__timeRange_end, v0: 10}].toGrid',
3638
hisRead: 'abcdef-123456',
37-
read: 'point and temp and air and outside',
39+
hisReadFilter: 'point and his and temp and air and outside',
40+
read: 'equip and ahu',
3841
};
3942

4043
/**

0 commit comments

Comments
 (0)