From 77a1af5ab837ad31a4427f9f5fb9718953f59c46 Mon Sep 17 00:00:00 2001 From: Gusted Date: Wed, 26 Feb 2025 14:36:53 +0000 Subject: [PATCH] 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 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6602 Reviewed-by: Otto Co-authored-by: Gusted Co-committed-by: Gusted --- models/quota/default.go | 2 +- models/quota/group.go | 31 ++++--- models/quota/quota.go | 3 +- models/quota/quota_group_test.go | 37 +++++--- models/quota/rule.go | 22 +++-- options/locale/locale_en-US.ini | 21 +++++ routers/web/org/setting/storage_overview.go | 20 +++++ routers/web/shared/storage_overview.go | 90 +++++++++++++++++++ routers/web/user/setting/storage_overview.go | 20 +++++ routers/web/web.go | 6 +- templates/org/settings/navbar.tmpl | 7 +- templates/org/settings/storage_overview.tmpl | 5 ++ templates/shared/quota_overview.tmpl | 32 +++++++ templates/user/settings/navbar.tmpl | 5 ++ templates/user/settings/storage_overview.tmpl | 5 ++ tests/e2e/e2e_test.go | 1 + tests/e2e/fixtures/attachment.yml | 12 +++ tests/e2e/fixtures/quota_group.yml | 1 + tests/e2e/fixtures/quota_group_mapping.yml | 5 ++ .../e2e/fixtures/quota_group_rule_mapping.yml | 14 +++ tests/e2e/fixtures/quota_rule.yml | 13 +++ tests/e2e/fixtures/repository.yml | 12 +++ tests/e2e/user-settings.test.e2e.ts | 9 ++ web_src/css/repo.css | 8 ++ 24 files changed, 348 insertions(+), 33 deletions(-) create mode 100644 routers/web/org/setting/storage_overview.go create mode 100644 routers/web/shared/storage_overview.go create mode 100644 routers/web/user/setting/storage_overview.go create mode 100644 templates/org/settings/storage_overview.tmpl create mode 100644 templates/shared/quota_overview.tmpl create mode 100644 templates/user/settings/storage_overview.tmpl create mode 100644 tests/e2e/fixtures/attachment.yml create mode 100644 tests/e2e/fixtures/quota_group.yml create mode 100644 tests/e2e/fixtures/quota_group_mapping.yml create mode 100644 tests/e2e/fixtures/quota_group_rule_mapping.yml create mode 100644 tests/e2e/fixtures/quota_rule.yml create mode 100644 tests/e2e/fixtures/repository.yml diff --git a/models/quota/default.go b/models/quota/default.go index 6b553d6f71..e53e47bade 100644 --- a/models/quota/default.go +++ b/models/quota/default.go @@ -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", diff --git a/models/quota/group.go b/models/quota/group.go index 0acb5b255e..50080748a1 100644 --- a/models/quota/group.go +++ b/models/quota/group.go @@ -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) { diff --git a/models/quota/quota.go b/models/quota/quota.go index d38bfab3cc..ee3fec6c1a 100644 --- a/models/quota/quota.go +++ b/models/quota/quota.go @@ -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 } diff --git a/models/quota/quota_group_test.go b/models/quota/quota_group_test.go index bc258588f9..edbf43fcf5 100644 --- a/models/quota/quota_group_test.go +++ b/models/quota/quota_group_test.go @@ -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) } diff --git a/models/quota/rule.go b/models/quota/rule.go index b0c6c0f4b6..cb23b74b52 100644 --- a/models/quota/rule.go +++ b/models/quota/rule.go @@ -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) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 29c696f29f..22c726a793 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -749,6 +749,8 @@ organization = Organizations uid = UID webauthn = Two-factor authentication (Security keys) blocked_users = Blocked users +storage_overview = Storage overview +quota = Quota public_profile = Public profile biography_placeholder = Tell others a little bit about yourself! (Markdown is supported) @@ -1054,6 +1056,25 @@ user_unblock_success = The user has been unblocked successfully. user_block_success = The user has been blocked successfully. user_block_yourself = You cannot block yourself. +quota.applies_to_user = The following quota rules apply to your account +quota.applies_to_org = The following quota rules apply to this organisation +quota.rule.exceeded = Exceeded +quota.rule.exceeded.helper = The total size of objects for this rule has exceeded the quota. +quota.rule.no_limit = Unlimited +quota.sizes.all = All +quota.sizes.repos.all = Repositories +quota.sizes.repos.public = Public repositories +quota.sizes.repos.private = Private repositories +quota.sizes.git.all = Git content +quota.sizes.git.lfs = Git LFS +quota.sizes.assets.all = Assets +quota.sizes.assets.attachments.all = Attachments +quota.sizes.assets.attachments.issues = Issue attachments +quota.sizes.assets.attachments.releases = Release attachments +quota.sizes.assets.artifacts = Artifacts +quota.sizes.assets.packages.all = Packages +quota.sizes.wiki = Wiki + [repo] rss.must_be_on_branch = You must be on a branch to have an RSS feed. diff --git a/routers/web/org/setting/storage_overview.go b/routers/web/org/setting/storage_overview.go new file mode 100644 index 0000000000..4b9bd02ca4 --- /dev/null +++ b/routers/web/org/setting/storage_overview.go @@ -0,0 +1,20 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package setting + +import ( + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/routers/web/shared" + "code.gitea.io/gitea/services/context" +) + +const ( + tplSettingsStorageOverview base.TplName = "org/settings/storage_overview" +) + +// StorageOverview render a size overview of the organization, as well as relevant +// quota limits of the instance. +func StorageOverview(ctx *context.Context) { + shared.StorageOverview(ctx, ctx.Org.Organization.ID, tplSettingsStorageOverview) +} diff --git a/routers/web/shared/storage_overview.go b/routers/web/shared/storage_overview.go new file mode 100644 index 0000000000..3bebdfb688 --- /dev/null +++ b/routers/web/shared/storage_overview.go @@ -0,0 +1,90 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package shared + +import ( + "html/template" + "net/http" + + quota_model "code.gitea.io/gitea/models/quota" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" +) + +// StorageOverview render a size overview of the user, as well as relevant +// quota limits of the instance. +func StorageOverview(ctx *context.Context, userID int64, tpl base.TplName) { + if !setting.Quota.Enabled { + ctx.NotFound("MustEnableQuota", nil) + } + ctx.Data["Title"] = ctx.Tr("settings.storage_overview") + ctx.Data["PageIsStorageOverview"] = true + + ctx.Data["Color"] = func(subject quota_model.LimitSubject) float64 { + return float64(subject) * 137.50776405003785 // Golden angle. + } + + ctx.Data["PrettySubject"] = func(subject quota_model.LimitSubject) template.HTML { + switch subject { + case quota_model.LimitSubjectSizeAll: + return ctx.Locale.Tr("settings.quota.sizes.all") + case quota_model.LimitSubjectSizeReposAll: + return ctx.Locale.Tr("settings.quota.sizes.repos.all") + case quota_model.LimitSubjectSizeReposPublic: + return ctx.Locale.Tr("settings.quota.sizes.repos.public") + case quota_model.LimitSubjectSizeReposPrivate: + return ctx.Locale.Tr("settings.quota.sizes.repos.private") + case quota_model.LimitSubjectSizeGitAll: + return ctx.Locale.Tr("settings.quota.sizes.git.all") + case quota_model.LimitSubjectSizeGitLFS: + return ctx.Locale.Tr("settings.quota.sizes.git.lfs") + case quota_model.LimitSubjectSizeAssetsAll: + return ctx.Locale.Tr("settings.quota.sizes.assets.all") + case quota_model.LimitSubjectSizeAssetsAttachmentsAll: + return ctx.Locale.Tr("settings.quota.sizes.assets.attachments.all") + case quota_model.LimitSubjectSizeAssetsAttachmentsIssues: + return ctx.Locale.Tr("settings.quota.sizes.assets.attachments.issues") + case quota_model.LimitSubjectSizeAssetsAttachmentsReleases: + return ctx.Locale.Tr("settings.quota.sizes.assets.attachments.releases") + case quota_model.LimitSubjectSizeAssetsArtifacts: + return ctx.Locale.Tr("settings.quota.sizes.assets.artifacts") + case quota_model.LimitSubjectSizeAssetsPackagesAll: + return ctx.Locale.Tr("settings.quota.sizes.assets.packages.all") + case quota_model.LimitSubjectSizeWiki: + return ctx.Locale.Tr("settings.quota.sizes.wiki") + default: + panic("unrecognized subject: " + subject.String()) + } + } + + sizeUsed, err := quota_model.GetUsedForUser(ctx, userID) + if err != nil { + ctx.ServerError("GetUsedForUser", err) + return + } + ctx.Data["SizeUsed"] = sizeUsed + + quotaGroups, err := quota_model.GetGroupsForUser(ctx, userID) + if err != nil { + ctx.ServerError("GetGroupsForUser", err) + return + } + if len(quotaGroups) == 0 { + quotaGroups = append(quotaGroups, "a_model.Group{ + Name: "Global quota", + Rules: []quota_model.Rule{ + { + Name: "Default", + Limit: setting.Quota.Default.Total, + Subjects: quota_model.LimitSubjects{quota_model.LimitSubjectSizeAll}, + }, + }, + }, + ) + } + ctx.Data["QuotaGroups"] = quotaGroups + + ctx.HTML(http.StatusOK, tpl) +} diff --git a/routers/web/user/setting/storage_overview.go b/routers/web/user/setting/storage_overview.go new file mode 100644 index 0000000000..8a0c773077 --- /dev/null +++ b/routers/web/user/setting/storage_overview.go @@ -0,0 +1,20 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package setting + +import ( + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/routers/web/shared" + "code.gitea.io/gitea/services/context" +) + +const ( + tplSettingsStorageOverview base.TplName = "user/settings/storage_overview" +) + +// StorageOverview render a size overview of the user, as well as relevant +// quota limits of the instance. +func StorageOverview(ctx *context.Context) { + shared.StorageOverview(ctx, ctx.Doer.ID, tplSettingsStorageOverview) +} diff --git a/routers/web/web.go b/routers/web/web.go index 4d8d280c89..a519ff74c1 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -644,7 +644,8 @@ func registerRoutes(m *web.Route) { m.Get("", user_setting.BlockedUsers) m.Post("/unblock", user_setting.UnblockUser) }) - }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled)) + m.Get("/storage_overview", user_setting.StorageOverview) + }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled, "EnableQuota", setting.Quota.Enabled)) m.Group("/user", func() { m.Get("/activate", auth.Activate) @@ -930,6 +931,7 @@ func registerRoutes(m *web.Route) { m.Post("/block", org_setting.BlockedUsersBlock) m.Post("/unblock", org_setting.BlockedUsersUnblock) }) + m.Get("/storage_overview", org_setting.StorageOverview) m.Group("/packages", func() { m.Get("", org.Packages) @@ -949,7 +951,7 @@ func registerRoutes(m *web.Route) { m.Post("/rebuild", org.RebuildCargoIndex) }) }, packagesEnabled) - }, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true)) + }, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "EnableQuota", setting.Quota.Enabled, "PageIsOrgSettings", true)) }, context.OrgAssignment(true, true)) }, reqSignIn) // ***** END: Organization ***** diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index b245768203..4ae7f56aca 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -38,9 +38,14 @@ {{end}} - + {{ctx.Locale.Tr "settings.blocked_users"}} + {{if .EnableQuota}} + + {{ctx.Locale.Tr "settings.storage_overview"}} + + {{end}} {{ctx.Locale.Tr "org.settings.delete"}} diff --git a/templates/org/settings/storage_overview.tmpl b/templates/org/settings/storage_overview.tmpl new file mode 100644 index 0000000000..e3f8c53152 --- /dev/null +++ b/templates/org/settings/storage_overview.tmpl @@ -0,0 +1,5 @@ +{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings size-overview")}} +
+ {{template "shared/quota_overview" .}} +
+{{template "org/settings/layout_footer" .}} diff --git a/templates/shared/quota_overview.tmpl b/templates/shared/quota_overview.tmpl new file mode 100644 index 0000000000..cb8b57d724 --- /dev/null +++ b/templates/shared/quota_overview.tmpl @@ -0,0 +1,32 @@ +

+ {{ctx.Locale.Tr "settings.quota"}} +

+
+

{{if .ContextUser.IsOrganization}}{{ctx.Locale.Tr "settings.quota.applies_to_org"}}{{else}}{{ctx.Locale.Tr "settings.quota.applies_to_user"}}{{end}}:

+ {{range $group := .QuotaGroups}} +

{{$group.Name}}

+
+ {{range $rule := .Rules}} +
+ + {{if $rule.Acceptable ($.SizeUsed)}} + {{svg "octicon-check-circle-fill" 16 "text green"}} + {{$rule.Name}} + {{else}} + {{svg "octicon-alert-fill" 16 "text red"}} + + {{$rule.Name}} – {{ctx.Locale.Tr "settings.quota.rule.exceeded"}} + + {{end}} + + {{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}} +
+
+ {{range $idx, $subject := .Subjects}} +
+ {{end}} +
+ {{end}} +
+ {{end}} +
diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index d45d89ee9f..eadf2ee9e5 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -51,6 +51,11 @@ {{ctx.Locale.Tr "settings.repos"}} + {{if .EnableQuota}} + + {{ctx.Locale.Tr "settings.storage_overview"}} + + {{end}} {{ctx.Locale.Tr "settings.blocked_users"}} diff --git a/templates/user/settings/storage_overview.tmpl b/templates/user/settings/storage_overview.tmpl new file mode 100644 index 0000000000..daa0cbd913 --- /dev/null +++ b/templates/user/settings/storage_overview.tmpl @@ -0,0 +1,5 @@ +{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings size-overview")}} +
+ {{template "shared/quota_overview" .}} +
+{{template "user/settings/layout_footer" .}} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 8e4a9653cd..aecb2c70c7 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -37,6 +37,7 @@ func TestMain(m *testing.M) { defer cancel() tests.InitTest(true) + setting.Quota.Enabled = true initChangedFiles() testE2eWebRoutes = routers.NormalRoutes() diff --git a/tests/e2e/fixtures/attachment.yml b/tests/e2e/fixtures/attachment.yml new file mode 100644 index 0000000000..26069b95c1 --- /dev/null +++ b/tests/e2e/fixtures/attachment.yml @@ -0,0 +1,12 @@ +- + id: 1001 + uuid: 5792c349-8a26-46b8-b4cd-2c290f118285 + repo_id: 1 + issue_id: 1 + release_id: 0 + uploader_id: 3 + comment_id: 1 + name: forgejo-secrets.txt + download_count: 0 + size: 536870911 + created_unix: 1730000000 diff --git a/tests/e2e/fixtures/quota_group.yml b/tests/e2e/fixtures/quota_group.yml new file mode 100644 index 0000000000..0d04ba6767 --- /dev/null +++ b/tests/e2e/fixtures/quota_group.yml @@ -0,0 +1 @@ +- name: trusted-user diff --git a/tests/e2e/fixtures/quota_group_mapping.yml b/tests/e2e/fixtures/quota_group_mapping.yml new file mode 100644 index 0000000000..8339569185 --- /dev/null +++ b/tests/e2e/fixtures/quota_group_mapping.yml @@ -0,0 +1,5 @@ +- + id: 1001 + kind: 0 + mapped_id: 2 + group_name: trusted-user diff --git a/tests/e2e/fixtures/quota_group_rule_mapping.yml b/tests/e2e/fixtures/quota_group_rule_mapping.yml new file mode 100644 index 0000000000..1144888f21 --- /dev/null +++ b/tests/e2e/fixtures/quota_group_rule_mapping.yml @@ -0,0 +1,14 @@ +- + id: 1001 + group_name: trusted-user + rule_name: git-lfs + +- + id: 1002 + group_name: trusted-user + rule_name: "all:assets" + +- + id: 1003 + group_name: trusted-user + rule_name: "Multi subjects" diff --git a/tests/e2e/fixtures/quota_rule.yml b/tests/e2e/fixtures/quota_rule.yml new file mode 100644 index 0000000000..f6170cbdaa --- /dev/null +++ b/tests/e2e/fixtures/quota_rule.yml @@ -0,0 +1,13 @@ +- + name: git-lfs + limit: 512 + subjects: [6] + +- + name: "all:assets" + limit: -1 + subjects: [7] + +- name: "Multi subjects" + limit: 5000000000 + subjects: [8,6] diff --git a/tests/e2e/fixtures/repository.yml b/tests/e2e/fixtures/repository.yml new file mode 100644 index 0000000000..ac87721d6a --- /dev/null +++ b/tests/e2e/fixtures/repository.yml @@ -0,0 +1,12 @@ +- + id: 1001 + owner_id: 2 + owner_name: user2 + lower_name: large-lfs + name: large-lfs + default_branch: main + is_empty: false + is_archived: false + is_private: true + status: 0 + lfs_size: 8192 diff --git a/tests/e2e/user-settings.test.e2e.ts b/tests/e2e/user-settings.test.e2e.ts index b36a17b5b2..b2b34c263b 100644 --- a/tests/e2e/user-settings.test.e2e.ts +++ b/tests/e2e/user-settings.test.e2e.ts @@ -63,3 +63,12 @@ test('User: Profile settings', async ({browser}, workerInfo) => { await page.goto('/user2?tab=activity'); await expect(page.getByText('Your activity is visible to everyone')).toBeVisible(); }); + +test('User: Storage overview', async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); + await page.goto('/user/settings/storage_overview'); + await page.waitForLoadState(); + await page.getByLabel('Git LFS – 8 KiB').nth(1).hover({position: {x: 250, y: 2}}); + await expect(page.getByText('Git LFS')).toBeVisible(); + await save_visual(page); +}); diff --git a/web_src/css/repo.css b/web_src/css/repo.css index a9720ec6da..46c6b70c29 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2035,6 +2035,14 @@ details.repo-search-result summary::marker { overflow: hidden; } +.size-overview .segment:has(> .bar) { + display: flex; + height: 10px; + padding: 0; + border-radius: 3px; + overflow: hidden; +} + #cite-repo-modal #citation-panel { display: flex; width: 100%;