diff --git a/go.mod b/go.mod index be97b484..836b5cea 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ toolchain go1.22.3 require ( github.com/briandowns/spinner v1.23.1 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.2.4 github.com/google/uuid v1.6.0 github.com/mitchellh/cli v1.1.5 github.com/olekukonko/tablewriter v0.0.5 @@ -18,13 +20,24 @@ require ( github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/Masterminds/sprig/v3 v3.2.1 // indirect github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/lipgloss v1.0.0 // indirect + github.com/charmbracelet/x/ansi v0.4.5 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.11 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/posener/complete v1.1.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect @@ -34,6 +47,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/sync v0.10.0 // indirect ) require ( diff --git a/go.sum b/go.sum index 2a2a23ce..09cef3e6 100644 --- a/go.sum +++ b/go.sum @@ -6,15 +6,31 @@ github.com/Masterminds/sprig/v3 v3.2.1 h1:n6EPaDyLSvCEa3frruQvAiHuNp2dhBlMSmkEr+ github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= @@ -52,6 +68,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -61,6 +79,8 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -72,6 +92,12 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= @@ -129,8 +155,11 @@ golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/service/service.go b/internal/service/service.go index 9b04f208..892cd30a 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -5,7 +5,10 @@ import ( "context" "errors" "fmt" + tea "github.com/charmbracelet/bubbletea" + "github.com/dream11/odin/internal/ui" "io" + "os" "github.com/briandowns/spinner" "github.com/dream11/odin/pkg/constant" @@ -21,6 +24,15 @@ type Service struct{} var responseMap = make(map[string]string) +var program = tea.NewProgram( + &ui.ServiceDeployModel{ + ServiceView: ui.ServiceView{ + Name: "Initiating service deployment", + }, + }, + tea.WithAltScreen(), +) + // DeployService deploys service func (e *Service) DeployService(ctx *context.Context, request *serviceProto.DeployServiceRequest) error { conn, requestCtx, err := grpcClient(ctx) @@ -33,35 +45,25 @@ func (e *Service) DeployService(ctx *context.Context, request *serviceProto.Depl return err } - log.Info("Deploying Service...") - spinnerInstance := spinner.New(spinner.CharSets[constant.SpinnerType], constant.SpinnerDelay) - err = spinnerInstance.Color(constant.SpinnerColor, constant.SpinnerStyle) - if err != nil { - return err - } - - var message string - for { - response, err := stream.Recv() - spinnerInstance.Stop() - if err != nil { - if errors.Is(err, context.Canceled) || err == io.EOF { - break + go func() { + for { + response, err := stream.Recv() + if err != nil { + if errors.Is(err, context.Canceled) || err == io.EOF { + break + } + log.Errorf("TraceID: %s", (*requestCtx).Value(constant.TraceIDKey)) + program.Quit() + } + if response != nil { + program.Send(ui.GetServiceDeployModel(response, (*requestCtx).Value(constant.TraceIDKey).(string))) } - log.Errorf("TraceID: %s", (*requestCtx).Value(constant.TraceIDKey)) - return err } + }() - if response != nil { - - message = util.GenerateResponseMessage(response.GetServiceResponse()) - logFailedComponentMessagesOnce(response.GetServiceResponse()) - spinnerInstance.Prefix = fmt.Sprintf(" %s ", message) - spinnerInstance.Start() - } + if _, err := program.Run(); err != nil { + os.Exit(1) } - - log.Info(message) return err } diff --git a/internal/ui/deploy.go b/internal/ui/deploy.go new file mode 100644 index 00000000..adce5fc3 --- /dev/null +++ b/internal/ui/deploy.go @@ -0,0 +1,163 @@ +package ui + +import ( + "fmt" + "github.com/charmbracelet/bubbles/progress" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/dream11/odin/pkg/util" + "strings" +) + +func (s *ServiceDeployModel) Init() tea.Cmd { + s.ServiceDisplayMeta.Progress = progress.New(progress.WithDefaultScaledGradient()) + s.ServiceDisplayMeta.Progress.PercentageStyle = ProgressBarStyle + s.ServiceDisplayMeta.Progress.SetPercent(100) + s.ServiceDisplayMeta.Cursor = 0 + return nil +} + +func (s *ServiceDeployModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var updateCmds []tea.Cmd + switch msg := msg.(type) { + // Handle updates + case ServiceDeployModel: + s.ServiceDisplayMeta.Ready = true + s.ServiceView = msg.ServiceView + if len(s.ServiceView.ComponentsView) > len(s.ServiceDisplayMeta.ComponentDisplayMeta) { + for i := len(s.ServiceDisplayMeta.ComponentDisplayMeta); i < len(s.ServiceView.ComponentsView); i++ { + s.ServiceDisplayMeta.ComponentDisplayMeta = append(s.ServiceDisplayMeta.ComponentDisplayMeta, ComponentDisplayMeta{ + LogViewPort: viewport.Model{ + Width: s.ServiceDisplayMeta.Width, + Height: 10, + }, + }) + s.ServiceDisplayMeta.ComponentDisplayMeta[i].Spinner = spinner.New() + s.ServiceDisplayMeta.ComponentDisplayMeta[i].Spinner.Style = SpinnerStyle + s.ServiceDisplayMeta.ComponentDisplayMeta[i].Spinner.Spinner = spinner.Points + } + } + // Handle key presses + case tea.KeyMsg: + anyToggled := false + for _, component := range s.ServiceDisplayMeta.ComponentDisplayMeta { + if component.Toggle { + anyToggled = true + break + } + } + switch msg.String() { + case "up": + if s.ServiceDisplayMeta.Cursor > 0 && !anyToggled { + s.ServiceDisplayMeta.Cursor-- + } + + case "down": + if s.ServiceDisplayMeta.Cursor < len(s.ServiceDisplayMeta.ComponentDisplayMeta)-1 && !anyToggled { + s.ServiceDisplayMeta.Cursor++ + } + + case "enter", " ": + if s.ServiceDisplayMeta.Cursor < len(s.ServiceDisplayMeta.ComponentDisplayMeta) { + s.ServiceDisplayMeta.ComponentDisplayMeta[s.ServiceDisplayMeta.Cursor].Toggle = + !s.ServiceDisplayMeta.ComponentDisplayMeta[s.ServiceDisplayMeta.Cursor].Toggle + } + + case "q": + return s, tea.Quit + } + + // Handle window resizes + case tea.WindowSizeMsg: + s.ServiceDisplayMeta.Height = msg.Height + s.ServiceDisplayMeta.Width = msg.Width + if s.ServiceDisplayMeta.Ready { + for i := range s.ServiceDisplayMeta.ComponentDisplayMeta { + s.ServiceDisplayMeta.ComponentDisplayMeta[i].LogViewPort.Width = msg.Width + s.ServiceDisplayMeta.ComponentDisplayMeta[i].LogViewPort.Height = + max(util.GetAvailableViewPortHeight(msg.Height, lipgloss.Height("text"), len(s.ServiceDisplayMeta.ComponentDisplayMeta)), 10) + } + } + case spinner.TickMsg: + spinnerUpdateCmds := s.updateSpinners(msg) + return s, tea.Batch(spinnerUpdateCmds...) + } + + // Handle keyboard and mouse events in the viewport + updateCmds = append(updateCmds, s.tickSpinners()...) + updateCmds = append(updateCmds, s.updateViewPort(msg)...) + + return s, tea.Batch(updateCmds...) +} + +func (s *ServiceDeployModel) View() string { + if !s.ServiceDisplayMeta.Ready { + return H1Style.Render("Initializing service deployment...") + } + var builder strings.Builder + + // Build Service View + serviceHeader := util.GetHeaderText(s.ServiceView.Name, s.ServiceView.Action, s.ServiceView.Status, "Service") + builder.WriteString(fmt.Sprintf("%s\n", H1Style.Render(serviceHeader))) + s.ServiceDisplayMeta.Progress.Width = lipgloss.Width(serviceHeader) + 6 // to accommodate the percentage text + builder.WriteString(fmt.Sprintf("%s\n", s.ServiceDisplayMeta.Progress.ViewAs(100.0))) + builder.WriteString(fmt.Sprintf("Trace Id: %s\n", TextStyle.Render(s.ServiceView.TraceID))) + + for i, componentView := range s.ServiceView.ComponentsView { + componentHeader := util.GetHeaderText(componentView.Name, componentView.Action, componentView.Status, "Component") + var componentHeaderText string + if i == s.ServiceDisplayMeta.Cursor { + componentHeaderText = SelectedStyle(H2Style).Render(componentHeader) + } else { + componentHeaderText = H2Style.Render(componentHeader) + } + if s.ServiceView.ComponentsView[i].Status != "IN_PROGRESS" { + s.ServiceDisplayMeta.ComponentDisplayMeta[i].Spinner.Spinner = spinner.Pulse + } + builder.WriteString(fmt.Sprintf("%s %s\n", componentHeaderText, s.ServiceDisplayMeta.ComponentDisplayMeta[i].Spinner.View())) + if s.ServiceDisplayMeta.ComponentDisplayMeta[i].Toggle { + // Render logs + logsText := strings.Split(componentView.Content, "\\n") + s.ServiceDisplayMeta.ComponentDisplayMeta[i].LogViewPort.SetContent(InfoStyle.Render(strings.Join(logsText, "\n"))) + builder.WriteString(fmt.Sprintf("%s \n", s.ServiceDisplayMeta.ComponentDisplayMeta[i].LogViewPort.View())) + } + } + + // Add Footer with operating instructions + builder.WriteString("\n\n") + builder.WriteString(FooterStyle.Render("Use ↑ and ↓ to navigate components, Enter to toggle logs, q to quit")) + + return builder.String() +} + +func (s *ServiceDeployModel) updateViewPort(msg tea.Msg) []tea.Cmd { + var cmds []tea.Cmd + var cmd tea.Cmd + for i := range s.ServiceDisplayMeta.ComponentDisplayMeta { + s.ServiceDisplayMeta.ComponentDisplayMeta[i].LogViewPort, cmd = + s.ServiceDisplayMeta.ComponentDisplayMeta[i].LogViewPort.Update(msg) + cmds = append(cmds, cmd) + } + return cmds +} + +func (s *ServiceDeployModel) updateSpinners(msg tea.Msg) []tea.Cmd { + var cmds []tea.Cmd + var cmd tea.Cmd + for i := range s.ServiceDisplayMeta.ComponentDisplayMeta { + s.ServiceDisplayMeta.ComponentDisplayMeta[i].Spinner, cmd = + s.ServiceDisplayMeta.ComponentDisplayMeta[i].Spinner.Update(msg) + cmds = append(cmds, cmd) + } + return cmds +} + +func (s *ServiceDeployModel) tickSpinners() []tea.Cmd { + var cmds []tea.Cmd + for i := range s.ServiceDisplayMeta.ComponentDisplayMeta { + cmds = append(cmds, s.ServiceDisplayMeta.ComponentDisplayMeta[i].Spinner.Tick) + } + return cmds +} diff --git a/internal/ui/deploymodel.go b/internal/ui/deploymodel.go new file mode 100644 index 00000000..d191f8a1 --- /dev/null +++ b/internal/ui/deploymodel.go @@ -0,0 +1,75 @@ +package ui + +import ( + "github.com/charmbracelet/bubbles/progress" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/viewport" + v1 "github.com/dream11/odin/proto/gen/go/dream11/od/service/v1" +) + +type ServiceDeployModel struct { + ServiceDisplayMeta ServiceDisplayMeta + ServiceView ServiceView +} + +type ServiceDisplayMeta struct { + Height int + Width int + Ready bool + Cursor int + ElapsedTime int + TotalCompletionTime int + Progress progress.Model + ComponentDisplayMeta []ComponentDisplayMeta +} + +type ComponentDisplayMeta struct { + Height int + Width int + Toggle bool + Spinner spinner.Model + LogViewPort viewport.Model +} + +type ServiceView struct { + Name string + Status string + Action string + TraceID string + ComponentsView []ComponentView +} + +type ComponentView struct { + Name string + Status string + Action string + Content string +} + +func GetServiceDeployModel(response *v1.DeployServiceResponse, traceID string) ServiceDeployModel { + serviceName := response.GetServiceResponse().Name + + serviceView := ServiceView{ + Name: serviceName, + Action: response.GetServiceResponse().ServiceStatus.GetServiceAction(), + Status: response.GetServiceResponse().ServiceStatus.GetServiceStatus(), + TraceID: traceID, + ComponentsView: make([]ComponentView, 0), + } + for _, component := range response.GetServiceResponse().GetComponentsStatus() { + errorMessage := "Awaiting component logs" + if component.GetError() != "" { + errorMessage = component.GetError() + } + componentView := ComponentView{ + Name: component.GetComponentName(), + Action: component.GetComponentAction(), + Status: component.GetComponentStatus(), + Content: errorMessage, + } + serviceView.ComponentsView = append(serviceView.ComponentsView, componentView) + } + return ServiceDeployModel{ + ServiceView: serviceView, + } +} diff --git a/internal/ui/styles.go b/internal/ui/styles.go new file mode 100644 index 00000000..188168ea --- /dev/null +++ b/internal/ui/styles.go @@ -0,0 +1,60 @@ +package ui + +import "github.com/charmbracelet/lipgloss" + +var H1Style = func() lipgloss.Style { + return lipgloss.NewStyle(). + Foreground(lipgloss.Color("#BC88D3")). + Margin(1). + Align(lipgloss.Left) +}() + +var H2Style = func() lipgloss.Style { + return lipgloss.NewStyle(). + Foreground(lipgloss.Color("#E8D2F2")). + Margin(1, 1, 0, 5). + Align(lipgloss.Left) +}() + +var TextStyle = func() lipgloss.Style { + return lipgloss.NewStyle(). + Foreground(lipgloss.Color("#E8D2F2")). + Italic(true). + Align(lipgloss.Left) +}() + +var FooterStyle = func() lipgloss.Style { + return lipgloss.NewStyle(). + Foreground(lipgloss.Color("#E8D2F2")). + Bold(false). + Italic(true). + Faint(true). + Align(lipgloss.Left) +}() + +var InfoStyle = func() lipgloss.Style { + return lipgloss.NewStyle(). + Foreground(lipgloss.Color("#E8D2F2")). + MarginLeft(5). + BorderLeft(true). + BorderStyle(lipgloss.RoundedBorder()). + PaddingLeft(2). + Align(lipgloss.Left) +}() + +var SelectedStyle = func(style lipgloss.Style) lipgloss.Style { + return style. + Underline(true). + Bold(true) +} + +var SpinnerStyle = func() lipgloss.Style { + return lipgloss.NewStyle(). + PaddingBottom(1). + Foreground(lipgloss.Color("#D9D9D9")) +}() + +var ProgressBarStyle = func() lipgloss.Style { + return lipgloss.NewStyle(). + Foreground(lipgloss.Color("#E88D4C")) +}() diff --git a/pkg/util/util.go b/pkg/util/util.go index 8ddf8a5d..7ee57100 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -166,3 +166,30 @@ func ConvertJSONToYAML(jsonStr string) (string, error) { // Return the YAML string return string(yamlData), nil } + +func GetHeaderText(name string, action string, status string, element string) string { + var header strings.Builder + var actionText string + switch action { + case "VALIDATE": + actionText = "validation" + case "DEPLOY": + actionText = "deployment" + case "UNDEPLOY": + actionText = "un-deployment" + } + switch status { + case "IN_PROGRESS": + header.WriteString(fmt.Sprintf("%s: %s %s in progress", element, name, actionText)) + case "SUCCESSFUL": + header.WriteString(fmt.Sprintf("%s: %s %s successful", element, name, actionText)) + case "FAILED": + header.WriteString(fmt.Sprintf("%s: %s %s failed", element, name, actionText)) + } + return header.String() +} + +func GetAvailableViewPortHeight(totalHeight int, headerHeight int, numberOfComponents int) int { + bottomPadding := 12 + return totalHeight - ((headerHeight + 2) + numberOfComponents*(headerHeight+1)) - bottomPadding +}