Merge pull request #7235 from hashicorp/ui-staging

ui: UI Release Merge (ui-staging merge)
pull/7236/head
Kenia 2020-02-06 15:34:02 -05:00 committed by GitHub
commit cb69613bf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 405 additions and 146 deletions

View File

@ -497,7 +497,6 @@ jobs:
environment: environment:
EMBER_TEST_PARALLEL: true #enables test parallelization with ember-exam EMBER_TEST_PARALLEL: true #enables test parallelization with ember-exam
EMBER_TEST_REPORT: test-results/report.xml #outputs test report for CircleCI test summary EMBER_TEST_REPORT: test-results/report.xml #outputs test report for CircleCI test summary
parallelism: 4
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@ -506,7 +505,7 @@ jobs:
at: ui-v2 at: ui-v2
- run: - run:
working_directory: ui-v2 working_directory: ui-v2
command: node_modules/ember-cli/bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit command: make test-ci
- store_test_results: - store_test_results:
path: ui-v2/test-results path: ui-v2/test-results

View File

@ -38,6 +38,9 @@ start-api: deps
test: deps test-node test: deps test-node
yarn run test yarn run test
test-ci: deps test-node
yarn run test:ci
test-view: deps test-node test-view: deps test-node
yarn run test:view yarn run test:view

View File

@ -53,6 +53,7 @@ export default Adapter.extend({
requestForUpdateRecord: function(request, serialized, data) { requestForUpdateRecord: function(request, serialized, data) {
const params = { const params = {
...this.formatDatacenter(data[DATACENTER_KEY]), ...this.formatDatacenter(data[DATACENTER_KEY]),
flags: data.Flags,
...this.formatNspace(data[NSPACE_KEY]), ...this.formatNspace(data[NSPACE_KEY]),
}; };
return request` return request`

View File

@ -39,6 +39,7 @@ const MENU_ITEMS = '[role^="menuitem"]';
export default Component.extend({ export default Component.extend({
tagName: '', tagName: '',
dom: service('dom'), dom: service('dom'),
router: service('router'),
guid: '', guid: '',
expanded: false, expanded: false,
orientation: 'vertical', orientation: 'vertical',
@ -47,6 +48,7 @@ export default Component.extend({
this._super(...arguments); this._super(...arguments);
set(this, 'guid', this.dom.guid(this)); set(this, 'guid', this.dom.guid(this));
this._listeners = this.dom.listeners(); this._listeners = this.dom.listeners();
this._routelisteners = this.dom.listeners();
}, },
didInsertElement: function() { didInsertElement: function() {
// TODO: How do you detect whether the children have changed? // TODO: How do you detect whether the children have changed?
@ -54,10 +56,14 @@ export default Component.extend({
this.$menu = this.dom.element(`#${COMPONENT_ID}menu-${this.guid}`); this.$menu = this.dom.element(`#${COMPONENT_ID}menu-${this.guid}`);
const labelledBy = this.$menu.getAttribute('aria-labelledby'); const labelledBy = this.$menu.getAttribute('aria-labelledby');
this.$trigger = this.dom.element(`#${labelledBy}`); this.$trigger = this.dom.element(`#${labelledBy}`);
this._routelisteners.add(this.router, {
routeWillChange: () => this.actions.close.apply(this, [{}]),
});
}, },
willDestroyElement: function() { willDestroyElement: function() {
this._super(...arguments); this._super(...arguments);
this._listeners.remove(); this._listeners.remove();
this._routelisteners.remove();
}, },
actions: { actions: {
keypressClick: function(e) { keypressClick: function(e) {

View File

@ -31,11 +31,13 @@ export default Component.extend({
this._super(...arguments); this._super(...arguments);
this._viewportlistener.add( this._viewportlistener.add(
this.dom.isInViewport(this.element, bool => { this.dom.isInViewport(this.element, bool => {
set(this, 'isDisplayed', bool); if (get(this, 'isDisplayed') !== bool) {
if (this.isDisplayed) { set(this, 'isDisplayed', bool);
this.addPathListeners(); if (this.isDisplayed) {
} else { this.addPathListeners();
this.ticker.destroy(this); } else {
this.ticker.destroy(this);
}
} }
}) })
); );
@ -63,24 +65,29 @@ export default Component.extend({
!routes.find(item => get(item, 'Definition.Match.HTTP.PathPrefix') === '/') && !routes.find(item => get(item, 'Definition.Match.HTTP.PathPrefix') === '/') &&
!routes.find(item => typeof item.Definition === 'undefined') !routes.find(item => typeof item.Definition === 'undefined')
) { ) {
let nextNode = `resolver:${this.chain.ServiceName}.${this.chain.Namespace}.${this.chain.Datacenter}`; let nextNode;
const splitterID = `splitter:${this.chain.ServiceName}`; const resolverID = `resolver:${this.chain.ServiceName}.${this.chain.Namespace}.${this.chain.Datacenter}`;
if (typeof this.chain.Nodes[splitterID] !== 'undefined') { const splitterID = `splitter:${this.chain.ServiceName}.${this.chain.Namespace}`;
if (typeof this.chain.Nodes[resolverID] !== 'undefined') {
nextNode = resolverID;
} else if (typeof this.chain.Nodes[splitterID] !== 'undefined') {
nextNode = splitterID; nextNode = splitterID;
} }
routes.push({ if (typeof nextNode !== 'undefined') {
Default: true, routes.push({
ID: `route:${this.chain.ServiceName}`, Default: true,
Name: this.chain.ServiceName, ID: `route:${this.chain.ServiceName}`,
Definition: { Name: this.chain.ServiceName,
Match: { Definition: {
HTTP: { Match: {
PathPrefix: '/', HTTP: {
PathPrefix: '/',
},
}, },
}, },
}, NextNode: nextNode,
NextNode: nextNode, });
}); }
} }
return routes; return routes;
}), }),
@ -92,23 +99,17 @@ export default Component.extend({
get(this, 'chain.Nodes') get(this, 'chain.Nodes')
); );
}), }),
graph: computed('chain.Nodes', function() { graph: computed('splitters', 'routes', function() {
const graph = this.dataStructs.graph(); const graph = this.dataStructs.graph();
const router = this.chain.ServiceName; const router = this.chain.ServiceName;
Object.entries(get(this, 'chain.Nodes')).forEach(([key, item]) => { this.splitters.forEach(item => {
switch (item.Type) { item.Splits.forEach(splitter => {
case 'splitter': graph.addLink(item.ID, splitter.NextNode);
item.Splits.forEach(splitter => { });
graph.addLink(item.ID, splitter.NextNode); });
}); this.routes.forEach((route, i) => {
break; route = createRoute(route, router, this.dom.guid);
case 'router': graph.addLink(route.ID, route.NextNode);
item.Routes.forEach((route, i) => {
route = createRoute(route, router, this.dom.guid);
graph.addLink(route.ID, route.NextNode);
});
break;
}
}); });
return graph; return graph;
}), }),

View File

@ -4,12 +4,6 @@ import { get, computed } from '@ember/object';
export default Component.extend({ export default Component.extend({
tagName: '', tagName: '',
path: computed('item', function() { path: computed('item', function() {
if (get(this, 'item.Default')) {
return {
type: 'Default',
value: '/',
};
}
return Object.entries(get(this, 'item.Definition.Match.HTTP') || {}).reduce( return Object.entries(get(this, 'item.Definition.Match.HTTP') || {}).reduce(
function(prev, [key, value]) { function(prev, [key, value]) {
if (key.toLowerCase().startsWith('path')) { if (key.toLowerCase().startsWith('path')) {

View File

@ -1,6 +1,7 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { hash } from 'rsvp'; import { hash } from 'rsvp';
import { get } from '@ember/object';
export default Route.extend({ export default Route.extend({
repo: service('repository/service'), repo: service('repository/service'),
@ -17,9 +18,15 @@ export default Route.extend({
const nspace = this.modelFor('nspace').nspace.substr(1); const nspace = this.modelFor('nspace').nspace.substr(1);
return hash({ return hash({
item: this.repo.findBySlug(params.name, dc, nspace), item: this.repo.findBySlug(params.name, dc, nspace),
chain: this.chainRepo.findBySlug(params.name, dc, nspace),
urls: this.settings.findBySlug('urls'), urls: this.settings.findBySlug('urls'),
dc: dc, dc: dc,
}).then(model => {
return hash({
chain: ['connect-proxy', 'mesh-gateway'].includes(get(model, 'item.Service.Kind'))
? null
: this.chainRepo.findBySlug(params.name, dc, nspace),
...model,
});
}); });
}, },
setupController: function(controller, model) { setupController: function(controller, model) {

View File

@ -4,6 +4,9 @@ export default function(filterable) {
const sLower = s.toLowerCase(); const sLower = s.toLowerCase();
return ( return (
get(item, 'Node') get(item, 'Node')
.toLowerCase()
.indexOf(sLower) !== -1 ||
get(item, 'Address')
.toLowerCase() .toLowerCase()
.indexOf(sLower) !== -1 .indexOf(sLower) !== -1
); );

View File

@ -65,8 +65,13 @@
padding: 10px; padding: 10px;
padding-left: 36px; padding-left: 36px;
} }
/* here the !important is only needed for what seems to be a difference */
/* with the CSS before and after compression */
/* i.e. before compression this style is applied */
/* after compression it is in the source but doesn't seem to get */
/* applied (unless you add the !important) */
%menu-panel .is-active { %menu-panel .is-active {
position: relative; position: relative !important;
} }
%menu-panel .is-active > *::after { %menu-panel .is-active > *::after {
position: absolute; position: absolute;

View File

@ -1,5 +1,5 @@
{{!<form>}} {{!<form>}}
{{freetext-filter searchable=searchable value=search placeholder="Search by name"}} {{freetext-filter searchable=searchable value=search placeholder="Search"}}
{{radio-group keyboardAccess=true name="status" value=status items=(array {{radio-group keyboardAccess=true name="status" value=status items=(array
(hash label='All (Any Status)' value='' ) (hash label='All (Any Status)' value='' )
(hash label='Critical Checks' value='critical') (hash label='Critical Checks' value='critical')

View File

@ -12,7 +12,7 @@
{{#if dc}} {{#if dc}}
<ul> <ul>
{{#if (and (env 'CONSUL_NSPACES_ENABLED') (gt nspaces.length 0))}} {{#if (and (env 'CONSUL_NSPACES_ENABLED') (gt nspaces.length 0))}}
<li> <li data-test-nspace-menu>
{{#if (and (eq nspaces.length 1) (not canManageNspaces)) }} {{#if (and (eq nspaces.length 1) (not canManageNspaces)) }}
<span data-test-nspace-selected={{nspace.Name}}>{{nspace.Name}}</span> <span data-test-nspace-selected={{nspace.Name}}>{{nspace.Name}}</span>
{{ else }} {{ else }}

View File

@ -1,6 +1,6 @@
{{yield (concat 'popover-menu-' guid)}} {{yield (concat 'popover-menu-' guid)}}
{{#aria-menu keyboardAccess=keyboardAccess as |change keypress ariaLabelledBy ariaControls ariaExpanded keypressClick|}} {{#aria-menu keyboardAccess=keyboardAccess as |change keypress ariaLabelledBy ariaControls ariaExpanded keypressClick|}}
{{#toggle-button checked=expanded onchange=(queue change (action 'change')) as |click|}} {{#toggle-button checked=ariaExpanded onchange=(queue change (action 'change')) as |click|}}
<button type="button" aria-haspopup="menu" onkeydown={{keypress}} onclick={{click}} id={{ariaLabelledBy}} aria-controls={{ariaControls}}> <button type="button" aria-haspopup="menu" onkeydown={{keypress}} onclick={{click}} id={{ariaLabelledBy}} aria-controls={{ariaControls}}>
{{#yield-slot name='trigger'}} {{#yield-slot name='trigger'}}
{{yield}} {{yield}}

View File

@ -12,9 +12,7 @@
{{path.type}} {{path.type}}
</dt> </dt>
<dd> <dd>
{{#if (not-eq path.type 'Default')}}
{{path.value}} {{path.value}}
{{/if}}
</dd> </dd>
</dl> </dl>
</header> </header>

View File

@ -1,59 +1,59 @@
{{title item.Service.Service}} {{title item.Service.Service}}
{{#app-view class="service show"}} {{#app-view class="service show"}}
{{#block-slot name='notification' as |status type|}} {{#block-slot name='notification' as |status type|}}
{{partial 'dc/services/notifications'}} {{partial 'dc/services/notifications'}}
{{/block-slot}} {{/block-slot}}
{{#block-slot name='breadcrumbs'}} {{#block-slot name='breadcrumbs'}}
<ol> <ol>
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li> <li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
</ol> </ol>
{{/block-slot}} {{/block-slot}}
{{#block-slot name='header'}} {{#block-slot name='header'}}
<h1> <h1>
{{ item.Service.Service }} {{item.Service.Service}}
{{#with (service/external-source item.Service) as |externalSource| }} {{#with (service/external-source item.Service) as |externalSource|}}
{{#with (css-var (concat '--' externalSource '-color-svg') 'none') as |bg| }} {{#with (css-var (concat '--' externalSource '-color-svg') 'none') as |bg|}}
{{#if (not-eq bg 'none') }} {{#if (not-eq bg 'none')}}
<span data-test-external-source="{{externalSource}}" style={{{ concat 'background-image:' bg }}} data-tooltip="Registered via {{externalSource}}">Registered via {{externalSource}}</span> <span data-test-external-source={{externalSource}} style={{{concat 'background-image:' bg}}} data-tooltip="Registered via {{externalSource}}">Registered via {{externalSource}}</span>
{{/if}} {{/if}}
{{/with}} {{/with}}
{{/with}} {{/with}}
{{#if (eq item.Service.Kind 'connect-proxy')}} {{#if (eq item.Service.Kind 'connect-proxy')}}
<span class="kind-proxy">Proxy</span> <span class="kind-proxy">Proxy</span>
{{else if (eq item.Service.Kind 'mesh-gateway')}} {{else if (eq item.Service.Kind 'mesh-gateway')}}
<span class="kind-proxy">Mesh Gateway</span> <span class="kind-proxy">Mesh Gateway</span>
{{/if}} {{/if}}
</h1> </h1>
<label for="toolbar-toggle"></label> <label for="toolbar-toggle"></label>
{{tab-nav {{tab-nav
items=(compact items=(compact
(array (array
'Instances' 'Instances'
'Routing' (if (not-eq chain null) 'Routing' '')
'Tags' 'Tags'
) )
) )
selected=selectedTab selected=selectedTab
}} }}
{{/block-slot}} {{/block-slot}}
{{#block-slot name='actions'}} {{#block-slot name='actions'}}
{{#if urls.service}} {{#if urls.service}}
{{#templated-anchor data-test-dashboard-anchor href=urls.service vars=(hash Datacenter=dc Service=(hash Name=item.Service.Service)) rel="external"}}Open Dashboard{{/templated-anchor}} {{#templated-anchor data-test-dashboard-anchor href=urls.service vars=(hash Datacenter=dc Service=(hash Name=item.Service.Service)) rel="external"}}Open Dashboard{{/templated-anchor}}
{{/if}} {{/if}}
{{/block-slot}} {{/block-slot}}
{{#block-slot name='content'}} {{#block-slot name='content'}}
{{#each {{#each
(compact (compact
(array (array
(hash id=(slugify 'Instances') partial='dc/services/instances') (hash id=(slugify 'Instances') partial='dc/services/instances')
(hash id=(slugify 'Routing') partial='dc/services/routing') (if (not-eq chain null) (hash id=(slugify 'Routing') partial='dc/services/routing') '')
(hash id=(slugify 'Tags') partial='dc/services/tags') (hash id=(slugify 'Tags') partial='dc/services/tags')
) )
) as |panel| ) as |panel|
}} }}
{{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab '') panel.id) onchange=(action "change")}} {{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab '') panel.id) onchange=(action 'change')}}
{{partial panel.partial}} {{partial panel.partial}}
{{/tab-section}} {{/tab-section}}
{{/each}} {{/each}}
{{/block-slot}} {{/block-slot}}
{{/app-view}} {{/app-view}}

View File

@ -26,7 +26,7 @@
</label> </label>
</fieldset> </fieldset>
{{#if (not (env 'CONSUL_UI_DISABLE_REALTIME'))}} {{#if (not (env 'CONSUL_UI_DISABLE_REALTIME'))}}
<fieldset> <fieldset data-test-blocking-queries>
<h2>Blocking Queries</h2> <h2>Blocking Queries</h2>
<p>Keep catalog info up-to-date without refreshing the page. Any changes made to services, nodes and intentions would be reflected in real time.</p> <p>Keep catalog info up-to-date without refreshing the page. Any changes made to services, nodes and intentions would be reflected in real time.</p>
<div class="type-toggle"> <div class="type-toggle">

View File

@ -37,7 +37,6 @@ export const getAlternateServices = function(targets, a) {
export const getSplitters = function(nodes) { export const getSplitters = function(nodes) {
return getNodesByType(nodes, 'splitter').map(function(item) { return getNodesByType(nodes, 'splitter').map(function(item) {
// Splitters need IDs adding so we can find them in the DOM later // Splitters need IDs adding so we can find them in the DOM later
item.ID = `splitter:${item.Name}`;
// splitters have a service.nspace as a name // splitters have a service.nspace as a name
// do the reverse dance to ensure we don't mess up any // do the reverse dance to ensure we don't mess up any
// serivice names with dots in them // serivice names with dots in them
@ -45,8 +44,11 @@ export const getSplitters = function(nodes) {
temp.reverse(); temp.reverse();
temp.shift(); temp.shift();
temp.reverse(); temp.reverse();
item.Name = temp.join('.'); return {
return item; ...item,
ID: `splitter:${item.Name}`,
Name: temp.join('.'),
};
}); });
}; };
export const getRoutes = function(nodes, uid) { export const getRoutes = function(nodes, uid) {

View File

@ -22,6 +22,7 @@
"start:consul": "ember serve --proxy=${CONSUL_HTTP_ADDR:-http://localhost:8500} --port=${EMBER_SERVE_PORT:-4200} --live-reload-port=${EMBER_LIVE_RELOAD_PORT:-7020}", "start:consul": "ember serve --proxy=${CONSUL_HTTP_ADDR:-http://localhost:8500} --port=${EMBER_SERVE_PORT:-4200} --live-reload-port=${EMBER_LIVE_RELOAD_PORT:-7020}",
"start:api": "api-double --dir ./node_modules/@hashicorp/consul-api-double", "start:api": "api-double --dir ./node_modules/@hashicorp/consul-api-double",
"test": "ember test --test-port=${EMBER_TEST_PORT:-7357}", "test": "ember test --test-port=${EMBER_TEST_PORT:-7357}",
"test:ci": "ember test --test-port=${EMBER_TEST_PORT:-7357} --path dist --silent --reporter xunit",
"test:parallel": "EMBER_EXAM_PARALLEL=true ember exam --split=4 --parallel", "test:parallel": "EMBER_EXAM_PARALLEL=true ember exam --split=4 --parallel",
"test:view": "ember test --server --test-port=${EMBER_TEST_PORT:-7357}", "test:view": "ember test --server --test-port=${EMBER_TEST_PORT:-7357}",
"test:node": "tape ./node-tests/**/*.js", "test:node": "tape ./node-tests/**/*.js",

View File

@ -6,6 +6,7 @@ Feature: dc / kvs / update: KV Update
And 1 kv model from yaml And 1 kv model from yaml
--- ---
Key: "[Name]" Key: "[Name]"
Flags: 12
--- ---
When I visit the kv page for yaml When I visit the kv page for yaml
--- ---
@ -21,7 +22,7 @@ Feature: dc / kvs / update: KV Update
value: [Value] value: [Value]
--- ---
And I submit And I submit
Then a PUT request was made to "/v1/kv/[EncodedName]?dc=datacenter&ns=@!namespace" with the body "[Value]" Then a PUT request was made to "/v1/kv/[EncodedName]?dc=datacenter&flags=12&ns=@!namespace" with the body "[Value]"
And "[data-notification]" has the "notification-update" class And "[data-notification]" has the "notification-update" class
And "[data-notification]" has the "success" class And "[data-notification]" has the "success" class
Where: Where:
@ -37,6 +38,7 @@ Feature: dc / kvs / update: KV Update
And 1 kv model from yaml And 1 kv model from yaml
--- ---
Key: key Key: key
Flags: 12
--- ---
When I visit the kv page for yaml When I visit the kv page for yaml
--- ---
@ -51,7 +53,7 @@ Feature: dc / kvs / update: KV Update
value: ' ' value: ' '
--- ---
And I submit And I submit
Then a PUT request was made to "/v1/kv/key?dc=datacenter&ns=@!namespace" with the body " " Then a PUT request was made to "/v1/kv/key?dc=datacenter&flags=12&ns=@!namespace" with the body " "
Then the url should be /datacenter/kv Then the url should be /datacenter/kv
And the title should be "Key/Value - Consul" And the title should be "Key/Value - Consul"
And "[data-notification]" has the "notification-update" class And "[data-notification]" has the "notification-update" class
@ -60,6 +62,7 @@ Feature: dc / kvs / update: KV Update
And 1 kv model from yaml And 1 kv model from yaml
--- ---
Key: key Key: key
Flags: 12
--- ---
When I visit the kv page for yaml When I visit the kv page for yaml
--- ---
@ -74,15 +77,16 @@ Feature: dc / kvs / update: KV Update
value: '' value: ''
--- ---
And I submit And I submit
Then a PUT request was made to "/v1/kv/key?dc=datacenter&ns=@!namespace" with no body Then a PUT request was made to "/v1/kv/key?dc=datacenter&flags=12&ns=@!namespace" with no body
Then the url should be /datacenter/kv Then the url should be /datacenter/kv
And "[data-notification]" has the "notification-update" class And "[data-notification]" has the "notification-update" class
And "[data-notification]" has the "success" class And "[data-notification]" has the "success" class
Scenario: Update to a key when the value is empty Scenario: Update to a key when the value is empty
And 1 kv model from yaml And 1 kv model from yaml
--- ---
Key: key Key: key
Value: ~ Value: ~
Flags: 12
--- ---
When I visit the kv page for yaml When I visit the kv page for yaml
--- ---
@ -91,7 +95,7 @@ Feature: dc / kvs / update: KV Update
--- ---
Then the url should be /datacenter/kv/key/edit Then the url should be /datacenter/kv/key/edit
And I submit And I submit
Then a PUT request was made to "/v1/kv/key?dc=datacenter&ns=@!namespace" with no body Then a PUT request was made to "/v1/kv/key?dc=datacenter&flags=12&ns=@!namespace" with no body
Then the url should be /datacenter/kv Then the url should be /datacenter/kv
And "[data-notification]" has the "notification-update" class And "[data-notification]" has the "notification-update" class
And "[data-notification]" has the "success" class And "[data-notification]" has the "success" class

View File

@ -50,3 +50,30 @@ Feature: dc / nodes / index
Then the url should be /dc-1/nodes Then the url should be /dc-1/nodes
Then I see 3 node models Then I see 3 node models
And I see leader on the healthyNodes And I see leader on the healthyNodes
Scenario: Searching the nodes with name and IP address
Given 3 node models from yaml
---
- Node: node-01
Address: 10.0.0.0
- Node: node-02
Address: 10.0.0.1
- Node: node-03
Address: 10.0.0.2
---
When I visit the nodes page for yaml
---
dc: dc-1
---
And I see 3 node models
Then I fill in with yaml
---
s: node-01
---
And I see 1 node model
And I see 1 node model with the name "node-01"
Then I fill in with yaml
---
s: 10.0.0.1
---
And I see 1 node model
And I see 1 node model with the name "node-02"

View File

@ -0,0 +1,33 @@
@setupApplicationTest
Feature: dc / nspaces / manage : Managing Namespaces
Scenario:
Given settings from yaml
---
consul:token:
SecretID: secret
AccessorID: accessor
Namespace: default
---
And 1 datacenter models from yaml
---
- dc-1
---
And 6 service models
When I visit the services page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/services
Then I see 6 service models
# In order to test this properly you have to click around a few times
# between services and nspace management
When I click nspace on the navigation
And I click manageNspaces on the navigation
Then the url should be /dc-1/namespaces
And I don't see manageNspacesIsVisible on the navigation
When I click services on the navigation
Then the url should be /dc-1/services
When I click nspace on the navigation
And I click manageNspaces on the navigation
Then the url should be /dc-1/namespaces
And I don't see manageNspacesIsVisible on the navigation

View File

@ -0,0 +1,37 @@
@setupApplicationTest
Feature: dc / services / Show Routing for Serivce
Scenario: Given a service, the Routing tab should display
Given 1 datacenter model with the value "dc1"
And 1 node models
And 1 service model from yaml
---
- Service:
Kind: consul
Name: service-0
ID: service-0-with-id
---
When I visit the service page for yaml
---
dc: dc1
service: service-0
---
And the title should be "service-0 - Consul"
And I see routing on the tabs
Scenario: Given a service proxy, the Routing tab should not display
Given 1 datacenter model with the value "dc1"
And 1 node models
And 1 service model from yaml
---
- Service:
Kind: connect-proxy
Name: service-0-proxy
ID: service-0-proxy-with-id
---
When I visit the service page for yaml
---
dc: dc1
service: service-0-proxy
---
And the title should be "service-0-proxy - Consul"
And I don't see routing on the tabs

View File

@ -2,8 +2,23 @@
@notNamespaceable @notNamespaceable
Feature: settings / show: Show Settings Page Feature: settings / show: Show Settings Page
Scenario: Scenario: I see the Blocking queries
Given 1 datacenter model with the value "datacenter" Given 1 datacenter model with the value "datacenter"
When I visit the settings page When I visit the settings page
Then the url should be /setting Then the url should be /setting
And the title should be "Settings - Consul" And the title should be "Settings - Consul"
And I see blockingQueries
Scenario: Setting CONSUL_UI_DISABLE_REALTIME hides Blocking Queries
Given 1 datacenter model with the value "datacenter"
And settings from yaml
---
CONSUL_UI_DISABLE_REALTIME: 1
---
Then I have settings like yaml
---
CONSUL_UI_DISABLE_REALTIME: "1"
---
When I visit the settings page
Then the url should be /setting
And the title should be "Settings - Consul"
And I don't see blockingQueries

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

@ -58,7 +58,8 @@ module('Integration | Adapter | kv', function(hooks) {
test(`requestForUpdateRecord returns the correct url/method when nspace is ${nspace}`, function(assert) { test(`requestForUpdateRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:kv'); const adapter = this.owner.lookup('adapter:kv');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `PUT /v1/kv/${id}?dc=${dc}${ const flags = 12;
const expected = `PUT /v1/kv/${id}?dc=${dc}&flags=${flags}${
typeof nspace !== 'undefined' ? `&ns=${nspace}` : `` typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``
}`; }`;
let actual = adapter let actual = adapter
@ -70,6 +71,7 @@ module('Integration | Adapter | kv', function(hooks) {
Key: id, Key: id,
Value: '', Value: '',
Namespace: nspace, Namespace: nspace,
Flags: flags,
} }
) )
.split('\n') .split('\n')

View File

@ -1,4 +1,12 @@
import { create, clickable, is, attribute, collection, text } from 'ember-cli-page-object'; import {
create,
clickable,
is,
attribute,
collection,
text,
isPresent,
} from 'ember-cli-page-object';
import { alias } from 'ember-cli-page-object/macros'; import { alias } from 'ember-cli-page-object/macros';
import { visitable } from 'consul-ui/tests/lib/page-object/visitable'; import { visitable } from 'consul-ui/tests/lib/page-object/visitable';
import createDeletable from 'consul-ui/tests/lib/page-object/createDeletable'; import createDeletable from 'consul-ui/tests/lib/page-object/createDeletable';
@ -59,8 +67,10 @@ const roleSelector = roleSelectorFactory(clickable, deletable, collection, alias
export default { export default {
index: create(index(visitable, collection)), index: create(index(visitable, collection)),
dcs: create(dcs(visitable, clickable, attribute, collection)), dcs: create(dcs(visitable, clickable, attribute, collection)),
services: create(services(visitable, clickable, attribute, collection, page, catalogFilter)), services: create(
service: create(service(visitable, attribute, collection, text, catalogFilter)), services(visitable, clickable, attribute, collection, page, catalogFilter, radiogroup)
),
service: create(service(visitable, attribute, collection, text, catalogFilter, radiogroup)),
instance: create(instance(visitable, attribute, collection, text, radiogroup)), instance: create(instance(visitable, attribute, collection, text, radiogroup)),
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)), nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
node: create(node(visitable, deletable, clickable, attribute, collection, radiogroup)), node: create(node(visitable, deletable, clickable, attribute, collection, radiogroup)),
@ -112,5 +122,5 @@ export default {
nspace: create( nspace: create(
nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector) nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector)
), ),
settings: create(settings(visitable, submitable)), settings: create(settings(visitable, submitable, isPresent)),
}; };

View File

@ -1,4 +1,4 @@
import { clickable } from 'ember-cli-page-object'; import { clickable, is } from 'ember-cli-page-object';
const page = { const page = {
navigation: ['services', 'nodes', 'kvs', 'acls', 'intentions', 'docs', 'settings'].reduce( navigation: ['services', 'nodes', 'kvs', 'acls', 'intentions', 'docs', 'settings'].reduce(
function(prev, item, i, arr) { function(prev, item, i, arr) {
@ -24,4 +24,10 @@ const page = {
), ),
}; };
page.navigation.dc = clickable('[data-test-datacenter-menu] button'); page.navigation.dc = clickable('[data-test-datacenter-menu] button');
page.navigation.nspace = clickable('[data-test-nspace-menu] button');
page.navigation.manageNspaces = clickable('[data-test-main-nav-nspaces] a');
page.navigation.manageNspacesIsVisible = is(
':checked',
'[data-test-nspace-menu] > input[type="checkbox"]'
);
export default page; export default page;

View File

@ -1,4 +1,4 @@
export default function(visitable, attribute, collection, text, filter) { export default function(visitable, attribute, collection, text, filter, radiogroup) {
return { return {
visit: visitable('/:dc/services/:service'), visit: visitable('/:dc/services/:service'),
externalSource: attribute('data-test-external-source', 'h1 span'), externalSource: attribute('data-test-external-source', 'h1 span'),
@ -8,6 +8,7 @@ export default function(visitable, attribute, collection, text, filter) {
dashboardAnchor: { dashboardAnchor: {
href: attribute('href', '[data-test-dashboard-anchor]'), href: attribute('href', '[data-test-dashboard-anchor]'),
}, },
tabs: radiogroup('tab', ['instances', 'routing', 'tags']),
filter: filter, filter: filter,
}; };
} }

View File

@ -1,5 +1,6 @@
export default function(visitable, submitable) { export default function(visitable, submitable, isPresent) {
return submitable({ return submitable({
visit: visitable('/setting'), visit: visitable('/setting'),
blockingQueries: isPresent('[data-test-blocking-queries]'),
}); });
} }

View File

@ -1,4 +1,30 @@
/* eslint no-console: "off" */ /* eslint no-console: "off" */
const notFound = 'Element not found';
const cannotDestructure = "Cannot destructure property 'context'";
const cannotReadContext = "Cannot read property 'context' of undefined";
// checking for existence of pageObjects is pretty difficult
// errors are thrown but we should check to make sure its the error that we
// want and not another real error
// to make things more difficult depending on how you reference the pageObject
// an error with a different message is thrown for example:
// pageObject[thing]() will give you a Element not found error
// whereas:
// const obj = pageObject[thing]; obj() will give you a 'cannot destructure error'
// and in CI it will give you a 'cannot read property' error
// the difference in CI could be a difference due to headless vs headed browser
// or difference in Chrome/browser versions
// ideally we wouldn't be checking on error messages at all, but we want to make sure
// that real errors are picked up by the tests, so if this gets unmanageable at any point
// look at checking for the instance of e being TypeError or similar
const isExpectedError = function(e) {
return [notFound, cannotDestructure, cannotReadContext].some(item => e.message.startsWith(item));
};
export default function(scenario, assert, find, currentPage) { export default function(scenario, assert, find, currentPage) {
scenario scenario
.then('I see $property on the $component like yaml\n$yaml', function( .then('I see $property on the $component like yaml\n$yaml', function(
@ -64,34 +90,63 @@ export default function(scenario, assert, find, currentPage) {
); );
}) })
.then(["I don't see $property on the $component"], function(property, component) { .then(["I don't see $property on the $component"], function(property, component) {
// Collection const message = `Expected to not see ${property} on ${component}`;
var obj; // Cope with collections
let obj;
if (typeof currentPage()[component].objectAt === 'function') { if (typeof currentPage()[component].objectAt === 'function') {
obj = currentPage()[component].objectAt(0); obj = currentPage()[component].objectAt(0);
} else { } else {
obj = currentPage()[component]; obj = currentPage()[component];
} }
assert.throws( let prop;
function() { try {
const func = obj[property].bind(obj); prop = obj[property];
func(); } catch (e) {
}, if (isExpectedError(e)) {
function(e) { assert.ok(true, message);
return e.message.startsWith('Element not found'); } else {
}, throw e;
`Expected to not see ${property} on ${component}` }
); }
if (typeof prop === 'function') {
assert.throws(
function() {
prop();
},
function(e) {
return isExpectedError(e);
},
message
);
} else {
assert.notOk(prop);
}
}) })
.then(["I don't see $property"], function(property) { .then(["I don't see $property"], function(property) {
assert.throws( const message = `Expected to not see ${property}`;
function() { let prop;
return currentPage()[property](); try {
}, prop = currentPage()[property];
function(e) { } catch (e) {
return e.message.startsWith('Element not found'); if (isExpectedError(e)) {
}, assert.ok(true, message);
`Expected to not see ${property}` } else {
); throw e;
}
}
if (typeof prop === 'function') {
assert.throws(
function() {
prop();
},
function(e) {
return isExpectedError(e);
},
message
);
} else {
assert.notOk(prop);
}
}) })
.then(['I see $property'], function(property) { .then(['I see $property'], function(property) {
assert.ok(currentPage()[property], `Expected to see ${property}`); assert.ok(currentPage()[property], `Expected to see ${property}`);

View File

@ -3,10 +3,11 @@ import { module, test } from 'qunit';
module('Unit | Search | Filter | node', function() { module('Unit | Search | Filter | node', function() {
const filter = getFilter(cb => cb); const filter = getFilter(cb => cb);
test('items are found by properties', function(assert) { test('items are found by name', function(assert) {
[ [
{ {
Node: 'node-HIT', Node: 'node-HIT',
Address: '10.0.0.0',
}, },
].forEach(function(item) { ].forEach(function(item) {
const actual = filter(item, { const actual = filter(item, {
@ -15,10 +16,24 @@ module('Unit | Search | Filter | node', function() {
assert.ok(actual); assert.ok(actual);
}); });
}); });
test('items are not found', function(assert) { test('items are found by IP address', function(assert) {
[
{
Node: 'node-HIT',
Address: '10.0.0.0',
},
].forEach(function(item) {
const actual = filter(item, {
s: '10',
});
assert.ok(actual);
});
});
test('items are not found by name', function(assert) {
[ [
{ {
Node: 'name', Node: 'name',
Address: '10.0.0.0',
}, },
].forEach(function(item) { ].forEach(function(item) {
const actual = filter(item, { const actual = filter(item, {
@ -27,4 +42,17 @@ module('Unit | Search | Filter | node', function() {
assert.notOk(actual); assert.notOk(actual);
}); });
}); });
test('items are not found by IP address', function(assert) {
[
{
Node: 'name',
Address: '10.0.0.0',
},
].forEach(function(item) {
const actual = filter(item, {
s: '9',
});
assert.notOk(actual);
});
});
}); });

View File

@ -992,9 +992,9 @@
js-yaml "^3.13.1" js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^2.6.2": "@hashicorp/consul-api-double@^2.6.2":
version "2.11.0" version "2.12.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.11.0.tgz#0b833893ccc5cfb9546b1513127d5e92d30f2262" resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.12.0.tgz#725078f770bbd0ef75a5f2498968c5c8891f90a2"
integrity sha512-2MO1jiwuJyPlSGQ4AeFtLKJWmLSj0msoiaRHPtj6YPjm69ZkY/t4U4SU3cfpVn2Dx7wHzXe//9GvNHI1gRxAzg== integrity sha512-8OcgesUjWQ8AjaXzbz3tGJQn1kM0sN6pLidGM7isNPUyYmIjIEXQzaeUQYzsfv0N2Ko9ZuOXYUsaBl8IK1KGow==
"@hashicorp/ember-cli-api-double@^2.0.0": "@hashicorp/ember-cli-api-double@^2.0.0":
version "2.0.0" version "2.0.0"