diff --git a/ui-v2/app/adapters/proxy.js b/ui-v2/app/adapters/proxy.js new file mode 100644 index 000000000000..d4f4f41ea539 --- /dev/null +++ b/ui-v2/app/adapters/proxy.js @@ -0,0 +1,20 @@ +import Adapter from './application'; +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/proxy'; +import { OK as HTTP_OK } from 'consul-ui/utils/http/status'; +export default Adapter.extend({ + urlForQuery: function(query, modelName) { + if (typeof query.id === 'undefined') { + throw new Error('You must specify an id'); + } + // https://www.consul.io/api/catalog.html#list-nodes-for-connect-capable-service + return this.appendURL('catalog/connect', [query.id], this.cleanQuery(query)); + }, + handleResponse: function(status, headers, payload, requestData) { + let response = payload; + if (status === HTTP_OK) { + const url = this.parseURL(requestData.url); + response = this.handleBatchResponse(url, response, PRIMARY_KEY, SLUG_KEY); + } + return this._super(status, headers, response, requestData); + }, +}); diff --git a/ui-v2/app/components/healthcheck-info.js b/ui-v2/app/components/healthcheck-info.js new file mode 100644 index 000000000000..abe1ccedb6d3 --- /dev/null +++ b/ui-v2/app/components/healthcheck-info.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; +export default Component.extend({ + tagName: '', +}); diff --git a/ui-v2/app/components/healthcheck-list.js b/ui-v2/app/components/healthcheck-list.js new file mode 100644 index 000000000000..092a1aadaf04 --- /dev/null +++ b/ui-v2/app/components/healthcheck-list.js @@ -0,0 +1,36 @@ +import Component from '@ember/component'; +import { get } from '@ember/object'; + +export default Component.extend({ + // TODO: Could potentially do this on attr change + actions: { + sortChecksByImportance: function(a, b) { + const statusA = get(a, 'Status'); + const statusB = get(b, 'Status'); + switch (statusA) { + case 'passing': + // a = passing + // unless b is also passing then a is less important + return statusB === 'passing' ? 0 : 1; + case 'critical': + // a = critical + // unless b is also critical then a is more important + return statusB === 'critical' ? 0 : -1; + case 'warning': + // a = warning + switch (statusB) { + // b is passing so a is more important + case 'passing': + return -1; + // b is critical so a is less important + case 'critical': + return 1; + // a and b are both warning, therefore equal + default: + return 0; + } + } + return 0; + }, + }, +}); diff --git a/ui-v2/app/components/healthcheck-output.js b/ui-v2/app/components/healthcheck-output.js new file mode 100644 index 000000000000..227501fc5c67 --- /dev/null +++ b/ui-v2/app/components/healthcheck-output.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + classNames: ['healthcheck-output'], +}); diff --git a/ui-v2/app/components/healthcheck-status.js b/ui-v2/app/components/healthcheck-status.js index 25a646d758c7..367cca6469f1 100644 --- a/ui-v2/app/components/healthcheck-status.js +++ b/ui-v2/app/components/healthcheck-status.js @@ -1,5 +1,12 @@ import Component from '@ember/component'; - +import { get, computed } from '@ember/object'; export default Component.extend({ - classNames: ['healthcheck-status'], + tagName: '', + count: computed('value', function() { + const value = get(this, 'value'); + if (Array.isArray(value)) { + return value.length; + } + return value; + }), }); diff --git a/ui-v2/app/components/tab-nav.js b/ui-v2/app/components/tab-nav.js index 142b50bf4d20..db166df6415e 100644 --- a/ui-v2/app/components/tab-nav.js +++ b/ui-v2/app/components/tab-nav.js @@ -3,4 +3,5 @@ import Component from '@ember/component'; export default Component.extend({ name: 'tab', tagName: 'nav', + classNames: ['tab-nav'], }); diff --git a/ui-v2/app/components/tag-list.js b/ui-v2/app/components/tag-list.js new file mode 100644 index 000000000000..1656e4a23c18 --- /dev/null +++ b/ui-v2/app/components/tag-list.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: 'dl', + classNames: ['tag-list'], +}); diff --git a/ui-v2/app/controllers/dc/services/instance.js b/ui-v2/app/controllers/dc/services/instance.js new file mode 100644 index 000000000000..a8934de52d1b --- /dev/null +++ b/ui-v2/app/controllers/dc/services/instance.js @@ -0,0 +1,17 @@ +import Controller from '@ember/controller'; +import { set } from '@ember/object'; + +export default Controller.extend({ + setProperties: function() { + this._super(...arguments); + // This method is called immediately after `Route::setupController`, and done here rather than there + // as this is a variable used purely for view level things, if the view was different we might not + // need this variable + set(this, 'selectedTab', 'service-checks'); + }, + actions: { + change: function(e) { + set(this, 'selectedTab', e.target.value); + }, + }, +}); diff --git a/ui-v2/app/controllers/dc/services/show.js b/ui-v2/app/controllers/dc/services/show.js index d4653888a56d..3f21a7b36bd6 100644 --- a/ui-v2/app/controllers/dc/services/show.js +++ b/ui-v2/app/controllers/dc/services/show.js @@ -1,38 +1,39 @@ import Controller from '@ember/controller'; -import { get, computed } from '@ember/object'; -import sumOfUnhealthy from 'consul-ui/utils/sumOfUnhealthy'; -import hasStatus from 'consul-ui/utils/hasStatus'; -import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering'; +import { get, set, computed } from '@ember/object'; +import { inject as service } from '@ember/service'; import WithSearching from 'consul-ui/mixins/with-searching'; -export default Controller.extend(WithSearching, WithHealthFiltering, { +export default Controller.extend(WithSearching, { + dom: service('dom'), init: function() { this.searchParams = { - healthyServiceNode: 's', - unhealthyServiceNode: 's', + serviceInstance: 's', }; this._super(...arguments); }, - searchableHealthy: computed('healthy', function() { - return get(this, 'searchables.healthyServiceNode') - .add(get(this, 'healthy')) - .search(get(this, this.searchParams.healthyServiceNode)); - }), - searchableUnhealthy: computed('unhealthy', function() { - return get(this, 'searchables.unhealthyServiceNode') - .add(get(this, 'unhealthy')) - .search(get(this, this.searchParams.unhealthyServiceNode)); - }), - unhealthy: computed('filtered', function() { - return get(this, 'filtered').filter(function(item) { - return sumOfUnhealthy(item.Checks) > 0; - }); - }), - healthy: computed('filtered', function() { - return get(this, 'filtered').filter(function(item) { - return sumOfUnhealthy(item.Checks) === 0; - }); + setProperties: function() { + this._super(...arguments); + // This method is called immediately after `Route::setupController`, and done here rather than there + // as this is a variable used purely for view level things, if the view was different we might not + // need this variable + set(this, 'selectedTab', 'instances'); + }, + searchable: computed('items', function() { + return get(this, 'searchables.serviceInstance') + .add(get(this, 'items')) + .search(get(this, this.searchParams.serviceInstance)); }), - filter: function(item, { s = '', status = '' }) { - return hasStatus(get(item, 'Checks'), status); + actions: { + change: function(e) { + set(this, 'selectedTab', e.target.value); + // Ensure tabular-collections sizing is recalculated + // now it is visible in the DOM + get(this, 'dom') + .components('.tab-section input[type="radio"]:checked + div table') + .forEach(function(item) { + if (typeof item.didAppear === 'function') { + item.didAppear(); + } + }); + }, }, }); diff --git a/ui-v2/app/initializers/search.js b/ui-v2/app/initializers/search.js index 69875fdb114f..ebdd48c926f6 100644 --- a/ui-v2/app/initializers/search.js +++ b/ui-v2/app/initializers/search.js @@ -22,8 +22,7 @@ export function initialize(application) { kv: kv(filterable), healthyNode: node(filterable), unhealthyNode: node(filterable), - healthyServiceNode: serviceNode(filterable), - unhealthyServiceNode: serviceNode(filterable), + serviceInstance: serviceNode(filterable), nodeservice: nodeService(filterable), service: service(filterable), }; diff --git a/ui-v2/app/models/proxy.js b/ui-v2/app/models/proxy.js new file mode 100644 index 000000000000..9e0858219927 --- /dev/null +++ b/ui-v2/app/models/proxy.js @@ -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 = 'ID'; +export default Model.extend({ + [PRIMARY_KEY]: attr('string'), + [SLUG_KEY]: attr('string'), + ServiceName: attr('string'), + ServiceID: attr('string'), + ServiceProxyDestination: attr('string'), +}); diff --git a/ui-v2/app/router.js b/ui-v2/app/router.js index 628a713c7aa2..764bc5f35802 100644 --- a/ui-v2/app/router.js +++ b/ui-v2/app/router.js @@ -18,6 +18,9 @@ export const routes = { show: { _options: { path: '/:name' }, }, + instance: { + _options: { path: '/:name/:id' }, + }, }, // Nodes represent a consul node nodes: { diff --git a/ui-v2/app/routes/dc/services/instance.js b/ui-v2/app/routes/dc/services/instance.js new file mode 100644 index 000000000000..da863ba41814 --- /dev/null +++ b/ui-v2/app/routes/dc/services/instance.js @@ -0,0 +1,29 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { hash } from 'rsvp'; +import { get } from '@ember/object'; + +export default Route.extend({ + repo: service('repository/service'), + proxyRepo: service('repository/proxy'), + model: function(params) { + const repo = get(this, 'repo'); + const proxyRepo = get(this, 'proxyRepo'); + const dc = this.modelFor('dc').dc.Name; + return hash({ + item: repo.findInstanceBySlug(params.id, params.name, dc), + }).then(function(model) { + return hash({ + proxy: + get(service, 'Kind') !== 'connect-proxy' + ? proxyRepo.findInstanceBySlug(params.id, params.name, dc) + : null, + ...model, + }); + }); + }, + setupController: function(controller, model) { + this._super(...arguments); + controller.setProperties(model); + }, +}); diff --git a/ui-v2/app/routes/dc/services/show.js b/ui-v2/app/routes/dc/services/show.js index bf5fa0d6579a..9376abdbc10a 100644 --- a/ui-v2/app/routes/dc/services/show.js +++ b/ui-v2/app/routes/dc/services/show.js @@ -19,6 +19,7 @@ export default Route.extend({ return { ...model, ...{ + // Nodes happen to be the ServiceInstances here items: model.item.Nodes, }, }; diff --git a/ui-v2/app/serializers/proxy.js b/ui-v2/app/serializers/proxy.js new file mode 100644 index 000000000000..7c3c5c42e08c --- /dev/null +++ b/ui-v2/app/serializers/proxy.js @@ -0,0 +1,6 @@ +import Serializer from './application'; +import { PRIMARY_KEY } from 'consul-ui/models/proxy'; + +export default Serializer.extend({ + primaryKey: PRIMARY_KEY, +}); diff --git a/ui-v2/app/services/dom.js b/ui-v2/app/services/dom.js index a3bda1c8c8a2..740406cb5982 100644 --- a/ui-v2/app/services/dom.js +++ b/ui-v2/app/services/dom.js @@ -70,7 +70,9 @@ export default Service.extend({ // with traditional/standard web components you wouldn't actually need this // method as you could just get to their methods from the dom element component: function(selector, context) { - // TODO: support passing a dom element, when we need to do that + if (typeof selector !== 'string') { + return $_(selector); + } return $_(this.element(selector, context)); }, components: function(selector, context) { diff --git a/ui-v2/app/services/repository/proxy.js b/ui-v2/app/services/repository/proxy.js new file mode 100644 index 000000000000..ce8c055d83f1 --- /dev/null +++ b/ui-v2/app/services/repository/proxy.js @@ -0,0 +1,33 @@ +import RepositoryService from 'consul-ui/services/repository'; +import { PRIMARY_KEY } from 'consul-ui/models/proxy'; +import { get } from '@ember/object'; +const modelName = 'proxy'; +export default RepositoryService.extend({ + getModelName: function() { + return modelName; + }, + getPrimaryKey: function() { + return PRIMARY_KEY; + }, + findAllBySlug: function(slug, dc, configuration = {}) { + const query = { + id: slug, + dc: dc, + }; + if (typeof configuration.cursor !== 'undefined') { + query.index = configuration.cursor; + } + return this.get('store').query(this.getModelName(), query); + }, + findInstanceBySlug: function(id, slug, dc, configuration) { + return this.findAllBySlug(slug, dc, configuration).then(function(items) { + if (get(items, 'length') > 0) { + const instance = items.findBy('ServiceProxyDestination', id); + if (instance) { + return instance; + } + } + return; + }); + }, +}); diff --git a/ui-v2/app/services/repository/service.js b/ui-v2/app/services/repository/service.js index 5654c3a61837..2da90a3c7265 100644 --- a/ui-v2/app/services/repository/service.js +++ b/ui-v2/app/services/repository/service.js @@ -7,16 +7,35 @@ export default RepositoryService.extend({ }, findBySlug: function(slug, dc) { return this._super(...arguments).then(function(item) { - const nodes = get(item, 'Nodes'); - const service = get(nodes, 'firstObject'); - const tags = nodes - .reduce(function(prev, item) { - return prev.concat(get(item, 'Service.Tags') || []); - }, []) - .uniq(); - set(service, 'Tags', tags); - set(service, 'Nodes', nodes); - return service; + const nodes = get(item, 'Nodes'); + const service = get(nodes, 'firstObject'); + const tags = nodes + .reduce(function(prev, item) { + return prev.concat(get(item, 'Service.Tags') || []); + }, []) + .uniq(); + set(service, 'Tags', tags); + set(service, 'Nodes', nodes); + return service; + }); + }, + findInstanceBySlug: function(id, slug, dc, configuration) { + return this.findBySlug(slug, dc, configuration).then(function(item) { + const i = item.Nodes.findIndex(function(item) { + return item.Service.ID === id; }); + if (i !== -1) { + const service = item.Nodes[i].Service; + service.Node = item.Nodes[i].Node; + service.ServiceChecks = item.Nodes[i].Checks.filter(function(item) { + return item.ServiceID != ''; + }); + service.NodeChecks = item.Nodes[i].Checks.filter(function(item) { + return item.ServiceID == ''; + }); + return service; + } + // TODO: probably need to throw a 404 here? + }); }, }); diff --git a/ui-v2/app/styles/components/app-view/layout.scss b/ui-v2/app/styles/components/app-view/layout.scss index 34827cc5bf55..f518b5d38d34 100644 --- a/ui-v2/app/styles/components/app-view/layout.scss +++ b/ui-v2/app/styles/components/app-view/layout.scss @@ -10,6 +10,15 @@ display: flex; align-items: flex-start; } +%app-view header dl { + float: left; + margin-top: 25px; + margin-right: 50px; + margin-bottom: 20px; +} +%app-view header dt { + font-weight: bold; +} /* units */ %app-view { margin-top: 50px; diff --git a/ui-v2/app/styles/components/app-view/skin.scss b/ui-v2/app/styles/components/app-view/skin.scss index e0269410ff2d..a117fa1258e8 100644 --- a/ui-v2/app/styles/components/app-view/skin.scss +++ b/ui-v2/app/styles/components/app-view/skin.scss @@ -1,10 +1,28 @@ -%app-view h2, -%app-view header > div:last-of-type { - border-bottom: $decor-border-100; +%app-view h2 { + border-bottom: $decor-border-200; +} +@media #{$--horizontal-selects} { + %app-view header h1 { + border-bottom: $decor-border-200; + } } -%app-view header > div:last-of-type, +@media #{$--lt-horizontal-selects} { + %app-view header > div > div:last-child { + border-bottom: $decor-border-200; + } +} +%app-view header > div > div:last-child, +%app-view header h1, %app-view h2 { - border-color: $keyline-light; + border-color: $gray-200; +} +// We know that any sibling navs might have a top border +// by default. As its squashed up to a h1, in this +// case hide its border to avoid double border +@media #{$--horizontal-selects} { + %app-view header h1 ~ nav { + border-top: 0 !important; + } } %app-content div > dl > dd { color: $gray-400; diff --git a/ui-v2/app/styles/components/breadcrumbs/skin.scss b/ui-v2/app/styles/components/breadcrumbs/skin.scss index 6bbe41f28847..fb86355e9244 100644 --- a/ui-v2/app/styles/components/breadcrumbs/skin.scss +++ b/ui-v2/app/styles/components/breadcrumbs/skin.scss @@ -1,9 +1,18 @@ -%breadcrumbs a { +%breadcrumbs li > * { @extend %with-chevron; } +%breadcrumbs li > strong::before { + color: $gray-300; +} +%breadcrumbs li > a::before { + color: rgba($color-action, 0.5); +} %breadcrumbs ol { list-style-type: none; } %breadcrumbs a { color: $color-action; } +%breadcrumbs strong { + color: $gray-400; +} diff --git a/ui-v2/app/styles/components/form-elements.scss b/ui-v2/app/styles/components/form-elements.scss index 942efbb8cba9..c0bd6fa6e0a6 100644 --- a/ui-v2/app/styles/components/form-elements.scss +++ b/ui-v2/app/styles/components/form-elements.scss @@ -24,7 +24,7 @@ form table, %app-content form dl { @extend %form-row; } -%app-content [role='radiogroup'] { +%app-content form:not(.filter-bar) [role='radiogroup'] { @extend %radio-group; } %radio-group label { diff --git a/ui-v2/app/styles/components/healthcheck-info.scss b/ui-v2/app/styles/components/healthcheck-info.scss new file mode 100644 index 000000000000..a249d32c4929 --- /dev/null +++ b/ui-v2/app/styles/components/healthcheck-info.scss @@ -0,0 +1,12 @@ +@import './healthcheck-info/index'; +@import './icons/index'; +tr dl { + @extend %healthcheck-info; +} +td span.zero { + @extend %with-no-healthchecks; + // TODO: Why isn't this is layout? + display: block; + text-indent: 20px; + color: $gray-400; +} diff --git a/ui-v2/app/styles/components/healthcheck-status/index.scss b/ui-v2/app/styles/components/healthcheck-info/index.scss similarity index 100% rename from ui-v2/app/styles/components/healthcheck-status/index.scss rename to ui-v2/app/styles/components/healthcheck-info/index.scss diff --git a/ui-v2/app/styles/components/healthcheck-info/layout.scss b/ui-v2/app/styles/components/healthcheck-info/layout.scss new file mode 100644 index 000000000000..0f084db303a5 --- /dev/null +++ b/ui-v2/app/styles/components/healthcheck-info/layout.scss @@ -0,0 +1,32 @@ +%healthcheck-info { + display: flex; + height: 100%; + float: left; +} +%healthcheck-info > * { + display: block; +} +%healthcheck-info dt.zero { + display: none; +} +%healthcheck-info dd.zero { + visibility: hidden; +} +%healthcheck-info dt { + text-indent: -9000px; +} +%healthcheck-info dt.warning { + overflow: visible; +} +%healthcheck-info dt.warning::before { + top: 7px; +} +%healthcheck-info dt.warning::after { + left: -2px; + top: -1px; +} +%healthcheck-info dd { + box-sizing: content-box; + margin-left: 22px; + padding-right: 10px; +} diff --git a/ui-v2/app/styles/components/healthcheck-info/skin.scss b/ui-v2/app/styles/components/healthcheck-info/skin.scss new file mode 100644 index 000000000000..9b22b05f01d7 --- /dev/null +++ b/ui-v2/app/styles/components/healthcheck-info/skin.scss @@ -0,0 +1,21 @@ +%healthcheck-info dt.passing { + @extend %with-passing; +} +%healthcheck-info dt.warning { + @extend %with-warning; +} +%healthcheck-info dt.critical { + @extend %with-critical; +} +%healthcheck-info dt.passing, +%healthcheck-info dt.passing + dd { + color: $color-success; +} +%healthcheck-info dt.warning, +%healthcheck-info dt.warning + dd { + color: $color-alert; +} +%healthcheck-info dt.critical, +%healthcheck-info dt.critical + dd { + color: $color-failure; +} diff --git a/ui-v2/app/styles/components/healthcheck-status.scss b/ui-v2/app/styles/components/healthcheck-output.scss similarity index 56% rename from ui-v2/app/styles/components/healthcheck-status.scss rename to ui-v2/app/styles/components/healthcheck-output.scss index 550b2c992aa7..96216d631597 100644 --- a/ui-v2/app/styles/components/healthcheck-status.scss +++ b/ui-v2/app/styles/components/healthcheck-output.scss @@ -1,32 +1,32 @@ -@import './healthcheck-status/index'; +@import './healthcheck-output/index'; @import './icons/index'; -.healthcheck-status { - @extend %healthcheck-status; +.healthcheck-output { + @extend %healthcheck-output; } -%healthcheck-status.passing { +%healthcheck-output.passing { @extend %with-passing; } -%healthcheck-status.warning { +%healthcheck-output.warning { @extend %with-warning; } -%healthcheck-status.critical { +%healthcheck-output.critical { @extend %with-critical; } -@media #{$--lt-spacious-healthcheck-status} { - .healthcheck-status button.copy-btn { +@media #{$--lt-spacious-healthcheck-output} { + .healthcheck-output button.copy-btn { margin-top: -11px; margin-right: -18px; padding: 0; width: 20px; visibility: hidden; } - %healthcheck-status { + %healthcheck-output { padding-left: 30px; padding-top: 10px; padding-bottom: 15px; padding-right: 13px; } - %healthcheck-status::before { + %healthcheck-output::before { width: 15px !important; height: 15px !important; left: 9px; diff --git a/ui-v2/app/styles/components/healthcheck-output/index.scss b/ui-v2/app/styles/components/healthcheck-output/index.scss new file mode 100644 index 000000000000..bc182521964a --- /dev/null +++ b/ui-v2/app/styles/components/healthcheck-output/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/healthcheck-status/layout.scss b/ui-v2/app/styles/components/healthcheck-output/layout.scss similarity index 63% rename from ui-v2/app/styles/components/healthcheck-status/layout.scss rename to ui-v2/app/styles/components/healthcheck-output/layout.scss index ef40daf3757f..5f1a9403debb 100644 --- a/ui-v2/app/styles/components/healthcheck-status/layout.scss +++ b/ui-v2/app/styles/components/healthcheck-output/layout.scss @@ -1,4 +1,4 @@ -%healthcheck-status::before { +%healthcheck-output::before { background-size: 55%; width: 25px !important; height: 25px !important; @@ -6,25 +6,25 @@ top: 20px !important; margin-top: 0 !important; } -%healthcheck-status.warning::before { +%healthcheck-output.warning::before { background-size: 100%; } -%healthcheck-status { +%healthcheck-output { padding: 20px 24px; padding-bottom: 26px; padding-left: 57px; margin-bottom: 24px; position: relative; } -%healthcheck-status pre { +%healthcheck-output pre { padding: 12px; } -%healthcheck-status .with-feedback { +%healthcheck-output .with-feedback { float: right; } -%healthcheck-status dt { +%healthcheck-output dt { margin-bottom: 0.2em; } -%healthcheck-status dd:first-of-type { +%healthcheck-output dd:first-of-type { margin-bottom: 0.6em; } diff --git a/ui-v2/app/styles/components/healthcheck-status/skin.scss b/ui-v2/app/styles/components/healthcheck-output/skin.scss similarity index 60% rename from ui-v2/app/styles/components/healthcheck-status/skin.scss rename to ui-v2/app/styles/components/healthcheck-output/skin.scss index d0fd2cec13d4..9d26d4d66317 100644 --- a/ui-v2/app/styles/components/healthcheck-status/skin.scss +++ b/ui-v2/app/styles/components/healthcheck-output/skin.scss @@ -1,35 +1,35 @@ -%healthcheck-status { +%healthcheck-output { border-width: 1px; } -%healthcheck-status, -%healthcheck-status pre { +%healthcheck-output, +%healthcheck-output pre { border-radius: $decor-radius-100; } -%healthcheck-status dd:first-of-type { +%healthcheck-output dd:first-of-type { color: $gray-400; } -%healthcheck-status pre { +%healthcheck-output pre { background-color: $black; color: $white; } -%healthcheck-status.passing { +%healthcheck-output.passing { /* TODO: this should be a frame-gray */ // @extend %frame-green-500; color: $gray-900; border-color: $gray-200; border-style: solid; } -%healthcheck-status.warning { +%healthcheck-output.warning { @extend %frame-yellow-500; color: $gray-900; } -%healthcheck-status.critical { +%healthcheck-output.critical { @extend %frame-red-500; color: $gray-900; } -%healthcheck-status.passing::before { +%healthcheck-output.passing::before { background-color: $color-success !important; } -%healthcheck-status.critical::before { +%healthcheck-output.critical::before { background-color: $color-danger !important; } diff --git a/ui-v2/app/styles/components/icons/index.scss b/ui-v2/app/styles/components/icons/index.scss index acc1eb7d6fd0..4c61d11f3cf6 100644 --- a/ui-v2/app/styles/components/icons/index.scss +++ b/ui-v2/app/styles/components/icons/index.scss @@ -93,12 +93,11 @@ } %with-chevron::before { @extend %pseudo-icon; - background-image: url('data:image/svg+xml;charset=UTF-8,'); + content: '❮'; width: 6px; - height: 9px; + background-color: transparent; left: 0; - margin-top: -4px; - background-color: $color-transparent; + font-size: 0.7rem; } %with-folder::before { @extend %pseudo-icon; diff --git a/ui-v2/app/styles/components/index.scss b/ui-v2/app/styles/components/index.scss index f461f7bea1c8..edfda9d16efd 100644 --- a/ui-v2/app/styles/components/index.scss +++ b/ui-v2/app/styles/components/index.scss @@ -16,7 +16,9 @@ @import './app-view'; @import './product'; -@import './healthcheck-status'; +@import './tag-list'; +@import './healthcheck-output'; +@import './healthcheck-info'; @import './healthchecked-resource'; @import './freetext-filter'; @import './filter-bar'; diff --git a/ui-v2/app/styles/components/pill.scss b/ui-v2/app/styles/components/pill.scss index 4d8f0673a1e2..af1809c33957 100644 --- a/ui-v2/app/styles/components/pill.scss +++ b/ui-v2/app/styles/components/pill.scss @@ -1,4 +1,5 @@ @import './pill/index'; -td strong { +td strong, +%tag-list span { @extend %pill; } diff --git a/ui-v2/app/styles/components/table.scss b/ui-v2/app/styles/components/table.scss index 364c214b6532..749774ad1a08 100644 --- a/ui-v2/app/styles/components/table.scss +++ b/ui-v2/app/styles/components/table.scss @@ -1,41 +1,30 @@ @import './icons/index'; @import './table/index'; + +html.template-service.template-list td:first-child a span, +html.template-node.template-show #services td:first-child a span, +html.template-service.template-show #instances td:first-child a span { + @extend %with-external-source-icon; + float: left; + margin-right: 10px; + margin-top: 2px; +} +/* This nudges the th in for the external source icons */ +html.template-node.template-show #services th:first-child, +html.template-service.template-show #instances th:first-child, +html.template-service.template-list main th:first-child { + text-indent: 28px; +} + td.folder { @extend %with-folder; } -td dt.passing { - @extend %with-passing; -} -td dt.warning { - @extend %with-warning; -} -td dt.critical { - @extend %with-critical; -} -td span.zero { - @extend %with-no-healthchecks; - display: block; - text-indent: 20px; - color: $gray-400; -} table:not(.sessions) tr { cursor: pointer; } table:not(.sessions) td:first-child { padding: 0; } -td dt.passing, -td dt.passing + dd { - color: $color-success; -} -td dt.warning, -td dt.warning + dd { - color: $color-alert; -} -td dt.critical, -td dt.critical + dd { - color: $color-failure; -} /* Header Tooltips/Icon*/ th { overflow: visible; diff --git a/ui-v2/app/styles/components/table/layout.scss b/ui-v2/app/styles/components/table/layout.scss index 23301b423eac..2706e64dc5aa 100644 --- a/ui-v2/app/styles/components/table/layout.scss +++ b/ui-v2/app/styles/components/table/layout.scss @@ -31,7 +31,7 @@ table th { padding-bottom: 0.6em; } table td, -table td a { +table td:first-child a { padding: 0.9em 0; } table th, @@ -50,44 +50,6 @@ td:not(.actions) a { overflow: hidden; } -// TODO: this isn't specific to table -// these are the node health 3 column display -tr > * dl { - float: left; -} -td dl { - height: 100%; -} -td dl { - display: flex; -} -td dl > * { - display: block; -} -td dt.zero { - display: none; -} -td dd.zero { - visibility: hidden; -} -td dt { - text-indent: -9000px; -} -td dt.warning { - overflow: visible; -} -td dt.warning::before { - top: 7px; -} -td dt.warning::after { - left: -2px; - top: -1px; -} -td dd { - box-sizing: content-box; - margin-left: 22px; - padding-right: 10px; -} /* hide actions on narrow screens, you can always click in do everything from there */ @media #{$--lt-wide-table} { tr > .actions { @@ -96,6 +58,8 @@ td dd { } /* ideally these would be in route css files, but left here as they */ /* accomplish the same thing (hide non-essential columns for tables) */ +/* TODO: Move these to component/table.scss for the moment */ +/* Also mixed with things in component/tabular-collection.scss move those also */ @media #{$--lt-medium-table} { /* Policy > Datacenters */ html.template-policy.template-list tr > :nth-child(2) { diff --git a/ui-v2/app/styles/components/tabs.scss b/ui-v2/app/styles/components/tabs.scss index b0c08a7f8c5a..64a1b9138ef0 100644 --- a/ui-v2/app/styles/components/tabs.scss +++ b/ui-v2/app/styles/components/tabs.scss @@ -1,5 +1,5 @@ @import './tabs/index'; -main header nav:last-of-type:not(:first-of-type) { +.tab-nav { @extend %tab-nav; } .tab-section { diff --git a/ui-v2/app/styles/components/tabs/layout.scss b/ui-v2/app/styles/components/tabs/layout.scss index 9588b870ebf5..7b20b1aa1f7d 100644 --- a/ui-v2/app/styles/components/tabs/layout.scss +++ b/ui-v2/app/styles/components/tabs/layout.scss @@ -2,6 +2,9 @@ /* this keeps in-tab-section toolbars flush to the top, see Node Detail > Services */ margin-top: 0 !important; } +%tab-nav { + clear: both; +} @media #{$--horizontal-tabs} { %tab-nav ul { display: flex; diff --git a/ui-v2/app/styles/components/tabs/skin.scss b/ui-v2/app/styles/components/tabs/skin.scss index 81faad36953b..1538bcf0d853 100644 --- a/ui-v2/app/styles/components/tabs/skin.scss +++ b/ui-v2/app/styles/components/tabs/skin.scss @@ -1,3 +1,12 @@ +%tab-nav { + /* %frame-gray-something */ + border-bottom: $decor-border-100; + border-top: $decor-border-200; +} +%tab-nav { + /* %frame-gray-something */ + border-color: $gray-200; +} %tab-nav label { cursor: pointer; } diff --git a/ui-v2/app/styles/components/tabular-collection.scss b/ui-v2/app/styles/components/tabular-collection.scss index c9745e787f3f..53d6678ef64e 100644 --- a/ui-v2/app/styles/components/tabular-collection.scss +++ b/ui-v2/app/styles/components/tabular-collection.scss @@ -35,17 +35,16 @@ table.dom-recycling { /* using: */ /* calc(<100% divided by number of non-fixed width cells> - ) */ -html.template-service.template-list td:first-child a span, -html.template-node.template-show #services td:first-child a span { - @extend %with-external-source-icon; - float: left; - margin-right: 10px; - margin-top: 2px; -} /*TODO: trs only live in tables, get rid of table */ html.template-service.template-list main table tr { @extend %services-row; } +html.template-service.template-show #instances table tr { + @extend %instances-row; +} +html.template-instance.template-show #upstreams table tr { + @extend %upstreams-row; +} html.template-intention.template-list main table tr { @extend %intentions-row; } @@ -146,6 +145,12 @@ html.template-node.template-show main table.sessions tr { html.template-token.template-list main table tr td.me ~ td:nth-of-type(5) { display: none; } + html.template-service.template-show #instances tr > :nth-child(3) { + display: none; + } + %instances-row > * { + width: calc(100% / 4); + } } %kvs-row > *:first-child { @@ -155,7 +160,7 @@ html.template-node.template-show main table.sessions tr { @extend %table-actions; } %node-services-row > * { - width: 33%; + width: calc(100% / 3); } %policies-row > * { width: calc(33% - 20px); @@ -172,3 +177,9 @@ html.template-node.template-show main table.sessions tr { %services-row > * { width: auto; } +%instances-row > * { + width: calc(100% / 5); +} +%upstreams-row > * { + width: calc(100% / 3); +} diff --git a/ui-v2/app/styles/components/tag-list.scss b/ui-v2/app/styles/components/tag-list.scss new file mode 100644 index 000000000000..6bc2ea8e709d --- /dev/null +++ b/ui-v2/app/styles/components/tag-list.scss @@ -0,0 +1,5 @@ +@import './tag-list/index'; +.tag-list, +td.tags { + @extend %tag-list; +} diff --git a/ui-v2/app/styles/components/tag-list/index.scss b/ui-v2/app/styles/components/tag-list/index.scss new file mode 100644 index 000000000000..bc182521964a --- /dev/null +++ b/ui-v2/app/styles/components/tag-list/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/tag-list/layout.scss b/ui-v2/app/styles/components/tag-list/layout.scss new file mode 100644 index 000000000000..2590f8b4c144 --- /dev/null +++ b/ui-v2/app/styles/components/tag-list/layout.scss @@ -0,0 +1,10 @@ +%tag-list dt { + display: none; +} +// TODO: Currently this is here to overwrite +// the default definition list layout used in edit pages +// ideally we'd be more specific with those to say +// only add padding to dl's in edit pages +%tag-list dd { + padding-left: 0; +} diff --git a/ui-v2/app/styles/components/tag-list/skin.scss b/ui-v2/app/styles/components/tag-list/skin.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/ui-v2/app/styles/core/typography.scss b/ui-v2/app/styles/core/typography.scss index 2331c49c901c..bc945842a88f 100644 --- a/ui-v2/app/styles/core/typography.scss +++ b/ui-v2/app/styles/core/typography.scss @@ -36,10 +36,10 @@ h1, h2, %header-nav, %healthchecked-resource header span, -%healthcheck-status dt, +%healthcheck-output dt, %copy-button, %app-content div > dl > dt, -td a { +td:first-child a { font-weight: $typo-weight-semibold; } %form-element > span, @@ -51,7 +51,7 @@ caption { font-weight: $typo-weight-semibold !important; } th, -%breadcrumbs a, +%breadcrumbs li > *, %action-group-action, %tab-nav, %tooltip-bubble { diff --git a/ui-v2/app/styles/routes/dc/service/index.scss b/ui-v2/app/styles/routes/dc/service/index.scss index c38a957a9ddf..e69de29bb2d1 100644 --- a/ui-v2/app/styles/routes/dc/service/index.scss +++ b/ui-v2/app/styles/routes/dc/service/index.scss @@ -1,17 +0,0 @@ -@import '../../../components/pill/index'; -html.template-service.template-show main dl { - display: flex; - margin-bottom: 1.4em; -} -html.template-service.template-show main dt { - display: none; -} -// TODO: Generalize this, also see nodes/index -html.template-service.template-list td.tags span, -html.template-service.template-show main dd span { - @extend %pill; -} -html.template-node.template-show #services th:first-child, -html.template-service.template-list main th:first-child { - text-indent: 28px; -} diff --git a/ui-v2/app/styles/variables/custom-query.scss b/ui-v2/app/styles/variables/custom-query.scss index 56895ef26631..8e7160e678c3 100644 --- a/ui-v2/app/styles/variables/custom-query.scss +++ b/ui-v2/app/styles/variables/custom-query.scss @@ -26,8 +26,8 @@ $--lt-wide-footer: '(max-width: 420px)'; $--spacious-page-header: '(min-width: 850px)'; $--lt-spacious-page-header: '(max-width: 849px)'; -$--spacious-healthcheck-status: '(min-width: 421px)'; -$--lt-spacious-healthcheck-status: '(max-width: 420px)'; +$--spacious-healthcheck-output: '(min-width: 421px)'; +$--lt-spacious-healthcheck-output: '(max-width: 420px)'; $--wide-form: '(min-width: 421px)'; $--lt-wide-form: '(max-width: 420px)'; diff --git a/ui-v2/app/templates/components/healthcheck-info.hbs b/ui-v2/app/templates/components/healthcheck-info.hbs new file mode 100644 index 000000000000..13b62ac08c33 --- /dev/null +++ b/ui-v2/app/templates/components/healthcheck-info.hbs @@ -0,0 +1,9 @@ +{{#if (and (lt passing 1) (lt warning 1) (lt critical 1) )}} + 0 +{{else}} +
+ {{healthcheck-status width=passingWidth name='passing' value=passing}} + {{healthcheck-status width=warningWidth name='warning' value=warning}} + {{healthcheck-status width=criticalWidth name='critical' value=critical}} +
+{{/if}} diff --git a/ui-v2/app/templates/components/healthcheck-list.hbs b/ui-v2/app/templates/components/healthcheck-list.hbs new file mode 100644 index 000000000000..4b5774588e07 --- /dev/null +++ b/ui-v2/app/templates/components/healthcheck-list.hbs @@ -0,0 +1,5 @@ + diff --git a/ui-v2/app/templates/components/healthcheck-output.hbs b/ui-v2/app/templates/components/healthcheck-output.hbs new file mode 100644 index 000000000000..05a75e40a249 --- /dev/null +++ b/ui-v2/app/templates/components/healthcheck-output.hbs @@ -0,0 +1,25 @@ +{{#feedback-dialog type='inline'}} + {{#block-slot 'action' as |success error|}} + {{#copy-button success=(action success) error=(action error) clipboardText=output title='copy output to clipboard'}} + Copy Output + {{/copy-button}} + {{/block-slot}} + {{#block-slot 'success' as |transition|}} +

+ Copied IP Address! +

+ {{/block-slot}} + {{#block-slot 'error' as |transition|}} +

+ Sorry, something went wrong! +

+ {{/block-slot}} +{{/feedback-dialog}} +
+
{{name}}
+
{{notes}}
+
Output
+
+
{{output}}
+
+
\ No newline at end of file diff --git a/ui-v2/app/templates/components/healthcheck-status.hbs b/ui-v2/app/templates/components/healthcheck-status.hbs index 05a75e40a249..383f67386c56 100644 --- a/ui-v2/app/templates/components/healthcheck-status.hbs +++ b/ui-v2/app/templates/components/healthcheck-status.hbs @@ -1,25 +1,3 @@ -{{#feedback-dialog type='inline'}} - {{#block-slot 'action' as |success error|}} - {{#copy-button success=(action success) error=(action error) clipboardText=output title='copy output to clipboard'}} - Copy Output - {{/copy-button}} - {{/block-slot}} - {{#block-slot 'success' as |transition|}} -

- Copied IP Address! -

- {{/block-slot}} - {{#block-slot 'error' as |transition|}} -

- Sorry, something went wrong! -

- {{/block-slot}} -{{/feedback-dialog}} -
-
{{name}}
-
{{notes}}
-
Output
-
-
{{output}}
-
-
\ No newline at end of file +{{!-- we use concat here to avoid ember adding returns between words, which causes a layout issue--}} +
{{ concat 'Healthchecks ' (capitalize name) }}
+
{{format-number count}}
\ No newline at end of file diff --git a/ui-v2/app/templates/components/tag-list.hbs b/ui-v2/app/templates/components/tag-list.hbs new file mode 100644 index 000000000000..c51ea2a41883 --- /dev/null +++ b/ui-v2/app/templates/components/tag-list.hbs @@ -0,0 +1,8 @@ +{{#if (gt items.length 0)}} +
Tags
+
+ {{#each items as |item|}} + {{item}} + {{/each}} +
+{{/if}} diff --git a/ui-v2/app/templates/dc/nodes/-healthchecks.hbs b/ui-v2/app/templates/dc/nodes/-healthchecks.hbs index a956fad63133..19acae8c38e7 100644 --- a/ui-v2/app/templates/dc/nodes/-healthchecks.hbs +++ b/ui-v2/app/templates/dc/nodes/-healthchecks.hbs @@ -1,9 +1,5 @@ {{#if (gt item.Checks.length 0) }} - + {{healthcheck-list items=item.Checks}} {{else}}

This node has no health checks. diff --git a/ui-v2/app/templates/dc/nodes/-services.hbs b/ui-v2/app/templates/dc/nodes/-services.hbs index 856e51b172de..e692bb0d522e 100644 --- a/ui-v2/app/templates/dc/nodes/-services.hbs +++ b/ui-v2/app/templates/dc/nodes/-services.hbs @@ -19,7 +19,7 @@ - {{item.Service}}{{#if (not-eq item.ID item.Service) }}({{item.ID}}){{/if}} + {{item.Service}}{{#if (not-eq item.ID item.Service) }} ({{item.ID}}){{/if}} diff --git a/ui-v2/app/templates/dc/services/-instances.hbs b/ui-v2/app/templates/dc/services/-instances.hbs new file mode 100644 index 000000000000..7ef34c3400e8 --- /dev/null +++ b/ui-v2/app/templates/dc/services/-instances.hbs @@ -0,0 +1,55 @@ +{{#if (gt items.length 0) }} + +

+ {{freetext-filter searchable=searchable value=s placeholder="Search"}} +
+{{/if}} + {{#changeable-set dispatcher=searchable}} + {{#block-slot 'set' as |filtered|}} + {{#tabular-collection + data-test-instances + items=filtered as |item index| + }} + {{#block-slot 'header'}} + ID + Node + Address + Node Checks + Service Checks + {{/block-slot}} + {{#block-slot 'row'}} + + + + {{ or item.Service.ID item.Service.Service }} + + + + {{item.Node.Node}} + + + {{item.Service.Address}}:{{item.Service.Port}} + + + {{#with (reject-by 'ServiceID' '' item.Checks) as |checks|}} + {{healthcheck-info + passing=(filter-by 'Status' 'passing' checks) warning=(filter-by 'Status' 'warning' checks) critical=(filter-by 'Status' 'critical' checks) + }} + {{/with}} + + + {{#with (filter-by 'ServiceID' '' item.Checks) as |checks|}} + {{healthcheck-info + passing=(filter-by 'Status' 'passing' checks) warning=(filter-by 'Status' 'warning' checks) critical=(filter-by 'Status' 'critical' checks) + }} + {{/with}} + + {{/block-slot}} + {{/tabular-collection}} + {{/block-slot}} + {{#block-slot 'empty'}} +

+ There are no services. +

+ {{/block-slot}} + {{/changeable-set}} diff --git a/ui-v2/app/templates/dc/services/-nodechecks.hbs b/ui-v2/app/templates/dc/services/-nodechecks.hbs new file mode 100644 index 000000000000..487db34fc9e9 --- /dev/null +++ b/ui-v2/app/templates/dc/services/-nodechecks.hbs @@ -0,0 +1,8 @@ +{{#if (gt item.NodeChecks.length 0) }} + {{healthcheck-list items=item.NodeChecks}} +{{else}} +

+ This instance has no node health checks. +

+{{/if}} + diff --git a/ui-v2/app/templates/dc/services/-servicechecks.hbs b/ui-v2/app/templates/dc/services/-servicechecks.hbs new file mode 100644 index 000000000000..424772e70591 --- /dev/null +++ b/ui-v2/app/templates/dc/services/-servicechecks.hbs @@ -0,0 +1,8 @@ +{{#if (gt item.ServiceChecks.length 0) }} + {{healthcheck-list items=item.ServiceChecks}} +{{else}} +

+ This instance has no service health checks. +

+{{/if}} + diff --git a/ui-v2/app/templates/dc/services/-tags.hbs b/ui-v2/app/templates/dc/services/-tags.hbs new file mode 100644 index 000000000000..c0a3a0f7830a --- /dev/null +++ b/ui-v2/app/templates/dc/services/-tags.hbs @@ -0,0 +1,7 @@ +{{#if (gt item.Tags.length 0) }} +{{tag-list items=item.Tags}} +{{else}} +

+ There are no tags. +

+{{/if}} diff --git a/ui-v2/app/templates/dc/services/-upstreams.hbs b/ui-v2/app/templates/dc/services/-upstreams.hbs new file mode 100644 index 000000000000..f4ad6fcc8139 --- /dev/null +++ b/ui-v2/app/templates/dc/services/-upstreams.hbs @@ -0,0 +1,27 @@ +{{#if (gt item.Proxy.Upstreams.length 0) }} +{{#tabular-collection + data-test-upstreams + items=item.Proxy.Upstreams as |item index| +}} + {{#block-slot 'header'}} + Destination Name + Destination Type + Local Bind Port + {{/block-slot}} + {{#block-slot 'row'}} + + {{item.DestinationName}} + + + {{item.DestinationType}} + + + {{item.LocalBindPort}} + + {{/block-slot}} +{{/tabular-collection}} +{{else}} +

+ There are no upstreams. +

+{{/if}} diff --git a/ui-v2/app/templates/dc/services/index.hbs b/ui-v2/app/templates/dc/services/index.hbs index aee20a971833..7306ea10f314 100644 --- a/ui-v2/app/templates/dc/services/index.hbs +++ b/ui-v2/app/templates/dc/services/index.hbs @@ -35,18 +35,10 @@ - {{#if (and (lt item.ChecksPassing 1) (lt item.ChecksWarning 1) (lt item.ChecksCritical 1) )}} - 0 - {{else}} -
-
Healthchecks Passing
-
{{format-number item.ChecksPassing}}
-
Healthchecks Warning
-
{{format-number item.ChecksWarning}}
-
Healthchecks Critical
-
{{format-number item.ChecksCritical}}
-
- {{/if}} + {{healthcheck-info + passing=item.ChecksPassing warning=item.ChecksWarning critical=item.ChecksCritical + passingWidth=passingWidth warningWidth=warningWidth criticalWidth=criticalWidth + }} {{#if (gt item.Tags.length 0)}} diff --git a/ui-v2/app/templates/dc/services/instance.hbs b/ui-v2/app/templates/dc/services/instance.hbs new file mode 100644 index 000000000000..063855210426 --- /dev/null +++ b/ui-v2/app/templates/dc/services/instance.hbs @@ -0,0 +1,72 @@ +{{#app-view class="instance show"}} + {{#block-slot 'breadcrumbs'}} +
    +
  1. All Services
  2. +
  3. Service ({{item.Service}})
  4. +
  5. Instance
  6. +
+ {{/block-slot}} + {{#block-slot 'header'}} +

+ {{ item.ID }} +{{#with (service/external-source item) as |externalSource| }} + {{#with (css-var (concat '--' externalSource '-color-svg') 'none') as |bg| }} + {{#if (not-eq bg 'none') }} + Registered via {{externalSource}} + {{/if}} + {{/with}} +{{/with}} +

+
+
Service Name
+
{{item.Service}}
+
+
+
Node Name
+
{{item.Node.Node}}
+
+{{#if proxy}} +
+
Sidecar Proxy
+
{{proxy.ServiceID}}
+
+{{/if}} +{{#if (eq item.Kind 'connect-proxy')}} +
+
Dest. Service Instance
+
{{item.Proxy.DestinationServiceID}}
+
+
+
Local Service Address
+
{{item.Proxy.LocalServiceAddress}}:{{item.Proxy.LocalServicePort}}
+
+{{/if}} + {{/block-slot}} + {{#block-slot 'content'}} + {{tab-nav + items=(compact + (array + 'Service Checks' + 'Node Checks' +(if (eq item.Kind 'connect-proxy') 'Upstreams' '') + 'Tags' + ) + ) + selected=selectedTab + }} + {{#each + (compact + (array + (hash id=(slugify 'Service Checks') partial='dc/services/servicechecks') + (hash id=(slugify 'Node Checks') partial='dc/services/nodechecks') +(if (eq item.Kind 'connect-proxy') (hash id=(slugify 'Upstreams') partial='dc/services/upstreams') '') + (hash id=(slugify 'Tags') partial='dc/services/tags') + ) + ) as |panel| + }} + {{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab '') panel.id) onchange=(action "change")}} + {{partial panel.partial}} + {{/tab-section}} + {{/each}} + {{/block-slot}} +{{/app-view}} \ No newline at end of file diff --git a/ui-v2/app/templates/dc/services/show.hbs b/ui-v2/app/templates/dc/services/show.hbs index 5a75f0055f9b..bd693d9b854b 100644 --- a/ui-v2/app/templates/dc/services/show.hbs +++ b/ui-v2/app/templates/dc/services/show.hbs @@ -15,76 +15,29 @@ {{/with}} {{/with}} - {{/block-slot}} - {{#block-slot 'toolbar'}} -{{#if (gt items.length 0) }} - {{catalog-filter searchable=(array searchableHealthy searchableUnhealthy) filters=healthFilters search=s status=filters.status onchange=(action 'filter')}} -{{/if}} + + {{tab-nav + items=(compact + (array + 'Instances' + 'Tags' + ) + ) + selected=selectedTab + }} {{/block-slot}} {{#block-slot 'content'}} -{{#if (gt item.Tags.length 0)}} -
-
Tags
-
- {{#each item.Tags as |item|}} - {{item}} - {{/each}} -
-
-{{/if}} -{{#if (gt unhealthy.length 0) }} -
-

Unhealthy Nodes

-
-
    - {{#changeable-set dispatcher=searchableUnhealthy}} - {{#block-slot 'set' as |unhealthy|}} - {{#each unhealthy as |item|}} - {{healthchecked-resource - tagName='li' - data-test-node=item.Node.Node - href=(href-to 'dc.nodes.show' item.Node.Node) - name=item.Node.Node - service=item.Service.ID - address=(concat (default item.Service.Address item.Node.Address) ':' item.Service.Port) - checks=item.Checks - }} - {{/each}} - {{/block-slot}} - {{#block-slot 'empty'}} -

    - There are no unhealthy nodes for that search. -

    - {{/block-slot}} - {{/changeable-set}} -
-
-
-{{/if}} -{{#if (gt healthy.length 0) }} -
-

Healthy Nodes

- {{#changeable-set dispatcher=searchableHealthy}} - {{#block-slot 'set' as |healthy|}} - {{#list-collection cellHeight=113 items=healthy as |item index|}} - {{healthchecked-resource - href=(href-to 'dc.nodes.show' item.Node.Node) - data-test-node=item.Node.Node - name=item.Node.Node - service=item.Service.ID - address=(concat (default item.Service.Address item.Node.Address) ':' item.Service.Port) - checks=item.Checks - status=item.Checks.[0].Status - }} - {{/list-collection}} - {{/block-slot}} - {{#block-slot 'empty'}} -

- There are no healthy nodes for that search. -

- {{/block-slot}} - {{/changeable-set}} -
-{{/if}} + {{#each + (compact + (array + (hash id=(slugify 'Instances') partial='dc/services/instances') + (hash id=(slugify 'Tags') partial='dc/services/tags') + ) + ) as |panel| + }} + {{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab '') panel.id) onchange=(action "change")}} + {{partial panel.partial}} + {{/tab-section}} + {{/each}} {{/block-slot}} {{/app-view}} diff --git a/ui-v2/app/utils/computed/purify.js b/ui-v2/app/utils/computed/purify.js index 3c9eba3410a5..1008ed8c4766 100644 --- a/ui-v2/app/utils/computed/purify.js +++ b/ui-v2/app/utils/computed/purify.js @@ -1,4 +1,4 @@ -import { get } from '@ember/object'; +import { get, computed } from '@ember/object'; /** * Converts a conventional non-pure Ember `computed` function into a pure one @@ -8,20 +8,18 @@ import { get } from '@ember/object'; * @param {function} filter - Optional string filter function to pre-process the names of computed properties * @returns {function} - A pure `computed` function */ - -export default function(computed, filter) { +const _success = function(value) { + return value; +}; +const purify = function(computed, filter = args => args) { return function() { let args = [...arguments]; - let success = function(value) { - return value; - }; + let success = _success; // pop the user function off the end if (typeof args[args.length - 1] === 'function') { success = args.pop(); } - if (typeof filter === 'function') { - args = filter(args); - } + args = filter(args); // this is the 'conventional' `computed` const cb = function(name) { return success.apply( @@ -39,4 +37,6 @@ export default function(computed, filter) { // concat/push the user function back on return computed(...args.concat([cb])); }; -} +}; +export const subscribe = purify(computed); +export default purify; diff --git a/ui-v2/tests/acceptance/components/catalog-filter.feature b/ui-v2/tests/acceptance/components/catalog-filter.feature index ed11e247bd16..3b5dad1177ea 100644 --- a/ui-v2/tests/acceptance/components/catalog-filter.feature +++ b/ui-v2/tests/acceptance/components/catalog-filter.feature @@ -123,31 +123,6 @@ Feature: components / catalog-filter | Model | Page | Url | | service | node | /dc-1/nodes/node-0 | ------------------------------------------------- - Scenario: Filtering [Model] in [Page] - Given 1 datacenter model with the value "dc1" - And 2 [Model] models from yaml - --- - - ID: node-0 - --- - When I visit the [Page] page for yaml - --- - dc: dc1 - service: service-0 - --- - Then I fill in with yaml - --- - s: service-0-with-id - --- - And I see 1 [Model] model - Then I see id on the unhealthy like yaml - --- - - service-0-with-id - --- - Where: - ------------------------------------------------- - | Model | Page | Url | - | nodes | service | /dc-1/services/service-0 | - ------------------------------------------------- Scenario: Given 1 datacenter model with the value "dc-1" And 3 service models from yaml diff --git a/ui-v2/tests/acceptance/dc/services/show.feature b/ui-v2/tests/acceptance/dc/services/show.feature index 5fa48f0defa0..0c707f31b986 100644 --- a/ui-v2/tests/acceptance/dc/services/show.feature +++ b/ui-v2/tests/acceptance/dc/services/show.feature @@ -52,7 +52,7 @@ Feature: dc / services / show: Show Service Then I see the text "Tag1" in "[data-test-tags] span:nth-child(1)" Then I see the text "Tag2" in "[data-test-tags] span:nth-child(2)" Then I see the text "Tag3" in "[data-test-tags] span:nth-child(3)" - Scenario: Given various services the various ports on their nodes are displayed + Scenario: Given various services the various nodes on their instances are displayed Given 1 datacenter model with the value "dc1" And 3 node models And 1 service model from yaml @@ -83,21 +83,9 @@ Feature: dc / services / show: Show Service dc: dc1 service: service-0 --- - Then I see address on the healthy like yaml + Then I see address on the instances like yaml --- - "1.1.1.1:8080" - --- - Then I see address on the unhealthy like yaml - --- - "2.2.2.2:8000" - "3.3.3.3:8888" --- - Then I see id on the healthy like yaml - --- - - "passing-service-8080" - --- - Then I see id on the unhealthy like yaml - --- - - "service-8000" - - "service-8888" - --- diff --git a/ui-v2/tests/integration/components/healthcheck-info-test.js b/ui-v2/tests/integration/components/healthcheck-info-test.js new file mode 100644 index 000000000000..613a65065779 --- /dev/null +++ b/ui-v2/tests/integration/components/healthcheck-info-test.js @@ -0,0 +1,22 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('healthcheck-info', 'Integration | Component | healthcheck info', { + integration: true, +}); + +test('it renders', function(assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{healthcheck-info}}`); + + assert.equal(this.$('dl').length, 1); + + // Template block usage: + this.render(hbs` + {{#healthcheck-info}} + {{/healthcheck-info}} + `); + assert.equal(this.$('dl').length, 1); +}); diff --git a/ui-v2/tests/integration/components/healthcheck-list-test.js b/ui-v2/tests/integration/components/healthcheck-list-test.js new file mode 100644 index 000000000000..a85c4866de8f --- /dev/null +++ b/ui-v2/tests/integration/components/healthcheck-list-test.js @@ -0,0 +1,23 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('healthcheck-list', 'Integration | Component | healthcheck list', { + integration: true, +}); + +test('it renders', function(assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{healthcheck-list}}`); + + assert.equal(this.$('ul').length, 1); + + // Template block usage: + this.render(hbs` + {{#healthcheck-list}} + {{/healthcheck-list}} + `); + + assert.equal(this.$('ul').length, 1); +}); diff --git a/ui-v2/tests/integration/components/healthcheck-output-test.js b/ui-v2/tests/integration/components/healthcheck-output-test.js new file mode 100644 index 000000000000..b72e7412f9be --- /dev/null +++ b/ui-v2/tests/integration/components/healthcheck-output-test.js @@ -0,0 +1,34 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('healthcheck-output', 'Integration | Component | healthcheck output', { + integration: true, +}); + +test('it renders', function(assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{healthcheck-output}}`); + + assert.notEqual( + this.$() + .text() + .trim() + .indexOf('Output'), + -1 + ); + + // Template block usage: + this.render(hbs` + {{#healthcheck-output}}{{/healthcheck-output}} + `); + + assert.notEqual( + this.$() + .text() + .trim() + .indexOf('Output'), + -1 + ); +}); diff --git a/ui-v2/tests/integration/components/healthcheck-status-test.js b/ui-v2/tests/integration/components/healthcheck-status-test.js index b19207e5a4cf..f4e9bd78ff22 100644 --- a/ui-v2/tests/integration/components/healthcheck-status-test.js +++ b/ui-v2/tests/integration/components/healthcheck-status-test.js @@ -10,25 +10,11 @@ test('it renders', function(assert) { // Handle any actions with this.on('myAction', function(val) { ... }); this.render(hbs`{{healthcheck-status}}`); - - assert.notEqual( - this.$() - .text() - .trim() - .indexOf('Output'), - -1 - ); + assert.equal(this.$('dt').length, 1); // Template block usage: this.render(hbs` {{#healthcheck-status}}{{/healthcheck-status}} `); - - assert.notEqual( - this.$() - .text() - .trim() - .indexOf('Output'), - -1 - ); + assert.equal(this.$('dt').length, 1); }); diff --git a/ui-v2/tests/integration/components/tag-list-test.js b/ui-v2/tests/integration/components/tag-list-test.js new file mode 100644 index 000000000000..6924c8562cd3 --- /dev/null +++ b/ui-v2/tests/integration/components/tag-list-test.js @@ -0,0 +1,33 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('tag-list', 'Integration | Component | tag list', { + integration: true, +}); + +test('it renders', function(assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{tag-list}}`); + + assert.equal( + this.$() + .text() + .trim(), + '' + ); + + // Template block usage: + this.render(hbs` + {{#tag-list}} + {{/tag-list}} + `); + + assert.equal( + this.$() + .text() + .trim(), + '' + ); +}); diff --git a/ui-v2/tests/pages/dc/services/show.js b/ui-v2/tests/pages/dc/services/show.js index f50c60c834dd..5b5fb2e3813b 100644 --- a/ui-v2/tests/pages/dc/services/show.js +++ b/ui-v2/tests/pages/dc/services/show.js @@ -2,18 +2,8 @@ export default function(visitable, attribute, collection, text, filter) { return { visit: visitable('/:dc/services/:service'), externalSource: attribute('data-test-external-source', 'h1 span'), - nodes: collection('[data-test-node]', { - name: attribute('data-test-node'), - }), - healthy: collection('[data-test-healthy] [data-test-node]', { - name: attribute('data-test-node'), - address: text('header strong'), - id: text('header em'), - }), - unhealthy: collection('[data-test-unhealthy] [data-test-node]', { - name: attribute('data-test-node'), - address: text('header strong'), - id: text('header em'), + instances: collection('#instances [data-test-tabular-row]', { + address: text('[data-test-address]'), }), filter: filter, }; diff --git a/ui-v2/tests/unit/adapters/proxy-test.js b/ui-v2/tests/unit/adapters/proxy-test.js new file mode 100644 index 000000000000..13859457edeb --- /dev/null +++ b/ui-v2/tests/unit/adapters/proxy-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | proxy', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let adapter = this.owner.lookup('adapter:proxy'); + assert.ok(adapter); + }); +}); diff --git a/ui-v2/tests/unit/controllers/dc/services/instance-test.js b/ui-v2/tests/unit/controllers/dc/services/instance-test.js new file mode 100644 index 000000000000..2b0693934f04 --- /dev/null +++ b/ui-v2/tests/unit/controllers/dc/services/instance-test.js @@ -0,0 +1,12 @@ +import { moduleFor, test } from 'ember-qunit'; + +moduleFor('controller:dc/services/instance', 'Unit | Controller | dc/services/instance', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +// Replace this with your real tests. +test('it exists', function(assert) { + let controller = this.subject(); + assert.ok(controller); +}); diff --git a/ui-v2/tests/unit/models/proxy-test.js b/ui-v2/tests/unit/models/proxy-test.js new file mode 100644 index 000000000000..b37e80f56d29 --- /dev/null +++ b/ui-v2/tests/unit/models/proxy-test.js @@ -0,0 +1,14 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { run } from '@ember/runloop'; + +module('Unit | Model | proxy', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let store = this.owner.lookup('service:store'); + let model = run(() => store.createRecord('proxy', {})); + assert.ok(model); + }); +}); diff --git a/ui-v2/tests/unit/routes/dc/services/instance-test.js b/ui-v2/tests/unit/routes/dc/services/instance-test.js new file mode 100644 index 000000000000..122dc9ee163e --- /dev/null +++ b/ui-v2/tests/unit/routes/dc/services/instance-test.js @@ -0,0 +1,11 @@ +import { moduleFor, test } from 'ember-qunit'; + +moduleFor('route:dc/services/instance', 'Unit | Route | dc/services/instance', { + // Specify the other units that are required for this test. + needs: ['service:repository/service', 'service:repository/proxy'], +}); + +test('it exists', function(assert) { + let route = this.subject(); + assert.ok(route); +}); diff --git a/ui-v2/tests/unit/serializers/proxy-test.js b/ui-v2/tests/unit/serializers/proxy-test.js new file mode 100644 index 000000000000..44090cfe02d1 --- /dev/null +++ b/ui-v2/tests/unit/serializers/proxy-test.js @@ -0,0 +1,24 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { run } from '@ember/runloop'; + +module('Unit | Serializer | proxy', 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('proxy'); + + assert.ok(serializer); + }); + + test('it serializes records', function(assert) { + let store = this.owner.lookup('service:store'); + let record = run(() => store.createRecord('proxy', {})); + + let serializedRecord = record.serialize(); + + assert.ok(serializedRecord); + }); +});