Skip to content

Commit

Permalink
feat: Pizza show (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
k1nho authored Sep 27, 2023
1 parent f399865 commit 72f21ce
Show file tree
Hide file tree
Showing 9 changed files with 873 additions and 19 deletions.
2 changes: 2 additions & 0 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/open-sauced/pizza-cli/cmd/bake"
"github.com/open-sauced/pizza-cli/cmd/insights"
repoquery "github.com/open-sauced/pizza-cli/cmd/repo-query"
"github.com/open-sauced/pizza-cli/cmd/show"
"github.com/open-sauced/pizza-cli/cmd/version"
"github.com/open-sauced/pizza-cli/pkg/constants"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -41,6 +42,7 @@ func NewRootCommand() (*cobra.Command, error) {
cmd.AddCommand(auth.NewLoginCommand())
cmd.AddCommand(insights.NewInsightsCommand())
cmd.AddCommand(version.NewVersionCommand())
cmd.AddCommand(show.NewShowCommand())

return cmd, nil
}
Expand Down
63 changes: 63 additions & 0 deletions cmd/show/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package show

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

// WindowSize stores the size of the terminal
var WindowSize tea.WindowSizeMsg

// Keymaps
var OpenPR = key.NewBinding(key.WithKeys("O"), key.WithHelp("O", "open pr"))
var BackToDashboard = key.NewBinding(key.WithKeys("B"), key.WithHelp("B", "back"))
var ToggleHelpMenu = key.NewBinding(key.WithKeys("H"), key.WithHelp("H", "toggle help"))

// STYLES
// Viewport: The viewport of the tui (my:2, mx:2)
var Viewport = lipgloss.NewStyle().Margin(1, 2)

// Container: container styling (width: 80, py: 0, px: 5)
var Container = lipgloss.NewStyle().Width(80).Padding(0, 5)

// WidgetContainer: container for tables, and graphs (py:2, px:2)
var WidgetContainer = lipgloss.NewStyle().Padding(2, 2)

// SquareBorder: Style to draw a border around a section
var SquareBorder = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).BorderForeground(Color)

// TextContainer: container for text
var TextContainer = lipgloss.NewStyle().Padding(1, 1)

// TableTitle: The style for table titles (width:25, align-horizontal:center, bold:true)
var TableTitle = lipgloss.NewStyle().Width(25).AlignHorizontal(lipgloss.Center).Bold(true)

// Color: the color palette (Light: #000000, Dark: #FF4500)
var Color = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FF4500"}

// ActiveStyle: table when selected (border:normal, border-foreground:#FF4500)
var ActiveStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#FF4500"))

// InactiveStyle: table when not selected (border:normal, border-foreground:#FFFFFF)
var InactiveStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#FFFFFF"))

// ItemStyle: style applied to items in a list.Model
var ItemStyle = lipgloss.NewStyle().PaddingLeft(4)

// SelectedItemStyle: style applied when the item is selected in a list.Model
var SelectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(Color)

// ListItemTitle: style for the list.Model title
var ListItemTitleStyle = lipgloss.NewStyle().MarginLeft(2)

// PaginationStyle: style for pagination of list.Model
var PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)

// HelpStyle: style for help menu
var HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
256 changes: 256 additions & 0 deletions cmd/show/contributors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package show

import (
"context"
"errors"
"fmt"
"io"
"strings"
"sync"

"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/cli/browser"
client "github.com/open-sauced/go-api/client"
)

// prItem: type for pull request to satisfy the list.Item interface
type prItem client.DbPullRequest

func (i prItem) FilterValue() string { return i.Title }
func (i prItem) GetRepoName() string {
if i.FullName != nil {
return *i.FullName
}
return ""
}

type itemDelegate struct{}

func (d itemDelegate) Height() int { return 1 }
func (d itemDelegate) Spacing() int { return 1 }
func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i, ok := listItem.(prItem)
if !ok {
return
}

prTitle := i.Title
if len(prTitle) >= 60 {
prTitle = fmt.Sprintf("%s...", prTitle[:60])
}

str := fmt.Sprintf("#%d %s\n%s\n(%s)", i.Number, i.GetRepoName(), prTitle, i.State)

fn := ItemStyle.Render
if index == m.Index() {
fn = func(s ...string) string {
return SelectedItemStyle.Render("🍕 " + strings.Join(s, " "))
}
}

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

// ContributorModel holds all the information related to a contributor
type ContributorModel struct {
username string
userInfo *client.DbUser
prList list.Model
APIClient *client.APIClient
serverContext context.Context
}

type (
// BackMsg: message to signal main model that we are back to dashboard when backspace is pressed
BackMsg struct{}

// ContributorErrMsg: message to signal that an error occurred when fetching contributor information
ContributorErrMsg struct {
name string
err error
}
)

// InitContributor: initializes the contributorModel
func InitContributor(opts *Options) (tea.Model, error) {
var model ContributorModel
model.APIClient = opts.APIClient
model.serverContext = opts.ServerContext

return model, nil
}

func (m ContributorModel) Init() tea.Cmd { return nil }

func (m ContributorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
WindowSize = msg
case SelectMsg:
m.username = msg.contributorName
model, err := m.fetchUser()
if err != nil {
return m, func() tea.Msg { return ContributorErrMsg{name: msg.contributorName, err: err} }
}
return model, func() tea.Msg { return SuccessMsg{} }
case tea.KeyMsg:
switch msg.String() {
case "B":
if !m.prList.SettingFilter() {
return m, func() tea.Msg { return BackMsg{} }
}
case "H":
if !m.prList.SettingFilter() {
m.prList.SetShowHelp(!m.prList.ShowHelp())
return m, nil
}
case "O":
if !m.prList.SettingFilter() {
pr, ok := m.prList.SelectedItem().(prItem)
if ok {
err := browser.OpenURL(fmt.Sprintf("https://github.com/%s/pull/%d", pr.GetRepoName(), pr.Number))
if err != nil {
fmt.Println("could not open pull request in browser")
}
}
}
case "q", "ctrl+c", "ctrl+d":
if !m.prList.SettingFilter() {
return m, tea.Quit
}
}
}
m.prList, cmd = m.prList.Update(msg)
return m, cmd
}

func (m ContributorModel) View() string {
return m.drawContributorView()
}

// fetchUser: fetches all the user information (general info, and pull requests)
func (m *ContributorModel) fetchUser() (tea.Model, error) {
var (
wg sync.WaitGroup
errChan = make(chan error, 2)
)

wg.Add(1)
go func() {
defer wg.Done()
err := m.fetchContributorInfo(m.username)
if err != nil {
errChan <- err
return
}
}()

wg.Add(1)
go func() {
defer wg.Done()
err := m.fetchContributorPRs(m.username)
if err != nil {
errChan <- err
return
}
}()

wg.Wait()
close(errChan)
if len(errChan) > 0 {
var allErrors error
for err := range errChan {
allErrors = errors.Join(allErrors, err)
}
return m, allErrors
}

return m, nil
}

// fetchContributorInfo: fetches the contributor info
func (m *ContributorModel) fetchContributorInfo(name string) error {
resp, r, err := m.APIClient.UserServiceAPI.FindOneUserByUserame(m.serverContext, name).Execute()
if err != nil {
return err
}

if r.StatusCode != 200 {
return fmt.Errorf("HTTP failed: %d", r.StatusCode)
}

m.userInfo = resp
return nil
}

// fetchContributorPRs: fetches the contributor pull requests and creates pull request list
func (m *ContributorModel) fetchContributorPRs(name string) error {
resp, r, err := m.APIClient.UserServiceAPI.FindContributorPullRequests(m.serverContext, name).Limit(10).Execute()
if err != nil {
return err
}

if r.StatusCode != 200 {
return fmt.Errorf("HTTP failed: %d", r.StatusCode)
}

// create contributor pull request list
var items []list.Item
for _, pr := range resp.Data {
items = append(items, prItem(pr))
}

l := list.New(items, itemDelegate{}, WindowSize.Width, 14)
l.Title = "✨ Latest Pull Requests"
l.Styles.Title = ListItemTitleStyle
l.Styles.HelpStyle = HelpStyle
l.Styles.NoItems = ItemStyle
l.SetShowStatusBar(false)
l.SetStatusBarItemName("pull request", "pull requests")
l.AdditionalShortHelpKeys = func() []key.Binding {
return []key.Binding{
OpenPR,
BackToDashboard,
ToggleHelpMenu,
}
}

m.prList = l
return nil
}

// drawContributorView: view of the contributor model
func (m *ContributorModel) drawContributorView() string {
contributorInfo := m.drawContributorInfo()

contributorView := lipgloss.JoinVertical(lipgloss.Left, lipgloss.NewStyle().PaddingLeft(2).Render(contributorInfo),
WidgetContainer.Render(m.prList.View()))

_, h := lipgloss.Size(contributorView)
if WindowSize.Height < h {
contributorView = lipgloss.JoinHorizontal(lipgloss.Center, contributorInfo, m.prList.View())
}

return contributorView
}

// drawContributorInfo: view of the contributor info (open issues, pr velocity, pr count, maintainer)
func (m *ContributorModel) drawContributorInfo() string {
userOpenIssues := fmt.Sprintf("📄 Issues: %d", m.userInfo.OpenIssues)
isUserMaintainer := fmt.Sprintf("🔨 Maintainer: %t", m.userInfo.GetIsMaintainer())
prVelocity := fmt.Sprintf("🔥 PR Velocity (30d): %d%%", m.userInfo.RecentPullRequestVelocityCount)
prCount := fmt.Sprintf("🚀 PR Count (30d): %d", m.userInfo.RecentPullRequestsCount)

prStats := lipgloss.JoinVertical(lipgloss.Left, TextContainer.Render(prVelocity), TextContainer.Render(prCount))
issuesAndMaintainer := lipgloss.JoinVertical(lipgloss.Center, TextContainer.Render(userOpenIssues), TextContainer.Render(isUserMaintainer))

contributorInfo := lipgloss.JoinHorizontal(lipgloss.Center, prStats, issuesAndMaintainer)
contributorView := lipgloss.JoinVertical(lipgloss.Center, m.userInfo.Login, contributorInfo)

return SquareBorder.Render(contributorView)
}
Loading

0 comments on commit 72f21ce

Please sign in to comment.