Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update client list to collapse statuses #5789

Merged
merged 45 commits into from
Jun 19, 2019
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
cf98ad3
Update client list to show combined status
backspace Jun 6, 2019
04c9745
Restore colouring for node status differences
backspace Jun 6, 2019
7416615
Add empty commit
backspace Jun 7, 2019
74e9cda
Remove no-longer-shared function
backspace Jun 7, 2019
1db5d79
Add title to status field to show its components
backspace Jun 7, 2019
fba9fe6
Update name to fully show on hover
backspace Jun 7, 2019
0da1b4e
Add scenario override for demonstration
backspace Jun 10, 2019
6e36a6e
Add border at edge of hover-showing element
backspace Jun 10, 2019
fcdf555
Change overflowed text colour to standard
backspace Jun 10, 2019
27de41a
Merge branch 'master' into f-ui/long-client-names
backspace Jun 10, 2019
6972740
Move flags filters into status list
backspace Jun 10, 2019
0c23494
Restore autofixed line
backspace Jun 10, 2019
afd6716
Remove commented-out tests
backspace Jun 10, 2019
ffac3d7
Rename statuses-only variable
backspace Jun 10, 2019
778af84
Fix padding
backspace Jun 10, 2019
c1cd142
Change name of overflow container
backspace Jun 10, 2019
387de5d
Remove TODO
backspace Jun 10, 2019
0693748
Remove transparency from background
backspace Jun 10, 2019
798b0f8
Rename hover-reveal classes
backspace Jun 11, 2019
c22aa41
Change revealable border to gradient
backspace Jun 11, 2019
4f32c5e
Fix height of revealable shadow
backspace Jun 11, 2019
046f464
Change name of composite status to state
backspace Jun 11, 2019
f6f9197
Update comment to remove historic reference
backspace Jun 11, 2019
0d11c06
Change name width to 300px
backspace Jun 11, 2019
02c1729
Change address column to be truncatable
backspace Jun 11, 2019
980af21
Fix tests for state facet
backspace Jun 11, 2019
80c6f14
Change composite state title to tooltip
backspace Jun 11, 2019
da93869
Change tooltip to be relative only on hover
backspace Jun 11, 2019
3d20925
Change is-revealable to be relative only on hover
backspace Jun 11, 2019
eba33f3
Change Netlify site id
backspace Jun 11, 2019
86f257a
Change Mirage record to use draining trait
backspace Jun 11, 2019
c38b155
Add pseudoelement to cover shadow when narrow
backspace Jun 11, 2019
768fa84
Move width to be near size/position properties
backspace Jun 11, 2019
8cc972a
Move .is-truncatable within .is-300px
backspace Jun 11, 2019
0f531ff
Remove calc
backspace Jun 11, 2019
29aec09
Fix gradient
backspace Jun 11, 2019
23b0075
Change combined column heading
backspace Jun 11, 2019
57c7579
Merge branch 'master' into f-ui/long-client-names
backspace Jun 11, 2019
7344e71
Remove demonstration data
backspace Jun 14, 2019
720ab34
Remove extraneous whitespace
backspace Jun 14, 2019
c2c4d5d
Remove overflow-reveal feature
backspace Jun 14, 2019
a109a9c
Merge branch 'master' into f-ui/long-client-names
backspace Jun 14, 2019
8570b84
Merge branch 'master' into f-ui/long-client-names
backspace Jun 17, 2019
37c4c55
Merge branch 'master' into f-ui/long-client-names
backspace Jun 19, 2019
f23cf56
Add changelog entry
backspace Jun 19, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ui/app/components/client-node-row.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { lazyClick } from '../helpers/lazy-click';
import { watchRelationship } from 'nomad-ui/utils/properties/watch';
import WithVisibilityDetection from 'nomad-ui/mixins/with-component-visibility-detection';
Expand All @@ -12,6 +13,11 @@ export default Component.extend(WithVisibilityDetection, {

node: null,

statusTitle: computed('node.status', 'node.isDraining', 'node.isEligible', function() {
const node = this.node;
return `status: ${node.status}\ndraining: ${node.isDraining}\neligible: ${node.isEligible}`;
}),

onClick() {},

click(event) {
Expand Down
26 changes: 9 additions & 17 deletions ui/app/controllers/clients/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export default Controller.extend(Sortable, Searchable, {
qpClass: 'class',
qpStatus: 'status',
qpDatacenter: 'dc',
qpFlags: 'flags',
},

currentPage: 1,
Expand All @@ -35,12 +34,10 @@ export default Controller.extend(Sortable, Searchable, {
qpClass: '',
qpStatus: '',
qpDatacenter: '',
qpFlags: '',

selectionClass: selection('qpClass'),
selectionStatus: selection('qpStatus'),
selectionDatacenter: selection('qpDatacenter'),
selectionFlags: selection('qpFlags'),

optionsClass: computed('nodes.[]', function() {
const classes = Array.from(new Set(this.nodes.mapBy('nodeClass'))).compact();
Expand All @@ -57,47 +54,42 @@ export default Controller.extend(Sortable, Searchable, {
{ key: 'initializing', label: 'Initializing' },
{ key: 'ready', label: 'Ready' },
{ key: 'down', label: 'Down' },
{ key: 'ineligible', label: 'Ineligible' },
{ key: 'draining', label: 'Draining' },
]),

optionsDatacenter: computed('nodes.[]', function() {
const datacenters = Array.from(new Set(this.nodes.mapBy('datacenter'))).compact();

// Remove any invalid datacenters from the query param/selection
scheduleOnce('actions', () => {
this.set(
'qpDatacenter',
serialize(intersection(datacenters, this.selectionDatacenter))
);
this.set('qpDatacenter', serialize(intersection(datacenters, this.selectionDatacenter)));
});

return datacenters.sort().map(dc => ({ key: dc, label: dc }));
}),

optionsFlags: computed(() => [
{ key: 'ineligible', label: 'Ineligible' },
{ key: 'draining', label: 'Draining' },
]),

filteredNodes: computed(
'nodes.[]',
'selectionClass',
'selectionStatus',
'selectionDatacenter',
'selectionFlags',
function() {
const {
selectionClass: classes,
selectionStatus: statuses,
selectionDatacenter: datacenters,
selectionFlags: flags,
} = this;

const onlyIneligible = flags.includes('ineligible');
const onlyDraining = flags.includes('draining');
const onlyIneligible = statuses.includes('ineligible');
const onlyDraining = statuses.includes('draining');

// “flags” were formerly a separate filter, now combined with statuses
backspace marked this conversation as resolved.
Show resolved Hide resolved
const trueStatuses = statuses.without('ineligible').without('draining');
backspace marked this conversation as resolved.
Show resolved Hide resolved

return this.nodes.filter(node => {
if (classes.length && !classes.includes(node.get('nodeClass'))) return false;
if (statuses.length && !statuses.includes(node.get('status'))) return false;
if (trueStatuses.length && !trueStatuses.includes(node.get('status'))) return false;
if (datacenters.length && !datacenters.includes(node.get('datacenter'))) return false;

if (onlyIneligible && node.get('isEligible')) return false;
Expand Down
24 changes: 24 additions & 0 deletions ui/app/styles/core/table.scss
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

&.is-hover-overflowable {
backspace marked this conversation as resolved.
Show resolved Hide resolved
position: relative;

.hover-overflow {
display: none;
position: absolute;
top: 0;
left: 0;
padding: calc(1.25em - 1px) 1.5em;
}
backspace marked this conversation as resolved.
Show resolved Hide resolved

&:hover {
color: transparent;
overflow: visible;

.hover-overflow {
color: $text;
display: block;
background-color: rgba(255, 255, 255, 0.9);
backspace marked this conversation as resolved.
Show resolved Hide resolved
border-right: 1px solid $grey-blue;
backspace marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

&.is-narrow {
Expand Down
8 changes: 0 additions & 8 deletions ui/app/templates/clients/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@
options=optionsDatacenter
selection=selectionDatacenter
onSelect=(action setFacetQueryParam "qpDatacenter")}}
{{multi-select-dropdown
data-test-flags-facet
label="Flags"
options=optionsFlags
selection=selectionFlags
onSelect=(action setFacetQueryParam "qpFlags")}}
</div>
</div>
</div>
Expand All @@ -54,8 +48,6 @@
{{#t.sort-by prop="id"}}ID{{/t.sort-by}}
{{#t.sort-by class="is-200px is-truncatable" prop="name"}}Name{{/t.sort-by}}
{{#t.sort-by prop="status"}}Status{{/t.sort-by}}
{{#t.sort-by prop="isDraining"}}Drain{{/t.sort-by}}
{{#t.sort-by prop="schedulingEligibility"}}Eligibility{{/t.sort-by}}
<th>Address</th>
{{#t.sort-by prop="datacenter"}}Datacenter{{/t.sort-by}}
<th># Allocs</th>
Expand Down
21 changes: 9 additions & 12 deletions ui/app/templates/components/client-node-row.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,17 @@
{{/if}}
</td>
<td data-test-client-id>{{#link-to "clients.client" node.id class="is-primary"}}{{node.shortId}}{{/link-to}}</td>
<td data-test-client-name class="is-200px is-truncatable" title="{{node.name}}">{{node.name}}</td>
<td data-test-client-status>{{node.status}}</td>
<td data-test-client-drain>
{{#if node.isDraining}}
<span class="status-text is-info">true</span>
{{else}}
false
{{/if}}
<td class="is-200px is-truncatable is-hover-overflowable" title="{{node.name}}">
backspace marked this conversation as resolved.
Show resolved Hide resolved
<span data-test-client-name>{{node.name}}</span>
<div class="hover-overflow" aria-hidden>{{node.name}}</div>
backspace marked this conversation as resolved.
Show resolved Hide resolved
</td>
<td data-test-client-eligibility>
{{#if node.isEligible}}
{{node.schedulingEligibility}}
<td data-test-client-status title={{statusTitle}}>
backspace marked this conversation as resolved.
Show resolved Hide resolved
{{#if node.isDraining}}
<span class="status-text is-info">draining</span>
{{else if (not node.isEligible)}}
<span class="status-text is-warning">ineligible</span>
{{else}}
<span class="status-text is-warning">{{node.schedulingEligibility}}</span>
{{node.status}}
{{/if}}
</td>
<td data-test-client-address>{{node.httpAddr}}</td>
Expand Down
35 changes: 34 additions & 1 deletion ui/mirage/scenarios/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,40 @@ export default function(server) {

function smallCluster(server) {
server.createList('agent', 3);
server.createList('node', 5);

// server.createList('node', 5);
// FIXME data to exercise all permutations; remove before merging PR
backspace marked this conversation as resolved.
Show resolved Hide resolved
server.create('node', {
modifyIndex: 4,
status: 'ready',
schedulingEligibility: 'eligible',
drain: false,
});
server.create('node', {
modifyIndex: 3,
status: 'initializing',
schedulingEligibility: 'eligible',
drain: false,
});
server.create('node', {
modifyIndex: 2,
status: 'down',
schedulingEligibility: 'eligible',
drain: false,
});
server.create('node', {
modifyIndex: 1,
status: 'ready',
schedulingEligibility: 'ineligible',
drain: false,
});
server.create('node', {
modifyIndex: 0,
status: 'ready',
schedulingEligibility: 'ineligible',
drain: true,
});

server.createList('job', 5);
}

Expand Down
104 changes: 72 additions & 32 deletions ui/tests/acceptance/clients-list-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import ClientsList from 'nomad-ui/tests/pages/clients/list';

function minimumSetup() {
server.createList('node', 1);
server.createList('agent', 1);
}

module('Acceptance | clients list', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
Expand All @@ -34,8 +29,8 @@ module('Acceptance | clients list', function(hooks) {
});

test('each client record should show high-level info of the client', async function(assert) {
minimumSetup();
const node = server.db.nodes[0];
const node = server.create('node', 'draining');
server.createList('agent', 1);

await ClientsList.visit();

Expand All @@ -44,16 +39,68 @@ module('Acceptance | clients list', function(hooks) {

assert.equal(nodeRow.id, node.id.split('-')[0], 'ID');
assert.equal(nodeRow.name, node.name, 'Name');
assert.equal(nodeRow.status, node.status, 'Status');
assert.equal(nodeRow.drain, node.drain + '', 'Draining');
assert.equal(nodeRow.eligibility, node.schedulingEligibility, 'Eligibility');
assert.equal(nodeRow.status.text, 'draining', 'Combined status, draining, and eligbility');
assert.equal(nodeRow.address, node.httpAddr);
assert.equal(nodeRow.datacenter, node.datacenter, 'Datacenter');
assert.equal(nodeRow.allocations, allocations.length, '# Allocations');
});

test('client status, draining, and eligibility are collapsed into one column', async function(assert) {
server.createList('agent', 1);

server.create('node', {
modifyIndex: 4,
status: 'ready',
schedulingEligibility: 'eligible',
drain: false,
});
server.create('node', {
modifyIndex: 3,
status: 'initializing',
schedulingEligibility: 'eligible',
drain: false,
});
server.create('node', {
modifyIndex: 2,
status: 'down',
schedulingEligibility: 'eligible',
drain: false,
});
server.create('node', {
modifyIndex: 1,
status: 'ready',
schedulingEligibility: 'ineligible',
drain: false,
});
server.create('node', {
modifyIndex: 0,
status: 'ready',
schedulingEligibility: 'ineligible',
drain: true,
});
backspace marked this conversation as resolved.
Show resolved Hide resolved

await ClientsList.visit();

ClientsList.nodes[0].status.as(readyClient => {
backspace marked this conversation as resolved.
Show resolved Hide resolved
assert.equal(readyClient.text, 'ready');
assert.ok(readyClient.isUnformatted, 'expected no status class');
assert.equal(readyClient.title, 'status: ready\ndraining: false\neligible: true');
});

assert.equal(ClientsList.nodes[1].status.text, 'initializing');
assert.equal(ClientsList.nodes[2].status.text, 'down');

assert.equal(ClientsList.nodes[3].status.text, 'ineligible');
assert.ok(ClientsList.nodes[3].status.isWarning, 'expected warning class');

assert.equal(ClientsList.nodes[4].status.text, 'draining');
assert.ok(ClientsList.nodes[4].status.isInfo, 'expected info class');
});

test('each client should link to the client detail page', async function(assert) {
minimumSetup();
server.createList('node', 1);
server.createList('agent', 1);

const node = server.db.nodes[0];

await ClientsList.visit();
Expand Down Expand Up @@ -115,15 +162,27 @@ module('Acceptance | clients list', function(hooks) {
testFacet('Status', {
facet: ClientsList.facets.status,
paramName: 'status',
expectedOptions: ['Initializing', 'Ready', 'Down'],
expectedOptions: ['Initializing', 'Ready', 'Down', 'Ineligible', 'Draining'],
async beforeEach() {
server.create('agent');

server.createList('node', 2, { status: 'initializing' });
server.createList('node', 2, { status: 'ready' });
server.createList('node', 2, { status: 'down' });

server.createList('node', 2, { schedulingEligibility: 'eligible', drain: false });
server.createList('node', 2, { schedulingEligibility: 'ineligible', drain: false });
server.createList('node', 2, { schedulingEligibility: 'ineligible', drain: true });

await ClientsList.visit();
},
filter: (node, selection) => selection.includes(node.status),
filter: (node, selection) => {
if (selection.includes('draining') && !node.drain) return false;
if (selection.includes('ineligible') && node.schedulingEligibility === 'eligible')
return false;

return selection.includes(node.status);
},
backspace marked this conversation as resolved.
Show resolved Hide resolved
});

testFacet('Datacenters', {
Expand All @@ -142,25 +201,6 @@ module('Acceptance | clients list', function(hooks) {
filter: (node, selection) => selection.includes(node.datacenter),
});

testFacet('Flags', {
facet: ClientsList.facets.flags,
paramName: 'flags',
expectedOptions: ['Ineligible', 'Draining'],
async beforeEach() {
server.create('agent');
server.createList('node', 2, { schedulingEligibility: 'eligible', drain: false });
server.createList('node', 2, { schedulingEligibility: 'ineligible', drain: false });
server.createList('node', 2, { schedulingEligibility: 'ineligible', drain: true });
await ClientsList.visit();
},
filter: (node, selection) => {
if (selection.includes('draining') && !node.drain) return false;
if (selection.includes('ineligible') && node.schedulingEligibility === 'eligible')
return false;
return true;
},
});

test('when the facet selections result in no matches, the empty state states why', async function(assert) {
server.create('agent');
server.createList('node', 2, { status: 'ready' });
Expand Down
Loading