Merge pull request #7870 from hashicorp/ui-staging

UI Release Merge (1.8: ui-staging merge)
This commit is contained in:
Kenia 2020-05-13 13:36:35 -04:00 committed by GitHub
commit c3082ea4df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 589 additions and 44 deletions

View File

@ -0,0 +1,17 @@
import Adapter from './application';
export default Adapter.extend({
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/internal/ui/gateway-services-nodes/${id}?${{ dc }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
});

View File

@ -0,0 +1,12 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'Name';
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
Datacenter: attr('string'),
Namespace: attr('string'),
Services: attr(),
});

View File

@ -19,6 +19,12 @@ export const routes = {
intentions: {
_options: { path: '/intentions' },
},
services: {
_options: { path: '/services' },
},
upstreams: {
_options: { path: '/upstreams' },
},
routing: {
_options: { path: '/routing' },
},

View File

@ -8,6 +8,7 @@ export default Route.extend({
intentionRepo: service('repository/intention'),
chainRepo: service('repository/discovery-chain'),
proxyRepo: service('repository/proxy'),
gatewayRepo: service('repository/gateway'),
settings: service('settings'),
model: function(params, transition = {}) {
const dc = this.modelFor('dc').dc.Name;
@ -17,32 +18,43 @@ export default Route.extend({
urls: this.settings.findBySlug('urls'),
dc: dc,
proxies: [],
}).then(model => {
return ['connect-proxy', 'mesh-gateway'].includes(get(model, 'item.Service.Kind'))
? model
: hash({
intentions: this.intentionRepo.findByService(params.name, dc, nspace),
chain: this.chainRepo.findBySlug(params.name, dc, nspace).catch(function(e) {
const code = get(e, 'errors.firstObject.status');
// Currently we are specifically catching a 500, but we return null
// by default, so null for all errors.
// The extra code here is mainly for documentation purposes
// and for if we need to perform different actions based on the error code
// in the future
switch (code) {
case '500':
// connect is likely to be disabled
// we just return a null to hide the tab
// `Connect must be enabled in order to use this endpoint`
return null;
default:
return null;
}
}),
proxies: this.proxyRepo.findAllBySlug(params.name, dc, nspace),
...model,
});
});
})
.then(model => {
return ['connect-proxy', 'mesh-gateway', 'ingress-gateway', 'terminating-gateway'].includes(
get(model, 'item.Service.Kind')
)
? model
: hash({
intentions: this.intentionRepo.findByService(params.name, dc, nspace),
chain: this.chainRepo.findBySlug(params.name, dc, nspace).catch(function(e) {
const code = get(e, 'errors.firstObject.status');
// Currently we are specifically catching a 500, but we return null
// by default, so null for all errors.
// The extra code here is mainly for documentation purposes
// and for if we need to perform different actions based on the error code
// in the future
switch (code) {
case '500':
// connect is likely to be disabled
// we just return a null to hide the tab
// `Connect must be enabled in order to use this endpoint`
return null;
default:
return null;
}
}),
proxies: this.proxyRepo.findAllBySlug(params.name, dc, nspace),
...model,
});
})
.then(model => {
return ['ingress-gateway', 'terminating-gateway'].includes(get(model, 'item.Service.Kind'))
? hash({
gateway: this.gatewayRepo.findBySlug(params.name, dc, nspace),
...model,
})
: model;
});
},
setupController: function(controller, model) {
controller.setProperties(model);

View File

@ -0,0 +1,14 @@
import Route from '@ember/routing/route';
export default Route.extend({
model: function() {
const parent = this.routeName
.split('.')
.slice(0, -1)
.join('.');
return this.modelFor(parent);
},
setupController: function(controller, model) {
controller.setProperties(model);
},
});

View File

@ -0,0 +1,14 @@
import Route from '@ember/routing/route';
export default Route.extend({
model: function() {
const parent = this.routeName
.split('.')
.slice(0, -1)
.join('.');
return this.modelFor(parent);
},
setupController: function(controller, model) {
controller.setProperties(model);
},
});

View File

@ -0,0 +1,17 @@
import Serializer from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/gateway';
export default Serializer.extend({
primaryKey: PRIMARY_KEY,
slugKey: SLUG_KEY,
respondForQueryRecord: function(respond, query) {
return this._super(function(cb) {
return respond(function(headers, body) {
return cb(headers, {
Name: query.id,
Services: body,
});
});
}, query);
},
});

View File

@ -0,0 +1,8 @@
import RepositoryService from 'consul-ui/services/repository';
const modelName = 'gateway';
export default RepositoryService.extend({
getModelName: function() {
return modelName;
},
});

View File

@ -33,3 +33,12 @@
%composite-row-detail .port button:hover {
background-color: transparent !important;
}
// Tooltip
%composite-row-detail .feedback-dialog-out {
left: -12px;
bottom: 12px;
}
%composite-row-detail .feedback-dialog-out::after {
left: 18px;
}

View File

@ -1,9 +1,9 @@
%composite-row {
list-style-type: none;
border-top-color: $gray-200;
border-bottom-color: transparent;
border-right-color: transparent;
border-left-color: transparent;
border-top-color: $transparent;
border-bottom-color: $gray-200;
border-right-color: $transparent;
border-left-color: $transparent;
}
%composite-row-intent {
border-color: $gray-200;
@ -16,9 +16,6 @@
%composite-row-detail {
color: $gray-500;
}
%composite-row:last-child {
border-bottom-color: $gray-200;
}
// Health Checks
%composite-row .passing::before {

View File

@ -1,7 +1,7 @@
.consul-service-list > ul {
@extend %consul-service-list;
}
%consul-service-list > li {
%consul-service-list > li:not(:first-child) {
@extend %consul-service-row;
}
%consul-service-row {

View File

@ -1,7 +1,19 @@
.list-collection {
@extend %list-collection;
height: 500px;
position: relative;
}
.list-collection > ul {
%list-collection > ul {
border-top: 1px solid $gray-200;
overflow-x: hidden !important;
}
%list-collection > ul > li:nth-child(2) .with-feedback p {
bottom: auto;
top: 24px;
}
%list-collection > ul > li:nth-child(2) p::after {
bottom: auto;
top: -10px !important;
border-bottom-width: 18px;
border-top-width: 0;
}

View File

@ -1,5 +1,19 @@
// Services - Linked Services tab
// TODO - move this into composite-row
.consul-gateway-services-list > ul {
@extend %consul-gateway-services-list;
}
%consul-gateway-services-list > li:not(:first-child) {
@extend %gateway-service-row;
}
%gateway-service-row {
@extend %composite-row, %with-composite-row-intent;
}
// Service Detail - Proxy Info tab
.proxy-upstreams > ul {
@extend %proxy-upstreams-list;
border-top: 1px solid $gray-200;
}
%proxy-upstreams-list > li {
@extend %composite-row;

View File

@ -17,14 +17,26 @@
<ConsulKind @item={{item.Service}} />
</BlockSlot>
<BlockSlot @name="nav">
{{#if (not item.Service.Kind)}}
{{#if (not-eq item.Service.Kind 'mesh-gateway')}}
<TabNav @items={{
compact
(array
(hash label="Instances" href=(href-to "dc.services.show.instances") selected=(is-href "dc.services.show.instances"))
(hash label="Intentions" href=(href-to "dc.services.show.intentions") selected=(is-href "dc.services.show.intentions"))
(if (not-eq chain null) (hash label="Routing" href=(href-to "dc.services.show.routing") selected=(is-href "dc.services.show.routing")) '')
(hash label="Tags" href=(href-to "dc.services.show.tags") selected=(is-href "dc.services.show.tags"))
(hash label="Instances" href=(href-to "dc.services.show.instances") selected=(is-href "dc.services.show.instances"))
(if (eq item.Service.Kind 'terminating-gateway')
(hash label="Linked Services" href=(href-to "dc.services.show.services") selected=(is-href "dc.services.show.services"))
'')
(if (eq item.Service.Kind 'ingress-gateway')
(hash label="Upstreams" href=(href-to "dc.services.show.upstreams") selected=(is-href "dc.services.show.upstreams"))
'')
(if (not item.Service.Kind)
(hash label="Intentions" href=(href-to "dc.services.show.intentions") selected=(is-href "dc.services.show.intentions"))
'')
(if chain
(hash label="Routing" href=(href-to "dc.services.show.routing") selected=(is-href "dc.services.show.routing"))
'')
(if (not item.Service.Kind)
(hash label="Tags" href=(href-to "dc.services.show.tags") selected=(is-href "dc.services.show.tags"))
'')
)
}}/>
{{/if}}

View File

@ -0,0 +1,43 @@
<div id="services" class="tab-section">
<div role="tabpanel">
{{#if (gt gateway.Services.length 0)}}
<section>
<p>
The following services may receive traffic from external services through this gateway. Learn more about configuring gateways in our
<a href="{{env 'CONSUL_DOCS_URL'}}/connect/terminating_gateway.html" target="_blank" rel="noopener noreferrer">step-by-step guide.</a>
</p>
{{#let item.Service.Namespace as |nspace|}}
<ListCollection @cellHeight={{73}} @items={{gateway.Services}} class="consul-gateway-services-list" as |item index|>
<a data-test-service-name href={{href-to 'dc.services.show' item.Name}} class={{service/health-checks item}}>
{{item.Name}}
</a>
<ul>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (not-eq item.Namespace nspace)}}
<li class="nspace">
{{item.Namespace}}
</li>
{{/if}}
{{/if}}
{{#if (not-eq item.InstanceCount 0)}}
<li>
{{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'Instance' without-count=true}}
</li>
{{/if}}
<TagList @item={{item}} as |Tags|>
<li>
<Tags />
</li>
</TagList>
</ul>
</ListCollection>
{{/let}}
</section>
{{else}}
<p>
There are no linked services.
</p>
{{/if}}
</div>
</div>

View File

@ -0,0 +1,41 @@
<div id="upstreams" class="tab-section">
<div role="tabpanel">
{{#if (gt gateway.Services.length 0)}}
<section>
<p>
Upstreams are services that may receive traffic from this gateway. Learn more about configuring gateways in our
<a href="{{env 'CONSUL_DOCS_URL'}}/connect/ingress_gateway.html" target="_blank" rel="noopener noreferrer">documentation.</a>
</p>
{{#let item.Service.Namespace as |nspace|}}
<ListCollection @cellHeight={{73}} @items={{gateway.Services}} class="consul-gateway-services-list" as |item index|>
<a data-test-service-name href={{href-to 'dc.services.show' item.Name}}>
{{item.Name}}
</a>
<ul>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (not-eq item.Namespace nspace)}}
<li class="nspace">
{{item.Namespace}}
</li>
{{/if}}
{{/if}}
{{#if (not-eq item.GatewayConfig.ListenerPort 0)}}
<li class="port">
<CopyButton
@value={{item.GatewayConfig.ListenerPort}}
@name="Port"
/>
<span>:{{item.GatewayConfig.ListenerPort}}</span>
</li>
{{/if}}
</ul>
</ListCollection>
{{/let}}
</section>
{{else}}
<p>
There are no upstreams.
</p>
{{/if}}
</div>
</div>

View File

@ -0,0 +1,57 @@
@setupApplicationTest
Feature: dc / services / show / services
Background:
Given 1 datacenter model with the value "dc1"
And 1 node models
And 1 service model from yaml
---
- Service:
Name: terminating-gateway-1
Kind: terminating-gateway
---
Scenario: Seeing the Linked Services tab
When I visit the service page for yaml
---
dc: dc1
service: terminating-gateway-1
---
And the title should be "terminating-gateway-1 - Consul"
And I see linkedServices on the tabs
When I click linkedServices on the tabs
And I see linkedServicesIsSelected on the tabs
Scenario: Seeing the list of Linked Services
Given 3 service models from yaml
When I visit the service page for yaml
---
dc: dc1
service: terminating-gateway-1
---
And the title should be "terminating-gateway-1 - Consul"
When I click linkedServices on the tabs
Then I see 3 service models
Scenario: Don't see the Linked Services tab
Given 1 datacenter model with the value "dc1"
And 1 node models
And 1 service model from yaml
---
- Service:
Name: [Name]
Kind: [Kind]
---
When I visit the service page for yaml
---
dc: dc1
service: [Name]
---
And the title should be "[Name] - Consul"
And I don't see linkedServices on the tabs
Where:
---------------------------------------------
| Name | Kind |
| service | ~ |
| ingress-gateway | ingress-gateway |
| mesh-gateway | mesh-gateway |
---------------------------------------------

View File

@ -0,0 +1,54 @@
@setupApplicationTest
Feature: dc / services / show / upstreams
Background:
Given 1 datacenter model with the value "dc1"
And 1 node models
And 1 service model from yaml
---
- Service:
Name: ingress-gateway-1
Kind: ingress-gateway
---
Scenario: Seeing the Upstreams tab
When I visit the service page for yaml
---
dc: dc1
service: ingress-gateway-1
---
And the title should be "ingress-gateway-1 - Consul"
And I see upstreams on the tabs
When I click upstreams on the tabs
And I see upstreamsIsSelected on the tabs
Scenario: Seeing the list of Upstreams
Given 3 service models from yaml
When I visit the service page for yaml
---
dc: dc1
service: ingress-gateway-1
---
And the title should be "ingress-gateway-1 - Consul"
When I click upstreams on the tabs
Then I see 3 service models
Scenario: Don't see the Upstreams tab
Given 1 datacenter model with the value "dc1"
And 1 node models
And 1 service model from yaml
---
- Service:
Name: [Name]
Kind: [Kind]
---
When I visit the service page for yaml
---
dc: dc1
service: [Name]
---
And the title should be "[Name] - Consul"
And I don't see upstreams on the tabs
Where:
---------------------------------------------
| Name | Kind |
| service | ~ |
| terminating-gateway | terminating-gateway |
| mesh-gateway | mesh-gateway |
---------------------------------------------

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,27 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Integration | Adapter | gateway', function(hooks) {
setupTest(hooks);
const dc = 'dc-1';
const id = 'slug';
test('requestForQueryRecord returns the correct url/method', function(assert) {
const adapter = this.owner.lookup('adapter:gateway');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/internal/ui/gateway-services-nodes/${id}?dc=${dc}`;
const actual = adapter.requestForQueryRecord(client.url, {
dc: dc,
id: id,
});
assert.equal(actual, expected);
});
test("requestForQueryRecord throws if you don't specify an id", function(assert) {
const adapter = this.owner.lookup('adapter:gateway');
const client = this.owner.lookup('service:client/http');
assert.throws(function() {
adapter.requestForQueryRecord(client.url, {
dc: dc,
});
});
});
});

View File

@ -0,0 +1,47 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { get } from 'consul-ui/tests/helpers/api';
import {
HEADERS_SYMBOL as META,
HEADERS_DATACENTER as DC,
HEADERS_NAMESPACE as NSPACE,
} from 'consul-ui/utils/http/consul';
module('Integration | Serializer | gateway', function(hooks) {
setupTest(hooks);
test('respondForQueryRecord returns the correct data for item endpoint', function(assert) {
const serializer = this.owner.lookup('serializer:gateway');
const dc = 'dc-1';
const id = 'slug';
const nspace = 'default';
const request = {
url: `/v1/internal/ui/gateway-services-nodes/${id}?dc=${dc}`,
};
return get(request.url).then(function(payload) {
const expected = {
Datacenter: dc,
[META]: {
[DC.toLowerCase()]: dc,
[NSPACE.toLowerCase()]: nspace,
},
uid: `["${nspace}","${dc}","${id}"]`,
Name: id,
Namespace: nspace,
Services: payload,
};
const actual = serializer.respondForQueryRecord(
function(cb) {
const headers = {};
const body = payload;
return cb(headers, body);
},
{
dc: dc,
id: id,
}
);
assert.deepEqual(actual, expected);
});
});
});

View File

@ -0,0 +1,42 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
moduleFor('service:repository/gateway', 'Integration | Repository | gateway', {
// Specify the other units that are required for this test.
integration: true,
});
const dc = 'dc-1';
const id = 'slug';
const nspace = 'default';
test('findBySlug returns the correct data for item endpoint', function(assert) {
return repo(
'Gateway',
'findBySlug',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/internal/ui/gateway-services-nodes/${id}`);
},
function performTest(service) {
return service.findBySlug(id, dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return Object.assign(
{},
{
Datacenter: dc,
Name: id,
Namespace: nspace,
uid: `["${nspace}","${dc}","${id}"]`,
},
{
Services: payload,
}
);
})
);
}
);
});

View File

@ -7,7 +7,14 @@ export default function(visitable, attribute, collection, text, intentions, filt
dashboardAnchor: {
href: attribute('href', '[data-test-dashboard-anchor]'),
},
tabs: tabs('tab', ['instances', 'intentions', 'routing', 'tags']),
tabs: tabs('tab', [
'instances',
'linked-services',
'upstreams',
'intentions',
'routing',
'tags',
]),
filter: filter,
// TODO: These need to somehow move to subpages
@ -15,5 +22,8 @@ export default function(visitable, attribute, collection, text, intentions, filt
address: text('[data-test-address]'),
}),
intentions: intentions(),
services: collection('.consul-gateway-services-list> ul > li:not(:first-child)', {
name: text('[data-test-service-name]'),
}),
};
}

View File

@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Adapter | gateway', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let adapter = this.owner.lookup('adapter:gateway');
assert.ok(adapter);
});
});

View File

@ -0,0 +1,13 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | gateway', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let model = store.createRecord('gateway', {});
assert.ok(model);
});
});

View File

@ -0,0 +1,23 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Serializer | gateway', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let serializer = store.serializerFor('gateway');
assert.ok(serializer);
});
test('it serializes records', function(assert) {
let store = this.owner.lookup('service:store');
let record = store.createRecord('gateway', {});
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
});
});

View File

@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Repository | gateway', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
const repo = this.owner.lookup('service:repository/gateway');
assert.ok(repo);
});
});

View File

@ -1211,9 +1211,9 @@
js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^2.6.2":
version "2.14.7"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.14.7.tgz#80cd19461a3f3716bf76ba28bcabff842bcd9aef"
integrity sha512-QjpwvrraUswn/hFh+9lIKuA9keCGOkh1yr/cT3I6fDiw4JKLyDLaRN8bF/JNGtgoA/SsQh10L1YI3feZ7M3VKw==
version "2.15.1"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.15.1.tgz#1b41c92ee7930e0bcead8283eea019b5f1238819"
integrity sha512-0q0h2krXFR5uj/A+x5WtsKVF1ltPPDrrxmX9g+SjUmeWHIcffH7qz/PCo4fdqWOPjcTXkPfBxSZwGd2uDishaQ==
"@hashicorp/ember-cli-api-double@^3.0.2":
version "3.0.2"