loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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:
![](https://codeberg.org/attachments/9f7480f2-4c31-4d70-8aec-61db79282a1e)
![](https://codeberg.org/attachments/0bd45bf3-28c5-47bf-8fff-c4ae9f38cb28)

With inspiration from concept by 0ko:
![](https://codeberg.org/attachments/b8154a52-6fba-42fc-a4a8-b3ab1527fb33)

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>

authored by

Gusted
Otto Richter
Gusted
and committed by
0ko
77a1af5a 6dad4575

+348 -33
+1 -1
models/quota/default.go
··· 7 7 "code.gitea.io/gitea/modules/setting" 8 8 ) 9 9 10 - func EvaluateDefault(used Used, forSubject LimitSubject) bool { 10 + func EvaluateDefault(used Used, forSubject LimitSubject) (bool, int64) { 11 11 groups := GroupList{ 12 12 &Group{ 13 13 Name: "builtin-default-group",
+20 -11
models/quota/group.go
··· 5 5 6 6 import ( 7 7 "context" 8 + "math" 8 9 9 10 "code.gitea.io/gitea/models/db" 10 11 user_model "code.gitea.io/gitea/models/user" ··· 199 200 }, 200 201 } 201 202 202 - func (g *Group) Evaluate(used Used, forSubject LimitSubject) (bool, bool) { 203 + // Evaluate returns whether the size used is acceptable for the topic if a rule 204 + // was found, and returns the smallest limit of all applicable rules or the 205 + // first limit found to be unacceptable for the size used. 206 + func (g *Group) Evaluate(used Used, forSubject LimitSubject) (bool, bool, int64) { 203 207 var found bool 208 + foundLimit := int64(math.MaxInt64) 204 209 for _, rule := range g.Rules { 205 210 ok, has := rule.Evaluate(used, forSubject) 206 211 if has { 207 - found = true 208 212 if !ok { 209 - return false, true 213 + return false, true, rule.Limit 210 214 } 215 + found = true 216 + foundLimit = min(foundLimit, rule.Limit) 211 217 } 212 218 } 213 219 ··· 216 222 // subjects below 217 223 218 224 for _, subject := range affectsMap[forSubject] { 219 - ok, has := g.Evaluate(used, subject) 225 + ok, has, limit := g.Evaluate(used, subject) 220 226 if has { 221 - found = true 222 227 if !ok { 223 - return false, true 228 + return false, true, limit 224 229 } 230 + found = true 231 + foundLimit = min(foundLimit, limit) 225 232 } 226 233 } 227 234 } 228 235 229 - return true, found 236 + return true, found, foundLimit 230 237 } 231 238 232 - func (gl *GroupList) Evaluate(used Used, forSubject LimitSubject) bool { 239 + // Evaluate returns if the used size is acceptable for the subject and the 240 + // lowest limit that is acceptable for the subject. 241 + func (gl *GroupList) Evaluate(used Used, forSubject LimitSubject) (bool, int64) { 233 242 // If there are no groups, use the configured defaults: 234 243 if gl == nil || len(*gl) == 0 { 235 244 return EvaluateDefault(used, forSubject) 236 245 } 237 246 238 247 for _, group := range *gl { 239 - ok, has := group.Evaluate(used, forSubject) 248 + ok, has, limit := group.Evaluate(used, forSubject) 240 249 if has && ok { 241 - return true 250 + return true, limit 242 251 } 243 252 } 244 - return false 253 + return false, 0 245 254 } 246 255 247 256 func GetGroupByName(ctx context.Context, name string) (*Group, error) {
+2 -1
models/quota/quota.go
··· 32 32 return false, err 33 33 } 34 34 35 - return groups.Evaluate(*used, subject), nil 35 + acceptable, _ := groups.Evaluate(*used, subject) 36 + return acceptable, nil 36 37 }
+25 -12
models/quota/quota_group_test.go
··· 4 4 package quota_test 5 5 6 6 import ( 7 + "math" 7 8 "testing" 8 9 9 10 quota_model "code.gitea.io/gitea/models/quota" ··· 36 37 37 38 // Within a group, *all* rules must pass. Thus, if we have a deny-all rule, 38 39 // and an unlimited rule, that will always fail. 39 - ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeAll) 40 + ok, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeAll) 40 41 assert.True(t, has) 41 42 assert.False(t, ok) 43 + assert.EqualValues(t, 0, limit) 42 44 } 43 45 44 46 func TestQuotaGroupRuleScenario1(t *testing.T) { ··· 66 68 used.Size.Assets.Packages.All = 256 67 69 used.Size.Git.LFS = 16 68 70 69 - ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeAssetsAttachmentsReleases) 71 + ok, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeAssetsAttachmentsReleases) 70 72 assert.True(t, has, "size:assets:attachments:releases is covered") 71 73 assert.True(t, ok, "size:assets:attachments:releases passes") 74 + assert.EqualValues(t, 1024, limit) 72 75 73 - ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll) 76 + ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll) 74 77 assert.True(t, has, "size:assets:packages:all is covered") 75 78 assert.True(t, ok, "size:assets:packages:all passes") 79 + assert.EqualValues(t, 1024, limit) 76 80 77 - ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS) 81 + ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS) 78 82 assert.True(t, has, "size:git:lfs is covered") 79 83 assert.False(t, ok, "size:git:lfs fails") 84 + assert.EqualValues(t, 0, limit) 80 85 81 - ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAll) 86 + ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAll) 82 87 assert.True(t, has, "size:all is covered") 83 88 assert.False(t, ok, "size:all fails") 89 + assert.EqualValues(t, 0, limit) 84 90 } 85 91 86 92 func TestQuotaGroupRuleCombination(t *testing.T) { ··· 109 115 } 110 116 111 117 // Git LFS isn't covered by any rule 112 - _, has := group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS) 118 + _, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS) 113 119 assert.False(t, has) 120 + assert.EqualValues(t, math.MaxInt, limit) 114 121 115 122 // repos:all is covered, and is passing 116 - ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeReposAll) 123 + ok, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeReposAll) 117 124 assert.True(t, has) 118 125 assert.True(t, ok) 126 + assert.EqualValues(t, 4096, limit) 119 127 120 128 // packages:all is covered, and is failing 121 - ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll) 129 + ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll) 122 130 assert.True(t, has) 123 131 assert.False(t, ok) 132 + assert.EqualValues(t, 0, limit) 124 133 125 134 // size:all is covered, and is failing (due to packages:all being over quota) 126 - ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAll) 135 + ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAll) 127 136 assert.True(t, has, "size:all should be covered") 128 137 assert.False(t, ok, "size:all should fail") 138 + assert.EqualValues(t, 0, limit) 129 139 } 130 140 131 141 func TestQuotaGroupListsRequireOnlyOnePassing(t *testing.T) { ··· 159 169 used.Size.Repos.Public = 1024 160 170 161 171 // In a group list, if any group passes, the entire evaluation passes. 162 - ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll) 172 + ok, limit := groups.Evaluate(used, quota_model.LimitSubjectSizeAll) 163 173 assert.True(t, ok) 174 + assert.EqualValues(t, -1, limit) 164 175 } 165 176 166 177 func TestQuotaGroupListAllFailing(t *testing.T) { ··· 193 204 used := quota_model.Used{} 194 205 used.Size.Repos.Public = 2048 195 206 196 - ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll) 207 + ok, limit := groups.Evaluate(used, quota_model.LimitSubjectSizeAll) 197 208 assert.False(t, ok) 209 + assert.EqualValues(t, 0, limit) 198 210 } 199 211 200 212 func TestQuotaGroupListEmpty(t *testing.T) { ··· 203 215 used := quota_model.Used{} 204 216 used.Size.Repos.Public = 2048 205 217 206 - ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll) 218 + ok, limit := groups.Evaluate(used, quota_model.LimitSubjectSizeAll) 207 219 assert.True(t, ok) 220 + assert.EqualValues(t, -1, limit) 208 221 }
+17 -5
models/quota/rule.go
··· 20 20 return "quota_rule" 21 21 } 22 22 23 + func (r Rule) Acceptable(used Used) bool { 24 + if r.Limit == -1 { 25 + return true 26 + } 27 + 28 + return r.Sum(used) <= r.Limit 29 + } 30 + 31 + func (r Rule) Sum(used Used) int64 { 32 + var sum int64 33 + for _, subject := range r.Subjects { 34 + sum += used.CalculateFor(subject) 35 + } 36 + return sum 37 + } 38 + 23 39 func (r Rule) Evaluate(used Used, forSubject LimitSubject) (bool, bool) { 24 40 // If there's no limit, short circuit out 25 41 if r.Limit == -1 { ··· 31 47 return false, false 32 48 } 33 49 34 - var sum int64 35 - for _, subject := range r.Subjects { 36 - sum += used.CalculateFor(subject) 37 - } 38 - return sum <= r.Limit, true 50 + return r.Sum(used) <= r.Limit, true 39 51 } 40 52 41 53 func (r *Rule) Edit(ctx context.Context, limit *int64, subjects *LimitSubjects) (*Rule, error) {
+21
options/locale/locale_en-US.ini
··· 749 749 uid = UID 750 750 webauthn = Two-factor authentication (Security keys) 751 751 blocked_users = Blocked users 752 + storage_overview = Storage overview 753 + quota = Quota 752 754 753 755 public_profile = Public profile 754 756 biography_placeholder = Tell others a little bit about yourself! (Markdown is supported) ··· 1053 1055 user_unblock_success = The user has been unblocked successfully. 1054 1056 user_block_success = The user has been blocked successfully. 1055 1057 user_block_yourself = You cannot block yourself. 1058 + 1059 + quota.applies_to_user = The following quota rules apply to your account 1060 + quota.applies_to_org = The following quota rules apply to this organisation 1061 + quota.rule.exceeded = Exceeded 1062 + quota.rule.exceeded.helper = The total size of objects for this rule has exceeded the quota. 1063 + quota.rule.no_limit = Unlimited 1064 + quota.sizes.all = All 1065 + quota.sizes.repos.all = Repositories 1066 + quota.sizes.repos.public = Public repositories 1067 + quota.sizes.repos.private = Private repositories 1068 + quota.sizes.git.all = Git content 1069 + quota.sizes.git.lfs = Git LFS 1070 + quota.sizes.assets.all = Assets 1071 + quota.sizes.assets.attachments.all = Attachments 1072 + quota.sizes.assets.attachments.issues = Issue attachments 1073 + quota.sizes.assets.attachments.releases = Release attachments 1074 + quota.sizes.assets.artifacts = Artifacts 1075 + quota.sizes.assets.packages.all = Packages 1076 + quota.sizes.wiki = Wiki 1056 1077 1057 1078 [repo] 1058 1079 rss.must_be_on_branch = You must be on a branch to have an RSS feed.
+20
routers/web/org/setting/storage_overview.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package setting 5 + 6 + import ( 7 + "code.gitea.io/gitea/modules/base" 8 + "code.gitea.io/gitea/routers/web/shared" 9 + "code.gitea.io/gitea/services/context" 10 + ) 11 + 12 + const ( 13 + tplSettingsStorageOverview base.TplName = "org/settings/storage_overview" 14 + ) 15 + 16 + // StorageOverview render a size overview of the organization, as well as relevant 17 + // quota limits of the instance. 18 + func StorageOverview(ctx *context.Context) { 19 + shared.StorageOverview(ctx, ctx.Org.Organization.ID, tplSettingsStorageOverview) 20 + }
+90
routers/web/shared/storage_overview.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package shared 5 + 6 + import ( 7 + "html/template" 8 + "net/http" 9 + 10 + quota_model "code.gitea.io/gitea/models/quota" 11 + "code.gitea.io/gitea/modules/base" 12 + "code.gitea.io/gitea/modules/setting" 13 + "code.gitea.io/gitea/services/context" 14 + ) 15 + 16 + // StorageOverview render a size overview of the user, as well as relevant 17 + // quota limits of the instance. 18 + func StorageOverview(ctx *context.Context, userID int64, tpl base.TplName) { 19 + if !setting.Quota.Enabled { 20 + ctx.NotFound("MustEnableQuota", nil) 21 + } 22 + ctx.Data["Title"] = ctx.Tr("settings.storage_overview") 23 + ctx.Data["PageIsStorageOverview"] = true 24 + 25 + ctx.Data["Color"] = func(subject quota_model.LimitSubject) float64 { 26 + return float64(subject) * 137.50776405003785 // Golden angle. 27 + } 28 + 29 + ctx.Data["PrettySubject"] = func(subject quota_model.LimitSubject) template.HTML { 30 + switch subject { 31 + case quota_model.LimitSubjectSizeAll: 32 + return ctx.Locale.Tr("settings.quota.sizes.all") 33 + case quota_model.LimitSubjectSizeReposAll: 34 + return ctx.Locale.Tr("settings.quota.sizes.repos.all") 35 + case quota_model.LimitSubjectSizeReposPublic: 36 + return ctx.Locale.Tr("settings.quota.sizes.repos.public") 37 + case quota_model.LimitSubjectSizeReposPrivate: 38 + return ctx.Locale.Tr("settings.quota.sizes.repos.private") 39 + case quota_model.LimitSubjectSizeGitAll: 40 + return ctx.Locale.Tr("settings.quota.sizes.git.all") 41 + case quota_model.LimitSubjectSizeGitLFS: 42 + return ctx.Locale.Tr("settings.quota.sizes.git.lfs") 43 + case quota_model.LimitSubjectSizeAssetsAll: 44 + return ctx.Locale.Tr("settings.quota.sizes.assets.all") 45 + case quota_model.LimitSubjectSizeAssetsAttachmentsAll: 46 + return ctx.Locale.Tr("settings.quota.sizes.assets.attachments.all") 47 + case quota_model.LimitSubjectSizeAssetsAttachmentsIssues: 48 + return ctx.Locale.Tr("settings.quota.sizes.assets.attachments.issues") 49 + case quota_model.LimitSubjectSizeAssetsAttachmentsReleases: 50 + return ctx.Locale.Tr("settings.quota.sizes.assets.attachments.releases") 51 + case quota_model.LimitSubjectSizeAssetsArtifacts: 52 + return ctx.Locale.Tr("settings.quota.sizes.assets.artifacts") 53 + case quota_model.LimitSubjectSizeAssetsPackagesAll: 54 + return ctx.Locale.Tr("settings.quota.sizes.assets.packages.all") 55 + case quota_model.LimitSubjectSizeWiki: 56 + return ctx.Locale.Tr("settings.quota.sizes.wiki") 57 + default: 58 + panic("unrecognized subject: " + subject.String()) 59 + } 60 + } 61 + 62 + sizeUsed, err := quota_model.GetUsedForUser(ctx, userID) 63 + if err != nil { 64 + ctx.ServerError("GetUsedForUser", err) 65 + return 66 + } 67 + ctx.Data["SizeUsed"] = sizeUsed 68 + 69 + quotaGroups, err := quota_model.GetGroupsForUser(ctx, userID) 70 + if err != nil { 71 + ctx.ServerError("GetGroupsForUser", err) 72 + return 73 + } 74 + if len(quotaGroups) == 0 { 75 + quotaGroups = append(quotaGroups, &quota_model.Group{ 76 + Name: "Global quota", 77 + Rules: []quota_model.Rule{ 78 + { 79 + Name: "Default", 80 + Limit: setting.Quota.Default.Total, 81 + Subjects: quota_model.LimitSubjects{quota_model.LimitSubjectSizeAll}, 82 + }, 83 + }, 84 + }, 85 + ) 86 + } 87 + ctx.Data["QuotaGroups"] = quotaGroups 88 + 89 + ctx.HTML(http.StatusOK, tpl) 90 + }
+20
routers/web/user/setting/storage_overview.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package setting 5 + 6 + import ( 7 + "code.gitea.io/gitea/modules/base" 8 + "code.gitea.io/gitea/routers/web/shared" 9 + "code.gitea.io/gitea/services/context" 10 + ) 11 + 12 + const ( 13 + tplSettingsStorageOverview base.TplName = "user/settings/storage_overview" 14 + ) 15 + 16 + // StorageOverview render a size overview of the user, as well as relevant 17 + // quota limits of the instance. 18 + func StorageOverview(ctx *context.Context) { 19 + shared.StorageOverview(ctx, ctx.Doer.ID, tplSettingsStorageOverview) 20 + }
+4 -2
routers/web/web.go
··· 644 644 m.Get("", user_setting.BlockedUsers) 645 645 m.Post("/unblock", user_setting.UnblockUser) 646 646 }) 647 - }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled)) 647 + m.Get("/storage_overview", user_setting.StorageOverview) 648 + }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled, "EnableQuota", setting.Quota.Enabled)) 648 649 649 650 m.Group("/user", func() { 650 651 m.Get("/activate", auth.Activate) ··· 930 931 m.Post("/block", org_setting.BlockedUsersBlock) 931 932 m.Post("/unblock", org_setting.BlockedUsersUnblock) 932 933 }) 934 + m.Get("/storage_overview", org_setting.StorageOverview) 933 935 934 936 m.Group("/packages", func() { 935 937 m.Get("", org.Packages) ··· 949 951 m.Post("/rebuild", org.RebuildCargoIndex) 950 952 }) 951 953 }, packagesEnabled) 952 - }, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true)) 954 + }, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "EnableQuota", setting.Quota.Enabled, "PageIsOrgSettings", true)) 953 955 }, context.OrgAssignment(true, true)) 954 956 }, reqSignIn) 955 957 // ***** END: Organization *****
+6 -1
templates/org/settings/navbar.tmpl
··· 38 38 </div> 39 39 </details> 40 40 {{end}} 41 - <a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users"> 41 + <a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users"> 42 42 {{ctx.Locale.Tr "settings.blocked_users"}} 43 43 </a> 44 + {{if .EnableQuota}} 45 + <a class="{{if .PageIsSettingsStorageOverview}}active {{end}}item" href="{{.OrgLink}}/settings/storage_overview"> 46 + {{ctx.Locale.Tr "settings.storage_overview"}} 47 + </a> 48 + {{end}} 44 49 <a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete"> 45 50 {{ctx.Locale.Tr "org.settings.delete"}} 46 51 </a>
+5
templates/org/settings/storage_overview.tmpl
··· 1 + {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings size-overview")}} 2 + <div class="org-setting-content"> 3 + {{template "shared/quota_overview" .}} 4 + </div> 5 + {{template "org/settings/layout_footer" .}}
+32
templates/shared/quota_overview.tmpl
··· 1 + <h4 class="ui top attached header"> 2 + {{ctx.Locale.Tr "settings.quota"}} 3 + </h4> 4 + <div class="ui attached segment"> 5 + <p>{{if .ContextUser.IsOrganization}}{{ctx.Locale.Tr "settings.quota.applies_to_org"}}{{else}}{{ctx.Locale.Tr "settings.quota.applies_to_user"}}{{end}}:</p> 6 + {{range $group := .QuotaGroups}} 7 + <p class="tw-my-4"><strong>{{$group.Name}}</strong></p> 8 + <div class="tw-ml-4"> 9 + {{range $rule := .Rules}} 10 + <div class="tw-flex tw-justify-between"> 11 + <span class="tw-flex tw-items-center tw-gap-2{{if eq $rule.Limit -1}} tw-mb-5{{end}}"> 12 + {{if $rule.Acceptable ($.SizeUsed)}} 13 + {{svg "octicon-check-circle-fill" 16 "text green"}} 14 + {{$rule.Name}} 15 + {{else}} 16 + {{svg "octicon-alert-fill" 16 "text red"}} 17 + <span data-tooltip-content="{{ctx.Locale.Tr "settings.quota.rule.exceeded.helper"}}" data-tooltip-placement="top"> 18 + {{$rule.Name}} – {{ctx.Locale.Tr "settings.quota.rule.exceeded"}} 19 + </span> 20 + {{end}} 21 + </span> 22 + <span>{{ctx.Locale.TrSize ($rule.Sum $.SizeUsed)}} / {{if eq $rule.Limit -1 -}}{{ctx.Locale.Tr "settings.quota.rule.no_limit"}}{{else}}{{ctx.Locale.TrSize $rule.Limit}}{{end}}</span> 23 + </div> 24 + <div class="ui segment"> 25 + {{range $idx, $subject := .Subjects}} 26 + <div class="bar" style="width: calc(max(1%, {{Eval 100.0 "*" ($.SizeUsed.CalculateFor $subject) "/" $rule.Limit}}%)); background-color: oklch(80% 30% {{call $.Color $subject}}deg)" data-tooltip-placement="top" data-tooltip-content="{{call $.PrettySubject $subject}} – {{ctx.Locale.TrSize ($.SizeUsed.CalculateFor $subject)}}" data-tooltip-follow-cursor="horizontal"></div> 27 + {{end}} 28 + </div> 29 + {{end}} 30 + </div> 31 + {{end}} 32 + </div>
+5
templates/user/settings/navbar.tmpl
··· 51 51 <a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos"> 52 52 {{ctx.Locale.Tr "settings.repos"}} 53 53 </a> 54 + {{if .EnableQuota}} 55 + <a class="{{if .PageIsStorageOverview}}active {{end}}item" href="{{AppSubUrl}}/user/settings/storage_overview"> 56 + {{ctx.Locale.Tr "settings.storage_overview"}} 57 + </a> 58 + {{end}} 54 59 <a class="{{if .PageIsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users"> 55 60 {{ctx.Locale.Tr "settings.blocked_users"}} 56 61 </a>
+5
templates/user/settings/storage_overview.tmpl
··· 1 + {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings size-overview")}} 2 + <div class="user-setting-content"> 3 + {{template "shared/quota_overview" .}} 4 + </div> 5 + {{template "user/settings/layout_footer" .}}
+1
tests/e2e/e2e_test.go
··· 37 37 defer cancel() 38 38 39 39 tests.InitTest(true) 40 + setting.Quota.Enabled = true 40 41 initChangedFiles() 41 42 testE2eWebRoutes = routers.NormalRoutes() 42 43
+12
tests/e2e/fixtures/attachment.yml
··· 1 + - 2 + id: 1001 3 + uuid: 5792c349-8a26-46b8-b4cd-2c290f118285 4 + repo_id: 1 5 + issue_id: 1 6 + release_id: 0 7 + uploader_id: 3 8 + comment_id: 1 9 + name: forgejo-secrets.txt 10 + download_count: 0 11 + size: 536870911 12 + created_unix: 1730000000
+1
tests/e2e/fixtures/quota_group.yml
··· 1 + - name: trusted-user
+5
tests/e2e/fixtures/quota_group_mapping.yml
··· 1 + - 2 + id: 1001 3 + kind: 0 4 + mapped_id: 2 5 + group_name: trusted-user
+14
tests/e2e/fixtures/quota_group_rule_mapping.yml
··· 1 + - 2 + id: 1001 3 + group_name: trusted-user 4 + rule_name: git-lfs 5 + 6 + - 7 + id: 1002 8 + group_name: trusted-user 9 + rule_name: "all:assets" 10 + 11 + - 12 + id: 1003 13 + group_name: trusted-user 14 + rule_name: "Multi subjects"
+13
tests/e2e/fixtures/quota_rule.yml
··· 1 + - 2 + name: git-lfs 3 + limit: 512 4 + subjects: [6] 5 + 6 + - 7 + name: "all:assets" 8 + limit: -1 9 + subjects: [7] 10 + 11 + - name: "Multi subjects" 12 + limit: 5000000000 13 + subjects: [8,6]
+12
tests/e2e/fixtures/repository.yml
··· 1 + - 2 + id: 1001 3 + owner_id: 2 4 + owner_name: user2 5 + lower_name: large-lfs 6 + name: large-lfs 7 + default_branch: main 8 + is_empty: false 9 + is_archived: false 10 + is_private: true 11 + status: 0 12 + lfs_size: 8192
+9
tests/e2e/user-settings.test.e2e.ts
··· 63 63 await page.goto('/user2?tab=activity'); 64 64 await expect(page.getByText('Your activity is visible to everyone')).toBeVisible(); 65 65 }); 66 + 67 + test('User: Storage overview', async ({browser}, workerInfo) => { 68 + const page = await login({browser}, workerInfo); 69 + await page.goto('/user/settings/storage_overview'); 70 + await page.waitForLoadState(); 71 + await page.getByLabel('Git LFS – 8 KiB').nth(1).hover({position: {x: 250, y: 2}}); 72 + await expect(page.getByText('Git LFS')).toBeVisible(); 73 + await save_visual(page); 74 + });
+8
web_src/css/repo.css
··· 2035 2035 overflow: hidden; 2036 2036 } 2037 2037 2038 + .size-overview .segment:has(> .bar) { 2039 + display: flex; 2040 + height: 10px; 2041 + padding: 0; 2042 + border-radius: 3px; 2043 + overflow: hidden; 2044 + } 2045 + 2038 2046 #cite-repo-modal #citation-panel { 2039 2047 display: flex; 2040 2048 width: 100%;