ui: refactor controllers, routes to use promise hash, comments

This commit is contained in:
Jack Pearkes 2014-04-30 17:31:40 -04:00
parent cd08e81958
commit 3329cc3846
5 changed files with 155 additions and 113 deletions

View file

@ -19,6 +19,16 @@
{{outlet}} {{outlet}}
</script> </script>
<script type="text/x-handlebars" data-template-name="error">
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<div class="text-center">
uh oh! This is an error page.
</div>
</div>
</div>
</script>
<script type="text/x-handlebars" data-template-name="dc"> <script type="text/x-handlebars" data-template-name="dc">
<div class="row"> <div class="row">
<div class="col-md-12 col-sm-12 col-xs-12 topbar"> <div class="col-md-12 col-sm-12 col-xs-12 topbar">
@ -347,6 +357,7 @@
<script type="text/x-handlebars" id="index"> <script type="text/x-handlebars" id="index">
<div class="col-md-8 col-md-offset-2 vertical-center"> <div class="col-md-8 col-md-offset-2 vertical-center">
{{errorMessage}}
{{#each item in model}} {{#each item in model}}
{{#link-to 'services' item }} {{#link-to 'services' item }}
<div class="panel panel-link panel-short"> <div class="panel panel-link panel-short">

View file

@ -1,83 +1,71 @@
// Add mixins
App.KvShowController = Ember.ObjectController.extend(Ember.Validations.Mixin);
//
// path: /
//
// The index is for choosing datacenters.
//
App.IndexController = Ember.Controller.extend({
});
//
// path: /:dc
//
App.DcController = Ember.Controller.extend({ App.DcController = Ember.Controller.extend({
// Whether or not the dropdown menu can be seen
isDropdownVisible: false, isDropdownVisible: false,
checks: function() { checks: function() {
var services = this.get('nodes'); var nodes = this.get('nodes');
var checks = Ember.A() var checks = Ember.A()
// loop over all of the services we have, // Combine the checks from all of our nodes
// merge their checks into one. // into one.
services.forEach(function(item) { nodes.forEach(function(item) {
checks = checks.concat(item.Checks) checks = checks.concat(item.Checks)
}); });
// return the checks
return checks return checks
}.property('checks'), }.property('Checks'),
// Returns the total number of failing checks.
//
// We treat any non-passing checks as failing
//
totalChecksFailing: function() {
var checks = this.get('checks')
return (checks.filterBy('Status', 'critical').get('length') +
checks.filterBy('Status', 'warning').get('length'))
}.property('Checks'),
//
// Returns the human formatted message for the button state
//
checkMessage: function() { checkMessage: function() {
var checks = this.get('checks') var checks = this.get('checks')
var failingChecks = this.get('totalChecksFailing');
var passingChecks = checks.filterBy('Status', 'passing').get('length');
// return the message for display
if (this.get('hasFailingChecks') == true) { if (this.get('hasFailingChecks') == true) {
return checks.filterBy('Status', 'critical').get('length') + ' checks failing'; return failingChecks + ' checks failing';
} else { } else {
return checks.filterBy('Status', 'passing').get('length') + ' checks passing'; return passingChecks + ' checks passing';
} }
}.property('checkMessage'), }.property('Checks'),
//
// Boolean if the datacenter has any failing checks.
//
hasFailingChecks: function() { hasFailingChecks: function() {
var checks = this.get('checks') var checks = this.get('checks')
// Return a boolean if checks are failing.
return (checks.filterBy('Status', 'critical').get('length') > 0); return (checks.filterBy('Status', 'critical').get('length') > 0);
}.property('Checks'),
}.property('hasFailingChecks'),
actions: { actions: {
// Hide and show the dropdown menu
toggle: function(item){ toggle: function(item){
this.toggleProperty('isDropdownVisible'); this.toggleProperty('isDropdownVisible');
} }
} }
}) })
// // Add mixins
// path: /:dc/services App.KvShowController = Ember.ObjectController.extend(Ember.Validations.Mixin);
//
// The index is for choosing services.
//
App.ServicesController = Ember.ArrayController.extend({
needs: ['application']
});
//
// path: /:dc/services/:name
//
// An individual service.
//
App.ServicesShowController = Ember.Controller.extend({
needs: ['services']
});
App.KvShowController.reopen({ App.KvShowController.reopen({
isLoading: false, isLoading: false,
actions: { actions: {
// Creates the key from the newKey model
// set on the route.
createKey: function() { createKey: function() {
this.set('isLoading', true); this.set('isLoading', true);
@ -86,21 +74,25 @@ App.KvShowController.reopen({
var controller = this; var controller = this;
// If we don't have a previous model to base // If we don't have a previous model to base
// see our parent, or we're not at the root level, // on our parent, or we're not at the root level,
// strip the leading slash. // strip the leading slash.
if (!topModel || topModel.get('parentKey') != "/") { if (!topModel || topModel.get('parentKey') != "/") {
newKey.set('Key', (topModel.get('parentKey') + newKey.get('Key'))); newKey.set('Key', (topModel.get('parentKey') + newKey.get('Key')));
} }
// Put the Key and the Value retrieved from the form
Ember.$.ajax({ Ember.$.ajax({
url: "/v1/kv/" + newKey.get('Key'), url: "/v1/kv/" + newKey.get('Key'),
type: 'PUT', type: 'PUT',
data: newKey.get('Value') data: newKey.get('Value')
}).then(function(response) { }).then(function(response) {
controller.set('isLoading', false) controller.set('isLoading', false)
// Transition to edit the key
controller.transitionToRoute('kv.edit', newKey.get('urlSafeKey')); controller.transitionToRoute('kv.edit', newKey.get('urlSafeKey'));
// Reload the keys in the left column
controller.get('keys').reload() controller.get('keys').reload()
}).fail(function(response) { }).fail(function(response) {
// Render the error message on the form if the request failed
controller.set('errorMessage', 'Received error while processing: ' + response.statusText) controller.set('errorMessage', 'Received error while processing: ' + response.statusText)
}); });
@ -112,19 +104,24 @@ App.KvEditController = Ember.Controller.extend({
isLoading: false, isLoading: false,
actions: { actions: {
// Updates the key set as the model on the route.
updateKey: function() { updateKey: function() {
this.set('isLoading', true); this.set('isLoading', true);
var key = this.get("model"); var key = this.get("model");
var controller = this; var controller = this;
// Put the key and the decoded (plain text) value
// from the form.
Ember.$.ajax({ Ember.$.ajax({
url: "/v1/kv/" + key.get('Key'), url: "/v1/kv/" + key.get('Key'),
type: 'PUT', type: 'PUT',
data: key.get('valueDecoded') data: key.get('valueDecoded')
}).then(function(response) { }).then(function(response) {
// If success, just reset the loading state.
controller.set('isLoading', false) controller.set('isLoading', false)
}).fail(function(response) { }).fail(function(response) {
// Render the error message on the form if the request failed
controller.set('errorMessage', 'Received error while processing: ' + response.statusText) controller.set('errorMessage', 'Received error while processing: ' + response.statusText)
}) })
}, },
@ -134,15 +131,20 @@ App.KvEditController = Ember.Controller.extend({
var key = this.get("model"); var key = this.get("model");
var controller = this; var controller = this;
// Get the parent for the transition back up a level
// after the delete
var parent = key.get('urlSafeParentKey'); var parent = key.get('urlSafeParentKey');
// Delete the key
Ember.$.ajax({ Ember.$.ajax({
url: "/v1/kv/" + key.get('Key'), url: "/v1/kv/" + key.get('Key'),
type: 'DELETE' type: 'DELETE'
}).then(function(response) { }).then(function(response) {
controller.set('isLoading', false); controller.set('isLoading', false);
// Tranisiton back up a level
controller.transitionToRoute('kv.show', parent); controller.transitionToRoute('kv.show', parent);
}).fail(function(response) { }).fail(function(response) {
// Render the error message on the form if the request failed
controller.set('errorMessage', 'Received error while processing: ' + response.statusText) controller.set('errorMessage', 'Received error while processing: ' + response.statusText)
}) })

View file

@ -6,10 +6,15 @@ App.Service = Ember.Object.extend({
// The number of failing checks within the service. // The number of failing checks within the service.
// //
failingChecks: function() { failingChecks: function() {
// If the service was returned from `/v1/internal/ui/services`
// then we have a aggregated value which we can just grab
if (this.get('ChecksCritical') != undefined) { if (this.get('ChecksCritical') != undefined) {
return (this.get('ChecksCritical') + this.get('ChecksWarning')) return (this.get('ChecksCritical') + this.get('ChecksWarning'))
// Otherwise, we need to filter the child checks by both failing
// states
} else { } else {
return this.get('Checks').filterBy('Status', 'critical').get('length'); return (checks.filterBy('Status', 'critical').get('length') +
checks.filterBy('Status', 'warning').get('length'))
} }
}.property('Checks'), }.property('Checks'),
@ -17,8 +22,12 @@ App.Service = Ember.Object.extend({
// The number of passing checks within the service. // The number of passing checks within the service.
// //
passingChecks: function() { passingChecks: function() {
// If the service was returned from `/v1/internal/ui/services`
// then we have a aggregated value which we can just grab
if (this.get('ChecksPassing') != undefined) { if (this.get('ChecksPassing') != undefined) {
return this.get('ChecksPassing') return this.get('ChecksPassing')
// Otherwise, we need to filter the child checks by both failing
// states
} else { } else {
return this.get('Checks').filterBy('Status', 'passing').get('length'); return this.get('Checks').filterBy('Status', 'passing').get('length');
} }
@ -53,7 +62,10 @@ App.Node = Ember.Object.extend({
// The number of failing checks within the service. // The number of failing checks within the service.
// //
failingChecks: function() { failingChecks: function() {
return this.get('Checks').filterBy('Status', 'critical').get('length'); var checks = this.get('Checks');
// We view both warning and critical as failing
return (checks.filterBy('Status', 'critical').get('length') +
checks.filterBy('Status', 'warning').get('length'))
}.property('Checks'), }.property('Checks'),
// //
@ -89,33 +101,46 @@ App.Node = Ember.Object.extend({
// A key/value object // A key/value object
// //
App.Key = Ember.Object.extend(Ember.Validations.Mixin, { App.Key = Ember.Object.extend(Ember.Validations.Mixin, {
// Validates using the Ember.Valdiations library
validations: { validations: {
Key: { presence: true }, Key: { presence: true },
Value: { presence: true } Value: { presence: true }
}, },
// Boolean if the key is valid
keyValid: Ember.computed.empty('errors.Key'), keyValid: Ember.computed.empty('errors.Key'),
// Boolean if the value is valid
valueValid: Ember.computed.empty('errors.Value'), valueValid: Ember.computed.empty('errors.Value'),
// The key with the parent removed.
// This is only for display purposes, and used for
// showing the key name inside of a nested key.
keyWithoutParent: function() { keyWithoutParent: function() {
return (this.get('Key').replace(this.get('parentKey'), '')); return (this.get('Key').replace(this.get('parentKey'), ''));
}.property('Key'), }.property('Key'),
// Boolean if the key is a "folder" or not, i.e is a nested key
// that feels like a folder. Used for UI
isFolder: function() { isFolder: function() {
return (this.get('Key').slice(-1) == "/") return (this.get('Key').slice(-1) == "/")
}.property('Key'), }.property('Key'),
// The dasherized URL safe version of the key for routing
urlSafeKey: function() { urlSafeKey: function() {
return this.get('Key').replace(/\//g, "-") return this.get('Key').replace(/\//g, "-")
}.property('Key'), }.property('Key'),
// The dasherized URL safe version of the parent key for routing
urlSafeParentKey: function() { urlSafeParentKey: function() {
return this.get('parentKey').replace(/\//g, "-") return this.get('parentKey').replace(/\//g, "-")
}.property('Key'), }.property('Key'),
// Determines what route to link to. If it's a folder,
// it will link to kv.show. Otherwise, kv.edit
linkToRoute: function() { linkToRoute: function() {
var key = this.get('urlSafeKey') var key = this.get('urlSafeKey')
// If the key ends in - it's a folder
if (key.slice(-1) === "-") { if (key.slice(-1) === "-") {
return 'kv.show' return 'kv.show'
} else { } else {
@ -123,39 +148,62 @@ App.Key = Ember.Object.extend(Ember.Validations.Mixin, {
} }
}.property('Key'), }.property('Key'),
// The base64 decoded value of the key.
// if you set on this key, it will update
// the key.Value
valueDecoded: function(key, value) { valueDecoded: function(key, value) {
// Setter
// setter
if (arguments.length > 1) { if (arguments.length > 1) {
this.set('Value', window.btoa(value)); this.set('Value', value);
} }
// getter
// If the value is null, we don't
// want to try and base64 decode it, so just return
if (this.get('Value') === null) { if (this.get('Value') === null) {
return ""; return "";
} }
// getter
// base64 decode the value
return window.atob(this.get('Value')); return window.atob(this.get('Value'));
}.property('Value'), }.property('Value'),
// An array of the key broken up by the /
keyParts: function() { keyParts: function() {
var key = this.get('Key'); var key = this.get('Key');
// If the key is a folder, remove the last
// slash to split properly
if (key.slice(-1) == "/") { if (key.slice(-1) == "/") {
key = key.substring(0, key.length - 1); key = key.substring(0, key.length - 1);
} }
return key.split('/'); return key.split('/');
}.property('Key'), }.property('Key'),
// The parent Key is the key one level above this.Key
// key: baz/bar/foobar/
// grandParent: baz/bar/
parentKey: function() { parentKey: function() {
var parts = this.get('keyParts').toArray(); var parts = this.get('keyParts').toArray();
// Remove the last item, essentially going up a level
// in hiearchy
parts.pop(); parts.pop();
return parts.join("/") + "/"; return parts.join("/") + "/";
}.property('Key'), }.property('Key'),
// The grandParent Key is the key two levels above this.Key
// key: baz/bar/foobar/
// grandParent: baz/
grandParentKey: function() { grandParentKey: function() {
var parts = this.get('keyParts').toArray(); var parts = this.get('keyParts').toArray();
// Remove the last two items, jumping two levels back
parts.pop(); parts.pop();
parts.pop(); parts.pop();

View file

@ -1,25 +1,39 @@
window.App = Ember.Application.create({ window.App = Ember.Application.create({
rootElement: "#app", rootElement: "#app",
LOG_TRANSITIONS: true, LOG_TRANSITIONS: true,
// The baseUrl for AJAX requests
// TODO implement in AJAX logic
baseUrl: 'http://localhost:8500' baseUrl: 'http://localhost:8500'
}); });
App.Router.map(function() { App.Router.map(function() {
// Our parent datacenter resource sets the namespace
// for the entire application
this.resource("dc", {path: "/:dc"}, function() { this.resource("dc", {path: "/:dc"}, function() {
// Services represent a consul service
this.resource("services", { path: "/services" }, function(){ this.resource("services", { path: "/services" }, function(){
// Show an individual service
this.route("show", { path: "/:name" }); this.route("show", { path: "/:name" });
}); });
// Nodes represent a consul node
this.resource("nodes", { path: "/nodes" }, function() { this.resource("nodes", { path: "/nodes" }, function() {
// Show an individual node
this.route("show", { path: "/:name" }); this.route("show", { path: "/:name" });
}); });
// Key/Value
this.resource("kv", { path: "/kv" }, function(){ this.resource("kv", { path: "/kv" }, function(){
// This route just redirects to /-
this.route("index", { path: "/" }); this.route("index", { path: "/" });
// List keys. This is more like an index
this.route("show", { path: "/:key" }); this.route("show", { path: "/:key" });
// Edit a specific key
this.route("edit", { path: "/:key/edit" }); this.route("edit", { path: "/:key/edit" });
}) })
}); });
// Shows a datacenter picker. If you only have one
// it just redirects you through.
this.route("index", { path: "/" }); this.route("index", { path: "/" });
}); });

View file

@ -5,6 +5,8 @@
// //
App.BaseRoute = Ember.Route.extend({ App.BaseRoute = Ember.Route.extend({
actions: { actions: {
// Used to link to keys that are not objects,
// like parents and grandParents
linkToKey: function(key) { linkToKey: function(key) {
key = key.replace(/\//g, "-") key = key.replace(/\//g, "-")
@ -20,37 +22,37 @@ App.BaseRoute = Ember.Route.extend({
// //
// The route for choosing datacenters, typically the first route loaded. // The route for choosing datacenters, typically the first route loaded.
// //
// Note: This *does not* extend from BaseRoute as that could cause App.IndexRoute = App.BaseRoute.extend({
// and loop of transitions. // Retrieve the list of datacenters
//
App.IndexRoute = Ember.Route.extend({
model: function(params) { model: function(params) {
return Ember.$.getJSON('/v1/catalog/datacenters').then(function(data) { return Ember.$.getJSON('/v1/catalog/datacenters').then(function(data) {
return data return data
}); })
},
setupController: function(controller, model) {
controller.set('content', model);
}, },
afterModel: function(model, transition) { afterModel: function(model, transition) {
// If we only have one datacenter, jump
// straight to it and bypass the global
// view
if (model.get('length') === 1) { if (model.get('length') === 1) {
this.transitionTo('services', model[0]); this.transitionTo('services', model[0]);
} }
} }
}); });
// The base DC route // The parent route for all resources. This keeps the top bar
// functioning, as well as the per-dc requests.
App.DcRoute = App.BaseRoute.extend({ App.DcRoute = App.BaseRoute.extend({
model: function(params) { model: function(params) {
// Return a promise hash to retreieve the
// dcs and nodes used in the header
return Ember.RSVP.hash({ return Ember.RSVP.hash({
dc: params.dc, dc: params.dc,
dcs: Ember.$.getJSON('/v1/catalog/datacenters'), dcs: Ember.$.getJSON('/v1/catalog/datacenters'),
nodes: Ember.$.getJSON('/v1/internal/ui/nodes').then(function(data) { nodes: Ember.$.getJSON('/v1/internal/ui/nodes').then(function(data) {
objs = []; objs = [];
// Merge the nodes into a list and create objects out of them
data.map(function(obj){ data.map(function(obj){
objs.push(App.Node.create(obj)); objs.push(App.Node.create(obj));
}); });
@ -69,6 +71,7 @@ App.DcRoute = App.BaseRoute.extend({
App.KvIndexRoute = App.BaseRoute.extend({ App.KvIndexRoute = App.BaseRoute.extend({
// If they hit /kv we want to just move them to /kv/-
beforeModel: function() { beforeModel: function() {
this.transitionTo('kv.show', '-') this.transitionTo('kv.show', '-')
} }
@ -76,16 +79,15 @@ App.KvIndexRoute = App.BaseRoute.extend({
App.KvShowRoute = App.BaseRoute.extend({ App.KvShowRoute = App.BaseRoute.extend({
model: function(params) { model: function(params) {
// Convert the key back to the format consul understands
var key = params.key.replace(/-/g, "/") var key = params.key.replace(/-/g, "/")
// Return a promise to retrieve the ?keys for that namespace
return Ember.$.getJSON('/v1/kv/' + key + '?keys&seperator=' + '/').then(function(data) { return Ember.$.getJSON('/v1/kv/' + key + '?keys&seperator=' + '/').then(function(data) {
objs = []; objs = [];
data.map(function(obj){ data.map(function(obj){
objs.push(App.Key.create({Key: obj})); objs.push(App.Key.create({Key: obj}));
}); });
return objs; return objs;
}); });
}, },
@ -108,15 +110,20 @@ App.KvEditRoute = App.BaseRoute.extend({
key = key.substring(0, key.length - 1); key = key.substring(0, key.length - 1);
} }
parts = key.split('/'); parts = key.split('/');
// Go one level up
parts.pop(); parts.pop();
// If we are all the way up, just return nothing for the root
if (parts.length == 0) { if (parts.length == 0) {
parentKey = "" parentKey = ""
} else { } else {
// Add a slash
parentKey = parts.join("/") + "/"; parentKey = parts.join("/") + "/";
} }
// Return a promise hash to get the data for both columns
return Ember.RSVP.hash({ return Ember.RSVP.hash({
key: Ember.$.getJSON('/v1/kv/' + keyName).then(function(data) { key: Ember.$.getJSON('/v1/kv/' + keyName).then(function(data) {
// Convert the returned data to a Key
return App.Key.create().setProperties(data[0]); return App.Key.create().setProperties(data[0]);
}), }),
keys: keysPromise = Ember.$.getJSON('/v1/kv/' + parentKey + '?keys&seperator=' + '/').then(function(data) { keys: keysPromise = Ember.$.getJSON('/v1/kv/' + parentKey + '?keys&seperator=' + '/').then(function(data) {
@ -132,6 +139,9 @@ App.KvEditRoute = App.BaseRoute.extend({
setupController: function(controller, models) { setupController: function(controller, models) {
controller.set('content', models.key); controller.set('content', models.key);
// If we don't have the cached model from our
// the kv.show controller, we need to go get it,
// otherwise we just load what we have.
if (this.modelFor('kv.show') == undefined ) { if (this.modelFor('kv.show') == undefined ) {
controller.set('siblings', models.keys); controller.set('siblings', models.keys);
} else { } else {
@ -140,13 +150,9 @@ App.KvEditRoute = App.BaseRoute.extend({
} }
}); });
/// services
//
// Display all the services, allow to drill down into the specific services.
//
App.ServicesRoute = App.BaseRoute.extend({ App.ServicesRoute = App.BaseRoute.extend({
model: function(params) { model: function(params) {
// Return a promise to retrieve all of the services
return Ember.$.getJSON('/v1/internal/ui/services').then(function(data) { return Ember.$.getJSON('/v1/internal/ui/services').then(function(data) {
objs = []; objs = [];
data.map(function(obj){ data.map(function(obj){
@ -155,56 +161,29 @@ App.ServicesRoute = App.BaseRoute.extend({
return objs return objs
}); });
}, },
//
// Set the services as the routes default model to be called in
// the template as {{model}}
//
setupController: function(controller, model) { setupController: function(controller, model) {
//
// Since we have 2 column layout, we need to also display the
// list of services on the left. Hence setting the attribute
// {{services}} on the controller.
//
controller.set('services', model); controller.set('services', model);
} }
}); });
//
// Display an individual service, as well as the global services in the left
// column.
//
App.ServicesShowRoute = App.BaseRoute.extend({ App.ServicesShowRoute = App.BaseRoute.extend({
//
// Set the model on the route. We look up the specific service
// by it's identifier passed via the route
//
model: function(params) { model: function(params) {
// Here we just use the built-in health endpoint, as it gives us everything
// we need.
return Ember.$.getJSON('/v1/health/service/' + params.name).then(function(data) { return Ember.$.getJSON('/v1/health/service/' + params.name).then(function(data) {
objs = []; objs = [];
data.map(function(obj){ data.map(function(obj){
objs.push(App.Node.create(obj)); objs.push(App.Node.create(obj));
}); });
return objs; return objs;
}); });
}, },
}); });
/// nodes
//
// Display an individual node, as well as the global nodes in the left
// column.
//
App.NodesShowRoute = App.BaseRoute.extend({ App.NodesShowRoute = App.BaseRoute.extend({
//
// Set the model on the route. We look up the specific node
// by it's identifier passed via the route
//
model: function(params) { model: function(params) {
// Return a promise hash of the node and nodes
return Ember.RSVP.hash({ return Ember.RSVP.hash({
node: Ember.$.getJSON('/v1/internal/ui/node/' + params.name).then(function(data) { node: Ember.$.getJSON('/v1/internal/ui/node/' + params.name).then(function(data) {
return App.Node.create(data) return App.Node.create(data)
@ -226,12 +205,9 @@ App.NodesShowRoute = App.BaseRoute.extend({
} }
}); });
//
// Display all the nodes, allow to drill down into the specific nodes.
//
App.NodesRoute = App.BaseRoute.extend({ App.NodesRoute = App.BaseRoute.extend({
model: function(params) { model: function(params) {
// Return a promise containing the nodes
return Ember.$.getJSON('/v1/internal/ui/nodes').then(function(data) { return Ember.$.getJSON('/v1/internal/ui/nodes').then(function(data) {
objs = []; objs = [];
data.map(function(obj){ data.map(function(obj){
@ -240,16 +216,7 @@ App.NodesRoute = App.BaseRoute.extend({
return objs return objs
}); });
}, },
//
// Set the node as the routes default model to be called in
// the template as {{model}}. This is the "expanded" view.
//
setupController: function(controller, model) { setupController: function(controller, model) {
//
// Since we have 2 column layout, we need to also display the
// list of nodes on the left. Hence setting the attribute
// {{nodes}} on the controller.
//
controller.set('nodes', model); controller.set('nodes', model);
} }
}); });