From c5e3eed64b41f094529d5b04dde5df61b7cb691d Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Fri, 10 May 2024 11:05:42 +0100 Subject: [PATCH] Add Repository Overview widget --- docs/configuration.md | 38 +++++ docs/images/repository-overview-preview.png | Bin 0 -> 20408 bytes internal/assets/static/main.css | 3 +- internal/assets/templates.go | 1 + .../assets/templates/repository-overview.html | 44 ++++++ internal/feed/github.go | 131 ++++++++++++++++++ internal/widget/repository-overview.go | 52 +++++++ internal/widget/widget.go | 2 + 8 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 docs/images/repository-overview-preview.png create mode 100644 internal/assets/templates/repository-overview.html create mode 100644 internal/widget/repository-overview.go diff --git a/docs/configuration.md b/docs/configuration.md index 56fc8e0..c21be09 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,6 +14,7 @@ - [Weather](#weather) - [Monitor](#monitor) - [Releases](#releases) + - [Repository Overview](#repository-overview) - [Bookmarks](#bookmarks) - [Calendar](#calendar) - [Stocks](#stocks) @@ -791,6 +792,43 @@ The maximum number of releases to show. #### `collapse-after` How many releases are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse. +### Repository Overview +Display general information about a repository as well as a list of the latest open pull requests and issues. + +Example: + +```yaml +- type: repository-overview + repository: glanceapp/glance + pull-requests-limit: 5 + issues-limit: 3 +``` + +Preview: + +![](images/repository-overview-preview.png) + +#### Properties + +| Name | Type | Required | Default | +| ---- | ---- | -------- | ------- | +| repository | string | yes | | +| token | string | no | | +| pull-requests-limit | integer | no | 3 | +| issues-limit | integer | no | 3 | + +##### `repository` +The owner and repository name that will have their information displayed. + +##### `token` +Without authentication Github allows for up to 60 requests per hour. You can easily exceed this limit and start seeing errors if your cache time is low or you have many instances of this widget. To circumvent this you can [create a read only token from your Github account](https://github.com/settings/personal-access-tokens/new) and provide it here. + +##### `pull-requests-limit` +The maximum number of latest open pull requests to show. Set to `-1` to not show any. + +##### `issues-limit` +The maximum number of latest open issues to show. Set to `-1` to not show any. + ### Bookmarks Display a list of links which can be grouped. diff --git a/docs/images/repository-overview-preview.png b/docs/images/repository-overview-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..5c43db5485c1d5fdefb47fac9702a3d0d54b5cfe GIT binary patch literal 20408 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 literal 0 HcmV?d00001 diff --git a/internal/assets/static/main.css b/internal/assets/static/main.css index 833f07f..7aa9356 100644 --- a/internal/assets/static/main.css +++ b/internal/assets/static/main.css @@ -108,7 +108,7 @@ .list-gap-24 { --list-half-gap: 1.2rem; } .list > *:not(:first-child) { - margin-top: calc(var(--list-half-gap) * 2 + 1px); + margin-top: calc(var(--list-half-gap) * 2); } .list-with-separator > *:not(:first-child) { @@ -1104,6 +1104,7 @@ body { .text-right { text-align: right; } .text-center { text-align: center; } .text-elevate { margin-top: -0.2em; } +.text-compact { word-spacing: -0.18em; } .rtl { direction: rtl; } .shrink { flex-shrink: 1; } .shrink-0 { flex-shrink: 0; } diff --git a/internal/assets/templates.go b/internal/assets/templates.go index b349d52..0dde279 100644 --- a/internal/assets/templates.go +++ b/internal/assets/templates.go @@ -31,6 +31,7 @@ var ( MonitorTemplate = compileTemplate("monitor.html", "widget-base.html") TwitchGamesListTemplate = compileTemplate("twitch-games-list.html", "widget-base.html") TwitchChannelsTemplate = compileTemplate("twitch-channels.html", "widget-base.html") + RepositoryOverviewTemplate = compileTemplate("repository-overview.html", "widget-base.html") ) var globalTemplateFunctions = template.FuncMap{ diff --git a/internal/assets/templates/repository-overview.html b/internal/assets/templates/repository-overview.html new file mode 100644 index 0000000..9122a8e --- /dev/null +++ b/internal/assets/templates/repository-overview.html @@ -0,0 +1,44 @@ +{{ template "widget-base.html" . }} + +{{ define "widget-content" }} +{{ .RepositoryDetails.Name }} +
    +
  • {{ .RepositoryDetails.Stars | formatNumber }} stars
  • +
  • {{ .RepositoryDetails.Forks | formatNumber }} forks
  • +
+ +{{ if gt (len .RepositoryDetails.PullRequests) 0 }} +
+Open pull requests ({{ .RepositoryDetails.OpenPullRequests | formatNumber }} total) +
+
    + {{ range .RepositoryDetails.PullRequests }} +
  • {{ .CreatedAt | relativeTime }}
  • + {{ end }} +
+
    + {{ range .RepositoryDetails.PullRequests }} +
  • {{ .Title }}
  • + {{ end }} +
+
+{{ end }} + +{{ if gt (len .RepositoryDetails.Issues) 0 }} +
+Open issues ({{ .RepositoryDetails.OpenIssues | formatNumber }} total) +
+
    + {{ range .RepositoryDetails.Issues }} +
  • {{ .CreatedAt | relativeTime }}
  • + {{ end }} +
+
    + {{ range .RepositoryDetails.Issues }} +
  • {{ .Title }}
  • + {{ end }} +
+
+{{ end }} + +{{ end }} diff --git a/internal/feed/github.go b/internal/feed/github.go index 4a34182..43a2459 100644 --- a/internal/feed/github.go +++ b/internal/feed/github.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "net/http" + "sync" "time" ) @@ -115,3 +116,133 @@ func FetchLatestReleasesFromGithub(repositories []string, token string) (AppRele return appReleases, nil } + +type GithubTicket struct { + Number int + CreatedAt time.Time + Title string +} + +type RepositoryDetails struct { + Name string + Stars int + Forks int + OpenPullRequests int + PullRequests []GithubTicket + OpenIssues int + Issues []GithubTicket +} + +type githubRepositoryDetailsResponseJson struct { + Name string `json:"full_name"` + Stars int `json:"stargazers_count"` + Forks int `json:"forks_count"` +} + +type githubTicketResponseJson struct { + Count int `json:"total_count"` + Tickets []struct { + Number int `json:"number"` + CreatedAt string `json:"created_at"` + Title string `json:"title"` + } `json:"items"` +} + +func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs int, maxIssues int) (RepositoryDetails, error) { + repositoryRequest, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s", repository), nil) + + if err != nil { + return RepositoryDetails{}, fmt.Errorf("%w: could not create request with repository: %v", ErrNoContent, err) + } + + PRsRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:pr+is:open+repo:%s&per_page=%d", repository, maxPRs), nil) + issuesRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:issue+is:open+repo:%s&per_page=%d", repository, maxIssues), nil) + + if token != "" { + token = fmt.Sprintf("Bearer %s", token) + repositoryRequest.Header.Add("Authorization", token) + PRsRequest.Header.Add("Authorization", token) + issuesRequest.Header.Add("Authorization", token) + } + + var detailsResponse githubRepositoryDetailsResponseJson + var detailsErr error + var PRsResponse githubTicketResponseJson + var PRsErr error + var issuesResponse githubTicketResponseJson + var issuesErr error + var wg sync.WaitGroup + + wg.Add(1) + go (func() { + defer wg.Done() + detailsResponse, detailsErr = decodeJsonFromRequest[githubRepositoryDetailsResponseJson](defaultClient, repositoryRequest) + })() + + if maxPRs > 0 { + wg.Add(1) + go (func() { + defer wg.Done() + PRsResponse, PRsErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, PRsRequest) + })() + } + + if maxIssues > 0 { + wg.Add(1) + go (func() { + defer wg.Done() + issuesResponse, issuesErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, issuesRequest) + })() + } + + wg.Wait() + + if detailsErr != nil { + return RepositoryDetails{}, fmt.Errorf("%w: could not get repository details: %s", ErrNoContent, detailsErr) + } + + details := RepositoryDetails{ + Name: detailsResponse.Name, + Stars: detailsResponse.Stars, + Forks: detailsResponse.Forks, + PullRequests: make([]GithubTicket, 0, len(PRsResponse.Tickets)), + Issues: make([]GithubTicket, 0, len(issuesResponse.Tickets)), + } + + err = nil + + if maxPRs > 0 { + if PRsErr != nil { + err = fmt.Errorf("%w: could not get PRs: %s", ErrPartialContent, PRsErr) + } else { + details.OpenPullRequests = PRsResponse.Count + + for i := range PRsResponse.Tickets { + details.PullRequests = append(details.PullRequests, GithubTicket{ + Number: PRsResponse.Tickets[i].Number, + CreatedAt: parseGithubTime(PRsResponse.Tickets[i].CreatedAt), + Title: PRsResponse.Tickets[i].Title, + }) + } + } + } + + if maxIssues > 0 { + if issuesErr != nil { + // TODO: fix, overwriting the previous error + err = fmt.Errorf("%w: could not get issues: %s", ErrPartialContent, issuesErr) + } else { + details.OpenIssues = issuesResponse.Count + + for i := range issuesResponse.Tickets { + details.Issues = append(details.Issues, GithubTicket{ + Number: issuesResponse.Tickets[i].Number, + CreatedAt: parseGithubTime(issuesResponse.Tickets[i].CreatedAt), + Title: issuesResponse.Tickets[i].Title, + }) + } + } + } + + return details, err +} diff --git a/internal/widget/repository-overview.go b/internal/widget/repository-overview.go new file mode 100644 index 0000000..8c50b31 --- /dev/null +++ b/internal/widget/repository-overview.go @@ -0,0 +1,52 @@ +package widget + +import ( + "context" + "html/template" + "time" + + "github.com/glanceapp/glance/internal/assets" + "github.com/glanceapp/glance/internal/feed" +) + +type RepositoryOverview struct { + widgetBase `yaml:",inline"` + RequestedRepository string `yaml:"repository"` + Token OptionalEnvString `yaml:"token"` + PullRequestsLimit int `yaml:"pull-requests-limit"` + IssuesLimit int `yaml:"issues-limit"` + RepositoryDetails feed.RepositoryDetails +} + +func (widget *RepositoryOverview) Initialize() error { + widget.withTitle("Repository").withCacheDuration(1 * time.Hour) + + if widget.PullRequestsLimit == 0 || widget.PullRequestsLimit < -1 { + widget.PullRequestsLimit = 3 + } + + if widget.IssuesLimit == 0 || widget.IssuesLimit < -1 { + widget.IssuesLimit = 3 + } + + return nil +} + +func (widget *RepositoryOverview) Update(ctx context.Context) { + details, err := feed.FetchRepositoryDetailsFromGithub( + widget.RequestedRepository, + string(widget.Token), + widget.PullRequestsLimit, + widget.IssuesLimit, + ) + + if !widget.canContinueUpdateAfterHandlingErr(err) { + return + } + + widget.RepositoryDetails = details +} + +func (widget *RepositoryOverview) Render() template.HTML { + return widget.render(widget, assets.RepositoryOverviewTemplate) +} diff --git a/internal/widget/widget.go b/internal/widget/widget.go index 367d822..48ebb4c 100644 --- a/internal/widget/widget.go +++ b/internal/widget/widget.go @@ -43,6 +43,8 @@ func New(widgetType string) (Widget, error) { return &TwitchGames{}, nil case "twitch-channels": return &TwitchChannels{}, nil + case "repository-overview": + return &RepositoryOverview{}, nil default: return nil, fmt.Errorf("unknown widget type: %s", widgetType) }