diff --git a/ui/app/components/search-box.js b/ui/app/components/search-box.js
index 8efb5dd5e..19aa76bef 100644
--- a/ui/app/components/search-box.js
+++ b/ui/app/components/search-box.js
@@ -12,7 +12,7 @@ export default Component.extend({
// Used to throttle sets to searchTerm
debounce: 150,
- classNames: ['field', 'has-addons'],
+ classNames: ['search-box', 'field', 'has-addons'],
actions: {
setSearchTerm(e) {
diff --git a/ui/app/initializers/app-token.js b/ui/app/initializers/app-token.js
new file mode 100644
index 000000000..f8b3dcee2
--- /dev/null
+++ b/ui/app/initializers/app-token.js
@@ -0,0 +1,12 @@
+export function initialize() {
+ const application = arguments[1] || arguments[0];
+
+ // Provides the acl token service to all templates
+ application.inject('controller', 'token', 'service:token');
+ application.inject('component', 'token', 'service:token');
+}
+
+export default {
+ name: 'app-token',
+ initialize,
+};
diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss
index 7832c934d..50f6ef1c8 100644
--- a/ui/app/styles/components.scss
+++ b/ui/app/styles/components.scss
@@ -1,6 +1,7 @@
@import "./components/badge";
@import "./components/boxed-section";
@import "./components/breadcrumbs";
+@import "./components/empty-message";
@import "./components/gutter";
@import "./components/inline-definitions";
@import "./components/job-diff";
diff --git a/ui/app/styles/components/empty-message.scss b/ui/app/styles/components/empty-message.scss
new file mode 100644
index 000000000..3f00021eb
--- /dev/null
+++ b/ui/app/styles/components/empty-message.scss
@@ -0,0 +1,21 @@
+.empty-message {
+ padding: 1.5rem;
+ background: $white-ter;
+ border-radius: $radius;
+
+ .empty-message-headline {
+ font-size: $size-3;
+ color: $grey;
+ text-align: center;
+ }
+
+ .empty-message-body {
+ padding: 0 20%;
+ text-align: center;
+ color: $grey;
+
+ strong {
+ color: $grey;
+ }
+ }
+}
diff --git a/ui/app/templates/jobs/index.hbs b/ui/app/templates/jobs/index.hbs
index 4ab6544fe..e5a57b38c 100644
--- a/ui/app/templates/jobs/index.hbs
+++ b/ui/app/templates/jobs/index.hbs
@@ -3,9 +3,11 @@
{{/global-header}}
{{#gutter-menu class="page-body"}}
-
-
{{search-box searchTerm=(mut searchTerm) placeholder="Search jobs..."}}
-
+ {{#if model.length}}
+
+
{{search-box searchTerm=(mut searchTerm) placeholder="Search jobs..."}}
+
+ {{/if}}
{{#list-pagination
source=sortedJobs
size=pageSize
@@ -37,6 +39,18 @@
+ {{else}}
+
+ {{#if (eq model.length 0)}}
+
No Jobs
+
+ There are currently no visible jobs in the cluster. It could be that the cluster is empty. It could also mean {{#link-to "settings.tokens"}}you don't have access to see any jobs{{/link-to}}.
+
+ {{else if searchTerm}}
+
No Matches
+
No jobs match the term {{searchTerm}}
+ {{/if}}
+
{{/list-pagination}}
{{/gutter-menu}}
diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs
index fc39e5589..bb4a0ed93 100644
--- a/ui/app/templates/jobs/job/task-group.hbs
+++ b/ui/app/templates/jobs/job/task-group.hbs
@@ -90,6 +90,13 @@
{{#p.last class="pagination-link"}} >| {{/p.last}}
+ {{else}}
+
+
+
No Matches
+
No allocations match the term {{searchTerm}}
+
+
{{/list-pagination}}
diff --git a/ui/app/templates/nodes/index.hbs b/ui/app/templates/nodes/index.hbs
index 580adbbae..1b324c69e 100644
--- a/ui/app/templates/nodes/index.hbs
+++ b/ui/app/templates/nodes/index.hbs
@@ -3,9 +3,11 @@
{{/global-header}}
{{#gutter-menu class="page-body"}}
-
-
{{search-box searchTerm=(mut searchTerm) placeholder="Search nodes..."}}
-
+ {{#if nodes.length}}
+
+
{{search-box searchTerm=(mut searchTerm) placeholder="Search nodes..."}}
+
+ {{/if}}
{{#list-pagination
source=sortedNodes
size=pageSize
@@ -38,6 +40,18 @@
+ {{else}}
+
+ {{#if (eq nodes.length 0)}}
+
No Clients
+
+ There are currently no visible nodes in the cluster. This could mean that the cluster is bootstrapped with no clients. It could also mean {{#link-to "settings.tokens"}}you don't have access to see any clients{{/link-to}}.
+
+ {{else if searchTerm}}
+
No Matches
+
No clients match the term {{searchTerm}}
+ {{/if}}
+
{{/list-pagination}}
{{/gutter-menu}}
diff --git a/ui/app/templates/servers.hbs b/ui/app/templates/servers.hbs
index e4d12d28b..3244a306e 100644
--- a/ui/app/templates/servers.hbs
+++ b/ui/app/templates/servers.hbs
@@ -1,6 +1,6 @@
{{#global-header class="page-header"}}
- Nodes
+ Servers
{{/global-header}}
{{#gutter-menu class="page-body"}}
+ {{else}}
+
+
Invalid Permissions
+
+ {{#if token.secret}}
+ Your ACL token does not grant access to see servers.
+ {{else}}
+ You have no ACL token set. {{#link-to "settings.tokens"}}Provide a token{{/link-to}} with the appropriate permissions to see servers.
+ {{/if}}
+
+
{{/list-pagination}}
{{outlet}}
diff --git a/ui/tests/acceptance/jobs-list-test.js b/ui/tests/acceptance/jobs-list-test.js
index 699739fe4..36f0fb576 100644
--- a/ui/tests/acceptance/jobs-list-test.js
+++ b/ui/tests/acceptance/jobs-list-test.js
@@ -1,5 +1,5 @@
import Ember from 'ember';
-import { click, findAll, currentURL, visit } from 'ember-native-dom-helpers';
+import { click, find, findAll, currentURL, visit, fillIn } from 'ember-native-dom-helpers';
import { test } from 'qunit';
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
@@ -80,3 +80,30 @@ test('each job row should link to the corresponding job', function(assert) {
assert.equal(currentURL(), `/jobs/${job.id}`);
});
});
+
+test('when there are no jobs, there is an empty message', function(assert) {
+ visit('/jobs');
+
+ andThen(() => {
+ assert.ok(find('.empty-message'));
+ assert.equal(find('.empty-message-headline').textContent, 'No Jobs');
+ });
+});
+
+test('when there are jobs, but no matches for a search result, there is an empty message', function(
+ assert
+) {
+ server.create('job', { name: 'cat 1' });
+ server.create('job', { name: 'cat 2' });
+
+ visit('/jobs');
+
+ andThen(() => {
+ fillIn('.search-box input', 'dog');
+ });
+
+ andThen(() => {
+ assert.ok(find('.empty-message'));
+ assert.equal(find('.empty-message-headline').textContent, 'No Matches');
+ });
+});
diff --git a/ui/tests/acceptance/nodes-list-test.js b/ui/tests/acceptance/nodes-list-test.js
index 4a6ee0301..23e1b10ed 100644
--- a/ui/tests/acceptance/nodes-list-test.js
+++ b/ui/tests/acceptance/nodes-list-test.js
@@ -1,5 +1,5 @@
import Ember from 'ember';
-import { click, findAll, currentURL, visit } from 'ember-native-dom-helpers';
+import { click, find, findAll, currentURL, visit } from 'ember-native-dom-helpers';
import { test } from 'qunit';
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
import { findLeader } from '../../mirage/config';
@@ -75,6 +75,35 @@ test('each client should link to the client detail page', function(assert) {
});
});
+test('when there are no clients, there is an empty message', function(assert) {
+ server.createList('agent', 1);
+
+ visit('/nodes');
+
+ andThen(() => {
+ assert.ok(find('.empty-message'));
+ assert.equal(find('.empty-message-headline').textContent, 'No Clients');
+ });
+});
+
+test('when there are clients, but no matches for a search term, there is an empty message', function(
+ assert
+) {
+ server.createList('agent', 1);
+ server.create('node', { name: 'node' });
+
+ visit('/nodes');
+
+ andThen(() => {
+ fillIn('.search-box input', 'client');
+ });
+
+ andThen(() => {
+ assert.ok(find('.empty-message'));
+ assert.equal(find('.empty-message-headline').textContent, 'No Matches');
+ });
+});
+
test('/servers should list all servers', function(assert) {
const agentsCount = 10;
const pageSize = 8;
@@ -141,3 +170,24 @@ test('each server should link to the server detail page', function(assert) {
assert.equal(currentURL(), `/servers/${agent.name}`);
});
});
+
+test('when the API returns no agents, show an empty message', function(assert) {
+ minimumSetup();
+
+ // Override the members handler to act as if server-side permissions
+ // are preventing a qualified response.
+ server.pretender.get('/v1/agent/members', () => [
+ 200,
+ {},
+ JSON.stringify({
+ Members: [],
+ }),
+ ]);
+
+ visit('/servers');
+
+ andThen(() => {
+ assert.ok(find('.empty-message'));
+ assert.equal(find('.empty-message-headline').textContent, 'Invalid Permissions');
+ });
+});
diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js
index 800e57544..593412317 100644
--- a/ui/tests/acceptance/task-group-detail-test.js
+++ b/ui/tests/acceptance/task-group-detail-test.js
@@ -1,5 +1,5 @@
import Ember from 'ember';
-import { click, findAll, currentURL, visit } from 'ember-native-dom-helpers';
+import { click, find, findAll, fillIn, currentURL, visit } from 'ember-native-dom-helpers';
import { test } from 'qunit';
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
@@ -41,6 +41,11 @@ moduleForAcceptance('Acceptance | task group detail', {
taskGroup: taskGroups[1].name,
});
+ // Set a static name to make the search test deterministic
+ server.db.allocations.forEach(alloc => {
+ alloc.name = 'aaaaa';
+ });
+
visit(`/jobs/${job.id}/${taskGroup.name}`);
},
});
@@ -212,3 +217,12 @@ test('each allocation should show stats about the allocation, retrieved directly
`Requests ${nodeStatsUrl}`
);
});
+
+test('when the allocation search has no matches, there is an empty message', function(assert) {
+ fillIn('.search-box input', 'zzzzzz');
+
+ andThen(() => {
+ assert.ok(find('.allocations .empty-message'));
+ assert.equal(find('.allocations .empty-message-headline').textContent, 'No Matches');
+ });
+});