ui: [BUGFIX] Ensure namespace is used for node API requests (#9410)
Nodes themselves are not namespaced, so we'd originally assumed we did not need to pass through the ns query parameter when listing or viewing nodes. As it turns out the API endpoints we use to list and view nodes (and related things) return things that are namespaced, therefore any API requests for nodes do require a the ns query parameter to be passed through to the request. This PR adds the necessary ns query param to all things Node, apart from the querying for the leader which only returns node related information. Additionally here we decided to show 0 Services text in the node listing if there are nodes with no service instances within the namespace you are viewing, as this is clearer than showing nothing at all. We also cleaned up/standardized the text we use to in the empty state for service instances.
This commit is contained in:
parent
b5498642d0
commit
dc89b8d340
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
ui: ensure namespace is used for node API requests
|
||||||
|
```
|
|
@ -2,17 +2,27 @@ import Adapter from './application';
|
||||||
|
|
||||||
// TODO: Update to use this.formatDatacenter()
|
// TODO: Update to use this.formatDatacenter()
|
||||||
|
|
||||||
|
// Node and Namespaces are a little strange in that Nodes don't belong in a
|
||||||
|
// namespace whereas things that belong to a Node do (Health Checks and
|
||||||
|
// Service Instances). So even though Nodes themselves don't require a
|
||||||
|
// namespace filter, you sill needs to pass the namespace through to API
|
||||||
|
// requests in order to get the correct information for the things that belong
|
||||||
|
// to the node.
|
||||||
|
|
||||||
export default class NodeAdapter extends Adapter {
|
export default class NodeAdapter extends Adapter {
|
||||||
requestForQuery(request, { dc, index, id, uri }) {
|
requestForQuery(request, { dc, ns, index, id, uri }) {
|
||||||
return request`
|
return request`
|
||||||
GET /v1/internal/ui/nodes?${{ dc }}
|
GET /v1/internal/ui/nodes?${{ dc }}
|
||||||
X-Request-ID: ${uri}
|
X-Request-ID: ${uri}
|
||||||
|
|
||||||
${{ index }}
|
${{
|
||||||
|
...this.formatNspace(ns),
|
||||||
|
index,
|
||||||
|
}}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
requestForQueryRecord(request, { dc, index, id, uri }) {
|
requestForQueryRecord(request, { dc, ns, index, id, uri }) {
|
||||||
if (typeof id === 'undefined') {
|
if (typeof id === 'undefined') {
|
||||||
throw new Error('You must specify an id');
|
throw new Error('You must specify an id');
|
||||||
}
|
}
|
||||||
|
@ -20,7 +30,10 @@ export default class NodeAdapter extends Adapter {
|
||||||
GET /v1/internal/ui/node/${id}?${{ dc }}
|
GET /v1/internal/ui/node/${id}?${{ dc }}
|
||||||
X-Request-ID: ${uri}
|
X-Request-ID: ${uri}
|
||||||
|
|
||||||
${{ index }}
|
${{
|
||||||
|
...this.formatNspace(ns),
|
||||||
|
index,
|
||||||
|
}}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,11 +29,9 @@ as |item index|>
|
||||||
{{#if (eq item.Address @leader.Address)}}
|
{{#if (eq item.Address @leader.Address)}}
|
||||||
<span class="leader" data-test-leader={{@leader.Address}}>Leader</span>
|
<span class="leader" data-test-leader={{@leader.Address}}>Leader</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if (gt item.Services.length 0)}}
|
|
||||||
<span>
|
<span>
|
||||||
{{item.Services.length}} {{pluralize item.Services.length 'Service' without-count=true}}
|
{{format-number item.Services.length}} {{pluralize item.Services.length 'Service' without-count=true}}
|
||||||
</span>
|
</span>
|
||||||
{{/if}}
|
|
||||||
<dl>
|
<dl>
|
||||||
<dt>
|
<dt>
|
||||||
<span>Address</span>
|
<span>Address</span>
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default class IndexRoute extends Route {
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
const dc = this.modelFor('dc').dc.Name;
|
||||||
const nspace = '*';
|
const nspace = this.modelFor('nspace').nspace.substr(1);
|
||||||
return hash({
|
return hash({
|
||||||
items: this.data.source(uri => uri`/${nspace}/${dc}/nodes`),
|
items: this.data.source(uri => uri`/${nspace}/${dc}/nodes`),
|
||||||
leader: this.data.source(uri => uri`/${nspace}/${dc}/leader`),
|
leader: this.data.source(uri => uri`/${nspace}/${dc}/leader`),
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
@sort={{sort}}
|
@sort={{sort}}
|
||||||
@filters={{filters}}
|
@filters={{filters}}
|
||||||
@search={{search}}
|
@search={{search}}
|
||||||
@items={{reject-by 'Service.Kind' 'connect-proxy' items}}
|
@items={{items}}
|
||||||
as |collection|>
|
as |collection|>
|
||||||
<collection.Collection>
|
<collection.Collection>
|
||||||
<Consul::ServiceInstance::List
|
<Consul::ServiceInstance::List
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
<EmptyState>
|
<EmptyState>
|
||||||
<BlockSlot @name="body">
|
<BlockSlot @name="body">
|
||||||
<p>
|
<p>
|
||||||
There are no services.
|
This node has no service instances{{#if (gt items.length 0)}} matching that search{{/if}}.
|
||||||
</p>
|
</p>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
|
|
|
@ -23,7 +23,7 @@ Feature: page-navigation
|
||||||
Where:
|
Where:
|
||||||
-------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------
|
||||||
| Link | URL | Endpoint |
|
| Link | URL | Endpoint |
|
||||||
| nodes | /dc-1/nodes | /v1/internal/ui/nodes?dc=dc-1 |
|
| nodes | /dc-1/nodes | /v1/internal/ui/nodes?dc=dc-1&ns=@namespace |
|
||||||
| kvs | /dc-1/kv | /v1/kv/?keys&dc=dc-1&separator=%2F&ns=@namespace |
|
| kvs | /dc-1/kv | /v1/kv/?keys&dc=dc-1&separator=%2F&ns=@namespace |
|
||||||
| acls | /dc-1/acls/tokens | /v1/acl/tokens?dc=dc-1&ns=@namespace |
|
| acls | /dc-1/acls/tokens | /v1/acl/tokens?dc=dc-1&ns=@namespace |
|
||||||
# | settings | /settings | /v1/catalog/datacenters |
|
# | settings | /settings | /v1/catalog/datacenters |
|
||||||
|
@ -39,11 +39,10 @@ Feature: page-navigation
|
||||||
And I click "[data-test-back]"
|
And I click "[data-test-back]"
|
||||||
Then the url should be [Back]
|
Then the url should be [Back]
|
||||||
Where:
|
Where:
|
||||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------------------------------------------------------
|
||||||
| Item | Model | URL | Endpoint | Back |
|
| Item | Model | URL | Endpoint | Back |
|
||||||
| kv | kvs | /dc-1/kv/0-key-value/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1&ns=@namespace | /dc-1/kv |
|
| kv | kvs | /dc-1/kv/0-key-value/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1&ns=@namespace | /dc-1/kv |
|
||||||
# | acl | acls | /dc-1/acls/anonymous | /v1/acl/info/anonymous?dc=dc-1 | /dc-1/acls |
|
-------------------------------------------------------------------------------------------------------------------------------------
|
||||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
||||||
Scenario: The node detail page calls the correct API endpoints
|
Scenario: The node detail page calls the correct API endpoints
|
||||||
When I visit the node page for yaml
|
When I visit the node page for yaml
|
||||||
---
|
---
|
||||||
|
@ -55,7 +54,7 @@ Feature: page-navigation
|
||||||
---
|
---
|
||||||
- /v1/catalog/datacenters
|
- /v1/catalog/datacenters
|
||||||
- /v1/namespaces
|
- /v1/namespaces
|
||||||
- /v1/internal/ui/node/node-0?dc=dc-1
|
- /v1/internal/ui/node/node-0?dc=dc-1&ns=@namespace
|
||||||
- /v1/coordinate/nodes?dc=dc-1
|
- /v1/coordinate/nodes?dc=dc-1
|
||||||
---
|
---
|
||||||
Scenario: The kv detail page calls the correct API endpoints
|
Scenario: The kv detail page calls the correct API endpoints
|
||||||
|
|
|
@ -1,28 +1,42 @@
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupTest } from 'ember-qunit';
|
import { setupTest } from 'ember-qunit';
|
||||||
|
import { env } from '../../../env';
|
||||||
|
const shouldHaveNspace = function(nspace) {
|
||||||
|
return typeof nspace !== 'undefined' && env('CONSUL_NSPACES_ENABLED');
|
||||||
|
};
|
||||||
module('Integration | Adapter | node', function(hooks) {
|
module('Integration | Adapter | node', function(hooks) {
|
||||||
setupTest(hooks);
|
setupTest(hooks);
|
||||||
const dc = 'dc-1';
|
const dc = 'dc-1';
|
||||||
const id = 'node-name';
|
const id = 'node-name';
|
||||||
test('requestForQuery returns the correct url', function(assert) {
|
const undefinedNspace = 'default';
|
||||||
|
[undefinedNspace, 'team-1', undefined].forEach(nspace => {
|
||||||
|
test(`requestForQuery returns the correct url when nspace is ${nspace}`, function(assert) {
|
||||||
const adapter = this.owner.lookup('adapter:node');
|
const adapter = this.owner.lookup('adapter:node');
|
||||||
const client = this.owner.lookup('service:client/http');
|
const client = this.owner.lookup('service:client/http');
|
||||||
const expected = `GET /v1/internal/ui/nodes?dc=${dc}`;
|
const expected = `GET /v1/internal/ui/nodes?dc=${dc}${
|
||||||
|
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
|
||||||
|
}`;
|
||||||
const actual = adapter.requestForQuery(client.requestParams.bind(client), {
|
const actual = adapter.requestForQuery(client.requestParams.bind(client), {
|
||||||
dc: dc,
|
dc: dc,
|
||||||
|
ns: nspace,
|
||||||
});
|
});
|
||||||
assert.equal(`${actual.method} ${actual.url}`, expected);
|
assert.equal(`${actual.method} ${actual.url}`, expected);
|
||||||
});
|
});
|
||||||
test('requestForQueryRecord returns the correct url', function(assert) {
|
test(`requestForQueryRecord returns the correct url when the nspace is ${nspace}`, function(assert) {
|
||||||
const adapter = this.owner.lookup('adapter:node');
|
const adapter = this.owner.lookup('adapter:node');
|
||||||
const client = this.owner.lookup('service:client/http');
|
const client = this.owner.lookup('service:client/http');
|
||||||
const expected = `GET /v1/internal/ui/node/${id}?dc=${dc}`;
|
const expected = `GET /v1/internal/ui/node/${id}?dc=${dc}${
|
||||||
|
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
|
||||||
|
}`;
|
||||||
const actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
|
const actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
|
||||||
dc: dc,
|
dc: dc,
|
||||||
id: id,
|
id: id,
|
||||||
|
ns: nspace,
|
||||||
});
|
});
|
||||||
assert.equal(`${actual.method} ${actual.url}`, expected);
|
assert.equal(`${actual.method} ${actual.url}`, expected);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
// the following don't require nspacing
|
||||||
test("requestForQueryRecord throws if you don't specify an id", function(assert) {
|
test("requestForQueryRecord throws if you don't specify an id", function(assert) {
|
||||||
const adapter = this.owner.lookup('adapter:node');
|
const adapter = this.owner.lookup('adapter:node');
|
||||||
const client = this.owner.lookup('service:client/http');
|
const client = this.owner.lookup('service:client/http');
|
||||||
|
|
Loading…
Reference in New Issue