开发容器分页查询:

* 用户设置-> 开发容器列表
* GET /api/devcontainer/user?page=1&page_size=10
This commit is contained in:
DAI Mingchen
2024-08-29 12:53:07 +08:00
repo.diff.parent 475e742e53
repo.diff.commit 2f5dfd3196
repo.diff.stats_desc%!(EXTRA int=15, int=306, int=94)

repo.diff.view_file

@@ -46,6 +46,10 @@ CHARSET_COLLATION = utf8mb4_bin
[cors]
CONTENT_SECURITY_POLICY = default-src 'self' data: 'unsafe-inline' https://mp.weixin.qq.com; img-src * data:
[ui.admin]
;; Dev Container 分页参数(每页展示 DevContainer 个数),若未指定,默认值 50
DEV_CONTAINERS_PAGING_NUM = 50
```
正式部署单机版

repo.diff.view_file

@@ -4,7 +4,7 @@ import (
"code.gitea.io/gitea/models/db"
)
// DevstarDevcontainer devContainer 代码仓库一对一关联表
// DevstarDevcontainer devContainer 关联 代码仓库 和 用户
//
// 遵循gonic规则映射数据库表 `devstar_devcontainer`,各字段注解见 https://xorm.io/docs/chapter-02/4.columns/
type DevstarDevcontainer struct {
@@ -15,7 +15,8 @@ type DevstarDevcontainer struct {
DevcontainerUsername string `xorm:"VARCHAR(32) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_username' comment('SSH Username')"`
DevcontainerPassword string `xorm:"VARCHAR(32) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_password' comment('SSH Password')"`
DevcontainerWorkDir string `xorm:"VARCHAR(256) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_work_dir' comment('SSH 工作路径,典型值 ~/${project_name}256字节以内')"`
RepoId int64 `xorm:"BIGINT UNIQUE NOT NULL 'repo_id' comment('repository表主键')"`
RepoId int64 `xorm:"BIGINT NOT NULL 'repo_id' comment('repository表主键')"`
UserId int64 `xorm:"BIGINT NOT NULL 'user_id' comment('user表主键')"`
}
func init() {

repo.diff.view_file

@@ -1,18 +0,0 @@
package devstar_devcontainer
import (
"code.gitea.io/gitea/models/db"
)
// DevstarDevcontainerUser devContainer 与 用户 多对多关联表
//
// 遵循gonic规则映射数据库表 `devstar_devcontainer_user`,各字段注解见 https://xorm.io/docs/chapter-02/4.columns/
type DevstarDevcontainerUser struct {
Id int64 `xorm:"BIGINT pk NOT NULL autoincr 'id' comment('主键')"`
DevcontainerId int64 `xorm:"BIGINT NOT NULL 'devcontainer_id' comment('devstar_devcontainer表主键')"`
UserId int64 `xorm:"BIGINT NOT NULL 'user_id' comment('user表主键')"`
}
func init() {
db.RegisterModel(new(DevstarDevcontainerUser))
}

repo.diff.view_file

@@ -13,16 +13,11 @@ func InitializeDevContainerDbTables(x *xorm.Engine) error {
var err error
// 1. 初始化 devContainer 与 Repository 一对一关系
// 1. 初始化 devContainer 表
if err = addDBDevStarDevContainer(x); err != nil {
return err
}
// 2. 初始化 devContainer 与 user 多对多关系表
if err = addDBDevStarDevContainerUser(x); err != nil {
return err
}
return nil
}
@@ -39,17 +34,3 @@ func addDBDevStarDevContainer(x *xorm.Engine) error {
return nil
}
// addDBDevStarDevContainerUser 2. 初始化 devContainer 与 user 多对多关系表
func addDBDevStarDevContainerUser(x *xorm.Engine) error {
err := x.Sync(new(devcontainer_models.DevstarDevcontainerUser))
if err != nil {
return ErrMigrateDevstarDatabase{
Step: "create table 'devstar_devcontainer_user'",
Message: err.Error(),
}
}
return nil
}

repo.diff.view_file

@@ -60,6 +60,8 @@ var UI = struct {
RepoPagingNum int
NoticePagingNum int
OrgPagingNum int
// DevContainersPagingNum Dev Container 分页参数,每页展示 Dev Container 数量
DevContainersPagingNum int
} `ini:"ui.admin"`
User struct {
RepoPagingNum int
@@ -118,11 +120,15 @@ var UI = struct {
RepoPagingNum int
NoticePagingNum int
OrgPagingNum int
// DevContainersPagingNum Dev Container 分页参数,每页展示 Dev Container 数量
DevContainersPagingNum int
}{
UserPagingNum: 50,
RepoPagingNum: 50,
NoticePagingNum: 25,
OrgPagingNum: 50,
// DevContainersPagingNum Dev Container 分页参数,每页展示 Dev Container 数量
DevContainersPagingNum: 50,
},
User: struct {
RepoPagingNum int

repo.diff.view_file

@@ -691,6 +691,8 @@ social = Social Accounts
applications = Applications
orgs = Manage Organizations
repos = Repositories
dev_containers_list = Dev Containers
dev_containers_none = You do not own any dev containers.
delete = Delete Account
twofa = Two-Factor Authentication (TOTP)
account_link = Linked Accounts
@@ -1010,6 +1012,7 @@ visibility.private = Private
visibility.private_tooltip = Visible only to members of organizations you have joined
[repo]
dev_container = Dev Container
new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository.</a>
owner = Owner
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.

repo.diff.view_file

@@ -690,6 +690,8 @@ social=社交帐号绑定
applications=应用
orgs=管理组织
repos=仓库列表
dev_containers_list = 开发容器列表
dev_containers_none = 你没有任何开发容器。
delete=删除帐户
twofa=两步验证
account_link=已绑定帐户
@@ -1009,6 +1011,7 @@ visibility.private=私有
visibility.private_tooltip=仅对您已加入的组织的成员可见。
[repo]
dev_container = 开发容器
new_repo_helper=代码仓库包含了所有的项目文件,包括版本历史记录。已经在其他地方托管了?<a href="%s">迁移仓库。</a>
owner=拥有者
owner_helper=由于最大仓库数量限制,一些组织可能不会显示在下拉列表中。

repo.diff.view_file

@@ -1,6 +1,8 @@
package devcontainer
import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
Result "code.gitea.io/gitea/routers/entity"
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
"code.gitea.io/gitea/services/context"
@@ -9,8 +11,10 @@ import (
// ListUserDevcontainers 枚举已登录用户所有的 devContainers但目前测试阶段只返回 mock数据
//
// GET /api/devcontainer/user
// 请求输入参数:已在ctx隐含
// GET /api/devcontainer/user
// 请求输入参数:
// - page: 当前第几页默认第1页从1开始计数
// - pageSize: 每页记录数(默认值 setting.UI.Admin.DevContainersPagingNum
func ListUserDevcontainers(ctx *context.Context) {
// 1. 检查用户登录状态,若未登录则返回未授权错误
@@ -20,17 +24,28 @@ func ListUserDevcontainers(ctx *context.Context) {
}
// 2. 查询数据库 当前登录用户拥有写入权限的仓库
userId := ctx.Doer.ID
userDevcontainersItemVOList, _ := devstar_devcontainer_service.GetUserDevcontainerListByUserId(ctx, userId)
userPage := ctx.FormInt("page")
if userPage <= 0 {
userPage = 1
}
userPageSize := ctx.FormInt("page_size")
if userPageSize <= 0 || userPageSize > setting.UI.Admin.DevContainersPagingNum {
userPageSize = setting.UI.Admin.DevContainersPagingNum
}
opts := &DevcontainersVO.SearchDevcontainerItemVoOptions{
Actor: ctx.Doer,
PaginationOptions: db.ListOptions{
Page: userPage,
PageSize: userPageSize,
},
}
userDevcontainersVO, _ := devstar_devcontainer_service.GetUserDevcontainersList(ctx, opts)
// 3. 封装VO
resultListUserDevcontainersVO := Result.ResultType{
Code: Result.RespSuccess.Code,
Msg: Result.RespSuccess.Msg,
Data: DevcontainersVO.ListUserDevcontainersVO{
UserID: userId,
DevContainers: userDevcontainersItemVOList,
},
Data: userDevcontainersVO,
}
// 4. JSON序列化写入输出流

repo.diff.view_file

@@ -1,21 +1,38 @@
package vo
import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
)
// ListUserDevcontainersVO 封装用户所有 devContainer 信息列表
type ListUserDevcontainersVO struct {
UserID int64 `json:"userId"`
Username string `json:"username"`
DevContainers []DevContainerItemVO `json:"devContainers"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
PageTotalNum int `json:"pageTotalNum"`
ItemTotalNum int64 `json:"itemTotalNum"`
}
// DevContainerItemVO 描述每一项 devContainer
type DevContainerItemVO struct {
RepoId int64 `json:"repoId" xorm:"RepoId"`
RepoName string `json:"repoName" xorm:"RepoName"`
RepoDescription string `json:"repoDescription" xorm:"RepoDescription"`
DevContainerId int64 `json:"devContainerId" xorm:"DevContainerId"`
DevContainerName string `json:"devContainerName" xorm:"DevContainerName"`
DevContainerHost string `json:"devContainerHost" xorm:"DevContainerHost"`
DevContainerPort uint16 `json:"devContainerPort" xorm:"DevContainerPort"`
DevContainerUsername string `json:"devContainerUsername" xorm:"DevContainerUsername"`
DevContainerPassword string `json:"devContainerPassword" xorm:"DevContainerPassword"`
DevContainerWorkDir string `json:"devContainerWorkDir" xorm:"DevContainerWorkDir"`
DevContainerId int64 `json:"devContainerId" xorm:"devcontainer_id"`
DevContainerName string `json:"devContainerName" xorm:"devcontainer_name"`
DevContainerHost string `json:"devContainerHost" xorm:"devcontainer_host"`
DevContainerPort uint16 `json:"devContainerPort" xorm:"devcontainer_port"`
DevContainerUsername string `json:"devContainerUsername" xorm:"devcontainer_username"`
DevContainerPassword string `json:"devContainerPassword" xorm:"devcontainer_password"`
DevContainerWorkDir string `json:"devContainerWorkDir" xorm:"devcontainer_work_dir"`
RepoId int64 `json:"repoId" xorm:"repo_id"`
RepoName string `json:"repoName" xorm:"repo_name"`
RepoDescription string `json:"repoDescription" xorm:"repo_description"`
}
// SearchDevcontainerItemVoOptions 查询条件
type SearchDevcontainerItemVoOptions struct {
PaginationOptions db.ListOptions // XORM 分页查询条件
Actor *user_model.User // Dev Container 所属用户
OrderBy db.SearchOrderBy // 排序
}

repo.diff.view_file

@@ -0,0 +1,59 @@
package setting
import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
gitea_context "code.gitea.io/gitea/services/context"
devstar_devcontainer_service "code.gitea.io/gitea/services/devstar_devcontainer"
"net/http"
)
const (
tplSettingsDevContainers base.TplName = "user/settings/dev_containers_list"
)
// ListDevContainers 展示用户所有开发容器
func ListDevContainers(ctx *gitea_context.Context) {
// 1. 检查页面是否登录,若未登录则重定向到登录页
ctxUser := ctx.Doer
if ctxUser == nil {
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageLogin))
return
}
// 2. 设置前端展示元数据数据
ctx.Data["Title"] = ctx.Tr("settings.dev_containers_list")
ctx.Data["PageIsSettingsDevContainersList"] = true
// 3. 分页查询
paginationOptions := db.ListOptions{
PageSize: setting.UI.Admin.DevContainersPagingNum, // 参考 app.ini 配置文件 ui.admin.DEV_CONTAINERS_PAGING_NUM默认50
Page: ctx.FormInt("page"), // URL参数起始页码
}
if paginationOptions.Page <= 0 {
paginationOptions.Page = 1
}
opts := &DevcontainersVO.SearchDevcontainerItemVoOptions{
PaginationOptions: paginationOptions,
Actor: ctxUser,
}
userDevcontainersListVO, err := devstar_devcontainer_service.GetUserDevcontainersList(ctx, opts)
if err != nil {
ctx.ServerError("ListDevContainers", err)
return
}
ctx.Data["DevContainers"] = userDevcontainersListVO.DevContainers
ctx.Data["ContextUser"] = ctxUser
itemTotalNum := userDevcontainersListVO.ItemTotalNum
pageTotalNum := userDevcontainersListVO.PageTotalNum
pageSize := userDevcontainersListVO.PageSize
currentPage := userDevcontainersListVO.Page
pager := gitea_context.NewPagination(int(itemTotalNum), pageSize, currentPage, pageTotalNum)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSettingsDevContainers)
return
}

repo.diff.view_file

@@ -651,6 +651,13 @@ func registerRoutes(m *web.Router) {
m.Get("/repos", user_setting.Repos)
m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository)
// ***** START: /user/settings/dev-containers *****
// 管理用户级别开发容器列表,现只做查看,如需控制/修改,需要跳转到对应仓库开发容器页面
m.Group("/dev-containers-list", func() {
m.Get("", user_setting.ListDevContainers)
})
// ***** END: /user/settings/dev-containers *****
m.Group("/hooks", func() {
m.Get("", user_setting.Webhooks)
m.Post("/delete", user_setting.DeleteWebhook)

repo.diff.view_file

@@ -3,52 +3,131 @@ package devstar_devcontainer
import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/devstar_devcontainer"
"code.gitea.io/gitea/modules/setting"
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
"context"
"fmt"
"xorm.io/builder"
)
// GetUserDevcontainerListByUserId 根据 userId 查询名下 devContainer 列表
func GetUserDevcontainerListByUserId(ctx context.Context, userId int64) ([]DevcontainersVO.DevContainerItemVO, error) {
// GetUserDevcontainersList 根据 userId 查询名下 devContainer 列表
func GetUserDevcontainersList(ctx context.Context, opts *DevcontainersVO.SearchDevcontainerItemVoOptions) (DevcontainersVO.ListUserDevcontainersVO, error) {
var devcontainerItemVOList []DevcontainersVO.DevContainerItemVO
/*
SELECT
devstar_devcontainer.repo_id AS RepoId,
repository.name AS RepoName,
repository.description AS RepoDescription,
devstar_devcontainer.id AS DevContainerId,
devstar_devcontainer.name AS DevContainerName,
devstar_devcontainer.devcontainer_host AS DevContainerHost,
devstar_devcontainer.devcontainer_port AS DevContainerPort,
devstar_devcontainer.devcontainer_username AS DevContainerUsername,
devstar_devcontainer.devcontainer_password AS DevContainerPassword,
devstar_devcontainer.devcontainer_work_dir AS DevContainerWorkDir
FROM devstar_devcontainer
LEFT JOIN repository ON devstar_devcontainer.repo_id = repository.id
WHERE devstar_devcontainer.id IN (
SELECT devcontainer_id
FROM devstar_devcontainer_user
WHERE user_id = ?
);
*/
err := db.GetEngine(ctx).
Table("devstar_devcontainer").
Select("devstar_devcontainer.repo_id AS RepoId, repository.name AS RepoName, repository.description AS RepoDescription, devstar_devcontainer.id AS DevContainerId, devstar_devcontainer.name AS DevContainerName, devstar_devcontainer.devcontainer_host AS DevContainerHost, devstar_devcontainer.devcontainer_port AS DevContainerPort, devstar_devcontainer.devcontainer_username AS DevContainerUsername,devstar_devcontainer.devcontainer_password AS DevContainerPassword, devstar_devcontainer.devcontainer_work_dir AS DevContainerWorkDir").
Join("LEFT", "repository", "devstar_devcontainer.repo_id = repository.id").
Where("devstar_devcontainer.id IN (SELECT devcontainer_id FROM devstar_devcontainer_user WHERE user_id = ?)", userId).
Find(&devcontainerItemVOList)
if devcontainerItemVOList == nil {
devcontainerItemVOList = []DevcontainersVO.DevContainerItemVO{}
// 0. 构造异常返回时的空数据
var resultDevContainerListVO = DevcontainersVO.ListUserDevcontainersVO{
Page: 0,
PageSize: setting.UI.Admin.DevContainersPagingNum,
PageTotalNum: 0,
ItemTotalNum: 0,
DevContainers: []DevcontainersVO.DevContainerItemVO{},
}
if err != nil {
return devcontainerItemVOList,
// 1. 查询参数预处理
if opts == nil || opts.Actor == nil {
return resultDevContainerListVO, devstar_devcontainer.ErrFailedToOperateDevstarDevcontainerDB{
Action: "construct query condition for devContainer user list",
Message: "invalid search condition",
}
}
resultDevContainerListVO.UserID = opts.Actor.ID
resultDevContainerListVO.Username = opts.Actor.Name
if len(opts.OrderBy) == 0 {
opts.OrderBy = "devcontainer_id DESC"
}
opts.PaginationOptions.ListAll = false // 强制使用分页查询,禁止一次性列举所有 devContainers
if opts.PaginationOptions.Page <= 0 { // 未指定页码/无效页码:查询第 1 页
opts.PaginationOptions.Page = 1
}
if opts.PaginationOptions.PageSize <= 0 || opts.PaginationOptions.PageSize > setting.UI.Admin.DevContainersPagingNum {
opts.PaginationOptions.PageSize = setting.UI.Admin.DevContainersPagingNum // /无效页面大小/超过每页最大限制:自动调整到系统最大开发容器页面大小
}
resultDevContainerListVO.Page = opts.PaginationOptions.Page
resultDevContainerListVO.PageSize = opts.PaginationOptions.PageSize
// 2. SQL 条件构建
sqlCondition := builder.NewCond()
sqlCondition = sqlCondition.And(builder.Eq{"user_id": opts.Actor.ID})
// 3. 开启数据库事务,进行分页查询,同时封装 VO
errDbTransaction := db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
var err error
// 2.1 查询符合条件的记录总数
/*
SELECT COUNT(*)
FROM devstar_devcontainer
LEFT JOIN repository ON devstar_devcontainer.repo_id = repository.id
WHERE devstar_devcontainer.user_id = #{opts.Actor.ID}
*/
resultDevContainerListVO.ItemTotalNum, err = sess.
Table("devstar_devcontainer").
Join("LEFT", "repository", "devstar_devcontainer.repo_id = repository.id").
Where(sqlCondition).
Count()
if err != nil {
return devstar_devcontainer.ErrFailedToOperateDevstarDevcontainerDB{
Action: "count devContainer item numbers",
Message: err.Error(),
}
}
if resultDevContainerListVO.ItemTotalNum == 0 {
return nil // 无符合记录,返回 nil 提交/结束 事务
}
// 2.3 计算分页参数
totalRecords := resultDevContainerListVO.ItemTotalNum
pageSize := int64(resultDevContainerListVO.PageSize)
resultDevContainerListVO.PageTotalNum += int(totalRecords/pageSize) + 1
// 2.3 数据库带条件分页查询
resultDevContainerListVO.DevContainers = make([]DevcontainersVO.DevContainerItemVO, 0, opts.PaginationOptions.PageSize)
/*
SELECT
devstar_devcontainer.id AS devcontainer_id,
devstar_devcontainer.name AS devcontainer_name,
devstar_devcontainer.devcontainer_host AS devcontainer_host,
devstar_devcontainer.devcontainer_port AS devcontainer_port,
devstar_devcontainer.devcontainer_username AS devcontainer_username,
devstar_devcontainer.devcontainer_password AS devcontainer_password,
devstar_devcontainer.devcontainer_work_dir AS devcontainer_work_dir,
repository.id AS repo_id,
repository.name AS repo_name,
repository.description AS repo_description
FROM devstar_devcontainer
LEFT JOIN repository ON devstar_devcontainer.repo_id = repository.id
WHERE devstar_devcontainer.user_id = #{opts.Actor.ID}
ORDER BY #{opts.OrderBy.String()}
LIMIT #{opts.PaginationOptions.PageSize}
OFFSET ( (#{opts.PaginationOptions.Page} - 1) * #{opts.PaginationOptions.PageSize});
*/
sess = sess.
Table("devstar_devcontainer").
Select("devstar_devcontainer.repo_id AS repo_id, repository.name AS repo_name, repository.description AS repo_description, devstar_devcontainer.id AS devcontainer_id, devstar_devcontainer.name AS devcontainer_name, devstar_devcontainer.devcontainer_host AS devcontainer_host, devstar_devcontainer.devcontainer_port AS devcontainer_port, devstar_devcontainer.devcontainer_username AS devcontainer_username, devstar_devcontainer.devcontainer_password AS devcontainer_password, devstar_devcontainer.devcontainer_work_dir AS devcontainer_work_dir").
Join("LEFT", "repository", "devstar_devcontainer.repo_id = repository.id").
Where(sqlCondition).
OrderBy(opts.OrderBy.String())
err = db.SetSessionPagination(sess, &opts.PaginationOptions).
Find(&resultDevContainerListVO.DevContainers)
if err != nil {
// 查询出错,返回错误信息,结束事务
return devstar_devcontainer.ErrFailedToOperateDevstarDevcontainerDB{
Action: fmt.Sprintf("list devContainer for user '%v' at page %v", opts.Actor.Name, opts.PaginationOptions.Page),
Message: err.Error(),
}
}
// 2.x 查询完成,返回 nil 自动提交事务
return nil
})
if errDbTransaction != nil {
return resultDevContainerListVO,
devstar_devcontainer.ErrFailedToOperateDevstarDevcontainerDB{
Action: "query user DevContainer List",
Message: err.Error(),
Message: errDbTransaction.Error(),
}
}
return devcontainerItemVOList, nil
return resultDevContainerListVO, nil
}

repo.diff.view_file

@@ -138,6 +138,13 @@
</a>
{{end}}
<!-- 定义tab DevContainer List -->
{{if .Permission.CanWrite ctx.Consts.RepoUnitTypeCode}}
<a class="{{if .PageIsRepoDevcontainerDetails}}active {{end}}item" href="{{.RepoLink}}/dev-container">
{{svg "octicon-container"}} {{ctx.Locale.Tr "repo.dev_container"}}
</a>
{{end}}
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeIssues}}
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoLink}}/issues">
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}}
@@ -234,3 +241,10 @@
</div>
<div class="ui tabs divider"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
console.log(`当用户点击 DevContainer 按钮,跳转到 {{.RepoLink}}/dev-container`)
})
</script>

repo.diff.view_file

@@ -0,0 +1,38 @@
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings repos")}}
<div class="user-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.dev_containers_list"}}
</h4>
<div class="ui attached segment">
{{if .DevContainers}}
<div class="ui middle aligned divided list">
{{range .DevContainers}}
<div class="item">
<div class="content flex-text-block">
<span class="text gold icon">{{svg "octicon-container"}}</span>
<a class="name"
href="{{AppSubUrl}}/{{$.ContextUser.Name}}/{{.RepoName}}/dev-container"
title="ssh://{{.DevContainerUsername}}@{{.DevContainerHost}}:{{.DevContainerPort}}">
{{.DevContainerName}}
</a>
</div>
<span>
{{svg "octicon-repo"}} {{ctx.Locale.Tr "repo.repo_name"}}:
<a class="name"
href="{{AppSubUrl}}/{{$.ContextUser.Name}}/{{.RepoName}}"
title="{{.RepoDescription}}">
{{.RepoName}}
</a>
</span>
</div>
{{end}}
</div>
{{template "base/paginate" .}}
{{else}}
<div class="item">
{{ctx.Locale.Tr "settings.dev_containers_none"}}
</div>
{{end}}
</div>
</div>
{{template "user/settings/layout_footer" .}}

repo.diff.view_file

@@ -54,5 +54,8 @@
<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
{{ctx.Locale.Tr "settings.repos"}}
</a>
<a class="{{if .PageIsSettingsDevContainersList}}active {{end}}item" href="{{AppSubUrl}}/user/settings/dev-containers-list">
{{ctx.Locale.Tr "settings.dev_containers_list"}}
</a>
</div>
</div>