Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Pizza show #24

Merged
merged 8 commits into from
Sep 27, 2023
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
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"))
jpmcb marked this conversation as resolved.
Show resolved Hide resolved
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
Loading