diff --git a/ui/index.html b/ui/index.html
index 9cd10a0ff5..6cf504f6b7 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -29,11 +29,13 @@
-
Services
+ {{#link-to 'services' controllers.application.getDc class='btn btn-default'}}Services{{/link-to}}
+
-
Nodes
+ {{#link-to 'nodes' controllers.application.getDc class='btn btn-default'}}Nodes{{/link-to}}
+
@@ -55,29 +57,60 @@
diff --git a/ui/javascripts/app.js b/ui/javascripts/app.js
index 12be2e2703..c74b2355e9 100755
--- a/ui/javascripts/app.js
+++ b/ui/javascripts/app.js
@@ -7,6 +7,7 @@ window.App = Ember.Application.create({
App.Router.map(function() {
this.route("index", { path: "/" });
this.route("services", { path: "/:dc/services" });
+ this.route("service", { path: "/:dc/services/:name" });
this.route("nodes", { path: "/:dc/nodes" });
this.route("node", { path: "/:dc/nodes/:name" });
this.route("kv", { path: "/:dc/kv" });
@@ -36,13 +37,6 @@ App.ApplicationController = Ember.Controller.extend({
}.property("getDc")
});
-//
-// path: /:dc/services
-//
-App.ServicesController = Ember.Controller.extend({
- needs: ['application']
-})
-
//
// Superclass to be used by all of the main routes below. All routes
// but the IndexRoute share the need to have a datacenter set.
@@ -140,12 +134,84 @@ App.Service = Ember.Object.extend({
}.property('hasFailingChecks')
});
+//
+// A Consul Node
+//
+App.Node = Ember.Object.extend({
+ //
+ // The number of failing checks within the service.
+ //
+ failingChecks: function() {
+ return this.get('Checks').filterBy('Status', 'critical').get('length');
+ }.property('failingChecks'),
+
+ //
+ // The number of passing checks within the service.
+ //
+ passingChecks: function() {
+ return this.get('Checks').filterBy('Status', 'passing').get('length');
+ }.property('passingChecks'),
+
+ //
+ // The formatted message returned for the user which represents the
+ // number of checks failing or passing. Returns `1 passing` or `2 failing`
+ //
+ checkMessage: function() {
+ if (this.get('hasFailingChecks') === false) {
+ return this.get('passingChecks') + ' passing';
+ } else {
+ return this.get('failingChecks') + ' failing';
+ }
+ }.property('checkMessage'),
+
+ //
+ // Boolean of whether or not there are failing checks in the service.
+ // This is used to set color backgrounds and so on.
+ //
+ hasFailingChecks: function() {
+ return (this.get('failingChecks') > 0);
+ }.property('hasFailingChecks')
+});
+
//
// Display all the services, allow to drill down into the specific services.
//
App.ServicesRoute = App.BaseRoute.extend({
- model: function() {
- return [App.Service.create(window.fixtures.services[0]), App.Service.create(window.fixtures.services[1])];
+ //
+ // 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', [App.Service.create(window.fixtures.services[0]), App.Service.create(window.fixtures.services[1])]);
+ }
+});
+
+//
+// Display an individual service, as well as the global services in the left
+// column.
+//
+App.ServiceRoute = 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) {
+ return [App.Node.create(window.fixtures.services_full[params.name][0]), App.Node.create(window.fixtures.services_full[params.name][1])];
+ },
+
+ setupController: function(controller, model) {
+ controller.set('content', 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', [App.Service.create(window.fixtures.services[0]), App.Service.create(window.fixtures.services[1])]);
}
});
@@ -153,6 +219,33 @@ App.ServicesRoute = App.BaseRoute.extend({
// Services
//
App.ServicesView = Ember.View.extend({
+ templateName: 'services',
layoutName: 'default_layout'
})
+//
+// Services
+//
+App.ServiceView = Ember.View.extend({
+ templateName: 'services',
+ layoutName: 'default_layout'
+})
+
+//
+// path: /:dc/services
+//
+App.ServicesController = Ember.Controller.extend({
+ needs: ['application']
+})
+
+//
+// path: /:dc/services/:name
+//
+App.ServiceController = Ember.Controller.extend({
+ //
+ // We use the same template as we do for the services
+ // array and have a simple conditional to display the nested
+ // individual service resource.
+ //
+ needs: ['application']
+})
diff --git a/ui/javascripts/fixtures.js b/ui/javascripts/fixtures.js
index 79146a0acb..aadd069532 100644
--- a/ui/javascripts/fixtures.js
+++ b/ui/javascripts/fixtures.js
@@ -27,8 +27,7 @@ fixtures.services = [
],
"Nodes": [
"node-10-0-1-109",
- "node-10-0-1-102",
- "node-10-0-1-103"
+ "node-10-0-3-84"
]
},
{
@@ -48,13 +47,114 @@ fixtures.services = [
}
],
"Nodes": [
- "node-10-0-1-109",
- "node-10-0-1-102",
- "node-10-0-1-103"
+ "node-10-0-1-103",
+ "node-10-0-1-104"
]
},
]
+// This is both of the fixture services full response. We
+// would just expect one of these, inside of the top level
+// key. We require that key just for the fixture lookup.
+fixtures.services_full = {
+ "vagrant-cloud-http": [
+ // A node
+ {
+ "ServicePort": 80,
+ "ServiceTags": null,
+ "ServiceName": "vagrant-cloud-http",
+ "ServiceID": "vagrant-cloud-http",
+ "Address": "10.0.1.109",
+ "Node": "node-10-0-1-109",
+ "Checks": [
+ {
+ "ServiceName": "",
+ "ServiceID": "",
+ "Notes": "",
+ "Status": "critical",
+ "Name": "Serf Health Status",
+ "CheckID": "serfHealth",
+ "Node": "node-10-0-3-83"
+ }
+ ]
+ },
+ // A node
+ {
+ "ServicePort": 80,
+ "ServiceTags": null,
+ "ServiceName": "vagrant-cloud-http",
+ "ServiceID": "vagrant-cloud-http",
+ "Address": "10.0.3.83",
+ "Node": "node-10-0-3-84",
+ "Checks": [
+ {
+ "ServiceName": "",
+ "ServiceID": "",
+ "Notes": "",
+ "Status": "passing",
+ "Name": "Serf Health Status",
+ "CheckID": "serfHealth",
+ "Node": "node-10-0-3-84"
+ }
+ ]
+ }
+ ],
+ "vagrant-share-mux": [
+ // A node
+ {
+ "ServicePort": 80,
+ "ServiceTags": null,
+ "ServiceName": "vagrant-share-mux",
+ "ServiceID": "vagrant-share-mux",
+ "Address": "10.0.1.104",
+ "Node": "node-10-0-1-104",
+ "Checks": [
+ {
+ "ServiceName": "vagrant-share-mux",
+ "ServiceID": "vagrant-share-mux",
+ "Notes": "",
+ "Output": "200 ok",
+ "Status": "passing",
+ "Name": "Foo Heathly",
+ "CheckID": "fooHealth",
+ "Node": "node-10-0-1-104"
+ }
+ ]
+ },
+ // A node
+ {
+ "ServicePort": 80,
+ "ServiceTags": null,
+ "ServiceName": "vagrant-share-mux",
+ "ServiceID": "vagrant-share-mux",
+ "Address": "10.0.1.103",
+ "Node": "node-10-0-1-103",
+ "Checks": [
+ {
+ "ServiceName": "",
+ "ServiceID": "",
+ "Notes": "",
+ "Output": "foobar baz",
+ "Status": "passing",
+ "Name": "Baz Status",
+ "CheckID": "bazHealth",
+ "Node": "node-10-0-1-103"
+ },
+ {
+ "ServiceName": "",
+ "ServiceID": "",
+ "Notes": "",
+ "Output": "foobar baz",
+ "Status": "passing",
+ "Name": "Serf Health Status",
+ "CheckID": "serfHealth",
+ "Node": "node-10-0-1-103"
+ }
+ ]
+ }
+ ]
+}
+
fixtures.dcs = ['nyc1', 'sf1', 'sg1']
localStorage.setItem("current_dc", fixtures.dcs[0]);
diff --git a/ui/styles/_buttons.scss b/ui/styles/_buttons.scss
index 45654b7594..96bf161995 100644
--- a/ui/styles/_buttons.scss
+++ b/ui/styles/_buttons.scss
@@ -13,7 +13,12 @@
.btn-default {
}
- &.btn-primary {
+ &.active {
+ box-shadow: none;
+ -webkit-box-shadow: none;
+ }
+
+ &.btn-primary, &.active {
color: $purple;
background-color: transparent;
border: 2px solid $purple;
diff --git a/ui/styles/_nav.scss b/ui/styles/_nav.scss
index 355be343ea..d74a0ba467 100644
--- a/ui/styles/_nav.scss
+++ b/ui/styles/_nav.scss
@@ -10,6 +10,7 @@
.topbar {
padding: 30px;
+ margin-bottom: 20px;
min-height: 100px;
border-bottom: 1px #eee solid;
diff --git a/ui/styles/_panels.scss b/ui/styles/_panels.scss
index ed0198e42a..79b80a0c7c 100644
--- a/ui/styles/_panels.scss
+++ b/ui/styles/_panels.scss
@@ -66,6 +66,7 @@
}
&.panel-link:hover {
- background-color: lighten($gray-background, 8%);
+ cursor: pointer;
+ background-color: lighten($gray-background, 8%);
}
}
diff --git a/ui/styles/base.scss b/ui/styles/base.scss
index 44980c3e7d..c06065d526 100644
--- a/ui/styles/base.scss
+++ b/ui/styles/base.scss
@@ -6,6 +6,26 @@
@import "buttons";
@import "lists";
+@media (min-width: 768px) { // + 18
+ .container {
+ width: 750px;
+ }
+}
+@media (min-width: 992px) { // + 22
+ .container {
+ width: 970px;
+ }
+}
+@media (min-width: 1200px) { // + 30
+ .container {
+ width: 1400px;
+ }
+}
+
+
+.no-margin {
+ margin: 0;
+}
.vertical-center {
margin-top: 200px;
@@ -17,6 +37,10 @@
}
}
+.bordered {
+ border-left: 2px solid $gray-background;
+}
+
.bg-purple {
background-color: $purple;