diff --git a/README.md b/README.md index c5177458..42f8598b 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,9 @@ The default OTA upload should complete in 10 minutes. Use `--deferred` flag to e `$ arduino-cloud-cli ota upload --device-id --file ` `$ arduino-cloud-cli ota upload --device-id --file --deferred` + +## Dashboard commands + +Print a list of available dashboards and their widgets by using this command: + +`$ arduino-cloud-cli dashboard list --show-widgets` \ No newline at end of file diff --git a/cli/cli.go b/cli/cli.go index 7f8ccc11..7b9a8267 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -26,6 +26,7 @@ import ( "github.com/arduino/arduino-cli/cli/errorcodes" "github.com/arduino/arduino-cli/cli/feedback" "github.com/arduino/arduino-cloud-cli/cli/config" + "github.com/arduino/arduino-cloud-cli/cli/dashboard" "github.com/arduino/arduino-cloud-cli/cli/device" "github.com/arduino/arduino-cloud-cli/cli/ota" "github.com/arduino/arduino-cloud-cli/cli/thing" @@ -51,6 +52,7 @@ func Execute() { cli.AddCommand(config.NewCommand()) cli.AddCommand(device.NewCommand()) cli.AddCommand(thing.NewCommand()) + cli.AddCommand(dashboard.NewCommand()) cli.AddCommand(ota.NewCommand()) cli.PersistentFlags().BoolVarP(&cliFlags.verbose, "verbose", "v", false, "Print the logs on the standard output.") diff --git a/cli/dashboard/dashboard.go b/cli/dashboard/dashboard.go new file mode 100644 index 00000000..4d79ac91 --- /dev/null +++ b/cli/dashboard/dashboard.go @@ -0,0 +1,34 @@ +// 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 ( + "github.com/spf13/cobra" +) + +func NewCommand() *cobra.Command { + dashboardCommand := &cobra.Command{ + Use: "dashboard", + Short: "Dashboard commands.", + Long: "Dashboard commands.", + } + + dashboardCommand.AddCommand(initListCommand()) + + return dashboardCommand +} diff --git a/cli/dashboard/list.go b/cli/dashboard/list.go new file mode 100644 index 00000000..f68abae6 --- /dev/null +++ b/cli/dashboard/list.go @@ -0,0 +1,112 @@ +// 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 ( + "math" + "os" + "strings" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cli/table" + "github.com/arduino/arduino-cloud-cli/command/dashboard" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +const ( + widgetsPerRow = 3 +) + +var listFlags struct { + showWidgets bool +} + +func initListCommand() *cobra.Command { + listCommand := &cobra.Command{ + Use: "list", + Short: "List dashboards", + Long: "List dashboards on Arduino IoT Cloud", + Run: runListCommand, + } + + listCommand.Flags().BoolVarP(&listFlags.showWidgets, "show-widgets", "s", false, "Show names of dashboard widgets") + return listCommand +} + +func runListCommand(cmd *cobra.Command, args []string) { + logrus.Info("Listing dashboards") + + dash, err := dashboard.List() + if err != nil { + feedback.Errorf("Error during dashboard list: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + + feedback.PrintResult(listResult{dash}) +} + +type listResult struct { + dashboards []dashboard.DashboardInfo +} + +func (r listResult) Data() interface{} { + return r.dashboards +} + +func (r listResult) String() string { + if len(r.dashboards) == 0 { + return "No dashboard found." + } + t := table.New() + + head := []interface{}{"Name", "ID", "UpdatedAt"} + if listFlags.showWidgets { + head = append(head, "Widgets") + } + t.SetHeader(head...) + + for _, dash := range r.dashboards { + row := []interface{}{dash.Name, dash.ID, dash.UpdatedAt} + + if listFlags.showWidgets { + // Limit number of widgets per row. + if len(dash.Widgets) > widgetsPerRow { + row = append(row, strings.Join(dash.Widgets[:widgetsPerRow], ", ")) + dash.Widgets = dash.Widgets[widgetsPerRow:] + } else { + row = append(row, strings.Join(dash.Widgets, ", ")) + dash.Widgets = nil + } + } + t.AddRow(row...) + + // Print remaining widgets in new rows + if listFlags.showWidgets { + for len(dash.Widgets) > 0 { + row := []interface{}{"", "", ""} + l := int(math.Min(float64(len(dash.Widgets)), widgetsPerRow)) + row = append(row, strings.Join(dash.Widgets[:l], ", ")) + dash.Widgets = dash.Widgets[l:] + t.AddRow(row...) + } + } + } + return t.Render() +} diff --git a/command/dashboard/dashboard.go b/command/dashboard/dashboard.go new file mode 100644 index 00000000..b1da267b --- /dev/null +++ b/command/dashboard/dashboard.go @@ -0,0 +1,45 @@ +// 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 ( + iotclient "github.com/arduino/iot-client-go" +) + +// DashboardInfo contains the most interesting +// information, in string format, of an Arduino IoT Cloud dashboard. +type DashboardInfo struct { + Name string `json:"name"` + ID string `json:"id"` + UpdatedAt string `json:"updated_at"` + Widgets []string `json:"widgets"` +} + +func getDashboardInfo(dashboard *iotclient.ArduinoDashboardv2) *DashboardInfo { + var widgets []string + for _, w := range dashboard.Widgets { + widgets = append(widgets, w.Name) + } + info := &DashboardInfo{ + Name: dashboard.Name, + ID: dashboard.Id, + UpdatedAt: dashboard.UpdatedAt.String(), + Widgets: widgets, + } + return info +} diff --git a/command/dashboard/list.go b/command/dashboard/list.go new file mode 100644 index 00000000..5e9aeb43 --- /dev/null +++ b/command/dashboard/list.go @@ -0,0 +1,49 @@ +// 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 ( + "github.com/arduino/arduino-cloud-cli/internal/config" + "github.com/arduino/arduino-cloud-cli/internal/iot" +) + +// List command is used to list +// the dashboards of Arduino IoT Cloud. +func List() ([]DashboardInfo, error) { + conf, err := config.Retrieve() + if err != nil { + return nil, err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return nil, err + } + + foundDashboards, err := iotClient.DashboardList() + if err != nil { + return nil, err + } + + var dashboards []DashboardInfo + for _, found := range foundDashboards { + info := getDashboardInfo(&found) + dashboards = append(dashboards, *info) + } + + return dashboards, nil +} diff --git a/go.mod b/go.mod index 260e1ebb..68e1a4f1 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/antihax/optional v1.0.0 github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3 github.com/arduino/go-paths-helper v1.6.1 - github.com/arduino/iot-client-go v1.3.4-0.20210902151346-1cd63fb0c784 + github.com/arduino/iot-client-go v1.3.4-0.20210930122852-04551f4cb061 github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index e8af50c8..2fdc7a11 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,8 @@ github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4l github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b/go.mod h1:uwGy5PpN4lqW97FiLnbcx+xx8jly5YuPMJWfVwwjJiQ= github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b h1:3PjgYG5gVPA7cipp7vIR2lF96KkEJIFBJ+ANnuv6J20= github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b/go.mod h1:iIPnclBMYm1g32Q5kXoqng4jLhMStReIP7ZxaoUC2y8= -github.com/arduino/iot-client-go v1.3.4-0.20210902151346-1cd63fb0c784 h1:Wo4g3vuu8Tv57GggLK1T2as/xEqG7CIDnOA49C6Ce7I= -github.com/arduino/iot-client-go v1.3.4-0.20210902151346-1cd63fb0c784/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0= +github.com/arduino/iot-client-go v1.3.4-0.20210930122852-04551f4cb061 h1:uQeaIHzj0tOlqnHaRskSy6UwMgQ6LIOECySpaYBCt5M= +github.com/arduino/iot-client-go v1.3.4-0.20210930122852-04551f4cb061/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= diff --git a/internal/iot/client.go b/internal/iot/client.go index 831feae6..e63808c5 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) + DashboardList() ([]iotclient.ArduinoDashboardv2, error) } type client struct { @@ -202,6 +203,16 @@ func (cl *client) ThingList(ids []string, device *string, props bool) ([]iotclie return things, 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) + if err != nil { + err = fmt.Errorf("listing dashboards: %w", errorDetail(err)) + return nil, err + } + return dashboards, nil +} + func (cl *client) setup(client, secret string) error { // Get the access token in exchange of client_id and client_secret tok, err := token(client, secret)