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

Add support for cloud installations #24

Merged
merged 3 commits into from
Jan 12, 2025

Conversation

xederro
Copy link
Contributor

@xederro xederro commented Dec 29, 2024

Hello,
I found this repo by accident and wanted to give it a try since I'm building something similar for myself. To test it I also needed support for cloud installations and since this is an open issue #16 and go-jira already supports it I managed to add it. During my work, I also found few things to improve.

So what changed:

  • documentation changes like:
    h/<Up>                                  Move cursor up
    k/<Down>                                Move cursor down

to

    k/<Up>                                  Move cursor up
    j/<Down>                                Move cursor down
  • allowed jira_time_delta_mins to not be added in config
  • changed validation to also check for nil pointer, so that when there is lack of some argument in config the program will tell what is lacking instead of throwing nil pointer dereference errors
  • moved printing of config after verification to avoid nil pointer dereference errors
  • when there is no config file the app won't exit since everything can be passed by CLI arguments
  • added support for cloud installations:
    • jira_cloud_username and jira_cloud_token is now used for Jira cloud auth
    • only one type of auth has to be present at once
    • the changes should not break current configs and behaviour

I hope it's up to your standard.

@dhth
Copy link
Owner

dhth commented Dec 30, 2024

Thanks for your contribution! :)
I am writing to acknowledge that I've seen this PR. I've got my hands full at the moment, but will take a look at this in the next few days.

@dhth
Copy link
Owner

dhth commented Jan 11, 2025

Hi. Thanks again for the change. Sry, took a while to get to it.

I tried it out locally, and it works great. I have a few minor/cosmetic tweaks to your change that I think make sense to be merged alongside this PR. Do you mind if I push directly to your branch?

Here's the patch if it helps.

expand
diff --git a/cmd/config.go b/cmd/config.go
index 10e12d5..6046753 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -1,30 +1,33 @@
 package cmd
 
 import (
 	"github.com/BurntSushi/toml"
 )
 
+const (
+	jiraInstallationTypeOnPremise = "onpremise"
+	jiraInstallationTypeCloud     = "cloud"
+)
+
 type JiraConfig struct {
+	InstallationType  string  `toml:"installation_type"`
 	JiraURL           *string `toml:"jira_url"`
 	Jql               *string
 	JiraTimeDeltaMins int     `toml:"jira_time_delta_mins"`
 	JiraToken         *string `toml:"jira_token"`
-	JiraCloudToken    *string `toml:"jira_cloud_token"`
-	JiraCloudUsername *string `toml:"jira_cloud_username"`
+	JiraUsername      *string `toml:"jira_username"`
 }
 
 type POConfig struct {
 	Jira JiraConfig
 }
 
 func readConfig(filePath string) (POConfig, error) {
-
 	var config POConfig
 	_, err := toml.DecodeFile(expandTilde(filePath), &config)
 	if err != nil {
 		return config, err
 	}
 
 	return config, nil
-
 }
diff --git a/cmd/root.go b/cmd/root.go
index 16f2bd1..daf3fe1 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -12,32 +12,31 @@ import (
 	jiraOnPremise "github.com/andygrunwald/go-jira/v2/onpremise"
 	"github.com/dhth/punchout/internal/ui"
 )
 
 func die(msg string, args ...any) {
 	fmt.Fprintf(os.Stderr, msg+"\n", args...)
 	os.Exit(1)
 }
 
 var (
+	jiraInstallationType = flag.String("jira-installation-type", "", "JIRA installation type; allowed values: [cloud, onpremise]")
 	jiraURL              = flag.String("jira-url", "", "URL of the JIRA server")
 	jiraToken            = flag.String("jira-token", "", "personal access token for the JIRA server")
-	jiraCloudToken       = flag.String("jira-cloud-token", "", "API token for the JIRA cloud")
-	jiraCloudUsername    = flag.String("jira-cloud-username", "", "username for the JIRA cloud")
+	jiraUsername         = flag.String("jira-username", "", "username for the JIRA cloud")
 	jql                  = flag.String("jql", "", "JQL to use to query issues at startup")
 	jiraTimeDeltaMinsStr = flag.String("jira-time-delta-mins", "", "Time delta (in minutes) between your timezone and the timezone of the server; can be +/-")
 	listConfig           = flag.Bool("list-config", false, "Whether to only print out the config that punchout will use or not")
 )
 
 func Execute() {
 	currentUser, err := user.Current()
-
 	if err != nil {
 		die("Error getting your home directory, explicitly specify the path for the config file using -config-file-path")
 	}
 
 	defaultConfigFP := fmt.Sprintf("%s/.config/punchout/punchout.toml", currentUser.HomeDir)
 	configFilePath := flag.String("config-file-path", defaultConfigFP, "location of the punchout config file")
 
 	defaultDBPath := fmt.Sprintf("%s/punchout.v%s.db", currentUser.HomeDir, PUNCHOUT_DB_VERSION)
 	dbPath := flag.String("db-path", defaultDBPath, "location where punchout should create its DB file")
 
@@ -59,109 +58,109 @@ func Execute() {
 	dbPathFull := expandTilde(*dbPath)
 
 	var jiraTimeDeltaMins int
 	if *jiraTimeDeltaMinsStr != "" {
 		jiraTimeDeltaMins, err = strconv.Atoi(*jiraTimeDeltaMinsStr)
 		if err != nil {
 			die("couldn't convert jira-time-delta-mins to a number")
 		}
 	}
 
-	poCfg, err := readConfig(*configFilePath)
+	cfg, err := readConfig(*configFilePath)
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "error reading config at %s: %s.\n"+
-			"continue with command line args only\n", *configFilePath, err.Error())
+		die("error reading config: %s.\n", err.Error())
+	}
+
+	if *jiraInstallationType != "" {
+		cfg.Jira.InstallationType = *jiraInstallationType
 	}
 
 	if *jiraURL != "" {
-		poCfg.Jira.JiraURL = jiraURL
+		cfg.Jira.JiraURL = jiraURL
 	}
 
 	if *jiraToken != "" {
-		poCfg.Jira.JiraToken = jiraToken
+		cfg.Jira.JiraToken = jiraToken
 	}
 
-	if *jiraCloudToken != "" {
-		poCfg.Jira.JiraCloudToken = jiraCloudToken
-	}
-
-	if *jiraCloudUsername != "" {
-		poCfg.Jira.JiraCloudUsername = jiraCloudUsername
+	if *jiraUsername != "" {
+		cfg.Jira.JiraUsername = jiraUsername
 	}
 
 	if *jql != "" {
-		poCfg.Jira.Jql = jql
+		cfg.Jira.Jql = jql
 	}
+
 	if *jiraTimeDeltaMinsStr != "" {
-		poCfg.Jira.JiraTimeDeltaMins = jiraTimeDeltaMins
+		cfg.Jira.JiraTimeDeltaMins = jiraTimeDeltaMins
 	}
 
 	// validations
-	if poCfg.Jira.JiraURL == nil || *poCfg.Jira.JiraURL == "" {
+	var installationType ui.JiraInstallationType
+	switch cfg.Jira.InstallationType {
+	case "", jiraInstallationTypeOnPremise: // "" to maintain backwards compatibility
+		installationType = ui.OnPremiseInstallation
+	case jiraInstallationTypeCloud:
+		installationType = ui.CloudInstallation
+	default:
+		die("invalid value for jira installation type (allowed values: [%s, %s]): %q", jiraInstallationTypeOnPremise, jiraInstallationTypeCloud, cfg.Jira.InstallationType)
+	}
+
+	if cfg.Jira.JiraURL == nil || *cfg.Jira.JiraURL == "" {
 		die("jira-url cannot be empty")
 	}
 
-	if poCfg.Jira.Jql == nil || *poCfg.Jira.Jql == "" {
+	if cfg.Jira.Jql == nil || *cfg.Jira.Jql == "" {
 		die("jql cannot be empty")
 	}
 
-	if (poCfg.Jira.JiraToken == nil) == (poCfg.Jira.JiraCloudToken == nil) {
-		die("only one of on-premise or cloud auth method must be provided")
+	if cfg.Jira.JiraToken != nil && *cfg.Jira.JiraToken == "" {
+		die("jira-token cannot be empty")
 	}
 
-	if poCfg.Jira.JiraToken != nil && *poCfg.Jira.JiraToken == "" {
-		die("jira-token cannot be empty for on premise auth")
-	}
-
-	if poCfg.Jira.JiraCloudToken != nil && *poCfg.Jira.JiraCloudToken == "" {
-		die("jira-token cannot be empty for cloud auth")
-	}
-
-	if poCfg.Jira.JiraCloudToken != nil && (poCfg.Jira.JiraCloudUsername == nil || *poCfg.Jira.JiraCloudUsername == "") {
-		die("jira-username cannot be empty for cloud auth")
+	if installationType == ui.CloudInstallation && (cfg.Jira.JiraUsername == nil || *cfg.Jira.JiraUsername == "") {
+		die("jira-username cannot be empty for installation type \"cloud\"")
 	}
 
 	configKeyMaxLen := 40
 	if *listConfig {
 		fmt.Fprint(os.Stdout, "Config:\n\n")
 		fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("Config File Path", configKeyMaxLen), *configFilePath)
 		fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("DB File Path", configKeyMaxLen), dbPathFull)
-		fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA URL", configKeyMaxLen), *poCfg.Jira.JiraURL)
-		if poCfg.Jira.JiraToken != nil {
-			fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA Token", configKeyMaxLen), *poCfg.Jira.JiraToken)
+		fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA Installation Type", configKeyMaxLen), cfg.Jira.InstallationType)
+		fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA URL", configKeyMaxLen), *cfg.Jira.JiraURL)
+		fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA Token", configKeyMaxLen), *cfg.Jira.JiraToken)
+		if installationType == ui.CloudInstallation {
+			fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA Username", configKeyMaxLen), *cfg.Jira.JiraUsername)
 		}
-		if poCfg.Jira.JiraCloudToken != nil {
-			fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA API Token", configKeyMaxLen), *poCfg.Jira.JiraCloudToken)
-			fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA Username", configKeyMaxLen), *poCfg.Jira.JiraCloudUsername)
-		}
-		fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JQL", configKeyMaxLen), *poCfg.Jira.Jql)
-		fmt.Fprintf(os.Stdout, "%s%d\n", ui.RightPadTrim("JIRA Time Delta Mins", configKeyMaxLen), poCfg.Jira.JiraTimeDeltaMins)
+		fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JQL", configKeyMaxLen), *cfg.Jira.Jql)
+		fmt.Fprintf(os.Stdout, "%s%d\n", ui.RightPadTrim("JIRA Time Delta Mins", configKeyMaxLen), cfg.Jira.JiraTimeDeltaMins)
 		os.Exit(0)
 	}
 
 	db, err := setupDB(dbPathFull)
 	if err != nil {
 		die("couldn't set up punchout database. This is a fatal error\n")
 	}
 
-	// setup jira client with one of available auth methods
-	var client *http.Client
-	if poCfg.Jira.JiraToken != nil {
+	var httpClient *http.Client
+	switch installationType {
+	case ui.OnPremiseInstallation:
 		tp := jiraOnPremise.BearerAuthTransport{
-			Token: *poCfg.Jira.JiraToken,
+			Token: *cfg.Jira.JiraToken,
 		}
-		client = tp.Client()
-	} else {
+		httpClient = tp.Client()
+	case ui.CloudInstallation:
 		tp := jiraCloud.BasicAuthTransport{
-			Username: *poCfg.Jira.JiraCloudUsername,
-			APIToken: *poCfg.Jira.JiraCloudToken,
+			Username: *cfg.Jira.JiraUsername,
+			APIToken: *cfg.Jira.JiraToken,
 		}
-		client = tp.Client()
+		httpClient = tp.Client()
 	}
 
-	cl, err := jiraOnPremise.NewClient(*poCfg.Jira.JiraURL, client)
+	cl, err := jiraOnPremise.NewClient(*cfg.Jira.JiraURL, httpClient)
 	if err != nil {
-		panic(err)
+		die("couldn't create JIRA client: %s", err)
 	}
 
-	ui.RenderUI(db, cl, *poCfg.Jira.Jql, poCfg.Jira.JiraTimeDeltaMins)
+	ui.RenderUI(db, cl, installationType, *cfg.Jira.Jql, cfg.Jira.JiraTimeDeltaMins)
 }
diff --git a/internal/ui/cmds.go b/internal/ui/cmds.go
index e6a75b5..9e2e861 100644
--- a/internal/ui/cmds.go
+++ b/internal/ui/cmds.go
@@ -159,48 +159,52 @@ func updateSyncStatusForEntry(db *sql.DB, entry worklogEntry, index int) tea.Cmd
 		return logEntrySyncUpdated{
 			entry: entry,
 			index: index,
 			err:   err,
 		}
 	}
 }
 
 func fetchJIRAIssues(cl *jira.Client, jql string) tea.Cmd {
 	return func() tea.Msg {
-		jIssues, err := getIssues(cl, jql)
+		jIssues, statusCode, err := getIssues(cl, jql)
 		var issues []Issue
+		if err != nil {
+			return issuesFetchedFromJIRAMsg{issues, statusCode, err}
+		}
+
 		for _, issue := range jIssues {
 			var assignee string
 			var totalSecsSpent int
 			var status string
 			if issue.Fields != nil {
 				if issue.Fields.Assignee != nil {
-					assignee = issue.Fields.Assignee.Name
+					assignee = issue.Fields.Assignee.DisplayName
 				}
 
 				totalSecsSpent = issue.Fields.AggregateTimeSpent
 
 				if issue.Fields.Status != nil {
 					status = issue.Fields.Status.Name
 				}
 			}
 			issues = append(issues, Issue{
 				issueKey:        issue.Key,
 				issueType:       issue.Fields.Type.Name,
 				summary:         issue.Fields.Summary,
 				assignee:        assignee,
 				status:          status,
 				aggSecondsSpent: totalSecsSpent,
 				trackingActive:  false,
 			})
 		}
-		return issuesFetchedFromJIRAMsg{issues, err}
+		return issuesFetchedFromJIRAMsg{issues, statusCode, nil}
 	}
 }
 
 func syncWorklogWithJIRA(cl *jira.Client, entry worklogEntry, index int, timeDeltaMins int) tea.Cmd {
 	return func() tea.Msg {
 		err := addWLtoJira(cl, entry, timeDeltaMins)
 		return wlAddedOnJIRA{index, entry, err}
 	}
 }
 
diff --git a/internal/ui/initial.go b/internal/ui/initial.go
index 49c0fb7..36f573b 100644
--- a/internal/ui/initial.go
+++ b/internal/ui/initial.go
@@ -2,21 +2,21 @@ package ui
 
 import (
 	"database/sql"
 
 	jira "github.com/andygrunwald/go-jira/v2/onpremise"
 	"github.com/charmbracelet/bubbles/list"
 	"github.com/charmbracelet/bubbles/textinput"
 	"github.com/charmbracelet/lipgloss"
 )
 
-func InitialModel(db *sql.DB, jiraClient *jira.Client, jql string, jiraTimeDeltaMins int, debug bool) model {
+func InitialModel(db *sql.DB, jiraClient *jira.Client, installationType JiraInstallationType, jql string, jiraTimeDeltaMins int, debug bool) model {
 	var stackItems []list.Item
 	var worklogListItems []list.Item
 	var syncedWorklogListItems []list.Item
 
 	trackingInputs := make([]textinput.Model, 3)
 	trackingInputs[entryBeginTS] = textinput.New()
 	trackingInputs[entryBeginTS].Placeholder = "09:30"
 	trackingInputs[entryBeginTS].Focus()
 	trackingInputs[entryBeginTS].CharLimit = len(string(timeFormat))
 	trackingInputs[entryBeginTS].Width = 30
@@ -29,20 +29,21 @@ func InitialModel(db *sql.DB, jiraClient *jira.Client, jql string, jiraTimeDelta
 
 	trackingInputs[entryComment] = textinput.New()
 	trackingInputs[entryComment].Placeholder = "Your comment goes here"
 	trackingInputs[entryComment].Focus()
 	trackingInputs[entryComment].CharLimit = 255
 	trackingInputs[entryComment].Width = 60
 
 	m := model{
 		db:                db,
 		jiraClient:        jiraClient,
+		installationType:  installationType,
 		jql:               jql,
 		issueList:         list.New(stackItems, newItemDelegate(lipgloss.Color(issueListColor)), listWidth, 0),
 		issueMap:          make(map[string]*Issue),
 		issueIndexMap:     make(map[string]int),
 		worklogList:       list.New(worklogListItems, newItemDelegate(lipgloss.Color(worklogListColor)), listWidth, 0),
 		syncedWorklogList: list.New(syncedWorklogListItems, newItemDelegate(syncedWorklogListColor), listWidth, 0),
 		jiraTimeDeltaMins: jiraTimeDeltaMins,
 		showHelpIndicator: true,
 		trackingInputs:    trackingInputs,
 		debug:             debug,
diff --git a/internal/ui/jira.go b/internal/ui/jira.go
index c0cc823..e083715 100644
--- a/internal/ui/jira.go
+++ b/internal/ui/jira.go
@@ -1,27 +1,25 @@
 package ui
 
 import (
 	"context"
 	"errors"
 	"time"
 
 	jira "github.com/andygrunwald/go-jira/v2/onpremise"
 )
 
-var (
-	jiraRepliedWithEmptyWorklogErr = errors.New("JIRA replied with an empty worklog; something is probably wrong")
-)
+var jiraRepliedWithEmptyWorklogErr = errors.New("JIRA replied with an empty worklog; something is probably wrong")
 
-func getIssues(cl *jira.Client, jql string) ([]jira.Issue, error) {
-	issues, _, err := cl.Issue.Search(context.Background(), jql, nil)
-	return issues, err
+func getIssues(cl *jira.Client, jql string) ([]jira.Issue, int, error) {
+	issues, resp, err := cl.Issue.Search(context.Background(), jql, nil)
+	return issues, resp.StatusCode, err
 }
 
 func addWLtoJira(cl *jira.Client, entry worklogEntry, timeDeltaMins int) error {
 	start := entry.BeginTS
 
 	if timeDeltaMins != 0 {
 		start = start.Add(time.Minute * time.Duration(timeDeltaMins))
 	}
 
 	timeSpentSecs := int(entry.EndTS.Sub(entry.BeginTS).Seconds())
diff --git a/internal/ui/model.go b/internal/ui/model.go
index caf66a7..a17847f 100644
--- a/internal/ui/model.go
+++ b/internal/ui/model.go
@@ -4,20 +4,27 @@ import (
 	"database/sql"
 	"time"
 
 	jira "github.com/andygrunwald/go-jira/v2/onpremise"
 	"github.com/charmbracelet/bubbles/list"
 	"github.com/charmbracelet/bubbles/textinput"
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
 )
 
+type JiraInstallationType uint
+
+const (
+	OnPremiseInstallation JiraInstallationType = iota
+	CloudInstallation
+)
+
 type trackingStatus uint
 
 const (
 	trackingInactive trackingStatus = iota
 	trackingActive
 )
 
 type dBChange uint
 
 const (
@@ -56,20 +63,21 @@ const (
 	dayAndTimeFormat = "Mon, 15:04"
 	dateFormat       = "2006/01/02"
 	timeOnlyFormat   = "15:04"
 )
 
 type model struct {
 	activeView            stateView
 	lastView              stateView
 	db                    *sql.DB
 	jiraClient            *jira.Client
+	installationType      JiraInstallationType
 	jql                   string
 	issueList             list.Model
 	issueMap              map[string]*Issue
 	issueIndexMap         map[string]int
 	issuesFetched         bool
 	worklogList           list.Model
 	unsyncedWLCount       uint
 	unsyncedWLSecsSpent   int
 	syncedWorklogList     list.Model
 	activeIssueBeginTS    time.Time
diff --git a/internal/ui/msgs.go b/internal/ui/msgs.go
index 55ad2bb..323fee4 100644
--- a/internal/ui/msgs.go
+++ b/internal/ui/msgs.go
@@ -45,22 +45,23 @@ type logEntriesDeletedMsg struct {
 	err error
 }
 
 type logEntrySyncUpdated struct {
 	entry worklogEntry
 	index int
 	err   error
 }
 
 type issuesFetchedFromJIRAMsg struct {
-	issues []Issue
-	err    error
+	issues             []Issue
+	responseStatusCode int
+	err                error
 }
 
 type wlAddedOnJIRA struct {
 	index int
 	entry worklogEntry
 	err   error
 }
 
 type urlOpenedinBrowserMsg struct {
 	url string
diff --git a/internal/ui/render_helpers.go b/internal/ui/render_helpers.go
index 0c43774..3a1f2c4 100644
--- a/internal/ui/render_helpers.go
+++ b/internal/ui/render_helpers.go
@@ -4,21 +4,21 @@ import "fmt"
 
 func (issue *Issue) setDesc() {
 	// TODO: The padding here is a bit of a mess; make it more readable
 	var assignee string
 	var status string
 	var totalSecsSpent string
 
 	issueType := getIssueTypeStyle(issue.issueType).Render(issue.issueType)
 
 	if issue.assignee != "" {
-		assignee = assigneeStyle(issue.assignee).Render(RightPadTrim("@"+issue.assignee, int(listWidth/4)))
+		assignee = assigneeStyle(issue.assignee).Render(RightPadTrim(issue.assignee, int(listWidth/4)))
 	} else {
 		assignee = assigneeStyle(issue.assignee).Render(RightPadTrim("", int(listWidth/4)))
 	}
 
 	status = issueStatusStyle.Render(RightPadTrim(issue.status, int(listWidth/4)))
 
 	if issue.aggSecondsSpent > 0 {
 		totalSecsSpent = aggTimeSpentStyle.Render(humanizeDuration(issue.aggSecondsSpent))
 	}
 
diff --git a/internal/ui/styles.go b/internal/ui/styles.go
index 901421d..c054bcf 100644
--- a/internal/ui/styles.go
+++ b/internal/ui/styles.go
@@ -1,20 +1,22 @@
 package ui
 
 import (
-	"github.com/charmbracelet/lipgloss"
 	"hash/fnv"
+
+	"github.com/charmbracelet/lipgloss"
 )
 
 const (
 	defaultBackgroundColor  = "#282828"
 	issueListUnfetchedColor = "#928374"
+	failureColor            = "#fb4934"
 	issueListColor          = "#fe8019"
 	worklogListColor        = "#fabd2f"
 	syncedWorklogListColor  = "#b8bb26"
 	trackingColor           = "#fe8019"
 	unsyncedCountColor      = "#fabd2f"
 	activeIssueKeyColor     = "#d3869b"
 	activeIssueSummaryColor = "#8ec07c"
 	trackingBeganColor      = "#fabd2f"
 	issueStatusColor        = "#665c54"
 	toolNameColor           = "#b8bb26"
diff --git a/internal/ui/ui.go b/internal/ui/ui.go
index 85bfab9..67a85f4 100644
--- a/internal/ui/ui.go
+++ b/internal/ui/ui.go
@@ -2,27 +2,27 @@ package ui
 
 import (
 	"database/sql"
 	"fmt"
 	"os"
 
 	jira "github.com/andygrunwald/go-jira/v2/onpremise"
 	tea "github.com/charmbracelet/bubbletea"
 )
 
-func RenderUI(db *sql.DB, jiraClient *jira.Client, jql string, jiraTimeDeltaMins int) {
+func RenderUI(db *sql.DB, jiraClient *jira.Client, installationType JiraInstallationType, jql string, jiraTimeDeltaMins int) {
 	if len(os.Getenv("DEBUG_LOG")) > 0 {
 		f, err := tea.LogToFile("debug.log", "debug")
 		if err != nil {
 			fmt.Println("fatal:", err)
 			os.Exit(1)
 		}
 		defer f.Close()
 	}
 
 	debug := os.Getenv("DEBUG") == "true"
-	p := tea.NewProgram(InitialModel(db, jiraClient, jql, jiraTimeDeltaMins, debug), tea.WithAltScreen())
+	p := tea.NewProgram(InitialModel(db, jiraClient, installationType, jql, jiraTimeDeltaMins, debug), tea.WithAltScreen())
 	if _, err := p.Run(); err != nil {
 		fmt.Printf("Alas, there has been an error: %v", err)
 		os.Exit(1)
 	}
 }
diff --git a/internal/ui/update.go b/internal/ui/update.go
index 5b9c177..3f43604 100644
--- a/internal/ui/update.go
+++ b/internal/ui/update.go
@@ -432,23 +432,35 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			m.helpVP.HighPerformanceRendering = useHighPerformanceRenderer
 			m.helpVP.SetContent(helpText)
 			m.helpVPReady = true
 		} else {
 			m.helpVP.Height = m.terminalHeight - 7
 			m.helpVP.Width = w - 5
 
 		}
 	case issuesFetchedFromJIRAMsg:
 		if msg.err != nil {
-			message := "error fetching issues from JIRA: " + msg.err.Error()
-			m.message = message
-			m.messages = append(m.messages, message)
+			var remoteServerName string
+			if msg.responseStatusCode >= 400 && msg.responseStatusCode < 500 {
+				switch m.installationType {
+				case OnPremiseInstallation:
+					remoteServerName = "Your on-premise installation"
+				case CloudInstallation:
+					remoteServerName = "Atlassian Cloud"
+				}
+				m.message = fmt.Sprintf("%s returned a %d status codel, check if your configuration is correct", remoteServerName, msg.responseStatusCode)
+			} else {
+				m.message = fmt.Sprintf("error fetching issues from JIRA: %s", msg.err.Error())
+			}
+			m.messages = append(m.messages, m.message)
+			m.issueList.Title = "Failure"
+			m.issueList.Styles.Title = m.issueList.Styles.Title.Background(lipgloss.Color(failureColor))
 		} else {
 			issues := make([]list.Item, 0, len(msg.issues))
 			for i, issue := range msg.issues {
 				issue.setDesc()
 				issues = append(issues, &issue)
 				m.issueMap[issue.issueKey] = &issue
 				m.issueIndexMap[issue.issueKey] = i
 			}
 			m.issueList.SetItems(issues)
 			m.issueList.Title = "Issues"

@xederro
Copy link
Contributor Author

xederro commented Jan 12, 2025

Hi. No worries, I understand that sometimes it's hard to find a moment, especially around New Year's Eve.
I totally missed that adding InstallationType in TOML would be a better approach to this.
Nonetheless, I see that in diff that you provided there is no change in readme, to remove f.g. jira_cloud_token = "XXX" for TOML config example or jira-cloud-token='XXX' for command.
So something like:

[jira]
jira_url = "https://jira.company.com"

# allowed values: [cloud, onpremise]
# default "onpremise"
# installation_type = "onpremise"

# for on-premise installations
# you can use a JIRA PAT token here
# or if you use cloud instance
# use your API token:
jira_token = "XXX"

# required when you use cloud instance
# jira_username = "[email protected]"

# put whatever JQL you want to query for
jql = "assignee = currentUser() AND updatedDate >= -14d ORDER BY updatedDate DESC"

# I don't know how many people will find use for this.
# I need this, since the JIRA server I use runs 5 hours behind
# the actual time, for whatever reason 🤷
# jira_time_delta_mins = 300

and

punchout \
    [ -db-path='/path/to/punchout/db/file.db' ] \
    [ -jira-url='https://jira.company.com' ] \
    [ -jira-token='XXX' ] \
    [ -jira-username='[email protected]' ] \
    [ -jql='assignee = currentUser() AND updatedDate >= -14d ORDER BY updatedDate DESC' ] \
    [ -jira-time-delta-mins='300' ] \
    [ -config-file-path='/path/to/punchout/config/file.toml' ] \
    [ -list-config ]

Go ahead, feel free to push directly to my branch👌

@dhth dhth merged commit afbdaef into dhth:main Jan 12, 2025
6 checks passed
@dhth
Copy link
Owner

dhth commented Jan 12, 2025

Done, thanks for the effort! Hope the tool saves you time :)

@xederro xederro deleted the add-support-for-cloud-installations branch January 12, 2025 18:42
dhth added a commit that referenced this pull request Jan 12, 2025
* origin/main:
  feat: add support for cloud installations (#24)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants