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}}
</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">
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12 topbar">
@ -347,6 +357,7 @@
<script type="text/x-handlebars" id="index">
<div class="col-md-8 col-md-offset-2 vertical-center">
{{errorMessage}}
{{#each item in model}}
{{#link-to 'services' item }}
<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({
// Whether or not the dropdown menu can be seen
isDropdownVisible: false,
checks: function() {
var services = this.get('nodes');
var nodes = this.get('nodes');
var checks = Ember.A()
// loop over all of the services we have,
// merge their checks into one.
services.forEach(function(item) {
// Combine the checks from all of our nodes
// into one.
nodes.forEach(function(item) {
checks = checks.concat(item.Checks)
});
// return the 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() {
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) {
return checks.filterBy('Status', 'critical').get('length') + ' checks failing';
return failingChecks + ' checks failing';
} 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() {
var checks = this.get('checks')
// Return a boolean if checks are failing.
return (checks.filterBy('Status', 'critical').get('length') > 0);
}.property('hasFailingChecks'),
}.property('Checks'),
actions: {
// Hide and show the dropdown menu
toggle: function(item){
this.toggleProperty('isDropdownVisible');
}
}
})
//
// path: /:dc/services
//
// 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']
});
// Add mixins
App.KvShowController = Ember.ObjectController.extend(Ember.Validations.Mixin);
App.KvShowController.reopen({
isLoading: false,
actions: {
// Creates the key from the newKey model
// set on the route.
createKey: function() {
this.set('isLoading', true);
@ -86,21 +74,25 @@ App.KvShowController.reopen({
var controller = this;
// 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.
if (!topModel || topModel.get('parentKey') != "/") {
newKey.set('Key', (topModel.get('parentKey') + newKey.get('Key')));
}
// Put the Key and the Value retrieved from the form
Ember.$.ajax({
url: "/v1/kv/" + newKey.get('Key'),
type: 'PUT',
data: newKey.get('Value')
}).then(function(response) {
controller.set('isLoading', false)
// Transition to edit the key
controller.transitionToRoute('kv.edit', newKey.get('urlSafeKey'));
// Reload the keys in the left column
controller.get('keys').reload()
}).fail(function(response) {
// Render the error message on the form if the request failed
controller.set('errorMessage', 'Received error while processing: ' + response.statusText)
});
@ -112,19 +104,24 @@ App.KvEditController = Ember.Controller.extend({
isLoading: false,
actions: {
// Updates the key set as the model on the route.
updateKey: function() {
this.set('isLoading', true);
var key = this.get("model");
var controller = this;
// Put the key and the decoded (plain text) value
// from the form.
Ember.$.ajax({
url: "/v1/kv/" + key.get('Key'),
type: 'PUT',
data: key.get('valueDecoded')
}).then(function(response) {
// If success, just reset the loading state.
controller.set('isLoading', false)
}).fail(function(response) {
// Render the error message on the form if the request failed
controller.set('errorMessage', 'Received error while processing: ' + response.statusText)
})
},
@ -134,15 +131,20 @@ App.KvEditController = Ember.Controller.extend({
var key = this.get("model");
var controller = this;
// Get the parent for the transition back up a level
// after the delete
var parent = key.get('urlSafeParentKey');
// Delete the key
Ember.$.ajax({
url: "/v1/kv/" + key.get('Key'),
type: 'DELETE'
}).then(function(response) {
controller.set('isLoading', false);
// Tranisiton back up a level
controller.transitionToRoute('kv.show', parent);
}).fail(function(response) {
// Render the error message on the form if the request failed
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.
//
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) {
return (this.get('ChecksCritical') + this.get('ChecksWarning'))
// Otherwise, we need to filter the child checks by both failing
// states
} 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'),
@ -17,8 +22,12 @@ App.Service = Ember.Object.extend({
// The number of passing checks within the service.
//
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) {
return this.get('ChecksPassing')
// Otherwise, we need to filter the child checks by both failing
// states
} else {
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.
//
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'),
//
@ -89,33 +101,46 @@ App.Node = Ember.Object.extend({
// A key/value object
//
App.Key = Ember.Object.extend(Ember.Validations.Mixin, {
// Validates using the Ember.Valdiations library
validations: {
Key: { presence: true },
Value: { presence: true }
},
// Boolean if the key is valid
keyValid: Ember.computed.empty('errors.Key'),
// Boolean if the value is valid
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() {
return (this.get('Key').replace(this.get('parentKey'), ''));
}.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() {
return (this.get('Key').slice(-1) == "/")
}.property('Key'),
// The dasherized URL safe version of the key for routing
urlSafeKey: function() {
return this.get('Key').replace(/\//g, "-")
}.property('Key'),
// The dasherized URL safe version of the parent key for routing
urlSafeParentKey: function() {
return this.get('parentKey').replace(/\//g, "-")
}.property('Key'),
// Determines what route to link to. If it's a folder,
// it will link to kv.show. Otherwise, kv.edit
linkToRoute: function() {
var key = this.get('urlSafeKey')
// If the key ends in - it's a folder
if (key.slice(-1) === "-") {
return 'kv.show'
} else {
@ -123,39 +148,62 @@ App.Key = Ember.Object.extend(Ember.Validations.Mixin, {
}
}.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) {
// Setter
// setter
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) {
return "";
}
// getter
// base64 decode the value
return window.atob(this.get('Value'));
}.property('Value'),
// An array of the key broken up by the /
keyParts: function() {
var key = this.get('Key');
// If the key is a folder, remove the last
// slash to split properly
if (key.slice(-1) == "/") {
key = key.substring(0, key.length - 1);
}
return key.split('/');
}.property('Key'),
// The parent Key is the key one level above this.Key
// key: baz/bar/foobar/
// grandParent: baz/bar/
parentKey: function() {
var parts = this.get('keyParts').toArray();
// Remove the last item, essentially going up a level
// in hiearchy
parts.pop();
return parts.join("/") + "/";
}.property('Key'),
// The grandParent Key is the key two levels above this.Key
// key: baz/bar/foobar/
// grandParent: baz/
grandParentKey: function() {
var parts = this.get('keyParts').toArray();
// Remove the last two items, jumping two levels back
parts.pop();
parts.pop();

View File

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

View File

@ -5,6 +5,8 @@
//
App.BaseRoute = Ember.Route.extend({
actions: {
// Used to link to keys that are not objects,
// like parents and grandParents
linkToKey: function(key) {
key = key.replace(/\//g, "-")
@ -20,37 +22,37 @@ App.BaseRoute = Ember.Route.extend({
//
// The route for choosing datacenters, typically the first route loaded.
//
// Note: This *does not* extend from BaseRoute as that could cause
// and loop of transitions.
//
App.IndexRoute = Ember.Route.extend({
App.IndexRoute = App.BaseRoute.extend({
// Retrieve the list of datacenters
model: function(params) {
return Ember.$.getJSON('/v1/catalog/datacenters').then(function(data) {
return data
});
},
setupController: function(controller, model) {
controller.set('content', model);
})
},
afterModel: function(model, transition) {
// If we only have one datacenter, jump
// straight to it and bypass the global
// view
if (model.get('length') === 1) {
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({
model: function(params) {
// Return a promise hash to retreieve the
// dcs and nodes used in the header
return Ember.RSVP.hash({
dc: params.dc,
dcs: Ember.$.getJSON('/v1/catalog/datacenters'),
nodes: Ember.$.getJSON('/v1/internal/ui/nodes').then(function(data) {
objs = [];
// Merge the nodes into a list and create objects out of them
data.map(function(obj){
objs.push(App.Node.create(obj));
});
@ -69,6 +71,7 @@ App.DcRoute = App.BaseRoute.extend({
App.KvIndexRoute = App.BaseRoute.extend({
// If they hit /kv we want to just move them to /kv/-
beforeModel: function() {
this.transitionTo('kv.show', '-')
}
@ -76,16 +79,15 @@ App.KvIndexRoute = App.BaseRoute.extend({
App.KvShowRoute = App.BaseRoute.extend({
model: function(params) {
// Convert the key back to the format consul understands
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) {
objs = [];
data.map(function(obj){
objs.push(App.Key.create({Key: obj}));
});
return objs;
});
},
@ -108,15 +110,20 @@ App.KvEditRoute = App.BaseRoute.extend({
key = key.substring(0, key.length - 1);
}
parts = key.split('/');
// Go one level up
parts.pop();
// If we are all the way up, just return nothing for the root
if (parts.length == 0) {
parentKey = ""
} else {
// Add a slash
parentKey = parts.join("/") + "/";
}
// Return a promise hash to get the data for both columns
return Ember.RSVP.hash({
key: Ember.$.getJSON('/v1/kv/' + keyName).then(function(data) {
// Convert the returned data to a Key
return App.Key.create().setProperties(data[0]);
}),
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) {
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 ) {
controller.set('siblings', models.keys);
} 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({
model: function(params) {
// Return a promise to retrieve all of the services
return Ember.$.getJSON('/v1/internal/ui/services').then(function(data) {
objs = [];
data.map(function(obj){
@ -155,56 +161,29 @@ App.ServicesRoute = App.BaseRoute.extend({
return objs
});
},
//
// Set the services as the routes default model to be called in
// the template as {{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);
}
});
//
// Display an individual service, as well as the global services in the left
// column.
//
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) {
// 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) {
objs = [];
data.map(function(obj){
objs.push(App.Node.create(obj));
});
return objs;
});
},
});
/// nodes
//
// Display an individual node, as well as the global nodes in the left
// column.
//
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) {
// Return a promise hash of the node and nodes
return Ember.RSVP.hash({
node: Ember.$.getJSON('/v1/internal/ui/node/' + params.name).then(function(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({
model: function(params) {
// Return a promise containing the nodes
return Ember.$.getJSON('/v1/internal/ui/nodes').then(function(data) {
objs = [];
data.map(function(obj){
@ -240,16 +216,7 @@ App.NodesRoute = App.BaseRoute.extend({
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) {
//
// 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);
}
});