Support add stack archive by uploading file

This commit is contained in:
cuigh 2017-11-14 16:05:46 +08:00
parent 3005cd6edb
commit 0c2dae4834
11 changed files with 259 additions and 81 deletions

View File

@ -338,6 +338,7 @@ var Swirl;
switch (this.options.encoder) { switch (this.options.encoder) {
case "none": case "none":
settings.contentType = false; settings.contentType = false;
settings.processData = false;
break; break;
case "json": case "json":
settings.contentType = "application/json; charset=UTF-8"; settings.contentType = "application/json; charset=UTF-8";
@ -438,7 +439,7 @@ var Swirl;
} }
class WidthRule extends LengthRule { class WidthRule extends LengthRule {
getLength(value) { getLength(value) {
var doubleByteChars = value.match(/[^\x00-\xff]/ig); let doubleByteChars = value.match(/[^\x00-\xff]/ig);
return value.length + (doubleByteChars == null ? 0 : doubleByteChars.length); return value.length + (doubleByteChars == null ? 0 : doubleByteChars.length);
} }
} }
@ -1865,6 +1866,71 @@ var Swirl;
})(Setting = Swirl.Setting || (Swirl.Setting = {})); })(Setting = Swirl.Setting || (Swirl.Setting = {}));
})(Swirl || (Swirl = {})); })(Swirl || (Swirl = {}));
var Swirl; var Swirl;
(function (Swirl) {
var Stack;
(function (Stack) {
var Archive;
(function (Archive) {
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/archive/";
}
else {
Notification.show("danger", `FAILED: ${r.message}`);
}
});
}
}
Archive.EditPage = EditPage;
})(Archive = Stack.Archive || (Stack.Archive = {}));
})(Stack = Swirl.Stack || (Swirl.Stack = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) { (function (Swirl) {
var Stack; var Stack;
(function (Stack) { (function (Stack) {

File diff suppressed because one or more lines are too long

View File

@ -314,6 +314,7 @@ namespace Swirl.Core {
switch (this.options.encoder) { switch (this.options.encoder) {
case "none": case "none":
settings.contentType = false; settings.contentType = false;
settings.processData = false;
break; break;
case "json": case "json":
settings.contentType = "application/json; charset=UTF-8"; settings.contentType = "application/json; charset=UTF-8";

View File

@ -8,7 +8,7 @@ namespace Swirl.Core {
type InputElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement; type InputElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement;
/** /**
* * The result of validation.
* *
* @interface ValidationResult * @interface ValidationResult
*/ */
@ -32,7 +32,7 @@ namespace Swirl.Core {
} }
/** /**
* HTML5 * HTML5 form element native validator
* *
* @class NativeRule * @class NativeRule
* @implements {ValidationRule} * @implements {ValidationRule}
@ -45,7 +45,7 @@ namespace Swirl.Core {
} }
/** /**
* * Required validator
* *
* @class RequiredRule * @class RequiredRule
* @implements {ValidationRule} * @implements {ValidationRule}
@ -57,7 +57,7 @@ namespace Swirl.Core {
} }
/** /**
* ( radio checkbox), 示例: checked, checked(2), checked(1~2) * Checked validator(for radio/checkbox), e.g. checked, checked(2), checked(1~2)
* *
* @class CheckedRule * @class CheckedRule
* @implements {ValidationRule} * @implements {ValidationRule}
@ -71,7 +71,7 @@ namespace Swirl.Core {
} }
/** /**
* * Email validator
* *
* @class EmailValidator * @class EmailValidator
* @implements {ValidationRule} * @implements {ValidationRule}
@ -85,7 +85,7 @@ namespace Swirl.Core {
} }
/** /**
* HTTP/FTP * HTTP/FTP URL validator
* *
* @class UrlValidator * @class UrlValidator
* @implements {ValidationRule} * @implements {ValidationRule}
@ -99,7 +99,7 @@ namespace Swirl.Core {
} }
/** /**
* IPV4 * IPV4 address validator
* *
* @class IPValidator * @class IPValidator
* @implements {ValidationRule} * @implements {ValidationRule}
@ -113,7 +113,7 @@ namespace Swirl.Core {
} }
/** /**
* () * Match validator(e.g. password confirmation)
* *
* @class MatchValidator * @class MatchValidator
* @implements {ValidationRule} * @implements {ValidationRule}
@ -125,7 +125,7 @@ namespace Swirl.Core {
} }
/** /**
* * String length validator.
* *
* @class LengthValidator * @class LengthValidator
* @implements {ValidationRule} * @implements {ValidationRule}
@ -155,14 +155,14 @@ namespace Swirl.Core {
} }
/** /**
* (2) * String width validator, the width of CJK characters is considered as 2.
* *
* @class WidthValidator * @class WidthValidator
* @extends {LengthRule} * @extends {LengthRule}
*/ */
class WidthRule extends LengthRule { class WidthRule extends LengthRule {
protected getLength(value: string): number { protected getLength(value: string): number {
var doubleByteChars = value.match(/[^\x00-\xff]/ig); let doubleByteChars = value.match(/[^\x00-\xff]/ig);
return value.length + (doubleByteChars == null ? 0 : doubleByteChars.length); return value.length + (doubleByteChars == null ? 0 : doubleByteChars.length);
} }
} }
@ -181,7 +181,7 @@ namespace Swirl.Core {
} }
/** /**
* * Regex validator
* *
* @class RegexValidator * @class RegexValidator
* @implements {ValidationRule} * @implements {ValidationRule}
@ -195,7 +195,7 @@ namespace Swirl.Core {
} }
/** /**
* * Remote validator
* *
* @class RemoteRule * @class RemoteRule
* @implements {ValidationRule} * @implements {ValidationRule}

View File

@ -0,0 +1,70 @@
///<reference path="../../core/core.ts" />
namespace Swirl.Stack.Archive {
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/archive/"
} else {
Notification.show("danger", `FAILED: ${r.message}`);
}
})
}
}
}
declare var CodeMirror: any;

View File

@ -173,16 +173,18 @@ func stackArchiveNew(ctx web.Context) error {
func stackArchiveCreate(ctx web.Context) error { func stackArchiveCreate(ctx web.Context) error {
archive := &model.Archive{} archive := &model.Archive{}
err := ctx.Bind(archive) err := ctx.Bind(archive)
if err == nil { if err != nil {
// Validate format return err
_, 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)
} }
// 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) return ajaxResult(ctx, err)
} }

View File

@ -23,7 +23,7 @@ func main() {
misc.BindOptions() misc.BindOptions()
app.Name = "Swirl" app.Name = "Swirl"
app.Version = "0.6.1" app.Version = "0.6.2"
app.Desc = "A web management UI for Docker, focused on swarm cluster" app.Desc = "A web management UI for Docker, focused on swarm cluster"
app.Action = func(ctx *app.Context) { app.Action = func(ctx *app.Context) {
misc.LoadOptions() misc.LoadOptions()

View File

@ -3,9 +3,9 @@ package model
import "time" import "time"
type Archive struct { type Archive struct {
ID string `bson:"_id" json:"id,omitempty"` ID string `bson:"_id" json:"id,omitempty" bind:"id=path"`
Name string `bson:"name" json:"name,omitempty"` Name string `bson:"name" json:"name,omitempty"`
Content string `bson:"content" json:"content,omitempty"` Content string `bson:"content" json:"content,omitempty" bind:"content=form,content=file"`
CreatedBy string `bson:"created_by" json:"created_by,omitempty"` CreatedBy string `bson:"created_by" json:"created_by,omitempty"`
CreatedAt time.Time `bson:"created_at" json:"created_at,omitempty"` CreatedAt time.Time `bson:"created_at" json:"created_at,omitempty"`
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"` UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`

View File

@ -22,10 +22,10 @@
<nav class="tabs is-boxed"> <nav class="tabs is-boxed">
<ul> <ul>
<li> <li>
<a href="/stack/task/">Tasks</a> <a href="/stack/task/">{{ i18n("menu.stack.task") }}</a>
</li> </li>
<li class="is-active"> <li class="is-active">
<a href="/stack/archive/">Archives</a> <a href="/stack/archive/">{{ i18n("menu.stack.archive") }}</a>
</li> </li>
</ul> </ul>
</nav> </nav>
@ -36,9 +36,9 @@
<div class="container"> <div class="container">
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs"> <nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
<ul> <ul>
<li><a href="/">Dashboard</a></li> <li><a href="/">{{ i18n("menu.dashboard") }}</a></li>
<li><a href="/stack/archive/">Archives</a></li> <li><a href="/stack/archive/">{{ i18n("menu.stack.archive") }}</a></li>
<li class="is-active"><a>Detail</a></li> <li class="is-active"><a>{{ i18n("menu.detail") }}</a></li>
</ul> </ul>
</nav> </nav>
</div> </div>
@ -55,8 +55,8 @@
<nav class="navbar has-shadow"> <nav class="navbar has-shadow">
<div class="container"> <div class="container">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item is-tab is-active" href="/stack/archive/{{.Archive.ID}}/detail">Detail</a> <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">Edit</a> <a class="navbar-item is-tab" href="/stack/archive/{{.Archive.ID}}/edit">{{ i18n("menu.edit") }}</a>
</div> </div>
</div> </div>
</nav> </nav>
@ -64,16 +64,16 @@
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<dl> <dl>
<dt>Created at</dt> <dt>{{ i18n("field.created-at") }}</dt>
<dd>{{ time(.Archive.CreatedAt) }}</dd> <dd>{{ time(.Archive.CreatedAt) }}</dd>
<dt>Updated at</dt> <dt>{{ i18n("field.updated-at") }}</dt>
<dd>{{ time(.Archive.UpdatedAt) }}</dd> <dd>{{ time(.Archive.UpdatedAt) }}</dd>
<dt>Content</dt> <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">{{ .Archive.Content }}</code></pre></dd>
</dl> </dl>
<a href="/stack/archive/" class="button is-primary"> <a href="/stack/archive/" class="button is-primary">
<span class="icon"><i class="fa fa-reply"></i></span> <span class="icon"><i class="fa fa-reply"></i></span>
<span>Return</span> <span>{{ i18n("button.return") }}</span>
</a> </a>
</div> </div>
</section> </section>

View File

@ -1,4 +1,5 @@
{{ extends "../../_layouts/default" }} {{ extends "../../_layouts/default" }}
{{ import "../../_modules/form" }}
{{ block style() }} {{ block style() }}
<link rel="stylesheet" href="/assets/codemirror/codemirror.css?v=5.30"> <link rel="stylesheet" href="/assets/codemirror/codemirror.css?v=5.30">
@ -7,7 +8,7 @@
{{ block script() }} {{ block script() }}
<script src="/assets/codemirror/codemirror.js?v=5.30"></script> <script src="/assets/codemirror/codemirror.js?v=5.30"></script>
<script src="/assets/codemirror/mode/yaml.js?v=5.30"></script> <script src="/assets/codemirror/mode/yaml.js?v=5.30"></script>
<script>var editor = CodeMirror.fromTextArea(document.getElementById("txt-content"), {lineNumbers: true});</script> <script>$(() => new Swirl.Stack.Archive.EditPage())</script>
{{ end }} {{ end }}
{{ block body() }} {{ block body() }}
@ -64,30 +65,48 @@
</nav> </nav>
<section class="section"> <section class="section">
<div class="container"> <div id="div-form" class="container">
<form method="post" action="update" data-form="ajax-json" data-url="/stack/archive/"> <div class="field">
<input name="id" value="{{ .Archive.ID }}" type="hidden"> <label class="label">{{ i18n("field.name") }}</label>
<div class="field"> <div class="control">
<label class="label">{{ i18n("field.name") }}</label> <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>
<div class="control">
<input 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>
</div>
</div> </div>
<div class="field"> </div>
<label class="label">Content</label> <div class="field">
<div class="control"> <div class="control">
<textarea id="txt-content" name="content" class="textarea code" rows="20" placeholder="Compose file content" data-v-rule="native" required>{{ .Archive.Content }}</textarea> {{ yield radio(name="type", value="input", label="Input", checked="input") }}
</div> {{ yield radio(name="type", value="upload", label="Upload") }}
</div> </div>
<div class="field is-grouped"> </div>
<div class="control"> <div id="div-input" class="field">
<button type="submit" class="button is-primary">{{ i18n("button.submit") }}</button> <label class="label">Content</label>
</div> <div class="control">
<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>
<a href="/stack/archive/" class="button is-link">{{ i18n("button.cancel") }}</a>
</div>
</div> </div>
</form> </div>
<div id="div-upload" class="field" style="display: none">
<label class="label">Content</label>
<div class="file has-name is-fullwidth">
<label class="file-label">
<input id="file-content" name="content" class="file-input" type="file" data-v-rule="content" data-v-arg-content="upload" required>
<span class="file-cta">
<span class="file-icon">
<i class="fa fa-upload"></i>
</span>
<span class="file-label">Choose a file</span>
</span>
<span id="filename" class="file-name"></span>
</label>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<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>
</div>
</div>
</div> </div>
</section> </section>
{{ end }} {{ end }}

View File

@ -1,4 +1,5 @@
{{ extends "../../_layouts/default" }} {{ extends "../../_layouts/default" }}
{{ import "../../_modules/form" }}
{{ block style() }} {{ block style() }}
<link rel="stylesheet" href="/assets/codemirror/codemirror.css?v=5.30"> <link rel="stylesheet" href="/assets/codemirror/codemirror.css?v=5.30">
@ -7,7 +8,7 @@
{{ block script() }} {{ block script() }}
<script src="/assets/codemirror/codemirror.js?v=5.30"></script> <script src="/assets/codemirror/codemirror.js?v=5.30"></script>
<script src="/assets/codemirror/mode/yaml.js?v=5.30"></script> <script src="/assets/codemirror/mode/yaml.js?v=5.30"></script>
<script>var editor = CodeMirror.fromTextArea(document.getElementById("txt-content"), {lineNumbers: true});</script> <script>$(() => new Swirl.Stack.Archive.EditPage())</script>
{{ end }} {{ end }}
{{ block body() }} {{ block body() }}
@ -34,31 +35,50 @@
</div> </div>
</section> </section>
<section class="section"> <section class="section">
<div class="container"> <div id="div-form" class="container">
<h2 class="title">Create stack archive</h2> <h2 class="title">Create stack archive</h2>
<hr> <hr>
<form method="post" data-form="ajax-json" data-url="/stack/archive/"> <div class="field">
<div class="field"> <label class="label">{{ i18n("field.name") }}</label>
<label class="label">{{ i18n("field.name") }}</label> <div class="control">
<div class="control"> <input id="name" name="name" class="input" 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 name="name" class="input" 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>
<div class="field"> </div>
<label class="label">Content</label> <div class="field">
<div class="control"> <div class="control">
<textarea id="txt-content" name="content" class="textarea" rows="20" placeholder="Compose file content" data-v-rule="native" required></textarea> {{ yield radio(name="type", value="input", label="Input", checked="input") }}
</div> {{ yield radio(name="type", value="upload", label="Upload") }}
</div> </div>
<div class="field is-grouped"> </div>
<div class="control"> <div id="div-input" class="field">
<button type="submit" class="button is-primary">{{ i18n("button.submit") }}</button> <label class="label">Content</label>
</div> <div class="control">
<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></textarea>
<a href="/stack/archive/" class="button is-link">{{ i18n("button.cancel") }}</a>
</div>
</div> </div>
</form> </div>
<div id="div-upload" class="field" style="display: none">
<label class="label">Content</label>
<div class="file has-name is-fullwidth">
<label class="file-label">
<input id="file-content" name="content" class="file-input" type="file" data-v-rule="content" data-v-arg-content="upload" required>
<span class="file-cta">
<span class="file-icon">
<i class="fa fa-upload"></i>
</span>
<span class="file-label">Choose a file</span>
</span>
<span id="filename" class="file-name"></span>
</label>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<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>
</div>
</div>
</div> </div>
</section> </section>
{{ end }} {{ end }}