Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nl insights objects #523

Merged
merged 6 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ func (cfg *Config) TestConnection(ctx context.Context) error {
if err != nil {
return errors.Wrap(err, "failed to set up REST client")
}
sessionState.Rest.SetClient(client)
sessionState.Rest.SetClient(client, "", "")

actionState := &action.State{}
sessionState.CurrentActionState = actionState
Expand Down
6 changes: 5 additions & 1 deletion scheduler/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,11 @@ func setupRESTHandler(sessionState *session.State, connectionSettings *connectio
return errors.WithStack(err)
}

sessionState.Rest.SetClient(client)
restProtocol := "http://"
if connectionSettings.Security {
restProtocol = "https://"
}
sessionState.Rest.SetClient(client, host, restProtocol)
return nil
}

Expand Down
25 changes: 22 additions & 3 deletions senseobjdef/defaultdefinitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var (
DataDef{DataDefHyperCube, "/qHyperCube"},
[]Data{
{DataCore{
[]*Constraint{&Constraint{
[]*Constraint{{
Path: "/qHyperCube/qSize/qcy",
Value: ">1000",
Required: true,
Expand Down Expand Up @@ -132,12 +132,12 @@ var (
[]Data{
{DataCore{
[]*Constraint{
&Constraint{
{
Path: "/preferContinuousAxis",
Value: "=true",
Required: false,
},
&Constraint{
{
Path: "/qHyperCube/qDimensionInfo/[0]/qTags",
Value: "~$numeric",
Required: false,
Expand Down Expand Up @@ -774,6 +774,24 @@ var (
nil,
}

DefaultSnNlgChart = ObjectDef{
DataDef: DataDef{
Type: DataDefHyperCube,
Path: "/qHyperCube",
},
Data: []Data{
{
DataCore{
Requests: []GetDataRequests{
{
Type: DataTypeLayout,
},
},
},
},
},
}

DefaultObjectDefs = ObjectDefs{
"listbox": &DefaultListboxDef,
"filterpane": &DefaultFilterpane,
Expand Down Expand Up @@ -821,5 +839,6 @@ var (
"sn-pivot-table": &DefaultSNPivotTable,
"sn-layout-container": &DefaultLayoutContainer,
"sn-tabbed-container": &DefaultTabbedContainer,
"sn-nlg-chart": &DefaultSnNlgChart,
}
)
244 changes: 244 additions & 0 deletions session/narrativeshandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package session

import (
"context"
"encoding/json"
"fmt"
"sync"

"github.com/pkg/errors"
"github.com/qlik-oss/enigma-go/v4"
"github.com/qlik-oss/gopherciser/action"
"github.com/qlik-oss/gopherciser/enigmahandlers"
"github.com/qlik-oss/gopherciser/logger"
"github.com/qlik-oss/gopherciser/senseobjdef"
)

type (
NarrativesPropertiesNlgChartObject struct {
ChartObjectId string `json:"chartObjectId"`
Label string `json:"label"`
Type string `json:"type"`
ExtendsId string `json:"qExtendsId"`
Dimensions []*enigma.NxDimension `json:"qDimensions"`
Measures []*enigma.NxMeasure `json:"qMeasures"`
}

NarrativesProperties struct {
*enigma.GenericObjectProperties
NlgChartObject *NarrativesPropertiesNlgChartObject `json:"nlgChartObject"`
}

NarrativesHandler struct{}
NarrativesHandlerInstance struct {
ID string
Properties *NarrativesProperties
}

NarrativesPayloadExpressionOverrides struct {
Classifications []string `json:"classifications"`
Format *enigma.FieldAttributes `json:"format,omitempty"`
}

NarrativesPayloadExpression struct {
Expr string `json:"expr"`
Label string `json:"label"`
Overrides NarrativesPayloadExpressionOverrides `json:"overrides"`
}

NarrativesPayloadLibItem struct {
LibId string `json:"libId"`
Overrides interface{} `json:"overrides"`
}

NarrativesPayload struct {
AlternateStateName string `json:"alternateStateName"`
AnalysisTypes []interface{} `json:"analysisTypes"`
AppID string `json:"appId"`
Expressions []NarrativesPayloadExpression `json:"expressions"`
Fields []interface{} `json:"fields"`
Lang string `json:"lang"`
LibItems []NarrativesPayloadLibItem `json:"libItems"`
Verbosity string `json:"verbosity"`
}
)

// Instance implements ObjectHandler interface
func (handler *NarrativesHandler) Instance(id string) ObjectHandlerInstance {
return &NarrativesHandlerInstance{ID: id}
}

// GetObjectDefinition implements ObjectHandlerInstance interface
func (handler *NarrativesHandlerInstance) GetObjectDefinition(objectType string) (string, senseobjdef.SelectType, senseobjdef.DataDefType, error) {
if objectType != "sn-nlg-chart" {
return "", senseobjdef.SelectTypeUnknown, senseobjdef.DataDefUnknown, errors.New("NarrativesHandlerInstance only handles objects of type sn-nlg-chart")
}
return (&DefaultHandlerInstance{}).GetObjectDefinition("sn-nlg-chart")
}

// SetObjectAndEvents implements ObjectHandlerInstance interface
func (handler *NarrativesHandlerInstance) SetObjectAndEvents(sessionState *State, actionState *action.State, obj *enigmahandlers.Object, genObj *enigma.GenericObject) {
var wg sync.WaitGroup

wg.Add(1)
sessionState.QueueRequest(func(ctx context.Context) error {
defer wg.Done()
return handler.GetNarrativesProperties(sessionState, actionState, obj)
}, actionState, true, "")

wg.Add(1)
sessionState.QueueRequest(func(ctx context.Context) error {
defer wg.Done()
return GetObjectLayout(sessionState, actionState, obj, nil)
}, actionState, true, "")

wg.Wait()

if sessionState.Rest == nil {
sessionState.LogEntry.Log(logger.WarningLevel, "no resthandler defined, nl insights object will not generated correctly")
return
}

app := sessionState.CurrentApp
if app == nil || app.ID == "" {
actionState.AddErrors(errors.Errorf("no current app found"))
return
}

content, err := handler.generateNarritivesPayload(app.ID, obj)
if err != nil {
actionState.AddErrors(err)
return
}

protocol := sessionState.Rest.Protocol()
host := sessionState.Rest.Host()

getXrfParam := func() string {
xrfKey := ""
xrfKeyState, exists := sessionState.GetCustomState(fmt.Sprintf("%s-%s", XrfKeyState, host))
if exists {
xrfKey = fmt.Sprintf("?xrfkey=%s", xrfKeyState)
}
return xrfKey
}

_, _ = sessionState.Rest.PostSync(fmt.Sprintf("%s%s/api/v1/narratives/actions/generate%s", protocol, host, getXrfParam()), actionState, sessionState.LogEntry, content, nil)

event := func(ctx context.Context, as *action.State) error {
if err := GetObjectLayout(sessionState, as, obj, nil); err != nil {
return err
}

content, err := handler.generateNarritivesPayload(app.ID, obj)
if err != nil {
return err
}

_, _ = sessionState.Rest.PostSync(fmt.Sprintf("%s%s/api/v1/narratives/actions/generate%s", protocol, host, getXrfParam()), actionState, sessionState.LogEntry, content, nil)
return nil
}

sessionState.RegisterEvent(genObj.Handle, event, nil, true)
}

func (handler *NarrativesHandlerInstance) GetNarrativesProperties(sessionState *State, actionState *action.State, obj *enigmahandlers.Object) error {
enigmaObject, ok := obj.EnigmaObject.(*enigma.GenericObject)
if !ok {
return errors.Errorf("Failed to cast object<%s> to *enigma.GenericObject", obj.ID)
}

//Get object properties
getProperties := func(ctx context.Context) error {
raw, err := enigmaObject.GetEffectivePropertiesRaw(ctx)
if err != nil {
return errors.Wrapf(err, "object<%s>.GetEffectiveProperties failed", obj.ID)
}
err = json.Unmarshal(raw, &handler.Properties)
if err != nil {
return errors.Wrapf(err, "object<%s>.GetEffectiveProperties unmarshal failed", obj.ID)
}

obj.SetProperties(handler.Properties.GenericObjectProperties)
return nil
}

return sessionState.SendRequest(actionState, getProperties)
}

func (handler *NarrativesHandlerInstance) generateNarritivesPayload(appId string, obj *enigmahandlers.Object) ([]byte, error) {
payload := NarrativesPayload{
AppID: appId,
Lang: "en",
Verbosity: "full",
Expressions: []NarrativesPayloadExpression{},
AnalysisTypes: []interface{}{},
Fields: []interface{}{},
LibItems: []NarrativesPayloadLibItem{},
}

if handler.Properties == nil {
return nil, errors.Errorf("no properties set for nl insights object<%s>", handler.ID)
}

if handler.Properties.HyperCubeDef == nil {
return nil, errors.Errorf("no hypercube definition in properties for nl insights object<%s>", handler.ID)
}

payload.AlternateStateName = handler.Properties.StateName

measures := handler.Properties.HyperCubeDef.Measures
if handler.Properties.NlgChartObject != nil {
measures = handler.Properties.NlgChartObject.Measures
}

for _, measure := range measures {
if measure.LibraryId != "" {
payload.LibItems = append(payload.LibItems, NarrativesPayloadLibItem{
LibId: measure.LibraryId,
Overrides: struct{}{},
})
continue
}
payload.Expressions = append(payload.Expressions, NarrativesPayloadExpression{
Expr: measure.Def.Def,
Overrides: NarrativesPayloadExpressionOverrides{
Classifications: []string{"measure"},
Format: measure.Def.NumFormat,
},
Label: "",
})
}

dimensions := handler.Properties.HyperCubeDef.Dimensions
if handler.Properties.NlgChartObject != nil {
dimensions = handler.Properties.NlgChartObject.Dimensions
}

for _, dimension := range dimensions {
if dimension.LibraryId != "" {
payload.LibItems = append(payload.LibItems, NarrativesPayloadLibItem{
LibId: dimension.LibraryId,
Overrides: struct{}{},
})
continue
}
expr := ""
if len(dimension.Def.FieldDefs) > 0 {
expr = dimension.Def.FieldDefs[0]
}
payload.Expressions = append(payload.Expressions, NarrativesPayloadExpression{
Expr: expr,
Overrides: NarrativesPayloadExpressionOverrides{
Classifications: []string{"dimension"},
},
Label: "",
})
}

content, err := json.Marshal(payload)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal narratives payload")
}
return content, nil
}
3 changes: 3 additions & 0 deletions session/objecthandling.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ func init() {
if err := GlobalObjectHandler.RegisterHandler("sn-tabbed-container", &TabbedContainerHandler{}, false); err != nil {
panic(err)
}
if err := GlobalObjectHandler.RegisterHandler("sn-nlg-chart", &NarrativesHandler{}, false); err != nil {
panic(err)
}
}

func (err NxValidationError) Error() string {
Expand Down
28 changes: 20 additions & 8 deletions session/resthandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ type (

// RestHandler handles waiting for pending requests and responses
RestHandler struct {
timeout time.Duration
Client *http.Client
trafficLogger enigma.TrafficLogger
headers *HeaderJar
virtualProxy string
ctx context.Context
pending *pending.Handler
timeout time.Duration
Client *http.Client
trafficLogger enigma.TrafficLogger
headers *HeaderJar
virtualProxy string
ctx context.Context
pending *pending.Handler
defaultHost string
defaultProtocol string
}

// RestRequest represents a REST request and its response
Expand Down Expand Up @@ -308,8 +310,18 @@ func DefaultReqOptions() *ReqOptions {
}

// SetClient set HTTP client for this RestHandler
func (handler *RestHandler) SetClient(client *http.Client) {
func (handler *RestHandler) SetClient(client *http.Client, defaultHost, defaultProtocol string) {
handler.Client = client
handler.defaultHost = defaultHost
handler.defaultProtocol = defaultProtocol
}

func (handler *RestHandler) Host() string {
return handler.defaultHost
}

func (handler *RestHandler) Protocol() string {
return handler.defaultProtocol
}

// GetSync sends synchronous GET request with options, using options=nil default options are used
Expand Down