Skip to content

Commit e0722ce

Browse files
Merge pull request #37 from NeedleInAJayStack/feature/nhaystack-basic
Adds nhaystack support using basic auth
2 parents 28553e5 + 56ed3fc commit e0722ce

9 files changed

Lines changed: 143 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## 0.0.16
4+
Upgrades to haystack v0.1.13 to loosen basic auth server requirements.
5+
36
## 0.0.15
47
Fixes haystack v0.1.12 upgrade.
58

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.21
55
require github.com/grafana/grafana-plugin-sdk-go v0.159.0
66

77
require (
8-
github.com/NeedleInAJayStack/haystack v0.1.12
8+
github.com/NeedleInAJayStack/haystack v0.2.2
99
github.com/google/go-cmp v0.5.9
1010
)
1111

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ github.com/NeedleInAJayStack/haystack v0.1.11 h1:MPKSkIxmwLA7WZm2Vvul54bhaENLrok
4545
github.com/NeedleInAJayStack/haystack v0.1.11/go.mod h1:fTn/58sAzfySfXNkBku0DyEX0LXDOdcyIle5xglh3tA=
4646
github.com/NeedleInAJayStack/haystack v0.1.12 h1:Osz2uoxm52w+Fyx/s6mAsZcQ6DU6hxMPbwWYCsz3r3k=
4747
github.com/NeedleInAJayStack/haystack v0.1.12/go.mod h1:fTn/58sAzfySfXNkBku0DyEX0LXDOdcyIle5xglh3tA=
48+
github.com/NeedleInAJayStack/haystack v0.1.13 h1:VXUsnLPS1vRF5x85xGTXjjM1gf3rMjQwafPLYN6izqA=
49+
github.com/NeedleInAJayStack/haystack v0.1.13/go.mod h1:fTn/58sAzfySfXNkBku0DyEX0LXDOdcyIle5xglh3tA=
50+
github.com/NeedleInAJayStack/haystack v0.2.0 h1:zJE96QkPbKFBTQrwP23j0oksTUh0wbEq/rmk4+AaigM=
51+
github.com/NeedleInAJayStack/haystack v0.2.0/go.mod h1:fTn/58sAzfySfXNkBku0DyEX0LXDOdcyIle5xglh3tA=
52+
github.com/NeedleInAJayStack/haystack v0.2.1 h1:Hy+qWGzMW62DRYQoVEuIwQOgptk37Koy9ix2V39+PQY=
53+
github.com/NeedleInAJayStack/haystack v0.2.1/go.mod h1:fTn/58sAzfySfXNkBku0DyEX0LXDOdcyIle5xglh3tA=
54+
github.com/NeedleInAJayStack/haystack v0.2.2 h1:X7RGIF9y/ibmx07yPPKND8NyAjJSXzUN+IFaWG3Ha0s=
55+
github.com/NeedleInAJayStack/haystack v0.2.2/go.mod h1:fTn/58sAzfySfXNkBku0DyEX0LXDOdcyIle5xglh3tA=
4856
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
4957
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
5058
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=

pkg/plugin/datasource.go

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,17 @@ func (datasource *Datasource) query(ctx context.Context, pCtx backend.PluginCont
149149
}
150150
return responseFromGrids([]haystack.Grid{eval})
151151
case "hisRead":
152-
hisRead, err := datasource.hisRead(haystack.NewRef(model.HisRead, ""), query.TimeRange)
152+
refStr := model.HisRead
153+
points, err := datasource.readById(refStr, variables)
154+
if err != nil {
155+
log.DefaultLogger.Error(err.Error())
156+
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("ReadById failure: %v", err.Error()))
157+
}
158+
if points.RowCount() < 1 {
159+
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Id not found: %v", refStr))
160+
}
161+
point := points.RowAt(0)
162+
hisRead, err := datasource.hisRead(point, query.TimeRange)
153163
if err != nil {
154164
log.DefaultLogger.Error(err.Error())
155165
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("HisRead failure: %v", err.Error()))
@@ -166,37 +176,31 @@ func (datasource *Datasource) query(ctx context.Context, pCtx backend.PluginCont
166176
return response
167177

168178
case "hisReadFilter":
169-
pointsGrid, readErr := datasource.read(model.HisReadFilter+" and hisStart", variables)
179+
pointsGrid, readErr := datasource.read(model.HisReadFilter+" and his", variables)
170180
if readErr != nil {
171181
log.DefaultLogger.Error(readErr.Error())
172182
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("HisReadFilter failure: %v", readErr.Error()))
173183
}
174184
points := pointsGrid.Rows()
175-
recLimit := 300
176-
if len(points) > recLimit {
177-
errMsg := fmt.Sprintf("Query exceeded record limit of %d: %d records", recLimit, len(points))
185+
pointMax := 300
186+
if len(points) == 0 {
187+
errMsg := fmt.Sprintf("Query returned no historized records")
188+
log.DefaultLogger.Error(errMsg)
189+
return backend.ErrDataResponse(backend.StatusBadRequest, errMsg)
190+
}
191+
if len(points) > pointMax {
192+
errMsg := fmt.Sprintf("Query exceeded record limit of %d: %d records", pointMax, len(points))
178193
log.DefaultLogger.Error(errMsg)
179194
return backend.ErrDataResponse(backend.StatusBadRequest, errMsg)
180195
}
181196

182197
// Function to read a single point and send it to a channel.
183198
readPoint := func(point haystack.Row, hisReadChannel chan haystack.Grid, wg *sync.WaitGroup) {
184-
id := point.Get("id")
185-
var ref haystack.Ref
186-
switch id.(type) {
187-
case haystack.Ref:
188-
ref = id.(haystack.Ref)
189-
default:
190-
errMsg := fmt.Sprintf("id is not a ref: %v", id)
191-
log.DefaultLogger.Error(errMsg)
192-
hisReadChannel <- haystack.EmptyGrid()
193-
}
194-
hisRead, err := datasource.hisRead(ref, query.TimeRange)
199+
hisRead, err := datasource.hisRead(point, query.TimeRange)
195200
if err != nil {
196201
log.DefaultLogger.Error(err.Error())
197-
hisReadChannel <- haystack.EmptyGrid()
198202
}
199-
hisReadChannel <- hisRead
203+
hisReadChannel <- hisRead // hisRead is empty under error condition
200204
wg.Done()
201205
}
202206

@@ -310,9 +314,26 @@ func (datasource *Datasource) eval(expr string, variables map[string]string) (ha
310314
)
311315
}
312316

313-
func (datasource *Datasource) hisRead(id haystack.Ref, timeRange backend.TimeRange) (haystack.Grid, error) {
314-
start := haystack.NewDateTimeFromGo(timeRange.From.UTC())
315-
end := haystack.NewDateTimeFromGo(timeRange.To.UTC())
317+
func (datasource *Datasource) hisRead(point haystack.Row, timeRange backend.TimeRange) (haystack.Grid, error) {
318+
id, idIsRef := point.Get("id").(haystack.Ref)
319+
if !idIsRef {
320+
return haystack.EmptyGrid(), fmt.Errorf("id is not a Ref")
321+
}
322+
tz, tzIsStr := point.Get("tz").(haystack.Str)
323+
if !tzIsStr {
324+
return haystack.EmptyGrid(), fmt.Errorf("tz is not a Str: %v", id)
325+
}
326+
327+
// Must convert input date range to the point's timezone.
328+
// See https://github.com/skyfoundry/haystack-java/blob/30380dbbe4b5d9be8eb3f400195b0cdcdcc67b95/src/main/java/org/projecthaystack/server/HServer.java#L328
329+
start, startErr := haystack.NewDateTimeFromGo(timeRange.From).ToTz(tz.String())
330+
if startErr != nil {
331+
return haystack.EmptyGrid(), startErr
332+
}
333+
end, endErr := haystack.NewDateTimeFromGo(timeRange.To).ToTz(tz.String())
334+
if endErr != nil {
335+
return haystack.EmptyGrid(), endErr
336+
}
316337

317338
return datasource.withRetry(
318339
func() (haystack.Grid, error) {
@@ -333,6 +354,19 @@ func (datasource *Datasource) read(filter string, variables map[string]string) (
333354
)
334355
}
335356

357+
func (datasource *Datasource) readById(id string, variables map[string]string) (haystack.Grid, error) {
358+
for name, val := range variables {
359+
id = strings.ReplaceAll(id, name, val)
360+
}
361+
362+
ref := haystack.NewRef(id, "")
363+
return datasource.withRetry(
364+
func() (haystack.Grid, error) {
365+
return datasource.client.ReadByIds([]haystack.Ref{ref})
366+
},
367+
)
368+
}
369+
336370
// nav returns the grid for the given navId, or the root nav if navId is nil
337371
// `navId` is expected to be a zinc-encoded Ref
338372
func (datasource *Datasource) nav(navId *string) (haystack.Grid, error) {
@@ -477,36 +511,35 @@ func dataFrameFromGrid(grid haystack.Grid) (*data.Frame, error) {
477511

478512
// Set Grafana field info from Haystack grid info
479513
config := &data.FieldConfig{}
480-
config.DisplayName = disFromGrid(grid, col)
514+
config.DisplayName = disFromMeta(col.Meta(), col.Name())
481515
config.Unit = unitFromGrid(grid, col)
482516
field.Config = config
483517
fields = append(fields, field)
484518
}
485519

486520
frame := data.NewFrame("response", fields...)
487-
488-
switch id := grid.Meta().Get("id").(type) {
489-
case haystack.Ref:
490-
frame.Name = id.Dis()
491-
default:
492-
frame.Name = ""
493-
}
521+
frameName := disFromMeta(grid.Meta(), "")
522+
frame.Name = frameName
494523
return frame, nil
495524
}
496525

497-
// disFromGrid returns the display name of a column in a haystack grid
498-
func disFromGrid(grid haystack.Grid, col haystack.Col) string {
499-
// If column has 'id' meta, use the Ref name
500-
id := col.Meta().Get("id")
501-
if id != nil {
502-
switch id := id.(type) {
503-
case haystack.Ref:
504-
return id.Dis()
505-
default:
506-
return col.Name()
526+
// disFromMeta returns the display name using metadata. It falls back to the provided string if no other name can be found
527+
func disFromMeta(meta haystack.Dict, name string) string {
528+
// Use meta 'dis'
529+
colDis, success := meta.Get("dis").(haystack.Str)
530+
if success {
531+
return colDis.String()
532+
}
533+
// Then 'id.dis' or 'id'
534+
colRef, success := meta.Get("id").(haystack.Ref)
535+
if success {
536+
if colRef.Dis() != "" {
537+
return colRef.Dis()
507538
}
539+
return colRef.ToZinc()
508540
}
509-
return col.Name()
541+
// Then the input name. We shouldn't get here because all records should have an
542+
return name
510543
}
511544

512545
// unitFromGrid returns the unit of a column in a haystack grid

pkg/plugin/datasource_test.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,19 @@ func TestQueryData_Eval_Dis(t *testing.T) {
7272
}
7373

7474
func TestQueryData_HisRead(t *testing.T) {
75-
response := haystack.NewGridBuilder()
76-
response.AddCol("ts", map[string]haystack.Val{})
77-
response.AddCol("v0", map[string]haystack.Val{})
78-
response.AddRow([]haystack.Val{haystack.NewDateTimeFromGo(time.Unix(0, 0)), haystack.NewNumber(5, "kWh")})
75+
readByIdsResponse := haystack.NewGridBuilder()
76+
readByIdsResponse.AddCol("id", map[string]haystack.Val{})
77+
readByIdsResponse.AddCol("tz", map[string]haystack.Val{})
78+
readByIdsResponse.AddRow([]haystack.Val{haystack.NewRef("abcdefg-12345678", ""), haystack.NewStr("UTC")})
79+
80+
hisReadResponse := haystack.NewGridBuilder()
81+
hisReadResponse.AddCol("ts", map[string]haystack.Val{})
82+
hisReadResponse.AddCol("v0", map[string]haystack.Val{})
83+
hisReadResponse.AddRow([]haystack.Val{haystack.NewDateTimeFromGo(time.Unix(0, 0)), haystack.NewNumber(5, "kWh")})
7984

8085
client := &testHaystackClient{
81-
hisReadResponse: response.ToGrid(),
86+
readByIdsResponse: readByIdsResponse.ToGrid(),
87+
hisReadResponse: hisReadResponse.ToGrid(),
8288
}
8389

8490
actual := getResponse(
@@ -244,10 +250,11 @@ func getResponse(
244250

245251
// TestHaystackClient is a mock of the HaystackClient interface
246252
type testHaystackClient struct {
247-
navResponse haystack.Grid
248-
evalResponse haystack.Grid
249-
hisReadResponse haystack.Grid
250-
readResponse haystack.Grid
253+
navResponse haystack.Grid
254+
evalResponse haystack.Grid
255+
hisReadResponse haystack.Grid
256+
readResponse haystack.Grid
257+
readByIdsResponse haystack.Grid
251258
}
252259

253260
// Open is a no-op
@@ -288,3 +295,8 @@ func (c *testHaystackClient) HisReadAbsDateTime(ref haystack.Ref, start haystack
288295
func (c *testHaystackClient) Read(query string) (haystack.Grid, error) {
289296
return c.readResponse, nil
290297
}
298+
299+
// Read returns the ReadByIdsResponse
300+
func (c *testHaystackClient) ReadByIds(refs []haystack.Ref) (haystack.Grid, error) {
301+
return c.readByIdsResponse, nil
302+
}

pkg/plugin/haystackClient.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ type HaystackClient interface {
1313
Eval(string) (haystack.Grid, error)
1414
HisReadAbsDateTime(haystack.Ref, haystack.DateTime, haystack.DateTime) (haystack.Grid, error)
1515
Read(string) (haystack.Grid, error)
16+
ReadByIds([]haystack.Ref) (haystack.Grid, error)
1617
Nav(haystack.Val) (haystack.Grid, error)
1718
}

src/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ to create a new Haystack datasource. Next, fill in the required information:
2424
- The root Haystack API URL. The URLs for some popular Haystack servers are listed below:
2525
- SkySpark: `http://<host>/api/<proj>/`
2626
- Haxall: `http://<host>/api/`
27-
- NHaystack: `http://<host>/`
27+
- NHaystack: `http://<host>/<name_of_nhaystack_service>/`
2828
- The username and password. It is best practice to create a dedicated user for the Grafana integration.
2929

3030
Once complete, select `Save & Test`. If you get a green check mark, the connection was successful!
@@ -77,6 +77,18 @@ are combined with commas, (`red,blue`), but this may be customized using the
7777

7878
[Standard grafana alerting](https://grafana.com/docs/grafana/latest/alerting/) is supported by this data source.
7979

80+
## Haystack Server Configuration
81+
82+
### NHaystack
83+
84+
Follow the setup instructions in the [`nhaystack` README](https://github.com/ci-richard-mcelhinney/nhaystack#usage).
85+
86+
Currently only `Basic Auth` connectivity to `nhaystack` is supported. Basic auth should only be enabled when Niagara web traffic is encrypted using HTTPS.
87+
88+
To add basic auth support, click and drag `baja/AuthenticationSchemes/WebServicesSchemes/HTTPBasicScheme` from the Palette to `Config/AuthenticationService/AuthenticationSchemes/` in the Nav pane. Then go to `Config/UserService/` in the Nav pane, create a user, and set the `Authentication Scheme Name` slot to `HTTPBasicScheme`. The user must also have an `Admin` role in order to access nhaystack endpoints.
89+
90+
The root Haystack API URL is dependent on the name given to the nhaystack service: `http://<host>/<name_of_nhaystack_service>/`. This service name defaults to `haystack`, so unless renamed the URL is `http://<host>/haystack/`.
91+
8092
## Support
8193

8294
You can view the code, contribute, or request support on this project's

src/datasource.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,29 @@ export class DataSource extends DataSourceWithBackend<HaystackQuery, HaystackDat
4949
let frame = result?.data?.find((frame: DataFrame) => {
5050
return frame.refId === refId;
5151
});
52-
let opSymbols =
53-
frame?.fields?.find((field: Field<any, Vector<string>>) => {
54-
return field.name === 'def';
55-
}).values ?? [];
56-
let ops: string[] = opSymbols.map((opSymbol: string) => {
57-
if (opSymbol.startsWith('^op:')) {
58-
return opSymbol.substring(4);
59-
} else {
60-
return opSymbol;
52+
53+
let ops: string[] = []
54+
55+
let defField = frame?.fields?.find((field: Field<any, Vector<string>>) => {
56+
return field.name === 'def';
57+
})
58+
if (defField != null) {
59+
ops = defField.values.map((opSymbol: string) => {
60+
if (opSymbol.startsWith('^op:')) {
61+
return opSymbol.substring(4);
62+
} else {
63+
return opSymbol;
64+
}
65+
});
66+
} else {
67+
// Include back-support for old `ops` format, which uses "name", not "defs". Used by nhaystack
68+
let nameField = frame?.fields?.find((field: Field<any, Vector<string>>) => {
69+
return field.name === 'name';
70+
})
71+
if (nameField != null) {
72+
ops = nameField.values;
6173
}
62-
});
74+
}
6375

6476
let availableQueryTypes = queryTypes.filter((queryType) => {
6577
return queryType.apiRequirements.every((apiRequirement) => {

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface HaystackVariableQuery {
3333
}
3434

3535
export const DEFAULT_QUERY: Partial<HaystackQuery> = {
36-
type: 'eval',
36+
type: 'read',
3737
eval: '[{ts: $__timeRange_start, v0: 0}, {ts: $__timeRange_end, v0: 10}].toGrid',
3838
hisRead: 'abcdef-123456',
3939
hisReadFilter: 'point and his and temp and air and outside',

0 commit comments

Comments
 (0)