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%;