mirror of
https://github.com/cuigh/swirl
synced 2025-04-10 15:35:38 +00:00
Add service template management
This commit is contained in:
parent
8c00a7f69d
commit
722c7e3e09
@ -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
@ -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();
|
||||
})
|
||||
});
|
||||
|
26
assets/swirl/ts/service/template/list.ts
Normal file
26
assets/swirl/ts/service/template/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
12
biz/event.go
12
biz/event.go
@ -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
69
biz/template.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
73
dao/mongo/template.go
Normal 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
|
||||
}
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
const (
|
||||
// Version is the version of Swirl
|
||||
Version = "0.5.2"
|
||||
Version = "0.5.3"
|
||||
)
|
||||
|
||||
const (
|
||||
|
11
misc/perm.go
11
misc/perm.go
@ -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{
|
||||
|
@ -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"`
|
||||
|
@ -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"
|
||||
|
@ -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"`
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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
564
views/_modules/service.jet
Normal 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
31
views/service/base.jet
Normal 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 }}
|
@ -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>
|
||||
|
@ -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 }}
|
||||
|
@ -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}}
|
||||
|
@ -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>
|
||||
|
@ -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 }}
|
||||
|
@ -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>
|
||||
|
31
views/service/template/base.jet
Normal file
31
views/service/template/base.jet
Normal 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 }}
|
92
views/service/template/edit.jet
Normal file
92
views/service/template/edit.jet
Normal 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 }}
|
@ -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 }}
|
Loading…
Reference in New Issue
Block a user