From 1198a776c013bc4fdfa27cf8dc81875658a4697f Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Tue, 2 Apr 2024 20:28:42 -0700 Subject: [PATCH 1/6] add accessToken/baseUri to flagModel --- internal/quickstart/container.go | 2 +- internal/quickstart/create_flag.go | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index 8726ff86..ab5acd39 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -29,7 +29,7 @@ func NewContainerModel(flagsClient flags.Client, accessToken string, baseUri str return ContainerModel{ accessToken: accessToken, baseUri: baseUri, - currentModel: NewCreateFlagModel(flagsClient), + currentModel: NewCreateFlagModel(flagsClient, accessToken, baseUri), flagsClient: flagsClient, } } diff --git a/internal/quickstart/create_flag.go b/internal/quickstart/create_flag.go index 6f71211e..50e472b8 100644 --- a/internal/quickstart/create_flag.go +++ b/internal/quickstart/create_flag.go @@ -2,10 +2,6 @@ package quickstart import ( "fmt" - "ldcli/cmd/cliflags" - - "github.com/spf13/viper" - "ldcli/internal/flags" "github.com/charmbracelet/bubbles/key" @@ -17,11 +13,13 @@ import ( const defaultFlagName = "my new flag" type createFlagModel struct { - client flags.Client - textInput textinput.Model + accessToken string + baseUri string + client flags.Client + textInput textinput.Model } -func NewCreateFlagModel(client flags.Client) tea.Model { +func NewCreateFlagModel(client flags.Client, accessToken, baseUri string) tea.Model { ti := textinput.New() ti.Focus() ti.CharLimit = 156 @@ -29,8 +27,10 @@ func NewCreateFlagModel(client flags.Client) tea.Model { ti.Placeholder = defaultFlagName return createFlagModel{ - client: client, - textInput: ti, + accessToken: accessToken, + baseUri: baseUri, + client: client, + textInput: ti, } } @@ -53,10 +53,7 @@ func (m createFlagModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, sendErr(err) } - accessToken := viper.GetString(cliflags.AccessTokenFlag) - baseUri := viper.GetString(cliflags.BaseURIFlag) - - return m, sendCreateFlagMsg(m.client, accessToken, baseUri, input, flagKey, "default") + return m, sendCreateFlagMsg(m.client, m.accessToken, m.baseUri, input, flagKey, "default") case key.Matches(msg, keys.Quit): return m, tea.Quit default: From 08c92c6ede1e181df1fa79838e07cafff80a271a Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Tue, 2 Apr 2024 20:51:35 -0700 Subject: [PATCH 2/6] refactor BuilToggleFlagPatch to flags client --- cmd/flags/update.go | 10 ++-------- internal/flags/client.go | 5 +++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cmd/flags/update.go b/cmd/flags/update.go index e789ccdf..21775144 100644 --- a/cmd/flags/update.go +++ b/cmd/flags/update.go @@ -123,10 +123,8 @@ func runUpdate(client flags.Client) func(*cobra.Command, []string) error { var patch []flags.UpdateInput if cmd.CalledAs() == "toggle-on" || cmd.CalledAs() == "toggle-off" { _ = viper.BindPFlag(cliflags.EnvironmentFlag, cmd.Flags().Lookup(cliflags.EnvironmentFlag)) - err := json.Unmarshal([]byte(buildPatch(viper.GetString(cliflags.EnvironmentFlag), cmd.CalledAs() == "toggle-on")), &patch) - if err != nil { - return err - } + envKey := viper.GetString(cliflags.EnvironmentFlag) + patch = flags.BuildToggleFlagPatch(envKey, cmd.CalledAs() == "toggle-on") } else { err := json.Unmarshal([]byte(viper.GetString(cliflags.DataFlag)), &patch) if err != nil { @@ -151,7 +149,3 @@ func runUpdate(client flags.Client) func(*cobra.Command, []string) error { return nil } } - -func buildPatch(envKey string, toggleValue bool) string { - return fmt.Sprintf(`[{"op": "replace", "path": "/environments/%s/on", "value": %t}]`, envKey, toggleValue) -} diff --git a/internal/flags/client.go b/internal/flags/client.go index d3de7de7..2a46ab18 100644 --- a/internal/flags/client.go +++ b/internal/flags/client.go @@ -3,6 +3,7 @@ package flags import ( "context" "encoding/json" + "fmt" ldapi "github.com/launchdarkly/api-client-go/v14" @@ -92,3 +93,7 @@ func (c FlagsClient) Update( return responseJSON, nil } + +func BuildToggleFlagPatch(envKey string, enabled bool) []UpdateInput { + return []UpdateInput{{Op: "replace", Path: fmt.Sprintf("/environments/%s/on", envKey), Value: enabled}} +} From a920be7ea863948dc11626679327dc9547ddd618 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Tue, 2 Apr 2024 20:53:02 -0700 Subject: [PATCH 3/6] add tests for toggle flag command --- cmd/flags/update_test.go | 80 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/cmd/flags/update_test.go b/cmd/flags/update_test.go index 9508fe79..1c30e29f 100644 --- a/cmd/flags/update_test.go +++ b/cmd/flags/update_test.go @@ -89,3 +89,83 @@ func TestUpdate(t *testing.T) { assert.EqualError(t, err, "base-uri is invalid"+errorHelp) }) } + +func TestToggle(t *testing.T) { + errorHelp := ". See `ldcli flags toggle-on --help` for supported flags and usage." + mockArgs := []interface{}{ + "testAccessToken", + "http://test.com", + "test-proj-key", + "test-flag-key", + []flags.UpdateInput{ + { + Op: "replace", + Path: "/environments/test-env-key/on", + Value: true, + }, + }, + } + t.Run("with valid flags calls projects API", func(t *testing.T) { + client := flags.MockClient{} + client. + On("Update", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + args := []string{ + "flags", "toggle-on", + "--access-token", "testAccessToken", + "--base-uri", "http://test.com", + "--flag", "test-flag-key", + "--project", "test-proj-key", + "--environment", "test-env-key", + } + + output, err := cmd.CallCmd(t, nil, &client, nil, nil, args) + + require.NoError(t, err) + assert.JSONEq(t, `{"valid": true}`, string(output)) + }) + + t.Run("with an error response is an error", func(t *testing.T) { + client := flags.MockClient{} + client. + On("Update", mockArgs...). + Return([]byte(`{}`), errors.NewError("An error")) + args := []string{ + "flags", "toggle-on", + "--access-token", "testAccessToken", + "--base-uri", "http://test.com", + "--flag", "test-flag-key", + "--project", "test-proj-key", + "--environment", "test-env-key", + } + + _, err := cmd.CallCmd(t, nil, &client, nil, nil, args) + + require.EqualError(t, err, "An error") + }) + + t.Run("with missing required flags is an error", func(t *testing.T) { + args := []string{ + "flags", "toggle-on", + } + + _, err := cmd.CallCmd(t, nil, &flags.MockClient{}, nil, nil, args) + + assert.EqualError(t, err, `required flag(s) "access-token", "environment", "flag", "project" not set`+errorHelp) + }) + + t.Run("with invalid base-uri is an error", func(t *testing.T) { + args := []string{ + "flags", "toggle-on", + "--access-token", "testAccessToken", + "--base-uri", "invalid", + "--flag", "test-flag-key", + "--project", "test-proj-key", + "--environment", "test-env-key", + } + + _, err := cmd.CallCmd(t, nil, &flags.MockClient{}, nil, nil, args) + + assert.EqualError(t, err, "base-uri is invalid"+errorHelp) + }) +} From 58b1958ad96f0375312463b037e789c43d4d5e87 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Tue, 2 Apr 2024 21:10:43 -0700 Subject: [PATCH 4/6] send patch when toggling flag --- internal/quickstart/container.go | 10 ++++--- internal/quickstart/messages.go | 21 +++++++++++++ internal/quickstart/toggle_flag.go | 47 +++++++++++++++++++++++++----- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index ab5acd39..51134d56 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -23,6 +23,7 @@ type ContainerModel struct { accessToken string baseUri string currentModel tea.Model + sdkKind string } func NewContainerModel(flagsClient flags.Client, accessToken string, baseUri string) tea.Model { @@ -53,18 +54,19 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case choseSDKMsg: m.currentModel = NewShowSDKInstructionsModel(m.accessToken, m.baseUri, msg.canonicalName, msg.url, m.flagKey) cmd = m.currentModel.Init() + m.sdkKind = msg.sdkKind case createdFlagMsg: m.currentModel = NewChooseSDKModel() m.flagKey = msg.flagKey // TODO: figure out if we maintain state here or pass in another message case errMsg: m.err = msg.err case noInstructionsMsg: - // TODO: set currentModel to toggle flag model - // m.currentModel = NewToggleFlagModel(m.flagsClient, m.flagKey) - case fetchedSDKInstructions, fetchedEnv: + // skip the ShowSDKInstructionsModel and move along to toggling the flag + m.currentModel = NewToggleFlagModel(m.flagsClient, m.accessToken, m.baseUri, m.flagKey, m.sdkKind) + case fetchedSDKInstructions, fetchedEnv, toggledFlagMsg: m.currentModel, cmd = m.currentModel.Update(msg) case showToggleFlagMsg: - m.currentModel = NewToggleFlagModel(m.flagKey) + m.currentModel = NewToggleFlagModel(m.flagsClient, m.accessToken, m.baseUri, m.flagKey, m.sdkKind) default: log.Println("container default - bad", msg) } diff --git a/internal/quickstart/messages.go b/internal/quickstart/messages.go index c911ab56..50b99243 100644 --- a/internal/quickstart/messages.go +++ b/internal/quickstart/messages.go @@ -24,6 +24,25 @@ func sendErr(err error) tea.Cmd { } } +type toggledFlagMsg struct{} + +func sendToggleFlagMsg(client flags.Client, accessToken, baseUri, flagKey string, enabled bool) tea.Cmd { + return func() tea.Msg { + _, err := client.Update( + context.Background(), + accessToken, + baseUri, + flagKey, + "default", + flags.BuildToggleFlagPatch("test", enabled), + ) + if err != nil { + return sendErr(err) + } + return toggledFlagMsg{} + } +} + type createdFlagMsg struct { flagKey string } @@ -75,6 +94,7 @@ type fetchedSDKInstructions struct { type choseSDKMsg struct { canonicalName string displayName string + sdkKind string url string } @@ -88,6 +108,7 @@ func sendChoseSDKMsg(sdk sdkDetail) tea.Cmd { canonicalName: sdk.canonicalName, displayName: sdk.displayName, url: sdk.url, + sdkKind: sdk.kind, } } } diff --git a/internal/quickstart/toggle_flag.go b/internal/quickstart/toggle_flag.go index 39caaeb5..8e1a558e 100644 --- a/internal/quickstart/toggle_flag.go +++ b/internal/quickstart/toggle_flag.go @@ -1,18 +1,30 @@ package quickstart import ( + "fmt" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "ldcli/internal/flags" ) type toggleFlagModel struct { - flagKey string + accessToken string + baseUri string + client flags.Client + enabled bool // whether the flag has ever been toggled on + on bool // whether the flag is currently on/off + flagKey string + sdkKind string } -func NewToggleFlagModel(flagKey string) tea.Model { +func NewToggleFlagModel(client flags.Client, accessToken string, baseUri string, flagKey string, sdkKind string) tea.Model { return toggleFlagModel{ - flagKey: flagKey, + accessToken: accessToken, + baseUri: baseUri, + client: client, + flagKey: flagKey, + sdkKind: sdkKind, } } @@ -25,22 +37,43 @@ func (m toggleFlagModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { + case key.Matches(msg, keys.Quit): + return m, tea.Quit case key.Matches(msg, keys.Tab): - // TODO: toggle flag + m.enabled = true + m.on = !m.on + return m, sendToggleFlagMsg(m.client, m.accessToken, m.baseUri, m.flagKey, m.on) } } return m, cmd } +var logTypeMap = map[string]string{ + serverSideSDK: "application logs", + clientSideSDK: "browser", +} + func (m toggleFlagModel) View() string { + var furtherInstructions string title := "Toggle your feature flag (press tab)" toggle := "OFF" + bgColor := "#646a73" + margin := 1 + if m.on { + bgColor = "#3d9c51" + margin = 2 + toggle = "ON" + } + + if m.enabled { + furtherInstructions = fmt.Sprintf("\n\nCheck your %s to see the change!\n\n(press ctrl + c to quit)", logTypeMap[m.sdkKind]) + } toggleStyle := lipgloss.NewStyle(). - Background(lipgloss.Color("#646a73")). + Background(lipgloss.Color(bgColor)). Padding(0, 1). - MarginRight(1) + MarginRight(margin) - return title + "\n\n" + toggleStyle.Render(toggle) + m.flagKey + return title + "\n\n" + toggleStyle.Render(toggle) + m.flagKey + furtherInstructions } From 6fbd79b27cd843d63e1c795f13c188d78c02a0be Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Tue, 2 Apr 2024 21:16:28 -0700 Subject: [PATCH 5/6] use consts for default proj/env keys --- internal/quickstart/container.go | 5 +++++ internal/quickstart/create_flag.go | 2 +- internal/quickstart/messages.go | 4 ++-- internal/quickstart/show_sdk_instructions.go | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index 51134d56..d0042151 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -11,6 +11,11 @@ import ( "ldcli/internal/flags" ) +const ( + defaultProjKey = "default" + defaultEnvKey = "test" +) + // ContainerModel is a high level container model that controls the nested models wher each // represents a step in the quick-start flow. type ContainerModel struct { diff --git a/internal/quickstart/create_flag.go b/internal/quickstart/create_flag.go index 50e472b8..db34269f 100644 --- a/internal/quickstart/create_flag.go +++ b/internal/quickstart/create_flag.go @@ -53,7 +53,7 @@ func (m createFlagModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, sendErr(err) } - return m, sendCreateFlagMsg(m.client, m.accessToken, m.baseUri, input, flagKey, "default") + return m, sendCreateFlagMsg(m.client, m.accessToken, m.baseUri, input, flagKey, defaultProjKey) case key.Matches(msg, keys.Quit): return m, tea.Quit default: diff --git a/internal/quickstart/messages.go b/internal/quickstart/messages.go index 50b99243..7c6c6425 100644 --- a/internal/quickstart/messages.go +++ b/internal/quickstart/messages.go @@ -33,8 +33,8 @@ func sendToggleFlagMsg(client flags.Client, accessToken, baseUri, flagKey string accessToken, baseUri, flagKey, - "default", - flags.BuildToggleFlagPatch("test", enabled), + defaultProjKey, + flags.BuildToggleFlagPatch(defaultEnvKey, enabled), ) if err != nil { return sendErr(err) diff --git a/internal/quickstart/show_sdk_instructions.go b/internal/quickstart/show_sdk_instructions.go index 116977a5..4cd0afe7 100644 --- a/internal/quickstart/show_sdk_instructions.go +++ b/internal/quickstart/show_sdk_instructions.go @@ -42,7 +42,7 @@ func NewShowSDKInstructionsModel( func (m showSDKInstructionsModel) Init() tea.Cmd { return tea.Sequence( sendFetchSDKInstructionsMsg(m.url), - sendFetchEnv(m.accessToken, m.baseUri, "test", "default"), + sendFetchEnv(m.accessToken, m.baseUri, defaultEnvKey, defaultProjKey), ) } From b1d2fd592ea41139798d2d31c05e4dff5bf9c4e8 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Wed, 3 Apr 2024 09:46:19 -0700 Subject: [PATCH 6/6] pr feedback --- internal/quickstart/toggle_flag.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/quickstart/toggle_flag.go b/internal/quickstart/toggle_flag.go index 8e1a558e..ac1e5cee 100644 --- a/internal/quickstart/toggle_flag.go +++ b/internal/quickstart/toggle_flag.go @@ -9,13 +9,13 @@ import ( ) type toggleFlagModel struct { - accessToken string - baseUri string - client flags.Client - enabled bool // whether the flag has ever been toggled on - on bool // whether the flag is currently on/off - flagKey string - sdkKind string + accessToken string + baseUri string + client flags.Client + enabled bool + flagKey string + flagWasEnabled bool + sdkKind string } func NewToggleFlagModel(client flags.Client, accessToken string, baseUri string, flagKey string, sdkKind string) tea.Model { @@ -40,9 +40,9 @@ func (m toggleFlagModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, keys.Quit): return m, tea.Quit case key.Matches(msg, keys.Tab): - m.enabled = true - m.on = !m.on - return m, sendToggleFlagMsg(m.client, m.accessToken, m.baseUri, m.flagKey, m.on) + m.flagWasEnabled = true + m.enabled = !m.enabled + return m, sendToggleFlagMsg(m.client, m.accessToken, m.baseUri, m.flagKey, m.enabled) } } @@ -60,13 +60,13 @@ func (m toggleFlagModel) View() string { toggle := "OFF" bgColor := "#646a73" margin := 1 - if m.on { + if m.enabled { bgColor = "#3d9c51" margin = 2 toggle = "ON" } - if m.enabled { + if m.flagWasEnabled { furtherInstructions = fmt.Sprintf("\n\nCheck your %s to see the change!\n\n(press ctrl + c to quit)", logTypeMap[m.sdkKind]) }