Add task list page

This commit is contained in:
cuigh 2017-10-19 20:40:05 +08:00
parent e862737699
commit 6d820149cf
14 changed files with 185 additions and 60 deletions

View File

@ -2,10 +2,12 @@ package docker
import (
"context"
"io"
"math"
"sort"
"io"
"github.com/cuigh/auxo/util/choose"
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -14,35 +16,50 @@ import (
)
// 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) {
var (
tasks []swarm.Task
opts = types.TaskListOptions{
Filters: filters.NewArgs(),
}
)
opts := types.TaskListOptions{
Filters: filters.NewArgs(),
if args.PageIndex < 1 {
args.PageIndex = 1
}
if service != "" {
opts.Filters.Add("service", service)
if args.PageSize < 1 {
args.PageSize = math.MaxInt32
}
if node != "" {
opts.Filters.Add("node", node)
if args.Service != "" {
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)
if err == nil && len(tasks) > 0 {
totalCount = len(tasks)
if err == nil && totalCount > 0 {
sort.Slice(tasks, func(i, j int) bool {
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)
for _, t := range tasks {
if _, ok := nodes[t.NodeID]; !ok {
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 {
nodes[t.NodeID] = ""
//mgr.Logger().Warnf("Node %s of task %s can't be load: %s", t.NodeID, t.ID, e)
}
}
}

View File

@ -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.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.title: Secret
secret.description: Secrets are sensitive data that can be used by services.

View File

@ -132,6 +132,10 @@ stack.title: 编排
stack.description: 编排是相关服务的一个逻辑分组,这些服务通常互相依赖,需要一块部署。
stack.button.deploy: 部署
# task pages
task.title: 任务
task.description: 任务是运行在 Swarm 集群上的一个容器,它是 Swarm 调度的原子单元。
# secret pages
secret.title: 私密配置
secret.description: 运行服务需要的敏感数据,如密钥。

View File

@ -52,7 +52,7 @@ func nodeDetail(ctx web.Context) error {
return err
}
tasks, err := docker.TaskList("", id)
tasks, _, err := docker.TaskList(&model.TaskListArgs{Node: id})
if err != nil {
return err
}

View File

@ -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})
}
tasks, err := docker.TaskList(name, "")
tasks, _, err := docker.TaskList(&model.TaskListArgs{Service: name})
if err != nil {
return err
}

View File

@ -4,10 +4,12 @@ import (
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/model"
)
// TaskController is a controller of swarm task
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"`
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
func Task() (c *TaskController) {
return &TaskController{
List: taskList,
Detail: taskDetail,
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 {
id := ctx.P("id")
task, _, err := docker.TaskInspect(id)

View File

@ -9,7 +9,7 @@ import (
const (
// Version is the version of Swirl
Version = "0.5.5"
Version = "0.5.6"
)
const (

View File

@ -87,6 +87,7 @@ var Perms = []PermGroup{
{
Name: "Task",
Perms: []Perm{
{Key: "task.list", Text: "View list"},
{Key: "task.detail", Text: "View detail"},
{Key: "task.raw", Text: "View raw"},
},

View File

@ -478,6 +478,15 @@ type ConfigUpdateInfo struct {
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 {
swarm.Task
NodeName string

View File

@ -39,6 +39,7 @@
<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="/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="/secret/">{{ i18n("menu.secret") }}</a>
<a class="navbar-item" href="/config/">{{ i18n("menu.config") }}</a>

13
views/task/_base.jet Normal file
View 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 }}

View File

@ -1,23 +1,12 @@
{{ extends "../_layouts/default" }}
{{ extends "_base" }}
{{ block body() }}
<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>
{{ block body_content() }}
<div class="container">
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
<ul>
<li><a href="/">Dashboard</a></li>
<li class="is-active"><a>Detail</a></li>
<li><a href="/">{{ i18n("menu.dashboard") }}</a></li>
<li><a href="/task/">{{ i18n("menu.task") }}</a></li>
<li class="is-active"><a>{{ i18n("menu.detail") }}</a></li>
</ul>
</nav>
</div>
@ -26,19 +15,15 @@
<div class="container">
<h2 class="title is-2">
{{ .Task.Name ? .Task.Name : .Task.ID }}
</h2>
</h2>
</div>
</div>
</section>
<nav class="navbar has-shadow">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item is-tab is-active" href="/task/{{.Task.ID}}/detail">
Detail
</a>
<a class="navbar-item is-tab " href="/task/{{.Task.ID}}/raw">
Raw
</a>
<a class="navbar-item is-tab is-active" href="/task/{{.Task.ID}}/detail">{{ i18n("menu.detail") }}</a>
<a class="navbar-item is-tab " href="/task/{{.Task.ID}}/raw">{{ i18n("menu.raw") }}</a>
</div>
</div>
</nav>
@ -104,6 +89,10 @@
</div>
</div>
{{ 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>
</section>
{{ end }}

73
views/task/list.jet Normal file
View 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 }}

View File

@ -1,4 +1,4 @@
{{ extends "../_layouts/default" }}
{{ extends "_base" }}
{{ block style() }}
<link rel="stylesheet" href="/highlight/highlight.css?v=9.12">
@ -9,24 +9,13 @@
<script>hljs.initHighlightingOnLoad();</script>
{{ end }}
{{ block body() }}
<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>
{{ block body_content() }}
<div class="container">
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
<ul>
<li><a href="/">Dashboard</a></li>
<li class="is-active"><a>Raw</a></li>
<li><a href="/">{{ i18n("menu.dashboard") }}</a></li>
<li><a href="/task/">{{ i18n("menu.task") }}</a></li>
<li class="is-active"><a>{{ i18n("menu.raw") }}</a></li>
</ul>
</nav>
</div>
@ -42,12 +31,8 @@
<nav class="navbar has-shadow">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item is-tab" href="/task/{{.Task.ID}}/detail">
Detail
</a>
<a class="navbar-item is-tab is-active" href="/task/{{.Task.ID}}/raw">
Raw
</a>
<a class="navbar-item is-tab" href="/task/{{.Task.ID}}/detail">{{ i18n("menu.detail") }}</a>
<a class="navbar-item is-tab is-active" href="/task/{{.Task.ID}}/raw">{{ i18n("menu.raw") }}</a>
</div>
</div>
</nav>
@ -56,6 +41,10 @@
<div class="content">
<pre class="is-paddingless"><code class="json">{{ .Raw }}</code></pre>
</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>
</section>
{{ end }}