mirror of
https://github.com/glanceapp/glance.git
synced 2024-12-14 17:13:11 +03:00
Add Repository Overview widget
This commit is contained in:
parent
7dd1cca65d
commit
c5e3eed64b
@ -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.
|
||||
|
||||
|
BIN
docs/images/repository-overview-preview.png
Normal file
BIN
docs/images/repository-overview-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@ -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; }
|
||||
|
@ -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{
|
||||
|
44
internal/assets/templates/repository-overview.html
Normal file
44
internal/assets/templates/repository-overview.html
Normal file
@ -0,0 +1,44 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<a class="size-h4 color-highlight" href="https://github.com/{{ $.RepositoryDetails.Name }}" target="_blank" rel="noreferrer">{{ .RepositoryDetails.Name }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
<li>{{ .RepositoryDetails.Stars | formatNumber }} stars</li>
|
||||
<li>{{ .RepositoryDetails.Forks | formatNumber }} forks</li>
|
||||
</ul>
|
||||
|
||||
{{ if gt (len .RepositoryDetails.PullRequests) 0 }}
|
||||
<hr class="margin-block-10">
|
||||
<a class="text-compact" href="https://github.com/{{ $.RepositoryDetails.Name }}/pulls" target="_blank" rel="noreferrer">Open pull requests ({{ .RepositoryDetails.OpenPullRequests | formatNumber }} total)</a>
|
||||
<div class="flex gap-7 size-h5 margin-top-3">
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .RepositoryDetails.PullRequests }}
|
||||
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul class="list list-gap-2 min-width-0">
|
||||
{{ range .RepositoryDetails.PullRequests }}
|
||||
<li><a class="color-primary-if-not-visited text-truncate block" title="{{ .Title }}" target="_blank" rel="noreferrer" href="https://github.com/{{ $.RepositoryDetails.Name }}/pull/{{ .Number }}">{{ .Title }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if gt (len .RepositoryDetails.Issues) 0 }}
|
||||
<hr class="margin-block-10">
|
||||
<a class="text-compact" href="https://github.com/{{ $.RepositoryDetails.Name }}/issues" target="_blank" rel="noreferrer">Open issues ({{ .RepositoryDetails.OpenIssues | formatNumber }} total)</a>
|
||||
<div class="flex gap-7 size-h5 margin-top-3">
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .RepositoryDetails.Issues }}
|
||||
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul class="list list-gap-2 min-width-0">
|
||||
{{ range .RepositoryDetails.Issues }}
|
||||
<li><a class="color-primary-if-not-visited text-truncate block" title="{{ .Title }}" target="_blank" rel="noreferrer" href="https://github.com/{{ $.RepositoryDetails.Name }}/issues/{{ .Number }}">{{ .Title }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
@ -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
|
||||
}
|
||||
|
52
internal/widget/repository-overview.go
Normal file
52
internal/widget/repository-overview.go
Normal file
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user