Skip to content

Add dashboard extract command #47

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

Merged
merged 5 commits into from
Oct 12, 2021
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dashboardID>`

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 <dashboardID> --outfile <templateFile> --format <yaml|json>`
1 change: 1 addition & 0 deletions cli/dashboard/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func NewCommand() *cobra.Command {

dashboardCommand.AddCommand(initListCommand())
dashboardCommand.AddCommand(initDeleteCommand())
dashboardCommand.AddCommand(initExtractCommand())

return dashboardCommand
}
74 changes: 74 additions & 0 deletions cli/dashboard/extract.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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")
}
81 changes: 81 additions & 0 deletions command/dashboard/extract.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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
}
12 changes: 12 additions & 0 deletions internal/iot/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
47 changes: 47 additions & 0 deletions internal/template/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,53 @@ 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)
}
if len(vars) > 0 {
widget["variables"] = vars
}

filterWidgetOptions(w.Options)
if len(w.Options) > 0 {
widget["options"] = w.Options
}

widgets = append(widgets, widget)
}
if len(widgets) > 0 {
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 {
Expand Down
41 changes: 41 additions & 0 deletions internal/template/filter.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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)
}
}
}