mirror of
https://github.com/gogs/gogs.git
synced 2025-05-24 08:22:34 +00:00
repo/webhook: able to retrigger delivery history (#2187)
This commit is contained in:
parent
55a5ad5cdc
commit
2807274e2d
@ -460,12 +460,16 @@ func runWeb(ctx *cli.Context) error {
|
||||
m.Post("/gogs/new", bindIgnErr(form.NewWebhook{}), repo.WebHooksNewPost)
|
||||
m.Post("/slack/new", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksNewPost)
|
||||
m.Post("/discord/new", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksNewPost)
|
||||
m.Get("/:id", repo.WebHooksEdit)
|
||||
m.Post("/:id/test", repo.TestWebhook)
|
||||
m.Post("/gogs/:id", bindIgnErr(form.NewWebhook{}), repo.WebHooksEditPost)
|
||||
m.Post("/slack/:id", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksEditPost)
|
||||
m.Post("/discord/:id", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksEditPost)
|
||||
|
||||
m.Group("/:id", func() {
|
||||
m.Get("", repo.WebHooksEdit)
|
||||
m.Post("/test", repo.TestWebhook)
|
||||
m.Post("/redelivery", repo.RedeliveryWebhook)
|
||||
})
|
||||
|
||||
m.Group("/git", func() {
|
||||
m.Get("", repo.SettingsGitHooks)
|
||||
m.Combo("/:name").Get(repo.SettingsGitHooksEdit).
|
||||
|
@ -742,6 +742,8 @@ settings.webhook_deletion_success = Webhook has been deleted successfully!
|
||||
settings.webhook.test_delivery = Test Delivery
|
||||
settings.webhook.test_delivery_desc = Send a fake push event delivery to test your webhook settings
|
||||
settings.webhook.test_delivery_success = Test webhook has been added to delivery queue. It may take few seconds before it shows up in the delivery history.
|
||||
settings.webhook.redelivery = Redelivery
|
||||
settings.webhook.redelivery_success = Hook task '%s' has been readded to delivery queue. It may take few seconds to update delivery status in history.
|
||||
settings.webhook.request = Request
|
||||
settings.webhook.response = Response
|
||||
settings.webhook.headers = Headers
|
||||
|
2
gogs.go
2
gogs.go
@ -16,7 +16,7 @@ import (
|
||||
"github.com/gogits/gogs/modules/setting"
|
||||
)
|
||||
|
||||
const APP_VER = "0.10.23.0318"
|
||||
const APP_VER = "0.10.24.0319"
|
||||
|
||||
func init() {
|
||||
setting.AppVer = APP_VER
|
||||
|
@ -436,26 +436,6 @@ func (err ErrBranchNotExist) Error() string {
|
||||
return fmt.Sprintf("branch does not exist [name: %s]", err.Name)
|
||||
}
|
||||
|
||||
// __ __ ___. .__ __
|
||||
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
|
||||
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
|
||||
// \ /\ ___/| \_\ \ Y ( <_> | <_> ) <
|
||||
// \__/\ / \___ >___ /___| /\____/ \____/|__|_ \
|
||||
// \/ \/ \/ \/ \/
|
||||
|
||||
type ErrWebhookNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
func IsErrWebhookNotExist(err error) bool {
|
||||
_, ok := err.(ErrWebhookNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrWebhookNotExist) Error() string {
|
||||
return fmt.Sprintf("webhook does not exist [id: %d]", err.ID)
|
||||
}
|
||||
|
||||
// .___
|
||||
// | | ______ ________ __ ____
|
||||
// | |/ ___// ___/ | \_/ __ \
|
||||
|
34
models/errors/webhook.go
Normal file
34
models/errors/webhook.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2017 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
type WebhookNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
func IsWebhookNotExist(err error) bool {
|
||||
_, ok := err.(WebhookNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err WebhookNotExist) Error() string {
|
||||
return fmt.Sprintf("webhook does not exist [id: %d]", err.ID)
|
||||
}
|
||||
|
||||
type HookTaskNotExist struct {
|
||||
HookID int64
|
||||
UUID string
|
||||
}
|
||||
|
||||
func IsHookTaskNotExist(err error) bool {
|
||||
_, ok := err.(HookTaskNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err HookTaskNotExist) Error() string {
|
||||
return fmt.Sprintf("hook task does not exist [hook_id: %d, uuid: %s]", err.HookID, err.UUID)
|
||||
}
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
api "github.com/gogits/go-gogs-client"
|
||||
|
||||
"github.com/gogits/gogs/models/errors"
|
||||
"github.com/gogits/gogs/modules/httplib"
|
||||
"github.com/gogits/gogs/modules/setting"
|
||||
"github.com/gogits/gogs/modules/sync"
|
||||
@ -241,7 +242,7 @@ func getWebhook(bean *Webhook) (*Webhook, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrWebhookNotExist{bean.ID}
|
||||
return nil, errors.WebhookNotExist{bean.ID}
|
||||
}
|
||||
return bean, nil
|
||||
}
|
||||
@ -494,6 +495,21 @@ func createHookTask(e Engine, t *HookTask) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetHookTaskOfWebhookByUUID returns hook task of given webhook by UUID.
|
||||
func GetHookTaskOfWebhookByUUID(webhookID int64, uuid string) (*HookTask, error) {
|
||||
hookTask := &HookTask{
|
||||
HookID: webhookID,
|
||||
UUID: uuid,
|
||||
}
|
||||
has, err := x.Get(hookTask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, errors.HookTaskNotExist{webhookID, uuid}
|
||||
}
|
||||
return hookTask, nil
|
||||
}
|
||||
|
||||
// UpdateHookTask updates information of hook task.
|
||||
func UpdateHookTask(t *HookTask) error {
|
||||
_, err := x.Id(t.ID).AllCols().Update(t)
|
||||
@ -704,7 +720,7 @@ func (t *HookTask) deliver() {
|
||||
// TODO: shoot more hooks at same time.
|
||||
func DeliverHooks() {
|
||||
tasks := make([]*HookTask, 0, 10)
|
||||
x.Where("is_delivered=?", false).Iterate(new(HookTask),
|
||||
x.Where("is_delivered = ?", false).Iterate(new(HookTask),
|
||||
func(idx int, bean interface{}) error {
|
||||
t := bean.(*HookTask)
|
||||
t.deliver()
|
||||
@ -725,7 +741,7 @@ func DeliverHooks() {
|
||||
HookQueue.Remove(repoID)
|
||||
|
||||
tasks = make([]*HookTask, 0, 5)
|
||||
if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil {
|
||||
if err := x.Where("repo_id = ?", repoID).And("is_delivered = ?", false).Find(&tasks); err != nil {
|
||||
log.Error(4, "Get repository [%s] hook tasks: %v", repoID, err)
|
||||
continue
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -2375,6 +2375,15 @@ footer .ui.language .menu {
|
||||
margin-left: 26px;
|
||||
padding-top: 0;
|
||||
}
|
||||
.webhook .hook.history.list .right.menu .redelivery.button {
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
height: 30px;
|
||||
}
|
||||
.webhook .hook.history.list .right.menu .redelivery.button .octicon {
|
||||
font: normal normal normal 13px/1 Octicons;
|
||||
width: 12px;
|
||||
}
|
||||
.user-cards .list {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -513,20 +513,6 @@ function initRepository() {
|
||||
}
|
||||
}
|
||||
|
||||
function initRepositoryCollaboration() {
|
||||
console.log('initRepositoryCollaboration');
|
||||
|
||||
// Change collaborator access mode
|
||||
$('.access-mode.menu .item').click(function () {
|
||||
var $menu = $(this).parent();
|
||||
$.post($menu.data('url'), {
|
||||
"_csrf": csrf,
|
||||
"uid": $menu.data('uid'),
|
||||
"mode": $(this).data('value')
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function initWikiForm() {
|
||||
var $editArea = $('.repository.wiki textarea#edit_area');
|
||||
if ($editArea.length > 0) {
|
||||
@ -828,61 +814,6 @@ function initOrganization() {
|
||||
}
|
||||
}
|
||||
|
||||
function initUserSettings() {
|
||||
console.log('initUserSettings');
|
||||
|
||||
// Options
|
||||
if ($('.user.settings.profile').length > 0) {
|
||||
$('#username').keyup(function () {
|
||||
var $prompt = $('#name-change-prompt');
|
||||
if ($(this).val().toString().toLowerCase() != $(this).data('name').toString().toLowerCase()) {
|
||||
$prompt.show();
|
||||
} else {
|
||||
$prompt.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initWebhook() {
|
||||
if ($('.new.webhook').length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('.events.checkbox input').change(function () {
|
||||
if ($(this).is(':checked')) {
|
||||
$('.events.fields').show();
|
||||
}
|
||||
});
|
||||
$('.non-events.checkbox input').change(function () {
|
||||
if ($(this).is(':checked')) {
|
||||
$('.events.fields').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Highlight payload on first click
|
||||
$('.hook.history.list .toggle.button').click(function () {
|
||||
$($(this).data('target') + ' .nohighlight').each(function () {
|
||||
var $this = $(this);
|
||||
$this.removeClass('nohighlight');
|
||||
setTimeout(function(){ hljs.highlightBlock($this[0]) }, 500);
|
||||
})
|
||||
})
|
||||
|
||||
// Test delivery
|
||||
$('#test-delivery').click(function () {
|
||||
var $this = $(this);
|
||||
$this.addClass('loading disabled');
|
||||
$.post($this.data('link'), {
|
||||
"_csrf": csrf
|
||||
}).done(
|
||||
setTimeout(function () {
|
||||
window.location.href = $this.data('redirect');
|
||||
}, 5000)
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
function initAdmin() {
|
||||
if ($('.admin').length == 0) {
|
||||
return;
|
||||
@ -1152,6 +1083,71 @@ function initCodeView() {
|
||||
}
|
||||
}
|
||||
|
||||
function initUserSettings() {
|
||||
console.log('initUserSettings');
|
||||
|
||||
// Options
|
||||
if ($('.user.settings.profile').length > 0) {
|
||||
$('#username').keyup(function () {
|
||||
var $prompt = $('#name-change-prompt');
|
||||
if ($(this).val().toString().toLowerCase() != $(this).data('name').toString().toLowerCase()) {
|
||||
$prompt.show();
|
||||
} else {
|
||||
$prompt.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initRepositoryCollaboration() {
|
||||
console.log('initRepositoryCollaboration');
|
||||
|
||||
// Change collaborator access mode
|
||||
$('.access-mode.menu .item').click(function () {
|
||||
var $menu = $(this).parent();
|
||||
$.post($menu.data('url'), {
|
||||
"_csrf": csrf,
|
||||
"uid": $menu.data('uid'),
|
||||
"mode": $(this).data('value')
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function initWebhookSettings() {
|
||||
$('.events.checkbox input').change(function () {
|
||||
if ($(this).is(':checked')) {
|
||||
$('.events.fields').show();
|
||||
}
|
||||
});
|
||||
$('.non-events.checkbox input').change(function () {
|
||||
if ($(this).is(':checked')) {
|
||||
$('.events.fields').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Highlight payload on first click
|
||||
$('.hook.history.list .toggle.button').click(function () {
|
||||
$($(this).data('target') + ' .nohighlight').each(function () {
|
||||
var $this = $(this);
|
||||
$this.removeClass('nohighlight');
|
||||
setTimeout(function(){ hljs.highlightBlock($this[0]) }, 500);
|
||||
})
|
||||
})
|
||||
|
||||
// Trigger delivery
|
||||
$('.delivery.button, .redelivery.button').click(function () {
|
||||
var $this = $(this);
|
||||
$this.addClass('loading disabled');
|
||||
$.post($this.data('link'), {
|
||||
"_csrf": csrf
|
||||
}).done(
|
||||
setTimeout(function () {
|
||||
window.location.href = $this.data('redirect');
|
||||
}, 5000)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
csrf = $('meta[name=_csrf]').attr("content");
|
||||
suburl = $('meta[name=_suburl]').attr("content");
|
||||
@ -1359,7 +1355,6 @@ $(document).ready(function () {
|
||||
initEditForm();
|
||||
initEditor();
|
||||
initOrganization();
|
||||
initWebhook();
|
||||
initAdmin();
|
||||
initCodeView();
|
||||
|
||||
@ -1379,7 +1374,8 @@ $(document).ready(function () {
|
||||
|
||||
var routes = {
|
||||
'div.user.settings': initUserSettings,
|
||||
'div.repository.settings.collaboration': initRepositoryCollaboration
|
||||
'div.repository.settings.collaboration': initRepositoryCollaboration,
|
||||
'div.webhook.settings': initWebhookSettings
|
||||
};
|
||||
|
||||
var selector;
|
||||
|
@ -1411,6 +1411,21 @@
|
||||
}
|
||||
// End of .repository
|
||||
|
||||
// Should apply organization webhooks page
|
||||
.webhook .hook.history.list {
|
||||
.right.menu {
|
||||
.redelivery.button {
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
height: 30px;
|
||||
.octicon {
|
||||
font: normal normal normal 13px/1 Octicons;
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.user-cards {
|
||||
.list {
|
||||
padding: 0;
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
api "github.com/gogits/go-gogs-client"
|
||||
|
||||
"github.com/gogits/gogs/models"
|
||||
"github.com/gogits/gogs/models/errors"
|
||||
"github.com/gogits/gogs/modules/context"
|
||||
"github.com/gogits/gogs/routers/api/v1/convert"
|
||||
)
|
||||
@ -106,7 +107,7 @@ func CreateHook(ctx *context.APIContext, form api.CreateHookOption) {
|
||||
func EditHook(ctx *context.APIContext, form api.EditHookOption) {
|
||||
w, err := models.GetWebhookOfRepoByID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if models.IsErrWebhookNotExist(err) {
|
||||
if errors.IsWebhookNotExist(err) {
|
||||
ctx.Status(404)
|
||||
} else {
|
||||
ctx.Error(500, "GetWebhookOfRepoByID", err)
|
||||
|
@ -55,6 +55,7 @@ type OrgRepoCtx struct {
|
||||
// getOrgRepoCtx determines whether this is a repo context or organization context.
|
||||
func getOrgRepoCtx(ctx *context.Context) (*OrgRepoCtx, error) {
|
||||
if len(ctx.Repo.RepoLink) > 0 {
|
||||
ctx.Data["PageIsRepositoryContext"] = true
|
||||
return &OrgRepoCtx{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Link: ctx.Repo.RepoLink,
|
||||
@ -63,6 +64,7 @@ func getOrgRepoCtx(ctx *context.Context) (*OrgRepoCtx, error) {
|
||||
}
|
||||
|
||||
if len(ctx.Org.OrgLink) > 0 {
|
||||
ctx.Data["PageIsOrganizationContext"] = true
|
||||
return &OrgRepoCtx{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
Link: ctx.Org.OrgLink,
|
||||
@ -284,11 +286,7 @@ func checkWebhook(ctx *context.Context) (*OrgRepoCtx, *models.Webhook) {
|
||||
w, err = models.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
|
||||
}
|
||||
if err != nil {
|
||||
if models.IsErrWebhookNotExist(err) {
|
||||
ctx.Handle(404, "GetWebhookByID", nil)
|
||||
} else {
|
||||
ctx.Handle(500, "GetWebhookByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetWebhookOfRepoByID/GetWebhookByOrgID", errors.IsWebhookNotExist, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -469,8 +467,7 @@ func TestWebhook(ctx *context.Context) {
|
||||
if err == nil {
|
||||
authorUsername = author.Name
|
||||
} else if !errors.IsUserNotExist(err) {
|
||||
ctx.Flash.Error(fmt.Sprintf("GetUserByEmail.(author) [%s]: %v", commit.Author.Email, err))
|
||||
ctx.Status(500)
|
||||
ctx.Handle(500, "GetUserByEmail.(author)", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -478,16 +475,14 @@ func TestWebhook(ctx *context.Context) {
|
||||
if err == nil {
|
||||
committerUsername = committer.Name
|
||||
} else if !errors.IsUserNotExist(err) {
|
||||
ctx.Flash.Error(fmt.Sprintf("GetUserByEmail.(committer) [%s]: %v", commit.Committer.Email, err))
|
||||
ctx.Status(500)
|
||||
ctx.Handle(500, "GetUserByEmail.(committer)", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fileStatus, err := commit.FileStatus()
|
||||
if err != nil {
|
||||
ctx.Flash.Error("FileStatus: " + err.Error())
|
||||
ctx.Status(500)
|
||||
ctx.Handle(500, "FileStatus", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -520,15 +515,37 @@ func TestWebhook(ctx *context.Context) {
|
||||
Pusher: apiUser,
|
||||
Sender: apiUser,
|
||||
}
|
||||
if err := models.TestWebhook(ctx.Repo.Repository, models.HOOK_EVENT_PUSH, p, ctx.QueryInt64("id")); err != nil {
|
||||
ctx.Flash.Error("TestWebhook: " + err.Error())
|
||||
ctx.Status(500)
|
||||
if err := models.TestWebhook(ctx.Repo.Repository, models.HOOK_EVENT_PUSH, p, ctx.ParamsInt64("id")); err != nil {
|
||||
ctx.Handle(500, "TestWebhook", err)
|
||||
} else {
|
||||
ctx.Flash.Info(ctx.Tr("repo.settings.webhook.test_delivery_success"))
|
||||
ctx.Status(200)
|
||||
}
|
||||
}
|
||||
|
||||
func RedeliveryWebhook(ctx *context.Context) {
|
||||
webhook, err := models.GetWebhookOfRepoByID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetWebhookOfRepoByID/GetWebhookByOrgID", errors.IsWebhookNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
hookTask, err := models.GetHookTaskOfWebhookByUUID(webhook.ID, ctx.Query("uuid"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetHookTaskOfWebhookByUUID/GetWebhookByOrgID", errors.IsHookTaskNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
hookTask.IsDelivered = false
|
||||
if err = models.UpdateHookTask(hookTask); err != nil {
|
||||
ctx.Handle(500, "UpdateHookTask", err)
|
||||
} else {
|
||||
go models.HookQueue.Add(ctx.Repo.Repository.ID)
|
||||
ctx.Flash.Info(ctx.Tr("repo.settings.webhook.redelivery_success", hookTask.UUID))
|
||||
ctx.Status(200)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteWebhook(ctx *context.Context) {
|
||||
if err := models.DeleteWebhookOfRepoByID(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeleteWebhookByRepoID: " + err.Error())
|
||||
|
@ -1 +1 @@
|
||||
0.10.23.0318
|
||||
0.10.24.0319
|
@ -11,8 +11,8 @@
|
||||
<div class="ui right">
|
||||
{{if eq .HookType "gogs"}}
|
||||
<img class="img-13" src="{{AppSubUrl}}/img/favicon.png">
|
||||
{{else if eq .HookType "slack"}}
|
||||
<img class="img-13" src="{{AppSubUrl}}/img/slack.png">
|
||||
{{else}}
|
||||
<img class="img-13" src="{{AppSubUrl}}/img/{{.HookType}}.png">
|
||||
{{end}}
|
||||
</div>
|
||||
</h4>
|
||||
|
@ -1,10 +1,10 @@
|
||||
{{if .PageIsSettingsHooksEdit}}
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "repo.settings.recent_deliveries"}}
|
||||
{{if .IsRepositoryAdmin}}
|
||||
{{if .PageIsRepositoryContext}}
|
||||
<div class="ui right">
|
||||
<button class="ui teal tiny button poping up" id="test-delivery" data-content=
|
||||
"{{.i18n.Tr "repo.settings.webhook.test_delivery_desc"}}" data-variation="inverted tiny" data-link="{{.Link}}/test?id={{.Webhook.ID}}" data-redirect="{{.Link}}">{{.i18n.Tr "repo.settings.webhook.test_delivery"}}</button>
|
||||
<button class="ui teal tiny delivery button poping up" data-content=
|
||||
"{{.i18n.Tr "repo.settings.webhook.test_delivery_desc"}}" data-variation="inverted tiny" data-link="{{.Link}}/test" data-redirect="{{.Link}}">{{.i18n.Tr "repo.settings.webhook.test_delivery"}}</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</h4>
|
||||
@ -40,6 +40,11 @@
|
||||
<span class="ui label">N/A</span>
|
||||
{{end}}
|
||||
</a>
|
||||
{{if $.PageIsRepositoryContext}}
|
||||
<div class="right menu">
|
||||
<div class="ui basic redelivery button" data-link="{{$.Link}}/redelivery?uuid={{.UUID}}" data-redirect="{{$.Link}}"><i class="octicon octicon-sync"></i> <span>{{$.i18n.Tr "repo.settings.webhook.redelivery"}}</span></div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment active" data-tab="request-{{.ID}}">
|
||||
{{if .RequestInfo}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user