From 70a3d6345095c4aac4a272fd38fc7557a291283b Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 18 Dec 2022 15:35:49 +0800 Subject: [PATCH 1/3] [add] dashboard widget --- cmd/upgrade.go | 5 +- widgets/component/types.go | 7 +- widgets/dashboard/action.go | 42 ++++++ widgets/dashboard/api.go | 118 +++++++++++++++ widgets/dashboard/dashboard.go | 217 ++++++++++++++++++++++++++++ widgets/dashboard/dashboard_test.go | 46 ++++++ widgets/dashboard/export.go | 7 + widgets/dashboard/fields.go | 49 +++++++ widgets/dashboard/handler.go | 85 +++++++++++ widgets/dashboard/layout.go | 99 +++++++++++++ widgets/dashboard/mapping.go | 175 ++++++++++++++++++++++ widgets/dashboard/process.go | 70 +++++++++ widgets/dashboard/process_test.go | 161 +++++++++++++++++++++ widgets/dashboard/types.go | 58 ++++++++ widgets/dashboard/vaildate.go | 6 + 15 files changed, 1140 insertions(+), 5 deletions(-) create mode 100644 widgets/dashboard/action.go create mode 100644 widgets/dashboard/api.go create mode 100644 widgets/dashboard/dashboard.go create mode 100644 widgets/dashboard/dashboard_test.go create mode 100644 widgets/dashboard/export.go create mode 100644 widgets/dashboard/fields.go create mode 100644 widgets/dashboard/handler.go create mode 100644 widgets/dashboard/layout.go create mode 100644 widgets/dashboard/mapping.go create mode 100644 widgets/dashboard/process.go create mode 100644 widgets/dashboard/process_test.go create mode 100644 widgets/dashboard/types.go create mode 100644 widgets/dashboard/vaildate.go diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 5a6927aadb..3e849897f8 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -3,12 +3,13 @@ package cmd import ( "bufio" "fmt" + "os" + "github.com/blang/semver" "github.com/fatih/color" "github.com/rhysd/go-github-selfupdate/selfupdate" "github.com/spf13/cobra" "github.com/yaoapp/yao/share" - "os" ) var upgradeCmd = &cobra.Command{ @@ -27,7 +28,7 @@ var upgradeCmd = &cobra.Command{ currentVersion := semver.MustParse(share.VERSION) if !found || latest.Version.LTE(currentVersion) { fmt.Println(color.GreenString(L("🎉Current version is the latest🎉"))) - os.Exit(1) + os.Exit(0) } fmt.Println(color.WhiteString(L("Do you want to update to %s ? (y/n): "), latest.Version)) input, err := bufio.NewReader(os.Stdin).ReadString('\n') diff --git a/widgets/component/types.go b/widgets/component/types.go index ea92bb0ef2..36f7265f3b 100644 --- a/widgets/component/types.go +++ b/widgets/component/types.go @@ -16,9 +16,10 @@ type Instances []InstanceDSL // InstanceDSL the component instance DSL type InstanceDSL struct { - Name string `json:"name,omitempty"` - Width interface{} `json:"width,omitempty"` - Height interface{} `json:"height,omitempty"` + Name string `json:"name,omitempty"` + Width interface{} `json:"width,omitempty"` + Height interface{} `json:"height,omitempty"` + Rows []InstanceDSL `json:"rows,omitempty"` } // ActionsExport the export actions diff --git a/widgets/dashboard/action.go b/widgets/dashboard/action.go new file mode 100644 index 0000000000..3b6db48ecd --- /dev/null +++ b/widgets/dashboard/action.go @@ -0,0 +1,42 @@ +package dashboard + +import ( + "github.com/yaoapp/yao/widgets/action" +) + +var processActionDefaults = map[string]*action.Process{ + + "Setting": { + Name: "yao.dashboard.Setting", + Guard: "bearer-jwt", + Process: "yao.dashboard.Xgen", + Default: []interface{}{nil}, + }, + "Component": { + Name: "yao.dashboard.Component", + Guard: "bearer-jwt", + Default: []interface{}{nil, nil, nil}, + }, + "Data": { + Name: "yao.dashboard.Data", + Guard: "bearer-jwt", + Default: []interface{}{nil}, + }, +} + +// SetDefaultProcess set the default value of action +func (act *ActionDSL) SetDefaultProcess() { + + act.Setting = action.ProcessOf(act.Setting). + Merge(processActionDefaults["Setting"]). + SetHandler(processHandler) + + act.Component = action.ProcessOf(act.Component). + Merge(processActionDefaults["Component"]). + SetHandler(processHandler) + + act.Data = action.ProcessOf(act.Data). + WithBefore(act.BeforeData).WithAfter(act.AfterData). + Merge(processActionDefaults["Data"]). + SetHandler(processHandler) +} diff --git a/widgets/dashboard/api.go b/widgets/dashboard/api.go new file mode 100644 index 0000000000..f3e380fc28 --- /dev/null +++ b/widgets/dashboard/api.go @@ -0,0 +1,118 @@ +package dashboard + +import ( + "fmt" + + "github.com/gin-gonic/gin" + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widgets/action" +) + +// Guard form widget dashboard +func Guard(c *gin.Context) { + + id := c.Param("id") + if id == "" { + abort(c, 400, "the dashboard widget id does not found") + return + } + + dashboard, has := Dashboards[id] + if !has { + abort(c, 404, fmt.Sprintf("the dashboard widget %s does not exist", id)) + return + } + + act, err := dashboard.getAction(c.FullPath()) + if err != nil { + abort(c, 404, err.Error()) + return + } + + err = act.UseGuard(c, id) + if err != nil { + abort(c, 400, err.Error()) + return + } + +} + +func abort(c *gin.Context, code int, message string) { + c.JSON(code, gin.H{"code": code, "message": message}) + c.Abort() +} + +func (dashboard *DSL) getAction(path string) (*action.Process, error) { + + switch path { + case "/api/__yao/dashboard/:id/setting": + return dashboard.Action.Setting, nil + case "/api/__yao/dashboard/:id/component/:xpath/:method": + return dashboard.Action.Component, nil + case "/api/__yao/dashboard/:id/data": + return dashboard.Action.Data, nil + } + + return nil, fmt.Errorf("the form widget %s %s action does not exist", dashboard.ID, path) +} + +// export API +func exportAPI() error { + + http := gou.HTTP{ + Name: "Widget Dashboard API", + Description: "Widget Dashboard API", + Version: share.VERSION, + Guard: "widget-dashboard", + Group: "__yao/dashboard", + Paths: []gou.Path{}, + } + + // GET /api/__yao/dashboard/:id/setting -> Default process: yao.dashboard.Xgen + path := gou.Path{ + Label: "Setting", + Description: "Setting", + Path: "/:id/setting", + Method: "GET", + Process: "yao.dashboard.Setting", + In: []string{"$param.id"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // GET /api/__yao/dashboard/:id/data -> Default process: yao.dashboard.Data $param.id :query + path = gou.Path{ + Label: "Data", + Description: "Data", + Path: "/:id/data", + Method: "GET", + Process: "yao.dashboard.Data", + In: []string{"$param.id", ":query"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // GET /api/__yao/dashboard/:id/component/:xpath/:method -> Default process: yao.dashboard.Component $param.id $param.xpath $param.method :query + path = gou.Path{ + Label: "Component", + Description: "Component", + Path: "/:id/component/:xpath/:method", + Method: "GET", + Process: "yao.dashboard.Component", + In: []string{"$param.id", "$param.xpath", "$param.method", ":query"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // api source + source, err := jsoniter.Marshal(http) + if err != nil { + return err + } + + // load apis + _, err = gou.LoadAPIReturn(string(source), "widgets.dashboard") + return err +} diff --git a/widgets/dashboard/dashboard.go b/widgets/dashboard/dashboard.go new file mode 100644 index 0000000000..5fd9e8423d --- /dev/null +++ b/widgets/dashboard/dashboard.go @@ -0,0 +1,217 @@ +package dashboard + +import ( + "fmt" + "path/filepath" + "strings" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/field" +) + +// +// API: +// GET /api/__yao/dashboard/:id/setting -> Default process: yao.dashboard.Xgen +// GET /api/__yao/dashboard/:id/data -> Default process: yao.dashboard.Data $param.id :query +// GET /api/__yao/dashboard/:id/component/:xpath/:method -> Default process: yao.dashboard.Component $param.id $param.xpath $param.method :query +// +// Process: +// yao.form.Setting Return the App DSL +// yao.form.Xgen Return the Xgen setting +// yao.form.Data Return the query data +// yao.form.Component Return the result defined in props.xProps +// +// Hook: +// before:data +// after:data +// + +// Dashboards the loaded dashboard widgets +var Dashboards map[string]*DSL = map[string]*DSL{} + +// New create a new DSL +func New(id string) *DSL { + return &DSL{ + ID: id, + Fields: &FieldsDSL{Dashboard: field.Columns{}, Filter: field.Filters{}}, + CProps: field.CloudProps{}, + Config: map[string]interface{}{}, + } +} + +// LoadAndExport load table +func LoadAndExport(cfg config.Config) error { + err := Load(cfg) + if err != nil { + return err + } + return Export() +} + +// Load load task +func Load(cfg config.Config) error { + var root = filepath.Join(cfg.Root, "dashboards") + return LoadFrom(root, "") +} + +// LoadFrom load from dir +func LoadFrom(dir string, prefix string) error { + + if share.DirNotExists(dir) { + return fmt.Errorf("%s does not exists", dir) + } + + messages := []string{} + err := share.Walk(dir, ".json", func(root, filename string) { + id := prefix + share.ID(root, filename) + data := share.ReadFile(filename) + dsl := New(id) + err := jsoniter.Unmarshal(data, dsl) + if err != nil { + messages = append(messages, fmt.Sprintf("[%s] %s", id, err.Error())) + return + } + + if dsl.Action == nil { + dsl.Action = &ActionDSL{} + } + dsl.Action.SetDefaultProcess() + + if dsl.Layout == nil { + dsl.Layout = &LayoutDSL{} + } + + // mapping + err = dsl.mapping() + if err != nil { + messages = append(messages, fmt.Sprintf("[%s] Mapping %s", id, err.Error())) + return + } + + // Validate + err = dsl.Validate() + if err != nil { + messages = append(messages, fmt.Sprintf("[%s] %s", id, err.Error())) + return + } + + Dashboards[id] = dsl + }) + + if len(messages) > 0 { + return fmt.Errorf(strings.Join(messages, ";")) + } + + return err +} + +// Get dashboard via process or id +func Get(dashboard interface{}) (*DSL, error) { + id := "" + switch dashboard.(type) { + case string: + id = dashboard.(string) + case *gou.Process: + id = dashboard.(*gou.Process).ArgsString(0) + default: + return nil, fmt.Errorf("%v type does not support", dashboard) + } + + t, has := Dashboards[id] + if !has { + return nil, fmt.Errorf("%s does not exist", id) + } + return t, nil +} + +// MustGet Get dashboard via process or id thow error +func MustGet(dashboard interface{}) *DSL { + t, err := Get(dashboard) + if err != nil { + exception.New(err.Error(), 400).Throw() + } + return t +} + +// Xgen trans to xgen setting +func (dsl *DSL) Xgen(data map[string]interface{}, excludes map[string]bool) (map[string]interface{}, error) { + + layout, err := dsl.Layout.Xgen(data, excludes, dsl.Mapping) + if err != nil { + return nil, err + } + + fields, err := dsl.Fields.Xgen(layout) + if err != nil { + return nil, err + } + + // full width default value + if _, has := dsl.Config["full"]; !has { + dsl.Config["full"] = true + } + + setting := map[string]interface{}{} + bytes, err := jsoniter.Marshal(layout) + if err != nil { + return nil, err + } + + err = jsoniter.Unmarshal(bytes, &setting) + if err != nil { + return nil, err + } + + setting["fields"] = fields + setting["config"] = dsl.Config + for _, cProp := range dsl.CProps { + err := cProp.Replace(setting, func(cProp component.CloudPropsDSL) interface{} { + return map[string]interface{}{ + "api": fmt.Sprintf("/api/__yao/dashboard/%s%s", dsl.ID, cProp.Path()), + "params": cProp.Query, + } + }) + if err != nil { + return nil, err + } + } + + return setting, nil +} + +// Actions get the dashboard actions +func (dsl *DSL) Actions() []component.ActionsExport { + + res := []component.ActionsExport{} + + // layout.actions + if dsl.Layout != nil && + dsl.Layout.Actions != nil && + len(dsl.Layout.Actions) > 0 { + + res = append(res, component.ActionsExport{ + Type: "operation", + Xpath: "layout.actions", + Actions: dsl.Layout.Actions, + }) + } + + // layout.filter.actions + if dsl.Layout != nil && + dsl.Layout.Filter != nil && + dsl.Layout.Filter.Actions != nil && + len(dsl.Layout.Filter.Actions) > 0 { + + res = append(res, component.ActionsExport{ + Type: "filter", + Xpath: "layout.filter.actions", + Actions: dsl.Layout.Filter.Actions, + }) + } + return res +} diff --git a/widgets/dashboard/dashboard_test.go b/widgets/dashboard/dashboard_test.go new file mode 100644 index 0000000000..87767c4bb9 --- /dev/null +++ b/widgets/dashboard/dashboard_test.go @@ -0,0 +1,46 @@ +package dashboard + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/i18n" + "github.com/yaoapp/yao/model" + "github.com/yaoapp/yao/runtime" + "github.com/yaoapp/yao/script" + "github.com/yaoapp/yao/share" +) + +func TestLoad(t *testing.T) { + prepare(t) + err := Load(config.Conf) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 1, len(Dashboards)) +} + +func prepare(t *testing.T, language ...string) { + runtime.Load(config.Conf) + i18n.Load(config.Conf) + share.DBConnect(config.Conf.DB) // removed later + + // load scripts + err := script.Load(config.Conf) + if err != nil { + t.Fatal(err) + } + + // load models + err = model.Load(config.Conf) + if err != nil { + t.Fatal(err) + } + + // export + err = Export() + if err != nil { + t.Fatal(err) + } +} diff --git a/widgets/dashboard/export.go b/widgets/dashboard/export.go new file mode 100644 index 0000000000..2e554820a8 --- /dev/null +++ b/widgets/dashboard/export.go @@ -0,0 +1,7 @@ +package dashboard + +// Export process & api +func Export() error { + exportProcess() + return exportAPI() +} diff --git a/widgets/dashboard/fields.go b/widgets/dashboard/fields.go new file mode 100644 index 0000000000..a9d2e6e976 --- /dev/null +++ b/widgets/dashboard/fields.go @@ -0,0 +1,49 @@ +package dashboard + +import ( + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/yao/widgets/field" +) + +// Xgen trans to xgen setting +func (fields *FieldsDSL) Xgen(layout *LayoutDSL) (map[string]interface{}, error) { + res := map[string]interface{}{} + + filters := map[string]field.FilterDSL{} + if layout.Filter != nil && layout.Filter.Columns != nil { + for _, inst := range layout.Filter.Columns { + if c, has := fields.Filter[inst.Name]; has { + filters[inst.Name] = c + } + } + } + + columns := map[string]field.ColumnDSL{} + if layout.Dashboard != nil && layout.Dashboard.Columns != nil { + for _, inst := range layout.Dashboard.Columns { + if c, has := fields.Dashboard[inst.Name]; has { + columns[inst.Name] = c + } + + if inst.Rows != nil { + for _, inst := range inst.Rows { + if c, has := fields.Dashboard[inst.Name]; has { + columns[inst.Name] = c + } + } + } + } + } + + data, err := jsoniter.Marshal(map[string]interface{}{"filter": filters, "dashboard": columns}) + if err != nil { + return nil, err + } + + err = jsoniter.Unmarshal(data, &res) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/widgets/dashboard/handler.go b/widgets/dashboard/handler.go new file mode 100644 index 0000000000..780e37226a --- /dev/null +++ b/widgets/dashboard/handler.go @@ -0,0 +1,85 @@ +package dashboard + +import ( + "fmt" + + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/yao/widgets/action" +) + +// ******************************** +// * Execute the process of form * +// ******************************** +// Life-Circle: Before Hook → Run Process → Compute View → After Hook +// Execute Compute View On: Data +func processHandler(p *action.Process, process *gou.Process) (interface{}, error) { + + dashboard, err := Get(process) + if err != nil { + return nil, err + } + args := p.Args(process) + + // Process + name := p.Process + if name == "" { + name = p.ProcessBind + } + + if name == "" { + log.Error("[dashboard] %s %s process is required", dashboard.ID, p.Name) + return nil, fmt.Errorf("[dashboard] %s %s process is required", dashboard.ID, p.Name) + } + + // Compute Filter + err = dashboard.ComputeFilter(p.Name, process, args, dashboard.getFilter()) + if err != nil { + log.Error("[dashboard] %s %s Compute Filter Error: %s", dashboard.ID, p.Name, err.Error()) + } + + // Before Hook + if p.Before != nil { + log.Trace("[dashboard] %s %s before: exec(%v)", dashboard.ID, p.Name, args) + newArgs, err := p.Before.Exec(args, process.Sid, process.Global) + if err != nil { + log.Error("[dashboard] %s %s before: %s", dashboard.ID, p.Name, err.Error()) + } else { + log.Trace("[dashboard] %s %s before: args:%v", dashboard.ID, p.Name, args) + args = newArgs + } + } + + // Execute Process + act, err := gou.ProcessOf(name, args...) + if err != nil { + log.Error("[dashboard] %s %s -> %s %s", dashboard.ID, p.Name, name, err.Error()) + return nil, fmt.Errorf("[dashboard] %s %s -> %s %s", dashboard.ID, p.Name, name, err.Error()) + } + + res, err := act.WithGlobal(process.Global).WithSID(process.Sid).Exec() + if err != nil { + log.Error("[dashboard] %s %s -> %s %s", dashboard.ID, p.Name, name, err.Error()) + return nil, fmt.Errorf("[dashboard] %s %s -> %s %s", dashboard.ID, p.Name, name, err.Error()) + } + + // Compute View + err = dashboard.ComputeView(p.Name, process, res, dashboard.getField()) + if err != nil { + log.Error("[dashboard] %s %s Compute View Error: %s", dashboard.ID, p.Name, err.Error()) + } + + // After hook + if p.After != nil { + log.Trace("[dashboard] %s %s after: exec(%v)", dashboard.ID, p.Name, res) + newRes, err := p.After.Exec(res, process.Sid, process.Global) + if err != nil { + log.Error("[dashboard] %s %s after: %s", dashboard.ID, p.Name, err.Error()) + } else { + log.Trace("[dashboard] %s %s after: %v", dashboard.ID, p.Name, newRes) + res = newRes + } + } + + return res, nil +} diff --git a/widgets/dashboard/layout.go b/widgets/dashboard/layout.go new file mode 100644 index 0000000000..82c677dbc6 --- /dev/null +++ b/widgets/dashboard/layout.go @@ -0,0 +1,99 @@ +package dashboard + +import ( + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/mapping" +) + +// Xgen trans to Xgen setting +func (layout *LayoutDSL) Xgen(data map[string]interface{}, excludes map[string]bool, mapping *mapping.Mapping) (*LayoutDSL, error) { + clone, err := layout.Clone() + if err != nil { + return nil, err + } + + // Filter + if clone.Filter != nil { + if clone.Filter.Actions != nil { + clone.Filter.Actions = clone.Filter.Actions.Filter(excludes) + } + + if clone.Filter.Columns != nil { + columns := []component.InstanceDSL{} + for _, column := range clone.Filter.Columns { + id, has := mapping.Filters[column.Name] + if !has { + continue + } + + if _, has := excludes[id]; has { + continue + } + + columns = append(columns, column) + } + clone.Filter.Columns = columns + } + } + + // Actions + if clone.Actions != nil { + clone.Actions = clone.Actions.Filter(excludes) + } + + // Columns + if clone.Dashboard != nil && clone.Dashboard.Columns != nil { + columns := []component.InstanceDSL{} + for _, column := range clone.Dashboard.Columns { + + if column.Rows != nil { + new := component.InstanceDSL{Rows: []component.InstanceDSL{}} + for _, column := range column.Rows { + id, has := mapping.Columns[column.Name] + if !has { + continue + } + + if _, has := excludes[id]; has { + continue + } + new.Rows = append(new.Rows, column) + } + + if len(new.Rows) > 0 { + columns = append(columns, new) + } + continue + } + + id, has := mapping.Columns[column.Name] + if !has { + continue + } + + if _, has := excludes[id]; has { + continue + } + + columns = append(columns, column) + } + clone.Dashboard.Columns = columns + } + + return clone, nil +} + +// Clone layout for output +func (layout *LayoutDSL) Clone() (*LayoutDSL, error) { + new := LayoutDSL{} + bytes, err := jsoniter.Marshal(layout) + if err != nil { + return nil, err + } + err = jsoniter.Unmarshal(bytes, &new) + if err != nil { + return nil, err + } + return &new, nil +} diff --git a/widgets/dashboard/mapping.go b/widgets/dashboard/mapping.go new file mode 100644 index 0000000000..8ec8122924 --- /dev/null +++ b/widgets/dashboard/mapping.go @@ -0,0 +1,175 @@ +package dashboard + +import ( + "fmt" + + "github.com/yaoapp/yao/widgets/compute" + "github.com/yaoapp/yao/widgets/field" + "github.com/yaoapp/yao/widgets/mapping" +) + +func (dsl *DSL) getField() func(string) (*field.ColumnDSL, string, string, error) { + return func(name string) (*field.ColumnDSL, string, string, error) { + field, has := dsl.Fields.Dashboard[name] + if !has { + return nil, "fields.dashboard", dsl.ID, fmt.Errorf("fields.dashboard.%s does not exist", name) + } + return &field, "fields.dashboard", dsl.ID, nil + } +} + +func (dsl *DSL) getFilter() func(string) (*field.FilterDSL, string, string, error) { + return func(name string) (*field.FilterDSL, string, string, error) { + field, has := dsl.Fields.Filter[name] + if !has { + return nil, "fields.filter", dsl.ID, fmt.Errorf("fields.filter.%s does not exist", name) + } + return &field, "fields.filter", dsl.ID, nil + } +} + +func (dsl *DSL) mapping() error { + + if dsl.Computes == nil { + dsl.Computes = &compute.Maps{ + Filter: map[string][]compute.Unit{}, + Edit: map[string][]compute.Unit{}, + View: map[string][]compute.Unit{}, + } + } + + if dsl.Mapping == nil { + dsl.Mapping = &mapping.Mapping{} + } + + if dsl.Mapping.Filters == nil { + dsl.Mapping.Filters = map[string]string{} + } + + if dsl.Mapping.Columns == nil { + dsl.Mapping.Columns = map[string]string{} + } + + if dsl.Fields == nil { + return nil + } + + // Mapping compute and id + // Filter + if dsl.Fields.Filter != nil && dsl.Layout.Filter != nil && dsl.Layout.Filter.Columns != nil { + for _, inst := range dsl.Layout.Filter.Columns { + if filter, has := dsl.Fields.Filter[inst.Name]; has { + + // Mapping ID + dsl.Mapping.Filters[filter.ID] = inst.Name + dsl.Mapping.Filters[inst.Name] = filter.ID + + if filter.Edit != nil && filter.Edit.Compute != nil { + bind := filter.FilterBind() + if _, has := dsl.Computes.Filter[bind]; !has { + dsl.Computes.Filter[bind] = []compute.Unit{} + } + dsl.Computes.Filter[bind] = append(dsl.Computes.Filter[bind], compute.Unit{Name: inst.Name, Kind: compute.Filter}) + } + } + } + } + + if dsl.Fields.Dashboard != nil && dsl.Layout.Dashboard != nil && dsl.Layout.Dashboard.Columns != nil { + for _, inst := range dsl.Layout.Dashboard.Columns { + + if field, has := dsl.Fields.Dashboard[inst.Name]; has { + + // Mapping ID + dsl.Mapping.Columns[field.ID] = inst.Name + dsl.Mapping.Columns[inst.Name] = field.ID + + // View + if field.View != nil && field.View.Compute != nil { + bind := field.ViewBind() + if _, has := dsl.Computes.View[bind]; !has { + dsl.Computes.View[bind] = []compute.Unit{} + } + dsl.Computes.View[bind] = append(dsl.Computes.View[bind], compute.Unit{Name: inst.Name, Kind: compute.View}) + } + } + + if inst.Rows != nil { + for _, inst := range inst.Rows { + + if field, has := dsl.Fields.Dashboard[inst.Name]; has { + // Mapping ID + dsl.Mapping.Columns[field.ID] = inst.Name + dsl.Mapping.Columns[inst.Name] = field.ID + + // View + if field.View != nil && field.View.Compute != nil { + bind := field.ViewBind() + if _, has := dsl.Computes.View[bind]; !has { + dsl.Computes.View[bind] = []compute.Unit{} + } + dsl.Computes.View[bind] = append(dsl.Computes.View[bind], compute.Unit{Name: inst.Name, Kind: compute.View}) + } + } + } + } + + } + } + + // Mapping Actions + dsl.mappingActions() + + // Filters + err := dsl.Fields.Filter.CPropsMerge(dsl.CProps, func(name string, filter field.FilterDSL) (xpath string) { + return fmt.Sprintf("fields.filter.%s.edit.props", name) + }) + + if err != nil { + return err + } + + // Columns + return dsl.Fields.Dashboard.CPropsMerge(dsl.CProps, func(name string, kind string, column field.ColumnDSL) (xpath string) { + return fmt.Sprintf("fields.dashboard.%s.%s.props", name, kind) + }) + +} + +// Actions get the table actions +func (dsl *DSL) mappingActions() { + + if dsl.Mapping == nil { + dsl.Mapping = &mapping.Mapping{} + } + + if dsl.Mapping.Actions == nil { + dsl.Mapping.Actions = map[string]string{} + } + + // layout.operation.actions + if dsl.Layout != nil && + dsl.Layout.Actions != nil && + len(dsl.Layout.Actions) > 0 { + + for idx, action := range dsl.Layout.Actions { + xpath := fmt.Sprintf("layout.actions[%d]", idx) + dsl.Mapping.Actions[action.ID] = xpath + dsl.Mapping.Actions[xpath] = action.ID + } + } + + // layout.filter.actions + if dsl.Layout != nil && + dsl.Layout.Filter != nil && + dsl.Layout.Filter.Actions != nil && + len(dsl.Layout.Filter.Actions) > 0 { + + for idx, action := range dsl.Layout.Filter.Actions { + xpath := fmt.Sprintf("layout.filter.actions[%d]", idx) + dsl.Mapping.Actions[action.ID] = xpath + dsl.Mapping.Actions[xpath] = action.ID + } + } + +} diff --git a/widgets/dashboard/process.go b/widgets/dashboard/process.go new file mode 100644 index 0000000000..9a8d5c111d --- /dev/null +++ b/widgets/dashboard/process.go @@ -0,0 +1,70 @@ +package dashboard + +import ( + "fmt" + + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/yao/widgets/app" +) + +// Export process +func exportProcess() { + gou.RegisterProcessHandler("yao.dashboard.setting", processSetting) + gou.RegisterProcessHandler("yao.dashboard.xgen", processXgen) + gou.RegisterProcessHandler("yao.dashboard.component", processComponent) + gou.RegisterProcessHandler("yao.dashboard.data", processData) +} + +func processXgen(process *gou.Process) interface{} { + + dashboard := MustGet(process) + data := process.ArgsMap(1, map[string]interface{}{}) + excludes := app.Permissions(process, "dashboards", dashboard.ID) + setting, err := dashboard.Xgen(data, excludes) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + return setting +} + +func processComponent(process *gou.Process) interface{} { + + process.ValidateArgNums(3) + dashboard := MustGet(process) + xpath := process.ArgsString(1) + method := process.ArgsString(2) + key := fmt.Sprintf("%s.$%s", xpath, method) + + // get cloud props + cProp, has := dashboard.CProps[key] + if !has { + exception.New("%s does not exist", 400, key).Throw() + } + + // :query + query := map[string]interface{}{} + if process.NumOfArgsIs(4) { + query = process.ArgsMap(3) + } + + // execute query + res, err := cProp.ExecQuery(process, query) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + return res +} + +func processSetting(process *gou.Process) interface{} { + dashboard := MustGet(process) + process.Args = append(process.Args, process.Args[0]) // dashboard name + return dashboard.Action.Setting.MustExec(process) +} + +func processData(process *gou.Process) interface{} { + dashboard := MustGet(process) + return dashboard.Action.Data.MustExec(process) +} diff --git a/widgets/dashboard/process_test.go b/widgets/dashboard/process_test.go new file mode 100644 index 0000000000..6b903819b4 --- /dev/null +++ b/widgets/dashboard/process_test.go @@ -0,0 +1,161 @@ +package dashboard + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou" + "github.com/yaoapp/gou/session" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/kun/maps" + "github.com/yaoapp/yao/config" + q "github.com/yaoapp/yao/query" +) + +func TestProcessData(t *testing.T) { + load(t) + args := []interface{}{"workspace", map[string]interface{}{"range": "2022-01-02", "status": "checked"}} + res, err := gou.NewProcess("yao.dashboard.Data", args...).Exec() + if err != nil { + t.Fatal(err) + } + data := any.Of(res).MapStr() + assert.Equal(t, 14, len(data)) +} + +func TestProcessComponent(t *testing.T) { + load(t) + args := []interface{}{ + "workspace", + "fields.filter.状态.edit.props.xProps", + "remote", + map[string]interface{}{"select": []string{"name", "status"}, "limit": 2}, + } + + res, err := gou.NewProcess("yao.dashboard.Component", args...).Exec() + if err != nil { + t.Fatal(err) + } + + pets, ok := res.([]maps.MapStr) + assert.True(t, ok) + assert.Equal(t, 2, len(pets)) + assert.Equal(t, "Cookie", pets[0]["name"]) + assert.Equal(t, "checked", pets[0]["status"]) + assert.Equal(t, "Baby", pets[1]["name"]) + assert.Equal(t, "checked", pets[1]["status"]) +} + +func TestProcessComponentError(t *testing.T) { + load(t) + args := []interface{}{ + "workspace", + "fields.filter.edit.props.状态.::not-exist", + "remote", + map[string]interface{}{"select": []string{"name", "status"}, "limit": 2}, + } + _, err := gou.NewProcess("yao.dashboard.Component", args...).Exec() + assert.Contains(t, err.Error(), "fields.filter.edit.props.状态.::not-exist") +} + +func TestProcessSetting(t *testing.T) { + load(t) + args := []interface{}{"workspace"} + res, err := gou.NewProcess("yao.dashboard.Setting", args...).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.filter."+url.QueryEscape("状态")+".edit.props.xProps/remote", data.Get("fields.filter.状态.edit.props.xProps.remote.api")) +} + +func TestProcessXgen(t *testing.T) { + load(t) + args := []interface{}{"workspace"} + res, err := gou.NewProcess("yao.dashboard.Xgen", args...).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.filter."+url.QueryEscape("状态")+".edit.props.xProps/remote", data.Get("fields.filter.状态.edit.props.xProps.remote.api")) +} + +func TestProcessXgenWithPermissions(t *testing.T) { + load(t) + session.Global().Set("__permissions", map[string]interface{}{ + "dashboards.workspace": []string{ + "7f46a38d7ff3f1832375ff63cd412f41", // operation.actions[0] 跳转至大屏 + "09302a46b1b6f13a346deeea79b859dd", // 时间区间 + "2e1b0010d201887cac2948ec75a2fae5", // 图表展示2 + "37dd001c9efada4f1d21530deba2a7cf", // 图表展示1 + }, + }) + + args := []interface{}{"workspace"} + res, err := gou.NewProcess("yao.dashboard.Xgen", args...).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.filter."+url.QueryEscape("状态")+".edit.props.xProps/remote", data.Get("fields.filter.状态.edit.props.xProps.remote.api")) + assert.NotEqual(t, "时间区间", data.Get("filter.columns[0].name")) + assert.Equal(t, nil, data.Get("actions[0]")) + assert.Equal(t, nil, data.Get("fields.dashboard.图表展示1")) + assert.Equal(t, nil, data.Get("fields.dashboard.图表展示2")) + + session.Global().Set("__permissions", nil) + res, err = gou.NewProcess("yao.dashboard.Xgen", args...).Exec() + if err != nil { + t.Fatal(err) + } + + data = any.Of(res).MapStr().Dot() + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.filter."+url.QueryEscape("状态")+".edit.props.xProps/remote", data.Get("fields.filter.状态.edit.props.xProps.remote.api")) + assert.Equal(t, "时间区间", data.Get("filter.columns[0].name")) + assert.NotEqual(t, nil, data.Get("actions[0]")) + assert.NotEqual(t, nil, data.Get("fields.dashboard.图表展示1")) + assert.NotEqual(t, nil, data.Get("fields.dashboard.图表展示2")) +} + +func load(t *testing.T) { + prepare(t) + err := Load(config.Conf) + if err != nil { + t.Fatal(err) + } + q.Load(config.Conf) + clear(t) + testData(t) +} + +func testData(t *testing.T) { + pet := gou.Select("pet") + err := pet.Insert( + []string{"name", "type", "status", "mode", "stay", "cost", "doctor_id"}, + [][]interface{}{ + {"Cookie", "cat", "checked", "enabled", 200, 105, 1}, + {"Baby", "dog", "checked", "enabled", 186, 24, 1}, + {"Poo", "others", "checked", "enabled", 199, 66, 1}, + }, + ) + if err != nil { + t.Fatal(err) + } +} + +func clear(t *testing.T) { + for _, m := range gou.Models { + err := m.DropTable() + if err != nil { + t.Fatal(err) + } + err = m.Migrate(true) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/widgets/dashboard/types.go b/widgets/dashboard/types.go new file mode 100644 index 0000000000..74d52c1bef --- /dev/null +++ b/widgets/dashboard/types.go @@ -0,0 +1,58 @@ +package dashboard + +import ( + "github.com/yaoapp/yao/widgets/action" + "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/compute" + "github.com/yaoapp/yao/widgets/field" + "github.com/yaoapp/yao/widgets/hook" + "github.com/yaoapp/yao/widgets/mapping" +) + +// DSL the dashboard DSL +type DSL struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Action *ActionDSL `json:"action"` + Layout *LayoutDSL `json:"layout"` + Fields *FieldsDSL `json:"fields"` + Config map[string]interface{} `json:"config,omitempty"` + CProps field.CloudProps `json:"-"` + compute.Computable + *mapping.Mapping +} + +// ActionDSL the dashboard action DSL +type ActionDSL struct { + Setting *action.Process `json:"setting,omitempty"` + Component *action.Process `json:"-"` + Data *action.Process `json:"data,omitempty"` + BeforeData *hook.Before `json:"before:data,omitempty"` + AfterData *hook.After `json:"after:data,omitempty"` +} + +// FieldsDSL the dashboard fields DSL +type FieldsDSL struct { + Filter field.Filters `json:"filter,omitempty"` + Dashboard field.Columns `json:"dashboard,omitempty"` + filterMap map[string]field.FilterDSL + dashboardMap map[string]field.ColumnDSL +} + +// LayoutDSL the dashboard layout DSL +type LayoutDSL struct { + Actions component.Actions `json:"actions,omitempty"` + Dashboard *ViewLayoutDSL `json:"dashboard,omitempty"` + Filter *FilterLayoutDSL `json:"filter,omitempty"` +} + +// FilterLayoutDSL layout.filter +type FilterLayoutDSL struct { + Actions component.Actions `json:"actions,omitempty"` + Columns component.Instances `json:"columns,omitempty"` +} + +// ViewLayoutDSL layout.form +type ViewLayoutDSL struct { + Columns component.Instances `json:"columns,omitempty"` +} diff --git a/widgets/dashboard/vaildate.go b/widgets/dashboard/vaildate.go new file mode 100644 index 0000000000..d035031f25 --- /dev/null +++ b/widgets/dashboard/vaildate.go @@ -0,0 +1,6 @@ +package dashboard + +// Validate table +func (dsl *DSL) Validate() error { + return nil +} From 448b0eaa0ea40a6b4ba901f16df6165858383df0 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 18 Dec 2022 16:54:33 +0800 Subject: [PATCH 2/3] [add] field.$data cloud props --- widgets/dashboard/dashboard_test.go | 7 ++++++ widgets/dashboard/mapping.go | 3 +++ widgets/dashboard/process_test.go | 39 +++++++++++++++++++++++++++-- widgets/field/column.go | 16 ++++++++++++ widgets/field/types.go | 13 +++++----- 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/widgets/dashboard/dashboard_test.go b/widgets/dashboard/dashboard_test.go index 87767c4bb9..33e7c08ae5 100644 --- a/widgets/dashboard/dashboard_test.go +++ b/widgets/dashboard/dashboard_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/flow" "github.com/yaoapp/yao/i18n" "github.com/yaoapp/yao/model" "github.com/yaoapp/yao/runtime" @@ -38,6 +39,12 @@ func prepare(t *testing.T, language ...string) { t.Fatal(err) } + // load flows + err = flow.Load(config.Conf) + if err != nil { + t.Fatal(err) + } + // export err = Export() if err != nil { diff --git a/widgets/dashboard/mapping.go b/widgets/dashboard/mapping.go index 8ec8122924..daa21b2936 100644 --- a/widgets/dashboard/mapping.go +++ b/widgets/dashboard/mapping.go @@ -131,6 +131,9 @@ func (dsl *DSL) mapping() error { // Columns return dsl.Fields.Dashboard.CPropsMerge(dsl.CProps, func(name string, kind string, column field.ColumnDSL) (xpath string) { + if kind == "data" { + return fmt.Sprintf("fields.dashboard.%s", name) + } return fmt.Sprintf("fields.dashboard.%s.%s.props", name, kind) }) diff --git a/widgets/dashboard/process_test.go b/widgets/dashboard/process_test.go index 6b903819b4..41b25b542d 100644 --- a/widgets/dashboard/process_test.go +++ b/widgets/dashboard/process_test.go @@ -45,6 +45,39 @@ func TestProcessComponent(t *testing.T) { assert.Equal(t, "checked", pets[0]["status"]) assert.Equal(t, "Baby", pets[1]["name"]) assert.Equal(t, "checked", pets[1]["status"]) + + args = []interface{}{ + "workspace", + "fields.dashboard.图表展示1", + "data", + map[string]interface{}{"foo": "bar"}, + } + + res2, err := gou.NewProcess("yao.dashboard.Component", args...).Exec() + if err != nil { + t.Fatal(err) + } + + values, ok := res2.([]interface{}) + assert.True(t, ok) + assert.Greater(t, len(values), 1) + + args = []interface{}{ + "workspace", + "fields.dashboard.图表展示2", + "data", + map[string]interface{}{"foo": "bar"}, + } + + res2, err = gou.NewProcess("yao.dashboard.Component", args...).Exec() + if err != nil { + t.Fatal(err) + } + + values, ok = res2.([]interface{}) + assert.True(t, ok) + assert.Greater(t, len(values), 1) + } func TestProcessComponentError(t *testing.T) { @@ -81,6 +114,8 @@ func TestProcessXgen(t *testing.T) { data := any.Of(res).MapStr().Dot() assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.filter."+url.QueryEscape("状态")+".edit.props.xProps/remote", data.Get("fields.filter.状态.edit.props.xProps.remote.api")) + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.dashboard."+url.QueryEscape("图表展示1")+"/data", data.Get("fields.dashboard.图表展示1.data.api")) + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.dashboard."+url.QueryEscape("图表展示2")+"/data", data.Get("fields.dashboard.图表展示2.data.api")) } func TestProcessXgenWithPermissions(t *testing.T) { @@ -89,8 +124,8 @@ func TestProcessXgenWithPermissions(t *testing.T) { "dashboards.workspace": []string{ "7f46a38d7ff3f1832375ff63cd412f41", // operation.actions[0] 跳转至大屏 "09302a46b1b6f13a346deeea79b859dd", // 时间区间 - "2e1b0010d201887cac2948ec75a2fae5", // 图表展示2 - "37dd001c9efada4f1d21530deba2a7cf", // 图表展示1 + "8b445709024e0e5361d8bcdd58c75fcb", // 图表展示2 + "0bdee1c9858ef2a821a0ff7109d3fc5b", // 图表展示1 }, }) diff --git a/widgets/field/column.go b/widgets/field/column.go index 3c659a3357..00346cf765 100644 --- a/widgets/field/column.go +++ b/widgets/field/column.go @@ -112,6 +112,10 @@ func (column ColumnDSL) Map() map[string]interface{} { "bind": column.Bind, } + if column.Data != nil { + res["data"] = map[string]interface{}{"process": column.Data.Process, "query": column.Data.Query} + } + if column.Link != "" { res["link"] = column.Link } @@ -131,6 +135,18 @@ func (columns Columns) CPropsMerge(cloudProps map[string]component.CloudPropsDSL for name, column := range columns { + if column.Data != nil { + xpath := getXpath(name, "data", column) + cProps := map[string]component.CloudPropsDSL{} + cprop := *column.Data + cprop.Xpath = xpath + cprop.Name = "data" + cprop.Type = "data" + fullname := fmt.Sprintf("%s.$data", xpath) + cProps[fullname] = cprop + mergeCProps(cloudProps, cProps) + } + if column.Edit != nil && column.Edit.Props != nil { xpath := getXpath(name, "edit", column) cProps, err := column.Edit.Props.CloudProps(xpath, column.Edit.Type) diff --git a/widgets/field/types.go b/widgets/field/types.go index 5145da32d4..58fd1daa35 100644 --- a/widgets/field/types.go +++ b/widgets/field/types.go @@ -18,12 +18,13 @@ type CloudProps map[string]component.CloudPropsDSL // ColumnDSL the field column dsl type ColumnDSL struct { - ID string `json:"id,omitempty"` - Key string `json:"key,omitempty"` - Bind string `json:"bind,omitempty"` - Link string `json:"link,omitempty"` - View *component.DSL `json:"view,omitempty"` - Edit *component.DSL `json:"edit,omitempty"` + ID string `json:"id,omitempty"` + Data *component.CloudPropsDSL `json:"$data,omitempty"` + Key string `json:"key,omitempty"` + Bind string `json:"bind,omitempty"` + Link string `json:"link,omitempty"` + View *component.DSL `json:"view,omitempty"` + Edit *component.DSL `json:"edit,omitempty"` } type aliasColumnDSL ColumnDSL From 605d4d9aabc8fc20849db49cff54fd895fd84e46 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 18 Dec 2022 17:06:58 +0800 Subject: [PATCH 3/3] [add] widget dashboard on change --- widgets/dashboard/dashboard.go | 15 ++++++++++++++- widgets/dashboard/fields.go | 26 ++++++++++++++++++++++++++ widgets/dashboard/process_test.go | 4 ++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/widgets/dashboard/dashboard.go b/widgets/dashboard/dashboard.go index 5fd9e8423d..09abe5f853 100644 --- a/widgets/dashboard/dashboard.go +++ b/widgets/dashboard/dashboard.go @@ -169,6 +169,8 @@ func (dsl *DSL) Xgen(data map[string]interface{}, excludes map[string]bool) (map setting["fields"] = fields setting["config"] = dsl.Config + + onChange := map[string]interface{}{} // Hooks for _, cProp := range dsl.CProps { err := cProp.Replace(setting, func(cProp component.CloudPropsDSL) interface{} { return map[string]interface{}{ @@ -179,8 +181,19 @@ func (dsl *DSL) Xgen(data map[string]interface{}, excludes map[string]bool) (map if err != nil { return nil, err } - } + // hooks + if cProp.Name == "on:change" { + field := strings.TrimPrefix(cProp.Xpath, "fields.dashboard.") + field = strings.TrimSuffix(field, ".view.props") + field = strings.TrimSuffix(field, ".edit.props") + onChange[field] = map[string]interface{}{ + "api": fmt.Sprintf("/api/__yao/dashboard/%s%s", dsl.ID, cProp.Path()), + "params": cProp.Query, + } + } + } + setting["hooks"] = map[string]interface{}{"onChange": onChange} return setting, nil } diff --git a/widgets/dashboard/fields.go b/widgets/dashboard/fields.go index a9d2e6e976..d5876e3fd2 100644 --- a/widgets/dashboard/fields.go +++ b/widgets/dashboard/fields.go @@ -22,12 +22,38 @@ func (fields *FieldsDSL) Xgen(layout *LayoutDSL) (map[string]interface{}, error) if layout.Dashboard != nil && layout.Dashboard.Columns != nil { for _, inst := range layout.Dashboard.Columns { if c, has := fields.Dashboard[inst.Name]; has { + + if c.Edit != nil && c.Edit.Props != nil { + if _, has := c.Edit.Props["$on:change"]; has { + delete(c.Edit.Props, "$on:change") + } + } + + if c.View != nil && c.View.Props != nil { + if _, has := c.View.Props["$on:change"]; has { + delete(c.View.Props, "$on:change") + } + } + columns[inst.Name] = c } if inst.Rows != nil { for _, inst := range inst.Rows { if c, has := fields.Dashboard[inst.Name]; has { + + if c.Edit != nil && c.Edit.Props != nil { + if _, has := c.Edit.Props["$on:change"]; has { + delete(c.Edit.Props, "$on:change") + } + } + + if c.View != nil && c.View.Props != nil { + if _, has := c.View.Props["$on:change"]; has { + delete(c.View.Props, "$on:change") + } + } + columns[inst.Name] = c } } diff --git a/widgets/dashboard/process_test.go b/widgets/dashboard/process_test.go index 41b25b542d..ff46e2614f 100644 --- a/widgets/dashboard/process_test.go +++ b/widgets/dashboard/process_test.go @@ -102,6 +102,9 @@ func TestProcessSetting(t *testing.T) { data := any.Of(res).MapStr().Dot() assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.filter."+url.QueryEscape("状态")+".edit.props.xProps/remote", data.Get("fields.filter.状态.edit.props.xProps.remote.api")) + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.dashboard."+url.QueryEscape("图表展示1")+"/data", data.Get("fields.dashboard.图表展示1.data.api")) + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.dashboard."+url.QueryEscape("图表展示2")+"/data", data.Get("fields.dashboard.图表展示2.data.api")) + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.dashboard."+url.QueryEscape("宠物列表")+".view.props/"+url.QueryEscape("on:change"), data.Get("hooks.onChange.宠物列表.api")) } func TestProcessXgen(t *testing.T) { @@ -116,6 +119,7 @@ func TestProcessXgen(t *testing.T) { assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.filter."+url.QueryEscape("状态")+".edit.props.xProps/remote", data.Get("fields.filter.状态.edit.props.xProps.remote.api")) assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.dashboard."+url.QueryEscape("图表展示1")+"/data", data.Get("fields.dashboard.图表展示1.data.api")) assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.dashboard."+url.QueryEscape("图表展示2")+"/data", data.Get("fields.dashboard.图表展示2.data.api")) + assert.Equal(t, "/api/__yao/dashboard/workspace/component/fields.dashboard."+url.QueryEscape("宠物列表")+".view.props/"+url.QueryEscape("on:change"), data.Get("hooks.onChange.宠物列表.api")) } func TestProcessXgenWithPermissions(t *testing.T) {