feat(runner): make admin runner template as common template, use for org runners setting

This commit is contained in:
fuxiaohei 2022-10-20 22:41:41 +08:00 committed by Jason Song
parent dca2ca2f47
commit 25fe4f6d9e
11 changed files with 414 additions and 214 deletions

View File

@ -65,7 +65,7 @@ func (Runner) TableName() string {
func (r *Runner) OwnType() string { func (r *Runner) OwnType() string {
if r.OwnerID == 0 { if r.OwnerID == 0 {
return "Global Type" return "Global"
} }
if r.RepoID == 0 { if r.RepoID == 0 {
return r.Owner.Name return r.Owner.Name
@ -91,6 +91,42 @@ func (r *Runner) AllLabels() []string {
return append(r.AgentLabels, r.CustomLabels...) return append(r.AgentLabels, r.CustomLabels...)
} }
// Editable checks if the runner is editable by the user
func (r *Runner) Editable(ownerID, repoID int64) bool {
if ownerID == 0 && repoID == 0 {
return true
}
if ownerID > 0 && r.OwnerID == ownerID {
return true
}
return repoID > 0 && r.RepoID == repoID
}
// LoadAttributes loads the attributes of the runner
func (r *Runner) LoadAttributes(ctx context.Context) error {
if r.OwnerID > 0 {
var user user_model.User
has, err := db.GetEngine(ctx).ID(r.OwnerID).Get(&user)
if err != nil {
return err
}
if has {
r.Owner = &user
}
}
if r.RepoID > 0 {
var repo repo_model.Repository
has, err := db.GetEngine(ctx).ID(r.RepoID).Get(&repo)
if err != nil {
return err
}
if has {
r.Repo = &repo
}
}
return nil
}
func init() { func init() {
db.RegisterModel(&Runner{}) db.RegisterModel(&Runner{})
} }
@ -106,13 +142,20 @@ type FindRunnerOptions struct {
func (opts FindRunnerOptions) toCond() builder.Cond { func (opts FindRunnerOptions) toCond() builder.Cond {
cond := builder.NewCond() cond := builder.NewCond()
withGlobal := false
if opts.RepoID > 0 { if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
withGlobal = true
} }
if opts.OwnerID > 0 { if opts.OwnerID > 0 {
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
withGlobal = true
} }
cond = cond.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) if withGlobal {
cond = cond.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
}
if opts.Filter != "" { if opts.Filter != "" {
cond = cond.And(builder.Like{"name", opts.Filter}) cond = cond.And(builder.Like{"name", opts.Filter})
} }
@ -223,6 +266,7 @@ func UpdateRunner(ctx context.Context, r *Runner, cols ...string) error {
return err return err
} }
// DeleteRunner deletes a runner by given ID.
func DeleteRunner(ctx context.Context, r *Runner) error { func DeleteRunner(ctx context.Context, r *Runner) error {
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
_, err := e.Delete(r) _, err := e.Delete(r)

118
routers/common/runners.go Normal file
View File

@ -0,0 +1,118 @@
package common
import (
"net/http"
"strings"
bots_model "code.gitea.io/gitea/models/bots"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
// RunnersList render common runners list page
func RunnersList(ctx *context.Context, tplName base.TplName, opts bots_model.FindRunnerOptions) {
count, err := bots_model.CountRunners(opts)
if err != nil {
ctx.ServerError("AdminRunners", err)
return
}
runners, err := bots_model.FindRunners(opts)
if err != nil {
ctx.ServerError("AdminRunners", err)
return
}
if err := runners.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
// ownid=0,repo_id=0,means this token is used for global
var token *bots_model.RunnerToken
token, err = bots_model.GetUnactivatedRunnerToken(opts.OwnerID, opts.RepoID)
if _, ok := err.(bots_model.ErrRunnerTokenNotExist); ok {
token, err = bots_model.NewRunnerToken(opts.OwnerID, opts.RepoID)
if err != nil {
ctx.ServerError("CreateRunnerToken", err)
return
}
} else {
if err != nil {
ctx.ServerError("GetUnactivatedRunnerToken", err)
return
}
}
ctx.Data["Keyword"] = opts.Filter
ctx.Data["Runners"] = runners
ctx.Data["Total"] = count
ctx.Data["RegistrationToken"] = token.Token
ctx.Data["RunnerOnwerID"] = opts.OwnerID
ctx.Data["RunnerRepoID"] = opts.RepoID
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplName)
}
func RunnerDetails(ctx *context.Context, tplName base.TplName, runnerID int64) {
runner, err := bots_model.GetRunnerByID(runnerID)
if err != nil {
ctx.ServerError("GetRunnerByID", err)
return
}
if err := runner.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
ctx.Data["Runner"] = runner
// TODO: get task list for this runner
ctx.HTML(http.StatusOK, tplName)
}
// RunnerDetailsEditPost response for edit runner details
func RunnerDetailsEditPost(ctx *context.Context, runnerID int64, redirectTo string) {
runner, err := bots_model.GetRunnerByID(runnerID)
if err != nil {
log.Warn("RunnerDetailsEditPost.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL)
ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err)
return
}
form := web.GetForm(ctx).(*forms.EditRunnerForm)
runner.Description = form.Description
runner.CustomLabels = strings.Split(form.CustomLabels, ",")
err = bots_model.UpdateRunner(ctx, runner, "description", "custom_labels")
if err != nil {
log.Warn("RunnerDetailsEditPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
ctx.Flash.Warning(ctx.Tr("admin.runners.update_runner_failed"))
ctx.Redirect(redirectTo)
return
}
log.Debug("RunnerDetailsEditPost success: %s", ctx.Req.URL)
ctx.Flash.Success(ctx.Tr("admin.runners.update_runner_success"))
ctx.Redirect(redirectTo)
}
// RunnerResetRegistrationToken reset registration token
func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, redirectTo string) {
_, err := bots_model.NewRunnerToken(ownerID, repoID)
if err != nil {
ctx.ServerError("ResetRunnerRegistrationToken", err)
return
}
ctx.Flash.Success(ctx.Tr("admin.runners.reset_registration_token_success"))
ctx.Redirect(redirectTo)
}

View File

@ -6,9 +6,7 @@
package admin package admin
import ( import (
"net/http"
"net/url" "net/url"
"strings"
bots_model "code.gitea.io/gitea/models/bots" bots_model "code.gitea.io/gitea/models/bots"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -16,14 +14,13 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/forms"
) )
const ( const (
tplRunners base.TplName = "runners/list" tplRunners base.TplName = "admin/runners/base"
tplRunnerNew base.TplName = "runners/new" tplRunnerNew base.TplName = "admin/runners/new"
tplRunnerEdit base.TplName = "runners/edit" tplRunnerEdit base.TplName = "admin/runners/edit"
) )
// Runners show all the runners // Runners show all the runners
@ -45,49 +42,11 @@ func Runners(ctx *context.Context) {
Sort: ctx.Req.URL.Query().Get("sort"), Sort: ctx.Req.URL.Query().Get("sort"),
Filter: ctx.Req.URL.Query().Get("q"), Filter: ctx.Req.URL.Query().Get("q"),
WithDeleted: false, WithDeleted: false,
RepoID: 0,
OwnerID: 0,
} }
count, err := bots_model.CountRunners(opts) common.RunnersList(ctx, tplRunners, opts)
if err != nil {
ctx.ServerError("AdminRunners", err)
return
}
runners, err := bots_model.FindRunners(opts)
if err != nil {
ctx.ServerError("AdminRunners", err)
return
}
if err := runners.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
// ownid=0,repo_id=0,means this token is used for global
var token *bots_model.RunnerToken
token, err = bots_model.GetUnactivatedRunnerToken(0, 0)
if _, ok := err.(bots_model.ErrRunnerTokenNotExist); ok {
token, err = bots_model.NewRunnerToken(0, 0)
if err != nil {
ctx.ServerError("CreateRunnerToken", err)
return
}
} else {
if err != nil {
ctx.ServerError("GetUnactivatedRunnerToken", err)
return
}
}
ctx.Data["Keyword"] = opts.Filter
ctx.Data["Runners"] = runners
ctx.Data["Total"] = count
ctx.Data["RegistrationToken"] = token.Token
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplRunners)
} }
// EditRunner show editing runner page // EditRunner show editing runner page
@ -96,47 +55,16 @@ func EditRunner(ctx *context.Context) {
ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminRunners"] = true ctx.Data["PageIsAdminRunners"] = true
runner, err := bots_model.GetRunnerByID(ctx.ParamsInt64(":runnerid")) common.RunnerDetails(ctx, tplRunnerEdit, ctx.ParamsInt64(":runnerid"))
if err != nil {
ctx.ServerError("GetRunnerByID", err)
return
}
ctx.Data["Runner"] = runner
// TODO: get task list for this runner
ctx.HTML(http.StatusOK, tplRunnerEdit)
} }
// EditRunnerPost response for editing runner // EditRunnerPost response for editing runner
func EditRunnerPost(ctx *context.Context) { func EditRunnerPost(ctx *context.Context) {
runner, err := bots_model.GetRunnerByID(ctx.ParamsInt64(":runnerid"))
if err != nil {
log.Warn("EditRunnerPost.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL)
ctx.ServerError("EditRunnerPost.GetRunnerByID", err)
return
}
form := web.GetForm(ctx).(*forms.AdminEditRunnerForm)
runner.Description = form.Description
runner.CustomLabels = strings.Split(form.CustomLabels, ",")
err = bots_model.UpdateRunner(ctx, runner, "description", "custom_labels")
if err != nil {
log.Warn("EditRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
ctx.Flash.Warning(ctx.Tr("admin.runners.update_runner_failed"))
ctx.Redirect(setting.AppSubURL + "/admin/runners/" + url.PathEscape(ctx.Params(":runnerid")))
return
}
ctx.Data["Title"] = ctx.Tr("admin.runners.edit") ctx.Data["Title"] = ctx.Tr("admin.runners.edit")
ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminRunners"] = true ctx.Data["PageIsAdminRunners"] = true
common.RunnerDetailsEditPost(ctx, ctx.ParamsInt64(":runnerid"),
log.Debug("EditRunnerPost success: %s", ctx.Req.URL) setting.AppSubURL+"/admin/runners/"+url.PathEscape(ctx.Params(":runnerid")))
ctx.Flash.Success(ctx.Tr("admin.runners.update_runner_success"))
ctx.Redirect(setting.AppSubURL + "/admin/runners/" + url.PathEscape(ctx.Params(":runnerid")))
} }
// DeleteRunner response for deleting a runner // DeleteRunner response for deleting a runner
@ -163,14 +91,7 @@ func DeleteRunnerPost(ctx *context.Context) {
} }
func ResetRunnerRegistrationToken(ctx *context.Context) { func ResetRunnerRegistrationToken(ctx *context.Context) {
_, err := bots_model.NewRunnerToken(0, 0) common.RunnerResetRegistrationToken(ctx, 0, 0, setting.AppSubURL+"/admin/runners/")
if err != nil {
ctx.ServerError("ResetRunnerRegistrationToken", err)
return
}
ctx.Flash.Success(ctx.Tr("admin.runners.reset_registration_token_success"))
ctx.Redirect(setting.AppSubURL + "/admin/runners/")
} }
/** /**

View File

@ -1,26 +1,41 @@
package org package org
import ( import (
"net/http" bots_model "code.gitea.io/gitea/models/bots"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/common"
) )
// Runners render runners page // Runners render runners page
func Runners(ctx *context.Context) { func Runners(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings") ctx.Data["Title"] = ctx.Tr("org.runners")
ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsOrgSettingsRunners"] = true ctx.Data["PageIsOrgSettingsRunners"] = true
ctx.Data["BaseLink"] = ctx.Org.OrgLink + "/settings/runners"
ctx.Data["Description"] = ctx.Tr("org.settings.runners_desc")
ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OrgID: ctx.Org.Organization.ID}) page := ctx.FormInt("page")
if err != nil { if page <= 1 {
ctx.ServerError("GetWebhooksByOrgId", err) page = 1
return
} }
ctx.Data["Webhooks"] = ws opts := bots_model.FindRunnerOptions{
ctx.HTML(http.StatusOK, tplSettingsRunners) ListOptions: db.ListOptions{
Page: page,
PageSize: 100,
},
Sort: ctx.Req.URL.Query().Get("sort"),
Filter: ctx.Req.URL.Query().Get("q"),
WithDeleted: false,
RepoID: 0,
OwnerID: ctx.Org.Organization.ID,
}
common.RunnersList(ctx, tplSettingsRunners, opts)
}
// ResetRunnerRegistrationToken reset runner registration token
func ResetRunnerRegistrationToken(ctx *context.Context) {
common.RunnerResetRegistrationToken(ctx,
ctx.Org.Organization.ID, 0,
ctx.Org.OrgLink+"/settings/runners")
} }

View File

@ -628,7 +628,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/runners", func() { m.Group("/runners", func() {
m.Get("", admin.Runners) m.Get("", admin.Runners)
m.Get("/reset_registration_token", admin.ResetRunnerRegistrationToken) m.Get("/reset_registration_token", admin.ResetRunnerRegistrationToken)
m.Combo("/{runnerid}").Get(admin.EditRunner).Post(bindIgnErr(forms.AdminEditRunnerForm{}), admin.EditRunnerPost) m.Combo("/{runnerid}").Get(admin.EditRunner).Post(bindIgnErr(forms.EditRunnerForm{}), admin.EditRunnerPost)
m.Post("/{runnerid}/delete", admin.DeleteRunnerPost) m.Post("/{runnerid}/delete", admin.DeleteRunnerPost)
}) })
}, func(ctx *context.Context) { }, func(ctx *context.Context) {
@ -799,6 +799,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/runners", func() { m.Group("/runners", func() {
m.Get("", org.Runners) m.Get("", org.Runners)
m.Get("/reset_registration_token", org.ResetRunnerRegistrationToken)
}) })
m.Route("/delete", "GET,POST", org.SettingsDelete) m.Route("/delete", "GET,POST", org.SettingsDelete)

View File

@ -71,27 +71,3 @@ func (f *AdminDashboardForm) Validate(req *http.Request, errs binding.Errors) bi
ctx := context.GetContext(req) ctx := context.GetContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
} }
// AdminCreateRunnerForm form for admin to create runner
type AdminCreateRunnerForm struct {
Name string `binding:"Required"`
Type string
}
// Validate validates form fields
func (f *AdminCreateRunnerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// AdminEditRunnerForm form for admin to create runner
type AdminEditRunnerForm struct {
Description string
CustomLabels string
}
// Validate validates form fields
func (f *AdminEditRunnerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}

33
services/forms/runner.go Normal file
View File

@ -0,0 +1,33 @@
package forms
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware"
"gitea.com/go-chi/binding"
)
// EditRunnerForm form for admin to create runner
type EditRunnerForm struct {
Description string
CustomLabels string
}
// Validate validates form fields
func (f *EditRunnerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// CreateRunnerForm form for admin to create runner
type CreateRunnerForm struct {
Name string `binding:"Required"`
Type string
}
// Validate validates form fields
func (f *CreateRunnerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}

View File

@ -0,0 +1,8 @@
{{template "base/head" .}}
<div class="page-content admin runners">
{{template "admin/navbar" .}}
<div class="ui container">
{{template "runners/list" .}}
</div>
</div>
{{template "base/footer" .}}

View File

@ -0,0 +1,85 @@
{{template "base/head" .}}
<div class="page-content admin runners">
{{template "admin/navbar" .}}
<div class="ui container">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.locale.Tr "admin.runners.runner_title"}} #{{.Runner.ID}} {{.Runner.Name}}
</h4>
<div class="ui attached segment">
<form class="ui form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}}
<div class="runner-basic-info">
<div class="field dib">
<label>{{.locale.Tr "admin.runners.status"}}</label>
<span class="runner-status-{{.Runner.StatusType}}">{{.Runner.StatusType}}</span>
</div>
<div class="field dib">
<label>{{.locale.Tr "admin.runners.latest_online"}}</label>
<span>{{TimeSinceUnix .Runner.LastOnline $.locale}}</span>
</div>
<div class="field dib">
<label>{{.locale.Tr "admin.runners.agent_labels"}}</label>
<span>
{{range .Runner.AgentLabels}}
<span>{{.}}</span>
{{end}}
</span>
</div>
<div class="field dib">
<label>{{.locale.Tr "admin.runners.owner_type"}}</label>
<span>{{.Runner.OwnType}}</span>
</div>
</div>
<div class="ui divider"></div>
<div class="field">
<label for="description">{{.locale.Tr "admin.runners.description"}}</label>
<input id="description" name="description" value="{{.Runner.Description}}">
</div>
<div class="field">
<label for="custom_labels">{{.locale.Tr "admin.runners.custom_labels"}}</label>
<input id="custom_labels" name="custom_labels" value="{{Join .Runner.CustomLabels `,`}}">
<p class="help">{{.locale.Tr "admin.runners.custom_labels_helper"}}</p>
</div>
<div class="ui divider"></div>
<div class="field">
<button type="submit" class="ui green button">{{.locale.Tr "admin.runners.update_runner"}}</button>
<button type="button" class="ui red button delete-button show-modal" data-modal="#runner-delete-modal">
{{.locale.Tr "admin.runners.delete_runner"}}</button>
</div>
</form>
</div>
<h4 class="ui top attached header">
{{.locale.Tr "admin.runners.task_list"}}
</h4>
<div class="ui attached segment">
Comming soon
</div>
</div>
</div>
<div class="ui small modal" id="runner-delete-modal">
<div class="header">
{{.locale.Tr "admin.runners.delete_runner_header"}}
</div>
<div class="content">
<div class="ui warning message text left">
{{.locale.Tr "admin.runners.delete_runner_notice" | Safe}}
</div>
<form class="ui form" action="{{.Link}}/delete" method="post">
{{.CsrfTokenHtml}}
<input type="hidden" name="action" value="delete">
<div class="text right actions">
<div class="ui cancel button">{{.locale.Tr "settings.cancel"}}</div>
<button class="ui red button">{{.locale.Tr "admin.runners.delete_runner_confirm"}}</button>
</div>
</form>
</div>
</div>
{{template "base/footer" .}}

View File

@ -5,7 +5,7 @@
<div class="ui grid"> <div class="ui grid">
{{template "org/settings/navbar" .}} {{template "org/settings/navbar" .}}
<div class="twelve wide column content"> <div class="twelve wide column content">
{{template "repo/settings/webhook/list" .}} {{template "runners/list" .}}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,92 +1,91 @@
{{template "base/head" .}} <div class="runner-container">
<div class="page-content admin runners"> {{template "base/alert" .}}
{{template "admin/navbar" .}}
<div class="ui container">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.locale.Tr "admin.runners.runner_manage_panel"}} ({{.locale.Tr "admin.total" .Total}})
<div class="ui right">
<!--<a class="ui primary tiny button" href="{{AppSubUrl}}/admin/runners/new">{{.locale.Tr "admin.runners.new"}}</a>-->
<div class="ui top right pointing dropdown"> <h4 class="ui top attached header">
<button class="ui button primary"> {{.locale.Tr "admin.runners.runner_manage_panel"}} ({{.locale.Tr "admin.total" .Total}})
{{.locale.Tr "admin.runners.new"}} <div class="ui right">
<i class="dropdown icon"></i> <!--<a class="ui primary tiny button" href="{{AppSubUrl}}/admin/runners/new">{{.locale.Tr "admin.runners.new"}}</a>-->
</button>
<div class="menu"> <div class="ui top right pointing dropdown">
<div class="item"> <button class="ui button primary">
<a href="#">{{.locale.Tr "admin.runners.new_notice"}}</a> {{.locale.Tr "admin.runners.new"}}
</div> <i class="dropdown icon"></i>
<div class="divider"></div> </button>
<div class="header"> <div class="menu">
Registration Token <div class="item">
</div> <a href="#">{{.locale.Tr "admin.runners.new_notice"}}</a>
<div class="ui input"> </div>
<input type="text" value="{{.RegistrationToken}}"> <div class="divider"></div>
<div class="ui basic label button" data-clipboard-text="{{.RegistrationToken}}"> <div class="header">
{{svg "octicon-copy" 14}} Registration Token
</div> </div>
</div> <div class="ui input">
<div class="divider"></div> <input type="text" value="{{.RegistrationToken}}">
<div class="item"> <div class="ui basic label button" data-clipboard-text="{{.RegistrationToken}}">
<a href="{{$.Link}}/reset_registration_token">Reset registration token</a> {{svg "octicon-copy" 14}}
</div> </div>
</div> </div>
<div class="divider"></div>
<div class="item">
<a href="{{$.Link}}/reset_registration_token">Reset registration token</a>
</div>
</div> </div>
</div> </div>
</h4>
<div class="ui attached segment">
<form class="ui form ignore-dirty" id="user-list-search-form" action="{{$.Link}}">
<!-- Search Text -->
<div class="ui fluid action input" style="max-width: 70%;">
<input name="q" value="{{.Keyword}}" placeholder="{{.locale.Tr "explore.search"}}..." autofocus>
<button class="ui primary button">{{.locale.Tr "explore.search"}}</button>
</div>
</form>
</div>
<div class="ui attached table segment">
<table class="ui very basic striped table unstackable">
<thead>
<tr>
<th data-sortt-asc="online" data-sortt-desc="offline">{{.locale.Tr "admin.runners.status"}}</th>
<th data-sortt-asc="alphabetically">
{{.locale.Tr "admin.runners.id"}}
</th>
<th>{{.locale.Tr "admin.runners.owner_type"}}</th>
<th>{{.locale.Tr "admin.runners.labels"}}</th>
<th>{{.locale.Tr "admin.runners.latest_online"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Runners}}
<tr>
<td><span class="runner-status-{{.StatusType}}">{{.StatusType}}</span></td>
<td>
<a href="{{$.Link}}/{{.ID}}" class="tooltip" data-content="{{.Description}}">
<span class="runner-id">#{{.ID}}</span>
<span class="runner-name">{{.Name}}</span>
</a>
</td>
<td>{{.OwnType}}</td>
<td class="runner-tags">
{{range .AllLabels}}
<span class="ui label">{{.}}</span>
{{end}}
</td>
<td>{{TimeSinceUnix .LastOnline $.locale}}</td>
<td class="runner-ops">
<a href="{{$.Link}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
<!--<a class="runner-ops-delete show-modal" data-modal="#runner-delete-modal">{{svg "octicon-x-circle"}}</a>-->
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "base/paginate" .}} </div>
</h4>
<div class="ui attached segment">
<form class="ui form ignore-dirty" id="user-list-search-form" action="{{$.Link}}">
<!-- Search Text -->
<div class="ui fluid action input" style="max-width: 70%;">
<input name="q" value="{{.Keyword}}" placeholder="{{.locale.Tr "explore.search"}}..." autofocus>
<button class="ui primary button">{{.locale.Tr "explore.search"}}</button>
</div>
</form>
</div> </div>
<div class="ui attached table segment">
<table class="ui very basic striped table unstackable">
<thead>
<tr>
<th data-sortt-asc="online" data-sortt-desc="offline">{{.locale.Tr "admin.runners.status"}}</th>
<th data-sortt-asc="alphabetically">
{{.locale.Tr "admin.runners.id"}}
</th>
<th>{{.locale.Tr "admin.runners.owner_type"}}</th>
<th>{{.locale.Tr "admin.runners.labels"}}</th>
<th>{{.locale.Tr "admin.runners.latest_online"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Runners}}
<tr>
<td><span class="runner-status-{{.StatusType}}">{{.StatusType}}</span></td>
<td>
<a href="{{$.Link}}/{{.ID}}" class="tooltip" data-content="{{.Description}}">
<span class="runner-id">#{{.ID}}</span>
<span class="runner-name">{{.Name}}</span>
</a>
</td>
<td>{{.OwnType}}</td>
<td class="runner-tags">
{{range .AllLabels}}
<span class="ui label">{{.}}</span>
{{end}}
</td>
<td>{{TimeSinceUnix .LastOnline $.locale}}</td>
<td class="runner-ops">
{{if .Editable $.RunnerOnwerID $.RunnerRepoID}}
<a href="{{$.Link}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
<!--<a class="runner-ops-delete show-modal" data-modal="#runner-delete-modal">{{svg "octicon-x-circle"}}</a>-->
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "base/paginate" .}}
</div> </div>
{{template "base/footer" .}}