Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions environment/handler/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Package handler provides the CMD handler logic for managing environments.
package handler

import (
"fmt"

"gofr.dev/pkg/gofr"
)

// Handler is responsible for managing environment-related operations.
type Handler struct {
envSvc EnvAdder
}

// New creates a new Handler with the given EnvAdder service.
func New(envSvc EnvAdder) *Handler {
return &Handler{envSvc: envSvc}
}

// Add handles the HTTP request to add environments. It delegates the task
// to the EnvAdder service and returns a success message or an error.
//
// Parameters:
// - ctx: The GoFR context containing request data.
//
// Returns:
// - A success message indicating how many environments were added, or an error
// if the operation failed.
func (h *Handler) Add(ctx *gofr.Context) (any, error) {
n, err := h.envSvc.Add(ctx)
if err != nil {
return nil, err
}

return fmt.Sprintf("%d environments added", n), nil
}
7 changes: 7 additions & 0 deletions environment/handler/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package handler

import "gofr.dev/pkg/gofr"

type EnvAdder interface {
Add(ctx *gofr.Context) (int, error)
}
132 changes: 132 additions & 0 deletions environment/service/app_selector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Package service provides functionalities for interacting with applications and environments.
// It supports selecting an application and adding environments to it by communicating with an external API.
// it gives users a text-based user interface (TUI) for displaying and selecting items
// using the Charmbracelet bubbletea and list packages.
package service

import (
"fmt"
"io"
"strings"

"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

const (
// listWidth defines the width of the list.
listWidth = 20
// listHeight defines the height of the list.
listHeight = 14
// listPaddingLeft defines the left padding of the list items.
listPaddingLeft = 2
// paginationPadding defines the padding for pagination controls.
paginationPadding = 4
)

//nolint:gochecknoglobals //required TUI styles for displaying the list
var (
// itemStyle defines the default style for list items.
itemStyle = lipgloss.NewStyle().PaddingLeft(listPaddingLeft)
// selectedItemStyle defines the style for the selected list item.
selectedItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("170"))
// paginationStyle defines the style for pagination controls.
paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(paginationPadding)
// helpStyle defines the style for the help text.
helpStyle = list.DefaultStyles().HelpStyle
)

// item represents a single item in the list.
type item struct {
id int // ID is the unique identifier for the item.
name string // Name is the display name of the item.
}

// FilterValue returns the value to be used for filtering list items.
// In this case, it's the name of the item.
func (i *item) FilterValue() string {
return i.name
}

// itemDelegate is a struct responsible for rendering and interacting with list items.
type itemDelegate struct{}

// Height returns the height of the item (always 1).
func (itemDelegate) Height() int { return 1 }

// Spacing returns the spacing between items (always 0).
func (itemDelegate) Spacing() int { return 0 }

// Update is used to handle updates to the item model. It doesn't do anything in this case.
func (itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd {
return nil
}

// Render renders a single list item, applying the selected item style if it's the currently selected item.
//
//nolint:gocritic //required for rendering list items and implementing ItemDelegate interface
func (itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i, ok := listItem.(*item)
if !ok {
return
}

str := fmt.Sprintf("%3d. %s", index+1, i.name)

fn := itemStyle.Render
if index == m.Index() {
fn = func(s ...string) string {
return selectedItemStyle.Render("> " + strings.Join(s, " "))
}
}

fmt.Fprint(w, fn(str))
}

// model represents the state of the TUI interface, including the list and selected item.
type model struct {
choice *item // choice is the selected item.
quitting bool // quitting indicates if the application is quitting.
list list.Model // list holds the list of items displayed in the TUI.
}

// Init initializes the model, returning nil for no commands.
func (*model) Init() tea.Cmd {
return nil
}

// Update handles updates from messages, such as key presses or window resizing.
// It updates the list and handles quitting or selecting an item.
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.list.SetWidth(msg.Width)
return m, nil

case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case "q", "ctrl+c":
m.quitting = true // Set quitting to true when 'q' or 'ctrl+c' is pressed.
return m, tea.Quit

case "enter":
i, ok := m.list.SelectedItem().(*item)
if ok {
m.choice = i
}

return m, tea.Quit
}
}

var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)

return m, cmd
}

// View renders the view of the current model, displaying the list to the user.
func (m *model) View() string {
return "\n" + m.list.View()
}
160 changes: 160 additions & 0 deletions environment/service/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package service

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"gofr.dev/pkg/gofr"
)

var (
// ErrUnableToRenderApps is returned when the application list cannot be rendered.
ErrUnableToRenderApps = errors.New("unable to render the list of applications")
// ErrConnectingZopAPI is returned when there is an error connecting to the Zop API.
ErrConnectingZopAPI = errors.New("unable to connect to Zop API")
// ErrorAddingEnv is returned when there is an error adding an environment.
ErrorAddingEnv = errors.New("unable to add environment")
// ErrNoApplicationSelected is returned when no application is selected.
ErrNoApplicationSelected = errors.New("no application selected")
)

// Service represents the application service that handles application and environment operations.
type Service struct {
appGet ApplicationGetter // appGet is responsible for fetching the list of applications.
}

// New creates a new Service instance with the provided ApplicationGetter.
func New(appGet ApplicationGetter) *Service {
return &Service{appGet: appGet}
}

// Add prompts the user to add environments to a selected application.
// It returns the number of environments added and an error, if any.
func (s *Service) Add(ctx *gofr.Context) (int, error) {
app, err := s.getSelectedApplication(ctx)
if err != nil {
return 0, err
}

ctx.Out.Println("Selected application: ", app.name)
ctx.Out.Println("Please provide names of environments to be added...")

var (
input string
level = 1
)

// Loop to gather environment names from the user and add them to the application.
for {
ctx.Out.Print("Enter environment name: ")

_, _ = fmt.Scanf("%s", &input)

err = postEnvironment(ctx, &Environment{Name: input, Level: level, ApplicationID: int64(app.id)})
if err != nil {
return level, err
}

level++

// Ask the user if they want to add more environments.
ctx.Out.Print("Do you wish to add more? (y/n) ")

_, _ = fmt.Scanf("%s", &input)

if input == "n" {
break
}
}

return level, nil
}

// getSelectedApplication renders a list of applications for the user to select from.
// It returns the selected application or an error if no selection is made.
func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) {
apps, err := s.appGet.List(ctx)
if err != nil {
return nil, err
}

// Prepare a list of items for the user to select from.
items := make([]list.Item, 0)
for _, app := range apps {
items = append(items, &item{app.ID, app.Name})
}

// Initialize the list component for application selection.
l := list.New(items, itemDelegate{}, listWidth, listHeight)
l.Title = "Select the application where you want to add the environment!"
l.SetShowStatusBar(false)
l.SetFilteringEnabled(true)
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle
l.SetShowStatusBar(false)

m := model{list: l}

// Render the list using the bubbletea program.
if _, er := tea.NewProgram(&m).Run(); er != nil {
ctx.Logger.Errorf("unable to render the list of applications! %v", er)
return nil, ErrUnableToRenderApps
}

if m.choice == nil {
return nil, ErrNoApplicationSelected
}

return m.choice, nil
}

// postEnvironment sends a POST request to the API to add the provided environment to the application.
// It returns an error if the request fails or the response status code is not created (201).
func postEnvironment(ctx *gofr.Context, env *Environment) error {
body, _ := json.Marshal(env)

resp, err := ctx.GetHTTPService("api-service").
PostWithHeaders(ctx, fmt.Sprintf("application/%d/environments", env.ApplicationID), nil, body, map[string]string{
"Content-Type": "application/json",
})
if err != nil {
ctx.Logger.Errorf("unable to connect to Zop API! %v", err)
return ErrConnectingZopAPI
}

if resp.StatusCode != http.StatusCreated {
var errResp struct {
Errors any `json:"errors,omitempty"`
}

err = getResponse(resp, &errResp)
if err != nil {
ctx.Logger.Errorf("unable to add environment!, could not decode error message %v", err)
}

ctx.Logger.Errorf("unable to add environment! %v", resp)

return ErrorAddingEnv
}

return nil
}

// getResponse reads the HTTP response body and unmarshals it into the provided interface.
func getResponse(resp *http.Response, i any) error {
defer resp.Body.Close()

b, _ := io.ReadAll(resp.Body)

err := json.Unmarshal(b, i)
if err != nil {
return err
}

return nil
}
14 changes: 14 additions & 0 deletions environment/service/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package service

import (
"gofr.dev/pkg/gofr"
appSvc "zop.dev/cli/zop/application/service"
)

// ApplicationGetter interface is used to abstract the process of fetching application data,
// which can be implemented by any service that has access to application-related data.
type ApplicationGetter interface {
// List fetches a list of applications from the service.
// It returns a slice of Application objects and an error if the request fails.
List(ctx *gofr.Context) ([]appSvc.Application, error)
}
Loading
Loading