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:
John Cowen 2021-01-04 16:42:44 +00:00 committed by GitHub
parent cdcfd0e4f9
commit 8c0473a622
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 60 additions and 33 deletions

3
.changelog/9410.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: ensure namespace is used for node API requests
```

View File

@ -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,
}}
`; `;
} }

View File

@ -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>

View File

@ -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`),

View File

@ -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>

View File

@ -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

View File

@ -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');