diff --git a/ui/index.html b/ui/index.html index 16cb2c439..6d95633ed 100644 --- a/ui/index.html +++ b/ui/index.html @@ -87,8 +87,33 @@ +
+
+
+
+

+ Create Key +

+
+ +
+
+
+
+ {{topModel.parentKey}} + {{ input value=newKey.key class="form-control" required=true }} +
+ {{ textarea value=newKey.value class="form-control"}} +
+
+ +
+
+
+
+ @@ -315,6 +340,7 @@ + diff --git a/ui/javascripts/app/controllers.js b/ui/javascripts/app/controllers.js index 63fd0d3fc..a8644cf2e 100644 --- a/ui/javascripts/app/controllers.js +++ b/ui/javascripts/app/controllers.js @@ -1,3 +1,6 @@ +// Add mixins +App.KvShowController = Ember.ObjectController.extend(Ember.Validations.Mixin); + // // path: / // @@ -71,6 +74,33 @@ App.ServicesShowController = Ember.Controller.extend({ needs: ['services'] }); +App.KvShowController.reopen({ + isLoading: false, + + actions: { + createKey: function() { + this.set('isLoading', true); + + var newKey = this.get('newKey'); + var topModel = this.get('topModel'); + + // If we don't have a previous model to base + // see our parent, or we're not at the root level, + // strip the leading slash. + if (!topModel || topModel.get('parentKey') != "/") { + newKey.set('key', (topModel.get('parentKey') + newKey.get('key'))); + } + + // Persist newKey + // + + Ember.run.later(this, function() { + this.set('isLoading', false) + }, 500); + + } + } +}); App.KvEditController = Ember.Controller.extend({ isLoading: false, diff --git a/ui/javascripts/app/models.js b/ui/javascripts/app/models.js index 21b15e750..552e0bf30 100644 --- a/ui/javascripts/app/models.js +++ b/ui/javascripts/app/models.js @@ -80,7 +80,12 @@ App.Node = Ember.Object.extend({ // // A key/value object // -App.Key = Ember.Object.extend({ +App.Key = Ember.Object.extend(Ember.Validations.Mixin, { + validations: { + key: { presence: true }, + value: { presence: true } + }, + isFolder: function() { return (this.get('key').slice(-1) == "/") }.property('key'), diff --git a/ui/javascripts/app/routes.js b/ui/javascripts/app/routes.js index 758f7a787..b6bc105c4 100644 --- a/ui/javascripts/app/routes.js +++ b/ui/javascripts/app/routes.js @@ -83,6 +83,7 @@ App.KvShowRoute = App.BaseRoute.extend({ setupController: function(controller, model) { controller.set('content', model); controller.set('topModel', model[0]); + controller.set('newKey', App.Key.create()); } }); diff --git a/ui/javascripts/libs/ember-validations.js b/ui/javascripts/libs/ember-validations.js new file mode 100644 index 000000000..4473efbca --- /dev/null +++ b/ui/javascripts/libs/ember-validations.js @@ -0,0 +1,778 @@ +// ========================================================================== +// Project: Ember Validations +// Copyright: Copyright 2013 DockYard, LLC. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== + + + // Version: 1.0.0.beta.2 + +(function() { +Ember.Validations = Ember.Namespace.create({ + VERSION: '1.0.0.beta.2' +}); + +})(); + + + +(function() { +Ember.Validations.messages = { + render: function(attribute, context) { + if (Ember.I18n) { + return Ember.I18n.t('errors.' + attribute, context); + } else { + var regex = new RegExp("{{(.*?)}}"), + attributeName = ""; + if (regex.test(this.defaults[attribute])) { + attributeName = regex.exec(this.defaults[attribute])[1]; + } + return this.defaults[attribute].replace(regex, context[attributeName]); + } + }, + defaults: { + inclusion: "is not included in the list", + exclusion: "is reserved", + invalid: "is invalid", + confirmation: "doesn't match {{attribute}}", + accepted: "must be accepted", + empty: "can't be empty", + blank: "can't be blank", + present: "must be blank", + tooLong: "is too long (maximum is {{count}} characters)", + tooShort: "is too short (minimum is {{count}} characters)", + wrongLength: "is the wrong length (should be {{count}} characters)", + notANumber: "is not a number", + notAnInteger: "must be an integer", + greaterThan: "must be greater than {{count}}", + greaterThanOrEqualTo: "must be greater than or equal to {{count}}", + equalTo: "must be equal to {{count}}", + lessThan: "must be less than {{count}}", + lessThanOrEqualTo: "must be less than or equal to {{count}}", + otherThan: "must be other than {{count}}", + odd: "must be odd", + even: "must be even", + url: "is not a valid URL" + } +}; + +})(); + + + +(function() { +Ember.Validations.Errors = Ember.Object.extend({ + unknownProperty: function(property) { + this.set(property, Ember.makeArray()); + return this.get(property); + } +}); + +})(); + + + +(function() { +var setValidityMixin = Ember.Mixin.create({ + isValid: function() { + return this.get('validators').compact().filterBy('isValid', false).get('length') === 0; + }.property('validators.@each.isValid'), + isInvalid: Ember.computed.not('isValid') +}); + +var pushValidatableObject = function(model, property) { + var content = model.get(property); + + model.removeObserver(property, pushValidatableObject); + if (Ember.isArray(content)) { + model.validators.pushObject(ArrayValidatorProxy.create({model: model, property: property, contentBinding: 'model.' + property})); + } else { + model.validators.pushObject(content); + } +}; + +var findValidator = function(validator) { + var klass = validator.classify(); + return Ember.Validations.validators.local[klass] || Ember.Validations.validators.remote[klass]; +}; + +var ArrayValidatorProxy = Ember.ArrayProxy.extend(setValidityMixin, { + validate: function() { + return this._validate(); + }, + _validate: function() { + var promises = this.get('content').invoke('_validate').without(undefined); + return Ember.RSVP.all(promises); + }.on('init'), + validators: Ember.computed.alias('content') +}); + +Ember.Validations.Mixin = Ember.Mixin.create(setValidityMixin, { + init: function() { + this._super(); + this.errors = Ember.Validations.Errors.create(); + this._dependentValidationKeys = {}; + this.validators = Ember.makeArray(); + if (this.get('validations') === undefined) { + this.validations = {}; + } + this.buildValidators(); + this.validators.forEach(function(validator) { + validator.addObserver('errors.[]', this, function(sender, key, value, context, rev) { + var errors = Ember.makeArray(); + this.validators.forEach(function(validator) { + if (validator.property === sender.property) { + errors = errors.concat(validator.errors); + } + }, this); + this.set('errors.' + sender.property, errors); + }); + }, this); + }, + buildValidators: function() { + var property, validator; + + for (property in this.validations) { + if (this.validations[property].constructor === Object) { + this.buildRuleValidator(property); + } else { + this.buildObjectValidator(property); + } + } + }, + buildRuleValidator: function(property) { + var validator; + for (validator in this.validations[property]) { + if (this.validations[property].hasOwnProperty(validator)) { + this.validators.pushObject(findValidator(validator).create({model: this, property: property, options: this.validations[property][validator]})); + } + } + }, + buildObjectValidator: function(property) { + if (Ember.isNone(this.get(property))) { + this.addObserver(property, this, pushValidatableObject); + } else { + pushValidatableObject(this, property); + } + }, + validate: function() { + var self = this; + return this._validate().then(function(vals) { + var errors = self.get('errors'); + if (vals.contains(false)) { + return Ember.RSVP.reject(errors); + } + return errors; + }); + }, + _validate: function() { + var promises = this.validators.invoke('_validate').without(undefined); + return Ember.RSVP.all(promises); + }.on('init') +}); + +})(); + + + +(function() { +Ember.Validations.patterns = Ember.Namespace.create({ + numericality: /^(-|\+)?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d*)?$/, + blank: /^\s*$/ +}); + +})(); + + + +(function() { +Ember.Validations.validators = Ember.Namespace.create(); +Ember.Validations.validators.local = Ember.Namespace.create(); +Ember.Validations.validators.remote = Ember.Namespace.create(); + +})(); + + + +(function() { +Ember.Validations.validators.Base = Ember.Object.extend({ + init: function() { + this.set('errors', Ember.makeArray()); + this._dependentValidationKeys = Ember.makeArray(); + this.conditionals = { + 'if': this.get('options.if'), + unless: this.get('options.unless') + }; + this.model.addObserver(this.property, this, this._validate); + }, + addObserversForDependentValidationKeys: function() { + this._dependentValidationKeys.forEach(function(key) { + this.model.addObserver(key, this, this._validate); + }, this); + }.on('init'), + pushDependentValidationKeyToModel: function() { + var model = this.get('model'); + if (model._dependentValidationKeys[this.property] === undefined) { + model._dependentValidationKeys[this.property] = Ember.makeArray(); + } + model._dependentValidationKeys[this.property].addObjects(this._dependentValidationKeys); + }.on('init'), + call: function () { + throw 'Not implemented!'; + }, + unknownProperty: function(key) { + var model = this.get('model'); + if (model) { + return model.get(key); + } + }, + isValid: Ember.computed.empty('errors.[]'), + validate: function() { + var self = this; + return this._validate().then(function(success) { + // Convert validation failures to rejects. + var errors = self.get('model.errors'); + if (success) { + return errors; + } else { + return Ember.RSVP.reject(errors); + } + }); + }, + _validate: function() { + this.errors.clear(); + if (this.canValidate()) { + this.call(); + } + if (this.get('isValid')) { + return Ember.RSVP.resolve(true); + } else { + return Ember.RSVP.resolve(false); + } + }.on('init'), + canValidate: function() { + if (typeof(this.conditionals) === 'object') { + if (this.conditionals['if']) { + if (typeof(this.conditionals['if']) === 'function') { + return this.conditionals['if'](this.model, this.property); + } else if (typeof(this.conditionals['if']) === 'string') { + if (typeof(this.model[this.conditionals['if']]) === 'function') { + return this.model[this.conditionals['if']](); + } else { + return this.model.get(this.conditionals['if']); + } + } + } else if (this.conditionals.unless) { + if (typeof(this.conditionals.unless) === 'function') { + return !this.conditionals.unless(this.model, this.property); + } else if (typeof(this.conditionals.unless) === 'string') { + if (typeof(this.model[this.conditionals.unless]) === 'function') { + return !this.model[this.conditionals.unless](); + } else { + return !this.model.get(this.conditionals.unless); + } + } + } else { + return true; + } + } else { + return true; + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Absence = Ember.Validations.validators.Base.extend({ + init: function() { + this._super(); + /*jshint expr:true*/ + if (this.options === true) { + this.set('options', {}); + } + + if (this.options.message === undefined) { + this.set('options.message', Ember.Validations.messages.render('present', this.options)); + } + }, + call: function() { + if (!Ember.isEmpty(this.model.get(this.property))) { + this.errors.pushObject(this.options.message); + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Acceptance = Ember.Validations.validators.Base.extend({ + init: function() { + this._super(); + /*jshint expr:true*/ + if (this.options === true) { + this.set('options', {}); + } + + if (this.options.message === undefined) { + this.set('options.message', Ember.Validations.messages.render('accepted', this.options)); + } + }, + call: function() { + if (this.options.accept) { + if (this.model.get(this.property) !== this.options.accept) { + this.errors.pushObject(this.options.message); + } + } else if (this.model.get(this.property) !== '1' && this.model.get(this.property) !== 1 && this.model.get(this.property) !== true) { + this.errors.pushObject(this.options.message); + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Confirmation = Ember.Validations.validators.Base.extend({ + init: function() { + this.originalProperty = this.property; + this.property = this.property + 'Confirmation'; + this._super(); + this._dependentValidationKeys.pushObject(this.originalProperty); + /*jshint expr:true*/ + if (this.options === true) { + this.set('options', { attribute: this.originalProperty }); + this.set('options', { message: Ember.Validations.messages.render('confirmation', this.options) }); + } + }, + call: function() { + if (this.model.get(this.originalProperty) !== this.model.get(this.property)) { + this.errors.pushObject(this.options.message); + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Exclusion = Ember.Validations.validators.Base.extend({ + init: function() { + this._super(); + if (this.options.constructor === Array) { + this.set('options', { 'in': this.options }); + } + + if (this.options.message === undefined) { + this.set('options.message', Ember.Validations.messages.render('exclusion', this.options)); + } + }, + call: function() { + /*jshint expr:true*/ + var message, lower, upper; + + if (Ember.isEmpty(this.model.get(this.property))) { + if (this.options.allowBlank === undefined) { + this.errors.pushObject(this.options.message); + } + } else if (this.options['in']) { + if (Ember.$.inArray(this.model.get(this.property), this.options['in']) !== -1) { + this.errors.pushObject(this.options.message); + } + } else if (this.options.range) { + lower = this.options.range[0]; + upper = this.options.range[1]; + + if (this.model.get(this.property) >= lower && this.model.get(this.property) <= upper) { + this.errors.pushObject(this.options.message); + } + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Format = Ember.Validations.validators.Base.extend({ + init: function() { + this._super(); + if (this.options.constructor === RegExp) { + this.set('options', { 'with': this.options }); + } + + if (this.options.message === undefined) { + this.set('options.message', Ember.Validations.messages.render('invalid', this.options)); + } + }, + call: function() { + if (Ember.isEmpty(this.model.get(this.property))) { + if (this.options.allowBlank === undefined) { + this.errors.pushObject(this.options.message); + } + } else if (this.options['with'] && !this.options['with'].test(this.model.get(this.property))) { + this.errors.pushObject(this.options.message); + } else if (this.options.without && this.options.without.test(this.model.get(this.property))) { + this.errors.pushObject(this.options.message); + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Inclusion = Ember.Validations.validators.Base.extend({ + init: function() { + this._super(); + if (this.options.constructor === Array) { + this.set('options', { 'in': this.options }); + } + + if (this.options.message === undefined) { + this.set('options.message', Ember.Validations.messages.render('inclusion', this.options)); + } + }, + call: function() { + var message, lower, upper; + if (Ember.isEmpty(this.model.get(this.property))) { + if (this.options.allowBlank === undefined) { + this.errors.pushObject(this.options.message); + } + } else if (this.options['in']) { + if (Ember.$.inArray(this.model.get(this.property), this.options['in']) === -1) { + this.errors.pushObject(this.options.message); + } + } else if (this.options.range) { + lower = this.options.range[0]; + upper = this.options.range[1]; + + if (this.model.get(this.property) < lower || this.model.get(this.property) > upper) { + this.errors.pushObject(this.options.message); + } + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Length = Ember.Validations.validators.Base.extend({ + init: function() { + var index, key; + this._super(); + /*jshint expr:true*/ + if (typeof(this.options) === 'number') { + this.set('options', { 'is': this.options }); + } + + if (this.options.messages === undefined) { + this.set('options.messages', {}); + } + + for (index = 0; index < this.messageKeys().length; index++) { + key = this.messageKeys()[index]; + if (this.options[key] !== undefined && this.options[key].constructor === String) { + this.model.addObserver(this.options[key], this, this._validate); + } + } + + this.options.tokenizer = this.options.tokenizer || function(value) { return value.split(''); }; + // if (typeof(this.options.tokenizer) === 'function') { + // debugger; + // // this.tokenizedLength = new Function('value', 'return ' + // } else { + // this.tokenizedLength = new Function('value', 'return (value || "").' + (this.options.tokenizer || 'split("")') + '.length'); + // } + }, + CHECKS: { + 'is' : '==', + 'minimum' : '>=', + 'maximum' : '<=' + }, + MESSAGES: { + 'is' : 'wrongLength', + 'minimum' : 'tooShort', + 'maximum' : 'tooLong' + }, + getValue: function(key) { + if (this.options[key].constructor === String) { + return this.model.get(this.options[key]) || 0; + } else { + return this.options[key]; + } + }, + messageKeys: function() { + return Ember.keys(this.MESSAGES); + }, + checkKeys: function() { + return Ember.keys(this.CHECKS); + }, + renderMessageFor: function(key) { + var options = {count: this.getValue(key)}, _key; + for (_key in this.options) { + options[_key] = this.options[_key]; + } + + return this.options.messages[this.MESSAGES[key]] || Ember.Validations.messages.render(this.MESSAGES[key], options); + }, + renderBlankMessage: function() { + if (this.options.is) { + return this.renderMessageFor('is'); + } else if (this.options.minimum) { + return this.renderMessageFor('minimum'); + } + }, + call: function() { + var check, fn, message, operator, key; + + if (Ember.isEmpty(this.model.get(this.property))) { + if (this.options.allowBlank === undefined && (this.options.is || this.options.minimum)) { + this.errors.pushObject(this.renderBlankMessage()); + } + } else { + for (key in this.CHECKS) { + operator = this.CHECKS[key]; + if (!this.options[key]) { + continue; + } + + fn = new Function('return ' + this.options.tokenizer(this.model.get(this.property)).length + ' ' + operator + ' ' + this.getValue(key)); + if (!fn()) { + this.errors.pushObject(this.renderMessageFor(key)); + } + } + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Numericality = Ember.Validations.validators.Base.extend({ + init: function() { + /*jshint expr:true*/ + var index, keys, key; + this._super(); + + if (this.options === true) { + this.options = {}; + } else if (this.options.constructor === String) { + key = this.options; + this.options = {}; + this.options[key] = true; + } + + if (this.options.messages === undefined || this.options.messages.numericality === undefined) { + this.options.messages = this.options.messages || {}; + this.options.messages = { numericality: Ember.Validations.messages.render('notANumber', this.options) }; + } + + if (this.options.onlyInteger !== undefined && this.options.messages.onlyInteger === undefined) { + this.options.messages.onlyInteger = Ember.Validations.messages.render('notAnInteger', this.options); + } + + keys = Ember.keys(this.CHECKS).concat(['odd', 'even']); + for(index = 0; index < keys.length; index++) { + key = keys[index]; + + if (isNaN(this.options[key])) { + this.model.addObserver(this.options[key], this, this._validate); + } + + if (this.options[key] !== undefined && this.options.messages[key] === undefined) { + if (Ember.$.inArray(key, Ember.keys(this.CHECKS)) !== -1) { + this.options.count = this.options[key]; + } + this.options.messages[key] = Ember.Validations.messages.render(key, this.options); + if (this.options.count !== undefined) { + delete this.options.count; + } + } + } + }, + CHECKS: { + equalTo :'===', + greaterThan : '>', + greaterThanOrEqualTo : '>=', + lessThan : '<', + lessThanOrEqualTo : '<=' + }, + call: function() { + var check, checkValue, fn, form, operator, val; + + if (Ember.isEmpty(this.model.get(this.property))) { + if (this.options.allowBlank === undefined) { + this.errors.pushObject(this.options.messages.numericality); + } + } else if (!Ember.Validations.patterns.numericality.test(this.model.get(this.property))) { + this.errors.pushObject(this.options.messages.numericality); + } else if (this.options.onlyInteger === true && !(/^[+\-]?\d+$/.test(this.model.get(this.property)))) { + this.errors.pushObject(this.options.messages.onlyInteger); + } else if (this.options.odd && parseInt(this.model.get(this.property), 10) % 2 === 0) { + this.errors.pushObject(this.options.messages.odd); + } else if (this.options.even && parseInt(this.model.get(this.property), 10) % 2 !== 0) { + this.errors.pushObject(this.options.messages.even); + } else { + for (check in this.CHECKS) { + operator = this.CHECKS[check]; + + if (this.options[check] === undefined) { + continue; + } + + if (!isNaN(parseFloat(this.options[check])) && isFinite(this.options[check])) { + checkValue = this.options[check]; + } else if (this.model.get(this.options[check]) !== undefined) { + checkValue = this.model.get(this.options[check]); + } + + fn = new Function('return ' + this.model.get(this.property) + ' ' + operator + ' ' + checkValue); + + if (!fn()) { + this.errors.pushObject(this.options.messages[check]); + } + } + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Presence = Ember.Validations.validators.Base.extend({ + init: function() { + this._super(); + /*jshint expr:true*/ + if (this.options === true) { + this.options = {}; + } + + if (this.options.message === undefined) { + this.options.message = Ember.Validations.messages.render('blank', this.options); + } + }, + call: function() { + if (Ember.isEmpty(this.model.get(this.property))) { + this.errors.pushObject(this.options.message); + } + } +}); + +})(); + + + +(function() { +Ember.Validations.validators.local.Url = Ember.Validations.validators.Base.extend({ + regexp: null, + regexp_ip: null, + + init: function() { + this._super(); + + if (this.get('options.message') === undefined) { + this.set('options.message', Ember.Validations.messages.render('url', this.options)); + } + + if (this.get('options.protocols') === undefined) { + this.set('options.protocols', ['http', 'https']); + } + + // Regular Expression Parts + var dec_octet = '(25[0-5]|2[0-4][0-9]|[0-1][0-9][0-9]|[1-9][0-9]|[0-9])'; // 0-255 + var ipaddress = '(' + dec_octet + '(\\.' + dec_octet + '){3})'; + var hostname = '([a-zA-Z0-9\\-]+\\.)+([a-zA-Z]{2,})'; + var encoded = '%[0-9a-fA-F]{2}'; + var characters = 'a-zA-Z0-9$\\-_.+!*\'(),;:@&='; + var segment = '([' + characters + ']|' + encoded + ')*'; + + // Build Regular Expression + var regex_str = '^'; + + if (this.get('options.domainOnly') === true) { + regex_str += hostname; + } else { + regex_str += '(' + this.get('options.protocols').join('|') + '):\\/\\/'; // Protocol + + // Username and password + if (this.get('options.allowUserPass') === true) { + regex_str += '(([a-zA-Z0-9$\\-_.+!*\'(),;:&=]|' + encoded + ')+@)?'; // Username & passwords + } + + // IP Addresses? + if (this.get('options.allowIp') === true) { + regex_str += '(' + hostname + '|' + ipaddress + ')'; // Hostname OR IP + } else { + regex_str += '(' + hostname + ')'; // Hostname only + } + + // Ports + if (this.get('options.allowPort') === true) { + regex_str += '(:[0-9]+)?'; // Port + } + + regex_str += '(\\/'; + regex_str += '(' + segment + '(\\/' + segment + ')*)?'; // Path + regex_str += '(\\?' + '([' + characters + '/?]|' + encoded + ')*)?'; // Query + regex_str += '(\\#' + '([' + characters + '/?]|' + encoded + ')*)?'; // Anchor + regex_str += ')?'; + } + + regex_str += '$'; + + // RegExp + this.regexp = new RegExp(regex_str); + this.regexp_ip = new RegExp(ipaddress); + }, + call: function() { + var url = this.model.get(this.property); + + if (Ember.isEmpty(url)) { + if (this.get('options.allowBlank') !== true) { + this.errors.pushObject(this.get('options.message')); + } + } else { + if (this.get('options.allowIp') !== true) { + if (this.regexp_ip.test(url)) { + this.errors.pushObject(this.get('options.message')); + return; + } + } + + if (!this.regexp.test(url)) { + this.errors.pushObject(this.get('options.message')); + } + } + } +}); + + +})(); + + + +(function() { + +})(); + + + +(function() { + +})(); +