Add service template management

This commit is contained in:
cuigh 2017-10-09 21:02:41 +08:00
parent 8c00a7f69d
commit 722c7e3e09
28 changed files with 1301 additions and 1254 deletions

View File

@ -1683,27 +1683,27 @@ var Swirl;
let $tr = $(e.target).closest("tr");
let name = $tr.find("td:eq(1)").text().trim();
Modal.confirm(`Are you sure to remove service: <strong>${name}</strong>?`, "Delete service", (dlg, e) => {
$ajax.post("delete", { names: name }).trigger(e.target).encoder("form").json(r => {
$ajax.post("delete", { names: name }).trigger(e.target).encoder("form").json(() => {
$tr.remove();
dlg.close();
});
});
}
deleteServices(e) {
deleteServices() {
let names = this.table.selectedKeys();
if (names.length == 0) {
Modal.alert("Please select one or more items.");
return;
}
Modal.confirm(`Are you sure to remove ${names.length} services?`, "Delete services", (dlg, e) => {
$ajax.post("delete", { names: names.join(",") }).trigger(e.target).encoder("form").json(r => {
$ajax.post("delete", { names: names.join(",") }).trigger(e.target).encoder("form").json(() => {
this.table.selectedRows().remove();
dlg.close();
});
});
}
scaleService(e) {
let $btn = $(e.target);
let $btn = $(e.target).closest("button");
let $tr = $btn.closest("tr");
let data = {
name: $tr.find("td:eq(1)").text().trim(),
@ -1711,7 +1711,7 @@ var Swirl;
};
Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", (dlg, e) => {
data.count = dlg.find("input[name=count]").val();
$ajax.post("scale", data).trigger($btn).encoder("form").json(r => {
$ajax.post("scale", data).trigger(e.target).encoder("form").json(() => {
location.reload();
});
});
@ -1721,6 +1721,35 @@ var Swirl;
})(Service = Swirl.Service || (Swirl.Service = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Service;
(function (Service) {
var Template;
(function (Template) {
var Modal = Swirl.Core.Modal;
var Dispatcher = Swirl.Core.Dispatcher;
class ListPage {
constructor() {
let dispatcher = Dispatcher.bind("#table-items");
dispatcher.on("delete-template", this.deleteTemplate.bind(this));
}
deleteTemplate(e) {
let $tr = $(e.target).closest("tr");
let id = $tr.data("id");
let name = $tr.find("td:first").text();
Modal.confirm(`Are you sure to remove template: <strong>${name}</strong>?`, "Delete template", (dlg, e) => {
$ajax.post("delete", { id: id }).trigger(e.target).encoder("form").json(() => {
$tr.remove();
dlg.close();
});
});
}
}
Template.ListPage = ListPage;
})(Template = Service.Template || (Service.Template = {}));
})(Service = Swirl.Service || (Swirl.Service = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Setting;
(function (Setting) {

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@ namespace Swirl.Service {
this.table = new Table("#table-items");
// bind events
this.table.on("delete-service", this.deleteService.bind(this)).on("scale-service", this.scaleService.bind(this))
this.table.on("delete-service", this.deleteService.bind(this)).on("scale-service", this.scaleService.bind(this));
$("#btn-delete").click(this.deleteServices.bind(this));
}
@ -19,22 +19,22 @@ namespace Swirl.Service {
let $tr = $(e.target).closest("tr");
let name = $tr.find("td:eq(1)").text().trim();
Modal.confirm(`Are you sure to remove service: <strong>${name}</strong>?`, "Delete service", (dlg, e) => {
$ajax.post("delete", { names: name }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$ajax.post("delete", { names: name }).trigger(e.target).encoder("form").json<AjaxResult>(() => {
$tr.remove();
dlg.close();
})
});
}
private deleteServices(e: JQueryEventObject) {
private deleteServices() {
let names = this.table.selectedKeys();
if (names.length == 0) {
Modal.alert("Please select one or more items.")
Modal.alert("Please select one or more items.");
return;
}
Modal.confirm(`Are you sure to remove ${names.length} services?`, "Delete services", (dlg, e) => {
$ajax.post("delete", { names: names.join(",") }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$ajax.post("delete", { names: names.join(",") }).trigger(e.target).encoder("form").json<AjaxResult>(() => {
this.table.selectedRows().remove();
dlg.close();
})
@ -42,7 +42,7 @@ namespace Swirl.Service {
}
private scaleService(e: JQueryEventObject) {
let $btn = $(e.target);
let $btn = $(e.target).closest("button");
let $tr = $btn.closest("tr");
let data = {
name: $tr.find("td:eq(1)").text().trim(),
@ -50,7 +50,7 @@ namespace Swirl.Service {
};
Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", (dlg, e) => {
data.count = dlg.find("input[name=count]").val();
$ajax.post("scale", data).trigger($btn).encoder("form").json<AjaxResult>(r => {
$ajax.post("scale", data).trigger(e.target).encoder("form").json<AjaxResult>(() => {
location.reload();
})
});

View File

@ -0,0 +1,26 @@
///<reference path="../../core/core.ts" />
namespace Swirl.Service.Template {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
export class ListPage {
constructor() {
// bind events
let dispatcher = Dispatcher.bind("#table-items");
dispatcher.on("delete-template", this.deleteTemplate.bind(this));
}
private deleteTemplate(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let id = $tr.data("id");
let name = $tr.find("td:first").text();
Modal.confirm(`Are you sure to remove template: <strong>${name}</strong>?`, "Delete template", (dlg, e) => {
$ajax.post("delete", { id: id }).trigger(e.target).encoder("form").json<AjaxResult>(() => {
$tr.remove();
dlg.close();
})
});
}
}
}

View File

@ -53,6 +53,18 @@ func (b *eventBiz) CreateService(action model.EventAction, name string, user web
b.Create(event)
}
func (b *eventBiz) CreateServiceTemplate(action model.EventAction, id, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeServiceTemplate,
Action: action,
Code: id,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateNetwork(action model.EventAction, id, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeNetwork,

69
biz/template.go Normal file
View File

@ -0,0 +1,69 @@
package biz
import (
"time"
"github.com/cuigh/auxo/data/guid"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/model"
)
// Template return a service template biz instance.
var Template = &templateBiz{}
type templateBiz struct {
}
func (b *templateBiz) List(args *model.TemplateListArgs) (tpls []*model.Template, count int, err error) {
do(func(d dao.Interface) {
tpls, count, err = d.TemplateList(args)
})
return
}
func (b *templateBiz) Create(tpl *model.Template, user web.User) (err error) {
do(func(d dao.Interface) {
tpl.ID = guid.New()
err = d.TemplateCreate(tpl)
if err == nil {
Event.CreateServiceTemplate(model.EventActionCreate, tpl.ID, tpl.Name, user)
}
})
return
}
func (b *templateBiz) Get(id string) (tpl *model.Template, err error) {
do(func(d dao.Interface) {
tpl, err = d.TemplateGet(id)
})
return
}
func (b *templateBiz) Delete(id string, user web.User) (err error) {
do(func(d dao.Interface) {
var tpl *model.Template
tpl, err = d.TemplateGet(id)
if err != nil {
return
}
err = d.TemplateDelete(id)
if err == nil {
Event.CreateServiceTemplate(model.EventActionDelete, id, tpl.Name, user)
}
})
return
}
func (b *templateBiz) Update(tpl *model.Template, user web.User) (err error) {
do(func(d dao.Interface) {
tpl.UpdatedBy = user.ID()
tpl.UpdatedAt = time.Now()
err = d.TemplateUpdate(tpl)
if err == nil {
Event.CreateServiceTemplate(model.EventActionUpdate, tpl.ID, tpl.Name, user)
}
})
return
}

View File

@ -25,7 +25,7 @@ type ServiceController struct {
New web.HandlerFunc `path:"/new" name:"service.new" authorize:"!" desc:"new service page"`
Create web.HandlerFunc `path:"/new" method:"post" name:"service.create" authorize:"!" desc:"create service"`
Edit web.HandlerFunc `path:"/:name/edit" name:"service.edit" authorize:"!" desc:"service edit page"`
Update web.HandlerFunc `path:"/:name/update" method:"post" name:"service.update" authorize:"!" desc:"update service"`
Update web.HandlerFunc `path:"/:name/edit" method:"post" name:"service.update" authorize:"!" desc:"update service"`
}
func Service() (c *ServiceController) {
@ -114,6 +114,30 @@ func Service() (c *ServiceController) {
}
c.New = func(ctx web.Context) error {
service := &model.ServiceInfo{}
tid := ctx.Q("template")
if tid != "" {
tpl, err := biz.Template.Get(tid)
if err != nil {
return err
}
if tpl != nil {
err = json.Unmarshal([]byte(tpl.Content), service)
if err != nil {
return err
}
if service.Registry != "" {
registry, err := biz.Registry.Get(service.Registry)
if err != nil {
return err
}
service.RegistryURL = registry.URL
}
}
}
networks, err := docker.NetworkList()
if err != nil {
return err
@ -130,7 +154,11 @@ func Service() (c *ServiceController) {
if err != nil {
return err
}
m := newModel(ctx).Add("Networks", networks).Add("Secrets", secrets).Add("Configs", configs).Add("Registries", registries)
checkedNetworks := set.FromSlice(service.Networks, func(i int) interface{} { return service.Networks[i] })
m := newModel(ctx).Add("Service", service).Add("Registries", registries).
Add("Networks", networks).Add("CheckedNetworks", checkedNetworks).
Add("Secrets", secrets).Add("Configs", configs)
return ctx.Render("service/new", m)
}
@ -179,17 +207,10 @@ func Service() (c *ServiceController) {
}
checkedNetworks := set.FromSlice(service.Endpoint.VirtualIPs, func(i int) interface{} { return service.Endpoint.VirtualIPs[i].NetworkID })
checkedSecrets := set.FromSlice(service.Spec.TaskTemplate.ContainerSpec.Secrets, func(i int) interface{} {
return service.Spec.TaskTemplate.ContainerSpec.Secrets[i].SecretName
})
checkedConfigs := set.FromSlice(service.Spec.TaskTemplate.ContainerSpec.Configs, func(i int) interface{} {
return service.Spec.TaskTemplate.ContainerSpec.Configs[i].ConfigName
})
m := newModel(ctx).Add("Service", model.NewServiceInfo(service)).
Add("Networks", networks).Add("CheckedNetworks", checkedNetworks).
Add("Secrets", secrets).Add("CheckedSecrets", checkedSecrets).
Add("Configs", configs).Add("CheckedConfigs", checkedConfigs)
Add("Secrets", secrets).Add("Configs", configs)
return ctx.Render("service/edit", m)
}

View File

@ -1,18 +1,170 @@
package controller
import "github.com/cuigh/auxo/net/web"
import (
"encoding/json"
"github.com/cuigh/auxo/data/set"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/model"
)
type TemplateController struct {
List web.HandlerFunc `path:"/" name:"template.list" authorize:"!" desc:"service template list page"`
List web.HandlerFunc `path:"/" name:"template.list" authorize:"!" desc:"service template list page"`
New web.HandlerFunc `path:"/new" name:"template.new" authorize:"!" desc:"new service template page"`
Create web.HandlerFunc `path:"/new" method:"post" name:"template.create" authorize:"!" desc:"create service template"`
Edit web.HandlerFunc `path:"/:id/edit" name:"template.edit" authorize:"!" desc:"edit service template page"`
Update web.HandlerFunc `path:"/:id/edit" method:"post" name:"template.update" authorize:"!" desc:"update service template"`
Delete web.HandlerFunc `path:"/delete" method:"post" name:"template.delete" authorize:"!" desc:"delete service template"`
}
func Template() (c *TemplateController) {
c = &TemplateController{}
return &TemplateController{
List: templateList,
New: templateNew,
Create: templateCreate,
Edit: templateEdit,
Update: templateUpdate,
Delete: templateDelete,
}
}
c.List = func(ctx web.Context) error {
m := newModel(ctx)
return ctx.Render("service/template/list", m)
func templateList(ctx web.Context) error {
args := &model.TemplateListArgs{}
err := ctx.Bind(args)
if err != nil {
return err
}
return
args.PageSize = model.PageSize
if args.PageIndex == 0 {
args.PageIndex = 1
}
tpls, totalCount, err := biz.Template.List(args)
if err != nil {
return err
}
m := newPagerModel(ctx, totalCount, model.PageSize, args.PageIndex).
Add("Name", args.Name).
Add("Templates", tpls)
return ctx.Render("service/template/list", m)
}
func templateNew(ctx web.Context) error {
service := model.ServiceInfo{}
networks, err := docker.NetworkList()
if err != nil {
return err
}
secrets, _, err := docker.SecretList("", 1, 100)
if err != nil {
return err
}
configs, _, err := docker.ConfigList("", 1, 100)
if err != nil {
return err
}
registries, err := biz.Registry.List()
if err != nil {
return err
}
m := newModel(ctx).Add("Action", "New").Add("Service", service).Add("Registries", registries).
Add("Networks", networks).Add("CheckedNetworks", set.Set{}).
Add("Secrets", secrets).Add("Configs", configs)
return ctx.Render("service/template/edit", m)
}
func templateCreate(ctx web.Context) error {
info := &model.ServiceInfo{}
err := ctx.Bind(info)
if err == nil {
tpl := &model.Template{Name: info.Name}
info.Name = ""
content, err := json.Marshal(info)
if err != nil {
return err
}
tpl.Content = string(content)
err = biz.Template.Create(tpl, ctx.User())
}
return ajaxResult(ctx, err)
}
func templateEdit(ctx web.Context) error {
id := ctx.P("id")
tpl, err := biz.Template.Get(id)
if err != nil {
return err
} else if tpl == nil {
return web.ErrNotFound
}
service := &model.ServiceInfo{}
err = json.Unmarshal([]byte(tpl.Content), service)
if err != nil {
return err
}
service.Name = tpl.Name
if service.Registry != "" {
registry, err := biz.Registry.Get(service.Registry)
if err != nil {
return err
}
service.RegistryURL = registry.URL
}
networks, err := docker.NetworkList()
if err != nil {
return err
}
secrets, _, err := docker.SecretList("", 1, 100)
if err != nil {
return err
}
configs, _, err := docker.ConfigList("", 1, 100)
if err != nil {
return err
}
registries, err := biz.Registry.List()
if err != nil {
return err
}
checkedNetworks := set.FromSlice(service.Networks, func(i int) interface{} { return service.Networks[i] })
m := newModel(ctx).Add("Action", "Edit").Add("Service", service).Add("Registries", registries).
Add("Networks", networks).Add("CheckedNetworks", checkedNetworks).
Add("Secrets", secrets).Add("Configs", configs)
return ctx.Render("service/template/edit", m)
}
func templateUpdate(ctx web.Context) error {
info := &model.ServiceInfo{}
err := ctx.Bind(info)
if err == nil {
tpl := &model.Template{
ID: ctx.P("id"),
Name: info.Name,
}
info.Name = ""
content, err := json.Marshal(info)
if err != nil {
return err
}
tpl.Content = string(content)
err = biz.Template.Update(tpl, ctx.User())
}
return ajaxResult(ctx, err)
}
func templateDelete(ctx web.Context) error {
id := ctx.F("id")
err := biz.Template.Delete(id, ctx.User())
return ajaxResult(ctx, err)
}

View File

@ -63,7 +63,6 @@ type Interface interface {
SessionUpdate(session *model.Session) error
SessionGet(token string) (*model.Session, error)
// UserDelete(id string) error
RegistryCreate(registry *model.Registry) error
RegistryUpdate(registry *model.Registry) error
RegistryGet(id string) (*model.Registry, error)
@ -76,6 +75,12 @@ type Interface interface {
ArchiveUpdate(archive *model.Archive) error
ArchiveDelete(id string) error
TemplateList(args *model.TemplateListArgs) (tpls []*model.Template, count int, err error)
TemplateGet(id string) (*model.Template, error)
TemplateCreate(tpl *model.Template) error
TemplateUpdate(tpl *model.Template) error
TemplateDelete(id string) error
EventCreate(event *model.Event) error
EventList(args *model.EventListArgs) (events []*model.Event, count int, err error)

73
dao/mongo/template.go Normal file
View File

@ -0,0 +1,73 @@
package mongo
import (
"time"
"github.com/cuigh/swirl/model"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
func (d *Dao) TemplateList(args *model.TemplateListArgs) (tpls []*model.Template, count int, err error) {
d.do(func(db *database) {
filter := bson.M{}
if args.Name != "" {
filter["name"] = args.Name
}
q := db.C("template").Find(filter)
count, err = q.Count()
if err != nil {
return
}
tpls = []*model.Template{}
err = q.Skip(args.PageSize * (args.PageIndex - 1)).Limit(args.PageSize).All(&tpls)
})
return
}
func (d *Dao) TemplateCreate(tpl *model.Template) (err error) {
tpl.CreatedAt = time.Now()
tpl.UpdatedAt = tpl.CreatedAt
d.do(func(db *database) {
err = db.C("template").Insert(tpl)
})
return
}
func (d *Dao) TemplateGet(id string) (tpl *model.Template, err error) {
d.do(func(db *database) {
tpl = &model.Template{}
err = db.C("template").FindId(id).One(tpl)
if err == mgo.ErrNotFound {
tpl, err = nil, nil
} else if err != nil {
tpl = nil
}
})
return
}
func (d *Dao) TemplateUpdate(tpl *model.Template) (err error) {
d.do(func(db *database) {
update := bson.M{
"$set": bson.M{
"name": tpl.Name,
"content": tpl.Content,
"updated_by": tpl.UpdatedBy,
"updated_at": tpl.UpdatedAt,
},
}
err = db.C("template").UpdateId(tpl.ID, update)
})
return
}
func (d *Dao) TemplateDelete(id string) (err error) {
d.do(func(db *database) {
err = db.C("template").RemoveId(id)
})
return
}

View File

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

View File

@ -58,6 +58,17 @@ var Perms = []PermGroup{
{Key: "service.scale", Text: "Scale"},
},
},
{
Name: "Template",
Perms: []Perm{
{Key: "service.list", Text: "View list"},
{Key: "service.new", Text: "View new"},
{Key: "service.edit", Text: "View edit"},
{Key: "service.create", Text: "Create"},
{Key: "service.delete", Text: "Delete"},
{Key: "service.update", Text: "Update"},
},
},
{
Name: "Stack",
Perms: []Perm{

View File

@ -149,6 +149,7 @@ func NewServiceDetailInfo(service swarm.Service) *ServiceDetailInfo {
type ServiceInfo struct {
Name string `json:"name"`
Registry string `json:"registry"`
RegistryURL string `json:"-"`
RegistryAuth string `json:"-"`
Image string `json:"image"`
Mode string `json:"mode"`

View File

@ -9,14 +9,15 @@ type EventType string
const (
// swarm
EventTypeRegistry EventType = "Registry"
EventTypeNode EventType = "Node"
EventTypeNetwork EventType = "Network"
EventTypeService EventType = "Service"
EventTypeStackTask EventType = "Stack Task"
EventTypeStackArchive EventType = "Stack Archive"
EventTypeSecret EventType = "Secret"
EventTypeConfig EventType = "Config"
EventTypeRegistry EventType = "Registry"
EventTypeNode EventType = "Node"
EventTypeNetwork EventType = "Network"
EventTypeService EventType = "Service"
EventTypeServiceTemplate EventType = "Service Template"
EventTypeStackTask EventType = "Stack Task"
EventTypeStackArchive EventType = "Stack Archive"
EventTypeSecret EventType = "Secret"
EventTypeConfig EventType = "Config"
// local
EventTypeVolume EventType = "Volume"

View File

@ -17,3 +17,19 @@ type ArchiveListArgs struct {
PageIndex int `query:"page"`
PageSize int `query:"size"`
}
type Template struct {
ID string `bson:"_id" json:"id,omitempty"`
Name string `bson:"name" json:"name,omitempty"`
Content string `bson:"content" json:"content,omitempty"`
CreatedBy string `bson:"created_by" json:"created_by,omitempty"`
CreatedAt time.Time `bson:"created_at" json:"created_at,omitempty"`
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at,omitempty"`
}
type TemplateListArgs struct {
Name string `query:"name"`
PageIndex int `query:"page"`
PageSize int `query:"size"`
}

View File

@ -19,7 +19,3 @@ type Setting struct {
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at,omitempty"`
}
func Test() {
time.Now().Zone()
}

View File

@ -114,4 +114,15 @@
{{end}}
</tbody>
</table>
{{ end }}
{{ end }}
{{ block form_submit(url) }}
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-primary">Submit</button>
</div>
<div class="control">
<a href="{{ url }}" class="button is-link">Cancel</a>
</div>
</div>
{{ end }}

564
views/_modules/service.jet Normal file
View File

@ -0,0 +1,564 @@
{{ import "form" }}
{{ block form_mode() }}
<div class="field">
<label class="label">Mode</label>
<div class="field has-addons is-marginless">
<div class="control">
<span class="select">
<select id="cb-mode" name="mode">
<option value="replicated"{{if .Service.Mode == "replicated"}} selected{{end}}>Replicated</option>
<option value="global"{{if .Service.Mode == "global"}} selected{{end}}>Global</option>
</select>
</span>
</div>
<div class="control is-expanded">
<input id="txt-replicas" name="replicas" class="input" placeholder="" data-type="integer" data-v-rule="service-mode" {{if .Service.Mode == "global"}}style="display: none"{{else}}value="{{ trimZero(.Service.Replicas) }}"{{end}}>
</div>
</div>
</div>
{{ end }}
{{ block form_network() }}
<div class="field">
<label class="label">Network</label>
<div class="control">
{{ set := .CheckedNetworks }}
{{range .Networks}}
{{ yield checkbox(name="networks", value=.ID, label=.Name, checked=set.Contains(.ID)) }}
{{end}}
</div>
</div>
{{ end }}
{{ block form_main_right() }}
<div class="column">
<div class="field">
<label class="label">Command</label>
<div class="control">
<input name="command" value="{{ .Service.Command }}" class="input" type="text" placeholder="">
</div>
</div>
<div class="field">
<label class="label">Args</label>
<div class="control">
<input name="args" value="{{ .Service.Args }}" class="input" type="text" placeholder="">
</div>
</div>
<div class="field">
<label class="label">Work directory</label>
<div class="control">
<input name="dir" value="{{ .Service.Dir }}" class="input" type="text" placeholder="">
</div>
</div>
<div class="field">
<label class="label">User</label>
<div class="control">
<input name="user" value="{{ .Service.User }}" class="input" type="text" placeholder="">
</div>
</div>
</div>
{{ end }}
{{ block form_others() }}
<div class="tabs is-toggle is-fullwidth is-marginless" data-target="tab-content">
<ul>
<li class="is-active">
<a>
<span>Ports</span>
</a>
</li>
<li>
<a>
<span>Volumes</span>
</a>
</li>
<li>
<a>
<span>Configurations</span>
</a>
</li>
<li>
<a>
<span>Resources</span>
</a>
</li>
<li>
<a>
<span>Placement</span>
</a>
</li>
<li>
<a>
<span>Schedule policy</span>
</a>
</li>
<li>
<a>
<span>Log driver</span>
</a>
</li>
</ul>
</div>
<div id="tab-content" class="tabs-content has-no-top-border">
<div>
<div class="field">
<label class="label">Resolution mode</label>
<div class="control">
{{ yield radios(name="endpoint.mode", values=slice("vip", "dnsrr"), labels=slice("VIP", "DNS-RR"), checked=.Service.Endpoint.Mode) }}
</div>
</div>
<div class="field">
<label class="label">Port config</label>
<table id="table-endpoint-ports" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="endpoint.port">
<thead>
<tr>
<th>Host</th>
<th>Container</th>
<th>Protocol</th>
<th>Mode</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-endpoint-port">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{range i, p := .Service.Endpoint.Ports}}
<tr>
<td><input name="endpoint.ports[{{i}}].published_port" value="{{p.PublishedPort}}" class="input is-small" placeholder="port in host" data-type="integer"></td>
<td>
<input name="endpoint.ports[{{i}}].target_port" value="{{p.TargetPort}}" class="input is-small" placeholder="port in container" data-type="integer">
</td>
<td>
<div class="select is-small">
<select name="endpoint.ports[{{i}}].protocol">
{{ yield option(value="tcp", label="TCP", selected=p.Protocol) }}
{{ yield option(value="udp", label="UDP", selected=p.Protocol) }}
</select>
</div>
</td>
<td>
<div class="select is-small">
<select name="endpoint.ports[{{i}}].publish_mode">
{{ yield option(value="ingress", selected=p.PublishMode) }}
{{ yield option(value="host", selected=p.PublishMode) }}
</select>
</div>
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-endpoint-port">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<div style="display: none">
<table id="table-mounts" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="mount">
<thead>
<tr>
<th width="80">Type</th>
<th>Source</th>
<th>Target</th>
<th width="30">ReadOnly</th>
<th>Propagation</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-mount">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{range i, m := .Service.Mounts}}
<tr>
<td>
<div class="select is-small">
{{ yield select(name="mounts["+i+"].type", values=slice("bind", "volume", "tmpfs"), labels=slice("Bind", "Volume", "TempFS"), selected=m.Type) }}
</div>
</td>
<td>
<input name="mounts[{{i}}].src" value="{{m.Source}}" class="input is-small" placeholder="path in host">
</td>
<td><input name="mounts[{{i}}].dst" value="{{m.Target}}" class="input is-small" placeholder="path in container"></td>
<td>
<div class="select is-small">
{{ yield select(name="mounts["+i+"].read_only", values=slice("false", "true"), labels=slice("No", "Yes"), selected=m.Type, dt="bool") }}
</div>
</td>
<td>
<div class="select is-small">
<select name="mounts[{{i}}].propagation">
<option value="">--Select--</option>
{{ yield option(value="rprivate", selected=m.Propagation) }}
{{ yield option(value="private", selected=m.Propagation) }}
{{ yield option(value="rshared", selected=m.Propagation) }}
{{ yield option(value="shared", selected=m.Propagation) }}
{{ yield option(value="rslave", selected=m.Propagation) }}
{{ yield option(value="slave", selected=m.Propagation) }}
</select>
</div>
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-mount">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div style="display: none">
<div class="field">
<label class="label">Secrets</label>
<table id="table-secrets" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="secret">
<thead>
<tr>
<th>Name</th>
<th>File name</th>
<th>UID</th>
<th>GID</th>
<th>Mode</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-secret">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{range i, c := .Service.Secrets}}
<tr>
<td>{{ c.Name }}<input name="secrets[{{ i }}].id" value="{{ c.ID }}" type="hidden"><input name="secrets[{{ i }}].name" value="{{ c.Name }}" type="hidden"></td>
<td><input name="secrets[{{ i }}].file_name" value="{{ c.FileName }}" class="input is-small"></td>
<td><input name="secrets[{{ i }}].uid" value="{{ c.UID }}" class="input is-small"></td>
<td><input name="secrets[{{ i }}].gid" value="{{ c.GID }}" class="input is-small"></td>
<td><input name="secrets[{{ i }}].mode" value="{{ c.Mode }}" class="input is-small" data-type="integer"></td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-secret">
<span class="icon is-small">
<i class="fa fa-remove"></i>
</span>
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
<p class="help">Secrets will be mounted as /run/secrets/$FILE_NAME in containers by default, You can specify a custom location in Docker 17.06 and higher.</p>
</div>
<div class="field">
<label class="label">Configs</label>
<table id="table-configs" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="config">
<thead>
<tr>
<th>Name</th>
<th>File name</th>
<th>UID</th>
<th>GID</th>
<th>Mode</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-config">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{range i, c := .Service.Configs}}
<tr>
<td>{{ c.Name }}<input name="configs[{{ i }}].id" value="{{ c.ID }}" type="hidden"><input name="configs[{{ i }}].name" value="{{ c.Name }}" type="hidden"></td>
<td><input name="configs[{{ i }}].file_name" value="{{ c.FileName }}" class="input is-small"></td>
<td><input name="configs[{{ i }}].uid" value="{{ c.UID }}" class="input is-small"></td>
<td><input name="configs[{{ i }}].gid" value="{{ c.GID }}" class="input is-small"></td>
<td><input name="configs[{{ i }}].mode" value="{{ c.Mode }}" class="input is-small" data-type="integer"></td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-config">
<span class="icon is-small">
<i class="fa fa-remove"></i>
</span>
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
<p class="help">Configs will be mounted as /$FILE_NAME in containers by default, You can specify a custom location.</p>
</div>
</div>
<div style="display: none">
<div class="columns">
<div class="column">
<fieldset>
<legend class="lead is-5">Limits</legend>
<div class="field">
<label class="label">CPU</label>
<div class="control">
<input name="resource.limit.cpu" value="{{ .Service.Resource.Limit.CPU ? .Service.Resource.Limit.CPU : "" }}" class="input" placeholder="e.g. 1" data-type="float">
</div>
</div>
<div class="field">
<label class="label">Memory</label>
<div class="control">
<input name="resource.limit.memory" value="{{ .Service.Resource.Limit.Memory }}" class="input" placeholder="e.g. 1G">
</div>
</div>
</fieldset>
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<fieldset>
<legend class="lead is-5">Reservations</legend>
<div class="field">
<label class="label">CPU</label>
<div class="control">
<input name="resource.reserve.cpu" value="{{ .Service.Resource.Reserve.CPU ? .Service.Resource.Reserve.CPU : "" }}" class="input" placeholder="e.g. 0.1" data-type="float">
</div>
</div>
<div class="field">
<label class="label">Memory</label>
<div class="control">
<input name="resource.reserve.memory" value="{{ .Service.Resource.Reserve.Memory }}" class="input" placeholder="e.g. 10M">
</div>
</div>
</fieldset>
</div>
</div>
</div>
<div style="display: none">
<div class="field">
<label class="label">Constraints</label>
<table id="table-constraints" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="constraint">
<thead>
<tr>
<th>Name</th>
<th width="30">Operator</th>
<th>Value</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-constraint">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{ range i, c := .Service.Placement.Constraints }}
<tr>
<td>
<input name="placement.constraints[{{i}}].name" value="{{c.Name}}" class="input is-small" placeholder="e.g. node.role/node.hostname/node.id/node.labels.*/engine.labels.*/...">
</td>
<td>
<div class="select is-small">
{{ yield select(name="placement.constraints["+i+"].op", values=slice("==", "!="), selected=c.Operator) }}
</div>
</td>
<td>
<input name="placement.constraints[{{i}}].value" value="{{c.Value}}" class="input is-small" placeholder="e.g. manager">
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-constraint">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div class="field">
<label class="label">Preferences</label>
<table id="table-preferences" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="preference">
<thead>
<tr>
<th>Spread</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-preference">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{ range i, p := .Service.Placement.Preferences }}
<tr>
<td>
<input name="placement.preferences[{{i}}].spread" value="{{p.Spread}}" class="input is-small" placeholder="e.g. engine.labels.az">
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-preference">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
<div style="display: none">
<div class="columns">
<div class="column">
<fieldset>
<legend class="lead is-5">Update</legend>
<div class="field">
<label class="label">Parallelism</label>
<div class="control">
<input name="update_policy.parallelism" value="{{ trimZero(.Service.UpdatePolicy.Parallelism) }}" class="input" placeholder="" data-type="integer">
</div>
</div>
<div class="field">
<label class="label">Delay</label>
<div class="control">
<input name="update_policy.delay" value="{{ .Service.UpdatePolicy.Delay }}" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
<div class="field">
<label class="label">Failure action</label>
<div class="control">
{{ yield radios(name="update_policy.failure_action", values=slice("pause", "continue", "rollback"), checked=.Service.UpdatePolicy.FailureAction) }}
</div>
</div>
<div class="field">
<label class="label">Order</label>
<div class="control">
{{ yield radios(name="update_policy.order", values=slice("start-first", "stop-first"), checked=.Service.UpdatePolicy.Order) }}
</div>
</div>
</fieldset>
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<fieldset>
<legend class="lead is-5">Rollback</legend>
<div class="field">
<label class="label">Parallelism</label>
<div class="control">
<input name="rollback_policy.parallelism" value="{{ trimZero(.Service.RollbackPolicy.Parallelism) }}" class="input" placeholder="" data-type="integer">
</div>
</div>
<div class="field">
<label class="label">Delay</label>
<div class="control">
<input name="rollback_policy.delay" value="{{ .Service.RollbackPolicy.Delay }}" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
<div class="field">
<label class="label">Failure action</label>
<div class="control">
{{ yield radios(name="rollback_policy.failure_action", values=slice("pause", "continue"), checked=.Service.RollbackPolicy.FailureAction) }}
</div>
</div>
<div class="field">
<label class="label">Order</label>
<div class="control">
{{ yield radios(name="rollback_policy.order", values=slice("start-first", "stop-first"), checked=.Service.RollbackPolicy.Order) }}
</div>
</div>
</fieldset>
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<fieldset>
<legend class="lead is-5">Restart</legend>
<div class="field">
<label class="label">Condition</label>
<div class="control">
{{ yield radios(name="restart_policy.condition", values=slice("any", "on-failure", "none"), checked=.Service.RestartPolicy.Condition) }}
</div>
</div>
<div class="field">
<label class="label">Max attempts</label>
<div class="control">
<input name="restart_policy.max_attempts" value="{{ trimZero(.Service.RestartPolicy.MaxAttempts) }}" class="input" placeholder="" data-type="integer">
</div>
</div>
<div class="field">
<label class="label">Delay</label>
<div class="control">
<input name="restart_policy.delay" value="{{ .Service.RestartPolicy.Delay }}" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
<div class="field">
<label class="label">Window</label>
<div class="control">
<input name="restart_policy.window" value="{{ .Service.RestartPolicy.Delay }}" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
</fieldset>
</div>
</div>
</div>
<div style="display: none">
<div class="field">
<label class="label">Name</label>
<div class="control">
{{ yield radios(name="log_driver.name", values=slice("json-file", "syslog", "journald", "gelf", "fluentd", "awslogs", "splunk", "etwlogs", "gcplogs", "none"), checked=.Service.LogDriver.Name) }}
</div>
</div>
<div class="field">
<label class="label">Options</label>
{{ yield options_table(name="log_driver.option", items=.Service.LogDriver.Options) }}
</div>
</div>
</div>
{{ end }}
{{ block dialog(name, items) }}
<div id="dlg-add-{{ name }}" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add {{ name }}</p>
<button class="delete"></button>
</header>
<section class="modal-card-body" style="max-height: 400px; overflow-y: auto">
<nav class="panel">
<div class="panel-block">
<p class="control has-icons-left">
<input class="input is-small" type="text" placeholder="Searching is not implemented...">
<span class="icon is-small is-left">
<i class="fa fa-search"></i>
</span>
</p>
</div>
{{ range items }}
<label class="panel-block">
<input type="checkbox" value="{{ .ID }}" data-name="{{ .Spec.Name }}">
{{ .Spec.Name }}
</label>
{{end}}
</nav>
</section>
<footer class="modal-card-foot">
<button id="btn-add-{{ name }}" type="button" class="button is-primary">Confirm</button>
<button type="button" class="button dismiss">Cancel</button>
</footer>
</div>
</div>
{{ end }}

31
views/service/base.jet Normal file
View File

@ -0,0 +1,31 @@
{{ 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">
SERVICE
</h1>
<h2 class="subtitle is-5">
Services are the definitions of tasks to run on a swarm.
</h2>
</div>
</div>
<div class="hero-foot">
<div class="container">
<nav class="tabs is-boxed">
<ul>
<li class="is-active">
<a href="/service/">Services</a>
</li>
<li>
<a href="/service/template/">Templates</a>
</li>
</ul>
</nav>
</div>
</div>
</section>
{{ yield body_content() }}
{{ end }}

View File

@ -1,20 +1,7 @@
{{ extends "../_layouts/default" }}
{{ extends "base" }}
{{ import "../_modules/detail" }}
{{ block body() }}
<section class="hero is-info">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title is-2">
SERVICE
</h1>
<h2 class="subtitle is-5">
Services are the definitions of tasks to run on a 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>

View File

@ -1,72 +1,11 @@
{{ extends "../_layouts/default" }}
{{ import "../_modules/form" }}
{{ extends "base" }}
{{ import "../_modules/service" }}
{{ block script() }}
<script>$(() => new Swirl.Service.EditPage())</script>
{{ end }}
{{ block pair(field, index=0, name="", value="")}}
<tr>
<td>
<input name="{{field}}s[{{index}}].name" class="input is-small" type="text" value="{{name}}">
</td>
<td>
<input name="{{field}}s[{{index}}].value" class="input is-small" type="text" value="{{value}}">
</td>
<td>
<a class="button is-small is-danger" data-action="delete-{{field}}">Delete</a>
</td>
</tr>
{{ end }}
{{ block dialog(name, items) }}
<div id="dlg-add-{{ name }}" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add {{ name }}</p>
<button class="delete"></button>
</header>
<section class="modal-card-body" style="max-height: 400px; overflow-y: auto">
<nav class="panel">
<div class="panel-block">
<p class="control has-icons-left">
<input class="input is-small" type="text" placeholder="Searching is not implemented...">
<span class="icon is-small is-left">
<i class="fa fa-search"></i>
</span>
</p>
</div>
{{ range items }}
<label class="panel-block">
<input type="checkbox" value="{{ .ID }}" data-name="{{ .Spec.Name }}">
{{ .Spec.Name }}
</label>
{{end}}
</nav>
</section>
<footer class="modal-card-foot">
<button id="btn-add-{{ name }}" type="button" class="button is-primary">Confirm</button>
<button type="button" class="button dismiss">Cancel</button>
</footer>
</div>
</div>
{{ end }}
{{ block body() }}
<section class="hero is-info">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title is-2">
SERVICE
</h1>
<h2 class="subtitle is-5">
Services are the definitions of tasks to run on a 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>
@ -79,13 +18,10 @@
<section class="hero is-small is-light">
<div class="hero-body">
<div class="container">
<h2 class="title is-2">
{{ .Service.Name }}
</h2>
<h2 class="title is-2">{{ .Service.Name }}</h2>
</div>
</div>
</section>
<nav class="navbar has-shadow">
<div class="container">
<div class="navbar-brand">
@ -96,10 +32,9 @@
</div>
</div>
</nav>
<section class="section">
<div class="container">
<form id="form-service" method="post" action="update" data-form="ajax-json" data-url="/service/">
<form method="post" data-form="ajax-json" data-url="/service/">
<div class="columns">
<div class="column">
<div class="field">
@ -108,59 +43,11 @@
<input name="image" value="{{ .Service.Image }}" class="input" placeholder="" data-v-rule="native" required>
</div>
</div>
<div class="field">
<label class="label">Mode</label>
<div class="field has-addons is-marginless">
<div class="control">
<span class="select">
<select id="cb-mode" name="mode">
<option value="replicated"{{if .Service.Mode == "replicated"}} selected{{end}}>Replicated</option>
<option value="global"{{if .Service.Mode == "global"}} selected{{end}}>Global</option>
</select>
</span>
</div>
<div class="control is-expanded">
<input id="txt-replicas" name="replicas" class="input" placeholder="" data-type="integer" data-v-rule="service-mode" {{if .Service.Mode == "global"}}style="display: none"{{else}}value="{{.Service.Replicas}}"{{end}}>
</div>
</div>
</div>
<div class="field">
<label class="label">Network</label>
<div class="control">
{{ set := .CheckedNetworks }}
{{range .Networks}}
{{ yield checkbox(name="networks", value=.Name, label=.Name, checked=set.Contains(.ID)) }}
{{end}}
</div>
</div>
{{ yield form_mode() }}
{{ yield form_network() }}
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<div class="field">
<label class="label">Command</label>
<div class="control">
<input name="command" value="{{ .Service.Command }}" class="input" type="text" placeholder="">
</div>
</div>
<div class="field">
<label class="label">Args</label>
<div class="control">
<input name="args" value="{{ .Service.Args }}" class="input" type="text" placeholder="">
</div>
</div>
<div class="field">
<label class="label">Work directory</label>
<div class="control">
<input name="dir" value="{{ .Service.Dir }}" class="input" type="text" placeholder="">
</div>
</div>
<div class="field">
<label class="label">User</label>
<div class="control">
<input name="user" value="{{ .Service.User }}" class="input" type="text" placeholder="">
</div>
</div>
</div>
{{ yield form_main_right() }}
</div>
<fieldset>
<legend class="lead is-5">Environments</legend>
@ -174,486 +61,11 @@
<legend class="lead is-5">Container Labels</legend>
{{ yield options_table(name="clabel", items=.Service.ContainerLabels) }}
</fieldset>
<hr>
<div class="tabs is-toggle is-fullwidth is-marginless" data-target="tab-content">
<ul>
<li class="is-active">
<a>
<span>Ports</span>
</a>
</li>
<li>
<a>
<span>Volumes</span>
</a>
</li>
<li>
<a>
<span>Configurations</span>
</a>
</li>
<li>
<a>
<span>Resources</span>
</a>
</li>
<li>
<a>
<span>Placement</span>
</a>
</li>
<li>
<a>
<span>Schedule policy</span>
</a>
</li>
<li>
<a>
<span>Log driver</span>
</a>
</li>
</ul>
</div>
<div id="tab-content" class="tabs-content has-no-top-border">
<div>
<div class="field">
<label class="label">Resolution mode</label>
<div class="control">
{{ yield radios(name="endpoint.mode", values=slice("vip", "dnsrr"), labels=slice("VIP", "DNS-RR"), checked=.Service.Endpoint.Mode) }}
</div>
</div>
<div class="field">
<label class="label">Port config</label>
<table id="table-endpoint-ports" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="endpoint.port">
<thead>
<tr>
<th>Host</th>
<th>Container</th>
<th>Protocol</th>
<th>Mode</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-endpoint-port">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{range i, p := .Service.Endpoint.Ports}}
<tr>
<td><input name="endpoint.ports[{{i}}].published_port" value="{{p.PublishedPort}}" class="input is-small" placeholder="port in host" data-type="integer"></td>
<td>
<input name="endpoint.ports[{{i}}].target_port" value="{{p.TargetPort}}" class="input is-small" placeholder="port in container" data-type="integer">
</td>
<td>
<div class="select is-small">
<select name="endpoint.ports[{{i}}].protocol">
{{ yield option(value="tcp", label="TCP", selected=p.Protocol) }}
{{ yield option(value="udp", label="UDP", selected=p.Protocol) }}
</select>
</div>
</td>
<td>
<div class="select is-small">
<select name="endpoint.ports[{{i}}].publish_mode">
{{ yield option(value="ingress", selected=p.PublishMode) }}
{{ yield option(value="host", selected=p.PublishMode) }}
</select>
</div>
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-endpoint-port">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<div style="display: none">
<table id="table-mounts" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="mount">
<thead>
<tr>
<th width="80">Type</th>
<th>Source</th>
<th>Target</th>
<th width="30">ReadOnly</th>
<th>Propagation</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-mount">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{range i, m := .Service.Mounts}}
<tr>
<td>
<div class="select is-small">
{{ yield select(name="mounts["+i+"].type", values=slice("bind", "volume", "tmpfs"), labels=slice("Bind", "Volume", "TempFS"), selected=m.Type) }}
</div>
</td>
<td>
<input name="mounts[{{i}}].src" value="{{m.Source}}" class="input is-small" placeholder="path in host">
</td>
<td><input name="mounts[{{i}}].dst" value="{{m.Target}}" class="input is-small" placeholder="path in container"></td>
<td>
<div class="select is-small">
{{ yield select(name="mounts["+i+"].read_only", values=slice("false", "true"), labels=slice("No", "Yes"), selected=m.Type, dt="bool") }}
</div>
</td>
<td>
<div class="select is-small">
<select name="mounts[{{i}}].propagation">
<option value="">--Select--</option>
{{ yield option(value="rprivate", selected=m.Propagation) }}
{{ yield option(value="private", selected=m.Propagation) }}
{{ yield option(value="rshared", selected=m.Propagation) }}
{{ yield option(value="shared", selected=m.Propagation) }}
{{ yield option(value="rslave", selected=m.Propagation) }}
{{ yield option(value="slave", selected=m.Propagation) }}
</select>
</div>
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-mount">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div style="display: none">
<div class="field">
<label class="label">Secrets</label>
<table id="table-secrets" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="secret">
<thead>
<tr>
<th>Name</th>
<th>File name</th>
<th>UID</th>
<th>GID</th>
<th>Mode</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-secret">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{range i, c := .Service.Secrets}}
<tr>
<td>{{ c.Name }}<input name="secrets[{{ i }}].id" value="{{ c.ID }}" type="hidden"><input name="secrets[{{ i }}].name" value="{{ c.Name }}" type="hidden"></td>
<td><input name="secrets[{{ i }}].file_name" value="{{ c.FileName }}" class="input is-small"></td>
<td><input name="secrets[{{ i }}].uid" value="{{ c.UID }}" class="input is-small"></td>
<td><input name="secrets[{{ i }}].gid" value="{{ c.GID }}" class="input is-small"></td>
<td><input name="secrets[{{ i }}].mode" value="{{ c.Mode }}" class="input is-small" data-type="integer"></td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-secret">
<span class="icon is-small">
<i class="fa fa-remove"></i>
</span>
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
<p class="help">Secrets will be mounted as /run/secrets/$FILE_NAME in containers by default, You can specify a custom location in Docker 17.06 and higher.</p>
</div>
<div class="field">
<label class="label">Configs</label>
<table id="table-configs" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="config">
<thead>
<tr>
<th>Name</th>
<th>File name</th>
<th>UID</th>
<th>GID</th>
<th>Mode</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-config">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{range i, c := .Service.Configs}}
<tr>
<td>{{ c.Name }}<input name="configs[{{ i }}].id" value="{{ c.ID }}" type="hidden"><input name="configs[{{ i }}].name" value="{{ c.Name }}" type="hidden"></td>
<td><input name="configs[{{ i }}].file_name" value="{{ c.FileName }}" class="input is-small"></td>
<td><input name="configs[{{ i }}].uid" value="{{ c.UID }}" class="input is-small"></td>
<td><input name="configs[{{ i }}].gid" value="{{ c.GID }}" class="input is-small"></td>
<td><input name="configs[{{ i }}].mode" value="{{ c.Mode }}" class="input is-small" data-type="integer"></td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-config">
<span class="icon is-small">
<i class="fa fa-remove"></i>
</span>
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
<p class="help">Configs will be mounted as /$FILE_NAME in containers by default, You can specify a custom location.</p>
</div>
</div>
<div style="display: none">
<div class="columns">
<div class="column">
<fieldset>
<legend class="lead is-5">Limits</legend>
<div class="field">
<label class="label">CPU</label>
<div class="control">
<input name="resource.limit.cpu" value="{{ .Service.Resource.Limit.CPU ? .Service.Resource.Limit.CPU : "" }}" class="input" placeholder="e.g. 1" data-type="float">
</div>
</div>
<div class="field">
<label class="label">Memory</label>
<div class="control">
<input name="resource.limit.memory" value="{{ .Service.Resource.Limit.Memory }}" class="input" placeholder="e.g. 1G">
</div>
</div>
</fieldset>
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<fieldset>
<legend class="lead is-5">Reservations</legend>
<div class="field">
<label class="label">CPU</label>
<div class="control">
<input name="resource.reserve.cpu" value="{{ .Service.Resource.Reserve.CPU ? .Service.Resource.Reserve.CPU : "" }}" class="input" placeholder="e.g. 0.1" data-type="float">
</div>
</div>
<div class="field">
<label class="label">Memory</label>
<div class="control">
<input name="resource.reserve.memory" value="{{ .Service.Resource.Reserve.Memory }}" class="input" placeholder="e.g. 10M">
</div>
</div>
</fieldset>
</div>
</div>
</div>
<div style="display: none">
<div class="field">
<label class="label">Constraints</label>
<table id="table-constraints" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="constraint">
<thead>
<tr>
<th>Name</th>
<th width="30">Operator</th>
<th>Value</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-constraint">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{ range i, c := .Service.Placement.Constraints }}
<tr>
<td>
<input name="placement.constraints[{{i}}].name" value="{{c.Name}}" class="input is-small" placeholder="e.g. node.role/node.hostname/node.id/node.labels.*/engine.labels.*/...">
</td>
<td>
<div class="select is-small">
{{ yield select(name="placement.constraints["+i+"].op", values=slice("==", "!="), selected=c.Operator) }}
</div>
</td>
<td>
<input name="placement.constraints[{{i}}].value" value="{{c.Value}}" class="input is-small" placeholder="e.g. manager">
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-constraint">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div class="field">
<label class="label">Preferences</label>
<table id="table-preferences" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="preference">
<thead>
<tr>
<th>Spread</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-preference">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{ range i, p := .Service.Placement.Preferences }}
<tr>
<td>
<input name="placement.preferences[{{i}}].spread" value="{{p.Spread}}" class="input is-small" placeholder="e.g. engine.labels.az">
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-preference">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
<div style="display: none">
<div class="columns">
<div class="column">
<fieldset>
<legend class="lead is-5">Update</legend>
<div class="field">
<label class="label">Parallelism</label>
<div class="control">
<input name="update_policy.parallelism" value="{{ trimZero(.Service.UpdatePolicy.Parallelism) }}" class="input" placeholder="" data-type="integer">
</div>
</div>
<div class="field">
<label class="label">Delay</label>
<div class="control">
<input name="update_policy.delay" value="{{ .Service.UpdatePolicy.Delay }}" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
<div class="field">
<label class="label">Failure action</label>
<div class="control">
{{ yield radios(name="update_policy.failure_action", values=slice("pause", "continue", "rollback"), checked=.Service.UpdatePolicy.FailureAction) }}
</div>
</div>
<div class="field">
<label class="label">Order</label>
<div class="control">
{{ yield radios(name="update_policy.order", values=slice("start-first", "stop-first"), checked=.Service.UpdatePolicy.Order) }}
</div>
</div>
</fieldset>
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<fieldset>
<legend class="lead is-5">Rollback</legend>
<div class="field">
<label class="label">Parallelism</label>
<div class="control">
<input name="rollback_policy.parallelism" value="{{ trimZero(.Service.RollbackPolicy.Parallelism) }}" class="input" placeholder="" data-type="integer">
</div>
</div>
<div class="field">
<label class="label">Delay</label>
<div class="control">
<input name="rollback_policy.delay" value="{{ .Service.RollbackPolicy.Delay }}" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
<div class="field">
<label class="label">Failure action</label>
<div class="control">
{{ yield radios(name="rollback_policy.failure_action", values=slice("pause", "continue"), checked=.Service.RollbackPolicy.FailureAction) }}
</div>
</div>
<div class="field">
<label class="label">Order</label>
<div class="control">
{{ yield radios(name="rollback_policy.order", values=slice("start-first", "stop-first"), checked=.Service.RollbackPolicy.Order) }}
</div>
</div>
</fieldset>
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<fieldset>
<legend class="lead is-5">Restart</legend>
<div class="field">
<label class="label">Condition</label>
<div class="control">
{{ yield radios(name="restart_policy.condition", values=slice("any", "on-failure", "none"), checked=.Service.RestartPolicy.Condition) }}
</div>
</div>
<div class="field">
<label class="label">Max attempts</label>
<div class="control">
<input name="restart_policy.max_attempts" value="{{ trimZero(.Service.RestartPolicy.MaxAttempts) }}" class="input" placeholder="" data-type="integer">
</div>
</div>
<div class="field">
<label class="label">Delay</label>
<div class="control">
<input name="restart_policy.delay" value="{{ .Service.RestartPolicy.Delay }}" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
<div class="field">
<label class="label">Window</label>
<div class="control">
<input name="restart_policy.window" value="{{ .Service.RestartPolicy.Delay }}" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
</fieldset>
</div>
</div>
</div>
<div style="display: none">
<div class="field">
<label class="label">Name</label>
<div class="control">
{{ yield radios(name="log_driver.name", values=slice("json-file", "syslog", "journald", "gelf", "fluentd", "awslogs", "splunk", "etwlogs", "gcplogs", "none"), checked=.Service.LogDriver.Name) }}
</div>
</div>
<div class="field">
<label class="label">Options</label>
{{ yield options_table(name="log_driver.option", items=.Service.LogDriver.Options) }}
</div>
</div>
</div>
<hr>
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-primary">Submit</button>
</div>
<div class="control">
<a href="/service/" class="button is-link">Cancel</a>
</div>
</div>
{{ yield form_others() }}
{{ yield form_submit(url="/service/") }}
</form>
</div>
</section>
{{ yield dialog(name="secret", items=.Secrets) }}
{{ yield dialog(name="config", items=.Configs) }}
{{ end }}
{{ end }}

View File

@ -1,38 +1,11 @@
{{ extends "../_layouts/default" }}
{{ extends "base" }}
{{ import "../_modules/pager" }}
{{ block script() }}
<script>$(() => new Swirl.Service.ListPage())</script>
{{ end }}
{{ block body() }}
<section class="hero is-info">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title is-2">
SERVICE
</h1>
<h2 class="subtitle is-5">
Services are the definitions of tasks to run on a swarm.
</h2>
</div>
</div>
<div class="hero-foot">
<div class="container">
<nav class="tabs is-boxed">
<ul>
<li class="is-active">
<a href="/service/">Services</a>
</li>
<li>
<a href="/service/template/">Templates</a>
</li>
</ul>
</nav>
</div>
</div>
</section>
{{ block body_content() }}
<section class="section">
<nav class="level">
<!-- Left side -->
@ -74,7 +47,7 @@
<th>Image</th>
<th width="145">Mode</th>
<th>Updated</th>
<th width="160">Action</th>
<th width="110">Action</th>
</tr>
</thead>
<tbody>
@ -91,13 +64,17 @@
</td>
<td>{{time(.UpdatedAt)}}</td>
<td>
<div class="field has-addons">
<p class="control"><a href="{{.Name}}/edit" class="button is-small is-dark is-outlined">Edit</a></p>
{{if .Mode == "replicated"}}
<p class="control"><button type="button" class="button is-small is-info is-outlined" data-action="scale-service" data-replicas="{{.Replicas}}">Scale</button></p>
{{end}}
<p class="control"><button class="button is-small is-danger is-outlined" data-action="delete-service">Delete</button></p>
</div>
<a href="{{.Name}}/edit" class="button is-small is-dark is-outlined tooltip is-tooltip-bottom" data-tooltip="Edit">
<span class="icon"><i class="fa fa-edit"></i></span>
</a>
{{if .Mode == "replicated"}}
<button type="button" class="button is-small is-info is-outlined tooltip is-tooltip-bottom" data-tooltip="Scale" data-action="scale-service" data-replicas="{{.Replicas}}">
<span class="icon"><i class="fa fa-arrows"></i></span>
</button>
{{end}}
<button class="button is-small is-danger is-outlined tooltip is-tooltip-bottom" data-tooltip="Delete" data-action="delete-service">
<span class="icon"><i class="fa fa-remove"></i></span>
</button>
</td>
</tr>
{{end}}

View File

@ -1,19 +1,6 @@
{{ 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">
SERVICE
</h1>
<h2 class="subtitle is-5">
Services are the definitions of tasks to run on a swarm.
</h2>
</div>
</div>
</section>
{{ extends "base" }}
{{ block body_content() }}
<div class="container">
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
<ul>

View File

@ -1,501 +1,79 @@
{{ extends "../_layouts/default" }}
{{ import "../_modules/form" }}
{{ extends "base" }}
{{ import "../_modules/service" }}
{{ block script() }}
<script>$(() => new Swirl.Service.NewPage())</script>
{{ end }}
{{ block dialog(name, items) }}
<div id="dlg-add-{{ name }}" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add {{ name }}</p>
<button class="delete"></button>
</header>
<section class="modal-card-body" style="max-height: 400px; overflow-y: auto">
<nav class="panel">
<div class="panel-block">
<p class="control has-icons-left">
<input class="input is-small" type="text" placeholder="Searching is not implemented...">
<span class="icon is-small is-left">
<i class="fa fa-search"></i>
</span>
</p>
</div>
{{ range items }}
<label class="panel-block">
<input type="checkbox" value="{{ .ID }}" data-name="{{ .Spec.Name }}">
{{ .Spec.Name }}
</label>
{{end}}
</nav>
</section>
<footer class="modal-card-foot">
<button id="btn-add-{{ name }}" type="button" class="button is-primary">Confirm</button>
<button type="button" class="button dismiss">Cancel</button>
</footer>
</div>
</div>
{{ end }}
{{ block body() }}
<section class="hero is-info">
{{ block body_content() }}
<section class="hero is-small is-light">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title is-2">
SERVICE
</h1>
<h2 class="subtitle is-5">
Services are the definitions of tasks to run on a swarm.
</h2>
<div class="container">
<h2 class="title is-2">Create service</h2>
</div>
</div>
</section>
<section class="section">
<h2 class="title">Create service</h2>
<hr>
<form id="form-service" method="post" data-form="ajax-json" data-url="/service/">
<div class="columns">
<div class="column">
<div class="field">
<label class="label">Name</label>
<div class="control">
<input name="name" class="input" type="text" placeholder="" data-v-rule="native" required>
</div>
</div>
<div class="field">
<label class="label">Image</label>
<div class="field has-addons is-marginless">
<div class="container">
<form method="post" data-form="ajax-json" data-url="/service/">
<div class="columns">
<div class="column">
<div class="field">
<label class="label">Name</label>
<div class="control">
<span class="select">
<select id="cb-registry" name="registry">
<option value="">DockerHub</option>
{{range .Registries}}
<option value="{{.ID}}" data-url="{{.URL}}">{{.Name}}</option>
{{end}}
</select>
</span>
</div>
<p class="control">
<a id="a-registry-url" class="button is-static" style="background-color: white; display: none"></a>
</p>
<div class="control is-expanded">
<input name="image" class="input" type="text" placeholder="" data-v-rule="native" required>
<input name="name" value="{{ .Service.Name }}" class="input" type="text" placeholder="" data-v-rule="native" required>
</div>
</div>
<p class="help">Do not enter registry host!</p>
</div>
<div class="field">
<label class="label">Mode</label>
<div class="field has-addons is-marginless">
<div class="control">
<span class="select">
<select id="cb-mode" name="mode">
<option value="replicated">Replicated</option>
<option value="global">Global</option>
</select>
</span>
</div>
<div class="control is-expanded">
<input id="txt-replicas" name="replicas" value="1" class="input" type="text" placeholder="" data-type="integer" data-v-rule="service-mode">
<div class="field">
<label class="label">Image</label>
<div class="field has-addons is-marginless">
<div class="control">
<span class="select">
<select id="cb-registry" name="registry">
<option value="">DockerHub</option>
{{ registry := .Service.Registry }}
{{ range .Registries }}
<option value="{{.ID}}" data-url="{{.URL}}"{{ if registry == .ID }} selected{{ end }}>{{.Name}}</option>
{{ end }}
</select>
</span>
</div>
<p class="control">
{{ if .Service.RegistryURL }}
<a id="a-registry-url" class="button is-static" style="background-color: white">{{ .Service.RegistryURL }}</a>
{{ else }}
<a id="a-registry-url" class="button is-static" style="background-color: white; display: none"></a>
{{ end }}
</p>
<div class="control is-expanded">
<input name="image" value="{{ .Service.Image }}" class="input" type="text" placeholder="" data-v-rule="native" required>
</div>
</div>
<p class="help">Do not enter registry host!</p>
</div>
{{ yield form_mode() }}
{{ yield form_network() }}
</div>
<div class="field">
<label class="label">Network</label>
<div class="control">
{{range .Networks}}
{{ yield checkbox(name="networks", value=.Name, label=.Name) }}
{{end}}
</div>
</div>
<div class="is-divider-vertical" data-content=""></div>
{{ yield form_main_right() }}
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<div class="field">
<label class="label">Command</label>
<div class="control">
<input name="command" class="input" type="text" placeholder="">
</div>
</div>
<div class="field">
<label class="label">Args</label>
<div class="control">
<input name="args" class="input" type="text" placeholder="">
</div>
</div>
<div class="field">
<label class="label">Work directory</label>
<div class="control">
<input name="dir" class="input" type="text" placeholder="">
</div>
</div>
<div class="field">
<label class="label">User</label>
<div class="control">
<input name="user" class="input" type="text" placeholder="">
</div>
</div>
</div>
</div>
<fieldset>
<legend class="lead is-5">Environments</legend>
{{ yield options(name="env") }}
</fieldset>
<fieldset>
<legend class="lead is-5">Service Labels</legend>
{{ yield options(name="slabel") }}
</fieldset>
<fieldset>
<legend class="lead is-5">Container Labels</legend>
{{ yield options(name="clabel") }}
</fieldset>
<hr>
<div class="tabs is-toggle is-fullwidth is-marginless" data-target="tab-content">
<ul>
<li class="is-active">
<a>
<span>Ports</span>
</a>
</li>
<li>
<a>
<span>Volumes</span>
</a>
</li>
<li>
<a>
<span>Configurations</span>
</a>
</li>
<li>
<a>
<span>Resources</span>
</a>
</li>
<li>
<a>
<span>Placement</span>
</a>
</li>
<li>
<a>
<span>Schedule policy</span>
</a>
</li>
<li>
<a>
<span>Log driver</span>
</a>
</li>
</ul>
</div>
<div id="tab-content" class="tabs-content has-no-top-border">
<div>
<div class="field">
<label class="label">Resolution mode</label>
<div class="control">
{{ yield radios(name="endpoint.mode", values=slice("vip", "dnsrr"), labels=slice("VIP", "DNS-RR")) }}
</div>
</div>
<div class="field">
<label class="label">Port config</label>
<table id="table-endpoint-ports" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="endpoint.port">
<thead>
<tr>
<th>Host</th>
<th>Container</th>
<th>Protocol</th>
<th>Mode</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-endpoint-port">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div style="display: none">
<table id="table-mounts" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="mount">
<thead>
<tr>
<th width="80">Type</th>
<th>Source</th>
<th>Target</th>
<th width="30">ReadOnly</th>
<th>Propagation</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-mount">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div style="display: none">
<div class="field">
<label class="label">Secrets</label>
<table id="table-secrets" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="secret">
<thead>
<tr>
<th>Name</th>
<th>File name</th>
<th>UID</th>
<th>GID</th>
<th>Mode</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-secret">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p class="help">Secrets will be mounted as /run/secrets/$FILE_NAME in containers by default, You can specify a custom location in Docker 17.06 and higher.</p>
</div>
<div class="field">
<label class="label">Configs</label>
<table id="table-configs" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="config">
<thead>
<tr>
<th>Name</th>
<th>File name</th>
<th>UID</th>
<th>GID</th>
<th>Mode</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-config">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p class="help">Configs will be mounted as /$FILE_NAME in containers by default, You can specify a custom location.</p>
</div>
</div>
<div style="display: none">
<div class="columns">
<div class="column">
<fieldset>
<legend class="lead is-5">Limits</legend>
<div class="field">
<label class="label">CPU</label>
<div class="control">
<input name="resource.limit.cpu" value="1" class="input" placeholder="e.g. 1" data-type="float">
</div>
</div>
<div class="field">
<label class="label">Memory</label>
<div class="control">
<input name="resource.limit.memory" value="" class="input" placeholder="e.g. 1G">
</div>
</div>
</fieldset>
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<fieldset>
<legend class="lead is-5">Reservations</legend>
<div class="field">
<label class="label">CPU</label>
<div class="control">
<input name="resource.reserve.cpu" value="" class="input" placeholder="e.g. 0.1" data-type="float">
</div>
</div>
<div class="field">
<label class="label">Memory</label>
<div class="control">
<input name="resource.reserve.memory" value="" class="input" placeholder="e.g. 10M">
</div>
</div>
</fieldset>
</div>
</div>
</div>
<div style="display: none">
<div class="field">
<label class="label">Constraints</label>
<table id="table-constraints" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="constraint">
<thead>
<tr>
<th>Name</th>
<th width="30">Operator</th>
<th>Value</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-constraint">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="field">
<label class="label">Preferences</label>
<table id="table-preferences" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="preference">
<thead>
<tr>
<th>Spread</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-preference">
<span class="icon is-small">
<i class="fa fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div style="display: none">
<div class="columns">
<div class="column">
<fieldset>
<legend class="lead is-5">Update</legend>
<div class="field">
<label class="label">Parallelism</label>
<div class="control">
<input name="update_policy.parallelism" value="1" class="input" placeholder="" data-type="integer">
</div>
</div>
<div class="field">
<label class="label">Delay</label>
<div class="control">
<input name="update_policy.delay" value="0s" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
<div class="field">
<label class="label">Failure action</label>
<div class="control">
{{ yield radios(name="update_policy.failure_action", values=slice("pause", "continue", "rollback"), checked="pause") }}
</div>
</div>
<div class="field">
<label class="label">Order</label>
<div class="control">
{{ yield radios(name="update_policy.order", values=slice("start-first", "stop-first"), checked="stop-first") }}
</div>
</div>
</fieldset>
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<fieldset>
<legend class="lead is-5">Rollback</legend>
<div class="field">
<label class="label">Parallelism</label>
<div class="control">
<input name="rollback_policy.parallelism" value="1" class="input" placeholder="" data-type="integer">
</div>
</div>
<div class="field">
<label class="label">Delay</label>
<div class="control">
<input name="rollback_policy.delay" value="0s" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
<div class="field">
<label class="label">Failure action</label>
<div class="control">
{{ yield radios(name="rollback_policy.failure_action", values=slice("pause", "continue"), checked="pause") }}
</div>
</div>
<div class="field">
<label class="label">Order</label>
<div class="control">
{{ yield radios(name="rollback_policy.order", values=slice("start-first", "stop-first"), checked="stop-first") }}
</div>
</div>
</fieldset>
</div>
<div class="is-divider-vertical" data-content=""></div>
<div class="column">
<fieldset>
<legend class="lead is-5">Restart</legend>
<div class="field">
<label class="label">Condition</label>
<div class="control">
{{ yield radios(name="restart_policy.condition", values=slice("any", "on-failure", "none"), checked="any") }}
</div>
</div>
<div class="field">
<label class="label">MaxAttempts</label>
<div class="control">
<input name="restart_policy.max_attempts" value="0" class="input" placeholder="" data-type="integer">
</div>
</div>
<div class="field">
<label class="label">Delay</label>
<div class="control">
<input name="restart_policy.delay" value="5s" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
<div class="field">
<label class="label">Window</label>
<div class="control">
<input name="restart_policy.window" value="0s" class="input" placeholder="ns|us|ms|s|m|h">
</div>
</div>
</fieldset>
</div>
</div>
</div>
<div style="display: none">
<div class="field">
<label class="label">Name</label>
<div class="control">
{{ yield radios(name="log_driver.name", values=slice("json-file", "syslog", "journald", "gelf", "fluentd", "awslogs", "splunk", "etwlogs", "gcplogs", "none")) }}
</div>
</div>
<div class="field">
<label class="label">Options</label>
{{ yield options(name="log_driver.option", alias="log_driver_option") }}
</div>
</div>
</div>
<hr>
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-primary">Submit</button>
</div>
<div class="control">
<a href="/service/" class="button is-link">Cancel</a>
</div>
</div>
</form>
<fieldset>
<legend class="lead is-5">Environments</legend>
{{ yield options_table(name="env", items=.Service.Environments) }}
</fieldset>
<fieldset>
<legend class="lead is-5">Service Labels</legend>
{{ yield options_table(name="slabel", items=.Service.ServiceLabels) }}
</fieldset>
<fieldset>
<legend class="lead is-5">Container Labels</legend>
{{ yield options_table(name="clabel", items=.Service.ContainerLabels) }}
</fieldset>
{{ yield form_others() }}
{{ yield form_submit(url="/service/") }}
</form>
</div>
</section>
{{ yield dialog(name="secret", items=.Secrets) }}
{{ yield dialog(name="config", items=.Configs) }}
{{ end }}
{{ 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,20 +9,7 @@
<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">
SERVICE
</h1>
<h2 class="subtitle is-5">
Services are the definitions of tasks to run on a 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>

View File

@ -0,0 +1,31 @@
{{ 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">
SERVICE TEMPLATE
</h1>
<h2 class="subtitle is-5">
Manage service templates.
</h2>
</div>
</div>
<div class="hero-foot">
<div class="container">
<nav class="tabs is-boxed">
<ul>
<li>
<a href="/service/">Services</a>
</li>
<li class="is-active">
<a href="/service/template/">Templates</a>
</li>
</ul>
</nav>
</div>
</div>
</section>
{{ yield body_content() }}
{{ end }}

View File

@ -0,0 +1,92 @@
{{ extends "base" }}
{{ import "../../_modules/service" }}
{{ block script() }}
<script>$(() => new Swirl.Service.NewPage())</script>
{{ end }}
{{ 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><a href="/service/">Services</a></li>
<li><a href="/service/template/">Templates</a></li>
<li class="is-active"><a>{{ .Action }}</a></li>
</ul>
</nav>
</div>
<section class="hero is-small is-light">
<div class="hero-body">
<div class="container">
<h2 class="title is-2">{{ .Action }} service template</h2>
</div>
</div>
</section>
<section class="section">
<div class="container">
<form method="post" data-form="ajax-json" data-url="/service/template/">
<div class="columns">
<div class="column">
<div class="field">
<label class="label">Name</label>
<div class="control">
<input name="name" value="{{ .Service.Name }}" class="input" type="text" placeholder="Template name" data-v-rule="native" required>
</div>
</div>
<div class="field">
<label class="label">Image</label>
<div class="field has-addons is-marginless">
<div class="control">
<span class="select">
<select id="cb-registry" name="registry">
<option value="">DockerHub</option>
{{ registry := .Service.Registry }}
{{ range .Registries }}
<option value="{{.ID}}" data-url="{{.URL}}"{{ if registry == .ID }} selected{{ end }}>{{.Name}}</option>
{{ end }}
</select>
</span>
</div>
<p class="control">
{{ if .Service.RegistryURL }}
<a id="a-registry-url" class="button is-static" style="background-color: white">{{ .Service.RegistryURL }}</a>
{{ else }}
<a id="a-registry-url" class="button is-static" style="background-color: white; display: none"></a>
{{ end }}
</p>
<div class="control is-expanded">
<input name="image" value="{{ .Service.Image }}" class="input" type="text" placeholder="">
</div>
</div>
<p class="help">Do not enter registry host!</p>
</div>
{{ yield form_mode() }}
{{ yield form_network() }}
</div>
<div class="is-divider-vertical" data-content=""></div>
{{ yield form_main_right() }}
</div>
<fieldset>
<legend class="lead is-5">Environments</legend>
{{ yield options_table(name="env", items=.Service.Environments) }}
</fieldset>
<fieldset>
<legend class="lead is-5">Service Labels</legend>
{{ yield options_table(name="slabel", items=.Service.ServiceLabels) }}
</fieldset>
<fieldset>
<legend class="lead is-5">Container Labels</legend>
{{ yield options_table(name="clabel", items=.Service.ContainerLabels) }}
</fieldset>
{{ yield form_others() }}
{{ yield form_submit(url="/service/template/") }}
</form>
</div>
</section>
{{ yield dialog(name="secret", items=.Secrets) }}
{{ yield dialog(name="config", items=.Configs) }}
{{ end }}

View File

@ -1,38 +1,11 @@
{{ extends "../../_layouts/default" }}
{{ extends "base" }}
{{ import "../../_modules/pager" }}
{*{{ block script() }}*}
{*<script>$(() => new Swirl.Service.ListPage())</script>*}
{*{{ end }}*}
{{ block body() }}
<section class="hero is-info">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title is-2">
SERVICE TEMPLATE
</h1>
<h2 class="subtitle is-5">
Manage service templates.
</h2>
</div>
</div>
<div class="hero-foot">
<div class="container">
<nav class="tabs is-boxed">
<ul>
<li>
<a href="/service/">Services</a>
</li>
<li class="is-active">
<a href="/service/template/">Templates</a>
</li>
</ul>
</nav>
</div>
</div>
</section>
{{ block script() }}
<script>$(() => new Swirl.Service.Template.ListPage())</script>
{{ end }}
{{ block body_content() }}
<section class="section">
<nav class="level">
<!-- Left side -->
@ -41,7 +14,7 @@
<form>
<div class="field has-addons">
<p class="control">
<input name="name" value="" class="input" placeholder="Search by name">
<input name="name" value="{{ .Name }}" class="input" placeholder="Search by name">
</p>
<p class="control">
<button class="button is-primary">Search</button>
@ -51,7 +24,7 @@
</div>
<div class="level-item">
<p class="subtitle is-5">
<strong>0</strong> templates
<strong>{{ .Pager.Count }}</strong> templates
</p>
</div>
</div>
@ -62,31 +35,36 @@
</p>
</div>
</nav>
<table id="table-items" class="table is-bordered is-striped is-narrow is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Created at</th>
<th>Updated at</th>
<th width="160">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="">Manage Web</a></td>
<td>后台站点</td>
<td>2017-09-11 21:17:17</td>
{{ range .Templates }}
<tr data-id="{{ .ID }}">
<td>{{ .Name }}</td>
<td>{{time(.CreatedAt)}}</td>
<td>{{time(.UpdatedAt)}}</td>
<td>
<div class="field has-addons">
{*<p class="control"><a href="{{.ID}}/edit" class="button is-small is-dark is-outlined">Edit</a></p>*}
{*<p class="control"><a href="/service/new?template={{.ID}}" class="button is-small is-success is-outlined">Create</a></p>*}
<p class="control"><button class="button is-small is-danger is-outlined" data-action="delete-template">Delete</button></p>
</div>
<a href="/service/new?template={{.ID}}" class="button is-small is-success is-outlined tooltip is-tooltip-bottom" data-tooltip="Create Service">
<span class="icon"><i class="fa fa-plus"></i></span>
</a>
<a href="{{.ID}}/edit" class="button is-small is-dark is-outlined tooltip is-tooltip-bottom" data-tooltip="Edit">
<span class="icon"><i class="fa fa-edit"></i></span>
</a>
<button class="button is-small is-danger is-outlined tooltip is-tooltip-bottom" data-tooltip="Delete" data-action="delete-template">
<span class="icon"><i class="fa fa-remove"></i></span>
</button>
</td>
</tr>
</tr>
{{ end }}
</tbody>
</table>
{*{{ yield pager(info=.Pager) }}*}
{{ yield pager(info=.Pager) }}
</section>
{{ end }}