diff --git a/internal/setup/flags.go b/internal/setup/flags.go index 76b79940..2f90ad05 100644 --- a/internal/setup/flags.go +++ b/internal/setup/flags.go @@ -44,7 +44,7 @@ func NewFlag() tea.Model { } l := list.New(flagsToItems(flags), flagDelegate{}, 30, 14) - l.Title = "Select an flag" + l.Title = "Select a flag" l.SetShowStatusBar(false) l.SetFilteringEnabled(false) diff --git a/internal/setup/sdk_build_instructions/js.md b/internal/setup/sdk_build_instructions/js.md new file mode 100644 index 00000000..5b7eb4a5 --- /dev/null +++ b/internal/setup/sdk_build_instructions/js.md @@ -0,0 +1,11 @@ +## Build Instructions +1. Edit `index.html` and set the value of `clientSideID` to your LaunchDarkly client-side ID. If there is an existing boolean feature flag in your LaunchDarkly project that you want to evaluate, set `flagKey` to the flag key. + +``` +const clientSideID = '1234567890abcdef'; +const flagKey = 'my-flag-key'; +``` + +2. Open `index.html` in your browser. + +You should receive the message "Feature flag key '' is for this user". \ No newline at end of file diff --git a/internal/setup/sdk_build_instructions/python.md b/internal/setup/sdk_build_instructions/python.md new file mode 100644 index 00000000..45b263ed --- /dev/null +++ b/internal/setup/sdk_build_instructions/python.md @@ -0,0 +1,15 @@ +## Build instructions + +1. Install the LaunchDarkly Python SDK by running `pip install -r requirements.txt` +2. On the command line, set the value of the environment variable `LAUNCHDARKLY_SERVER_KEY` to your LaunchDarkly SDK key. + ```bash + export LAUNCHDARKLY_SERVER_KEY="1234567890abcdef" + ``` +3. On the command line, set the value of the environment variable `LAUNCHDARKLY_FLAG_KEY` to an existing boolean feature flag in your LaunchDarkly project that you want to evaluate. + + ```bash + export LAUNCHDARKLY_FLAG_KEY="my-flag-key" + ``` +4. Run `python test.py`. + +You should receive the message `"Feature flag 'my-flag-key' is for this user"`. diff --git a/internal/setup/sdks.go b/internal/setup/sdks.go new file mode 100644 index 00000000..142b1772 --- /dev/null +++ b/internal/setup/sdks.go @@ -0,0 +1,118 @@ +package setup + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + sdkStyle = lipgloss.NewStyle().PaddingLeft(4) + selectedSdkItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) + + _ list.Item = sdk{} +) + +type sdk struct { + Name string `json:"name"` + InstructionsFileName string `json:"instructionFile"` +} + +func (s sdk) FilterValue() string { return "" } + +type sdkModel struct { + choice sdk + instructions string + err error + list list.Model +} + +const sdkInstructionsFilePath = "internal/setup/sdk_build_instructions/" + +func NewSdk() tea.Model { + sdks := []sdk{ + { + Name: "JavaScript", + InstructionsFileName: sdkInstructionsFilePath + "js.md", + }, + { + Name: "Python", + InstructionsFileName: sdkInstructionsFilePath + "python.md", + }, + } + + l := list.New(sdksToItems(sdks), sdkDelegate{}, 30, 14) + l.Title = "Select your SDK." + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + + return sdkModel{ + list: l, + } +} + +func (p sdkModel) Init() tea.Cmd { + return nil +} + +// This method has drifted from the ProjectModel's version, but it should do something similar. +func (m sdkModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.Enter): + i, ok := m.list.SelectedItem().(sdk) + if ok { + m.choice = i + } + case key.Matches(msg, keys.Quit): + return m, tea.Quit + default: + m.list, cmd = m.list.Update(msg) + } + } + + return m, cmd +} + +func (m sdkModel) View() string { + return "\n" + m.list.View() +} + +type sdkDelegate struct{} + +func (d sdkDelegate) Height() int { return 1 } +func (d sdkDelegate) Spacing() int { return 0 } +func (d sdkDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d sdkDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(sdk) + if !ok { + return + } + + str := fmt.Sprintf("%d. %s", index+1, i.Name) + + fn := sdkStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return selectedSdkItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} + +func sdksToItems(sdks []sdk) []list.Item { + items := make([]list.Item, len(sdks)) + for i, proj := range sdks { + items[i] = list.Item(proj) + } + + return items +} diff --git a/internal/setup/wizard.go b/internal/setup/wizard.go index 5b6a772a..ad289207 100644 --- a/internal/setup/wizard.go +++ b/internal/setup/wizard.go @@ -2,9 +2,12 @@ package setup import ( "fmt" + "os" + "strings" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" + "github.com/muesli/reflow/wordwrap" ) // TODO: we may want to rename this for clarity @@ -16,6 +19,7 @@ const ( projectsStep environmentsStep flagsStep + sdksStep ) // WizardModel is a high level container model that controls the nested models which each @@ -29,6 +33,8 @@ type WizardModel struct { currProjectKey string currEnvironmentKey string currFlagKey string + currSdk sdk + width int } func NewWizardModel() tea.Model { @@ -40,6 +46,7 @@ func NewWizardModel() tea.Model { NewProject(), NewEnvironment(), NewFlag(), + NewSdk(), } return WizardModel{ @@ -56,6 +63,8 @@ func (m WizardModel) Init() tea.Cmd { // the user is on. func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width case tea.KeyMsg: switch { case key.Matches(msg, keys.Enter): @@ -110,10 +119,22 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.currFlagKey = f.choice m.currStep += 1 } + case sdksStep: + model, _ := m.steps[sdksStep].Update(msg) + f, ok := model.(sdkModel) + if ok { + m.currSdk = f.choice + m.currStep += 1 + } // add additional cases for additional steps default: } case key.Matches(msg, keys.Back): + // if we've opted to use recommended resources but want to go back from the SDK step, + // make sure we go back to the right step + if m.useRecommendedResources && m.currStep == sdksStep { + m.currStep = autoCreateStep + } // only go back if not on the first step if m.currStep > autoCreateStep { m.currStep -= 1 @@ -139,8 +160,23 @@ func (m WizardModel) View() string { return fmt.Sprintf("ERROR: %s", m.err) } - if m.currStep > flagsStep { - return fmt.Sprintf("envKey is %s, projKey is %s, flagKey is %s", m.currEnvironmentKey, m.currProjectKey, m.currFlagKey) + if m.currStep > sdksStep { + // consider moving this to its own view (in a new model?) + content, err := os.ReadFile(m.currSdk.InstructionsFileName) + if err != nil { + fmt.Println("could not load file:", err) + os.Exit(1) + } + sdkInstructions := strings.ReplaceAll(string(content), "my-flag-key", m.currFlagKey) + return wordwrap.String(fmt.Sprintf( + "Selected project: %s\nSelected environment: %s\n\nSet up your application. Here are the steps to incorporate the LaunchDarkly %s SDK into your code. \n\n%s", + m.currProjectKey, + m.currEnvironmentKey, + m.currSdk.Name, + sdkInstructions, + ), + m.width, + ) } return fmt.Sprintf("\nstep %d of %d\n"+m.steps[m.currStep].View(), m.currStep+1, len(m.steps))