mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 14:51:57 +00:00
Combine stack tasks with archives
This commit is contained in:
parent
c624b1f9bd
commit
f2a4b7209c
@ -2813,4 +2813,111 @@ var Swirl;
|
||||
Volume.NewPage = NewPage;
|
||||
})(Volume = Swirl.Volume || (Swirl.Volume = {}));
|
||||
})(Swirl || (Swirl = {}));
|
||||
var Swirl;
|
||||
(function (Swirl) {
|
||||
var Stack;
|
||||
(function (Stack) {
|
||||
var Validator = Swirl.Core.Validator;
|
||||
var Notification = Swirl.Core.Notification;
|
||||
class ContentRequiredRule {
|
||||
validate($form, $input, arg) {
|
||||
let el = $input[0];
|
||||
if ($("#type-" + arg).prop("checked")) {
|
||||
console.log(el.value);
|
||||
return { ok: el.checkValidity ? el.checkValidity() : true, error: el.validationMessage };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
class EditPage {
|
||||
constructor() {
|
||||
Validator.register("content", new ContentRequiredRule(), "");
|
||||
this.editor = CodeMirror.fromTextArea($("#txt-content")[0], { lineNumbers: true });
|
||||
$("#file-content").change(e => {
|
||||
let file = e.target;
|
||||
if (file.files.length > 0) {
|
||||
$('#filename').text(file.files[0].name);
|
||||
}
|
||||
});
|
||||
$("#type-input,#type-upload").click(e => {
|
||||
let type = $(e.target).val();
|
||||
$("#div-input").toggle(type == "input");
|
||||
$("#div-upload").toggle(type == "upload");
|
||||
});
|
||||
$("#btn-submit").click(this.submit.bind(this));
|
||||
}
|
||||
submit(e) {
|
||||
this.editor.save();
|
||||
let results = Validator.bind("#div-form").validate();
|
||||
if (results.length > 0) {
|
||||
return;
|
||||
}
|
||||
let data = new FormData();
|
||||
data.append('name', $("#name").val());
|
||||
if ($("#type-input").prop("checked")) {
|
||||
data.append('content', $('#txt-content').val());
|
||||
}
|
||||
else {
|
||||
let file = $('#file-content')[0];
|
||||
data.append('content', file.files[0]);
|
||||
}
|
||||
let url = $(e.target).data("url") || "";
|
||||
$ajax.post(url, data).encoder("none").trigger(e.target).json((r) => {
|
||||
if (r.success) {
|
||||
location.href = "/stack/";
|
||||
}
|
||||
else {
|
||||
Notification.show("danger", `FAILED: ${r.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Stack.EditPage = EditPage;
|
||||
})(Stack = Swirl.Stack || (Swirl.Stack = {}));
|
||||
})(Swirl || (Swirl = {}));
|
||||
var Swirl;
|
||||
(function (Swirl) {
|
||||
var Stack;
|
||||
(function (Stack) {
|
||||
var Modal = Swirl.Core.Modal;
|
||||
var Dispatcher = Swirl.Core.Dispatcher;
|
||||
class ListPage {
|
||||
constructor() {
|
||||
let dispatcher = Dispatcher.bind("#table-items");
|
||||
dispatcher.on("deploy-stack", this.deployStack.bind(this));
|
||||
dispatcher.on("shutdown-stack", this.shutdownStack.bind(this));
|
||||
dispatcher.on("delete-stack", this.deleteStack.bind(this));
|
||||
}
|
||||
deployStack(e) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to deploy stack: <strong>${name}</strong>?`, "Deploy stack", (dlg, e) => {
|
||||
$ajax.post(`${name}/deploy`).trigger(e.target).json(r => {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
shutdownStack(e) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to shutdown stack: <strong>${name}</strong>?`, "Shutdown stack", (dlg, e) => {
|
||||
$ajax.post(`${name}/shutdown`).trigger(e.target).json(r => {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
deleteStack(e) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to remove archive: <strong>${name}</strong>?`, "Delete stack", (dlg, e) => {
|
||||
$ajax.post(`${name}/delete`).trigger(e.target).json(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Stack.ListPage = ListPage;
|
||||
})(Stack = Swirl.Stack || (Swirl.Stack = {}));
|
||||
})(Swirl || (Swirl = {}));
|
||||
//# sourceMappingURL=swirl.js.map
|
File diff suppressed because one or more lines are too long
70
assets/swirl/ts/stack/edit.ts
Normal file
70
assets/swirl/ts/stack/edit.ts
Normal file
@ -0,0 +1,70 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Stack {
|
||||
import Validator = Swirl.Core.Validator;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Notification = Swirl.Core.Notification;
|
||||
import ValidationRule = Swirl.Core.ValidationRule;
|
||||
|
||||
class ContentRequiredRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
let el = <HTMLInputElement>$input[0];
|
||||
if ($("#type-" + arg).prop("checked")) {
|
||||
console.log(el.value);
|
||||
return {ok: el.checkValidity ? el.checkValidity() : true, error: el.validationMessage};
|
||||
}
|
||||
return {ok: true}
|
||||
}
|
||||
}
|
||||
|
||||
export class EditPage {
|
||||
private editor: any;
|
||||
|
||||
constructor() {
|
||||
Validator.register("content", new ContentRequiredRule(), "");
|
||||
|
||||
this.editor = CodeMirror.fromTextArea($("#txt-content")[0], {lineNumbers: true});
|
||||
|
||||
$("#file-content").change(e => {
|
||||
let file = <HTMLInputElement>e.target;
|
||||
if (file.files.length > 0) {
|
||||
$('#filename').text(file.files[0].name);
|
||||
}
|
||||
});
|
||||
$("#type-input,#type-upload").click(e => {
|
||||
let type = $(e.target).val();
|
||||
$("#div-input").toggle(type == "input");
|
||||
$("#div-upload").toggle(type == "upload");
|
||||
});
|
||||
$("#btn-submit").click(this.submit.bind(this))
|
||||
}
|
||||
|
||||
private submit(e: JQueryEventObject) {
|
||||
this.editor.save();
|
||||
|
||||
let results = Validator.bind("#div-form").validate();
|
||||
if (results.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = new FormData();
|
||||
data.append('name', $("#name").val());
|
||||
if ($("#type-input").prop("checked")) {
|
||||
data.append('content', $('#txt-content').val());
|
||||
} else {
|
||||
let file = <HTMLInputElement>$('#file-content')[0];
|
||||
data.append('content', file.files[0]);
|
||||
}
|
||||
|
||||
let url = $(e.target).data("url") || "";
|
||||
$ajax.post(url, data).encoder("none").trigger(e.target).json((r: AjaxResult) => {
|
||||
if (r.success) {
|
||||
location.href = "/stack/"
|
||||
} else {
|
||||
Notification.show("danger", `FAILED: ${r.message}`);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare var CodeMirror: any;
|
46
assets/swirl/ts/stack/list.ts
Normal file
46
assets/swirl/ts/stack/list.ts
Normal file
@ -0,0 +1,46 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Stack {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class ListPage {
|
||||
constructor() {
|
||||
let dispatcher = Dispatcher.bind("#table-items");
|
||||
dispatcher.on("deploy-stack", this.deployStack.bind(this));
|
||||
dispatcher.on("shutdown-stack", this.shutdownStack.bind(this));
|
||||
dispatcher.on("delete-stack", this.deleteStack.bind(this));
|
||||
}
|
||||
|
||||
private deployStack(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to deploy stack: <strong>${name}</strong>?`, "Deploy stack", (dlg, e) => {
|
||||
$ajax.post(`${name}/deploy`).trigger(e.target).json<AjaxResult>(r => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private shutdownStack(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to shutdown stack: <strong>${name}</strong>?`, "Shutdown stack", (dlg, e) => {
|
||||
$ajax.post(`${name}/shutdown`).trigger(e.target).json<AjaxResult>(r => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private deleteStack(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to remove archive: <strong>${name}</strong>?`, "Delete stack", (dlg, e) => {
|
||||
$ajax.post(`${name}/delete`).trigger(e.target).json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ import (
|
||||
const stackLabel = "com.docker.stack.namespace"
|
||||
|
||||
// StackList return all stacks.
|
||||
func StackList() (stacks []*model.StackListInfo, err error) {
|
||||
func StackList() (stacks []*model.Stack, err error) {
|
||||
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
var services []swarm.Service
|
||||
opts := types.ServiceListOptions{
|
||||
@ -30,7 +30,7 @@ func StackList() (stacks []*model.StackListInfo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
m := make(map[string]*model.StackListInfo)
|
||||
m := make(map[string]*model.Stack)
|
||||
for _, service := range services {
|
||||
labels := service.Spec.Labels
|
||||
name, ok := labels[stackLabel]
|
||||
@ -42,7 +42,7 @@ func StackList() (stacks []*model.StackListInfo, err error) {
|
||||
if stack, ok := m[name]; ok {
|
||||
stack.Services = append(stack.Services, service.Spec.Name)
|
||||
} else {
|
||||
m[name] = &model.StackListInfo{
|
||||
m[name] = &model.Stack{
|
||||
Name: name,
|
||||
Services: []string{service.Spec.Name},
|
||||
}
|
||||
@ -125,7 +125,8 @@ func StackRemove(name string) error {
|
||||
}
|
||||
|
||||
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
|
||||
return fmt.Errorf("nothing found in stack: %s", name)
|
||||
//return fmt.Errorf("nothing found in stack: %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove services
|
||||
|
16
biz/event.go
16
biz/event.go
@ -89,9 +89,9 @@ func (b *eventBiz) CreateVolume(action model.EventAction, name string, user web.
|
||||
b.Create(event)
|
||||
}
|
||||
|
||||
func (b *eventBiz) CreateStackTask(action model.EventAction, name string, user web.User) {
|
||||
func (b *eventBiz) CreateStack(action model.EventAction, name string, user web.User) {
|
||||
event := &model.Event{
|
||||
Type: model.EventTypeStackTask,
|
||||
Type: model.EventTypeStack,
|
||||
Action: action,
|
||||
Code: name,
|
||||
Name: name,
|
||||
@ -101,18 +101,6 @@ func (b *eventBiz) CreateStackTask(action model.EventAction, name string, user w
|
||||
b.Create(event)
|
||||
}
|
||||
|
||||
func (b *eventBiz) CreateStackArchive(action model.EventAction, id, name string, user web.User) {
|
||||
event := &model.Event{
|
||||
Type: model.EventTypeStackArchive,
|
||||
Action: action,
|
||||
Code: id,
|
||||
Name: name,
|
||||
UserID: user.ID(),
|
||||
Username: user.Name(),
|
||||
}
|
||||
b.Create(event)
|
||||
}
|
||||
|
||||
func (b *eventBiz) CreateSecret(action model.EventAction, name string, user web.User) {
|
||||
event := &model.Event{
|
||||
Type: model.EventTypeSecret,
|
||||
|
124
biz/stack.go
124
biz/stack.go
@ -1,60 +1,128 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz/docker"
|
||||
"github.com/cuigh/swirl/dao"
|
||||
"github.com/cuigh/swirl/model"
|
||||
)
|
||||
|
||||
// Stack return a stack biz instance.
|
||||
var Archive = &archiveBiz{}
|
||||
var Stack = &stackBiz{}
|
||||
|
||||
type archiveBiz struct {
|
||||
type stackBiz struct {
|
||||
}
|
||||
|
||||
func (b *archiveBiz) List(args *model.ArchiveListArgs) (archives []*model.Archive, count int, err error) {
|
||||
func (b *stackBiz) List(args *model.StackListArgs) (stacks []*model.Stack, err error) {
|
||||
var (
|
||||
upStacks, internalStacks []*model.Stack
|
||||
upMap = make(map[string]*model.Stack)
|
||||
)
|
||||
|
||||
// load real stacks
|
||||
upStacks, err = docker.StackList()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, stack := range upStacks {
|
||||
upMap[stack.Name] = stack
|
||||
}
|
||||
|
||||
// load stack definitions
|
||||
do(func(d dao.Interface) {
|
||||
archives, count, err = d.ArchiveList(args)
|
||||
internalStacks, err = d.StackList()
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *archiveBiz) Create(archive *model.Archive) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
err = d.ArchiveCreate(archive)
|
||||
//if err == nil {
|
||||
// Event.CreateStackArchive(model.EventActionCreate, archive.ID, archive.Name, ctx.User())
|
||||
//}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *archiveBiz) Delete(id string, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
var archive *model.Archive
|
||||
archive, err = d.ArchiveGet(id)
|
||||
if err != nil {
|
||||
return
|
||||
// merge stacks and definitions
|
||||
for _, stack := range internalStacks {
|
||||
stack.Internal = true
|
||||
if s, ok := upMap[stack.Name]; ok {
|
||||
stack.Services = s.Services
|
||||
delete(upMap, stack.Name)
|
||||
}
|
||||
if !b.filter(stack, args) {
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
}
|
||||
for _, stack := range upMap {
|
||||
if !b.filter(stack, args) {
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = d.ArchiveDelete(id)
|
||||
func (b *stackBiz) filter(stack *model.Stack, args *model.StackListArgs) bool {
|
||||
if args.Name != "" {
|
||||
if !strings.Contains(strings.ToLower(stack.Name), strings.ToLower(args.Name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
switch args.Filter {
|
||||
case "up":
|
||||
if len(stack.Services) == 0 {
|
||||
return true
|
||||
}
|
||||
case "internal":
|
||||
if !stack.Internal {
|
||||
return true
|
||||
}
|
||||
case "external":
|
||||
if stack.Internal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *stackBiz) Create(stack *model.Stack, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
err = d.StackCreate(stack)
|
||||
if err == nil {
|
||||
Event.CreateStackArchive(model.EventActionDelete, id, archive.Name, user)
|
||||
Event.CreateStack(model.EventActionCreate, stack.Name, user)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *archiveBiz) Get(id string) (archives *model.Archive, err error) {
|
||||
func (b *stackBiz) Get(name string) (stack *model.Stack, err error) {
|
||||
do(func(d dao.Interface) {
|
||||
archives, err = d.ArchiveGet(id)
|
||||
stack, err = d.StackGet(name)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *archiveBiz) Update(archive *model.Archive) (err error) {
|
||||
func (b *stackBiz) Update(stack *model.Stack, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
err = d.ArchiveUpdate(archive)
|
||||
err = d.StackUpdate(stack)
|
||||
if err == nil {
|
||||
Event.CreateStack(model.EventActionUpdate, stack.Name, user)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *stackBiz) Delete(name string, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
err = d.StackDelete(name)
|
||||
if err == nil {
|
||||
Event.CreateStack(model.EventActionDelete, name, user)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Migrate migrates old archives to stack collection.
|
||||
func (b *stackBiz) Migrate() {
|
||||
do(func(d dao.Interface) {
|
||||
d.StackMigrate()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ button.submit: Submit
|
||||
button.cancel: Cancel
|
||||
button.confirm: Confirm
|
||||
button.delete: Delete
|
||||
button.remove: Remove
|
||||
button.prune: Prune
|
||||
button.new: New
|
||||
button.add: Add
|
||||
@ -150,6 +151,7 @@ service.template.button.create: Create service
|
||||
stack.title: Stack
|
||||
stack.description: A stack is a logical grouping of related services that are usually deployed together and require each other to work as intended.
|
||||
stack.button.deploy: Deploy
|
||||
stack.button.shutdown: Shutdown
|
||||
|
||||
# task pages
|
||||
task.title: Task
|
||||
|
@ -10,6 +10,7 @@ button.submit: 提交
|
||||
button.cancel: 取消
|
||||
button.confirm: 确定
|
||||
button.delete: 删除
|
||||
button.remove: 移除
|
||||
button.prune: 清理
|
||||
button.new: 新建
|
||||
button.add: 添加
|
||||
@ -149,7 +150,8 @@ service.template.button.create: 创建服务
|
||||
# stack pages
|
||||
stack.title: 编排
|
||||
stack.description: 编排是相关服务的一个逻辑分组,这些服务通常互相依赖,需要一块部署。
|
||||
stack.button.deploy: 部署
|
||||
stack.button.deploy: 发布
|
||||
stack.button.shutdown: 发布
|
||||
|
||||
# task pages
|
||||
task.title: 任务
|
||||
|
@ -10,136 +10,126 @@ import (
|
||||
|
||||
// StackController is a controller of docker stack(compose)
|
||||
type StackController struct {
|
||||
TaskList web.HandlerFunc `path:"/task/" name:"stack.task.list" authorize:"!" desc:"stack task list page"`
|
||||
TaskDelete web.HandlerFunc `path:"/task/delete" method:"post" name:"stack.task.delete" authorize:"!" desc:"delete stack task"`
|
||||
ArchiveList web.HandlerFunc `path:"/archive/" name:"stack.archive.list" authorize:"!" desc:"stack archive list page"`
|
||||
ArchiveDetail web.HandlerFunc `path:"/archive/:id/detail" name:"stack.archive.detail" authorize:"!" desc:"stack archive detail page"`
|
||||
ArchiveEdit web.HandlerFunc `path:"/archive/:id/edit" name:"stack.archive.edit" authorize:"!" desc:"stack archive edit page"`
|
||||
ArchiveUpdate web.HandlerFunc `path:"/archive/:id/update" method:"post" name:"stack.archive.update" authorize:"!" desc:"update stack archive"`
|
||||
ArchiveDelete web.HandlerFunc `path:"/archive/delete" method:"post" name:"stack.archive.delete" authorize:"!" desc:"delete stack archive"`
|
||||
ArchiveDeploy web.HandlerFunc `path:"/archive/deploy" method:"post" name:"stack.archive.deploy" authorize:"!" desc:"deploy stack archive"`
|
||||
ArchiveNew web.HandlerFunc `path:"/archive/new" name:"stack.archive.new" authorize:"!" desc:"new stack.archive page"`
|
||||
ArchiveCreate web.HandlerFunc `path:"/archive/new" method:"post" name:"stack.archive.create" authorize:"!" desc:"create stack.archive"`
|
||||
List web.HandlerFunc `path:"/" name:"stack.list" authorize:"!" desc:"stack list page"`
|
||||
New web.HandlerFunc `path:"/new" name:"stack.new" authorize:"!" desc:"new stack page"`
|
||||
Create web.HandlerFunc `path:"/new" method:"post" name:"stack.create" authorize:"!" desc:"create stack"`
|
||||
Detail web.HandlerFunc `path:"/:name/detail" name:"stack.detail" authorize:"!" desc:"stack detail page"`
|
||||
Edit web.HandlerFunc `path:"/:name/edit" name:"stack.edit" authorize:"!" desc:"stack edit page"`
|
||||
Update web.HandlerFunc `path:"/:name/update" method:"post" name:"stack.update" authorize:"!" desc:"update stack"`
|
||||
Deploy web.HandlerFunc `path:"/:name/deploy" method:"post" name:"stack.deploy" authorize:"!" desc:"deploy stack"`
|
||||
Shutdown web.HandlerFunc `path:"/:name/shutdown" method:"post" name:"stack.shutdown" authorize:"!" desc:"shutdown stack"`
|
||||
Delete web.HandlerFunc `path:"/:name/delete" method:"post" name:"stack.delete" authorize:"!" desc:"delete stack"`
|
||||
}
|
||||
|
||||
// Stack creates an instance of StackController
|
||||
func Stack() (c *StackController) {
|
||||
return &StackController{
|
||||
TaskList: stackTaskList,
|
||||
TaskDelete: stackTaskDelete,
|
||||
ArchiveList: stackArchiveList,
|
||||
ArchiveDetail: stackArchiveDetail,
|
||||
ArchiveEdit: stackArchiveEdit,
|
||||
ArchiveUpdate: stackArchiveUpdate,
|
||||
ArchiveDelete: stackArchiveDelete,
|
||||
ArchiveDeploy: stackArchiveDeploy,
|
||||
ArchiveNew: stackArchiveNew,
|
||||
ArchiveCreate: stackArchiveCreate,
|
||||
List: stackList,
|
||||
New: stackNew,
|
||||
Create: stackCreate,
|
||||
Detail: stackDetail,
|
||||
Edit: stackEdit,
|
||||
Update: stackUpdate,
|
||||
Deploy: stackDeploy,
|
||||
Shutdown: stackShutdown,
|
||||
Delete: stackDelete,
|
||||
}
|
||||
}
|
||||
|
||||
func stackTaskList(ctx web.Context) error {
|
||||
stacks, err := docker.StackList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := newModel(ctx).Set("Stacks", stacks)
|
||||
return ctx.Render("stack/task/list", m)
|
||||
}
|
||||
|
||||
func stackTaskDelete(ctx web.Context) error {
|
||||
name := ctx.F("name")
|
||||
err := docker.StackRemove(name)
|
||||
if err == nil {
|
||||
biz.Event.CreateStackTask(model.EventActionDelete, name, ctx.User())
|
||||
}
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
|
||||
func stackArchiveList(ctx web.Context) error {
|
||||
args := &model.ArchiveListArgs{}
|
||||
func stackList(ctx web.Context) error {
|
||||
args := &model.StackListArgs{}
|
||||
err := ctx.Bind(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args.PageSize = model.PageSize
|
||||
if args.PageIndex == 0 {
|
||||
args.PageIndex = 1
|
||||
}
|
||||
|
||||
archives, totalCount, err := biz.Archive.List(args)
|
||||
stacks, err := biz.Stack.List(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := newPagerModel(ctx, totalCount, model.PageSize, args.PageIndex).
|
||||
m := newModel(ctx).Set("Stacks", stacks).
|
||||
Set("Name", args.Name).
|
||||
Set("Archives", archives)
|
||||
return ctx.Render("stack/archive/list", m)
|
||||
Set("Filter", args.Filter)
|
||||
return ctx.Render("stack/list", m)
|
||||
}
|
||||
|
||||
func stackArchiveDetail(ctx web.Context) error {
|
||||
id := ctx.P("id")
|
||||
archive, err := biz.Archive.Get(id)
|
||||
func stackNew(ctx web.Context) error {
|
||||
m := newModel(ctx)
|
||||
return ctx.Render("stack/new", m)
|
||||
}
|
||||
|
||||
func stackCreate(ctx web.Context) error {
|
||||
stack := &model.Stack{}
|
||||
err := ctx.Bind(stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if archive == nil {
|
||||
return web.ErrNotFound
|
||||
}
|
||||
|
||||
m := newModel(ctx).Set("Archive", archive)
|
||||
return ctx.Render("stack/archive/detail", m)
|
||||
}
|
||||
|
||||
func stackArchiveEdit(ctx web.Context) error {
|
||||
id := ctx.P("id")
|
||||
archive, err := biz.Archive.Get(id)
|
||||
// Validate format
|
||||
_, err = compose.Parse(stack.Name, stack.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if archive == nil {
|
||||
|
||||
stack.CreatedBy = ctx.User().ID()
|
||||
stack.UpdatedBy = stack.CreatedBy
|
||||
err = biz.Stack.Create(stack, ctx.User())
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
|
||||
func stackDetail(ctx web.Context) error {
|
||||
name := ctx.P("name")
|
||||
stack, err := biz.Stack.Get(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stack == nil {
|
||||
return web.ErrNotFound
|
||||
}
|
||||
|
||||
m := newModel(ctx).Set("Archive", archive)
|
||||
return ctx.Render("stack/archive/edit", m)
|
||||
m := newModel(ctx).Set("Stack", stack)
|
||||
return ctx.Render("stack/detail", m)
|
||||
}
|
||||
|
||||
func stackArchiveUpdate(ctx web.Context) error {
|
||||
archive := &model.Archive{}
|
||||
err := ctx.Bind(archive)
|
||||
func stackEdit(ctx web.Context) error {
|
||||
name := ctx.P("name")
|
||||
stack, err := biz.Stack.Get(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stack == nil {
|
||||
return web.ErrNotFound
|
||||
}
|
||||
|
||||
m := newModel(ctx).Set("Stack", stack)
|
||||
return ctx.Render("stack/edit", m)
|
||||
}
|
||||
|
||||
func stackUpdate(ctx web.Context) error {
|
||||
stack := &model.Stack{}
|
||||
err := ctx.Bind(stack)
|
||||
if err == nil {
|
||||
// Validate format
|
||||
_, err = compose.Parse(archive.Name, archive.Content)
|
||||
_, err = compose.Parse(stack.Name, stack.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
archive.UpdatedBy = ctx.User().ID()
|
||||
err = biz.Archive.Update(archive)
|
||||
}
|
||||
if err == nil {
|
||||
biz.Event.CreateStackArchive(model.EventActionUpdate, archive.ID, archive.Name, ctx.User())
|
||||
stack.UpdatedBy = ctx.User().ID()
|
||||
err = biz.Stack.Update(stack, ctx.User())
|
||||
}
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
|
||||
func stackArchiveDelete(ctx web.Context) error {
|
||||
id := ctx.F("id")
|
||||
err := biz.Archive.Delete(id, ctx.User())
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
|
||||
func stackArchiveDeploy(ctx web.Context) error {
|
||||
id := ctx.F("id")
|
||||
archive, err := biz.Archive.Get(id)
|
||||
func stackDeploy(ctx web.Context) error {
|
||||
name := ctx.P("name")
|
||||
stack, err := biz.Stack.Get(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := compose.Parse(archive.Name, archive.Content)
|
||||
cfg, err := compose.Parse(stack.Name, stack.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -161,30 +151,27 @@ func stackArchiveDeploy(ctx web.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = docker.StackDeploy(archive.Name, archive.Content, authes)
|
||||
err = docker.StackDeploy(stack.Name, stack.Content, authes)
|
||||
if err == nil {
|
||||
biz.Event.CreateStack(model.EventActionDeploy, name, ctx.User())
|
||||
}
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
|
||||
func stackArchiveNew(ctx web.Context) error {
|
||||
m := newModel(ctx)
|
||||
return ctx.Render("stack/archive/new", m)
|
||||
}
|
||||
|
||||
func stackArchiveCreate(ctx web.Context) error {
|
||||
archive := &model.Archive{}
|
||||
err := ctx.Bind(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
func stackShutdown(ctx web.Context) error {
|
||||
name := ctx.P("name")
|
||||
err := docker.StackRemove(name)
|
||||
if err == nil {
|
||||
biz.Event.CreateStack(model.EventActionShutdown, name, ctx.User())
|
||||
}
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
|
||||
func stackDelete(ctx web.Context) error {
|
||||
name := ctx.P("name")
|
||||
err := docker.StackRemove(name)
|
||||
if err == nil {
|
||||
err = biz.Stack.Delete(name, ctx.User())
|
||||
}
|
||||
|
||||
// Validate format
|
||||
_, err = compose.Parse(archive.Name, archive.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
archive.CreatedBy = ctx.User().ID()
|
||||
archive.UpdatedBy = archive.CreatedBy
|
||||
err = biz.Archive.Create(archive)
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
|
@ -47,6 +47,14 @@ type Interface interface {
|
||||
ArchiveUpdate(archive *model.Archive) error
|
||||
ArchiveDelete(id string) error
|
||||
|
||||
StackList() (stacks []*model.Stack, err error)
|
||||
StackGet(name string) (*model.Stack, error)
|
||||
StackCreate(stack *model.Stack) error
|
||||
StackUpdate(stack *model.Stack) error
|
||||
StackDelete(name string) error
|
||||
// StackMigrate migrates stacks from old archive collection. This method will removed after v0.8.
|
||||
StackMigrate()
|
||||
|
||||
TemplateList(args *model.TemplateListArgs) (tpls []*model.Template, count int, err error)
|
||||
TemplateGet(id string) (*model.Template, error)
|
||||
TemplateCreate(tpl *model.Template) error
|
||||
|
@ -23,9 +23,6 @@ var (
|
||||
"session": {
|
||||
mgo.Index{Key: []string{"token"}, Unique: true},
|
||||
},
|
||||
"archive": {
|
||||
mgo.Index{Key: []string{"name"}, Unique: true},
|
||||
},
|
||||
"event": {
|
||||
mgo.Index{Key: []string{"type"}},
|
||||
mgo.Index{Key: []string{"name"}},
|
||||
|
@ -3,6 +3,8 @@ package mongo
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/app"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/cuigh/swirl/model"
|
||||
"github.com/globalsign/mgo"
|
||||
@ -73,3 +75,107 @@ func (d *Dao) ArchiveDelete(id string) (err error) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
//===============================
|
||||
|
||||
func (d *Dao) StackList() (stacks []*model.Stack, err error) {
|
||||
d.do(func(db *database) {
|
||||
stacks = []*model.Stack{}
|
||||
err = db.C("stack").Find(nil).All(&stacks)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) StackCreate(stack *model.Stack) (err error) {
|
||||
stack.CreatedAt = time.Now()
|
||||
stack.UpdatedAt = stack.CreatedAt
|
||||
|
||||
d.do(func(db *database) {
|
||||
err = db.C("stack").Insert(stack)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) StackGet(name string) (stack *model.Stack, err error) {
|
||||
d.do(func(db *database) {
|
||||
stack = &model.Stack{}
|
||||
err = db.C("stack").FindId(name).One(stack)
|
||||
if err == mgo.ErrNotFound {
|
||||
stack, err = nil, nil
|
||||
} else if err != nil {
|
||||
stack = nil
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) StackUpdate(stack *model.Stack) (err error) {
|
||||
d.do(func(db *database) {
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"content": stack.Content,
|
||||
"updated_by": stack.UpdatedBy,
|
||||
"updated_at": time.Now(),
|
||||
},
|
||||
}
|
||||
err = db.C("stack").UpdateId(stack.Name, update)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) StackDelete(name string) (err error) {
|
||||
d.do(func(db *database) {
|
||||
err = db.C("stack").RemoveId(name)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// StackMigrate migrates stacks from old archive collection.
|
||||
func (d *Dao) StackMigrate() {
|
||||
d.do(func(db *database) {
|
||||
logger := log.Get(app.Name)
|
||||
archiveColl := db.C("archive")
|
||||
|
||||
// check collection is exists.
|
||||
if _, err := archiveColl.Indexes(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
archives := make([]*model.Archive, 0)
|
||||
err := archiveColl.Find(nil).All(&archives)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to migrate archives: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
var errs []error
|
||||
stackColl := db.C("stack")
|
||||
for _, archive := range archives {
|
||||
stack := &model.Stack{
|
||||
Name: archive.Name,
|
||||
Content: archive.Content,
|
||||
CreatedBy: archive.CreatedBy,
|
||||
CreatedAt: archive.CreatedAt,
|
||||
UpdatedBy: archive.UpdatedBy,
|
||||
UpdatedAt: archive.UpdatedAt,
|
||||
}
|
||||
err = stackColl.Insert(stack)
|
||||
if err == nil || mgo.IsDup(err) {
|
||||
archiveColl.RemoveId(archive.ID)
|
||||
} else {
|
||||
logger.Warnf("Failed to migrate archive '%s': %v", archive.Name, err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// drop archive collection
|
||||
if len(errs) == 0 {
|
||||
err = archiveColl.DropCollection()
|
||||
if err != nil {
|
||||
logger.Warn("Failed to drop archive collection: ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
2
main.go
2
main.go
@ -43,6 +43,8 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
biz.Stack.Migrate()
|
||||
|
||||
scaler.Start()
|
||||
app.Run(server(setting))
|
||||
}
|
||||
|
@ -91,11 +91,6 @@ func (opts Options) Compress() Options {
|
||||
return opts
|
||||
}
|
||||
|
||||
type StackListInfo struct {
|
||||
Name string
|
||||
Services []string
|
||||
}
|
||||
|
||||
type ServiceListInfo struct {
|
||||
Name string
|
||||
Image string
|
||||
|
@ -13,17 +13,14 @@ const (
|
||||
EventTypeNetwork EventType = "Network"
|
||||
EventTypeService EventType = "Service"
|
||||
EventTypeServiceTemplate EventType = "Service Template"
|
||||
EventTypeStackTask EventType = "Stack Task"
|
||||
EventTypeStackArchive EventType = "Stack Archive"
|
||||
EventTypeStack EventType = "Stack"
|
||||
EventTypeSecret EventType = "Secret"
|
||||
EventTypeConfig EventType = "Config"
|
||||
|
||||
EventTypeVolume EventType = "Volume"
|
||||
|
||||
EventTypeAuthentication EventType = "Authentication"
|
||||
EventTypeRole EventType = "Role"
|
||||
EventTypeUser EventType = "User"
|
||||
EventTypeSetting EventType = "Setting"
|
||||
EventTypeVolume EventType = "Volume"
|
||||
EventTypeAuthentication EventType = "Authentication"
|
||||
EventTypeRole EventType = "Role"
|
||||
EventTypeUser EventType = "User"
|
||||
EventTypeSetting EventType = "Setting"
|
||||
)
|
||||
|
||||
type EventAction string
|
||||
@ -38,6 +35,8 @@ const (
|
||||
EventActionRollback EventAction = "Rollback"
|
||||
EventActionRestart EventAction = "Restart"
|
||||
EventActionDisconnect EventAction = "Disconnect"
|
||||
EventActionDeploy EventAction = "Deploy"
|
||||
EventActionShutdown EventAction = "Shutdown"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
@ -61,8 +60,8 @@ func (e *Event) URL(et EventType, code string) string {
|
||||
return fmt.Sprintf("/network/%s/detail", code)
|
||||
case EventTypeService:
|
||||
return fmt.Sprintf("/service/%s/detail", code)
|
||||
case EventTypeStackArchive:
|
||||
return fmt.Sprintf("/stack/archive/%s/detail", code)
|
||||
case EventTypeStack:
|
||||
return fmt.Sprintf("/stack/%s/detail", code)
|
||||
case EventTypeVolume:
|
||||
return fmt.Sprintf("/volume/%s/detail", code)
|
||||
case EventTypeRole:
|
||||
|
@ -18,6 +18,22 @@ type ArchiveListArgs struct {
|
||||
PageSize int `bind:"size"`
|
||||
}
|
||||
|
||||
type Stack struct {
|
||||
Name string `bson:"_id" json:"name,omitempty"`
|
||||
Content string `bson:"content" json:"content,omitempty" bind:"content=form,content=file"`
|
||||
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"`
|
||||
Services []string `bson:"-" json:"services,omitempty"`
|
||||
Internal bool `bson:"-" json:"internal"`
|
||||
}
|
||||
|
||||
type StackListArgs struct {
|
||||
Name string `bind:"name"`
|
||||
Filter string `bind:"filter"`
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
ID string `bson:"_id" json:"id,omitempty"`
|
||||
Name string `bson:"name" json:"name,omitempty"`
|
||||
|
@ -109,16 +109,15 @@ var Perms = []PermGroup{
|
||||
{
|
||||
Name: "Stack",
|
||||
Perms: []Perm{
|
||||
{Key: "stack.task.list", Text: "View task list"},
|
||||
{Key: "stack.task.delete", Text: "Delete task"},
|
||||
{Key: "stack.archive.list", Text: "View archive list"},
|
||||
{Key: "stack.archive.new", Text: "View archive new"},
|
||||
{Key: "stack.archive.detail", Text: "View archive detail"},
|
||||
{Key: "stack.archive.edit", Text: "View archive edit"},
|
||||
{Key: "stack.archive.delete", Text: "Delete archive"},
|
||||
{Key: "stack.archive.create", Text: "Create archive"},
|
||||
{Key: "stack.archive.update", Text: "Update archive"},
|
||||
{Key: "stack.archive.deploy", Text: "Deploy archive"},
|
||||
{Key: "stack.list", Text: "View list"},
|
||||
{Key: "stack.new", Text: "View new"},
|
||||
{Key: "stack.detail", Text: "View detail"},
|
||||
{Key: "stack.edit", Text: "View edit"},
|
||||
{Key: "stack.create", Text: "Create"},
|
||||
{Key: "stack.update", Text: "Update"},
|
||||
{Key: "stack.deploy", Text: "Deploy"},
|
||||
{Key: "stack.shutdown", Text: "Shutdown"},
|
||||
{Key: "stack.delete", Text: "Delete"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -43,7 +43,7 @@
|
||||
<a class="navbar-item" href="/network/">{{ i18n("menu.network") }}</a>
|
||||
<a class="navbar-item" href="/service/">{{ i18n("menu.service") }}</a>
|
||||
<a class="navbar-item" href="/task/">{{ i18n("menu.task") }}</a>
|
||||
<a class="navbar-item" href="/stack/task/">{{ i18n("menu.stack") }}</a>
|
||||
<a class="navbar-item" href="/stack/">{{ i18n("menu.stack") }}</a>
|
||||
<a class="navbar-item" href="/secret/">{{ i18n("menu.secret") }}</a>
|
||||
<a class="navbar-item" href="/config/">{{ i18n("menu.config") }}</a>
|
||||
</div>
|
||||
|
@ -1,88 +0,0 @@
|
||||
{{ extends "../../_layouts/default" }}
|
||||
{{ import "../../_modules/pager" }}
|
||||
|
||||
{{ block script() }}
|
||||
<script>$(() => new Swirl.Stack.Archive.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 is-uppercase">{{ i18n("stack.title") }}</h1>
|
||||
<h2 class="subtitle is-5">{{ i18n("stack.description") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-foot">
|
||||
<div class="container">
|
||||
<nav class="tabs is-boxed">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/stack/task/">{{ i18n("menu.stack.task") }}</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="/stack/archive/">{{ i18n("menu.stack.archive") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<nav class="level">
|
||||
<!-- Left side -->
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<form>
|
||||
<div class="field has-addons">
|
||||
<p class="control">
|
||||
<input name="name" value="{{.Name}}" class="input" type="text" placeholder="Search by name">
|
||||
</p>
|
||||
<p class="control">
|
||||
<button type="submit" class="button is-primary">{{ i18n("button.search") }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="level-item">
|
||||
<p class="subtitle is-5">
|
||||
<strong>{{len(.Archives)}}</strong>
|
||||
<span class="is-lowercase">{{ i18n("menu.stack.archive") }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right side -->
|
||||
<div class="level-right">
|
||||
<p class="level-item">
|
||||
<a href="new" class="button is-success"><span class="icon"><i class="fas fa-plus"></i></span><span>{{ i18n("button.new") }}</span></a>
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<table id="table-items" class="table is-bordered is-striped is-narrow is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ i18n("field.name") }}</th>
|
||||
<th>{{ i18n("field.created-at") }}</th>
|
||||
<th>{{ i18n("field.updated-at") }}</th>
|
||||
<th>{{ i18n("field.action") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Archives}}
|
||||
<tr data-id="{{.ID}}">
|
||||
<td><a href="{{.ID}}/detail">{{.Name}}</a></td>
|
||||
<td>{{time(.CreatedAt)}}</td>
|
||||
<td>{{time(.UpdatedAt)}}</td>
|
||||
<td>
|
||||
<a href="{{.ID}}/edit" class="button is-small is-dark is-outlined">{{ i18n("button.edit") }}</a>
|
||||
<button type="button" class="button is-small is-info is-outlined" data-action="deploy-archive">{{ i18n("stack.button.deploy") }}</button>
|
||||
<button type="button" class="button is-small is-danger is-outlined" data-action="delete-archive">{{ i18n("button.delete") }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ yield pager(info=.Pager) }}
|
||||
</section>
|
||||
{{ end }}
|
@ -1,4 +1,4 @@
|
||||
{{ extends "../../_layouts/default" }}
|
||||
{{ extends "../_layouts/default" }}
|
||||
|
||||
{{ block style() }}
|
||||
<link rel="stylesheet" href="/assets/highlight/highlight.css?v=9.12">
|
||||
@ -17,27 +17,13 @@
|
||||
<h2 class="subtitle is-5">{{ i18n("stack.description") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-foot">
|
||||
<div class="container">
|
||||
<nav class="tabs is-boxed">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/stack/task/">{{ i18n("menu.stack.task") }}</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="/stack/archive/">{{ i18n("menu.stack.archive") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container">
|
||||
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="/">{{ i18n("menu.home") }}</a></li>
|
||||
<li><a href="/stack/archive/">{{ i18n("menu.stack.archive") }}</a></li>
|
||||
<li><a href="/stack/">{{ i18n("menu.stack") }}</a></li>
|
||||
<li class="is-active"><a>{{ i18n("menu.detail") }}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
@ -46,7 +32,7 @@
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h2 class="title is-2">
|
||||
{{ .Archive.Name }}
|
||||
{{ .Stack.Name }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
@ -55,8 +41,8 @@
|
||||
<nav class="navbar has-shadow">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item is-tab is-active" href="/stack/archive/{{.Archive.ID}}/detail">{{ i18n("menu.detail") }}</a>
|
||||
<a class="navbar-item is-tab" href="/stack/archive/{{.Archive.ID}}/edit">{{ i18n("menu.edit") }}</a>
|
||||
<a class="navbar-item is-tab is-active" href="/stack/{{.Stack.Name}}/detail">{{ i18n("menu.detail") }}</a>
|
||||
<a class="navbar-item is-tab" href="/stack/{{.Stack.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@ -65,13 +51,13 @@
|
||||
<div class="container">
|
||||
<dl>
|
||||
<dt>{{ i18n("field.created-at") }}</dt>
|
||||
<dd>{{ time(.Archive.CreatedAt) }}</dd>
|
||||
<dd>{{ time(.Stack.CreatedAt) }}</dd>
|
||||
<dt>{{ i18n("field.updated-at") }}</dt>
|
||||
<dd>{{ time(.Archive.UpdatedAt) }}</dd>
|
||||
<dd>{{ time(.Stack.UpdatedAt) }}</dd>
|
||||
<dt>Content</dt>
|
||||
<dd class="content"><pre class="is-paddingless"><code class="yaml">{{ .Archive.Content }}</code></pre></dd>
|
||||
<dd class="content"><pre class="is-paddingless"><code class="yaml">{{ .Stack.Content }}</code></pre></dd>
|
||||
</dl>
|
||||
<a href="/stack/archive/" class="button is-primary">
|
||||
<a href="/stack/" class="button is-primary">
|
||||
<span class="icon"><i class="fas fa-reply"></i></span>
|
||||
<span>{{ i18n("button.return") }}</span>
|
||||
</a>
|
@ -1,5 +1,5 @@
|
||||
{{ extends "../../_layouts/default" }}
|
||||
{{ import "../../_modules/form" }}
|
||||
{{ extends "../_layouts/default" }}
|
||||
{{ import "../_modules/form" }}
|
||||
|
||||
{{ block style() }}
|
||||
<link rel="stylesheet" href="/assets/codemirror/codemirror.css?v=5.30">
|
||||
@ -8,7 +8,7 @@
|
||||
{{ block script() }}
|
||||
<script src="/assets/codemirror/codemirror.js?v=5.30"></script>
|
||||
<script src="/assets/codemirror/mode/yaml.js?v=5.30"></script>
|
||||
<script>$(() => new Swirl.Stack.Archive.EditPage())</script>
|
||||
<script>$(() => new Swirl.Stack.EditPage())</script>
|
||||
{{ end }}
|
||||
|
||||
{{ block body() }}
|
||||
@ -19,27 +19,13 @@
|
||||
<h2 class="subtitle is-5">{{ i18n("stack.description") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-foot">
|
||||
<div class="container">
|
||||
<nav class="tabs is-boxed">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/stack/task/">{{ i18n("menu.stack.task") }}</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="/stack/archive/">{{ i18n("menu.stack.archive") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container">
|
||||
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="/">{{ i18n("menu.home") }}</a></li>
|
||||
<li><a href="/stack/archive/">{{ i18n("menu.stack.archive") }}</a></li>
|
||||
<li><a href="/stack/">{{ i18n("menu.stack") }}</a></li>
|
||||
<li class="is-active"><a>{{ i18n("menu.edit") }}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
@ -49,7 +35,7 @@
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h2 class="title is-2">
|
||||
{{ .Archive.Name }}
|
||||
{{ .Stack.Name }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
@ -58,8 +44,8 @@
|
||||
<nav class="navbar has-shadow">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item is-tab" href="/stack/archive/{{.Archive.ID}}/detail">{{ i18n("menu.detail") }}</a>
|
||||
<a class="navbar-item is-tab is-active" href="/stack/archive/{{.Archive.ID}}/edit">{{ i18n("menu.edit") }}</a>
|
||||
<a class="navbar-item is-tab" href="/stack/{{.Stack.Name}}/detail">{{ i18n("menu.detail") }}</a>
|
||||
<a class="navbar-item is-tab is-active" href="/stack/{{.Stack.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@ -69,7 +55,7 @@
|
||||
<div class="field">
|
||||
<label class="label">{{ i18n("field.name") }}</label>
|
||||
<div class="control">
|
||||
<input id="name" name="name" class="input" value="{{ .Archive.Name }}" type="text" placeholder="" data-v-rule="native;regex" data-v-arg-regex="^[a-z0-9_-]+$" data-v-msg-regex="Name can contain only letters, digits, '_' and '-'." required>
|
||||
<input id="name" name="name" class="input" value="{{ .Stack.Name }}" type="text" placeholder="" data-v-rule="native;regex" data-v-arg-regex="^[a-z0-9_-]+$" data-v-msg-regex="Name can contain only letters, digits, '_' and '-'." required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
@ -81,7 +67,7 @@
|
||||
<div id="div-input" class="field">
|
||||
<label class="label">Content</label>
|
||||
<div class="control">
|
||||
<textarea id="txt-content" name="content" class="textarea" rows="20" placeholder="Compose file content" data-v-rule="content" data-v-arg-content="input" required>{{ .Archive.Content }}</textarea>
|
||||
<textarea id="txt-content" name="content" class="textarea" rows="20" placeholder="Compose file content" data-v-rule="content" data-v-arg-content="input" required>{{ .Stack.Content }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="div-upload" class="field" style="display: none">
|
||||
@ -104,7 +90,7 @@
|
||||
<button id="btn-submit" type="submit" class="button is-primary" data-url="update">{{ i18n("button.submit") }}</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="/stack/archive/" class="button is-link">{{ i18n("button.cancel") }}</a>
|
||||
<a href="/stack/" class="button is-link">{{ i18n("button.cancel") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
114
views/stack/list.jet
Normal file
114
views/stack/list.jet
Normal file
@ -0,0 +1,114 @@
|
||||
{{ extends "../_layouts/default" }}
|
||||
|
||||
{{ block script() }}
|
||||
<script>$(() => new Swirl.Stack.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 is-uppercase">{{ i18n("stack.title") }}</h1>
|
||||
<h2 class="subtitle is-5">{{ i18n("stack.description") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<nav class="level">
|
||||
<!-- Left side -->
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<form>
|
||||
<div class="field has-addons">
|
||||
<p class="control">
|
||||
<input name="name" value="{{.Name}}" class="input" type="text" placeholder="Search by name">
|
||||
</p>
|
||||
<p class="control">
|
||||
<button type="submit" class="button is-primary">{{ i18n("button.search") }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="level-item">
|
||||
<p class="subtitle is-5">
|
||||
<strong>{{len(.Stacks)}}</strong>
|
||||
<span class="is-lowercase">{{ i18n("menu.stack") }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right side -->
|
||||
<div class="level-right">
|
||||
<p class="level-item">
|
||||
{{if .Filter == ""}}
|
||||
<strong>All</strong>
|
||||
{{else}}
|
||||
<a href="/stack/">All</a>
|
||||
{{end}}
|
||||
</p>
|
||||
<p class="level-item">
|
||||
{{if .Filter == "up"}}
|
||||
<strong>Up</strong>
|
||||
{{else}}
|
||||
<a href="?filter=up">Up</a>
|
||||
{{end}}
|
||||
</p>
|
||||
<p class="level-item">
|
||||
{{if .Filter == "internal"}}
|
||||
<strong>Internal</strong>
|
||||
{{else}}
|
||||
<a href="?filter=internal">Internal</a>
|
||||
{{end}}
|
||||
</p>
|
||||
<p class="level-item">
|
||||
{{if .Filter == "external"}}
|
||||
<strong>External</strong>
|
||||
{{else}}
|
||||
<a href="?filter=external">External</a>
|
||||
{{end}}
|
||||
</p>
|
||||
<p class="level-item">
|
||||
<a href="new" class="button is-success"><span class="icon"><i class="fas fa-plus"></i></span><span>{{ i18n("button.new") }}</span></a>
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
<table id="table-items" class="table is-bordered is-striped is-narrow is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ i18n("field.name") }}</th>
|
||||
<th>Services</th>
|
||||
<th>{{ i18n("field.created-at") }}</th>
|
||||
<th>{{ i18n("field.updated-at") }}</th>
|
||||
<th>{{ i18n("field.action") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Stacks}}
|
||||
<tr>
|
||||
<td><a href="{{.Name}}/detail">{{.Name}}</a>{{ if !.Internal }}<span class="icon has-text-danger tooltip is-tooltip-right" data-tooltip="External stack, can't be edited by Swirl"><i class="fas fa-exclamation-circle"></i></span>{{ end }}</td>
|
||||
<td>
|
||||
<div class="tags">
|
||||
{{range .Services}}
|
||||
<a href="/service/{{.}}/detail" class="tag is-success">{{.}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{time(.CreatedAt)}}</td>
|
||||
<td>{{time(.UpdatedAt)}}</td>
|
||||
<td>
|
||||
{{ if .Internal }}
|
||||
<a href="{{ .Name }}/edit" class="button is-small is-dark is-outlined">{{ i18n("button.edit") }}</a>
|
||||
<button class="button is-small is-info is-outlined" data-action="deploy-stack">{{ i18n("stack.button.deploy") }}</button>
|
||||
{{ end }}
|
||||
{{ if .Services }}
|
||||
<button class="button is-small is-danger is-outlined" data-action="shutdown-stack">{{ i18n("stack.button.shutdown") }}</button>
|
||||
{{ end }}
|
||||
{{ if .Internal }}
|
||||
<button class="button is-small is-danger is-outlined" data-action="delete-stack">{{ i18n("button.delete") }}</button>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{{ end }}
|
@ -1,5 +1,5 @@
|
||||
{{ extends "../../_layouts/default" }}
|
||||
{{ import "../../_modules/form" }}
|
||||
{{ extends "../_layouts/default" }}
|
||||
{{ import "../_modules/form" }}
|
||||
|
||||
{{ block style() }}
|
||||
<link rel="stylesheet" href="/assets/codemirror/codemirror.css?v=5.30">
|
||||
@ -8,7 +8,7 @@
|
||||
{{ block script() }}
|
||||
<script src="/assets/codemirror/codemirror.js?v=5.30"></script>
|
||||
<script src="/assets/codemirror/mode/yaml.js?v=5.30"></script>
|
||||
<script>$(() => new Swirl.Stack.Archive.EditPage())</script>
|
||||
<script>$(() => new Swirl.Stack.EditPage())</script>
|
||||
{{ end }}
|
||||
|
||||
{{ block body() }}
|
||||
@ -19,20 +19,6 @@
|
||||
<h2 class="subtitle is-5">{{ i18n("stack.description") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-foot">
|
||||
<div class="container">
|
||||
<nav class="tabs is-boxed">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/stack/task/">{{ i18n("menu.stack.task") }}</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="/stack/archive/">{{ i18n("menu.stack.archive") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<div id="div-form" class="container">
|
||||
@ -76,7 +62,7 @@
|
||||
<button id="btn-submit" type="submit" class="button is-primary">{{ i18n("button.submit") }}</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="/stack/archive/" class="button is-link">{{ i18n("button.cancel") }}</a>
|
||||
<a href="/stack/" class="button is-link">{{ i18n("button.cancel") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,69 +0,0 @@
|
||||
{{ extends "../../_layouts/default" }}
|
||||
|
||||
{{ block script() }}
|
||||
<script>$(() => new Swirl.Stack.Task.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 is-uppercase">{{ i18n("stack.title") }}</h1>
|
||||
<h2 class="subtitle is-5">{{ i18n("stack.description") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-foot">
|
||||
<div class="container">
|
||||
<nav class="tabs is-boxed">
|
||||
<ul>
|
||||
<li class="is-active">
|
||||
<a href="/stack/task/">{{ i18n("menu.stack.task") }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/stack/archive/">{{ i18n("menu.stack.archive") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<nav class="level">
|
||||
<!-- Left side -->
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<p class="subtitle is-5">
|
||||
<strong>{{len(.Stacks)}}</strong>
|
||||
<span class="is-lowercase">{{ i18n("menu.stack") }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<table id="table-items" class="table is-bordered is-striped is-narrow is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ i18n("field.name") }}</th>
|
||||
<th>Services</th>
|
||||
<th>{{ i18n("field.action") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Stacks}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>
|
||||
<div class="tags">
|
||||
{{range .Services}}
|
||||
<a href="/service/{{.}}/detail" class="tag is-success">{{.}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="button is-small is-danger is-outlined" data-action="delete-stack">{{ i18n("button.delete") }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{{ end }}
|
Loading…
Reference in New Issue
Block a user