From 53aa6d1c0364d0c032592d94c4a37856e16fb7ee Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 5 Oct 2021 16:00:45 +0200 Subject: [PATCH 1/5] Add dashboard extract command --- cli/dashboard/dashboard.go | 1 + cli/dashboard/extract.go | 74 ++++++++++++++++++++++++++++++++ command/dashboard/extract.go | 81 ++++++++++++++++++++++++++++++++++++ internal/iot/client.go | 12 ++++++ internal/template/extract.go | 47 +++++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 cli/dashboard/extract.go create mode 100644 command/dashboard/extract.go diff --git a/cli/dashboard/dashboard.go b/cli/dashboard/dashboard.go index 4531db1f..ae39236f 100644 --- a/cli/dashboard/dashboard.go +++ b/cli/dashboard/dashboard.go @@ -30,6 +30,7 @@ func NewCommand() *cobra.Command { dashboardCommand.AddCommand(initListCommand()) dashboardCommand.AddCommand(initDeleteCommand()) + dashboardCommand.AddCommand(initExtractCommand()) return dashboardCommand } diff --git a/cli/dashboard/extract.go b/cli/dashboard/extract.go new file mode 100644 index 00000000..4ee3680b --- /dev/null +++ b/cli/dashboard/extract.go @@ -0,0 +1,74 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package dashboard + +import ( + "os" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/dashboard" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var extractFlags struct { + id string + outfile string + format string +} + +func initExtractCommand() *cobra.Command { + extractCommand := &cobra.Command{ + Use: "extract", + Short: "Extract a template from a dashboard", + Long: "Extract a template from a Arduino IoT Cloud dashboard and save it in a file", + Run: runExtractCommand, + } + extractCommand.Flags().StringVarP(&extractFlags.id, "id", "i", "", "Dashboard ID") + extractCommand.Flags().StringVarP(&extractFlags.outfile, "outfile", "o", "", "Template file destination path") + extractCommand.Flags().StringVar( + &extractFlags.format, + "format", + "yaml", + "Format of template file, can be {json|yaml}. Default is 'yaml'", + ) + + extractCommand.MarkFlagRequired("id") + return extractCommand +} + +func runExtractCommand(cmd *cobra.Command, args []string) { + logrus.Infof("Extracting template from dashboard %s\n", extractFlags.id) + + params := &dashboard.ExtractParams{ + ID: extractFlags.id, + Format: extractFlags.format, + } + if extractFlags.outfile != "" { + params.Outfile = &extractFlags.outfile + } + + err := dashboard.Extract(params) + if err != nil { + feedback.Errorf("Error during template extraction: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + + logrus.Info("Template successfully extracted") +} diff --git a/command/dashboard/extract.go b/command/dashboard/extract.go new file mode 100644 index 00000000..8b2a2208 --- /dev/null +++ b/command/dashboard/extract.go @@ -0,0 +1,81 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package dashboard + +import ( + "errors" + "fmt" + "strings" + + "github.com/arduino/arduino-cloud-cli/internal/config" + "github.com/arduino/arduino-cloud-cli/internal/iot" + "github.com/arduino/arduino-cloud-cli/internal/template" +) + +// ExtractParams contains the parameters needed to +// extract a template dashboard from Arduino IoT Cloud and save it on local storage. +type ExtractParams struct { + ID string + Format string // Format determines the file format of the template ("json" or "yaml") + Outfile *string // Destination path of the extracted template +} + +// Extract command is used to extract a dashboard template +// from a dashboard on Arduino IoT Cloud. +func Extract(params *ExtractParams) error { + params.Format = strings.ToLower(params.Format) + if params.Format != "json" && params.Format != "yaml" { + return errors.New("format is not valid: only 'json' and 'yaml' are supported") + } + + conf, err := config.Retrieve() + if err != nil { + return err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return err + } + + dashboard, err := iotClient.DashboardShow(params.ID) + if err != nil { + err = fmt.Errorf("%s: %w", "cannot extract dashboard: ", err) + return err + } + + templ, err := template.FromDashboard(dashboard) + if err != nil { + return err + } + + if params.Outfile == nil { + name, ok := templ["name"].(string) + if name == "" || !ok { + return errors.New("dashboard template does not have a valid name") + } + outfile := name + "-dashboard" + "." + params.Format + params.Outfile = &outfile + } + + err = template.ToFile(templ, *params.Outfile, params.Format) + if err != nil { + return fmt.Errorf("saving template: %w", err) + } + + return nil +} diff --git a/internal/iot/client.go b/internal/iot/client.go index 7d31ad75..54aac5b5 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -39,6 +39,7 @@ type Client interface { ThingDelete(id string) error ThingShow(id string) (*iotclient.ArduinoThing, error) ThingList(ids []string, device *string, props bool) ([]iotclient.ArduinoThing, error) + DashboardShow(id string) (*iotclient.ArduinoDashboardv2, error) DashboardDelete(id string) error DashboardList() ([]iotclient.ArduinoDashboardv2, error) } @@ -204,6 +205,17 @@ func (cl *client) ThingList(ids []string, device *string, props bool) ([]iotclie return things, nil } +// DashboardShow allows to retrieve a specific dashboard, given its id, +// from Arduino IoT Cloud. +func (cl *client) DashboardShow(id string) (*iotclient.ArduinoDashboardv2, error) { + dashboard, _, err := cl.api.DashboardsV2Api.DashboardsV2Show(cl.ctx, id) + if err != nil { + err = fmt.Errorf("retrieving dashboard, %w", errorDetail(err)) + return nil, err + } + return &dashboard, nil +} + // DashboardList returns a list of dashboards on Arduino IoT Cloud. func (cl *client) DashboardList() ([]iotclient.ArduinoDashboardv2, error) { dashboards, _, err := cl.api.DashboardsV2Api.DashboardsV2List(cl.ctx, nil) diff --git a/internal/template/extract.go b/internal/template/extract.go index b9790561..c0d6f67e 100644 --- a/internal/template/extract.go +++ b/internal/template/extract.go @@ -50,6 +50,48 @@ func FromThing(thing *iotclient.ArduinoThing) (map[string]interface{}, error) { return template, nil } +// FromDashboard extracts a template of type map[string]interface{} from a dashboard. +func FromDashboard(dashboard *iotclient.ArduinoDashboardv2) (map[string]interface{}, error) { + template := make(map[string]interface{}) + template["name"] = dashboard.Name + + // Extract template from dashboard structure + var widgets []map[string]interface{} + for _, w := range dashboard.Widgets { + widget := make(map[string]interface{}) + widget["type"] = w.Type + widget["name"] = w.Name + widget["width"] = w.Width + widget["height"] = w.Height + widget["x"] = w.X + widget["y"] = w.Y + + if w.WidthMobile != 0 && w.HeightMobile != 0 { + widget["width_mobile"] = w.WidthMobile + widget["height_mobile"] = w.HeightMobile + widget["x_mobile"] = w.XMobile + widget["y_mobile"] = w.YMobile + } + + var vars []map[string]interface{} + for _, v := range w.Variables { + variable := make(map[string]interface{}) + variable["thing_id"] = v.ThingName + variable["variable_id"] = v.VariableName + vars = append(vars, variable) + } + widget["variables"] = vars + + if validateWidgetOptions(w.Options) { + widget["options"] = w.Options + } + + widgets = append(widgets, widget) + } + template["widgets"] = widgets + return template, nil +} + // ToFile takes a generic template and saves it into a file, // in the specified format (yaml or json). func ToFile(template map[string]interface{}, outfile string, format string) error { @@ -79,3 +121,8 @@ func ToFile(template map[string]interface{}, outfile string, format string) erro return nil } + +// A whitelist is needed to validate widget options +func validateWidgetOptions(opts map[string]interface{}) bool { + return opts != nil +} From df56ae835ce52f35309c30c7d49d712643016abb Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Wed, 6 Oct 2021 11:21:24 +0200 Subject: [PATCH 2/5] Include dashboard variables only if any var is present --- internal/template/extract.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/template/extract.go b/internal/template/extract.go index c0d6f67e..a889a6d8 100644 --- a/internal/template/extract.go +++ b/internal/template/extract.go @@ -80,7 +80,9 @@ func FromDashboard(dashboard *iotclient.ArduinoDashboardv2) (map[string]interfac variable["variable_id"] = v.VariableName vars = append(vars, variable) } - widget["variables"] = vars + if len(vars) > 0 { + widget["variables"] = vars + } if validateWidgetOptions(w.Options) { widget["options"] = w.Options From c22fe380f046e68a1fbf1cb483827e979d209740 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 12 Oct 2021 10:45:14 +0200 Subject: [PATCH 3/5] Add widget options filter --- internal/template/extract.go | 8 ++----- internal/template/filter.go | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 internal/template/filter.go diff --git a/internal/template/extract.go b/internal/template/extract.go index a889a6d8..3a706b4a 100644 --- a/internal/template/extract.go +++ b/internal/template/extract.go @@ -84,7 +84,8 @@ func FromDashboard(dashboard *iotclient.ArduinoDashboardv2) (map[string]interfac widget["variables"] = vars } - if validateWidgetOptions(w.Options) { + filterWidgetOptions(w.Options) + if len(w.Options) > 0 { widget["options"] = w.Options } @@ -123,8 +124,3 @@ func ToFile(template map[string]interface{}, outfile string, format string) erro return nil } - -// A whitelist is needed to validate widget options -func validateWidgetOptions(opts map[string]interface{}) bool { - return opts != nil -} diff --git a/internal/template/filter.go b/internal/template/filter.go new file mode 100644 index 00000000..5a83a991 --- /dev/null +++ b/internal/template/filter.go @@ -0,0 +1,41 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package template + +var ( + widgetOptWhitelist = map[string]struct{}{ + "showThing": {}, + "frameless": {}, + "interpolation": {}, + "max": {}, + "min": {}, + "mode": {}, + "percentage": {}, + "showLabels": {}, + "step": {}, + "vertical": {}, + } +) + +func filterWidgetOptions(opts map[string]interface{}) { + for opt := range opts { + if _, ok := widgetOptWhitelist[opt]; !ok { + delete(opts, opt) + } + } +} From 0d917ec22e8825590ca26b874bec0ed452c75d63 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 12 Oct 2021 16:07:47 +0200 Subject: [PATCH 4/5] Include widgets only if not empty --- internal/template/extract.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/template/extract.go b/internal/template/extract.go index 3a706b4a..69b6adb4 100644 --- a/internal/template/extract.go +++ b/internal/template/extract.go @@ -91,7 +91,9 @@ func FromDashboard(dashboard *iotclient.ArduinoDashboardv2) (map[string]interfac widgets = append(widgets, widget) } - template["widgets"] = widgets + if len(widgets) > 0 { + template["widgets"] = widgets + } return template, nil } From d9566d2d59023679bdaef162a2758bb43524f692 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 12 Oct 2021 11:04:41 +0200 Subject: [PATCH 5/5] Update readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 49153296..7273cd97 100644 --- a/README.md +++ b/README.md @@ -128,3 +128,7 @@ Print a list of available dashboards and their widgets by using this command: Delete a dashboard with the following command: `$ arduino-cloud-cli dashboard delete --id ` + +Extract a template from an existing dashboard. The template can be saved in two formats: json or yaml. The default format is yaml: + +`$ arduino-cloud-cli dashboard extract --id --outfile --format `