feat(ui): add quota overview (#6602)
Add UI to the quota feature to see what quotas applies to you and if you're exceeding any quota, it's designed to be a general size overview although it's exclusively filled with quota features for now. There's also no UI to see what item is actually taking in the most size. Purely an quota overview. Screenshots:   With inspiration from concept by 0ko:  Co-authored-by: Otto Richter <git@otto.splvs.net> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6602 Reviewed-by: Otto <otto@codeberg.org> Co-authored-by: Gusted <postmaster@gusted.xyz> Co-committed-by: Gusted <postmaster@gusted.xyz>
This commit is contained in:
parent
6dad457552
commit
77a1af5ab8
24 changed files with 348 additions and 33 deletions
|
@ -7,7 +7,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func EvaluateDefault(used Used, forSubject LimitSubject) bool {
|
||||
func EvaluateDefault(used Used, forSubject LimitSubject) (bool, int64) {
|
||||
groups := GroupList{
|
||||
&Group{
|
||||
Name: "builtin-default-group",
|
||||
|
|
|
@ -5,6 +5,7 @@ package quota
|
|||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
@ -199,15 +200,20 @@ var affectsMap = map[LimitSubject]LimitSubjects{
|
|||
},
|
||||
}
|
||||
|
||||
func (g *Group) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
||||
// Evaluate returns whether the size used is acceptable for the topic if a rule
|
||||
// was found, and returns the smallest limit of all applicable rules or the
|
||||
// first limit found to be unacceptable for the size used.
|
||||
func (g *Group) Evaluate(used Used, forSubject LimitSubject) (bool, bool, int64) {
|
||||
var found bool
|
||||
foundLimit := int64(math.MaxInt64)
|
||||
for _, rule := range g.Rules {
|
||||
ok, has := rule.Evaluate(used, forSubject)
|
||||
if has {
|
||||
found = true
|
||||
if !ok {
|
||||
return false, true
|
||||
return false, true, rule.Limit
|
||||
}
|
||||
found = true
|
||||
foundLimit = min(foundLimit, rule.Limit)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,32 +222,35 @@ func (g *Group) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
|||
// subjects below
|
||||
|
||||
for _, subject := range affectsMap[forSubject] {
|
||||
ok, has := g.Evaluate(used, subject)
|
||||
ok, has, limit := g.Evaluate(used, subject)
|
||||
if has {
|
||||
found = true
|
||||
if !ok {
|
||||
return false, true
|
||||
return false, true, limit
|
||||
}
|
||||
found = true
|
||||
foundLimit = min(foundLimit, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, found
|
||||
return true, found, foundLimit
|
||||
}
|
||||
|
||||
func (gl *GroupList) Evaluate(used Used, forSubject LimitSubject) bool {
|
||||
// Evaluate returns if the used size is acceptable for the subject and the
|
||||
// lowest limit that is acceptable for the subject.
|
||||
func (gl *GroupList) Evaluate(used Used, forSubject LimitSubject) (bool, int64) {
|
||||
// If there are no groups, use the configured defaults:
|
||||
if gl == nil || len(*gl) == 0 {
|
||||
return EvaluateDefault(used, forSubject)
|
||||
}
|
||||
|
||||
for _, group := range *gl {
|
||||
ok, has := group.Evaluate(used, forSubject)
|
||||
ok, has, limit := group.Evaluate(used, forSubject)
|
||||
if has && ok {
|
||||
return true
|
||||
return true, limit
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func GetGroupByName(ctx context.Context, name string) (*Group, error) {
|
||||
|
|
|
@ -32,5 +32,6 @@ func EvaluateForUser(ctx context.Context, userID int64, subject LimitSubject) (b
|
|||
return false, err
|
||||
}
|
||||
|
||||
return groups.Evaluate(*used, subject), nil
|
||||
acceptable, _ := groups.Evaluate(*used, subject)
|
||||
return acceptable, nil
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package quota_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
quota_model "code.gitea.io/gitea/models/quota"
|
||||
|
@ -36,9 +37,10 @@ func TestQuotaGroupAllRulesMustPass(t *testing.T) {
|
|||
|
||||
// Within a group, *all* rules must pass. Thus, if we have a deny-all rule,
|
||||
// and an unlimited rule, that will always fail.
|
||||
ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, has)
|
||||
assert.False(t, ok)
|
||||
assert.EqualValues(t, 0, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupRuleScenario1(t *testing.T) {
|
||||
|
@ -66,21 +68,25 @@ func TestQuotaGroupRuleScenario1(t *testing.T) {
|
|||
used.Size.Assets.Packages.All = 256
|
||||
used.Size.Git.LFS = 16
|
||||
|
||||
ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeAssetsAttachmentsReleases)
|
||||
ok, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeAssetsAttachmentsReleases)
|
||||
assert.True(t, has, "size:assets:attachments:releases is covered")
|
||||
assert.True(t, ok, "size:assets:attachments:releases passes")
|
||||
assert.EqualValues(t, 1024, limit)
|
||||
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||
assert.True(t, has, "size:assets:packages:all is covered")
|
||||
assert.True(t, ok, "size:assets:packages:all passes")
|
||||
assert.EqualValues(t, 1024, limit)
|
||||
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||
assert.True(t, has, "size:git:lfs is covered")
|
||||
assert.False(t, ok, "size:git:lfs fails")
|
||||
assert.EqualValues(t, 0, limit)
|
||||
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, has, "size:all is covered")
|
||||
assert.False(t, ok, "size:all fails")
|
||||
assert.EqualValues(t, 0, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupRuleCombination(t *testing.T) {
|
||||
|
@ -109,23 +115,27 @@ func TestQuotaGroupRuleCombination(t *testing.T) {
|
|||
}
|
||||
|
||||
// Git LFS isn't covered by any rule
|
||||
_, has := group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||
_, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||
assert.False(t, has)
|
||||
assert.EqualValues(t, math.MaxInt, limit)
|
||||
|
||||
// repos:all is covered, and is passing
|
||||
ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeReposAll)
|
||||
ok, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeReposAll)
|
||||
assert.True(t, has)
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, 4096, limit)
|
||||
|
||||
// packages:all is covered, and is failing
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||
assert.True(t, has)
|
||||
assert.False(t, ok)
|
||||
assert.EqualValues(t, 0, limit)
|
||||
|
||||
// size:all is covered, and is failing (due to packages:all being over quota)
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, has, "size:all should be covered")
|
||||
assert.False(t, ok, "size:all should fail")
|
||||
assert.EqualValues(t, 0, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupListsRequireOnlyOnePassing(t *testing.T) {
|
||||
|
@ -159,8 +169,9 @@ func TestQuotaGroupListsRequireOnlyOnePassing(t *testing.T) {
|
|||
used.Size.Repos.Public = 1024
|
||||
|
||||
// In a group list, if any group passes, the entire evaluation passes.
|
||||
ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, limit := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, -1, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupListAllFailing(t *testing.T) {
|
||||
|
@ -193,8 +204,9 @@ func TestQuotaGroupListAllFailing(t *testing.T) {
|
|||
used := quota_model.Used{}
|
||||
used.Size.Repos.Public = 2048
|
||||
|
||||
ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, limit := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.False(t, ok)
|
||||
assert.EqualValues(t, 0, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupListEmpty(t *testing.T) {
|
||||
|
@ -203,6 +215,7 @@ func TestQuotaGroupListEmpty(t *testing.T) {
|
|||
used := quota_model.Used{}
|
||||
used.Size.Repos.Public = 2048
|
||||
|
||||
ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, limit := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, -1, limit)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,22 @@ func (r *Rule) TableName() string {
|
|||
return "quota_rule"
|
||||
}
|
||||
|
||||
func (r Rule) Acceptable(used Used) bool {
|
||||
if r.Limit == -1 {
|
||||
return true
|
||||
}
|
||||
|
||||
return r.Sum(used) <= r.Limit
|
||||
}
|
||||
|
||||
func (r Rule) Sum(used Used) int64 {
|
||||
var sum int64
|
||||
for _, subject := range r.Subjects {
|
||||
sum += used.CalculateFor(subject)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (r Rule) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
||||
// If there's no limit, short circuit out
|
||||
if r.Limit == -1 {
|
||||
|
@ -31,11 +47,7 @@ func (r Rule) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
|||
return false, false
|
||||
}
|
||||
|
||||
var sum int64
|
||||
for _, subject := range r.Subjects {
|
||||
sum += used.CalculateFor(subject)
|
||||
}
|
||||
return sum <= r.Limit, true
|
||||
return r.Sum(used) <= r.Limit, true
|
||||
}
|
||||
|
||||
func (r *Rule) Edit(ctx context.Context, limit *int64, subjects *LimitSubjects) (*Rule, error) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue