From f4a544f893198f96e12bebfeb076fdbf28c6188d Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Wed, 4 Dec 2024 12:56:49 -0600 Subject: [PATCH 01/18] Scaffold out custom dashboard base commands --- pkg/api/interface.go | 6 + pkg/commands/commands.go | 13 + pkg/commands/dashboard/common.go | 1 + pkg/commands/dashboard/create.go | 43 +++ pkg/commands/dashboard/dashboard_test.go | 329 +++++++++++++++++++++++ pkg/commands/dashboard/delete.go | 40 +++ pkg/commands/dashboard/describe.go | 46 ++++ pkg/commands/dashboard/doc.go | 2 + pkg/commands/dashboard/list.go | 48 ++++ pkg/commands/dashboard/root.go | 31 +++ pkg/commands/dashboard/update.go | 43 +++ pkg/mock/api.go | 31 +++ 12 files changed, 633 insertions(+) create mode 100644 pkg/commands/dashboard/common.go create mode 100644 pkg/commands/dashboard/create.go create mode 100644 pkg/commands/dashboard/dashboard_test.go create mode 100644 pkg/commands/dashboard/delete.go create mode 100644 pkg/commands/dashboard/describe.go create mode 100644 pkg/commands/dashboard/doc.go create mode 100644 pkg/commands/dashboard/list.go create mode 100644 pkg/commands/dashboard/root.go create mode 100644 pkg/commands/dashboard/update.go diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 93998b511..67fb76f82 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -406,6 +406,12 @@ type Interface interface { DeleteAlertDefinition(i *fastly.DeleteAlertDefinitionInput) error TestAlertDefinition(i *fastly.TestAlertDefinitionInput) error ListAlertHistory(i *fastly.ListAlertHistoryInput) (*fastly.AlertHistoryResponse, error) + + ListObservabilityCustomDashboards(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ListDashboardsResponse, error) + CreateObservabilityCustomDashboard(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) + GetObservabilityCustomDashboard(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) + UpdateObservabilityCustomDashboard(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) + DeleteObservabilityCustomDashboard(i *fastly.DeleteObservabilityCustomDashboardInput) error } // RealtimeStatsInterface is the subset of go-fastly's realtime stats API used here. diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index a9079d7aa..bd9bfad19 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -13,6 +13,7 @@ import ( "github.com/fastly/cli/pkg/commands/config" "github.com/fastly/cli/pkg/commands/configstore" "github.com/fastly/cli/pkg/commands/configstoreentry" + "github.com/fastly/cli/pkg/commands/dashboard" "github.com/fastly/cli/pkg/commands/dictionary" "github.com/fastly/cli/pkg/commands/dictionaryentry" "github.com/fastly/cli/pkg/commands/domain" @@ -154,6 +155,12 @@ func Define( configstoreentryDescribe := configstoreentry.NewDescribeCommand(configstoreentryCmdRoot.CmdClause, data) configstoreentryList := configstoreentry.NewListCommand(configstoreentryCmdRoot.CmdClause, data) configstoreentryUpdate := configstoreentry.NewUpdateCommand(configstoreentryCmdRoot.CmdClause, data) + dashboardCmdRoot := dashboard.NewRootCommand(app, data) + dashboardList := dashboard.NewListCommand(dashboardCmdRoot.CmdClause, data) + dashboardCreate := dashboard.NewCreateCommand(dashboardCmdRoot.CmdClause, data) + dashboardDescribe := dashboard.NewDescribeCommand(dashboardCmdRoot.CmdClause, data) + dashboardUpdate := dashboard.NewUpdateCommand(dashboardCmdRoot.CmdClause, data) + dashboardDelete := dashboard.NewDeleteCommand(dashboardCmdRoot.CmdClause, data) dictionaryCmdRoot := dictionary.NewRootCommand(app, data) dictionaryCreate := dictionary.NewCreateCommand(dictionaryCmdRoot.CmdClause, data) dictionaryDelete := dictionary.NewDeleteCommand(dictionaryCmdRoot.CmdClause, data) @@ -535,6 +542,12 @@ func Define( configstoreentryDescribe, configstoreentryList, configstoreentryUpdate, + dashboardCmdRoot, + dashboardList, + dashboardCreate, + dashboardDescribe, + dashboardUpdate, + dashboardDelete, dictionaryCmdRoot, dictionaryCreate, dictionaryDelete, diff --git a/pkg/commands/dashboard/common.go b/pkg/commands/dashboard/common.go new file mode 100644 index 000000000..cfdd5f819 --- /dev/null +++ b/pkg/commands/dashboard/common.go @@ -0,0 +1 @@ +package dashboard diff --git a/pkg/commands/dashboard/create.go b/pkg/commands/dashboard/create.go new file mode 100644 index 000000000..ee10ab6df --- /dev/null +++ b/pkg/commands/dashboard/create.go @@ -0,0 +1,43 @@ +package dashboard + +import ( + "io" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, globals *global.Data) *CreateCommand { + var c CreateCommand + c.CmdClause = parent.Command("create", "Create a custom dashboard").Alias("add") + c.Globals = globals + + // Required flags + + // Optional flags + c.RegisterFlagBool(c.JSONFlag()) // --json + + return &c +} + +// CreateCommand calls the Fastly API to create an appropriate resource. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { + // text.Success(out, "Created <...> '%s' (service: %s, version: %d)", r.<...>, r.ServiceID, r.ServiceVersion) + return nil +} + +// constructInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *CreateCommand) constructInput() *fastly.CreateObservabilityCustomDashboardInput { + var input fastly.CreateObservabilityCustomDashboardInput + + return &input +} diff --git a/pkg/commands/dashboard/dashboard_test.go b/pkg/commands/dashboard/dashboard_test.go new file mode 100644 index 000000000..1d3d54515 --- /dev/null +++ b/pkg/commands/dashboard/dashboard_test.go @@ -0,0 +1,329 @@ +package dashboard_test + +import ( + "testing" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +const ( + baseCommand = "dashboard" +) + +func TestCreate(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --version flag", + WantError: "error parsing arguments: required flag --version not provided", + }, + { + Name: "validate missing --service-id flag", + Args: "--version 3", + WantError: "error reading service: no service ID found", + }, + { + Name: "validate missing --autoclone flag with 'active' service", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + }, + Args: "--service-id 123 --version 1", + WantError: "service version 1 is active", + }, + { + Name: "validate missing --autoclone flag with 'locked' service", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + }, + Args: "--service-id 123 --version 2", + WantError: "service version 2 is locked", + }, + { + Name: "validate CreateObservabilityCustomDashboard API error", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CreateObservabilityCustomDashboardFn: func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return nil, testutil.Err + }, + }, + Args: "--service-id 123 --version 3", + WantError: testutil.Err.Error(), + }, + { + Name: "validate CreateObservabilityCustomDashboard API success", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CreateObservabilityCustomDashboardFn: func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return &fastly.ObservabilityCustomDashboard{ + ServiceID: i.ServiceID, + }, nil + }, + }, + Args: "--service-id 123 --version 3", + WantOutput: "Created <...> '456' (service: 123)", + }, + { + Name: "validate --autoclone results in cloned service version", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + CreateObservabilityCustomDashboardFn: func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return &fastly.VCL{ + ServiceID: i.ServiceID, + ServiceVersion: i.ServiceVersion, + }, nil + }, + }, + Args: "--autoclone --service-id 123 --version 1", + WantOutput: "Created <...> 'foo' (service: 123, version: 4)", + }, + } + + testutil.RunCLIScenarios(t, []string{baseCommand, "create"}, scenarios) +} + +func TestDelete(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --version flag", + WantError: "error parsing arguments: required flag --version not provided", + }, + { + Name: "validate missing --service-id flag", + Args: "--version 1", + WantError: "error reading service: no service ID found", + }, + { + Name: "validate missing --autoclone flag with 'active' service", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + }, + Args: "--service-id 123 --version 1", + WantError: "service version 1 is active", + }, + { + Name: "validate missing --autoclone flag with 'locked' service", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + }, + Args: "--service-id 123 --version 2", + WantError: "service version 2 is locked", + }, + { + Name: "validate DeleteObservabilityCustomDashboard API error", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + DeleteObservabilityCustomDashboardFn: func(i *fastly.DeleteObservabilityCustomDashboardInput) error { + return testutil.Err + }, + }, + Args: "--service-id 123 --version 3", + WantError: testutil.Err.Error(), + }, + { + Name: "validate DeleteObservabilityCustomDashboard API success", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + DeleteObservabilityCustomDashboardFn: func(i *fastly.DeleteObservabilityCustomDashboardInput) error { + return nil + }, + }, + Args: "--service-id 123 --version 3", + WantOutput: "Deleted <...> '456' (service: 123)", + }, + { + Name: "validate --autoclone results in cloned service version", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + DeleteObservabilityCustomDashboardFn: func(i *fastly.DeleteObservabilityCustomDashboardInput) error { + return nil + }, + }, + Args: "--autoclone --service-id 123 --version 1", + WantOutput: "Deleted <...> 'foo' (service: 123, version: 4)", + }, + } + + testutil.RunCLIScenarios(t, []string{baseCommand, "delete"}, scenarios) +} + +func TestDescribe(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --version flag", + WantError: "error parsing arguments: required flag --version not provided", + }, + { + Name: "validate missing --service-id flag", + Args: "--version 3", + WantError: "error reading service: no service ID found", + }, + { + Name: "validate GetObservabilityCustomDashboard API error", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + GetObservabilityCustomDashboardFn: func(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return nil, testutil.Err + }, + }, + Args: "--service-id 123 --version 3", + WantError: testutil.Err.Error(), + }, + { + Name: "validate GetObservabilityCustomDashboard API success", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + GetObservabilityCustomDashboardFn: getObservabilityCustomDashboard, + }, + Args: "--service-id 123 --version 3", + WantOutput: "<...>", + }, + } + + testutil.RunCLIScenarios(t, []string{baseCommand, "describe"}, scenarios) +} + +func TestList(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --version flag", + WantError: "error parsing arguments: required flag --version not provided", + }, + { + Name: "validate missing --service-id flag", + Args: "--version 3", + WantError: "error reading service: no service ID found", + }, + { + Name: "validate ListObservabilityCustomDashboards API error", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListObservabilityCustomDashboardsFn: func(i *fastly.ListObservabilityCustomDashboardsInput) ([]*fastly.ObservabilityCustomDashboard, error) { + return nil, testutil.Err + }, + }, + Args: "--service-id 123 --version 3", + WantError: testutil.Err.Error(), + }, + { + Name: "validate ListObservabilityCustomDashboards API success", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListObservabilityCustomDashboardsFn: listObservabilityCustomDashboards, + }, + Args: "--service-id 123 --version 3", + WantOutput: "<...>", + }, + { + Name: "validate --verbose flag", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListObservabilityCustomDashboardsFn: listObservabilityCustomDashboards, + }, + Args: "--service-id 123 --version 3 --verbose", + WantOutput: "<...>", + }, + } + + testutil.RunCLIScenarios(t, []string{baseCommand, "list"}, scenarios) +} + +func TestUpdate(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --name flag", + Args: "--version 3", + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Name: "validate missing --version flag", + Args: "--name foobar", + WantError: "error parsing arguments: required flag --version not provided", + }, + { + Name: "validate missing --service-id flag", + Args: "--name foobar --version 3", + WantError: "error reading service: no service ID found", + }, + { + Name: "validate missing --autoclone flag with 'active' service", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + }, + Args: "--name foobar --service-id 123 --version 1", + WantError: "service version 1 is active", + }, + { + Name: "validate missing --autoclone flag with 'locked' service", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + }, + Args: "--name foobar --service-id 123 --version 2", + WantError: "service version 2 is locked", + }, + { + Name: "validate UpdateObservabilityCustomDashboard API error", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + UpdateObservabilityCustomDashboardFn: func(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return nil, testutil.Err + }, + }, + Args: "--name foobar --service-id 123 --version 3", + WantError: testutil.Err.Error(), + }, + { + Name: "validate UpdateObservabilityCustomDashboard API success with --new-name", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + UpdateObservabilityCustomDashboardFn: func(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return &fastly.ObservabilityCustomDashboard{ + Name: *i.NewName, + ServiceID: i.ServiceID, + ServiceVersion: i.ServiceVersion, + }, nil + }, + }, + Args: "--name foobar --new-name beepboop --service-id 123 --version 3", + WantOutput: "Updated <...> 'beepboop' (previously: 'foobar', service: 123, version: 3)", + }, + } + + testutil.RunCLIScenarios(t, []string{baseCommand, "update"}, scenarios) +} + +func getObservabilityCustomDashboard(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + t := testutil.Date + + return &fastly.ObservabilityCustomDashboard{ + ServiceID: i.ServiceID, + + CreatedAt: &t, + DeletedAt: &t, + UpdatedAt: &t, + }, nil +} + +func listObservabilityCustomDashboards(i *fastly.ListObservabilityCustomDashboardsInput) ([]*fastly.ObservabilityCustomDashboard, error) { + t := testutil.Date + vs := []*fastly.ObservabilityCustomDashboard{ + { + ServiceID: i.ServiceID, + + CreatedAt: &t, + DeletedAt: &t, + UpdatedAt: &t, + }, + { + ServiceID: i.ServiceID, + + CreatedAt: &t, + DeletedAt: &t, + UpdatedAt: &t, + }, + } + return vs, nil +} diff --git a/pkg/commands/dashboard/delete.go b/pkg/commands/dashboard/delete.go new file mode 100644 index 000000000..6362d8775 --- /dev/null +++ b/pkg/commands/dashboard/delete.go @@ -0,0 +1,40 @@ +package dashboard + +import ( + "io" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// NewDeleteCommand returns a usable command registered under the parent. +func NewDeleteCommand(parent argparser.Registerer, globals *global.Data) *DeleteCommand { + var c DeleteCommand + c.CmdClause = parent.Command("delete", "Delete a custom dashboard").Alias("remove") + c.Globals = globals + + // Optional flags + c.RegisterFlagBool(c.JSONFlag()) // --json + + return &c +} + +// DeleteCommand calls the Fastly API to delete an appropriate resource. +type DeleteCommand struct { + argparser.Base + argparser.JSONOutput +} + +// Exec invokes the application logic for the command. +func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error { + return nil +} + +// constructInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *DeleteCommand) constructInput(serviceID string, serviceVersion int) *fastly.DeleteObservabilityCustomDashboardInput { + var input fastly.DeleteObservabilityCustomDashboardInput + + return &input +} diff --git a/pkg/commands/dashboard/describe.go b/pkg/commands/dashboard/describe.go new file mode 100644 index 000000000..f5323b6e0 --- /dev/null +++ b/pkg/commands/dashboard/describe.go @@ -0,0 +1,46 @@ +package dashboard + +import ( + "io" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/argparser" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" +) + +// NewDescribeCommand returns a usable command registered under the parent. +func NewDescribeCommand(parent argparser.Registerer, globals *global.Data) *DescribeCommand { + var c DescribeCommand + c.CmdClause = parent.Command("describe", "Show detailed information about a custom dashboard").Alias("get") + c.Globals = globals + + // Required flags + + // Optional flags + c.RegisterFlagBool(c.JSONFlag()) + return &c +} + +// DescribeCommand calls the Fastly API to describe an appropriate resource. +type DescribeCommand struct { + argparser.Base + argparser.JSONOutput +} + +// Exec invokes the application logic for the command. +func (c *DescribeCommand) Exec(in io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + return nil +} + +// constructInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *DescribeCommand) constructInput() *fastly.GetObservabilityCustomDashboardInput { + var input fastly.GetObservabilityCustomDashboardInput + + return &input +} diff --git a/pkg/commands/dashboard/doc.go b/pkg/commands/dashboard/doc.go new file mode 100644 index 000000000..0c1516259 --- /dev/null +++ b/pkg/commands/dashboard/doc.go @@ -0,0 +1,2 @@ +// Package dashboard contains commands to <...>. +package dashboard diff --git a/pkg/commands/dashboard/list.go b/pkg/commands/dashboard/list.go new file mode 100644 index 000000000..fdedfd9ad --- /dev/null +++ b/pkg/commands/dashboard/list.go @@ -0,0 +1,48 @@ +package dashboard + +import ( + "errors" + "io" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/argparser" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// NewListCommand returns a usable command registered under the parent. +func NewListCommand(parent argparser.Registerer, globals *global.Data) *ListCommand { + var c ListCommand + c.CmdClause = parent.Command("list", "List custom dashboards") + c.Globals = globals + + // Optional Flags + c.RegisterFlagBool(c.JSONFlag()) // --json + + return &c +} + +// ListCommand calls the Fastly API to list appropriate resources. +type ListCommand struct { + argparser.Base + argparser.JSONOutput +} + +// Exec invokes the application logic for the command. +func (c *ListCommand) Exec(in io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + for { + return nil + } +} + +// constructInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *ListCommand) constructInput() (*fastly.ListObservabilityCustomDashboardsInput, error) { + var input fastly.ListObservabilityCustomDashboardsInput + + return &input, nil +} diff --git a/pkg/commands/dashboard/root.go b/pkg/commands/dashboard/root.go new file mode 100644 index 000000000..3571976d2 --- /dev/null +++ b/pkg/commands/dashboard/root.go @@ -0,0 +1,31 @@ +package dashboard + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// RootCommand is the parent command for all subcommands in this package. +// It should be installed under the primary root command. +type RootCommand struct { + argparser.Base + // no flags +} + +// CommandName is the string to be used to invoke this command +const CommandName = "dashboard" + +// NewRootCommand returns a new command registered in the parent. +func NewRootCommand(parent argparser.Registerer, globals *global.Data) *RootCommand { + var c RootCommand + c.Globals = globals + c.CmdClause = parent.Command(CommandName, "Manipulate Fastly Custom Dashboards") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/dashboard/update.go b/pkg/commands/dashboard/update.go new file mode 100644 index 000000000..46f131de4 --- /dev/null +++ b/pkg/commands/dashboard/update.go @@ -0,0 +1,43 @@ +package dashboard + +import ( + "io" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, globals *global.Data) *UpdateCommand { + var c UpdateCommand + c.CmdClause = parent.Command("update", "Update a custom dashboard") + c.Globals = globals + + // Required flags + + // Optional flags + c.RegisterFlagBool(c.JSONFlag()) // --json + + return &c +} + +// UpdateCommand calls the Fastly API to update an appropriate resource. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { + // text.Success(out, "Updated <...> '%s' (service: %s, version: %d)", r.<...>, r.ServiceID, r.ServiceVersion) + return nil +} + +// constructInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *UpdateCommand) constructInput() *fastly.UpdateObservabilityCustomDashboardInput { + var input fastly.UpdateObservabilityCustomDashboardInput + + return &input +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 03fb87759..2deeb8f0b 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -398,6 +398,12 @@ type API struct { DeleteAlertDefinitionFn func(i *fastly.DeleteAlertDefinitionInput) error TestAlertDefinitionFn func(i *fastly.TestAlertDefinitionInput) error ListAlertHistoryFn func(i *fastly.ListAlertHistoryInput) (*fastly.AlertHistoryResponse, error) + + ListObservabilityCustomDashboardsFn func(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ObservabilityCustomDashboard, error) + CreateObservabilityCustomDashboardFn func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) + GetObservabilityCustomDashboardFn func(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) + UpdateObservabilityCustomDashboardFn func(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) + DeleteObservabilityCustomDashboardFn func(i *fastly.DeleteObservabilityCustomDashboardInput) error } // AllDatacenters implements Interface. @@ -2024,3 +2030,28 @@ func (m API) TestAlertDefinition(i *fastly.TestAlertDefinitionInput) error { func (m API) ListAlertHistory(i *fastly.ListAlertHistoryInput) (*fastly.AlertHistoryResponse, error) { return m.ListAlertHistoryFn(i) } + +// CreateObservabilityCustomDashboard implements Interface. +func (m API) CreateObservabilityCustomDashboard(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return m.CreateObservabilityCustomDashboardFn(i) +} + +// DeleteObservabilityCustomDashboard implements Interface. +func (m API) DeleteObservabilityCustomDashboard(i *fastly.DeleteObservabilityCustomDashboardInput) error { + return m.DeleteObservabilityCustomDashboardFn(i) +} + +// GetObservabilityCustomDashboard implements Interface. +func (m API) GetObservabilityCustomDashboard(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return m.GetObservabilityCustomDashboardFn(i) +} + +// ListObservabilityCustomDashboards implements Interface. +func (m API) ListObservabilityCustomDashboards(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ObservabilityCustomDashboard, error) { + return m.ListObservabilityCustomDashboardsFn(i) +} + +// UpdateObservabilityCustomDashboard implements Interface. +func (m API) UpdateObservabilityCustomDashboard(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return m.UpdateObservabilityCustomDashboardFn(i) +} From 5c32287d419a82f94f14fdecdb38f754c68a5a57 Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Wed, 4 Dec 2024 12:57:20 -0600 Subject: [PATCH 02/18] Implement dashboard print funcs --- pkg/commands/dashboard/common.go | 87 ++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/pkg/commands/dashboard/common.go b/pkg/commands/dashboard/common.go index cfdd5f819..3cf35196f 100644 --- a/pkg/commands/dashboard/common.go +++ b/pkg/commands/dashboard/common.go @@ -1 +1,88 @@ package dashboard + +import ( + "fmt" + "io" + "strings" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/text" +) + +// printSummary displays the information returned from the API in a summarised +// format. +func printSummary(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { + t := text.NewTable(out) + t.AddHeader("DASHBOARD ID", "NAME", "DESCRIPTION", "# ITEMS") + for _, d := range ds { + t.AddLine( + d.ID, + d.Name, + d.Description, + len(d.Items), + ) + } + t.Print() +} + +// printVerbose displays the information returned from the API in a verbose +// format. +func printVerbose(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { + for _, d := range ds { + printDashboard(out, 0, d) + fmt.Fprintf(out, "\n") + } +} + +func printDashboard(out io.Writer, indent uint, dashboard *fastly.ObservabilityCustomDashboard) { + indentStep := uint(4) + level := indent + text.Indent(out, level, "Name: %s", dashboard.Name) + text.Indent(out, level, "Description: %s", dashboard.Description) + text.Indent(out, level, "Items:") + + level += indentStep + for i, di := range dashboard.Items { + text.Indent(out, level, "[%d]:", i) + level += indentStep + printItem(out, level, &di) + level -= indentStep + } + level -= indentStep + + text.Indent(out, level, "Meta:") + level += indentStep + text.Indent(out, level, "Created at: %s", dashboard.CreatedAt) + text.Indent(out, level, "Updated at: %s", dashboard.UpdatedAt) + text.Indent(out, level, "Created by: %s", dashboard.CreatedBy) + text.Indent(out, level, "Updated by: %s", dashboard.UpdatedBy) +} + +func printItem(out io.Writer, indent uint, item *fastly.DashboardItem) { + indentStep := uint(4) + level := indent + if item != nil { + text.Indent(out, level, "ID: %s", item.ID) + text.Indent(out, level, "Title: %s", item.Title) + text.Indent(out, level, "Subtitle: %s", item.Subtitle) + text.Indent(out, level, "Span: %d", item.Span) + + text.Indent(out, level, "Data Source:") + level += indentStep + text.Indent(out, level, "Type: %s", item.DataSource.Type) + text.Indent(out, level, "Metrics: %s", strings.Join(item.DataSource.Config.Metrics, ", ")) + level -= indentStep + + text.Indent(out, level, "Visualization:") + level += indentStep + text.Indent(out, level, "Type: %s", item.Visualization.Type) + text.Indent(out, level, "Plot Type: %s", item.Visualization.Config.PlotType) + if item.Visualization.Config.CalculationMethod != nil { + text.Indent(out, level, "Calculation Method: %s", *item.Visualization.Config.CalculationMethod) + } + if item.Visualization.Config.Format != nil { + text.Indent(out, level, "Format: %s", *item.Visualization.Config.Format) + } + } +} From 8d629fed054584d5d13e55c1448e1b7ba3e8231d Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Wed, 4 Dec 2024 12:57:41 -0600 Subject: [PATCH 03/18] Implement dashboard list command --- pkg/commands/dashboard/list.go | 73 ++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/pkg/commands/dashboard/list.go b/pkg/commands/dashboard/list.go index fdedfd9ad..2e3256d9e 100644 --- a/pkg/commands/dashboard/list.go +++ b/pkg/commands/dashboard/list.go @@ -20,6 +20,10 @@ func NewListCommand(parent argparser.Registerer, globals *global.Data) *ListComm // Optional Flags c.RegisterFlagBool(c.JSONFlag()) // --json + c.CmdClause.Flag("cursor", "Pagination cursor (Use 'next_cursor' value from list output)").Action(c.cursor.Set).StringVar(&c.cursor.Value) + c.CmdClause.Flag("limit", "Maximum number of items to list").Action(c.limit.Set).IntVar(&c.limit.Value) + c.CmdClause.Flag("order", "Sort by one of the following [asc, desc]").Action(c.order.Set).StringVar(&c.order.Value) + c.CmdClause.Flag("sort", "Sort by one of the following [name, created_at, updated_at]").Action(c.sort.Set).StringVar(&c.sort.Value) return &c } @@ -28,6 +32,11 @@ func NewListCommand(parent argparser.Registerer, globals *global.Data) *ListComm type ListCommand struct { argparser.Base argparser.JSONOutput + + cursor argparser.OptionalString + limit argparser.OptionalInt + sort argparser.OptionalString + order argparser.OptionalString } // Exec invokes the application logic for the command. @@ -35,7 +44,47 @@ func (c *ListCommand) Exec(in io.Reader, out io.Writer) error { if c.Globals.Verbose() && c.JSONOutput.Enabled { return fsterr.ErrInvalidVerboseJSONCombo } + + input, err := c.constructInput() + if err != nil { + return err + } + for { + dashboards, err := c.Globals.APIClient.ListObservabilityCustomDashboards(input) + if err != nil { + return err + } + + if ok, err := c.WriteJSON(out, dashboards); ok { + // No pagination prompt w/ JSON output. + return err + } + + dashboardsPtr := make([]*fastly.ObservabilityCustomDashboard, len(dashboards.Data)) + for i := range dashboards.Data { + dashboardsPtr[i] = &dashboards.Data[i] + } + + if c.Globals.Verbose() { + printVerbose(out, dashboardsPtr) + } else { + printSummary(out, dashboardsPtr) + } + + if dashboards != nil && dashboards.Meta.NextCursor != "" { + if !c.Globals.Flags.NonInteractive && !c.Globals.Flags.AutoYes && text.IsTTY(out) { + printNext, err := text.AskYesNo(out, "Print next page [y/N]: ", in) + if err != nil { + return err + } + if printNext { + input.Cursor = &dashboards.Meta.NextCursor + continue + } + } + } + return nil } } @@ -44,5 +93,29 @@ func (c *ListCommand) Exec(in io.Reader, out io.Writer) error { func (c *ListCommand) constructInput() (*fastly.ListObservabilityCustomDashboardsInput, error) { var input fastly.ListObservabilityCustomDashboardsInput + if c.cursor.WasSet { + input.Cursor = &c.cursor.Value + } + if c.limit.WasSet { + input.Limit = &c.limit.Value + } + var sign string + if c.order.WasSet { + switch c.order.Value { + case "asc": + case "desc": + sign = "-" + default: + err := errors.New("'order' flag must be one of the following [asc, desc]") + c.Globals.ErrLog.Add(err) + return nil, err + } + } + + if c.sort.WasSet { + str := sign + c.sort.Value + input.Sort = &str + } + return &input, nil } From 4132123da961b60a727231a18ae85c6fe308c8de Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Wed, 4 Dec 2024 12:57:54 -0600 Subject: [PATCH 04/18] Implement dashboard describe command --- pkg/commands/dashboard/describe.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/commands/dashboard/describe.go b/pkg/commands/dashboard/describe.go index f5323b6e0..cc05a2cf7 100644 --- a/pkg/commands/dashboard/describe.go +++ b/pkg/commands/dashboard/describe.go @@ -17,6 +17,7 @@ func NewDescribeCommand(parent argparser.Registerer, globals *global.Data) *Desc c.Globals = globals // Required flags + c.CmdClause.Flag("id", "Alphanumeric string identifying a Dashboard").Required().StringVar(&c.dashboardID) // Optional flags c.RegisterFlagBool(c.JSONFlag()) @@ -27,6 +28,8 @@ func NewDescribeCommand(parent argparser.Registerer, globals *global.Data) *Desc type DescribeCommand struct { argparser.Base argparser.JSONOutput + + dashboardID string } // Exec invokes the application logic for the command. @@ -35,6 +38,18 @@ func (c *DescribeCommand) Exec(in io.Reader, out io.Writer) error { return fsterr.ErrInvalidVerboseJSONCombo } + input := c.constructInput() + dashboard, err := c.Globals.APIClient.GetObservabilityCustomDashboard(input) + if err != nil { + return err + } + + if ok, err := c.WriteJSON(out, dashboard); ok { + return err + } + + dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} + printVerbose(out, dashboards) return nil } @@ -42,5 +57,7 @@ func (c *DescribeCommand) Exec(in io.Reader, out io.Writer) error { func (c *DescribeCommand) constructInput() *fastly.GetObservabilityCustomDashboardInput { var input fastly.GetObservabilityCustomDashboardInput + input.ID = &c.dashboardID + return &input } From 0cad7865f1e2ddbcfa32c3ae680a46b2476ab31d Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Wed, 4 Dec 2024 13:08:06 -0600 Subject: [PATCH 05/18] Implement dashboard delete command --- pkg/commands/dashboard/delete.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/commands/dashboard/delete.go b/pkg/commands/dashboard/delete.go index 6362d8775..7af10faae 100644 --- a/pkg/commands/dashboard/delete.go +++ b/pkg/commands/dashboard/delete.go @@ -7,6 +7,7 @@ import ( "github.com/fastly/cli/pkg/argparser" "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" ) // NewDeleteCommand returns a usable command registered under the parent. @@ -15,8 +16,8 @@ func NewDeleteCommand(parent argparser.Registerer, globals *global.Data) *Delete c.CmdClause = parent.Command("delete", "Delete a custom dashboard").Alias("remove") c.Globals = globals - // Optional flags - c.RegisterFlagBool(c.JSONFlag()) // --json + // Required flags + c.CmdClause.Flag("id", "Alphanumeric string identifying a Dashboard").Required().StringVar(&c.dashboardID) return &c } @@ -24,17 +25,27 @@ func NewDeleteCommand(parent argparser.Registerer, globals *global.Data) *Delete // DeleteCommand calls the Fastly API to delete an appropriate resource. type DeleteCommand struct { argparser.Base - argparser.JSONOutput + + dashboardID string } // Exec invokes the application logic for the command. func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error { + input := c.constructInput() + err := c.Globals.APIClient.DeleteObservabilityCustomDashboard(input) + if err != nil { + return err + } + + text.Success(out, "Deleted custom dashboard '%s'", fastly.ToValue(input.ID)) return nil } // constructInput transforms values parsed from CLI flags into an object to be used by the API client library. -func (c *DeleteCommand) constructInput(serviceID string, serviceVersion int) *fastly.DeleteObservabilityCustomDashboardInput { +func (c *DeleteCommand) constructInput() *fastly.DeleteObservabilityCustomDashboardInput { var input fastly.DeleteObservabilityCustomDashboardInput + input.ID = &c.dashboardID + return &input } From 47058194c6a81c6619b9dfaec46ce563d95aa756 Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Wed, 4 Dec 2024 16:15:48 -0600 Subject: [PATCH 06/18] Implement dashboard create command --- pkg/commands/dashboard/create.go | 36 +++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/pkg/commands/dashboard/create.go b/pkg/commands/dashboard/create.go index ee10ab6df..22a2ccc3f 100644 --- a/pkg/commands/dashboard/create.go +++ b/pkg/commands/dashboard/create.go @@ -6,7 +6,9 @@ import ( "github.com/fastly/go-fastly/v9/fastly" "github.com/fastly/cli/pkg/argparser" + fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" ) // NewCreateCommand returns a usable command registered under the parent. @@ -16,9 +18,11 @@ func NewCreateCommand(parent argparser.Registerer, globals *global.Data) *Create c.Globals = globals // Required flags + c.CmdClause.Flag("name", "A human-readable name for the dashboard").Short('n').Required().StringVar(&c.name) // --name // Optional flags - c.RegisterFlagBool(c.JSONFlag()) // --json + c.RegisterFlagBool(c.JSONFlag()) // --json + c.CmdClause.Flag("description", "A short description of the dashboard").Action(c.description.Set).StringVar(&c.description.Value) // --description return &c } @@ -27,17 +31,43 @@ func NewCreateCommand(parent argparser.Registerer, globals *global.Data) *Create type CreateCommand struct { argparser.Base argparser.JSONOutput + + name string + description argparser.OptionalString } // Exec invokes the application logic for the command. func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { - // text.Success(out, "Created <...> '%s' (service: %s, version: %d)", r.<...>, r.ServiceID, r.ServiceVersion) + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := c.constructInput() + dashboard, err := c.Globals.APIClient.CreateObservabilityCustomDashboard(input) + if err != nil { + return err + } + + text.Success(out, `Created Custom Dashboard "%s" (id: %s)`, dashboard.Name, dashboard.ID) + dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} + if c.Globals.Verbose() { + printVerbose(out, dashboards) + } else { + printSummary(out, dashboards) + } return nil } // constructInput transforms values parsed from CLI flags into an object to be used by the API client library. func (c *CreateCommand) constructInput() *fastly.CreateObservabilityCustomDashboardInput { - var input fastly.CreateObservabilityCustomDashboardInput + input := fastly.CreateObservabilityCustomDashboardInput{ + Name: c.name, + Items: []fastly.DashboardItem{}, + } + + if c.description.WasSet { + input.Description = &c.description.Value + } return &input } From a23239e69633c5b1a1a9f4072723731ce962ccb3 Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Wed, 4 Dec 2024 16:31:40 -0600 Subject: [PATCH 07/18] Implement dashboard update command --- pkg/commands/dashboard/update.go | 38 ++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/pkg/commands/dashboard/update.go b/pkg/commands/dashboard/update.go index 46f131de4..2a1a9816e 100644 --- a/pkg/commands/dashboard/update.go +++ b/pkg/commands/dashboard/update.go @@ -6,7 +6,9 @@ import ( "github.com/fastly/go-fastly/v9/fastly" "github.com/fastly/cli/pkg/argparser" + fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" ) // NewUpdateCommand returns a usable command registered under the parent. @@ -16,9 +18,12 @@ func NewUpdateCommand(parent argparser.Registerer, globals *global.Data) *Update c.Globals = globals // Required flags + c.CmdClause.Flag("id", "Alphanumeric string identifying a Dashboard").Required().StringVar(&c.dashboardID) // Optional flags - c.RegisterFlagBool(c.JSONFlag()) // --json + c.RegisterFlagBool(c.JSONFlag()) // --json + c.CmdClause.Flag("name", "A human-readable name for the dashboard").Short('n').Action(c.name.Set).StringVar(&c.name.Value) // --name + c.CmdClause.Flag("description", "A short description of the dashboard").Action(c.description.Set).StringVar(&c.description.Value) // --description return &c } @@ -27,11 +32,31 @@ func NewUpdateCommand(parent argparser.Registerer, globals *global.Data) *Update type UpdateCommand struct { argparser.Base argparser.JSONOutput + + dashboardID string + name argparser.OptionalString + description argparser.OptionalString } // Exec invokes the application logic for the command. func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { - // text.Success(out, "Updated <...> '%s' (service: %s, version: %d)", r.<...>, r.ServiceID, r.ServiceVersion) + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := c.constructInput() + dashboard, err := c.Globals.APIClient.UpdateObservabilityCustomDashboard(input) + if err != nil { + return err + } + + text.Success(out, `Updated Custom Dashboard "%s" (id: %s)`, dashboard.Name, dashboard.ID) + dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} + if c.Globals.Verbose() { + printVerbose(out, dashboards) + } else { + printSummary(out, dashboards) + } return nil } @@ -39,5 +64,14 @@ func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { func (c *UpdateCommand) constructInput() *fastly.UpdateObservabilityCustomDashboardInput { var input fastly.UpdateObservabilityCustomDashboardInput + input.ID = &c.dashboardID + + if c.name.WasSet { + input.Name = &c.name.Value + } + if c.description.WasSet { + input.Description = &c.description.Value + } + return &input } From 757db3248ec64b0aca1f242663665ffd49f0a529 Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Thu, 5 Dec 2024 11:22:25 -0600 Subject: [PATCH 08/18] Scaffold dashboard/item command package --- pkg/commands/commands.go | 3 ++ .../dashboard/{common.go => common/print.go} | 18 +++++------ pkg/commands/dashboard/create.go | 5 +-- pkg/commands/dashboard/describe.go | 3 +- pkg/commands/dashboard/item/root.go | 31 +++++++++++++++++++ pkg/commands/dashboard/list.go | 5 +-- pkg/commands/dashboard/update.go | 5 +-- pkg/mock/api.go | 24 ++++++++++++++ 8 files changed, 78 insertions(+), 16 deletions(-) rename pkg/commands/dashboard/{common.go => common/print.go} (82%) create mode 100644 pkg/commands/dashboard/item/root.go diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index bd9bfad19..701534ca2 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -14,6 +14,7 @@ import ( "github.com/fastly/cli/pkg/commands/configstore" "github.com/fastly/cli/pkg/commands/configstoreentry" "github.com/fastly/cli/pkg/commands/dashboard" + dashboardItem "github.com/fastly/cli/pkg/commands/dashboard/item" "github.com/fastly/cli/pkg/commands/dictionary" "github.com/fastly/cli/pkg/commands/dictionaryentry" "github.com/fastly/cli/pkg/commands/domain" @@ -161,6 +162,7 @@ func Define( dashboardDescribe := dashboard.NewDescribeCommand(dashboardCmdRoot.CmdClause, data) dashboardUpdate := dashboard.NewUpdateCommand(dashboardCmdRoot.CmdClause, data) dashboardDelete := dashboard.NewDeleteCommand(dashboardCmdRoot.CmdClause, data) + dashboardItemCmdRoot := dashboardItem.NewRootCommand(dashboardCmdRoot.CmdClause, data) dictionaryCmdRoot := dictionary.NewRootCommand(app, data) dictionaryCreate := dictionary.NewCreateCommand(dictionaryCmdRoot.CmdClause, data) dictionaryDelete := dictionary.NewDeleteCommand(dictionaryCmdRoot.CmdClause, data) @@ -548,6 +550,7 @@ func Define( dashboardDescribe, dashboardUpdate, dashboardDelete, + dashboardItemCmdRoot, dictionaryCmdRoot, dictionaryCreate, dictionaryDelete, diff --git a/pkg/commands/dashboard/common.go b/pkg/commands/dashboard/common/print.go similarity index 82% rename from pkg/commands/dashboard/common.go rename to pkg/commands/dashboard/common/print.go index 3cf35196f..90a96971f 100644 --- a/pkg/commands/dashboard/common.go +++ b/pkg/commands/dashboard/common/print.go @@ -1,4 +1,4 @@ -package dashboard +package common import ( "fmt" @@ -10,9 +10,9 @@ import ( "github.com/fastly/cli/pkg/text" ) -// printSummary displays the information returned from the API in a summarised +// PrintSummary displays the information returned from the API in a summarised // format. -func printSummary(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { +func PrintSummary(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { t := text.NewTable(out) t.AddHeader("DASHBOARD ID", "NAME", "DESCRIPTION", "# ITEMS") for _, d := range ds { @@ -26,16 +26,16 @@ func printSummary(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { t.Print() } -// printVerbose displays the information returned from the API in a verbose +// PrintVerbose displays the information returned from the API in a verbose // format. -func printVerbose(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { +func PrintVerbose(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { for _, d := range ds { - printDashboard(out, 0, d) + PrintDashboard(out, 0, d) fmt.Fprintf(out, "\n") } } -func printDashboard(out io.Writer, indent uint, dashboard *fastly.ObservabilityCustomDashboard) { +func PrintDashboard(out io.Writer, indent uint, dashboard *fastly.ObservabilityCustomDashboard) { indentStep := uint(4) level := indent text.Indent(out, level, "Name: %s", dashboard.Name) @@ -46,7 +46,7 @@ func printDashboard(out io.Writer, indent uint, dashboard *fastly.ObservabilityC for i, di := range dashboard.Items { text.Indent(out, level, "[%d]:", i) level += indentStep - printItem(out, level, &di) + PrintItem(out, level, &di) level -= indentStep } level -= indentStep @@ -59,7 +59,7 @@ func printDashboard(out io.Writer, indent uint, dashboard *fastly.ObservabilityC text.Indent(out, level, "Updated by: %s", dashboard.UpdatedBy) } -func printItem(out io.Writer, indent uint, item *fastly.DashboardItem) { +func PrintItem(out io.Writer, indent uint, item *fastly.DashboardItem) { indentStep := uint(4) level := indent if item != nil { diff --git a/pkg/commands/dashboard/create.go b/pkg/commands/dashboard/create.go index 22a2ccc3f..6483bdb57 100644 --- a/pkg/commands/dashboard/create.go +++ b/pkg/commands/dashboard/create.go @@ -6,6 +6,7 @@ import ( "github.com/fastly/go-fastly/v9/fastly" "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/dashboard/common" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" @@ -51,9 +52,9 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { text.Success(out, `Created Custom Dashboard "%s" (id: %s)`, dashboard.Name, dashboard.ID) dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} if c.Globals.Verbose() { - printVerbose(out, dashboards) + common.PrintVerbose(out, dashboards) } else { - printSummary(out, dashboards) + common.PrintSummary(out, dashboards) } return nil } diff --git a/pkg/commands/dashboard/describe.go b/pkg/commands/dashboard/describe.go index cc05a2cf7..bc6a4eb2a 100644 --- a/pkg/commands/dashboard/describe.go +++ b/pkg/commands/dashboard/describe.go @@ -6,6 +6,7 @@ import ( "github.com/fastly/go-fastly/v9/fastly" "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/dashboard/common" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" ) @@ -49,7 +50,7 @@ func (c *DescribeCommand) Exec(in io.Reader, out io.Writer) error { } dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} - printVerbose(out, dashboards) + common.PrintVerbose(out, dashboards) return nil } diff --git a/pkg/commands/dashboard/item/root.go b/pkg/commands/dashboard/item/root.go new file mode 100644 index 000000000..cee4da10c --- /dev/null +++ b/pkg/commands/dashboard/item/root.go @@ -0,0 +1,31 @@ +package item + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// RootCommand is the parent command for all subcommands in this package. +// It should be installed under the primary root command. +type RootCommand struct { + argparser.Base + // no flags +} + +// CommandName is the string to be used to invoke this command +const CommandName = "item" + +// NewRootCommand returns a new command registered in the parent. +func NewRootCommand(parent argparser.Registerer, globals *global.Data) *RootCommand { + var c RootCommand + c.Globals = globals + c.CmdClause = parent.Command(CommandName, "Manipulate Fastly Custom Dashboard Items") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/dashboard/list.go b/pkg/commands/dashboard/list.go index 2e3256d9e..da2b44fc8 100644 --- a/pkg/commands/dashboard/list.go +++ b/pkg/commands/dashboard/list.go @@ -7,6 +7,7 @@ import ( "github.com/fastly/go-fastly/v9/fastly" "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/dashboard/common" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" @@ -67,9 +68,9 @@ func (c *ListCommand) Exec(in io.Reader, out io.Writer) error { } if c.Globals.Verbose() { - printVerbose(out, dashboardsPtr) + common.PrintVerbose(out, dashboardsPtr) } else { - printSummary(out, dashboardsPtr) + common.PrintSummary(out, dashboardsPtr) } if dashboards != nil && dashboards.Meta.NextCursor != "" { diff --git a/pkg/commands/dashboard/update.go b/pkg/commands/dashboard/update.go index 2a1a9816e..f49c5e73d 100644 --- a/pkg/commands/dashboard/update.go +++ b/pkg/commands/dashboard/update.go @@ -6,6 +6,7 @@ import ( "github.com/fastly/go-fastly/v9/fastly" "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/dashboard/common" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" @@ -53,9 +54,9 @@ func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { text.Success(out, `Updated Custom Dashboard "%s" (id: %s)`, dashboard.Name, dashboard.ID) dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} if c.Globals.Verbose() { - printVerbose(out, dashboards) + common.PrintVerbose(out, dashboards) } else { - printSummary(out, dashboards) + common.PrintSummary(out, dashboards) } return nil } diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 2deeb8f0b..6b36760c5 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -404,6 +404,10 @@ type API struct { GetObservabilityCustomDashboardFn func(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) UpdateObservabilityCustomDashboardFn func(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) DeleteObservabilityCustomDashboardFn func(i *fastly.DeleteObservabilityCustomDashboardInput) error + CreateObservabilityCustomDashboardItemFn func(i *fastly.DashboardItem) (*fastly.ObservabilityCustomDashboard, error) + GetObservabilityCustomDashboardItemFn func(dashboardID, itemID string) (*fastly.ObservabilityCustomDashboard, error) + UpdateObservabilityCustomDashboardItemFn func(i *fastly.DashboardItem) (*fastly.ObservabilityCustomDashboard, error) + DeleteObservabilityCustomDashboardItemFn func(dashboardID, itemID string) (*fastly.ObservabilityCustomDashboard, error) } // AllDatacenters implements Interface. @@ -2055,3 +2059,23 @@ func (m API) ListObservabilityCustomDashboards(i *fastly.ListObservabilityCustom func (m API) UpdateObservabilityCustomDashboard(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { return m.UpdateObservabilityCustomDashboardFn(i) } + +// CreateObservabilityCustomDashboard implements Interface. +func (m API) CreateObservabilityCustomDashboardItem(i *fastly.DashboardItem) (*fastly.ObservabilityCustomDashboard, error) { + return m.CreateObservabilityCustomDashboardItemFn(i) +} + +// DeleteObservabilityCustomDashboard implements Interface. +func (m API) DeleteObservabilityCustomDashboardItem(dashboardID, itemID string) (*fastly.ObservabilityCustomDashboard, error) { + return m.DeleteObservabilityCustomDashboardItemFn(dashboardID, itemID) +} + +// GetObservabilityCustomDashboard implements Interface. +func (m API) GetObservabilityCustomDashboardItem(dashboardID, itemID string) (*fastly.ObservabilityCustomDashboard, error) { + return m.GetObservabilityCustomDashboardItemFn(dashboardID, itemID) +} + +// UpdateObservabilityCustomDashboard implements Interface. +func (m API) UpdateObservabilityCustomDashboardItem(i *fastly.DashboardItem) (*fastly.ObservabilityCustomDashboard, error) { + return m.UpdateObservabilityCustomDashboardItemFn(i) +} From a65ba93bac223da1d336fd5003dd0711a2575131 Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Thu, 5 Dec 2024 11:23:33 -0600 Subject: [PATCH 09/18] Implement dashboard item create command --- pkg/commands/commands.go | 2 + pkg/commands/dashboard/item/common.go | 9 ++ pkg/commands/dashboard/item/create.go | 122 ++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 pkg/commands/dashboard/item/common.go create mode 100644 pkg/commands/dashboard/item/create.go diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 701534ca2..8e4c574ba 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -163,6 +163,7 @@ func Define( dashboardUpdate := dashboard.NewUpdateCommand(dashboardCmdRoot.CmdClause, data) dashboardDelete := dashboard.NewDeleteCommand(dashboardCmdRoot.CmdClause, data) dashboardItemCmdRoot := dashboardItem.NewRootCommand(dashboardCmdRoot.CmdClause, data) + dashboardItemCreate := dashboardItem.NewCreateCommand(dashboardItemCmdRoot.CmdClause, data) dictionaryCmdRoot := dictionary.NewRootCommand(app, data) dictionaryCreate := dictionary.NewCreateCommand(dictionaryCmdRoot.CmdClause, data) dictionaryDelete := dictionary.NewDeleteCommand(dictionaryCmdRoot.CmdClause, data) @@ -551,6 +552,7 @@ func Define( dashboardUpdate, dashboardDelete, dashboardItemCmdRoot, + dashboardItemCreate, dictionaryCmdRoot, dictionaryCreate, dictionaryDelete, diff --git a/pkg/commands/dashboard/item/common.go b/pkg/commands/dashboard/item/common.go new file mode 100644 index 000000000..6089afd81 --- /dev/null +++ b/pkg/commands/dashboard/item/common.go @@ -0,0 +1,9 @@ +package item + +var ( + sourceTypes = []string{"stats.domain", "stats.edge", "stats.origin"} + visualizationTypes = []string{"chart"} + plotTypes = []string{"bar", "donut", "line", "single-metric"} + calculationMethods = []string{"avg", "sum", "min", "max", "latest", "p95"} + formats = []string{"number", "bytes", "percent", "requests", "responses", "seconds", "milliseconds", "ratio", "bitrate"} +) diff --git a/pkg/commands/dashboard/item/create.go b/pkg/commands/dashboard/item/create.go new file mode 100644 index 000000000..a7882984f --- /dev/null +++ b/pkg/commands/dashboard/item/create.go @@ -0,0 +1,122 @@ +package item + +import ( + "io" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/dashboard/common" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, globals *global.Data) *CreateCommand { + var c CreateCommand + c.CmdClause = parent.Command("create", "Create a custom dashboard item").Alias("add") + c.Globals = globals + + // Required flags + c.CmdClause.Flag("dashboard-id", "Alphanumeric string identifying a Dashboard").Required().StringVar(&c.dashboardID) + c.CmdClause.Flag("title", "A human-readable title for the dashboard item").Required().StringVar(&c.title) + c.CmdClause.Flag("subtitle", "A human-readable subtitle for the dashboard item. Often a description of the visualization").Required().StringVar(&c.subtitle) + c.CmdClause.Flag("source-type", "The source of the data to display").Required().HintOptions(sourceTypes...).EnumVar(&c.sourceType, sourceTypes...) + c.CmdClause.Flag("metrics", "The metrics to visualize. Valid options depend on the selected data source (set flag once per Metric)").Required().StringsVar(&c.metrics) + c.CmdClause.Flag("plot-type", "The type of chart to display").Required().HintOptions(plotTypes...).EnumVar(&c.plotType, plotTypes...) + + // Optional flags + c.RegisterFlagBool(c.JSONFlag()) // --json + c.CmdClause.Flag("visualization-type", `The type of visualization to display. Currently, only "chart" is supported`).Default("chart").HintOptions(visualizationTypes...).EnumVar(&c.vizType, visualizationTypes...) + c.CmdClause.Flag("calculation-method", "The aggregation function to apply to the dataset").Action(c.calculationMethod.Set).HintOptions(calculationMethods...).EnumVar(&c.calculationMethod.Value, calculationMethods...) // --calculation-method + c.CmdClause.Flag("format", "foo").Action(c.format.Set).HintOptions(formats...).EnumVar(&c.format.Value, formats...) // --format + c.CmdClause.Flag("span", `The number of columns for the dashboard item to span. Dashboards are rendered on a 12-column grid on "desktop" screen sizes`).Default("4").Uint8Var(&c.span) + + return &c +} + +// CreateCommand calls the Fastly API to create an appropriate resource. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // required + dashboardID string + title string + subtitle string + sourceType string + metrics []string + plotType string + + // optional + vizType string + calculationMethod argparser.OptionalString + format argparser.OptionalString + span uint8 +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + d, err := c.Globals.APIClient.GetObservabilityCustomDashboard(&fastly.GetObservabilityCustomDashboardInput{ID: &c.dashboardID}) + if err != nil { + return err + } + + input := c.constructInput(d) + d, err = c.Globals.APIClient.UpdateObservabilityCustomDashboard(input) + if err != nil { + return err + } + + if ok, err := c.WriteJSON(out, d); ok { + return err + } + + text.Success(out, `Added item to Custom Dashboard "%s" (id: %s)`, d.Name, d.ID) + dashboards := []*fastly.ObservabilityCustomDashboard{d} + // Summary isn't useful for a single dashboard, so print verbose by default + common.PrintVerbose(out, dashboards) + return nil +} + +// constructInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *CreateCommand) constructInput(d *fastly.ObservabilityCustomDashboard) *fastly.UpdateObservabilityCustomDashboardInput { + input := fastly.UpdateObservabilityCustomDashboardInput{ + ID: &d.ID, + Name: &d.Name, + Description: &d.Description, + Items: &d.Items, + } + item := fastly.DashboardItem{ + Title: c.title, + Subtitle: c.subtitle, + Span: c.span, + DataSource: fastly.DashboardDataSource{ + Type: fastly.DashboardSourceType(c.sourceType), + Config: fastly.DashboardSourceConfig{ + Metrics: c.metrics, + }, + }, + Visualization: fastly.DashboardVisualization{ + Type: fastly.VisualizationType(c.vizType), + Config: fastly.VisualizationConfig{ + PlotType: fastly.PlotType(c.plotType), + }, + }, + } + if c.calculationMethod.WasSet { + item.Visualization.Config.CalculationMethod = fastly.ToPointer(fastly.CalculationMethod(c.calculationMethod.Value)) + } + if c.format.WasSet { + item.Visualization.Config.Format = fastly.ToPointer(fastly.VisualizationFormat(c.format.Value)) + } + + *input.Items = append(*input.Items, item) + + return &input +} From fde77c00219e8842d3b1ad479c174435682b9abe Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Thu, 5 Dec 2024 11:46:58 -0600 Subject: [PATCH 10/18] Implement dashboard item describe command --- pkg/commands/commands.go | 2 + pkg/commands/dashboard/item/describe.go | 80 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 pkg/commands/dashboard/item/describe.go diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 8e4c574ba..2414781f4 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -164,6 +164,7 @@ func Define( dashboardDelete := dashboard.NewDeleteCommand(dashboardCmdRoot.CmdClause, data) dashboardItemCmdRoot := dashboardItem.NewRootCommand(dashboardCmdRoot.CmdClause, data) dashboardItemCreate := dashboardItem.NewCreateCommand(dashboardItemCmdRoot.CmdClause, data) + dashboardItemDescribe := dashboardItem.NewDescribeCommand(dashboardItemCmdRoot.CmdClause, data) dictionaryCmdRoot := dictionary.NewRootCommand(app, data) dictionaryCreate := dictionary.NewCreateCommand(dictionaryCmdRoot.CmdClause, data) dictionaryDelete := dictionary.NewDeleteCommand(dictionaryCmdRoot.CmdClause, data) @@ -553,6 +554,7 @@ func Define( dashboardDelete, dashboardItemCmdRoot, dashboardItemCreate, + dashboardItemDescribe, dictionaryCmdRoot, dictionaryCreate, dictionaryDelete, diff --git a/pkg/commands/dashboard/item/describe.go b/pkg/commands/dashboard/item/describe.go new file mode 100644 index 000000000..3b7ad6e43 --- /dev/null +++ b/pkg/commands/dashboard/item/describe.go @@ -0,0 +1,80 @@ +package item + +import ( + "fmt" + "io" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/dashboard/common" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" +) + +// NewDescribeCommand returns a usable command registered under the parent. +func NewDescribeCommand(parent argparser.Registerer, globals *global.Data) *DescribeCommand { + var c DescribeCommand + c.CmdClause = parent.Command("describe", "Describe a custom dashboard item").Alias("add") + c.Globals = globals + + // Required flags + c.CmdClause.Flag("dashboard-id", "Alphanumeric string identifying a Dashboard").Required().StringVar(&c.dashboardID) // --dashboard-id + c.CmdClause.Flag("item-id", "Alphanumeric string identifying a Dashboard Item").Required().StringVar(&c.itemID) // --item-id + + // Optional flags + c.RegisterFlagBool(c.JSONFlag()) // --json + + return &c +} + +// DescribeCommand calls the Fastly API to describe an appropriate resource. +type DescribeCommand struct { + argparser.Base + argparser.JSONOutput + + // required + dashboardID string + itemID string +} + +// Exec invokes the application logic for the command. +func (c *DescribeCommand) Exec(in io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := c.constructInput() + + d, err := c.Globals.APIClient.GetObservabilityCustomDashboard(input) + if err != nil { + return err + } + + di, err := getItemFromDashboard(d, c.itemID) + if err != nil { + return err + } + + if c.JSONOutput.Enabled { + c.WriteJSON(out, di) + } else { + common.PrintItem(out, 0, di) + } + + return nil +} + +// constructInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *DescribeCommand) constructInput() *fastly.GetObservabilityCustomDashboardInput { + return &fastly.GetObservabilityCustomDashboardInput{ID: &c.dashboardID} +} + +func getItemFromDashboard(d *fastly.ObservabilityCustomDashboard, itemID string) (*fastly.DashboardItem, error) { + for _, di := range d.Items { + if di.ID == itemID { + return &di, nil + } + } + return nil, fmt.Errorf("could not find item with ID (%s) in Dashboard (%s)", itemID, d.ID) +} From 12bbde2879f84f87ddf4e397c0a42fe070543fcb Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Thu, 5 Dec 2024 12:49:02 -0600 Subject: [PATCH 11/18] Implement dashboard item delete command --- pkg/commands/commands.go | 2 + pkg/commands/dashboard/item/delete.go | 107 ++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 pkg/commands/dashboard/item/delete.go diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 2414781f4..e8c0ce1aa 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -165,6 +165,7 @@ func Define( dashboardItemCmdRoot := dashboardItem.NewRootCommand(dashboardCmdRoot.CmdClause, data) dashboardItemCreate := dashboardItem.NewCreateCommand(dashboardItemCmdRoot.CmdClause, data) dashboardItemDescribe := dashboardItem.NewDescribeCommand(dashboardItemCmdRoot.CmdClause, data) + dashboardItemDelete := dashboardItem.NewDeleteCommand(dashboardItemCmdRoot.CmdClause, data) dictionaryCmdRoot := dictionary.NewRootCommand(app, data) dictionaryCreate := dictionary.NewCreateCommand(dictionaryCmdRoot.CmdClause, data) dictionaryDelete := dictionary.NewDeleteCommand(dictionaryCmdRoot.CmdClause, data) @@ -555,6 +556,7 @@ func Define( dashboardItemCmdRoot, dashboardItemCreate, dashboardItemDescribe, + dashboardItemDelete, dictionaryCmdRoot, dictionaryCreate, dictionaryDelete, diff --git a/pkg/commands/dashboard/item/delete.go b/pkg/commands/dashboard/item/delete.go new file mode 100644 index 000000000..1b6df644a --- /dev/null +++ b/pkg/commands/dashboard/item/delete.go @@ -0,0 +1,107 @@ +package item + +import ( + "io" + "slices" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/dashboard/common" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// NewDeleteCommand returns a usable command registered under the parent. +func NewDeleteCommand(parent argparser.Registerer, globals *global.Data) *DeleteCommand { + var c DeleteCommand + c.CmdClause = parent.Command("delete", "Delete a custom dashboard item").Alias("add") + c.Globals = globals + + // Required flags + c.CmdClause.Flag("dashboard-id", "Alphanumeric string identifying a Dashboard").Required().StringVar(&c.dashboardID) // --dashboard-id + c.CmdClause.Flag("item-id", "Alphanumeric string identifying a Dashboard Item").Required().StringVar(&c.itemID) // --item-id + + // Optional flags + c.RegisterFlagBool(c.JSONFlag()) // --json + + return &c +} + +// DeleteCommand calls the Fastly API to delete an appropriate resource. +type DeleteCommand struct { + argparser.Base + argparser.JSONOutput + + // required + dashboardID string + itemID string +} + +// Exec invokes the application logic for the command. +func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + d, err := c.Globals.APIClient.GetObservabilityCustomDashboard(&fastly.GetObservabilityCustomDashboardInput{ID: &c.dashboardID}) + if err != nil { + return err + } + + success := false + numItems := len(d.Items) + + if slices.ContainsFunc(d.Items, func(di fastly.DashboardItem) bool { + return di.ID == c.itemID + }) { + input := c.constructInput(d) + d, err = c.Globals.APIClient.UpdateObservabilityCustomDashboard(input) + if err != nil { + return err + } + + success = true + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"item_id"` + Deleted bool `json:"deleted"` + NewState *fastly.ObservabilityCustomDashboard `json:"dashboard_state"` + }{ + c.itemID, + success, + d, + } + _, err := c.WriteJSON(out, o) + return err + } + + if success { + text.Success(out, `Removed %d dashboard item(s) from Custom Dashboard "%s" (dashboardID: %s)`, (numItems - (len(d.Items))), d.Name, d.ID) + } else { + text.Warning(out, "dashboard (%s) has no item with ID (%s)", d.ID, c.itemID) + } + dashboards := []*fastly.ObservabilityCustomDashboard{d} + // Summary isn't useful for a single dashboard, so print verbose by default + common.PrintVerbose(out, dashboards) + return nil +} + +// constructInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *DeleteCommand) constructInput(d *fastly.ObservabilityCustomDashboard) *fastly.UpdateObservabilityCustomDashboardInput { + input := fastly.UpdateObservabilityCustomDashboardInput{ + ID: &d.ID, + Name: &d.Name, + Description: &d.Description, + } + + items := slices.DeleteFunc(d.Items, func(di fastly.DashboardItem) bool { + return di.ID == c.itemID + }) + input.Items = &items + + return &input +} From 3b6b8fc28932ccfb96a3d4ea4b3e811fca659efc Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Thu, 5 Dec 2024 14:46:36 -0600 Subject: [PATCH 12/18] Implement dashboard item update command --- pkg/commands/commands.go | 2 + pkg/commands/dashboard/item/update.go | 141 ++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 pkg/commands/dashboard/item/update.go diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index e8c0ce1aa..8c3ab83ec 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -165,6 +165,7 @@ func Define( dashboardItemCmdRoot := dashboardItem.NewRootCommand(dashboardCmdRoot.CmdClause, data) dashboardItemCreate := dashboardItem.NewCreateCommand(dashboardItemCmdRoot.CmdClause, data) dashboardItemDescribe := dashboardItem.NewDescribeCommand(dashboardItemCmdRoot.CmdClause, data) + dashboardItemUpdate := dashboardItem.NewUpdateCommand(dashboardItemCmdRoot.CmdClause, data) dashboardItemDelete := dashboardItem.NewDeleteCommand(dashboardItemCmdRoot.CmdClause, data) dictionaryCmdRoot := dictionary.NewRootCommand(app, data) dictionaryCreate := dictionary.NewCreateCommand(dictionaryCmdRoot.CmdClause, data) @@ -556,6 +557,7 @@ func Define( dashboardItemCmdRoot, dashboardItemCreate, dashboardItemDescribe, + dashboardItemUpdate, dashboardItemDelete, dictionaryCmdRoot, dictionaryCreate, diff --git a/pkg/commands/dashboard/item/update.go b/pkg/commands/dashboard/item/update.go new file mode 100644 index 000000000..e14097d14 --- /dev/null +++ b/pkg/commands/dashboard/item/update.go @@ -0,0 +1,141 @@ +package item + +import ( + "fmt" + "io" + "slices" + + "github.com/fastly/go-fastly/v9/fastly" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/dashboard/common" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" +) + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, globals *global.Data) *UpdateCommand { + var c UpdateCommand + c.CmdClause = parent.Command("update", "Update a custom dashboard item").Alias("add") + c.Globals = globals + + // Required flags + c.CmdClause.Flag("dashboard-id", "Alphanumeric string identifying a Dashboard").Required().StringVar(&c.dashboardID) // --dashboard-id + c.CmdClause.Flag("item-id", "Alphanumeric string identifying a Dashboard Item").Required().StringVar(&c.itemID) // --item-id + + // Optional flags + c.RegisterFlagBool(c.JSONFlag()) // --json + c.CmdClause.Flag("title", "A human-readable title for the dashboard item").Action(c.title.Set).StringVar(&c.title.Value) // --title + c.CmdClause.Flag("subtitle", "A human-readable subtitle for the dashboard item. Often a description of the visualization").Action(c.subtitle.Set).StringVar(&c.subtitle.Value) // --subtitle + c.CmdClause.Flag("span", `The number of columns for the dashboard item to span. Dashboards are rendered on a 12-column grid on "desktop" screen sizes`).Action(c.span.Set).IntVar(&c.span.Value) // --span + c.CmdClause.Flag("source-type", "The source of the data to display").Action(c.sourceType.Set).HintOptions(sourceTypes...).EnumVar(&c.sourceType.Value, sourceTypes...) // --source-type + c.CmdClause.Flag("metrics", "The metrics to visualize. Valid options depend on the selected data source (set flag once per Metric)").Action(c.metrics.Set).StringsVar(&c.metrics.Value) // --metrics + c.CmdClause.Flag("visualization-type", `The type of visualization to display. Currently, only "chart" is supported`).Action(c.vizType.Set).HintOptions(visualizationTypes...).EnumVar(&c.vizType.Value, visualizationTypes...) // --visualization-type + c.CmdClause.Flag("calculation-method", "The aggregation function to apply to the dataset").Action(c.calculationMethod.Set).HintOptions(calculationMethods...).EnumVar(&c.calculationMethod.Value, calculationMethods...) // --calculation-method + c.CmdClause.Flag("format", "The units to use to format the data").Action(c.format.Set).HintOptions(formats...).EnumVar(&c.format.Value, formats...) // --format + c.CmdClause.Flag("plot-type", "The type of chart to display").Action(c.plotType.Set).HintOptions(plotTypes...).EnumVar(&c.plotType.Value, plotTypes...) // --plot-type + + return &c +} + +// UpdateCommand calls the Fastly API to update an appropriate resource. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // required + dashboardID string + itemID string + + // optional + title argparser.OptionalString + subtitle argparser.OptionalString + span argparser.OptionalInt + sourceType argparser.OptionalString + metrics argparser.OptionalStringSlice + plotType argparser.OptionalString + vizType argparser.OptionalString + calculationMethod argparser.OptionalString + format argparser.OptionalString +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + d, err := c.Globals.APIClient.GetObservabilityCustomDashboard(&fastly.GetObservabilityCustomDashboardInput{ID: &c.dashboardID}) + if err != nil { + return err + } + + input, err := c.constructInput(d) + if err != nil { + return err + } + + d, err = c.Globals.APIClient.UpdateObservabilityCustomDashboard(input) + if err != nil { + return err + } + + if ok, err := c.WriteJSON(out, d); ok { + return err + } + + // text.Success(out, `Added %d items to Custom Dashboard "%s" (id: %s)`, len(*input.Items), d.Name, d.ID) + dashboards := []*fastly.ObservabilityCustomDashboard{d} + // Summary isn't useful for a single dashboard, so print verbose by default + common.PrintVerbose(out, dashboards) + return nil +} + +// constructInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *UpdateCommand) constructInput(d *fastly.ObservabilityCustomDashboard) (*fastly.UpdateObservabilityCustomDashboardInput, error) { + var input fastly.UpdateObservabilityCustomDashboardInput + + input.ID = &d.ID + input.Items = &d.Items + idx := slices.IndexFunc(*input.Items, func(di fastly.DashboardItem) bool { + return di.ID == c.itemID + }) + if idx < 0 { + return nil, fmt.Errorf("dashboard (%s) does not contain item with ID %s", d.ID, c.itemID) + } + item := &(*input.Items)[idx] + + if c.title.WasSet { + (*item).Title = c.title.Value + } + if c.subtitle.WasSet { + (*item).Subtitle = c.subtitle.Value + } + if c.span.WasSet { + if span := c.span.Value; span <= 255 && span >= 0 { + (*item).Span = uint8(span) + } else { + return nil, fmt.Errorf("invalid span value %d", span) + } + } + if c.sourceType.WasSet { + (*item).DataSource.Type = fastly.DashboardSourceType(c.sourceType.Value) + } + if c.metrics.WasSet { + (*item).DataSource.Config.Metrics = c.metrics.Value + } + if c.vizType.WasSet { + (*item).Visualization.Type = fastly.VisualizationType(c.vizType.Value) + } + if c.plotType.WasSet { + (*item).Visualization.Config.PlotType = fastly.PlotType(c.plotType.Value) + } + if c.calculationMethod.WasSet { + (*item).Visualization.Config.CalculationMethod = fastly.ToPointer(fastly.CalculationMethod(c.calculationMethod.Value)) + } + if c.format.WasSet { + (*item).Visualization.Config.Format = fastly.ToPointer(fastly.VisualizationFormat(c.format.Value)) + } + + return &input, nil +} From 8ea978d3af612bb654da94bd3a1822f4cf599166 Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Thu, 5 Dec 2024 14:57:49 -0600 Subject: [PATCH 13/18] Improve/fix JSON printing for dashboard commands --- pkg/commands/dashboard/create.go | 4 ++++ pkg/commands/dashboard/delete.go | 21 +++++++++++++++++++++ pkg/commands/dashboard/describe.go | 1 + pkg/commands/dashboard/update.go | 4 ++++ 4 files changed, 30 insertions(+) diff --git a/pkg/commands/dashboard/create.go b/pkg/commands/dashboard/create.go index 6483bdb57..f9a44f7be 100644 --- a/pkg/commands/dashboard/create.go +++ b/pkg/commands/dashboard/create.go @@ -49,6 +49,10 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { return err } + if ok, err := c.WriteJSON(out, dashboard); ok { + return err + } + text.Success(out, `Created Custom Dashboard "%s" (id: %s)`, dashboard.Name, dashboard.ID) dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} if c.Globals.Verbose() { diff --git a/pkg/commands/dashboard/delete.go b/pkg/commands/dashboard/delete.go index 7af10faae..78f00ec18 100644 --- a/pkg/commands/dashboard/delete.go +++ b/pkg/commands/dashboard/delete.go @@ -6,6 +6,7 @@ import ( "github.com/fastly/go-fastly/v9/fastly" "github.com/fastly/cli/pkg/argparser" + fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" ) @@ -19,24 +20,44 @@ func NewDeleteCommand(parent argparser.Registerer, globals *global.Data) *Delete // Required flags c.CmdClause.Flag("id", "Alphanumeric string identifying a Dashboard").Required().StringVar(&c.dashboardID) + // Optional. + c.RegisterFlagBool(c.JSONFlag()) // --json + return &c } // DeleteCommand calls the Fastly API to delete an appropriate resource. type DeleteCommand struct { argparser.Base + argparser.JSONOutput dashboardID string } // Exec invokes the application logic for the command. func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + input := c.constructInput() err := c.Globals.APIClient.DeleteObservabilityCustomDashboard(input) if err != nil { return err } + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"dashboard_id"` + Deleted bool `json:"deleted"` + }{ + c.dashboardID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + text.Success(out, "Deleted custom dashboard '%s'", fastly.ToValue(input.ID)) return nil } diff --git a/pkg/commands/dashboard/describe.go b/pkg/commands/dashboard/describe.go index bc6a4eb2a..999561be0 100644 --- a/pkg/commands/dashboard/describe.go +++ b/pkg/commands/dashboard/describe.go @@ -50,6 +50,7 @@ func (c *DescribeCommand) Exec(in io.Reader, out io.Writer) error { } dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} + // Summary isn't useful for a single dashboard, so print verbose by default common.PrintVerbose(out, dashboards) return nil } diff --git a/pkg/commands/dashboard/update.go b/pkg/commands/dashboard/update.go index f49c5e73d..d8ffae872 100644 --- a/pkg/commands/dashboard/update.go +++ b/pkg/commands/dashboard/update.go @@ -51,6 +51,10 @@ func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { return err } + if ok, err := c.WriteJSON(out, dashboard); ok { + return err + } + text.Success(out, `Updated Custom Dashboard "%s" (id: %s)`, dashboard.Name, dashboard.ID) dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} if c.Globals.Verbose() { From 1a0565f9120689ed1aa3a01041cf497e06ebcc11 Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Thu, 5 Dec 2024 18:27:59 -0600 Subject: [PATCH 14/18] Update dashboard print funcs/commands to behave consistently with other resources --- pkg/commands/dashboard/common/print.go | 6 +-- pkg/commands/dashboard/create.go | 7 ---- pkg/commands/dashboard/delete.go | 2 +- pkg/commands/dashboard/describe.go | 4 +- pkg/commands/dashboard/item/create.go | 3 +- pkg/commands/dashboard/item/delete.go | 4 +- pkg/commands/dashboard/item/update.go | 4 +- pkg/commands/dashboard/list.go | 57 +++++++++++++++++--------- pkg/commands/dashboard/update.go | 7 ---- 9 files changed, 46 insertions(+), 48 deletions(-) diff --git a/pkg/commands/dashboard/common/print.go b/pkg/commands/dashboard/common/print.go index 90a96971f..db07b9b3f 100644 --- a/pkg/commands/dashboard/common/print.go +++ b/pkg/commands/dashboard/common/print.go @@ -12,7 +12,7 @@ import ( // PrintSummary displays the information returned from the API in a summarised // format. -func PrintSummary(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { +func PrintSummary(out io.Writer, ds []fastly.ObservabilityCustomDashboard) { t := text.NewTable(out) t.AddHeader("DASHBOARD ID", "NAME", "DESCRIPTION", "# ITEMS") for _, d := range ds { @@ -28,9 +28,9 @@ func PrintSummary(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { // PrintVerbose displays the information returned from the API in a verbose // format. -func PrintVerbose(out io.Writer, ds []*fastly.ObservabilityCustomDashboard) { +func PrintVerbose(out io.Writer, ds []fastly.ObservabilityCustomDashboard) { for _, d := range ds { - PrintDashboard(out, 0, d) + PrintDashboard(out, 0, &d) fmt.Fprintf(out, "\n") } } diff --git a/pkg/commands/dashboard/create.go b/pkg/commands/dashboard/create.go index f9a44f7be..24d0f1424 100644 --- a/pkg/commands/dashboard/create.go +++ b/pkg/commands/dashboard/create.go @@ -6,7 +6,6 @@ import ( "github.com/fastly/go-fastly/v9/fastly" "github.com/fastly/cli/pkg/argparser" - "github.com/fastly/cli/pkg/commands/dashboard/common" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" @@ -54,12 +53,6 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { } text.Success(out, `Created Custom Dashboard "%s" (id: %s)`, dashboard.Name, dashboard.ID) - dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} - if c.Globals.Verbose() { - common.PrintVerbose(out, dashboards) - } else { - common.PrintSummary(out, dashboards) - } return nil } diff --git a/pkg/commands/dashboard/delete.go b/pkg/commands/dashboard/delete.go index 78f00ec18..f18efaeb7 100644 --- a/pkg/commands/dashboard/delete.go +++ b/pkg/commands/dashboard/delete.go @@ -58,7 +58,7 @@ func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error { return err } - text.Success(out, "Deleted custom dashboard '%s'", fastly.ToValue(input.ID)) + text.Success(out, `Deleted Custom Dashboard %s`, fastly.ToValue(input.ID)) return nil } diff --git a/pkg/commands/dashboard/describe.go b/pkg/commands/dashboard/describe.go index 999561be0..6a78074cc 100644 --- a/pkg/commands/dashboard/describe.go +++ b/pkg/commands/dashboard/describe.go @@ -49,9 +49,7 @@ func (c *DescribeCommand) Exec(in io.Reader, out io.Writer) error { return err } - dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} - // Summary isn't useful for a single dashboard, so print verbose by default - common.PrintVerbose(out, dashboards) + common.PrintDashboard(out, 0, dashboard) return nil } diff --git a/pkg/commands/dashboard/item/create.go b/pkg/commands/dashboard/item/create.go index a7882984f..6215e860d 100644 --- a/pkg/commands/dashboard/item/create.go +++ b/pkg/commands/dashboard/item/create.go @@ -78,9 +78,8 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { } text.Success(out, `Added item to Custom Dashboard "%s" (id: %s)`, d.Name, d.ID) - dashboards := []*fastly.ObservabilityCustomDashboard{d} // Summary isn't useful for a single dashboard, so print verbose by default - common.PrintVerbose(out, dashboards) + common.PrintDashboard(out, 0, d) return nil } diff --git a/pkg/commands/dashboard/item/delete.go b/pkg/commands/dashboard/item/delete.go index 1b6df644a..d0ea0c5a2 100644 --- a/pkg/commands/dashboard/item/delete.go +++ b/pkg/commands/dashboard/item/delete.go @@ -84,9 +84,7 @@ func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error { } else { text.Warning(out, "dashboard (%s) has no item with ID (%s)", d.ID, c.itemID) } - dashboards := []*fastly.ObservabilityCustomDashboard{d} - // Summary isn't useful for a single dashboard, so print verbose by default - common.PrintVerbose(out, dashboards) + common.PrintDashboard(out, 0, d) return nil } diff --git a/pkg/commands/dashboard/item/update.go b/pkg/commands/dashboard/item/update.go index e14097d14..df75ee493 100644 --- a/pkg/commands/dashboard/item/update.go +++ b/pkg/commands/dashboard/item/update.go @@ -85,9 +85,7 @@ func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { } // text.Success(out, `Added %d items to Custom Dashboard "%s" (id: %s)`, len(*input.Items), d.Name, d.ID) - dashboards := []*fastly.ObservabilityCustomDashboard{d} - // Summary isn't useful for a single dashboard, so print verbose by default - common.PrintVerbose(out, dashboards) + common.PrintDashboard(out, 0, d) return nil } diff --git a/pkg/commands/dashboard/list.go b/pkg/commands/dashboard/list.go index da2b44fc8..cfe966b91 100644 --- a/pkg/commands/dashboard/list.go +++ b/pkg/commands/dashboard/list.go @@ -51,36 +51,41 @@ func (c *ListCommand) Exec(in io.Reader, out io.Writer) error { return err } + var dashboards []fastly.ObservabilityCustomDashboard + loadAllPages := c.JSONOutput.Enabled || c.Globals.Flags.NonInteractive || c.Globals.Flags.AutoYes + for { - dashboards, err := c.Globals.APIClient.ListObservabilityCustomDashboards(input) + o, err := c.Globals.APIClient.ListObservabilityCustomDashboards(input) if err != nil { return err } - if ok, err := c.WriteJSON(out, dashboards); ok { - // No pagination prompt w/ JSON output. - return err - } + if o != nil { + dashboards = append(dashboards, o.Data...) - dashboardsPtr := make([]*fastly.ObservabilityCustomDashboard, len(dashboards.Data)) - for i := range dashboards.Data { - dashboardsPtr[i] = &dashboards.Data[i] - } + if loadAllPages { + if o.Meta.NextCursor != "" { + input.Cursor = &o.Meta.NextCursor + continue + } + break + } - if c.Globals.Verbose() { - common.PrintVerbose(out, dashboardsPtr) - } else { - common.PrintSummary(out, dashboardsPtr) - } + if c.Globals.Verbose() { + common.PrintVerbose(out, dashboards) + } else { + common.PrintSummary(out, dashboards) + } - if dashboards != nil && dashboards.Meta.NextCursor != "" { - if !c.Globals.Flags.NonInteractive && !c.Globals.Flags.AutoYes && text.IsTTY(out) { - printNext, err := text.AskYesNo(out, "Print next page [y/N]: ", in) + if o.Meta.NextCursor != "" && text.IsTTY(out) { + text.Break(out) + printNextPage, err := text.AskYesNo(out, "Print next page [y/N]: ", in) if err != nil { return err } - if printNext { - input.Cursor = &dashboards.Meta.NextCursor + if printNextPage { + dashboards = []fastly.ObservabilityCustomDashboard{} + input.Cursor = &o.Meta.NextCursor continue } } @@ -88,6 +93,20 @@ func (c *ListCommand) Exec(in io.Reader, out io.Writer) error { return nil } + + if ok, err := c.WriteJSON(out, dashboards); ok { + // No pagination prompt w/ JSON output. + return err + } else { + // Only print output here if we've not already printed JSON. + if c.Globals.Verbose() { + common.PrintVerbose(out, dashboards) + } else { + common.PrintSummary(out, dashboards) + } + } + + return nil } // constructInput transforms values parsed from CLI flags into an object to be used by the API client library. diff --git a/pkg/commands/dashboard/update.go b/pkg/commands/dashboard/update.go index d8ffae872..b6ad3b457 100644 --- a/pkg/commands/dashboard/update.go +++ b/pkg/commands/dashboard/update.go @@ -6,7 +6,6 @@ import ( "github.com/fastly/go-fastly/v9/fastly" "github.com/fastly/cli/pkg/argparser" - "github.com/fastly/cli/pkg/commands/dashboard/common" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" @@ -56,12 +55,6 @@ func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { } text.Success(out, `Updated Custom Dashboard "%s" (id: %s)`, dashboard.Name, dashboard.ID) - dashboards := []*fastly.ObservabilityCustomDashboard{dashboard} - if c.Globals.Verbose() { - common.PrintVerbose(out, dashboards) - } else { - common.PrintSummary(out, dashboards) - } return nil } From a6d19320218913fda853d48523792e7ac84ecba8 Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Thu, 5 Dec 2024 18:28:14 -0600 Subject: [PATCH 15/18] Add tests for dashboard commands --- pkg/commands/dashboard/dashboard_test.go | 255 ++++++++--------------- pkg/mock/api.go | 4 +- 2 files changed, 84 insertions(+), 175 deletions(-) diff --git a/pkg/commands/dashboard/dashboard_test.go b/pkg/commands/dashboard/dashboard_test.go index 1d3d54515..f0ec16554 100644 --- a/pkg/commands/dashboard/dashboard_test.go +++ b/pkg/commands/dashboard/dashboard_test.go @@ -5,325 +5,234 @@ import ( "github.com/fastly/go-fastly/v9/fastly" + root "github.com/fastly/cli/pkg/commands/dashboard" "github.com/fastly/cli/pkg/mock" "github.com/fastly/cli/pkg/testutil" ) const ( - baseCommand = "dashboard" + userID = "test-user" ) func TestCreate(t *testing.T) { scenarios := []testutil.CLIScenario{ { - Name: "validate missing --version flag", - WantError: "error parsing arguments: required flag --version not provided", - }, - { - Name: "validate missing --service-id flag", - Args: "--version 3", - WantError: "error reading service: no service ID found", - }, - { - Name: "validate missing --autoclone flag with 'active' service", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - }, - Args: "--service-id 123 --version 1", - WantError: "service version 1 is active", - }, - { - Name: "validate missing --autoclone flag with 'locked' service", + Name: "validate CreateObservabilityCustomDashboard API error", API: mock.API{ - ListVersionsFn: testutil.ListVersions, + CreateObservabilityCustomDashboardFn: func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + return nil, testutil.Err + }, }, - Args: "--service-id 123 --version 2", - WantError: "service version 2 is locked", + Args: "--name Testing", + WantError: testutil.Err.Error(), }, { - Name: "validate CreateObservabilityCustomDashboard API error", + Name: "validate missing --name flag", API: mock.API{ - ListVersionsFn: testutil.ListVersions, CreateObservabilityCustomDashboardFn: func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { return nil, testutil.Err }, }, - Args: "--service-id 123 --version 3", - WantError: testutil.Err.Error(), + Args: "", + WantError: "error parsing arguments: required flag --name not provided", }, { - Name: "validate CreateObservabilityCustomDashboard API success", + Name: "validate optional --description flag", API: mock.API{ - ListVersionsFn: testutil.ListVersions, CreateObservabilityCustomDashboardFn: func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { return &fastly.ObservabilityCustomDashboard{ - ServiceID: i.ServiceID, + ID: "beepboop", + Name: i.Name, }, nil }, }, - Args: "--service-id 123 --version 3", - WantOutput: "Created <...> '456' (service: 123)", + Args: "--name Testing", + WantOutput: `Created Custom Dashboard "Testing" (id: beepboop)`, }, { - Name: "validate --autoclone results in cloned service version", + Name: "validate CreateObservabilityCustomDashboard API success", API: mock.API{ - ListVersionsFn: testutil.ListVersions, - CloneVersionFn: testutil.CloneVersionResult(4), CreateObservabilityCustomDashboardFn: func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { - return &fastly.VCL{ - ServiceID: i.ServiceID, - ServiceVersion: i.ServiceVersion, + return &fastly.ObservabilityCustomDashboard{ + ID: "beepboop", + Name: i.Name, + Description: *i.Description, }, nil }, }, - Args: "--autoclone --service-id 123 --version 1", - WantOutput: "Created <...> 'foo' (service: 123, version: 4)", + Args: "--name Testing --description foo", + WantOutput: `Created Custom Dashboard "Testing" (id: beepboop)`, }, } - testutil.RunCLIScenarios(t, []string{baseCommand, "create"}, scenarios) + testutil.RunCLIScenarios(t, []string{root.CommandName, "create"}, scenarios) } func TestDelete(t *testing.T) { scenarios := []testutil.CLIScenario{ { - Name: "validate missing --version flag", - WantError: "error parsing arguments: required flag --version not provided", - }, - { - Name: "validate missing --service-id flag", - Args: "--version 1", - WantError: "error reading service: no service ID found", - }, - { - Name: "validate missing --autoclone flag with 'active' service", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - }, - Args: "--service-id 123 --version 1", - WantError: "service version 1 is active", - }, - { - Name: "validate missing --autoclone flag with 'locked' service", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - }, - Args: "--service-id 123 --version 2", - WantError: "service version 2 is locked", + Name: "validate missing --id flag", + WantError: "error parsing arguments: required flag --id not provided", }, { Name: "validate DeleteObservabilityCustomDashboard API error", API: mock.API{ - ListVersionsFn: testutil.ListVersions, DeleteObservabilityCustomDashboardFn: func(i *fastly.DeleteObservabilityCustomDashboardInput) error { return testutil.Err }, }, - Args: "--service-id 123 --version 3", + Args: "--id beepboop", WantError: testutil.Err.Error(), }, { Name: "validate DeleteObservabilityCustomDashboard API success", API: mock.API{ - ListVersionsFn: testutil.ListVersions, DeleteObservabilityCustomDashboardFn: func(i *fastly.DeleteObservabilityCustomDashboardInput) error { return nil }, }, - Args: "--service-id 123 --version 3", - WantOutput: "Deleted <...> '456' (service: 123)", - }, - { - Name: "validate --autoclone results in cloned service version", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - CloneVersionFn: testutil.CloneVersionResult(4), - DeleteObservabilityCustomDashboardFn: func(i *fastly.DeleteObservabilityCustomDashboardInput) error { - return nil - }, - }, - Args: "--autoclone --service-id 123 --version 1", - WantOutput: "Deleted <...> 'foo' (service: 123, version: 4)", + Args: "--id beepboop", + WantOutput: "Deleted Custom Dashboard beepboop", }, } - testutil.RunCLIScenarios(t, []string{baseCommand, "delete"}, scenarios) + testutil.RunCLIScenarios(t, []string{root.CommandName, "delete"}, scenarios) } func TestDescribe(t *testing.T) { scenarios := []testutil.CLIScenario{ { - Name: "validate missing --version flag", - WantError: "error parsing arguments: required flag --version not provided", - }, - { - Name: "validate missing --service-id flag", - Args: "--version 3", - WantError: "error reading service: no service ID found", + Name: "validate missing --id flag", + WantError: "error parsing arguments: required flag --id not provided", }, { Name: "validate GetObservabilityCustomDashboard API error", API: mock.API{ - ListVersionsFn: testutil.ListVersions, GetObservabilityCustomDashboardFn: func(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { return nil, testutil.Err }, }, - Args: "--service-id 123 --version 3", + Args: "--id beepboop", WantError: testutil.Err.Error(), }, { Name: "validate GetObservabilityCustomDashboard API success", API: mock.API{ - ListVersionsFn: testutil.ListVersions, GetObservabilityCustomDashboardFn: getObservabilityCustomDashboard, }, - Args: "--service-id 123 --version 3", - WantOutput: "<...>", + Args: "--id beepboop", + WantOutput: "Name: Testing\nDescription: This is a test dashboard\nItems:\nMeta:\n Created at: 2021-06-15 23:00:00 +0000 UTC\n Updated at: 2021-06-15 23:00:00 +0000 UTC\n Created by: test-user\n Updated by: test-user\n", }, } - testutil.RunCLIScenarios(t, []string{baseCommand, "describe"}, scenarios) + testutil.RunCLIScenarios(t, []string{root.CommandName, "describe"}, scenarios) } func TestList(t *testing.T) { scenarios := []testutil.CLIScenario{ - { - Name: "validate missing --version flag", - WantError: "error parsing arguments: required flag --version not provided", - }, - { - Name: "validate missing --service-id flag", - Args: "--version 3", - WantError: "error reading service: no service ID found", - }, { Name: "validate ListObservabilityCustomDashboards API error", API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ListObservabilityCustomDashboardsFn: func(i *fastly.ListObservabilityCustomDashboardsInput) ([]*fastly.ObservabilityCustomDashboard, error) { + ListObservabilityCustomDashboardsFn: func(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ListDashboardsResponse, error) { return nil, testutil.Err }, }, - Args: "--service-id 123 --version 3", WantError: testutil.Err.Error(), }, { Name: "validate ListObservabilityCustomDashboards API success", API: mock.API{ - ListVersionsFn: testutil.ListVersions, ListObservabilityCustomDashboardsFn: listObservabilityCustomDashboards, }, - Args: "--service-id 123 --version 3", - WantOutput: "<...>", + WantOutput: "DASHBOARD ID NAME DESCRIPTION # ITEMS\nbeepboop Testing 1 This is #1 0\nbleepblorp Testing 2 This is #2 0\n", }, { Name: "validate --verbose flag", API: mock.API{ - ListVersionsFn: testutil.ListVersions, ListObservabilityCustomDashboardsFn: listObservabilityCustomDashboards, }, - Args: "--service-id 123 --version 3 --verbose", - WantOutput: "<...>", + Args: "--verbose", + WantOutput: "Fastly API endpoint: https://api.fastly.com\nFastly API token provided via config file (profile: user)\n\nName: Testing 1\nDescription: This is #1\nItems:\nMeta:\n Created at: 2021-06-15 23:00:00 +0000 UTC\n Updated at: 2021-06-15 23:00:00 +0000 UTC\n Created by: test-user\n Updated by: test-user\n\nName: Testing 2\nDescription: This is #2\nItems:\nMeta:\n Created at: 2021-06-15 23:00:00 +0000 UTC\n Updated at: 2021-06-15 23:00:00 +0000 UTC\n Created by: test-user\n Updated by: test-user\n\n", }, } - testutil.RunCLIScenarios(t, []string{baseCommand, "list"}, scenarios) + testutil.RunCLIScenarios(t, []string{root.CommandName, "list"}, scenarios) } func TestUpdate(t *testing.T) { scenarios := []testutil.CLIScenario{ { - Name: "validate missing --name flag", - Args: "--version 3", - WantError: "error parsing arguments: required flag --name not provided", - }, - { - Name: "validate missing --version flag", - Args: "--name foobar", - WantError: "error parsing arguments: required flag --version not provided", - }, - { - Name: "validate missing --service-id flag", - Args: "--name foobar --version 3", - WantError: "error reading service: no service ID found", - }, - { - Name: "validate missing --autoclone flag with 'active' service", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - }, - Args: "--name foobar --service-id 123 --version 1", - WantError: "service version 1 is active", - }, - { - Name: "validate missing --autoclone flag with 'locked' service", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - }, - Args: "--name foobar --service-id 123 --version 2", - WantError: "service version 2 is locked", + Name: "validate missing --id flag", + WantError: "error parsing arguments: required flag --id not provided", }, { Name: "validate UpdateObservabilityCustomDashboard API error", API: mock.API{ - ListVersionsFn: testutil.ListVersions, UpdateObservabilityCustomDashboardFn: func(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { return nil, testutil.Err }, }, - Args: "--name foobar --service-id 123 --version 3", + Args: "--id beepboop", WantError: testutil.Err.Error(), }, { - Name: "validate UpdateObservabilityCustomDashboard API success with --new-name", + Name: "validate UpdateObservabilityCustomDashboard API success", API: mock.API{ - ListVersionsFn: testutil.ListVersions, UpdateObservabilityCustomDashboardFn: func(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { return &fastly.ObservabilityCustomDashboard{ - Name: *i.NewName, - ServiceID: i.ServiceID, - ServiceVersion: i.ServiceVersion, + ID: *i.ID, + Name: *i.Name, + Description: *i.Description, }, nil }, }, - Args: "--name foobar --new-name beepboop --service-id 123 --version 3", - WantOutput: "Updated <...> 'beepboop' (previously: 'foobar', service: 123, version: 3)", + Args: "--id beepboop --name Foo --description Bleepblorp", + WantOutput: "SUCCESS: Updated Custom Dashboard \"Foo\" (id: beepboop)\n", }, } - testutil.RunCLIScenarios(t, []string{baseCommand, "update"}, scenarios) + testutil.RunCLIScenarios(t, []string{root.CommandName, "update"}, scenarios) } func getObservabilityCustomDashboard(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { t := testutil.Date return &fastly.ObservabilityCustomDashboard{ - ServiceID: i.ServiceID, - - CreatedAt: &t, - DeletedAt: &t, - UpdatedAt: &t, + CreatedAt: t, + CreatedBy: userID, + Description: "This is a test dashboard", + ID: *i.ID, + Items: []fastly.DashboardItem{}, + Name: "Testing", + UpdatedAt: t, + UpdatedBy: userID, }, nil } -func listObservabilityCustomDashboards(i *fastly.ListObservabilityCustomDashboardsInput) ([]*fastly.ObservabilityCustomDashboard, error) { +func listObservabilityCustomDashboards(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ListDashboardsResponse, error) { t := testutil.Date - vs := []*fastly.ObservabilityCustomDashboard{ - { - ServiceID: i.ServiceID, - - CreatedAt: &t, - DeletedAt: &t, - UpdatedAt: &t, - }, - { - ServiceID: i.ServiceID, - - CreatedAt: &t, - DeletedAt: &t, - UpdatedAt: &t, - }, + vs := &fastly.ListDashboardsResponse{ + Data: []fastly.ObservabilityCustomDashboard{{ + CreatedAt: t, + CreatedBy: userID, + Description: "This is #1", + ID: "beepboop", + Items: []fastly.DashboardItem{}, + Name: "Testing 1", + UpdatedAt: t, + UpdatedBy: userID, + }, { + CreatedAt: t, + CreatedBy: userID, + Description: "This is #2", + ID: "bleepblorp", + Items: []fastly.DashboardItem{}, + Name: "Testing 2", + UpdatedAt: t, + UpdatedBy: userID, + }}, + Meta: fastly.DashboardMeta{}, } + return vs, nil } diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 6b36760c5..23e1c9c56 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -399,7 +399,7 @@ type API struct { TestAlertDefinitionFn func(i *fastly.TestAlertDefinitionInput) error ListAlertHistoryFn func(i *fastly.ListAlertHistoryInput) (*fastly.AlertHistoryResponse, error) - ListObservabilityCustomDashboardsFn func(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ObservabilityCustomDashboard, error) + ListObservabilityCustomDashboardsFn func(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ListDashboardsResponse, error) CreateObservabilityCustomDashboardFn func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) GetObservabilityCustomDashboardFn func(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) UpdateObservabilityCustomDashboardFn func(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) @@ -2051,7 +2051,7 @@ func (m API) GetObservabilityCustomDashboard(i *fastly.GetObservabilityCustomDas } // ListObservabilityCustomDashboards implements Interface. -func (m API) ListObservabilityCustomDashboards(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ObservabilityCustomDashboard, error) { +func (m API) ListObservabilityCustomDashboards(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ListDashboardsResponse, error) { return m.ListObservabilityCustomDashboardsFn(i) } From bcfa417fe11b8c9a67680aa91d82100b7f82e745 Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Fri, 6 Dec 2024 16:35:22 -0600 Subject: [PATCH 16/18] Fix app.Run test --- pkg/app/run_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/app/run_test.go b/pkg/app/run_test.go index e5c83d8cd..c9d3a53cc 100644 --- a/pkg/app/run_test.go +++ b/pkg/app/run_test.go @@ -68,6 +68,7 @@ compute config config-store config-store-entry +dashboard dictionary dictionary-entry domain From 38f1d9f8d49bca7d8e9bb5db55fc0d57af88e8fe Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Fri, 6 Dec 2024 16:35:50 -0600 Subject: [PATCH 17/18] Remove unnecessary mock fns --- pkg/mock/api.go | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 23e1c9c56..4633c4b95 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -399,15 +399,11 @@ type API struct { TestAlertDefinitionFn func(i *fastly.TestAlertDefinitionInput) error ListAlertHistoryFn func(i *fastly.ListAlertHistoryInput) (*fastly.AlertHistoryResponse, error) - ListObservabilityCustomDashboardsFn func(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ListDashboardsResponse, error) - CreateObservabilityCustomDashboardFn func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) - GetObservabilityCustomDashboardFn func(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) - UpdateObservabilityCustomDashboardFn func(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) - DeleteObservabilityCustomDashboardFn func(i *fastly.DeleteObservabilityCustomDashboardInput) error - CreateObservabilityCustomDashboardItemFn func(i *fastly.DashboardItem) (*fastly.ObservabilityCustomDashboard, error) - GetObservabilityCustomDashboardItemFn func(dashboardID, itemID string) (*fastly.ObservabilityCustomDashboard, error) - UpdateObservabilityCustomDashboardItemFn func(i *fastly.DashboardItem) (*fastly.ObservabilityCustomDashboard, error) - DeleteObservabilityCustomDashboardItemFn func(dashboardID, itemID string) (*fastly.ObservabilityCustomDashboard, error) + ListObservabilityCustomDashboardsFn func(i *fastly.ListObservabilityCustomDashboardsInput) (*fastly.ListDashboardsResponse, error) + CreateObservabilityCustomDashboardFn func(i *fastly.CreateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) + GetObservabilityCustomDashboardFn func(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) + UpdateObservabilityCustomDashboardFn func(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) + DeleteObservabilityCustomDashboardFn func(i *fastly.DeleteObservabilityCustomDashboardInput) error } // AllDatacenters implements Interface. @@ -2059,23 +2055,3 @@ func (m API) ListObservabilityCustomDashboards(i *fastly.ListObservabilityCustom func (m API) UpdateObservabilityCustomDashboard(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { return m.UpdateObservabilityCustomDashboardFn(i) } - -// CreateObservabilityCustomDashboard implements Interface. -func (m API) CreateObservabilityCustomDashboardItem(i *fastly.DashboardItem) (*fastly.ObservabilityCustomDashboard, error) { - return m.CreateObservabilityCustomDashboardItemFn(i) -} - -// DeleteObservabilityCustomDashboard implements Interface. -func (m API) DeleteObservabilityCustomDashboardItem(dashboardID, itemID string) (*fastly.ObservabilityCustomDashboard, error) { - return m.DeleteObservabilityCustomDashboardItemFn(dashboardID, itemID) -} - -// GetObservabilityCustomDashboard implements Interface. -func (m API) GetObservabilityCustomDashboardItem(dashboardID, itemID string) (*fastly.ObservabilityCustomDashboard, error) { - return m.GetObservabilityCustomDashboardItemFn(dashboardID, itemID) -} - -// UpdateObservabilityCustomDashboard implements Interface. -func (m API) UpdateObservabilityCustomDashboardItem(i *fastly.DashboardItem) (*fastly.ObservabilityCustomDashboard, error) { - return m.UpdateObservabilityCustomDashboardItemFn(i) -} From dccf6447b1fd2a58bb316cbae539aeb583d0432c Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Fri, 6 Dec 2024 16:36:13 -0600 Subject: [PATCH 18/18] Add tests for dashboard item commands --- pkg/commands/dashboard/item/item_test.go | 308 +++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 pkg/commands/dashboard/item/item_test.go diff --git a/pkg/commands/dashboard/item/item_test.go b/pkg/commands/dashboard/item/item_test.go new file mode 100644 index 000000000..a233fdc97 --- /dev/null +++ b/pkg/commands/dashboard/item/item_test.go @@ -0,0 +1,308 @@ +package item_test + +import ( + "fmt" + "testing" + + "github.com/fastly/go-fastly/v9/fastly" + + root "github.com/fastly/cli/pkg/commands/dashboard" + sub "github.com/fastly/cli/pkg/commands/dashboard/item" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +var ( + testDate = testutil.Date + userID = "test-user" + dashboardID = "beepboop" + itemID = "bleepblorp" + dashboardName = "Foo" + dashboardDescription = "Testing..." + title = "Title" + subtitle = "Subtitle" + sourceType = "stats.edge" + metrics = "requests" + plotType = "line" + vizType = "chart" + calculationMethod = "latest" + format = "requests" + span = 8 + defaultItem = fastly.DashboardItem{ + DataSource: fastly.DashboardDataSource{ + Config: fastly.DashboardSourceConfig{ + Metrics: []string{metrics}, + }, + Type: fastly.DashboardSourceType(sourceType), + }, + ID: itemID, + Span: uint8(span), + Subtitle: subtitle, + Title: title, + Visualization: fastly.DashboardVisualization{ + Config: fastly.VisualizationConfig{ + CalculationMethod: fastly.ToPointer(fastly.CalculationMethod(calculationMethod)), + Format: fastly.ToPointer(fastly.VisualizationFormat(format)), + PlotType: fastly.PlotType(plotType), + }, + Type: fastly.VisualizationType(vizType), + }, + } + defaultDashboard = func() fastly.ObservabilityCustomDashboard { + return fastly.ObservabilityCustomDashboard{ + CreatedAt: testDate, + CreatedBy: userID, + Description: dashboardDescription, + ID: dashboardID, + Items: []fastly.DashboardItem{defaultItem}, + Name: dashboardName, + UpdatedAt: testDate, + UpdatedBy: userID, + } + } +) + +func TestCreate(t *testing.T) { + allRequiredFlags := fmt.Sprintf("--dashboard-id %s --title %s --subtitle %s --source-type %s --metrics %s --plot-type %s", dashboardID, title, subtitle, sourceType, metrics, plotType) + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --dashboard-id flag", + Args: fmt.Sprintf("--title %s --subtitle %s --source-type %s --metrics %s --plot-type %s", title, subtitle, sourceType, metrics, plotType), + WantError: "error parsing arguments: required flag --dashboard-id not provided", + }, + { + Name: "validate missing --title flag", + Args: fmt.Sprintf("--dashboard-id %s --subtitle %s --source-type %s --metrics %s --plot-type %s", dashboardID, subtitle, sourceType, metrics, plotType), + WantError: "error parsing arguments: required flag --title not provided", + }, + { + Name: "validate missing --subtitle flag", + Args: fmt.Sprintf("--dashboard-id %s --title %s --source-type %s --metrics %s --plot-type %s", dashboardID, title, sourceType, metrics, plotType), + WantError: "error parsing arguments: required flag --subtitle not provided", + }, + { + Name: "validate missing --source-type flag", + Args: fmt.Sprintf("--dashboard-id %s --title %s --subtitle %s --metrics %s --plot-type %s", dashboardID, title, subtitle, metrics, plotType), + WantError: "error parsing arguments: required flag --source-type not provided", + }, + { + Name: "validate missing --metrics flag", + Args: fmt.Sprintf("--dashboard-id %s --title %s --subtitle %s --source-type %s --plot-type %s", dashboardID, title, subtitle, sourceType, plotType), + WantError: "error parsing arguments: required flag --metrics not provided", + }, + { + Name: "validate missing --plot-type flag", + Args: fmt.Sprintf("--dashboard-id %s --title %s --subtitle %s --source-type %s --metrics %s", dashboardID, title, subtitle, sourceType, metrics), + WantError: "error parsing arguments: required flag --plot-type not provided", + }, + { + Name: "validate multiple --metrics flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --metrics %s", allRequiredFlags, "responses"), + WantOutput: "Metrics: requests, responses", + }, + { + Name: "validate all required flags", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: allRequiredFlags, + WantOutput: `Added item to Custom Dashboard "Foo"`, + }, + { + Name: "validate all optional flags", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --visualization-type %s --calculation-method %s --format %s --span %d", allRequiredFlags, vizType, calculationMethod, format, span), + WantOutput: `Added item to Custom Dashboard "Foo"`, + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestDelete(t *testing.T) { + allRequiredFlags := fmt.Sprintf("--dashboard-id %s --item-id %s", dashboardID, itemID) + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --dashboard-id flag", + Args: fmt.Sprintf("--item-id %s", itemID), + WantError: "error parsing arguments: required flag --dashboard-id not provided", + }, + { + Name: "validate missing --item-id flag", + Args: fmt.Sprintf("--dashboard-id %s", dashboardID), + WantError: "error parsing arguments: required flag --item-id not provided", + }, + { + Name: "validate all required flags", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardEmpty, + }, + Args: allRequiredFlags, + WantOutput: `Removed 1 dashboard item(s) from Custom Dashboard "Foo"`, + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "delete"}, scenarios) +} + +func TestDescribe(t *testing.T) { + allRequiredFlags := fmt.Sprintf("--dashboard-id %s --item-id %s", dashboardID, itemID) + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --dashboard-id flag", + Args: fmt.Sprintf("--item-id %s", itemID), + WantError: "error parsing arguments: required flag --dashboard-id not provided", + }, + { + Name: "validate missing --item-id flag", + Args: fmt.Sprintf("--dashboard-id %s", dashboardID), + WantError: "error parsing arguments: required flag --item-id not provided", + }, + { + Name: "validate all required flags", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardEmpty, + }, + Args: allRequiredFlags, + WantOutput: "ID: bleepblorp\nTitle: Title\nSubtitle: Subtitle\nSpan: 8\nData Source:\n Type: stats.edge\n Metrics: requests\nVisualization:\n Type: chart\n Plot Type: line\n Calculation Method: latest\n Format: requests\n", + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "describe"}, scenarios) +} + +func TestUpdate(t *testing.T) { + allRequiredFlags := fmt.Sprintf("--dashboard-id %s --item-id %s", dashboardID, itemID) + + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --dashboard-id flag", + Args: fmt.Sprintf("--item-id %s", itemID), + WantError: "error parsing arguments: required flag --dashboard-id not provided", + }, + { + Name: "validate missing --item-id flag", + Args: fmt.Sprintf("--dashboard-id %s", dashboardID), + WantError: "error parsing arguments: required flag --item-id not provided", + }, + { + Name: "validate all required flags", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: allRequiredFlags, + WantOutput: "Name: Foo\nDescription: Testing...\nItems:\n [0]:\n ID: bleepblorp\n Title: Title\n Subtitle: Subtitle\n Span: 8\n Data Source:\n Type: stats.edge\n Metrics: requests\n Visualization:\n Type: chart\n Plot Type: line\n Calculation Method: latest\n Format: requests\nMeta:\n Created at: 2021-06-15 23:00:00 +0000 UTC\n Updated at: 2021-06-15 23:00:00 +0000 UTC\n Created by: test-user\n Updated by: test-user\n", + }, + { + Name: "validate optional --title flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --title %s", allRequiredFlags, "NewTitle"), + WantOutput: "Title: NewTitle", + }, + { + Name: "validate optional --subtitle flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --subtitle %s", allRequiredFlags, "NewSubtitle"), + WantOutput: "Subtitle: NewSubtitle", + }, + { + Name: "validate optional --span flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --span %d", allRequiredFlags, 12), + WantOutput: "Span: 12", + }, + { + Name: "validate optional --source-type flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --source-type %s", allRequiredFlags, "stats.domain"), + WantOutput: "Type: stats.domain", + }, + { + Name: "validate optional --metrics flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --metrics %s", allRequiredFlags, "status_4xx"), + WantOutput: "Metrics: status_4xx", + }, + { + Name: "validate multiple --metrics flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --metrics %s --metrics %s --metrics %s", allRequiredFlags, "status_2xx", "status_4xx", "status_5xx"), + WantOutput: "Metrics: status_2xx, status_4xx, status_5xx", + }, + { + Name: "validate optional --calculation-method flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --calculation-method %s", allRequiredFlags, "avg"), + WantOutput: "Calculation Method: avg", + }, + { + Name: "validate optional --format flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --format %s", allRequiredFlags, "ratio"), + WantOutput: "Format: ratio", + }, + { + Name: "validate optional --plot-type flag", + API: mock.API{ + GetObservabilityCustomDashboardFn: getDashboardOK, + UpdateObservabilityCustomDashboardFn: updateDashboardOK, + }, + Args: fmt.Sprintf("%s --plot-type %s", allRequiredFlags, "single-metric"), + WantOutput: "Plot Type: single-metric", + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} + +func getDashboardOK(i *fastly.GetObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + d := defaultDashboard() + return &d, nil +} + +func updateDashboardOK(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + d := defaultDashboard() + d.Items = *i.Items + return &d, nil +} + +func updateDashboardEmpty(i *fastly.UpdateObservabilityCustomDashboardInput) (*fastly.ObservabilityCustomDashboard, error) { + d := defaultDashboard() + d.Items = []fastly.DashboardItem{} + return &d, nil +}