mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
[ui] node eligibilty taken into consideration when clients list filtered to "ready" (#18607)
* node eligibilty taken into consideration when clients list filtered to 'ready' * A working draft of complex positive querying * tags and filter badge * CompositeStatus -> Status * Buttons within a Helios SegmentedGroup * Convert the other dropdowns to helios on clients index * A bunch of client index test fixes * Remaining clients list acceptance tests for State facet modified
This commit is contained in:
3
.changelog/18607.txt
Normal file
3
.changelog/18607.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
ui: change the State filter on clients page to split out eligibility and drain status
|
||||
```
|
||||
@@ -57,37 +57,31 @@ export default class ClientNodeRow extends Component.extend(
|
||||
|
||||
@watchRelationship('allocations') watch;
|
||||
|
||||
@computed('node.compositeStatus')
|
||||
@computed('node.status')
|
||||
get nodeStatusColor() {
|
||||
let compositeStatus = this.get('node.compositeStatus');
|
||||
|
||||
if (compositeStatus === 'draining') {
|
||||
return 'neutral';
|
||||
} else if (compositeStatus === 'ineligible') {
|
||||
let status = this.get('node.status');
|
||||
if (status === 'disconnected') {
|
||||
return 'warning';
|
||||
} else if (compositeStatus === 'down') {
|
||||
} else if (status === 'down') {
|
||||
return 'critical';
|
||||
} else if (compositeStatus === 'ready') {
|
||||
} else if (status === 'ready') {
|
||||
return 'success';
|
||||
} else if (compositeStatus === 'initializing') {
|
||||
} else if (status === 'initializing') {
|
||||
return 'neutral';
|
||||
} else {
|
||||
return 'neutral';
|
||||
}
|
||||
}
|
||||
@computed('node.compositeStatus')
|
||||
@computed('node.status')
|
||||
get nodeStatusIcon() {
|
||||
let compositeStatus = this.get('node.compositeStatus');
|
||||
|
||||
if (compositeStatus === 'draining') {
|
||||
return 'minus-circle';
|
||||
} else if (compositeStatus === 'ineligible') {
|
||||
let status = this.get('node.status');
|
||||
if (status === 'disconnected') {
|
||||
return 'skip';
|
||||
} else if (compositeStatus === 'down') {
|
||||
} else if (status === 'down') {
|
||||
return 'x-circle';
|
||||
} else if (compositeStatus === 'ready') {
|
||||
} else if (status === 'ready') {
|
||||
return 'check-circle';
|
||||
} else if (compositeStatus === 'initializing') {
|
||||
} else if (status === 'initializing') {
|
||||
return 'entry-point';
|
||||
} else {
|
||||
return '';
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */
|
||||
import { alias, readOnly } from '@ember/object/computed';
|
||||
import { inject as service } from '@ember/service';
|
||||
@@ -45,9 +47,6 @@ export default class IndexController extends Controller.extend(
|
||||
{
|
||||
qpClass: 'class',
|
||||
},
|
||||
{
|
||||
qpState: 'state',
|
||||
},
|
||||
{
|
||||
qpDatacenter: 'dc',
|
||||
},
|
||||
@@ -62,6 +61,110 @@ export default class IndexController extends Controller.extend(
|
||||
},
|
||||
];
|
||||
|
||||
filterFunc = (node) => {
|
||||
return node.isEligible;
|
||||
};
|
||||
|
||||
clientFilterToggles = {
|
||||
state: [
|
||||
{
|
||||
label: 'initializing',
|
||||
qp: 'state_initializing',
|
||||
default: true,
|
||||
filter: (node) => node.status === 'initializing',
|
||||
},
|
||||
{
|
||||
label: 'ready',
|
||||
qp: 'state_ready',
|
||||
default: true,
|
||||
filter: (node) => node.status === 'ready',
|
||||
},
|
||||
{
|
||||
label: 'down',
|
||||
qp: 'state_down',
|
||||
default: true,
|
||||
filter: (node) => node.status === 'down',
|
||||
},
|
||||
{
|
||||
label: 'disconnected',
|
||||
qp: 'state_disconnected',
|
||||
default: true,
|
||||
filter: (node) => node.status === 'disconnected',
|
||||
},
|
||||
],
|
||||
eligibility: [
|
||||
{
|
||||
label: 'eligible',
|
||||
qp: 'eligibility_eligible',
|
||||
default: true,
|
||||
filter: (node) => node.isEligible,
|
||||
},
|
||||
{
|
||||
label: 'ineligible',
|
||||
qp: 'eligibility_ineligible',
|
||||
default: true,
|
||||
filter: (node) => !node.isEligible,
|
||||
},
|
||||
],
|
||||
drainStatus: [
|
||||
{
|
||||
label: 'draining',
|
||||
qp: 'drain_status_draining',
|
||||
default: true,
|
||||
filter: (node) => node.isDraining,
|
||||
},
|
||||
{
|
||||
label: 'not draining',
|
||||
qp: 'drain_status_not_draining',
|
||||
default: true,
|
||||
filter: (node) => !node.isDraining,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@computed(
|
||||
'state_initializing',
|
||||
'state_ready',
|
||||
'state_down',
|
||||
'state_disconnected',
|
||||
'eligibility_eligible',
|
||||
'eligibility_ineligible',
|
||||
'drain_status_draining',
|
||||
'drain_status_not_draining',
|
||||
'allToggles.[]'
|
||||
)
|
||||
get activeToggles() {
|
||||
return this.allToggles.filter((t) => this[t.qp]);
|
||||
}
|
||||
|
||||
get allToggles() {
|
||||
return Object.values(this.clientFilterToggles).reduce(
|
||||
(acc, filters) => acc.concat(filters),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line ember/classic-decorator-hooks
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.addDynamicQueryParams();
|
||||
}
|
||||
|
||||
addDynamicQueryParams() {
|
||||
this.clientFilterToggles.state.forEach((filter) => {
|
||||
this.queryParams.push({ [filter.qp]: filter.qp });
|
||||
this.set(filter.qp, filter.default);
|
||||
});
|
||||
this.clientFilterToggles.eligibility.forEach((filter) => {
|
||||
this.queryParams.push({ [filter.qp]: filter.qp });
|
||||
this.set(filter.qp, filter.default);
|
||||
});
|
||||
this.clientFilterToggles.drainStatus.forEach((filter) => {
|
||||
this.queryParams.push({ [filter.qp]: filter.qp });
|
||||
this.set(filter.qp, filter.default);
|
||||
});
|
||||
}
|
||||
|
||||
currentPage = 1;
|
||||
@readOnly('userSettings.pageSize') pageSize;
|
||||
|
||||
@@ -74,14 +177,12 @@ export default class IndexController extends Controller.extend(
|
||||
}
|
||||
|
||||
qpClass = '';
|
||||
qpState = '';
|
||||
qpDatacenter = '';
|
||||
qpVersion = '';
|
||||
qpVolume = '';
|
||||
qpNodePool = '';
|
||||
|
||||
@selection('qpClass') selectionClass;
|
||||
@selection('qpState') selectionState;
|
||||
@selection('qpDatacenter') selectionDatacenter;
|
||||
@selection('qpVersion') selectionVersion;
|
||||
@selection('qpVolume') selectionVolume;
|
||||
@@ -105,18 +206,6 @@ export default class IndexController extends Controller.extend(
|
||||
return classes.sort().map((dc) => ({ key: dc, label: dc }));
|
||||
}
|
||||
|
||||
@computed
|
||||
get optionsState() {
|
||||
return [
|
||||
{ key: 'initializing', label: 'Initializing' },
|
||||
{ key: 'ready', label: 'Ready' },
|
||||
{ key: 'down', label: 'Down' },
|
||||
{ key: 'ineligible', label: 'Ineligible' },
|
||||
{ key: 'draining', label: 'Draining' },
|
||||
{ key: 'disconnected', label: 'Disconnected' },
|
||||
];
|
||||
}
|
||||
|
||||
@computed('nodes.[]', 'selectionDatacenter')
|
||||
get optionsDatacenter() {
|
||||
const datacenters = Array.from(
|
||||
@@ -195,35 +284,50 @@ export default class IndexController extends Controller.extend(
|
||||
}
|
||||
|
||||
@computed(
|
||||
'clientFilterToggles',
|
||||
'drain_status_draining',
|
||||
'drain_status_not_draining',
|
||||
'eligibility_eligible',
|
||||
'eligibility_ineligible',
|
||||
'nodes.[]',
|
||||
'selectionClass',
|
||||
'selectionState',
|
||||
'selectionDatacenter',
|
||||
'selectionNodePool',
|
||||
'selectionVersion',
|
||||
'selectionVolume'
|
||||
'selectionVolume',
|
||||
'state_disconnected',
|
||||
'state_down',
|
||||
'state_initializing',
|
||||
'state_ready'
|
||||
)
|
||||
get filteredNodes() {
|
||||
const {
|
||||
selectionClass: classes,
|
||||
selectionState: states,
|
||||
selectionDatacenter: datacenters,
|
||||
selectionNodePool: nodePools,
|
||||
selectionVersion: versions,
|
||||
selectionVolume: volumes,
|
||||
} = this;
|
||||
|
||||
const onlyIneligible = states.includes('ineligible');
|
||||
const onlyDraining = states.includes('draining');
|
||||
let nodes = this.nodes;
|
||||
|
||||
// states is a composite of node status and other node states
|
||||
const statuses = states.without('ineligible').without('draining');
|
||||
// new QP style filtering
|
||||
for (let category in this.clientFilterToggles) {
|
||||
nodes = nodes.filter((node) => {
|
||||
let includeNode = false;
|
||||
for (let filter of this.clientFilterToggles[category]) {
|
||||
if (this[filter.qp] && filter.filter(node)) {
|
||||
includeNode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return includeNode;
|
||||
});
|
||||
}
|
||||
|
||||
return this.nodes.filter((node) => {
|
||||
return nodes.filter((node) => {
|
||||
if (classes.length && !classes.includes(node.get('nodeClass')))
|
||||
return false;
|
||||
if (statuses.length && !statuses.includes(node.get('status')))
|
||||
return false;
|
||||
if (datacenters.length && !datacenters.includes(node.get('datacenter')))
|
||||
return false;
|
||||
if (versions.length && !versions.includes(node.get('version')))
|
||||
@@ -237,9 +341,6 @@ export default class IndexController extends Controller.extend(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onlyIneligible && node.get('isEligible')) return false;
|
||||
if (onlyDraining && !node.get('isDraining')) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -254,6 +355,16 @@ export default class IndexController extends Controller.extend(
|
||||
this.set(queryParam, serialize(selection));
|
||||
}
|
||||
|
||||
@action
|
||||
handleFilterChange(queryParamValue, option, queryParamLabel) {
|
||||
if (queryParamValue.includes(option)) {
|
||||
queryParamValue.removeObject(option);
|
||||
} else {
|
||||
queryParamValue.addObject(option);
|
||||
}
|
||||
this.set(queryParamLabel, serialize(queryParamValue));
|
||||
}
|
||||
|
||||
@action
|
||||
gotoNode(node) {
|
||||
this.transitionToRoute('clients.client', node);
|
||||
|
||||
@@ -110,6 +110,12 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.node-status-badges {
|
||||
.hds-badge__text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-narrow {
|
||||
padding: 1.25em 0 1.25em 0.5em;
|
||||
|
||||
|
||||
@@ -18,52 +18,190 @@
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="toolbar-item is-right-aligned is-mobile-full-width">
|
||||
<div class="button-bar">
|
||||
<MultiSelectDropdown
|
||||
data-test-state-facet
|
||||
@label="State"
|
||||
@options={{this.optionsState}}
|
||||
@selection={{this.selectionState}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpState"}}
|
||||
|
||||
<Hds::SegmentedGroup as |S|>
|
||||
<S.Dropdown data-test-state-facet as |dd|>
|
||||
<dd.ToggleButton
|
||||
@text="State"
|
||||
@color="secondary"
|
||||
@badge={{if (eq this.activeToggles.length this.allToggles.length) false this.activeToggles.length}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
data-test-node-pool-facet
|
||||
@label="Node Pool"
|
||||
@options={{this.optionsNodePool}}
|
||||
@selection={{this.selectionNodePool}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpNodePool"}}
|
||||
<dd.Title @text="Status" />
|
||||
{{#each this.clientFilterToggles.state as |option|}}
|
||||
<dd.Checkbox
|
||||
{{on "change" (toggle option.qp this)}}
|
||||
@value={{option.label}}
|
||||
@count={{get (filter (action option.filter) this.nodes) "length"}}
|
||||
checked={{get this option.qp}}
|
||||
data-test-dropdown-option={{option.label}}
|
||||
>
|
||||
{{capitalize option.label}}
|
||||
</dd.Checkbox>
|
||||
{{/each}}
|
||||
<dd.Separator />
|
||||
<dd.Title @text="Eligibility" />
|
||||
{{#each this.clientFilterToggles.eligibility as |option|}}
|
||||
<dd.Checkbox
|
||||
{{on "change" (toggle option.qp this)}}
|
||||
@value={{option.label}}
|
||||
@count={{get (filter (action option.filter) this.nodes) "length"}}
|
||||
checked={{get this option.qp}}
|
||||
data-test-dropdown-option={{option.label}}
|
||||
>
|
||||
{{capitalize option.label}}
|
||||
</dd.Checkbox>
|
||||
{{/each}}
|
||||
<dd.Separator />
|
||||
<dd.Title @text="Drain Status" />
|
||||
{{#each this.clientFilterToggles.drainStatus as |option|}}
|
||||
<dd.Checkbox
|
||||
{{on "change" (toggle option.qp this)}}
|
||||
@value={{option.label}}
|
||||
@count={{get (filter (action option.filter) this.nodes) "length"}}
|
||||
checked={{get this option.qp}}
|
||||
data-test-dropdown-option={{option.label}}
|
||||
>
|
||||
{{capitalize option.label}}
|
||||
</dd.Checkbox>
|
||||
{{/each}}
|
||||
</S.Dropdown>
|
||||
|
||||
<S.Dropdown data-test-node-pool-facet as |dd|>
|
||||
<dd.ToggleButton
|
||||
@text="Node Pool"
|
||||
@color="secondary"
|
||||
@badge={{or this.selectionNodePool.length false}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
data-test-class-facet
|
||||
@label="Class"
|
||||
@options={{this.optionsClass}}
|
||||
@selection={{this.selectionClass}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpClass"}}
|
||||
{{#each this.optionsNodePool key="label" as |option|}}
|
||||
<dd.Checkbox
|
||||
{{on "change" (action this.handleFilterChange
|
||||
this.selectionNodePool
|
||||
option.label
|
||||
"qpNodePool"
|
||||
)}}
|
||||
@value={{option.label}}
|
||||
checked={{includes option.label this.selectionNodePool}}
|
||||
@count={{get (filter-by 'nodePool' option.label this.nodes) "length"}}
|
||||
data-test-dropdown-option={{option.label}}
|
||||
>
|
||||
{{option.label}}
|
||||
</dd.Checkbox>
|
||||
{{else}}
|
||||
<dd.Generic data-test-dropdown-empty>
|
||||
No Node Pool filters
|
||||
</dd.Generic>
|
||||
{{/each}}
|
||||
</S.Dropdown>
|
||||
|
||||
<S.Dropdown data-test-class-facet as |dd|>
|
||||
<dd.ToggleButton
|
||||
@text="Class"
|
||||
@color="secondary"
|
||||
@badge={{or this.selectionClass.length false}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
data-test-datacenter-facet
|
||||
@label="Datacenter"
|
||||
@options={{this.optionsDatacenter}}
|
||||
@selection={{this.selectionDatacenter}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpDatacenter"}}
|
||||
{{#each this.optionsClass key="label" as |option|}}
|
||||
<dd.Checkbox
|
||||
{{on "change" (action this.handleFilterChange
|
||||
this.selectionClass
|
||||
option.label
|
||||
"qpClass"
|
||||
)}}
|
||||
@value={{option.label}}
|
||||
checked={{includes option.label this.selectionClass}}
|
||||
@count={{get (filter-by 'nodeClass' option.label this.nodes) "length"}}
|
||||
data-test-dropdown-option={{option.label}}
|
||||
>
|
||||
{{option.label}}
|
||||
</dd.Checkbox>
|
||||
{{else}}
|
||||
<dd.Generic data-test-dropdown-empty>
|
||||
No Class filters
|
||||
</dd.Generic>
|
||||
{{/each}}
|
||||
</S.Dropdown>
|
||||
|
||||
<S.Dropdown data-test-datacenter-facet as |dd|>
|
||||
<dd.ToggleButton
|
||||
@text="Datacenter"
|
||||
@color="secondary"
|
||||
@badge={{or this.selectionDatacenter.length false}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
data-test-version-facet
|
||||
@label="Version"
|
||||
@options={{this.optionsVersion}}
|
||||
@selection={{this.selectionVersion}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpVersion"}}
|
||||
{{#each this.optionsDatacenter key="label" as |option|}}
|
||||
<dd.Checkbox
|
||||
{{on "change" (action this.handleFilterChange
|
||||
this.selectionDatacenter
|
||||
option.label
|
||||
"qpDatacenter"
|
||||
)}}
|
||||
@value={{option.label}}
|
||||
checked={{includes option.label this.selectionDatacenter}}
|
||||
@count={{get (filter-by 'datacenter' option.label this.nodes) "length"}}
|
||||
data-test-dropdown-option={{option.label}}
|
||||
>
|
||||
{{option.label}}
|
||||
</dd.Checkbox>
|
||||
{{else}}
|
||||
<dd.Generic data-test-dropdown-empty>
|
||||
No Datacenter filters
|
||||
</dd.Generic>
|
||||
{{/each}}
|
||||
|
||||
</S.Dropdown>
|
||||
|
||||
<S.Dropdown data-test-version-facet as |dd|>
|
||||
<dd.ToggleButton
|
||||
@text="Version"
|
||||
@color="secondary"
|
||||
@badge={{or this.selectionVersion.length false}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
data-test-volume-facet
|
||||
@label="Volume"
|
||||
@options={{this.optionsVolume}}
|
||||
@selection={{this.selectionVolume}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpVolume"}}
|
||||
{{#each this.optionsVersion key="label" as |option|}}
|
||||
<dd.Checkbox
|
||||
{{on "change" (action this.handleFilterChange
|
||||
this.selectionVersion
|
||||
option.label
|
||||
"qpVersion"
|
||||
)}}
|
||||
@value={{option.label}}
|
||||
checked={{includes option.label this.selectionVersion}}
|
||||
@count={{get (filter-by 'version' option.label this.nodes) "length"}}
|
||||
data-test-dropdown-option={{option.label}}
|
||||
>
|
||||
{{option.label}}
|
||||
</dd.Checkbox>
|
||||
{{else}}
|
||||
<dd.Generic data-test-dropdown-empty>
|
||||
No Version filters
|
||||
</dd.Generic>
|
||||
{{/each}}
|
||||
</S.Dropdown>
|
||||
|
||||
<S.Dropdown data-test-volume-facet as |dd|>
|
||||
<dd.ToggleButton
|
||||
@text="Volume"
|
||||
@color="secondary"
|
||||
@badge={{or this.selectionVolume.length false}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{#each this.optionsVolume key="label" as |option|}}
|
||||
<dd.Checkbox
|
||||
{{on "change" (action this.handleFilterChange
|
||||
this.selectionVolume
|
||||
option.label
|
||||
"qpVolume"
|
||||
)}}
|
||||
@value={{option.label}}
|
||||
checked={{includes option.label this.selectionVolume}}
|
||||
@count={{get (filter-by 'volume' option.label this.nodes) "length"}}
|
||||
data-test-dropdown-option={{option.label}}
|
||||
>
|
||||
{{option.label}}
|
||||
</dd.Checkbox>
|
||||
{{else}}
|
||||
<dd.Generic data-test-dropdown-empty>
|
||||
No Volume filters
|
||||
</dd.Generic>
|
||||
{{/each}}
|
||||
</S.Dropdown>
|
||||
</Hds::SegmentedGroup>
|
||||
</div>
|
||||
{{#if this.sortedNodes}}
|
||||
<ListPagination
|
||||
@@ -86,7 +224,7 @@
|
||||
@class="is-200px is-truncatable"
|
||||
@prop="name"
|
||||
>Name</t.sort-by>
|
||||
<t.sort-by @prop="compositeStatus">State</t.sort-by>
|
||||
<t.sort-by @prop="status">State</t.sort-by>
|
||||
<th class="is-200px is-truncatable">Address</th>
|
||||
<t.sort-by @prop="nodePool">Node Pool</t.sort-by>
|
||||
<t.sort-by @prop="datacenter">Datacenter</t.sort-by>
|
||||
|
||||
@@ -12,15 +12,41 @@
|
||||
</td>
|
||||
<td data-test-client-id><LinkTo @route="clients.client" @model={{this.node.id}} class="is-primary">{{this.node.shortId}}</LinkTo></td>
|
||||
<td data-test-client-name class="is-200px is-truncatable" title="{{this.node.name}}">{{this.node.name}}</td>
|
||||
<td data-test-client-composite-status>
|
||||
<span class="tooltip" aria-label="{{this.node.status}} / {{if this.node.isDraining "draining" "not draining"}} / {{if this.node.isEligible "eligible" "not eligible"}}">
|
||||
<td class="node-status-badges" data-test-client-composite-status>
|
||||
<Hds::Badge
|
||||
@text={{capitalize this.node.status}}
|
||||
@icon={{this.nodeStatusIcon}}
|
||||
@color={{this.nodeStatusColor}}
|
||||
@size="small"
|
||||
/>
|
||||
|
||||
{{#if this.node.isEligible}}
|
||||
<Hds::Badge
|
||||
@text={{capitalize this.node.compositeStatus}}
|
||||
@icon={{this.nodeStatusIcon}}
|
||||
@color={{this.nodeStatusColor}}
|
||||
@size="large"
|
||||
@text="Eligible"
|
||||
@color="neutral"
|
||||
@size="small"
|
||||
/>
|
||||
</span>
|
||||
{{else}}
|
||||
<Hds::Badge
|
||||
@text="Ineligible"
|
||||
@color="neutral"
|
||||
@size="small"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.node.isDraining}}
|
||||
<Hds::Badge
|
||||
@text="Draining"
|
||||
@color="neutral"
|
||||
@size="small"
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::Badge
|
||||
@text="Not Draining"
|
||||
@color="neutral"
|
||||
@size="small"
|
||||
/>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-test-client-address class="is-200px is-truncatable">{{this.node.httpAddr}}</td>
|
||||
<td data-test-client-node-pool title="{{this.node.nodePool}}">
|
||||
|
||||
@@ -77,7 +77,7 @@ module('Acceptance | clients list', function (hooks) {
|
||||
assert.equal(nodeRow.nodePool, node.nodePool, 'Node Pool');
|
||||
assert.equal(
|
||||
nodeRow.compositeStatus.text,
|
||||
'Draining',
|
||||
'Ready Ineligible Draining',
|
||||
'Combined status, draining, and eligbility'
|
||||
);
|
||||
assert.equal(nodeRow.address, node.httpAddr);
|
||||
@@ -111,13 +111,13 @@ module('Acceptance | clients list', function (hooks) {
|
||||
assert.equal(nodeRow.id, node.id.split('-')[0], 'ID');
|
||||
assert.equal(
|
||||
nodeRow.compositeStatus.text,
|
||||
'Ready',
|
||||
'Ready Eligible Not Draining',
|
||||
'Combined status, draining, and eligbility'
|
||||
);
|
||||
assert.equal(nodeRow.allocations, running.length, '# Allocations');
|
||||
});
|
||||
|
||||
test('client status, draining, and eligibility are collapsed into one column that stays sorted', async function (assert) {
|
||||
test('client status, draining, and eligibility are combined into one column that stays sorted on status', async function (assert) {
|
||||
server.createList('agent', 1);
|
||||
|
||||
server.create('node', {
|
||||
@@ -151,47 +151,71 @@ module('Acceptance | clients list', function (hooks) {
|
||||
drain: false,
|
||||
});
|
||||
server.create('node', 'draining', {
|
||||
schedulingEligibility: 'eligible',
|
||||
modifyIndex: 0,
|
||||
status: 'ready',
|
||||
});
|
||||
|
||||
await ClientsList.visit();
|
||||
ClientsList.nodes[0].compositeStatus.as((readyClient) => {
|
||||
assert.equal(readyClient.text, 'Ready');
|
||||
console.log('readyClient', readyClient.text);
|
||||
assert.equal(readyClient.tooltip, 'ready / not draining / eligible');
|
||||
});
|
||||
|
||||
assert.equal(ClientsList.nodes[1].compositeStatus.text, 'Initializing');
|
||||
assert.equal(ClientsList.nodes[2].compositeStatus.text, 'Down');
|
||||
assert.equal(
|
||||
ClientsList.nodes[0].compositeStatus.text,
|
||||
'Ready Eligible Not Draining'
|
||||
);
|
||||
assert.equal(
|
||||
ClientsList.nodes[1].compositeStatus.text,
|
||||
'Initializing Eligible Not Draining'
|
||||
);
|
||||
assert.equal(
|
||||
ClientsList.nodes[2].compositeStatus.text,
|
||||
'Down',
|
||||
'down takes priority over ineligible'
|
||||
'Down Eligible Not Draining'
|
||||
);
|
||||
assert.equal(
|
||||
ClientsList.nodes[3].compositeStatus.text,
|
||||
'Down Ineligible Not Draining'
|
||||
);
|
||||
assert.equal(
|
||||
ClientsList.nodes[4].compositeStatus.text,
|
||||
'Ready Ineligible Not Draining'
|
||||
);
|
||||
assert.equal(
|
||||
ClientsList.nodes[5].compositeStatus.text,
|
||||
'Ready Eligible Draining'
|
||||
);
|
||||
assert.equal(ClientsList.nodes[4].compositeStatus.text, 'Ineligible');
|
||||
|
||||
assert.equal(ClientsList.nodes[5].compositeStatus.text, 'Draining');
|
||||
|
||||
await ClientsList.sortBy('compositeStatus');
|
||||
await ClientsList.sortBy('status');
|
||||
|
||||
assert.deepEqual(
|
||||
ClientsList.nodes.map((n) => n.compositeStatus.text),
|
||||
['Ready', 'Initializing', 'Ineligible', 'Draining', 'Down', 'Down']
|
||||
[
|
||||
'Ready Eligible Draining',
|
||||
'Ready Ineligible Not Draining',
|
||||
'Ready Eligible Not Draining',
|
||||
'Initializing Eligible Not Draining',
|
||||
'Down Ineligible Not Draining',
|
||||
'Down Eligible Not Draining',
|
||||
],
|
||||
'Nodes are sorted only by status, and otherwise default to modifyIndex'
|
||||
);
|
||||
|
||||
// Simulate a client state change arriving through polling
|
||||
let readyClient = this.owner
|
||||
let discoClient = this.owner
|
||||
.lookup('service:store')
|
||||
.peekAll('node')
|
||||
.findBy('modifyIndex', 5);
|
||||
readyClient.set('schedulingEligibility', 'ineligible');
|
||||
discoClient.set('status', 'disconnected');
|
||||
|
||||
await settled();
|
||||
|
||||
assert.deepEqual(
|
||||
ClientsList.nodes.map((n) => n.compositeStatus.text),
|
||||
['Initializing', 'Ineligible', 'Ineligible', 'Draining', 'Down', 'Down']
|
||||
[
|
||||
'Ready Eligible Draining',
|
||||
'Ready Ineligible Not Draining',
|
||||
'Disconnected Eligible Not Draining',
|
||||
'Initializing Eligible Not Draining',
|
||||
'Down Ineligible Not Draining',
|
||||
'Down Eligible Not Draining',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -275,12 +299,14 @@ module('Acceptance | clients list', function (hooks) {
|
||||
facet: ClientsList.facets.state,
|
||||
paramName: 'state',
|
||||
expectedOptions: [
|
||||
'Initializing',
|
||||
'Ready',
|
||||
'Down',
|
||||
'Ineligible',
|
||||
'Draining',
|
||||
'Disconnected',
|
||||
'initializing',
|
||||
'ready',
|
||||
'down',
|
||||
'disconnected',
|
||||
'eligible',
|
||||
'ineligible',
|
||||
'draining',
|
||||
'not draining',
|
||||
],
|
||||
async beforeEach() {
|
||||
server.create('agent');
|
||||
@@ -312,7 +338,7 @@ module('Acceptance | clients list', function (hooks) {
|
||||
)
|
||||
return false;
|
||||
|
||||
return selection.includes(node.status);
|
||||
return !selection.includes(node.status);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -400,9 +426,8 @@ module('Acceptance | clients list', function (hooks) {
|
||||
server.createList('node', 2, { status: 'ready' });
|
||||
|
||||
await ClientsList.visit();
|
||||
|
||||
await ClientsList.facets.state.toggle();
|
||||
await ClientsList.facets.state.options.objectAt(0).toggle();
|
||||
await ClientsList.facets.state.options.objectAt(1).toggle();
|
||||
assert.ok(ClientsList.isEmpty, 'There is an empty message');
|
||||
assert.equal(
|
||||
ClientsList.empty.headline,
|
||||
@@ -441,7 +466,9 @@ module('Acceptance | clients list', function (hooks) {
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
facet.options.map((option) => option.label.trim()),
|
||||
facet.options.map((option) => {
|
||||
return option.key.trim();
|
||||
}),
|
||||
expectation,
|
||||
'Options for facet are as expected'
|
||||
);
|
||||
@@ -511,11 +538,20 @@ module('Acceptance | clients list', function (hooks) {
|
||||
await option2.toggle();
|
||||
selection.push(option2.key);
|
||||
|
||||
// State is different from the other facets, in that it is an "exclusive" filter, whete others are "inclusive".
|
||||
// Because of this, it doesn't pass "state" as a stringified-array query param; rather, exclusion is indicated
|
||||
// for each option with a "${optionName}=false" query param.
|
||||
|
||||
const stateString = `/clients?${selection
|
||||
.map((option) => `state_${option}=false`)
|
||||
.join('&')}`;
|
||||
const nonStateString = `/clients?${paramName}=${encodeURIComponent(
|
||||
JSON.stringify(selection)
|
||||
)}`;
|
||||
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/clients?${paramName}=${encodeURIComponent(
|
||||
JSON.stringify(selection)
|
||||
)}`,
|
||||
paramName === 'state' ? stateString : nonStateString,
|
||||
'URL has the correct query param key and value'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,9 +16,24 @@ import {
|
||||
visitable,
|
||||
} from 'ember-cli-page-object';
|
||||
|
||||
import { multiFacet } from 'nomad-ui/tests/pages/components/facet';
|
||||
import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select';
|
||||
|
||||
const heliosFacet = (scope) => ({
|
||||
scope,
|
||||
toggle: clickable('button'),
|
||||
options: collection(
|
||||
'.hds-menu-primitive__content .hds-dropdown__content .hds-dropdown__list .hds-dropdown-list-item--variant-checkbox',
|
||||
{
|
||||
toggle: clickable('label'),
|
||||
count: text('label .hds-dropdown-list-item__count'),
|
||||
key: attribute(
|
||||
'data-test-dropdown-option',
|
||||
'[data-test-dropdown-option]'
|
||||
),
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export default create({
|
||||
pageSize: 25,
|
||||
|
||||
@@ -77,11 +92,11 @@ export default create({
|
||||
},
|
||||
|
||||
facets: {
|
||||
nodePools: multiFacet('[data-test-node-pool-facet]'),
|
||||
class: multiFacet('[data-test-class-facet]'),
|
||||
state: multiFacet('[data-test-state-facet]'),
|
||||
datacenter: multiFacet('[data-test-datacenter-facet]'),
|
||||
version: multiFacet('[data-test-version-facet]'),
|
||||
volume: multiFacet('[data-test-volume-facet]'),
|
||||
nodePools: heliosFacet('[data-test-node-pool-facet]'),
|
||||
class: heliosFacet('[data-test-class-facet]'),
|
||||
state: heliosFacet('[data-test-state-facet]'),
|
||||
datacenter: heliosFacet('[data-test-datacenter-facet]'),
|
||||
version: heliosFacet('[data-test-version-facet]'),
|
||||
volume: heliosFacet('[data-test-volume-facet]'),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user