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) {
case "none":
settings.contentType = false;
settings.processData = false;
break;
case "json":
settings.contentType = "application/json; charset=UTF-8";
@ -438,7 +439,7 @@ var Swirl;
}
class WidthRule extends LengthRule {
getLength(value) {
var doubleByteChars = value.match(/[^\x00-\xff]/ig);
let doubleByteChars = value.match(/[^\x00-\xff]/ig);
return value.length + (doubleByteChars == null ? 0 : doubleByteChars.length);
}
}
@ -1865,6 +1866,71 @@ var Swirl;
})(Setting = Swirl.Setting || (Swirl.Setting = {}));
})(Swirl || (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) {
var 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) {
case "none":
settings.contentType = false;
settings.processData = false;
break;
case "json":
settings.contentType = "application/json; charset=UTF-8";

View File

@ -8,7 +8,7 @@ namespace Swirl.Core {
type InputElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement;
/**
*
* The result of validation.
*
* @interface ValidationResult
*/
@ -32,7 +32,7 @@ namespace Swirl.Core {
}
/**
* HTML5
* HTML5 form element native validator
*
* @class NativeRule
* @implements {ValidationRule}
@ -45,7 +45,7 @@ namespace Swirl.Core {
}
/**
*
* Required validator
*
* @class RequiredRule
* @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
* @implements {ValidationRule}
@ -71,7 +71,7 @@ namespace Swirl.Core {
}
/**
*
* Email validator
*
* @class EmailValidator
* @implements {ValidationRule}
@ -85,7 +85,7 @@ namespace Swirl.Core {
}
/**
* HTTP/FTP
* HTTP/FTP URL validator
*
* @class UrlValidator
* @implements {ValidationRule}
@ -99,7 +99,7 @@ namespace Swirl.Core {
}
/**
* IPV4
* IPV4 address validator
*
* @class IPValidator
* @implements {ValidationRule}
@ -113,7 +113,7 @@ namespace Swirl.Core {
}
/**
* ()
* Match validator(e.g. password confirmation)
*
* @class MatchValidator
* @implements {ValidationRule}
@ -125,7 +125,7 @@ namespace Swirl.Core {
}
/**
*
* String length validator.
*
* @class LengthValidator
* @implements {ValidationRule}
@ -155,14 +155,14 @@ namespace Swirl.Core {
}
/**
* (2)
* String width validator, the width of CJK characters is considered as 2.
*
* @class WidthValidator
* @extends {LengthRule}
*/
class WidthRule extends LengthRule {
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);
}
}
@ -181,7 +181,7 @@ namespace Swirl.Core {
}
/**
*
* Regex validator
*
* @class RegexValidator
* @implements {ValidationRule}
@ -195,7 +195,7 @@ namespace Swirl.Core {
}
/**
*
* Remote validator
*
* @class RemoteRule
* @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 {
archive := &model.Archive{}
err := ctx.Bind(archive)
if err == nil {
// 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)
if err != nil {
return err
}
// 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)
}

View File

@ -23,7 +23,7 @@ func main() {
misc.BindOptions()
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.Action = func(ctx *app.Context) {
misc.LoadOptions()

View File

@ -3,9 +3,9 @@ package model
import "time"
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"`
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"`
CreatedAt time.Time `bson:"created_at" json:"created_at,omitempty"`
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`

View File

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

View File

@ -1,4 +1,5 @@
{{ extends "../../_layouts/default" }}
{{ import "../../_modules/form" }}
{{ block style() }}
<link rel="stylesheet" href="/assets/codemirror/codemirror.css?v=5.30">
@ -7,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>var editor = CodeMirror.fromTextArea(document.getElementById("txt-content"), {lineNumbers: true});</script>
<script>$(() => new Swirl.Stack.Archive.EditPage())</script>
{{ end }}
{{ block body() }}
@ -64,30 +65,48 @@
</nav>
<section class="section">
<div class="container">
<form method="post" action="update" data-form="ajax-json" data-url="/stack/archive/">
<input name="id" value="{{ .Archive.ID }}" type="hidden">
<div class="field">
<label class="label">{{ i18n("field.name") }}</label>
<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 id="div-form" class="container">
<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>
</div>
<div class="field">
<label class="label">Content</label>
<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>
</div>
</div>
<div class="field">
<div class="control">
{{ yield radio(name="type", value="input", label="Input", checked="input") }}
{{ yield radio(name="type", value="upload", label="Upload") }}
</div>
<div class="field is-grouped">
<div class="control">
<button 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 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>
</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>
</section>
{{ end }}

View File

@ -1,4 +1,5 @@
{{ extends "../../_layouts/default" }}
{{ import "../../_modules/form" }}
{{ block style() }}
<link rel="stylesheet" href="/assets/codemirror/codemirror.css?v=5.30">
@ -7,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>var editor = CodeMirror.fromTextArea(document.getElementById("txt-content"), {lineNumbers: true});</script>
<script>$(() => new Swirl.Stack.Archive.EditPage())</script>
{{ end }}
{{ block body() }}
@ -34,31 +35,50 @@
</div>
</section>
<section class="section">
<div class="container">
<div id="div-form" class="container">
<h2 class="title">Create stack archive</h2>
<hr>
<form method="post" data-form="ajax-json" data-url="/stack/archive/">
<div class="field">
<label class="label">{{ i18n("field.name") }}</label>
<div class="control">
<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 class="field">
<label class="label">{{ i18n("field.name") }}</label>
<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>
</div>
<div 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="native" required></textarea>
</div>
</div>
<div class="field">
<div class="control">
{{ yield radio(name="type", value="input", label="Input", checked="input") }}
{{ yield radio(name="type", value="upload", label="Upload") }}
</div>
<div class="field is-grouped">
<div class="control">
<button 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 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></textarea>
</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>
</section>
{{ end }}