mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 23:02:02 +00:00
Add task list page
This commit is contained in:
parent
e862737699
commit
6d820149cf
@ -2,10 +2,12 @@ package docker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"io"
|
"github.com/cuigh/auxo/util/choose"
|
||||||
|
"github.com/cuigh/swirl/misc"
|
||||||
"github.com/cuigh/swirl/model"
|
"github.com/cuigh/swirl/model"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
@ -14,35 +16,50 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TaskList return all running tasks of a service or a node.
|
// TaskList return all running tasks of a service or a node.
|
||||||
func TaskList(service, node string) (infos []*model.TaskInfo, err error) {
|
func TaskList(args *model.TaskListArgs) (infos []*model.TaskInfo, totalCount int, err error) {
|
||||||
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||||
var (
|
var (
|
||||||
tasks []swarm.Task
|
tasks []swarm.Task
|
||||||
|
opts = types.TaskListOptions{
|
||||||
|
Filters: filters.NewArgs(),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
opts := types.TaskListOptions{
|
if args.PageIndex < 1 {
|
||||||
Filters: filters.NewArgs(),
|
args.PageIndex = 1
|
||||||
}
|
}
|
||||||
if service != "" {
|
if args.PageSize < 1 {
|
||||||
opts.Filters.Add("service", service)
|
args.PageSize = math.MaxInt32
|
||||||
}
|
}
|
||||||
if node != "" {
|
if args.Service != "" {
|
||||||
opts.Filters.Add("node", node)
|
opts.Filters.Add("service", args.Service)
|
||||||
}
|
}
|
||||||
|
if args.Node != "" {
|
||||||
|
opts.Filters.Add("node", args.Node)
|
||||||
|
}
|
||||||
|
if args.Name != "" {
|
||||||
|
opts.Filters.Add("name", args.Name)
|
||||||
|
}
|
||||||
|
if args.State != "" {
|
||||||
|
opts.Filters.Add("desired-state", args.State)
|
||||||
|
}
|
||||||
|
|
||||||
tasks, err = cli.TaskList(ctx, opts)
|
tasks, err = cli.TaskList(ctx, opts)
|
||||||
if err == nil && len(tasks) > 0 {
|
totalCount = len(tasks)
|
||||||
|
if err == nil && totalCount > 0 {
|
||||||
sort.Slice(tasks, func(i, j int) bool {
|
sort.Slice(tasks, func(i, j int) bool {
|
||||||
return tasks[i].UpdatedAt.After(tasks[j].UpdatedAt)
|
return tasks[i].UpdatedAt.After(tasks[j].UpdatedAt)
|
||||||
})
|
})
|
||||||
|
start, end := misc.Page(totalCount, args.PageIndex, args.PageSize)
|
||||||
|
tasks = tasks[start:end]
|
||||||
|
|
||||||
nodes := make(map[string]string)
|
nodes := make(map[string]string)
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
if _, ok := nodes[t.NodeID]; !ok {
|
if _, ok := nodes[t.NodeID]; !ok {
|
||||||
if n, _, e := cli.NodeInspectWithRaw(ctx, t.NodeID); e == nil {
|
if n, _, e := cli.NodeInspectWithRaw(ctx, t.NodeID); e == nil {
|
||||||
nodes[t.NodeID] = n.Description.Hostname
|
nodes[t.NodeID] = choose.String(n.Spec.Name == "", n.Description.Hostname, n.Spec.Name)
|
||||||
} else {
|
} else {
|
||||||
nodes[t.NodeID] = ""
|
nodes[t.NodeID] = ""
|
||||||
//mgr.Logger().Warnf("Node %s of task %s can't be load: %s", t.NodeID, t.ID, e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,6 +132,10 @@ stack.title: Stack
|
|||||||
stack.description: A stack is a logical grouping of related services that are usually deployed together and require each other to work as intended.
|
stack.description: A stack is a logical grouping of related services that are usually deployed together and require each other to work as intended.
|
||||||
stack.button.deploy: Deploy
|
stack.button.deploy: Deploy
|
||||||
|
|
||||||
|
# task pages
|
||||||
|
task.title: Task
|
||||||
|
task.description: A task is a container running on a swarm. It is the atomic scheduling unit of swarm.
|
||||||
|
|
||||||
# secret pages
|
# secret pages
|
||||||
secret.title: Secret
|
secret.title: Secret
|
||||||
secret.description: Secrets are sensitive data that can be used by services.
|
secret.description: Secrets are sensitive data that can be used by services.
|
||||||
|
@ -132,6 +132,10 @@ stack.title: 编排
|
|||||||
stack.description: 编排是相关服务的一个逻辑分组,这些服务通常互相依赖,需要一块部署。
|
stack.description: 编排是相关服务的一个逻辑分组,这些服务通常互相依赖,需要一块部署。
|
||||||
stack.button.deploy: 部署
|
stack.button.deploy: 部署
|
||||||
|
|
||||||
|
# task pages
|
||||||
|
task.title: 任务
|
||||||
|
task.description: 任务是运行在 Swarm 集群上的一个容器,它是 Swarm 调度的原子单元。
|
||||||
|
|
||||||
# secret pages
|
# secret pages
|
||||||
secret.title: 私密配置
|
secret.title: 私密配置
|
||||||
secret.description: 运行服务需要的敏感数据,如密钥。
|
secret.description: 运行服务需要的敏感数据,如密钥。
|
||||||
|
@ -52,7 +52,7 @@ func nodeDetail(ctx web.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, err := docker.TaskList("", id)
|
tasks, _, err := docker.TaskList(&model.TaskListArgs{Node: id})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ func serviceDetail(ctx web.Context) error {
|
|||||||
info.Networks = append(info.Networks, model.Network{ID: vip.NetworkID, Name: n.Name, Address: vip.Addr})
|
info.Networks = append(info.Networks, model.Network{ID: vip.NetworkID, Name: n.Name, Address: vip.Addr})
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, err := docker.TaskList(name, "")
|
tasks, _, err := docker.TaskList(&model.TaskListArgs{Service: name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,12 @@ import (
|
|||||||
"github.com/cuigh/auxo/net/web"
|
"github.com/cuigh/auxo/net/web"
|
||||||
"github.com/cuigh/swirl/biz/docker"
|
"github.com/cuigh/swirl/biz/docker"
|
||||||
"github.com/cuigh/swirl/misc"
|
"github.com/cuigh/swirl/misc"
|
||||||
|
"github.com/cuigh/swirl/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TaskController is a controller of swarm task
|
// TaskController is a controller of swarm task
|
||||||
type TaskController struct {
|
type TaskController struct {
|
||||||
|
List web.HandlerFunc `path:"/" name:"task.list" authorize:"!" desc:"task list page"`
|
||||||
Detail web.HandlerFunc `path:"/:id/detail" name:"task.detail" authorize:"!" desc:"task detail page"`
|
Detail web.HandlerFunc `path:"/:id/detail" name:"task.detail" authorize:"!" desc:"task detail page"`
|
||||||
Raw web.HandlerFunc `path:"/:id/raw" name:"task.raw" authorize:"!" desc:"task raw page"`
|
Raw web.HandlerFunc `path:"/:id/raw" name:"task.raw" authorize:"!" desc:"task raw page"`
|
||||||
}
|
}
|
||||||
@ -15,11 +17,34 @@ type TaskController struct {
|
|||||||
// Task creates an instance of TaskController
|
// Task creates an instance of TaskController
|
||||||
func Task() (c *TaskController) {
|
func Task() (c *TaskController) {
|
||||||
return &TaskController{
|
return &TaskController{
|
||||||
|
List: taskList,
|
||||||
Detail: taskDetail,
|
Detail: taskDetail,
|
||||||
Raw: taskRaw,
|
Raw: taskRaw,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func taskList(ctx web.Context) error {
|
||||||
|
args := &model.TaskListArgs{}
|
||||||
|
err := ctx.Bind(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
args.PageSize = model.PageSize
|
||||||
|
if args.PageIndex == 0 {
|
||||||
|
args.PageIndex = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, totalCount, err := docker.TaskList(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := newPagerModel(ctx, totalCount, args.PageSize, args.PageIndex).
|
||||||
|
Add("Args", args).
|
||||||
|
Add("Tasks", tasks)
|
||||||
|
return ctx.Render("task/list", m)
|
||||||
|
}
|
||||||
|
|
||||||
func taskDetail(ctx web.Context) error {
|
func taskDetail(ctx web.Context) error {
|
||||||
id := ctx.P("id")
|
id := ctx.P("id")
|
||||||
task, _, err := docker.TaskInspect(id)
|
task, _, err := docker.TaskInspect(id)
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Version is the version of Swirl
|
// Version is the version of Swirl
|
||||||
Version = "0.5.5"
|
Version = "0.5.6"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -87,6 +87,7 @@ var Perms = []PermGroup{
|
|||||||
{
|
{
|
||||||
Name: "Task",
|
Name: "Task",
|
||||||
Perms: []Perm{
|
Perms: []Perm{
|
||||||
|
{Key: "task.list", Text: "View list"},
|
||||||
{Key: "task.detail", Text: "View detail"},
|
{Key: "task.detail", Text: "View detail"},
|
||||||
{Key: "task.raw", Text: "View raw"},
|
{Key: "task.raw", Text: "View raw"},
|
||||||
},
|
},
|
||||||
|
@ -478,6 +478,15 @@ type ConfigUpdateInfo struct {
|
|||||||
ConfigCreateInfo
|
ConfigCreateInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskListArgs struct {
|
||||||
|
Service string `query:"service"`
|
||||||
|
Node string `query:"node"`
|
||||||
|
Name string `query:"name"`
|
||||||
|
State string `query:"state"`
|
||||||
|
PageIndex int `query:"page"`
|
||||||
|
PageSize int `query:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
type TaskInfo struct {
|
type TaskInfo struct {
|
||||||
swarm.Task
|
swarm.Task
|
||||||
NodeName string
|
NodeName string
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
<a class="navbar-item" href="/node/">{{ i18n("menu.node") }}</a>
|
<a class="navbar-item" href="/node/">{{ i18n("menu.node") }}</a>
|
||||||
<a class="navbar-item" href="/network/">{{ i18n("menu.network") }}</a>
|
<a class="navbar-item" href="/network/">{{ i18n("menu.network") }}</a>
|
||||||
<a class="navbar-item" href="/service/">{{ i18n("menu.service") }}</a>
|
<a class="navbar-item" href="/service/">{{ i18n("menu.service") }}</a>
|
||||||
|
<a class="navbar-item" href="/task/">{{ i18n("menu.task") }}</a>
|
||||||
<a class="navbar-item" href="/stack/task/">{{ i18n("menu.stack") }}</a>
|
<a class="navbar-item" href="/stack/task/">{{ i18n("menu.stack") }}</a>
|
||||||
<a class="navbar-item" href="/secret/">{{ i18n("menu.secret") }}</a>
|
<a class="navbar-item" href="/secret/">{{ i18n("menu.secret") }}</a>
|
||||||
<a class="navbar-item" href="/config/">{{ i18n("menu.config") }}</a>
|
<a class="navbar-item" href="/config/">{{ i18n("menu.config") }}</a>
|
||||||
|
13
views/task/_base.jet
Normal file
13
views/task/_base.jet
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{{ extends "../_layouts/default" }}
|
||||||
|
|
||||||
|
{{ block body() }}
|
||||||
|
<section class="hero is-info">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container has-text-centered">
|
||||||
|
<h1 class="title is-2 is-uppercase">{{ i18n("task.title") }}</h1>
|
||||||
|
<h2 class="subtitle is-5">{{ i18n("task.description") }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ yield body_content() }}
|
||||||
|
{{ end }}
|
@ -1,23 +1,12 @@
|
|||||||
{{ extends "../_layouts/default" }}
|
{{ extends "_base" }}
|
||||||
|
|
||||||
{{ block body() }}
|
{{ block body_content() }}
|
||||||
<section class="hero is-info">
|
|
||||||
<div class="hero-body">
|
|
||||||
<div class="container has-text-centered">
|
|
||||||
<h1 class="title is-2">
|
|
||||||
TASK
|
|
||||||
</h1>
|
|
||||||
<h2 class="subtitle is-5">
|
|
||||||
A task is a container running on a swarm. It is the atomic scheduling unit of swarm.
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
|
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">Dashboard</a></li>
|
<li><a href="/">{{ i18n("menu.dashboard") }}</a></li>
|
||||||
<li class="is-active"><a>Detail</a></li>
|
<li><a href="/task/">{{ i18n("menu.task") }}</a></li>
|
||||||
|
<li class="is-active"><a>{{ i18n("menu.detail") }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@ -26,19 +15,15 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="title is-2">
|
<h2 class="title is-2">
|
||||||
{{ .Task.Name ? .Task.Name : .Task.ID }}
|
{{ .Task.Name ? .Task.Name : .Task.ID }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<nav class="navbar has-shadow">
|
<nav class="navbar has-shadow">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item is-tab is-active" href="/task/{{.Task.ID}}/detail">
|
<a class="navbar-item is-tab is-active" href="/task/{{.Task.ID}}/detail">{{ i18n("menu.detail") }}</a>
|
||||||
Detail
|
<a class="navbar-item is-tab " href="/task/{{.Task.ID}}/raw">{{ i18n("menu.raw") }}</a>
|
||||||
</a>
|
|
||||||
<a class="navbar-item is-tab " href="/task/{{.Task.ID}}/raw">
|
|
||||||
Raw
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -104,6 +89,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
<a href="/task/" class="button is-primary">
|
||||||
|
<span class="icon"><i class="fa fa-reply"></i></span>
|
||||||
|
<span>{{ i18n("button.return") }}</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{ end }}
|
{{ end }}
|
73
views/task/list.jet
Normal file
73
views/task/list.jet
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{{ extends "_base" }}
|
||||||
|
{{ import "../_modules/pager" }}
|
||||||
|
|
||||||
|
{{ block body_content() }}
|
||||||
|
<section class="section">
|
||||||
|
<nav class="level">
|
||||||
|
<!-- Left side -->
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item">
|
||||||
|
<form>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<p class="control">
|
||||||
|
<input name="service" value="{{ .Args.Service }}" class="input" placeholder="Search by service">
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-primary">{{ i18n("button.search") }}</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<p class="subtitle is-5">
|
||||||
|
<strong>{{.Pager.Count}}</strong>
|
||||||
|
<span class="is-lowercase">{{ i18n("menu.task") }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Right side -->
|
||||||
|
<div class="level-right">
|
||||||
|
<p class="level-item">
|
||||||
|
{{if .Args.State == ""}}
|
||||||
|
<strong>All</strong>
|
||||||
|
{{else}}
|
||||||
|
<a href="/task/">All</a>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
<p class="level-item">
|
||||||
|
{{if .Args.State == "running"}}
|
||||||
|
<strong>Running</strong>
|
||||||
|
{{else}}
|
||||||
|
<a href="?state=running">Running</a>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<table id="table-items" class="table is-bordered is-striped is-narrow is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ i18n("field.id") }}</th>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>{{ i18n("field.image") }}</th>
|
||||||
|
<th>Node</th>
|
||||||
|
<th>{{ i18n("field.state") }}</th>
|
||||||
|
<th>{{ i18n("field.created-at") }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Tasks}}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ .ID }}/detail">{{ .ID }}</a></td>
|
||||||
|
<td><a href="/service/{{ .ServiceID }}/detail">{{ .ServiceID }}</a></td>
|
||||||
|
<td>{{ limit(.Image, 50) }}</td>
|
||||||
|
<td><a href="/node/{{ .NodeID }}/detail">{{ .NodeName }}</a></td>
|
||||||
|
<td><span class="tag is-{{ .Status.State == "running" ? "success" : "danger" }}">{{ .Status.State }}</span></td>
|
||||||
|
<td>{{ time(.CreatedAt) }}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{ yield pager(info=.Pager) }}
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
@ -1,4 +1,4 @@
|
|||||||
{{ extends "../_layouts/default" }}
|
{{ extends "_base" }}
|
||||||
|
|
||||||
{{ block style() }}
|
{{ block style() }}
|
||||||
<link rel="stylesheet" href="/highlight/highlight.css?v=9.12">
|
<link rel="stylesheet" href="/highlight/highlight.css?v=9.12">
|
||||||
@ -9,24 +9,13 @@
|
|||||||
<script>hljs.initHighlightingOnLoad();</script>
|
<script>hljs.initHighlightingOnLoad();</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ block body() }}
|
{{ block body_content() }}
|
||||||
<section class="hero is-info">
|
|
||||||
<div class="hero-body">
|
|
||||||
<div class="container has-text-centered">
|
|
||||||
<h1 class="title is-2">
|
|
||||||
TASK
|
|
||||||
</h1>
|
|
||||||
<h2 class="subtitle is-5">
|
|
||||||
A task is a container running on a swarm. It is the atomic scheduling unit of swarm.
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
|
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">Dashboard</a></li>
|
<li><a href="/">{{ i18n("menu.dashboard") }}</a></li>
|
||||||
<li class="is-active"><a>Raw</a></li>
|
<li><a href="/task/">{{ i18n("menu.task") }}</a></li>
|
||||||
|
<li class="is-active"><a>{{ i18n("menu.raw") }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@ -42,12 +31,8 @@
|
|||||||
<nav class="navbar has-shadow">
|
<nav class="navbar has-shadow">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item is-tab" href="/task/{{.Task.ID}}/detail">
|
<a class="navbar-item is-tab" href="/task/{{.Task.ID}}/detail">{{ i18n("menu.detail") }}</a>
|
||||||
Detail
|
<a class="navbar-item is-tab is-active" href="/task/{{.Task.ID}}/raw">{{ i18n("menu.raw") }}</a>
|
||||||
</a>
|
|
||||||
<a class="navbar-item is-tab is-active" href="/task/{{.Task.ID}}/raw">
|
|
||||||
Raw
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -56,6 +41,10 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<pre class="is-paddingless"><code class="json">{{ .Raw }}</code></pre>
|
<pre class="is-paddingless"><code class="json">{{ .Raw }}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="/task/" class="button is-primary">
|
||||||
|
<span class="icon"><i class="fa fa-reply"></i></span>
|
||||||
|
<span>{{ i18n("button.return") }}</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{ end }}
|
{{ end }}
|
Loading…
Reference in New Issue
Block a user