diff --git a/ui-v2/.eslintignore b/ui-v2/.eslintignore new file mode 100644 index 0000000000..06978bbca0 --- /dev/null +++ b/ui-v2/.eslintignore @@ -0,0 +1 @@ +app/utils/dom/event-target/event-target-shim/event.js diff --git a/ui-v2/GNUmakefile b/ui-v2/GNUmakefile index e87521b992..682077582b 100644 --- a/ui-v2/GNUmakefile +++ b/ui-v2/GNUmakefile @@ -25,6 +25,9 @@ lint: deps format: deps yarn run format:js +steps: + yarn run steps:list + node_modules: yarn.lock package.json yarn install diff --git a/ui-v2/app/adapters/application.js b/ui-v2/app/adapters/application.js index b28ed0a861..d5f073972a 100644 --- a/ui-v2/app/adapters/application.js +++ b/ui-v2/app/adapters/application.js @@ -1,9 +1,12 @@ import Adapter from 'ember-data/adapters/rest'; +import { AbortError } from 'ember-data/adapters/errors'; import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; import URL from 'url'; import createURL from 'consul-ui/utils/createURL'; import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc'; +import { HEADERS_SYMBOL as HTTP_HEADERS_SYMBOL } from 'consul-ui/utils/http/consul'; export const REQUEST_CREATE = 'createRecord'; export const REQUEST_READ = 'queryRecord'; @@ -16,12 +19,68 @@ export const DATACENTER_QUERY_PARAM = 'dc'; export default Adapter.extend({ namespace: 'v1', repo: service('settings'), + client: service('client/http'), + manageConnection: function(options) { + const client = get(this, 'client'); + const complete = options.complete; + const beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + if (typeof beforeSend === 'function') { + beforeSend(...arguments); + } + options.id = client.request(options, xhr); + }; + options.complete = function(xhr, textStatus) { + client.complete(options.id); + if (typeof complete === 'function') { + complete(...arguments); + } + }; + return options; + }, + _ajaxRequest: function(options) { + return this._super(this.manageConnection(options)); + }, + queryRecord: function() { + return this._super(...arguments).catch(function(e) { + if (e instanceof AbortError) { + e.errors[0].status = '0'; + } + throw e; + }); + }, + query: function() { + return this._super(...arguments).catch(function(e) { + if (e instanceof AbortError) { + e.errors[0].status = '0'; + } + throw e; + }); + }, headersForRequest: function(params) { return { ...this.get('repo').findHeaders(), ...this._super(...arguments), }; }, + handleResponse: function(status, headers, response, requestData) { + // The ember-data RESTAdapter drops the headers after this call, + // and there is no where else to get to these + // save them to response[HTTP_HEADERS_SYMBOL] for the moment + // so we can save them as meta in the serializer... + if ( + (typeof response == 'object' && response.constructor == Object) || + Array.isArray(response) + ) { + // lowercase everything incase we get browser inconsistencies + const lower = {}; + Object.keys(headers).forEach(function(key) { + lower[key.toLowerCase()] = headers[key]; + }); + response[HTTP_HEADERS_SYMBOL] = lower; + } + return this._super(status, headers, response, requestData); + }, handleBooleanResponse: function(url, response, primary, slug) { return { // consider a check for a boolean, also for future me, @@ -52,6 +111,12 @@ export default Adapter.extend({ delete _query.id; } const query = { ..._query }; + if (typeof query.separator !== 'undefined') { + delete query.separator; + } + if (typeof query.index !== 'undefined') { + delete query.index; + } delete _query[DATACENTER_QUERY_PARAM]; return query; }, diff --git a/ui-v2/app/adapters/kv.js b/ui-v2/app/adapters/kv.js index 509b1440f0..b99422abba 100644 --- a/ui-v2/app/adapters/kv.js +++ b/ui-v2/app/adapters/kv.js @@ -136,6 +136,7 @@ export default Adapter.extend({ } return null; } + return data; }, methodForRequest: function(params) { switch (params.requestType) { diff --git a/ui-v2/app/adapters/proxy.js b/ui-v2/app/adapters/proxy.js new file mode 100644 index 0000000000..d4f4f41ea5 --- /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/adapters/role.js b/ui-v2/app/adapters/role.js new file mode 100644 index 0000000000..8112812592 --- /dev/null +++ b/ui-v2/app/adapters/role.js @@ -0,0 +1,72 @@ +import Adapter, { + REQUEST_CREATE, + REQUEST_UPDATE, + DATACENTER_QUERY_PARAM as API_DATACENTER_KEY, +} from './application'; + +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/role'; +import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc'; +import { OK as HTTP_OK } from 'consul-ui/utils/http/status'; +import { PUT as HTTP_PUT } from 'consul-ui/utils/http/method'; + +import WithPolicies from 'consul-ui/mixins/policy/as-many'; + +export default Adapter.extend(WithPolicies, { + urlForQuery: function(query, modelName) { + return this.appendURL('acl/roles', [], this.cleanQuery(query)); + }, + urlForQueryRecord: function(query, modelName) { + if (typeof query.id === 'undefined') { + throw new Error('You must specify an id'); + } + return this.appendURL('acl/role', [query.id], this.cleanQuery(query)); + }, + urlForCreateRecord: function(modelName, snapshot) { + return this.appendURL('acl/role', [], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + urlForUpdateRecord: function(id, modelName, snapshot) { + return this.appendURL('acl/role', [snapshot.attr(SLUG_KEY)], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + urlForDeleteRecord: function(id, modelName, snapshot) { + return this.appendURL('acl/role', [snapshot.attr(SLUG_KEY)], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + handleResponse: function(status, headers, payload, requestData) { + let response = payload; + if (status === HTTP_OK) { + const url = this.parseURL(requestData.url); + switch (true) { + case response === true: + response = this.handleBooleanResponse(url, response, PRIMARY_KEY, SLUG_KEY); + break; + case Array.isArray(response): + response = this.handleBatchResponse(url, response, PRIMARY_KEY, SLUG_KEY); + break; + default: + response = this.handleSingleResponse(url, response, PRIMARY_KEY, SLUG_KEY); + } + } + return this._super(status, headers, response, requestData); + }, + methodForRequest: function(params) { + switch (params.requestType) { + case REQUEST_CREATE: + return HTTP_PUT; + } + return this._super(...arguments); + }, + dataForRequest: function(params) { + const data = this._super(...arguments); + switch (params.requestType) { + case REQUEST_UPDATE: + case REQUEST_CREATE: + return data.role; + } + return data; + }, +}); diff --git a/ui-v2/app/adapters/token.js b/ui-v2/app/adapters/token.js index 402b1f6be5..1d34970f58 100644 --- a/ui-v2/app/adapters/token.js +++ b/ui-v2/app/adapters/token.js @@ -10,12 +10,15 @@ import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc'; import { OK as HTTP_OK } from 'consul-ui/utils/http/status'; import { PUT as HTTP_PUT } from 'consul-ui/utils/http/method'; +import WithPolicies from 'consul-ui/mixins/policy/as-many'; +import WithRoles from 'consul-ui/mixins/role/as-many'; + import { get } from '@ember/object'; const REQUEST_CLONE = 'cloneRecord'; const REQUEST_SELF = 'querySelf'; -export default Adapter.extend({ +export default Adapter.extend(WithRoles, WithPolicies, { store: service('store'), cleanQuery: function(_query) { const query = this._super(...arguments); @@ -108,10 +111,6 @@ export default Adapter.extend({ return this._makeRequest(request); }, handleSingleResponse: function(url, response, primary, slug) { - // Sometimes we get `Policies: null`, make null equal an empty array - if (typeof response.Policies === 'undefined' || response.Policies === null) { - response.Policies = []; - } // Convert an old style update response to a new style if (typeof response['ID'] !== 'undefined') { const item = get(this, 'store') @@ -169,19 +168,6 @@ export default Adapter.extend({ } // falls through case REQUEST_CREATE: - if (Array.isArray(data.token.Policies)) { - data.token.Policies = data.token.Policies.filter(function(item) { - // Just incase, don't save any policies that aren't saved - return !get(item, 'isNew'); - }).map(function(item) { - return { - ID: get(item, 'ID'), - Name: get(item, 'Name'), - }; - }); - } else { - delete data.token.Policies; - } data = data.token; break; case REQUEST_SELF: diff --git a/ui-v2/app/components/app-view.js b/ui-v2/app/components/app-view.js index eaf2f029fe..ed526eab0d 100644 --- a/ui-v2/app/components/app-view.js +++ b/ui-v2/app/components/app-view.js @@ -1,31 +1,33 @@ import Component from '@ember/component'; import SlotsMixin from 'block-slots'; import { get } from '@ember/object'; +import { inject as service } from '@ember/service'; import templatize from 'consul-ui/utils/templatize'; -const $html = document.documentElement; export default Component.extend(SlotsMixin, { loading: false, authorized: true, enabled: true, classNames: ['app-view'], classNameBindings: ['enabled::disabled', 'authorized::unauthorized'], + dom: service('dom'), didReceiveAttrs: function() { // right now only manually added classes are hoisted to + const $root = get(this, 'dom').root(); let cls = get(this, 'class') || ''; if (get(this, 'loading')) { cls += ' loading'; } else { - $html.classList.remove(...templatize(['loading'])); + $root.classList.remove(...templatize(['loading'])); } if (cls) { // its possible for 'layout' templates to change after insert // check for these specific layouts and clear them out - [...$html.classList].forEach(function(item, i) { + [...$root.classList].forEach(function(item, i) { if (templatize(['edit', 'show', 'list']).indexOf(item) !== -1) { - $html.classList.remove(item); + $root.classList.remove(item); } }); - $html.classList.add(...templatize(cls.split(' '))); + $root.classList.add(...templatize(cls.split(' '))); } }, didInsertElement: function() { @@ -34,7 +36,8 @@ export default Component.extend(SlotsMixin, { didDestroyElement: function() { const cls = get(this, 'class') + ' loading'; if (cls) { - $html.classList.remove(...templatize(cls.split(' '))); + const $root = get(this, 'dom').root(); + $root.classList.remove(...templatize(cls.split(' '))); } }, }); diff --git a/ui-v2/app/components/changeable-set.js b/ui-v2/app/components/changeable-set.js new file mode 100644 index 0000000000..9a50c24293 --- /dev/null +++ b/ui-v2/app/components/changeable-set.js @@ -0,0 +1,19 @@ +import Component from '@ember/component'; +import { get, set } from '@ember/object'; +import SlotsMixin from 'block-slots'; +import WithListeners from 'consul-ui/mixins/with-listeners'; + +export default Component.extend(WithListeners, SlotsMixin, { + tagName: '', + didReceiveAttrs: function() { + this._super(...arguments); + this.removeListeners(); + const dispatcher = get(this, 'dispatcher'); + if (dispatcher) { + this.listen(dispatcher, 'change', e => { + set(this, 'items', e.target.data); + }); + set(this, 'items', get(dispatcher, 'data')); + } + }, +}); diff --git a/ui-v2/app/components/child-selector.js b/ui-v2/app/components/child-selector.js new file mode 100644 index 0000000000..5a471b9caa --- /dev/null +++ b/ui-v2/app/components/child-selector.js @@ -0,0 +1,113 @@ +import Component from '@ember/component'; +import { get, set, computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import { Promise } from 'rsvp'; + +import SlotsMixin from 'block-slots'; +import WithListeners from 'consul-ui/mixins/with-listeners'; + +export default Component.extend(SlotsMixin, WithListeners, { + onchange: function() {}, + + error: function() {}, + type: '', + + dom: service('dom'), + container: service('search'), + formContainer: service('form'), + + item: alias('form.data'), + + selectedOptions: alias('items'), + + init: function() { + this._super(...arguments); + this.searchable = get(this, 'container').searchable(get(this, 'type')); + this.form = get(this, 'formContainer').form(get(this, 'type')); + this.form.clear({ Datacenter: get(this, 'dc') }); + }, + options: computed('selectedOptions.[]', 'allOptions.[]', function() { + // It's not massively important here that we are defaulting `items` and + // losing reference as its just to figure out the diff + let options = get(this, 'allOptions') || []; + const items = get(this, 'selectedOptions') || []; + if (get(items, 'length') > 0) { + // find a proper ember-data diff + options = options.filter(item => !items.findBy('ID', get(item, 'ID'))); + this.searchable.add(options); + } + return options; + }), + actions: { + search: function(term) { + // TODO: make sure we can either search before things are loaded + // or wait until we are loaded, guess power select take care of that + return new Promise(resolve => { + const remove = this.listen(this.searchable, 'change', function(e) { + remove(); + resolve(e.target.data); + }); + this.searchable.search(term); + }); + }, + reset: function() { + get(this, 'form').clear({ Datacenter: get(this, 'dc') }); + }, + open: function() { + if (!get(this, 'allOptions.closed')) { + set(this, 'allOptions', get(this, 'repo').findAllByDatacenter(get(this, 'dc'))); + } + }, + save: function(item, items, success = function() {}) { + // Specifically this saves an 'new' option/child + // and then adds it to the selectedOptions, not options + const repo = get(this, 'repo'); + set(item, 'CreateTime', new Date().getTime()); + // TODO: temporary async + // this should be `set(this, 'item', repo.persist(item));` + // need to be sure that its saved before adding/closing the modal for now + // and we don't open the modal on prop change yet + item = repo.persist(item); + this.listen(item, 'message', e => { + this.actions.change.bind(this)( + { + target: { + name: 'items[]', + value: items, + }, + }, + items, + e.data + ); + success(); + }); + this.listen(item, 'error', this.error.bind(this)); + }, + remove: function(item, items) { + const prop = get(this, 'repo').getSlugKey(); + const value = get(item, prop); + const pos = items.findIndex(function(item) { + return get(item, prop) === value; + }); + if (pos !== -1) { + return items.removeAt(pos, 1); + } + this.onchange({ target: this }); + }, + change: function(e, value, item) { + const event = get(this, 'dom').normalizeEvent(...arguments); + const items = value; + switch (event.target.name) { + case 'items[]': + set(item, 'CreateTime', new Date().getTime()); + // this always happens synchronously + items.pushObject(item); + // TODO: Fire a proper event + this.onchange({ target: this }); + break; + default: + } + }, + }, +}); diff --git a/ui-v2/app/components/code-editor.js b/ui-v2/app/components/code-editor.js index 524fd597bd..b697657e89 100644 --- a/ui-v2/app/components/code-editor.js +++ b/ui-v2/app/components/code-editor.js @@ -11,31 +11,57 @@ const DEFAULTS = { }; export default Component.extend({ settings: service('settings'), - helper: service('code-mirror'), + dom: service('dom'), + helper: service('code-mirror/linter'), classNames: ['code-editor'], + readonly: false, syntax: '', - onchange: function(value) { - get(this, 'settings').persist({ - 'code-editor': value, - }); - this.setMode(value); - }, + // TODO: Change this to oninput to be consistent? We'll have to do it throughout the templates onkeyup: function() {}, + oninput: function() {}, init: function() { this._super(...arguments); set(this, 'modes', get(this, 'helper').modes()); }, + didReceiveAttrs: function() { + this._super(...arguments); + const editor = get(this, 'editor'); + if (editor) { + editor.setOption('readOnly', get(this, 'readonly')); + } + }, setMode: function(mode) { set(this, 'options', { ...DEFAULTS, mode: mode.mime, + readOnly: get(this, 'readonly'), }); const editor = get(this, 'editor'); editor.setOption('mode', mode.mime); get(this, 'helper').lint(editor, mode.mode); set(this, 'mode', mode); }, + willDestroyElement: function() { + this._super(...arguments); + if (this.observer) { + this.observer.disconnect(); + } + }, didInsertElement: function() { + this._super(...arguments); + const $code = get(this, 'dom').element('textarea ~ pre code', get(this, 'element')); + if ($code.firstChild) { + this.observer = new MutationObserver(([e]) => { + this.oninput(set(this, 'value', e.target.wholeText)); + }); + this.observer.observe($code, { + attributes: false, + subtree: true, + childList: false, + characterData: true, + }); + set(this, 'value', $code.firstChild.wholeText); + } set(this, 'editor', get(this, 'helper').getEditor(this.element)); get(this, 'settings') .findBySlug('code-editor') @@ -54,4 +80,12 @@ export default Component.extend({ didAppear: function() { get(this, 'editor').refresh(); }, + actions: { + change: function(value) { + get(this, 'settings').persist({ + 'code-editor': value, + }); + this.setMode(value); + }, + }, }); diff --git a/ui-v2/app/components/dom-buffer-flush.js b/ui-v2/app/components/dom-buffer-flush.js index 035d1ac491..e05c9b1b7e 100644 --- a/ui-v2/app/components/dom-buffer-flush.js +++ b/ui-v2/app/components/dom-buffer-flush.js @@ -11,9 +11,11 @@ export default Component.extend({ this.append = append.bind(this); }, didInsertElement: function() { + this._super(...arguments); get(this, 'buffer').on('add', this.append); }, didDestroyElement: function() { + this._super(...arguments); get(this, 'buffer').off('add', this.append); }, }); diff --git a/ui-v2/app/components/dom-buffer.js b/ui-v2/app/components/dom-buffer.js index 054d4ca3da..3d70f72059 100644 --- a/ui-v2/app/components/dom-buffer.js +++ b/ui-v2/app/components/dom-buffer.js @@ -9,9 +9,11 @@ export default Component.extend({ return 'modal'; }, didInsertElement: function() { + this._super(...arguments); get(this, 'buffer').add(this.getBufferName(), this.element); }, didDestroyElement: function() { + this._super(...arguments); get(this, 'buffer').remove(this.getBufferName()); }, }); diff --git a/ui-v2/app/components/feedback-dialog.js b/ui-v2/app/components/feedback-dialog.js index ebdc003fce..b42df6304e 100644 --- a/ui-v2/app/components/feedback-dialog.js +++ b/ui-v2/app/components/feedback-dialog.js @@ -1,8 +1,7 @@ import Component from '@ember/component'; import { get, set } from '@ember/object'; import { inject as service } from '@ember/service'; -import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; -const $$ = qsaFactory(); +import { Promise } from 'rsvp'; import SlotsMixin from 'block-slots'; const STATE_READY = 'ready'; @@ -10,6 +9,7 @@ const STATE_SUCCESS = 'success'; const STATE_ERROR = 'error'; export default Component.extend(SlotsMixin, { wait: service('timeout'), + dom: service('dom'), classNames: ['with-feedback'], transition: '', transitionClassName: 'feedback-dialog-out', @@ -23,6 +23,7 @@ export default Component.extend(SlotsMixin, { applyTransition: function() { const wait = get(this, 'wait').execute; const className = get(this, 'transitionClassName'); + // TODO: Make 0 default in wait wait(0) .then(() => { set(this, 'transition', className); @@ -30,7 +31,9 @@ export default Component.extend(SlotsMixin, { }) .then(() => { return new Promise(resolve => { - $$(`.${className}`, this.element)[0].addEventListener('transitionend', resolve); + get(this, 'dom') + .element(`.${className}`, this.element) + .addEventListener('transitionend', resolve); }); }) .then(() => { diff --git a/ui-v2/app/components/form-component.js b/ui-v2/app/components/form-component.js new file mode 100644 index 0000000000..5a6e5f393a --- /dev/null +++ b/ui-v2/app/components/form-component.js @@ -0,0 +1,42 @@ +import Component from '@ember/component'; +import SlotsMixin from 'block-slots'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import WithListeners from 'consul-ui/mixins/with-listeners'; +// match anything that isn't a [ or ] into multiple groups +const propRe = /([^[\]])+/g; +export default Component.extend(WithListeners, SlotsMixin, { + onreset: function() {}, + onchange: function() {}, + onerror: function() {}, + onsuccess: function() {}, + + data: alias('form.data'), + item: alias('form.data'), + // TODO: Could probably alias item + // or just use data/value instead + + dom: service('dom'), + container: service('form'), + + actions: { + change: function(e, value, item) { + let event = get(this, 'dom').normalizeEvent(e, value); + const matches = [...event.target.name.matchAll(propRe)]; + const prop = matches[matches.length - 1][0]; + event = get(this, 'dom').normalizeEvent( + `${get(this, 'type')}[${prop}]`, + event.target.value, + event.target + ); + const form = get(this, 'form'); + try { + form.handleEvent(event); + this.onchange({ target: this }); + } catch (err) { + throw err; + } + }, + }, +}); diff --git a/ui-v2/app/components/freetext-filter.js b/ui-v2/app/components/freetext-filter.js index b5fc2d39ef..d776119ecf 100644 --- a/ui-v2/app/components/freetext-filter.js +++ b/ui-v2/app/components/freetext-filter.js @@ -1,7 +1,15 @@ import Component from '@ember/component'; - +import { get } from '@ember/object'; export default Component.extend({ tagName: 'fieldset', classNames: ['freetext-filter'], - onchange: function(){} + onchange: function(e) { + let searchable = get(this, 'searchable'); + if (!Array.isArray(searchable)) { + searchable = [searchable]; + } + searchable.forEach(function(item) { + item.search(e.target.value); + }); + }, }); diff --git a/ui-v2/app/components/hashicorp-consul.js b/ui-v2/app/components/hashicorp-consul.js index 69272e5b16..2935d043b3 100644 --- a/ui-v2/app/components/hashicorp-consul.js +++ b/ui-v2/app/components/hashicorp-consul.js @@ -1,11 +1,13 @@ import Component from '@ember/component'; import { get, set } from '@ember/object'; -const $html = document.documentElement; -const $body = document.body; +import { inject as service } from '@ember/service'; export default Component.extend({ + dom: service('dom'), isDropdownVisible: false, didInsertElement: function() { - $html.classList.remove('template-with-vertical-menu'); + get(this, 'dom') + .root() + .classList.remove('template-with-vertical-menu'); }, actions: { dropdown: function(e) { @@ -14,12 +16,16 @@ export default Component.extend({ } }, change: function(e) { + const dom = get(this, 'dom'); + const win = dom.viewport(); + const $root = dom.root(); + const $body = dom.element('body'); if (e.target.checked) { - $html.classList.add('template-with-vertical-menu'); - $body.style.height = $html.style.height = window.innerHeight + 'px'; + $root.classList.add('template-with-vertical-menu'); + $body.style.height = $root.style.height = win.innerHeight + 'px'; } else { - $html.classList.remove('template-with-vertical-menu'); - $body.style.height = $html.style.height = null; + $root.classList.remove('template-with-vertical-menu'); + $body.style.height = $root.style.height = null; } }, }, diff --git a/ui-v2/app/components/healthcheck-info.js b/ui-v2/app/components/healthcheck-info.js new file mode 100644 index 0000000000..abe1ccedb6 --- /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 0000000000..092a1aadaf --- /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 0000000000..227501fc5c --- /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 25a646d758..367cca6469 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/list-collection.js b/ui-v2/app/components/list-collection.js index f3f41f2a28..1a5229583e 100644 --- a/ui-v2/app/components/list-collection.js +++ b/ui-v2/app/components/list-collection.js @@ -1,11 +1,12 @@ +import { inject as service } from '@ember/service'; import { computed, get, set } from '@ember/object'; import Component from 'ember-collection/components/ember-collection'; import PercentageColumns from 'ember-collection/layouts/percentage-columns'; import style from 'ember-computed-style'; import WithResizing from 'consul-ui/mixins/with-resizing'; -import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; -const $$ = qsaFactory(); + export default Component.extend(WithResizing, { + dom: service('dom'), tagName: 'div', attributeBindings: ['style'], height: 500, @@ -30,11 +31,13 @@ export default Component.extend(WithResizing, { }; }), resize: function(e) { - const $self = this.element; - const $appContent = [...$$('main > div')][0]; + // TODO: This top part is very similar to resize in tabular-collection + // see if it make sense to DRY out + const dom = get(this, 'dom'); + const $appContent = dom.element('main > div'); if ($appContent) { - const rect = $self.getBoundingClientRect(); - const $footer = [...$$('footer[role="contentinfo"]')][0]; + const rect = this.element.getBoundingClientRect(); + const $footer = dom.element('footer[role="contentinfo"]'); const space = rect.top + $footer.clientHeight; const height = e.detail.height - space; this.set('height', Math.max(0, height)); diff --git a/ui-v2/app/components/modal-dialog.js b/ui-v2/app/components/modal-dialog.js index 787f996ae6..3d95b77f3a 100644 --- a/ui-v2/app/components/modal-dialog.js +++ b/ui-v2/app/components/modal-dialog.js @@ -1,11 +1,11 @@ import { get, set } from '@ember/object'; import { inject as service } from '@ember/service'; -import Component from 'consul-ui/components/dom-buffer'; +import DomBufferComponent from 'consul-ui/components/dom-buffer'; import SlotsMixin from 'block-slots'; import WithResizing from 'consul-ui/mixins/with-resizing'; import templatize from 'consul-ui/utils/templatize'; -export default Component.extend(SlotsMixin, WithResizing, { +export default DomBufferComponent.extend(SlotsMixin, WithResizing, { dom: service('dom'), checked: true, height: null, @@ -38,9 +38,11 @@ export default Component.extend(SlotsMixin, WithResizing, { _close: function(e) { set(this, 'checked', false); const dialogPanel = get(this, 'dialog'); - const overflowing = get(this, 'overflowingClass'); - if (dialogPanel.classList.contains(overflowing)) { - dialogPanel.classList.remove(overflowing); + if (dialogPanel) { + const overflowing = get(this, 'overflowingClass'); + if (dialogPanel.classList.contains(overflowing)) { + dialogPanel.classList.remove(overflowing); + } } // TODO: should we make a didDisappear? get(this, 'dom') @@ -105,15 +107,17 @@ export default Component.extend(SlotsMixin, WithResizing, { }, actions: { change: function(e) { - if (e && e.target && e.target.checked) { + if (get(e, 'target.checked')) { this._open(e); } else { - this._close(); + this._close(e); } }, close: function() { - get(this, 'dom').element('#modal_close').checked = true; - this.onclose(); + const $close = get(this, 'dom').element('#modal_close'); + $close.checked = true; + const $input = get(this, 'dom').element('input[name="modal"]', this.element); + $input.onchange({ target: $input }); }, }, }); diff --git a/ui-v2/app/components/modal-layer.js b/ui-v2/app/components/modal-layer.js index 66e760e563..003bf86870 100644 --- a/ui-v2/app/components/modal-layer.js +++ b/ui-v2/app/components/modal-layer.js @@ -1,8 +1,8 @@ -import Component from 'consul-ui/components/dom-buffer-flush'; +import DomBufferFlushComponent from 'consul-ui/components/dom-buffer-flush'; import { inject as service } from '@ember/service'; import { get } from '@ember/object'; -export default Component.extend({ +export default DomBufferFlushComponent.extend({ dom: service('dom'), actions: { change: function(e) { @@ -10,8 +10,10 @@ export default Component.extend({ .filter(function(item) { return item.getAttribute('id') !== 'modal_close'; }) - .forEach(function(item) { - item.onchange(); + .forEach(function(item, i) { + if (item.getAttribute('data-checked') === 'true') { + item.onchange({ target: item }); + } }); }, }, diff --git a/ui-v2/app/components/phrase-editor.js b/ui-v2/app/components/phrase-editor.js new file mode 100644 index 0000000000..e280b571c1 --- /dev/null +++ b/ui-v2/app/components/phrase-editor.js @@ -0,0 +1,44 @@ +import Component from '@ember/component'; +import { get, set } from '@ember/object'; + +export default Component.extend({ + classNames: ['phrase-editor'], + item: '', + remove: function(index, e) { + this.items.removeAt(index, 1); + this.onchange(e); + }, + add: function(e) { + const value = get(this, 'item').trim(); + if (value !== '') { + set(this, 'item', ''); + const currentItems = get(this, 'items') || []; + const items = new Set(currentItems).add(value); + if (items.size > currentItems.length) { + set(this, 'items', [...items]); + this.onchange(e); + } + } + }, + onkeydown: function(e) { + switch (e.keyCode) { + case 8: + if (e.target.value == '' && this.items.length > 0) { + this.remove(this.items.length - 1); + } + break; + } + }, + oninput: function(e) { + set(this, 'item', e.target.value); + }, + onchange: function(e) { + let searchable = get(this, 'searchable'); + if (!Array.isArray(searchable)) { + searchable = [searchable]; + } + searchable.forEach(item => { + item.search(get(this, 'items')); + }); + }, +}); diff --git a/ui-v2/app/components/policy-form.js b/ui-v2/app/components/policy-form.js new file mode 100644 index 0000000000..f178e64bbc --- /dev/null +++ b/ui-v2/app/components/policy-form.js @@ -0,0 +1,53 @@ +import FormComponent from './form-component'; +import { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; + +export default FormComponent.extend({ + repo: service('repository/policy/component'), + datacenterRepo: service('repository/dc/component'), + type: 'policy', + name: 'policy', + classNames: ['policy-form'], + + isScoped: false, + init: function() { + this._super(...arguments); + set(this, 'isScoped', get(this, 'item.Datacenters.length') > 0); + set(this, 'datacenters', get(this, 'datacenterRepo').findAll()); + this.templates = [ + { + name: 'Policy', + template: '', + }, + { + name: 'Service Identity', + template: 'service-identity', + }, + ]; + }, + actions: { + change: function(e) { + try { + this._super(...arguments); + } catch (err) { + const scoped = get(this, 'isScoped'); + const name = err.target.name; + switch (name) { + case 'policy[isScoped]': + if (scoped) { + set(this, 'previousDatacenters', get(this.item, 'Datacenters')); + set(this.item, 'Datacenters', null); + } else { + set(this.item, 'Datacenters', get(this, 'previousDatacenters')); + set(this, 'previousDatacenters', null); + } + set(this, 'isScoped', !scoped); + break; + default: + this.onerror(err); + } + this.onchange({ target: get(this, 'form') }); + } + }, + }, +}); diff --git a/ui-v2/app/components/policy-selector.js b/ui-v2/app/components/policy-selector.js new file mode 100644 index 0000000000..778c7296eb --- /dev/null +++ b/ui-v2/app/components/policy-selector.js @@ -0,0 +1,82 @@ +import ChildSelectorComponent from './child-selector'; +import { get, set } from '@ember/object'; +import { inject as service } from '@ember/service'; +import updateArrayObject from 'consul-ui/utils/update-array-object'; + +const ERROR_PARSE_RULES = 'Failed to parse ACL rules'; +const ERROR_NAME_EXISTS = 'Invalid Policy: A Policy with Name'; + +export default ChildSelectorComponent.extend({ + repo: service('repository/policy/component'), + datacenterRepo: service('repository/dc/component'), + name: 'policy', + type: 'policy', + classNames: ['policy-selector'], + init: function() { + this._super(...arguments); + const source = get(this, 'source'); + if (source) { + const event = 'save'; + this.listen(source, event, e => { + this.actions[event].bind(this)(...e.data); + }); + } + }, + reset: function(e) { + this._super(...arguments); + set(this, 'isScoped', false); + set(this, 'datacenters', get(this, 'datacenterRepo').findAll()); + }, + refreshCodeEditor: function(e, target) { + const selector = '.code-editor'; + get(this, 'dom') + .component(selector, target) + .didAppear(); + }, + error: function(e) { + const item = get(this, 'item'); + const err = e.error; + if (typeof err.errors !== 'undefined') { + const error = err.errors[0]; + let prop; + let message = error.detail; + switch (true) { + case message.indexOf(ERROR_PARSE_RULES) === 0: + prop = 'Rules'; + message = error.detail; + break; + case message.indexOf(ERROR_NAME_EXISTS) === 0: + prop = 'Name'; + message = message.substr(ERROR_NAME_EXISTS.indexOf(':') + 1); + break; + } + if (prop) { + item.addError(prop, message); + } + } else { + // TODO: Conponents can't throw, use onerror + throw err; + } + }, + actions: { + loadItem: function(e, item, items) { + const target = e.target; + // the Details expander toggle, only load on opening + if (target.checked) { + const value = item; + this.refreshCodeEditor(e, target.parentNode); + if (get(item, 'template') === 'service-identity') { + return; + } + // potentially the item could change between load, so we don't check + // anything to see if its already loaded here + const repo = get(this, 'repo'); + // TODO: Temporarily add dc here, will soon be serialized onto the policy itself + const dc = get(this, 'dc'); + const slugKey = repo.getSlugKey(); + const slug = get(value, slugKey); + updateArrayObject(items, repo.findBySlug(slug, dc), slugKey, slug); + } + }, + }, +}); diff --git a/ui-v2/app/components/role-form.js b/ui-v2/app/components/role-form.js new file mode 100644 index 0000000000..5ebe540c99 --- /dev/null +++ b/ui-v2/app/components/role-form.js @@ -0,0 +1,6 @@ +import FormComponent from './form-component'; +export default FormComponent.extend({ + type: 'role', + name: 'role', + classNames: ['role-form'], +}); diff --git a/ui-v2/app/components/role-selector.js b/ui-v2/app/components/role-selector.js new file mode 100644 index 0000000000..f1243ad178 --- /dev/null +++ b/ui-v2/app/components/role-selector.js @@ -0,0 +1,42 @@ +import ChildSelectorComponent from './child-selector'; +import { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; + +import { alias } from '@ember/object/computed'; + +import { CallableEventSource as EventSource } from 'consul-ui/utils/dom/event-source'; + +export default ChildSelectorComponent.extend({ + repo: service('repository/role/component'), + name: 'role', + type: 'role', + classNames: ['role-selector'], + state: 'role', + init: function() { + this._super(...arguments); + this.policyForm = get(this, 'formContainer').form('policy'); + this.source = new EventSource(); + }, + // You have to alias data + // is you just set it it loses its reference? + policy: alias('policyForm.data'), + actions: { + reset: function(e) { + this._super(...arguments); + get(this, 'policyForm').clear({ Datacenter: get(this, 'dc') }); + }, + dispatch: function(type, data) { + this.source.dispatchEvent({ type: type, data: data }); + }, + change: function() { + const event = get(this, 'dom').normalizeEvent(...arguments); + switch (event.target.name) { + case 'role[state]': + set(this, 'state', event.target.value); + break; + default: + this._super(...arguments); + } + }, + }, +}); diff --git a/ui-v2/app/components/service-identity.js b/ui-v2/app/components/service-identity.js new file mode 100644 index 0000000000..4798652642 --- /dev/null +++ b/ui-v2/app/components/service-identity.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui-v2/app/components/tab-nav.js b/ui-v2/app/components/tab-nav.js index 142b50bf4d..db166df641 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/tabular-collection.js b/ui-v2/app/components/tabular-collection.js index 15241374f3..c4d2e1c7a8 100644 --- a/ui-v2/app/components/tabular-collection.js +++ b/ui-v2/app/components/tabular-collection.js @@ -1,16 +1,12 @@ -import Component from 'ember-collection/components/ember-collection'; +import CollectionComponent from 'ember-collection/components/ember-collection'; import needsRevalidate from 'ember-collection/utils/needs-revalidate'; import identity from 'ember-collection/utils/identity'; import Grid from 'ember-collection/layouts/grid'; import SlotsMixin from 'block-slots'; import WithResizing from 'consul-ui/mixins/with-resizing'; import style from 'ember-computed-style'; -import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; -import sibling from 'consul-ui/utils/dom/sibling'; -import closest from 'consul-ui/utils/dom/closest'; -import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor'; -const clickFirstAnchor = clickFirstAnchorFactory(closest); +import { inject as service } from '@ember/service'; import { computed, get, set } from '@ember/object'; /** * Heavily extended `ember-collection` component @@ -24,8 +20,6 @@ import { computed, get, set } from '@ember/object'; * in the future */ -// ember doesn't like you using `$` hence `$$` -const $$ = qsaFactory(); // need to copy Cell in wholesale as there is no way to import it // there is no change made to `Cell` here, its only here as its // private in `ember-collection` @@ -85,13 +79,17 @@ const change = function(e) { // 'actions_close' would mean that all menus have been closed // therefore we don't need to calculate if (e.currentTarget.getAttribute('id') !== 'actions_close') { - const $tr = closest('tr', e.currentTarget); - const $group = sibling(e.currentTarget, 'ul'); - const $footer = [...$$('footer[role="contentinfo"]')][0]; + const dom = get(this, 'dom'); + + const $tr = dom.closest('tr', e.currentTarget); + const $group = dom.sibling(e.currentTarget, 'ul'); const groupRect = $group.getBoundingClientRect(); - const footerRect = $footer.getBoundingClientRect(); const groupBottom = groupRect.top + $group.clientHeight; + + const $footer = dom.element('footer[role="contentinfo"]'); + const footerRect = $footer.getBoundingClientRect(); const footerTop = footerRect.top; + if (groupBottom > footerTop) { $group.classList.add('above'); } else { @@ -113,39 +111,50 @@ const change = function(e) { } } }; -export default Component.extend(SlotsMixin, WithResizing, { +export default CollectionComponent.extend(SlotsMixin, WithResizing, { tagName: 'table', classNames: ['dom-recycling'], + classNameBindings: ['hasActions'], attributeBindings: ['style'], width: 1150, - height: 500, + rowHeight: 50, + maxHeight: 500, style: style('getStyle'), checked: null, hasCaption: false, + dom: service('dom'), init: function() { this._super(...arguments); this.change = change.bind(this); this.confirming = []; // TODO: The row height should auto calculate properly from the CSS - this['cell-layout'] = new ZIndexedGrid(get(this, 'width'), 50); + this['cell-layout'] = new ZIndexedGrid(get(this, 'width'), get(this, 'rowHeight')); }, - getStyle: computed('height', function() { + getStyle: computed('rowHeight', '_items', 'maxRows', 'maxHeight', function() { + const maxRows = get(this, 'rows'); + let height = get(this, 'maxHeight'); + if (maxRows) { + let rows = Math.max(3, get(this._items || [], 'length')); + rows = Math.min(maxRows, rows); + height = get(this, 'rowHeight') * rows + 29; + } return { - height: get(this, 'height'), + height: height, }; }), resize: function(e) { const $tbody = this.element; - const $appContent = [...$$('main > div')][0]; + const dom = get(this, 'dom'); + const $appContent = dom.element('main > div'); if ($appContent) { const border = 1; const rect = $tbody.getBoundingClientRect(); - const $footer = [...$$('footer[role="contentinfo"]')][0]; + const $footer = dom.element('footer[role="contentinfo"]'); const space = rect.top + $footer.clientHeight + border; const height = e.detail.height - space; - this.set('height', Math.max(0, height)); + this.set('maxHeight', Math.max(0, height)); // TODO: The row height should auto calculate properly from the CSS - this['cell-layout'] = new ZIndexedGrid($appContent.clientWidth, 50); + this['cell-layout'] = new ZIndexedGrid($appContent.clientWidth, get(this, 'rowHeight')); this.updateItems(); this.updateScrollPosition(); } @@ -273,7 +282,7 @@ export default Component.extend(SlotsMixin, WithResizing, { }, actions: { click: function(e) { - return clickFirstAnchor(e); + return get(this, 'dom').clickFirstAnchor(e); }, }, }); diff --git a/ui-v2/app/components/tabular-details.js b/ui-v2/app/components/tabular-details.js index 8b1d2d0fb6..5112ffffdc 100644 --- a/ui-v2/app/components/tabular-details.js +++ b/ui-v2/app/components/tabular-details.js @@ -1,17 +1,26 @@ import Component from '@ember/component'; import SlotsMixin from 'block-slots'; -import closest from 'consul-ui/utils/dom/closest'; -import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor'; -const clickFirstAnchor = clickFirstAnchorFactory(closest); +import { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; +import { subscribe } from 'consul-ui/utils/computed/purify'; +let uid = 0; export default Component.extend(SlotsMixin, { + dom: service('dom'), onchange: function() {}, + init: function() { + this._super(...arguments); + set(this, 'uid', uid++); + }, + inputId: subscribe('name', 'uid', function(name = 'name') { + return `tabular-details-${name}-toggle-${uid}_`; + }), actions: { click: function(e) { - clickFirstAnchor(e); + get(this, 'dom').clickFirstAnchor(e); }, - change: function(item, e) { - this.onchange(e, item); + change: function(item, items, e) { + this.onchange(e, item, items); }, }, }); diff --git a/ui-v2/app/components/tag-list.js b/ui-v2/app/components/tag-list.js new file mode 100644 index 0000000000..1656e4a23c --- /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/components/templated-anchor.js b/ui-v2/app/components/templated-anchor.js new file mode 100644 index 0000000000..9336ebf50d --- /dev/null +++ b/ui-v2/app/components/templated-anchor.js @@ -0,0 +1,73 @@ +import Component from '@ember/component'; +import { get, set, computed } from '@ember/object'; + +const createWeak = function(wm = new WeakMap()) { + return { + get: function(ref, prop) { + let map = wm.get(ref); + if (map) { + return map[prop]; + } + }, + set: function(ref, prop, value) { + let map = wm.get(ref); + if (typeof map === 'undefined') { + map = {}; + wm.set(ref, map); + } + map[prop] = value; + return map[prop]; + }, + }; +}; +const weak = createWeak(); +// Covers alpha-capitalized dot separated API keys such as +// `{{Name}}`, `{{Service.Name}}` etc. but not `{{}}` +const templateRe = /{{([A-Za-z.0-9_-]+)}}/g; +export default Component.extend({ + tagName: 'a', + attributeBindings: ['href', 'rel', 'target'], + rel: computed({ + get: function(prop) { + return weak.get(this, prop); + }, + set: function(prop, value) { + switch (value) { + case 'external': + value = `${value} noopener noreferrer`; + set(this, 'target', '_blank'); + break; + } + return weak.set(this, prop, value); + }, + }), + vars: computed({ + get: function(prop) { + return weak.get(this, prop); + }, + set: function(prop, value) { + weak.set(this, prop, value); + set(this, 'href', weak.get(this, 'template')); + }, + }), + href: computed({ + get: function(prop) { + return weak.get(this, prop); + }, + set: function(prop, value) { + weak.set(this, 'template', value); + const vars = weak.get(this, 'vars'); + if (typeof vars !== 'undefined' && typeof value !== 'undefined') { + value = value.replace(templateRe, function(match, group) { + try { + return get(vars, group) || ''; + } catch (e) { + return ''; + } + }); + return weak.set(this, prop, value); + } + return ''; + }, + }), +}); diff --git a/ui-v2/app/components/tomography-graph.js b/ui-v2/app/components/tomography-graph.js index c5d4beb130..5701acea71 100644 --- a/ui-v2/app/components/tomography-graph.js +++ b/ui-v2/app/components/tomography-graph.js @@ -28,25 +28,26 @@ export default Component.extend({ ]; }), distances: computed('tomography', function() { - const tomography = this.get('tomography'); - let distances = tomography.distances || []; + const tomography = get(this, 'tomography'); + let distances = get(tomography, 'distances') || []; distances.forEach((d, i) => { if (d.distance > get(this, 'max')) { set(this, 'max', d.distance); } }); - if (tomography.n > 360) { - let n = distances.length; + let n = get(distances, 'length'); + if (n > 360) { // We have more nodes than we want to show, take a random sampling to keep // the number around 360. - const sampling = 360 / tomography.n; + const sampling = 360 / n; distances = distances.filter(function(_, i) { return i == 0 || i == n - 1 || Math.random() < sampling; }); + n = get(distances, 'length'); } return distances.map((d, i) => { return { - rotate: i * 360 / distances.length, + rotate: (i * 360) / n, y2: -insetSize * (d.distance / get(this, 'max')), node: d.node, distance: d.distance, diff --git a/ui-v2/app/computed/catchable.js b/ui-v2/app/computed/catchable.js new file mode 100644 index 0000000000..9cc2bf008b --- /dev/null +++ b/ui-v2/app/computed/catchable.js @@ -0,0 +1,11 @@ +import ComputedProperty from '@ember/object/computed'; +import computedFactory from 'consul-ui/utils/computed/factory'; + +export default class Catchable extends ComputedProperty { + catch(cb) { + return this.meta({ + catch: cb, + }); + } +} +export const computed = computedFactory(Catchable); diff --git a/ui-v2/app/controllers/dc/acls/edit.js b/ui-v2/app/controllers/dc/acls/edit.js index a1c86d2798..2c1d2d17fe 100644 --- a/ui-v2/app/controllers/dc/acls/edit.js +++ b/ui-v2/app/controllers/dc/acls/edit.js @@ -1,30 +1,31 @@ import Controller from '@ember/controller'; -import { set } from '@ember/object'; -import Changeset from 'ember-changeset'; -import validations from 'consul-ui/validations/acl'; -import lookupValidator from 'ember-changeset-validations'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; export default Controller.extend({ + builder: service('form'), + dom: service('dom'), + init: function() { + this._super(...arguments); + this.form = get(this, 'builder').form('acl'); + }, setProperties: function(model) { - this.changeset = new Changeset(model.item, lookupValidator(validations), validations); - this._super({ - ...model, - ...{ - item: this.changeset, - }, - }); + // essentially this replaces the data with changesets + this._super( + Object.keys(model).reduce((prev, key, i) => { + switch (key) { + case 'item': + prev[key] = this.form.setData(prev[key]).getData(); + break; + } + return prev; + }, model) + ); }, actions: { - change: function(e) { - const target = e.target || { name: 'Rules', value: e }; - switch (target.name) { - case 'Type': - set(this.changeset, target.name, target.value); - break; - case 'Rules': - set(this, 'item.Rules', target.value); - break; - } + change: function(e, value, item) { + const event = get(this, 'dom').normalizeEvent(e, value); + get(this, 'form').handleEvent(event); }, }, }); diff --git a/ui-v2/app/controllers/dc/acls/index.js b/ui-v2/app/controllers/dc/acls/index.js index 4da546da81..2d49d9fed4 100644 --- a/ui-v2/app/controllers/dc/acls/index.js +++ b/ui-v2/app/controllers/dc/acls/index.js @@ -1,11 +1,12 @@ import Controller from '@ember/controller'; import { computed, get } from '@ember/object'; import WithFiltering from 'consul-ui/mixins/with-filtering'; +import WithSearching from 'consul-ui/mixins/with-searching'; import ucfirst from 'consul-ui/utils/ucfirst'; const countType = function(items, type) { return type === '' ? get(items, 'length') : items.filterBy('Type', type).length; }; -export default Controller.extend(WithFiltering, { +export default Controller.extend(WithSearching, WithFiltering, { queryParams: { type: { as: 'type', @@ -15,6 +16,17 @@ export default Controller.extend(WithFiltering, { replace: true, }, }, + init: function() { + this.searchParams = { + acl: 's', + }; + this._super(...arguments); + }, + searchable: computed('filtered', function() { + return get(this, 'searchables.acl') + .add(get(this, 'filtered')) + .search(get(this, this.searchParams.acl)); + }), typeFilters: computed('items', function() { const items = get(this, 'items'); return ['', 'management', 'client'].map(function(item) { @@ -27,17 +39,8 @@ export default Controller.extend(WithFiltering, { }; }); }), - filter: function(item, { s = '', type = '' }) { - const sLower = s.toLowerCase(); - return ( - (get(item, 'Name') - .toLowerCase() - .indexOf(sLower) !== -1 || - get(item, 'ID') - .toLowerCase() - .indexOf(sLower) !== -1) && - (type === '' || get(item, 'Type') === type) - ); + filter: function(item, { type = '' }) { + return type === '' || get(item, 'Type') === type; }, actions: { sendClone: function(item) { diff --git a/ui-v2/app/controllers/dc/acls/policies/edit.js b/ui-v2/app/controllers/dc/acls/policies/edit.js index 1c02cbfaf1..a9958e95c7 100644 --- a/ui-v2/app/controllers/dc/acls/policies/edit.js +++ b/ui-v2/app/controllers/dc/acls/policies/edit.js @@ -1,10 +1,8 @@ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; -import { get, set } from '@ember/object'; +import { get } from '@ember/object'; export default Controller.extend({ builder: service('form'), - dom: service('dom'), - isScoped: false, init: function() { this._super(...arguments); this.form = get(this, 'builder').form('policy'); @@ -21,25 +19,5 @@ export default Controller.extend({ return prev; }, model) ); - set(this, 'isScoped', get(model.item, 'Datacenters.length') > 0); - }, - actions: { - change: function(e, value, item) { - const form = get(this, 'form'); - const event = get(this, 'dom').normalizeEvent(e, value); - try { - form.handleEvent(event); - } catch (err) { - const target = event.target; - switch (target.name) { - case 'policy[isScoped]': - set(this, 'isScoped', !get(this, 'isScoped')); - set(this.item, 'Datacenters', null); - break; - default: - throw err; - } - } - }, }, }); diff --git a/ui-v2/app/controllers/dc/acls/policies/index.js b/ui-v2/app/controllers/dc/acls/policies/index.js index 534a82cda2..ea6142dbd7 100644 --- a/ui-v2/app/controllers/dc/acls/policies/index.js +++ b/ui-v2/app/controllers/dc/acls/policies/index.js @@ -1,23 +1,23 @@ import Controller from '@ember/controller'; -import { get } from '@ember/object'; -import WithFiltering from 'consul-ui/mixins/with-filtering'; -export default Controller.extend(WithFiltering, { +import { get, computed } from '@ember/object'; +import WithSearching from 'consul-ui/mixins/with-searching'; +export default Controller.extend(WithSearching, { queryParams: { s: { as: 'filter', replace: true, }, }, - filter: function(item, { s = '', type = '' }) { - const sLower = s.toLowerCase(); - return ( - get(item, 'Name') - .toLowerCase() - .indexOf(sLower) !== -1 || - get(item, 'Description') - .toLowerCase() - .indexOf(sLower) !== -1 - ); + init: function() { + this.searchParams = { + policy: 's', + }; + this._super(...arguments); }, + searchable: computed('items', function() { + return get(this, 'searchables.policy') + .add(get(this, 'items')) + .search(get(this, this.searchParams.policy)); + }), actions: {}, }); diff --git a/ui-v2/app/controllers/dc/acls/roles/create.js b/ui-v2/app/controllers/dc/acls/roles/create.js new file mode 100644 index 0000000000..4723e0ce43 --- /dev/null +++ b/ui-v2/app/controllers/dc/acls/roles/create.js @@ -0,0 +1,2 @@ +import Controller from './edit'; +export default Controller.extend(); diff --git a/ui-v2/app/controllers/dc/acls/roles/edit.js b/ui-v2/app/controllers/dc/acls/roles/edit.js new file mode 100644 index 0000000000..b576505a88 --- /dev/null +++ b/ui-v2/app/controllers/dc/acls/roles/edit.js @@ -0,0 +1,23 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +export default Controller.extend({ + builder: service('form'), + init: function() { + this._super(...arguments); + this.form = get(this, 'builder').form('role'); + }, + setProperties: function(model) { + // essentially this replaces the data with changesets + this._super( + Object.keys(model).reduce((prev, key, i) => { + switch (key) { + case 'item': + prev[key] = this.form.setData(prev[key]).getData(); + break; + } + return prev; + }, model) + ); + }, +}); diff --git a/ui-v2/app/controllers/dc/acls/roles/index.js b/ui-v2/app/controllers/dc/acls/roles/index.js new file mode 100644 index 0000000000..0ef2236d10 --- /dev/null +++ b/ui-v2/app/controllers/dc/acls/roles/index.js @@ -0,0 +1,23 @@ +import Controller from '@ember/controller'; +import { get, computed } from '@ember/object'; +import WithSearching from 'consul-ui/mixins/with-searching'; +export default Controller.extend(WithSearching, { + queryParams: { + s: { + as: 'filter', + replace: true, + }, + }, + init: function() { + this.searchParams = { + role: 's', + }; + this._super(...arguments); + }, + searchable: computed('items', function() { + return get(this, 'searchables.role') + .add(get(this, 'items')) + .search(get(this, this.searchParams.role)); + }), + actions: {}, +}); diff --git a/ui-v2/app/controllers/dc/acls/tokens/edit.js b/ui-v2/app/controllers/dc/acls/tokens/edit.js index a10c6e10e6..0a3585647c 100644 --- a/ui-v2/app/controllers/dc/acls/tokens/edit.js +++ b/ui-v2/app/controllers/dc/acls/tokens/edit.js @@ -1,6 +1,6 @@ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; -import { get, set } from '@ember/object'; +import { get } from '@ember/object'; export default Controller.extend({ dom: service('dom'), builder: service('form'), @@ -17,59 +17,20 @@ export default Controller.extend({ case 'item': prev[key] = this.form.setData(prev[key]).getData(); break; - case 'policy': - prev[key] = this.form - .form(key) - .setData(prev[key]) - .getData(); - break; } return prev; }, model) ); }, actions: { - sendClearPolicy: function(item) { - set(this, 'isScoped', false); - this.send('clearPolicy'); - }, - sendCreatePolicy: function(item, policies, success) { - this.send('createPolicy', item, policies, success); - }, - refreshCodeEditor: function(selector, parent) { - if (parent.target) { - parent = undefined; - } - get(this, 'dom') - .component(selector, parent) - .didAppear(); - }, change: function(e, value, item) { - const form = get(this, 'form'); const event = get(this, 'dom').normalizeEvent(e, value); + const form = get(this, 'form'); try { form.handleEvent(event); } catch (err) { const target = event.target; switch (target.name) { - case 'policy[isScoped]': - set(this, 'isScoped', !get(this, 'isScoped')); - set(this.policy, 'Datacenters', null); - break; - case 'Policy': - set(value, 'CreateTime', new Date().getTime()); - get(this, 'item.Policies').pushObject(value); - break; - case 'Details': - // the Details expander toggle - // only load on opening - if (target.checked) { - this.send('refreshCodeEditor', '.code-editor', target.parentNode); - if (!get(value, 'Rules')) { - this.send('loadPolicy', value, get(this, 'item.Policies')); - } - } - break; default: throw err; } diff --git a/ui-v2/app/controllers/dc/acls/tokens/index.js b/ui-v2/app/controllers/dc/acls/tokens/index.js index 729664c72c..4c65831667 100644 --- a/ui-v2/app/controllers/dc/acls/tokens/index.js +++ b/ui-v2/app/controllers/dc/acls/tokens/index.js @@ -1,30 +1,24 @@ import Controller from '@ember/controller'; -import { get } from '@ember/object'; -import WithFiltering from 'consul-ui/mixins/with-filtering'; -export default Controller.extend(WithFiltering, { +import { computed, get } from '@ember/object'; +import WithSearching from 'consul-ui/mixins/with-searching'; +export default Controller.extend(WithSearching, { queryParams: { s: { as: 'filter', replace: true, }, }, - filter: function(item, { s = '', type = '' }) { - const sLower = s.toLowerCase(); - return ( - get(item, 'AccessorID') - .toLowerCase() - .indexOf(sLower) !== -1 || - get(item, 'Name') - .toLowerCase() - .indexOf(sLower) !== -1 || - get(item, 'Description') - .toLowerCase() - .indexOf(sLower) !== -1 || - (get(item, 'Policies') || []).some(function(item) { - return item.Name.toLowerCase().indexOf(sLower) !== -1; - }) - ); + init: function() { + this.searchParams = { + token: 's', + }; + this._super(...arguments); }, + searchable: computed('items', function() { + return get(this, 'searchables.token') + .add(get(this, 'items')) + .search(get(this, this.searchParams.token)); + }), actions: { sendClone: function(item) { this.send('clone', item); diff --git a/ui-v2/app/controllers/dc/intentions/edit.js b/ui-v2/app/controllers/dc/intentions/edit.js index 0773435c3a..0d622d4858 100644 --- a/ui-v2/app/controllers/dc/intentions/edit.js +++ b/ui-v2/app/controllers/dc/intentions/edit.js @@ -1,13 +1,14 @@ import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; import { get, set } from '@ember/object'; -import Changeset from 'ember-changeset'; -import lookupValidator from 'ember-changeset-validations'; - -import validations from 'consul-ui/validations/intention'; - export default Controller.extend({ + dom: service('dom'), + builder: service('form'), + init: function() { + this._super(...arguments); + this.form = get(this, 'builder').form('intention'); + }, setProperties: function(model) { - this.changeset = new Changeset(model.item, lookupValidator(validations), validations); const sourceName = get(model.item, 'SourceName'); const destinationName = get(model.item, 'DestinationName'); let source = model.items.findBy('Name', sourceName); @@ -23,50 +24,57 @@ export default Controller.extend({ this._super({ ...model, ...{ - item: this.changeset, + item: this.form.setData(model.item).getData(), SourceName: source, DestinationName: destination, }, }); }, actions: { - createNewLabel: function(term) { - return `Use a future Consul Service called '${term}'`; + createNewLabel: function(template, term) { + return template.replace(/{{term}}/g, term); }, isUnique: function(term) { return !get(this, 'items').findBy('Name', term); }, - change: function(e, value, _target) { - // normalize back to standard event - const target = e.target || { ..._target, ...{ name: e, value: value } }; - let name, selected; - name = selected = target.value; - // TODO: - // linter needs this here? + change: function(e, value, item) { + const event = get(this, 'dom').normalizeEvent(e, value); + const form = get(this, 'form'); + const target = event.target; + + let name; + let selected; let match; switch (target.name) { - case 'Description': - case 'Action': - set(this.changeset, target.name, target.value); - break; case 'SourceName': case 'DestinationName': + name = selected = target.value; + // Names can be selected Service EmberObjects or typed in strings + // if its not a string, use the `Name` from the Service EmberObject if (typeof name !== 'string') { name = get(target.value, 'Name'); } - // linter doesn't like const here + // see if the name is already in the list match = get(this, 'items').filterBy('Name', name); if (match.length === 0) { + // if its not make a new 'fake' Service that doesn't exist yet + // and add it to the possible services to make an intention between selected = { Name: name }; - // linter doesn't mind const here? const items = [selected].concat(this.items.toArray()); set(this, 'items', items); } - set(this.changeset, target.name, name); + // mutate the value with the string name + // which will be handled by the form + target.value = name; + // these are 'non-form' variables so not on `item` + // these variables also exist in the template so we know + // the current selection + // basically the difference between + // `item.DestinationName` and just `DestinationName` set(this, target.name, selected); break; } - this.changeset.validate(); + form.handleEvent(event); }, }, }); diff --git a/ui-v2/app/controllers/dc/intentions/index.js b/ui-v2/app/controllers/dc/intentions/index.js index 0ff73e7cdf..88c0b8e72d 100644 --- a/ui-v2/app/controllers/dc/intentions/index.js +++ b/ui-v2/app/controllers/dc/intentions/index.js @@ -1,6 +1,7 @@ import Controller from '@ember/controller'; import { computed, get } from '@ember/object'; import WithFiltering from 'consul-ui/mixins/with-filtering'; +import WithSearching from 'consul-ui/mixins/with-searching'; import ucfirst from 'consul-ui/utils/ucfirst'; // TODO: DRY out in acls at least const createCounter = function(prop) { @@ -9,7 +10,7 @@ const createCounter = function(prop) { }; }; const countAction = createCounter('Action'); -export default Controller.extend(WithFiltering, { +export default Controller.extend(WithSearching, WithFiltering, { queryParams: { action: { as: 'action', @@ -19,6 +20,17 @@ export default Controller.extend(WithFiltering, { replace: true, }, }, + init: function() { + this.searchParams = { + intention: 's', + }; + this._super(...arguments); + }, + searchable: computed('filtered', function() { + return get(this, 'searchables.intention') + .add(get(this, 'filtered')) + .search(get(this, this.searchParams.intention)); + }), actionFilters: computed('items', function() { const items = get(this, 'items'); return ['', 'allow', 'deny'].map(function(item) { @@ -32,16 +44,6 @@ export default Controller.extend(WithFiltering, { }); }), filter: function(item, { s = '', action = '' }) { - const source = get(item, 'SourceName').toLowerCase(); - const destination = get(item, 'DestinationName').toLowerCase(); - const sLower = s.toLowerCase(); - const allLabel = 'All Services (*)'.toLowerCase(); - return ( - (source.indexOf(sLower) !== -1 || - destination.indexOf(sLower) !== -1 || - (source === '*' && allLabel.indexOf(sLower) !== -1) || - (destination === '*' && allLabel.indexOf(sLower) !== -1)) && - (action === '' || get(item, 'Action') === action) - ); + return action === '' || get(item, 'Action') === action; }, }); diff --git a/ui-v2/app/controllers/dc/kv/edit.js b/ui-v2/app/controllers/dc/kv/edit.js index 33f2632909..bdfeb18a63 100644 --- a/ui-v2/app/controllers/dc/kv/edit.js +++ b/ui-v2/app/controllers/dc/kv/edit.js @@ -2,41 +2,56 @@ import Controller from '@ember/controller'; import { get, set } from '@ember/object'; import { inject as service } from '@ember/service'; -import Changeset from 'ember-changeset'; -import validations from 'consul-ui/validations/kv'; -import lookupValidator from 'ember-changeset-validations'; export default Controller.extend({ - json: true, + dom: service('dom'), + builder: service('form'), encoder: service('btoa'), + json: true, + init: function() { + this._super(...arguments); + this.form = get(this, 'builder').form('kv'); + }, setProperties: function(model) { - // TODO: Potentially save whether json has been clicked to the model, - // setting set(this, 'json', true) here will force the form to always default to code=on - // even if the user has selected code=off on another KV - // ideally we would save the value per KV, but I'd like to not do that on the model - // a set(this, 'json', valueFromSomeStorageJustForThisKV) would be added here - this.changeset = new Changeset(model.item, lookupValidator(validations), validations); - this._super({ - ...model, - ...{ - item: this.changeset, - }, - }); + // essentially this replaces the data with changesets + this._super( + Object.keys(model).reduce((prev, key, i) => { + switch (key) { + case 'item': + prev[key] = this.form.setData(prev[key]).getData(); + break; + } + return prev; + }, model) + ); }, actions: { - change: function(e) { - const target = e.target || { name: 'value', value: e }; - var parent; - switch (target.name) { - case 'additional': - parent = get(this, 'parent.Key'); - set(this.changeset, 'Key', `${parent !== '/' ? parent : ''}${target.value}`); - break; - case 'json': - set(this, 'json', !get(this, 'json')); - break; - case 'value': - set(this, 'item.Value', get(this, 'encoder').execute(target.value)); - break; + change: function(e, value, item) { + const event = get(this, 'dom').normalizeEvent(e, value); + const form = get(this, 'form'); + try { + form.handleEvent(event); + } catch (err) { + const target = event.target; + let parent; + switch (target.name) { + case 'value': + set(this.item, 'Value', get(this, 'encoder').execute(target.value)); + break; + case 'additional': + parent = get(this, 'parent.Key'); + set(this.item, 'Key', `${parent !== '/' ? parent : ''}${target.value}`); + break; + case 'json': + // TODO: Potentially save whether json has been clicked to the model, + // setting set(this, 'json', true) here will force the form to always default to code=on + // even if the user has selected code=off on another KV + // ideally we would save the value per KV, but I'd like to not do that on the model + // a set(this, 'json', valueFromSomeStorageJustForThisKV) would be added here + set(this, 'json', !get(this, 'json')); + break; + default: + throw err; + } } }, }, diff --git a/ui-v2/app/controllers/dc/kv/index.js b/ui-v2/app/controllers/dc/kv/index.js index 20a2399058..ac19fabfd5 100644 --- a/ui-v2/app/controllers/dc/kv/index.js +++ b/ui-v2/app/controllers/dc/kv/index.js @@ -1,18 +1,22 @@ import Controller from '@ember/controller'; -import { get } from '@ember/object'; -import WithFiltering from 'consul-ui/mixins/with-filtering'; -import rightTrim from 'consul-ui/utils/right-trim'; -export default Controller.extend(WithFiltering, { +import { get, computed } from '@ember/object'; +import WithSearching from 'consul-ui/mixins/with-searching'; +export default Controller.extend(WithSearching, { queryParams: { s: { as: 'filter', replace: true, }, }, - filter: function(item, { s = '' }) { - const key = rightTrim(get(item, 'Key'), '/') - .split('/') - .pop(); - return key.toLowerCase().indexOf(s.toLowerCase()) !== -1; + init: function() { + this.searchParams = { + kv: 's', + }; + this._super(...arguments); }, + searchable: computed('items', function() { + return get(this, 'searchables.kv') + .add(get(this, 'items')) + .search(get(this, this.searchParams.kv)); + }), }); diff --git a/ui-v2/app/controllers/dc/nodes/index.js b/ui-v2/app/controllers/dc/nodes/index.js index 2202b73da1..8265570612 100644 --- a/ui-v2/app/controllers/dc/nodes/index.js +++ b/ui-v2/app/controllers/dc/nodes/index.js @@ -1,12 +1,27 @@ import Controller from '@ember/controller'; import { computed } from '@ember/object'; +import WithEventSource from 'consul-ui/mixins/with-event-source'; import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering'; +import WithSearching from 'consul-ui/mixins/with-searching'; import { get } from '@ember/object'; -export default Controller.extend(WithHealthFiltering, { +export default Controller.extend(WithEventSource, WithSearching, WithHealthFiltering, { init: function() { + this.searchParams = { + healthyNode: 's', + unhealthyNode: 's', + }; this._super(...arguments); - this.columns = [25, 25, 25, 25]; }, + searchableHealthy: computed('healthy', function() { + return get(this, 'searchables.healthyNode') + .add(get(this, 'healthy')) + .search(get(this, this.searchParams.healthyNode)); + }), + searchableUnhealthy: computed('unhealthy', function() { + return get(this, 'searchables.unhealthyNode') + .add(get(this, 'unhealthy')) + .search(get(this, this.searchParams.unhealthyNode)); + }), unhealthy: computed('filtered', function() { return get(this, 'filtered').filter(function(item) { return get(item, 'isUnhealthy'); @@ -18,10 +33,6 @@ export default Controller.extend(WithHealthFiltering, { }); }), filter: function(item, { s = '', status = '' }) { - return ( - get(item, 'Node') - .toLowerCase() - .indexOf(s.toLowerCase()) !== -1 && item.hasStatus(status) - ); + return item.hasStatus(status); }, }); diff --git a/ui-v2/app/controllers/dc/nodes/show.js b/ui-v2/app/controllers/dc/nodes/show.js index 1eb34e6bec..3dc82775e6 100644 --- a/ui-v2/app/controllers/dc/nodes/show.js +++ b/ui-v2/app/controllers/dc/nodes/show.js @@ -1,18 +1,46 @@ import Controller from '@ember/controller'; -import { get, set } from '@ember/object'; -import { getOwner } from '@ember/application'; -import WithFiltering from 'consul-ui/mixins/with-filtering'; -import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; -import getComponentFactory from 'consul-ui/utils/get-component-factory'; +import { inject as service } from '@ember/service'; +import { get, set, computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import WithSearching from 'consul-ui/mixins/with-searching'; +import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source'; -const $$ = qsaFactory(); -export default Controller.extend(WithFiltering, { +export default Controller.extend(WithEventSource, WithSearching, { + dom: service('dom'), + notify: service('flashMessages'), + items: alias('item.Services'), queryParams: { s: { as: 'filter', replace: true, }, }, + init: function() { + this.searchParams = { + nodeservice: 's', + }; + this._super(...arguments); + }, + item: listen('item').catch(function(e) { + if (e.target.readyState === 1) { + // OPEN + if (get(e, 'error.errors.firstObject.status') === '404') { + get(this, 'notify').add({ + destroyOnClick: false, + sticky: true, + type: 'warning', + action: 'update', + }); + get(this, 'tomography').close(); + get(this, 'sessions').close(); + } + } + }), + searchable: computed('items', function() { + return get(this, 'searchables.nodeservice') + .add(get(this, 'items')) + .search(get(this, this.searchParams.nodeservice)); + }), setProperties: function() { this._super(...arguments); // the default selected tab depends on whether you have any healthchecks or not @@ -20,38 +48,20 @@ export default Controller.extend(WithFiltering, { // 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', get(this.item, 'Checks.length') > 0 ? 'health-checks' : 'services'); - }, - filter: function(item, { s = '' }) { - const term = s.toLowerCase(); - return ( - get(item, 'Service') - .toLowerCase() - .indexOf(term) !== -1 || - get(item, 'ID') - .toLowerCase() - .indexOf(term) !== -1 || - (get(item, 'Tags') || []).some(function(item) { - return item.toLowerCase().indexOf(term) !== -1; - }) || - get(item, 'Port') - .toString() - .toLowerCase() - .indexOf(term) !== -1 - ); + set(this, 'selectedTab', get(this, 'item.Checks.length') > 0 ? 'health-checks' : 'services'); }, actions: { change: function(e) { set(this, 'selectedTab', e.target.value); - const getComponent = getComponentFactory(getOwner(this)); // Ensure tabular-collections sizing is recalculated // now it is visible in the DOM - [...$$('.tab-section input[type="radio"]:checked + div table')].forEach(function(item) { - const component = getComponent(item); - if (component && typeof component.didAppear === 'function') { - getComponent(item).didAppear(); - } - }); + get(this, 'dom') + .components('.tab-section input[type="radio"]:checked + div table') + .forEach(function(item) { + if (typeof item.didAppear === 'function') { + item.didAppear(); + } + }); }, sortChecksByImportance: function(a, b) { const statusA = get(a, 'Status'); diff --git a/ui-v2/app/controllers/dc/services/index.js b/ui-v2/app/controllers/dc/services/index.js index a0cc0bd399..e8bfff9a45 100644 --- a/ui-v2/app/controllers/dc/services/index.js +++ b/ui-v2/app/controllers/dc/services/index.js @@ -1,7 +1,8 @@ import Controller from '@ember/controller'; import { get, computed } from '@ember/object'; import { htmlSafe } from '@ember/string'; -import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering'; +import WithEventSource from 'consul-ui/mixins/with-event-source'; +import WithSearching from 'consul-ui/mixins/with-searching'; const max = function(arr, prop) { return arr.reduce(function(prev, item) { return Math.max(prev, get(item, prop)); @@ -24,19 +25,23 @@ const width = function(num) { const widthDeclaration = function(num) { return htmlSafe(`width: ${num}px`); }; -export default Controller.extend(WithHealthFiltering, { - filter: function(item, { s = '', status = '' }) { - const term = s.toLowerCase(); - return ( - (get(item, 'Name') - .toLowerCase() - .indexOf(term) !== -1 || - (get(item, 'Tags') || []).some(function(item) { - return item.toLowerCase().indexOf(term) !== -1; - })) && - item.hasStatus(status) - ); +export default Controller.extend(WithEventSource, WithSearching, { + queryParams: { + s: { + as: 'filter', + }, }, + init: function() { + this.searchParams = { + service: 's', + }; + this._super(...arguments); + }, + searchable: computed('items.[]', function() { + return get(this, 'searchables.service') + .add(get(this, 'items')) + .search(get(this, 'terms')); + }), maxWidth: computed('{maxPassing,maxWarning,maxCritical}', function() { const PADDING = 32 * 3 + 13; return ['maxPassing', 'maxWarning', 'maxCritical'].reduce((prev, item) => { @@ -47,15 +52,20 @@ export default Controller.extend(WithHealthFiltering, { return widthDeclaration(get(this, 'maxWidth')); }), remainingWidth: computed('maxWidth', function() { - return htmlSafe(`width: calc(50% - ${Math.round(get(this, 'maxWidth') / 2)}px)`); + // maxWidth is the maximum width of the healthchecks column + // there are currently 2 other columns so divide it by 2 and + // take that off 50% (100% / number of fluid columns) + // also we added a Type column which we've currently fixed to 100px + // so again divide that by 2 and take it off each fluid column + return htmlSafe(`width: calc(50% - 50px - ${Math.round(get(this, 'maxWidth') / 2)}px)`); }), - maxPassing: computed('items', function() { + maxPassing: computed('items.[]', function() { return max(get(this, 'items'), 'ChecksPassing'); }), - maxWarning: computed('items', function() { + maxWarning: computed('items.[]', function() { return max(get(this, 'items'), 'ChecksWarning'); }), - maxCritical: computed('items', function() { + maxCritical: computed('items.[]', function() { return max(get(this, 'items'), 'ChecksCritical'); }), passingWidth: computed('maxPassing', function() { 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 0000000000..dcde2743ad --- /dev/null +++ b/ui-v2/app/controllers/dc/services/instance.js @@ -0,0 +1,37 @@ +import Controller from '@ember/controller'; +import { get, set } from '@ember/object'; +import { inject as service } from '@ember/service'; +import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source'; + +export default Controller.extend(WithEventSource, { + notify: service('flashMessages'), + 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'); + }, + item: listen('item').catch(function(e) { + if (e.target.readyState === 1) { + // OPEN + if (get(e, 'error.errors.firstObject.status') === '404') { + get(this, 'notify').add({ + destroyOnClick: false, + sticky: true, + type: 'warning', + action: 'update', + }); + const proxy = get(this, 'proxy'); + if (proxy) { + proxy.close(); + } + } + } + }), + 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 5a08b008ae..8182d66203 100644 --- a/ui-v2/app/controllers/dc/services/show.js +++ b/ui-v2/app/controllers/dc/services/show.js @@ -1,34 +1,56 @@ import Controller from '@ember/controller'; -import { get } from '@ember/object'; -import { 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'; -export default Controller.extend(WithHealthFiltering, { +import { get, set, computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import WithSearching from 'consul-ui/mixins/with-searching'; +import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source'; +export default Controller.extend(WithEventSource, WithSearching, { + dom: service('dom'), + notify: service('flashMessages'), + items: alias('item.Nodes'), init: function() { + this.searchParams = { + serviceInstance: 's', + }; this._super(...arguments); }, - unhealthy: 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'); + }, + item: listen('item').catch(function(e) { + if (e.target.readyState === 1) { + // OPEN + if (get(e, 'error.errors.firstObject.status') === '404') { + get(this, 'notify').add({ + destroyOnClick: false, + sticky: true, + type: 'warning', + action: 'update', + }); + } + } }), - healthy: computed('filtered', function() { - return get(this, 'filtered').filter(function(item) { - return sumOfUnhealthy(item.Checks) === 0; - }); + searchable: computed('items', function() { + return get(this, 'searchables.serviceInstance') + .add(get(this, 'items')) + .search(get(this, this.searchParams.serviceInstance)); }), - filter: function(item, { s = '', status = '' }) { - const term = s.toLowerCase(); - - return ( - get(item, 'Node.Node') - .toLowerCase() - .indexOf(term) !== -1 || - (get(item, 'Service.ID') - .toLowerCase() - .indexOf(term) !== -1 && - 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/controllers/settings.js b/ui-v2/app/controllers/settings.js new file mode 100644 index 0000000000..91771e0f94 --- /dev/null +++ b/ui-v2/app/controllers/settings.js @@ -0,0 +1,43 @@ +import Controller from '@ember/controller'; +import { get, set } from '@ember/object'; +import { inject as service } from '@ember/service'; + +export default Controller.extend({ + repo: service('settings'), + dom: service('dom'), + actions: { + key: function(e) { + switch (true) { + case e.keyCode === 13: + // disable ENTER + e.preventDefault(); + } + }, + change: function(e, value, item) { + const event = get(this, 'dom').normalizeEvent(e, value); + // TODO: Switch to using forms like the rest of the app + // setting utils/form/builder for things to be done before we + // can do that. For the moment just do things normally its a simple + // enough form at the moment + + const target = event.target; + const blocking = get(this, 'item.client.blocking'); + switch (target.name) { + case 'client[blocking]': + if (typeof blocking === 'undefined') { + set(this, 'item.client', {}); + } + set(this, 'item.client.blocking', !blocking); + this.send('update', get(this, 'item')); + break; + case 'urls[service]': + if (typeof get(this, 'item.urls') === 'undefined') { + set(this, 'item.urls', {}); + } + set(this, 'item.urls.service', target.value); + this.send('update', get(this, 'item')); + break; + } + }, + }, +}); diff --git a/ui-v2/app/env.js b/ui-v2/app/env.js new file mode 100644 index 0000000000..5b4ab0e371 --- /dev/null +++ b/ui-v2/app/env.js @@ -0,0 +1,5 @@ +import config from './config/environment'; +export default function(str) { + const user = window.localStorage.getItem(str); + return user !== null ? user : config[str]; +} diff --git a/ui-v2/app/forms/acl.js b/ui-v2/app/forms/acl.js new file mode 100644 index 0000000000..a6c2503046 --- /dev/null +++ b/ui-v2/app/forms/acl.js @@ -0,0 +1,6 @@ +import validations from 'consul-ui/validations/acl'; +import builderFactory from 'consul-ui/utils/form/builder'; +const builder = builderFactory(); +export default function(container, name = '', v = validations, form = builder) { + return form(name, {}).setValidators(v); +} diff --git a/ui-v2/app/forms/intention.js b/ui-v2/app/forms/intention.js new file mode 100644 index 0000000000..3d4139fd0f --- /dev/null +++ b/ui-v2/app/forms/intention.js @@ -0,0 +1,6 @@ +import validations from 'consul-ui/validations/intention'; +import builderFactory from 'consul-ui/utils/form/builder'; +const builder = builderFactory(); +export default function(container, name = '', v = validations, form = builder) { + return form(name, {}).setValidators(v); +} diff --git a/ui-v2/app/forms/kv.js b/ui-v2/app/forms/kv.js new file mode 100644 index 0000000000..a68d5f1b8f --- /dev/null +++ b/ui-v2/app/forms/kv.js @@ -0,0 +1,6 @@ +import validations from 'consul-ui/validations/kv'; +import builderFactory from 'consul-ui/utils/form/builder'; +const builder = builderFactory(); +export default function(container, name = '', v = validations, form = builder) { + return form(name, {}).setValidators(v); +} diff --git a/ui-v2/app/forms/policy.js b/ui-v2/app/forms/policy.js index f1452fd97b..f920c5b56f 100644 --- a/ui-v2/app/forms/policy.js +++ b/ui-v2/app/forms/policy.js @@ -1,7 +1,7 @@ import validations from 'consul-ui/validations/policy'; import builderFactory from 'consul-ui/utils/form/builder'; const builder = builderFactory(); -export default function(name = 'policy', v = validations, form = builder) { +export default function(container, name = 'policy', v = validations, form = builder) { return form(name, { Datacenters: { type: 'array', diff --git a/ui-v2/app/forms/role.js b/ui-v2/app/forms/role.js new file mode 100644 index 0000000000..54f6f80eff --- /dev/null +++ b/ui-v2/app/forms/role.js @@ -0,0 +1,8 @@ +import validations from 'consul-ui/validations/role'; +import builderFactory from 'consul-ui/utils/form/builder'; +const builder = builderFactory(); +export default function(container, name = 'role', v = validations, form = builder) { + return form(name, {}) + .setValidators(v) + .add(container.form('policy')); +} diff --git a/ui-v2/app/forms/token.js b/ui-v2/app/forms/token.js index 3a0e0bb82e..03f3f1d431 100644 --- a/ui-v2/app/forms/token.js +++ b/ui-v2/app/forms/token.js @@ -1,9 +1,9 @@ -import builderFactory from 'consul-ui/utils/form/builder'; import validations from 'consul-ui/validations/token'; -import policy from 'consul-ui/forms/policy'; +import builderFactory from 'consul-ui/utils/form/builder'; const builder = builderFactory(); -export default function(name = '', v = validations, form = builder) { +export default function(container, name = '', v = validations, form = builder) { return form(name, {}) .setValidators(v) - .add(policy()); + .add(container.form('policy')) + .add(container.form('role')); } diff --git a/ui-v2/app/helpers/policy/is-management.js b/ui-v2/app/helpers/policy/is-management.js deleted file mode 100644 index d35093f227..0000000000 --- a/ui-v2/app/helpers/policy/is-management.js +++ /dev/null @@ -1,8 +0,0 @@ -import { helper } from '@ember/component/helper'; -import { get } from '@ember/object'; -const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001'; -export function isManagement(params, hash) { - return get(params[0], 'ID') === MANAGEMENT_ID; -} - -export default helper(isManagement); diff --git a/ui-v2/app/helpers/policy/typeof.js b/ui-v2/app/helpers/policy/typeof.js new file mode 100644 index 0000000000..ec4b5e441a --- /dev/null +++ b/ui-v2/app/helpers/policy/typeof.js @@ -0,0 +1,18 @@ +import { helper } from '@ember/component/helper'; +import { get } from '@ember/object'; +const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001'; +export function typeOf(params, hash) { + const item = params[0]; + switch (true) { + case get(item, 'ID') === MANAGEMENT_ID: + return 'policy-management'; + case typeof get(item, 'template') === 'undefined': + return 'role'; + case get(item, 'template') !== '': + return 'policy-service-identity'; + default: + return 'policy'; + } +} + +export default helper(typeOf); diff --git a/ui-v2/app/initializers/client.js b/ui-v2/app/initializers/client.js new file mode 100644 index 0000000000..933290b9e9 --- /dev/null +++ b/ui-v2/app/initializers/client.js @@ -0,0 +1,15 @@ +const scripts = document.getElementsByTagName('script'); +const current = scripts[scripts.length - 1]; + +export function initialize(application) { + const Client = application.resolveRegistration('service:client/http'); + Client.reopen({ + isCurrent: function(src) { + return current.src === src; + }, + }); +} + +export default { + initialize, +}; diff --git a/ui-v2/app/initializers/controller-lifecycle.js b/ui-v2/app/initializers/controller-lifecycle.js new file mode 100644 index 0000000000..bdfee585b6 --- /dev/null +++ b/ui-v2/app/initializers/controller-lifecycle.js @@ -0,0 +1,23 @@ +import Route from '@ember/routing/route'; +/** + * This initializer is very similar to: + * https://github.com/kellyselden/ember-controller-lifecycle + * + * Why is this included here: + * 1. Make sure lifecycle functions are functions, not just truthy. + * 2. Right now we don't want a setup function (at least until we are definitely decided that we want one) + * This is possibly a very personal opinion so it makes sense to just include this file here. + */ +Route.reopen({ + resetController(controller, exiting, transition) { + this._super(...arguments); + if (typeof controller.reset === 'function') { + controller.reset(exiting); + } + }, +}); +export function initialize() {} + +export default { + initialize, +}; diff --git a/ui-v2/app/initializers/form.js b/ui-v2/app/initializers/form.js index 099e98036c..3596e2411e 100644 --- a/ui-v2/app/initializers/form.js +++ b/ui-v2/app/initializers/form.js @@ -1,15 +1,40 @@ +import { get, set } from '@ember/object'; + +import kv from 'consul-ui/forms/kv'; +import acl from 'consul-ui/forms/acl'; import token from 'consul-ui/forms/token'; import policy from 'consul-ui/forms/policy'; +import role from 'consul-ui/forms/role'; +import intention from 'consul-ui/forms/intention'; + export function initialize(application) { // Service-less injection using private properties at a per-project level const FormBuilder = application.resolveRegistration('service:form'); const forms = { - token: token(), - policy: policy(), + kv: kv, + acl: acl, + token: token, + policy: policy, + role: role, + intention: intention, }; FormBuilder.reopen({ form: function(name) { - return forms[name]; + let form = get(this.forms, name); + if (!form) { + form = set(this.forms, name, forms[name](this)); + // only do special things for our new things for the moment + if (name === 'role' || name === 'policy') { + const repo = get(this, name); + form.clear(function(obj) { + return repo.create(obj); + }); + form.submit(function(obj) { + return repo.persist(obj); + }); + } + } + return form; }, }); } diff --git a/ui-v2/app/initializers/ivy-codemirror.js b/ui-v2/app/initializers/ivy-codemirror.js index 16f12963ec..c4f3436057 100644 --- a/ui-v2/app/initializers/ivy-codemirror.js +++ b/ui-v2/app/initializers/ivy-codemirror.js @@ -1,44 +1,9 @@ -import { inject as service } from '@ember/service'; -import { get } from '@ember/object'; -import lint from 'consul-ui/utils/editor/lint'; -const MODES = [ - { - name: 'JSON', - mime: 'application/json', - mode: 'javascript', - ext: ['json', 'map'], - alias: ['json5'], - }, - { - name: 'HCL', - mime: 'text/x-ruby', - mode: 'ruby', - ext: ['rb'], - alias: ['jruby', 'macruby', 'rake', 'rb', 'rbx'], - }, - { name: 'YAML', mime: 'text/x-yaml', mode: 'yaml', ext: ['yaml', 'yml'], alias: ['yml'] }, -]; export function initialize(application) { const IvyCodeMirrorComponent = application.resolveRegistration('component:ivy-codemirror'); - const IvyCodeMirrorService = application.resolveRegistration('service:code-mirror'); // Make sure ivy-codemirror respects/maintains a `name=""` attribute IvyCodeMirrorComponent.reopen({ attributeBindings: ['name'], }); - // Add some method to the code-mirror service so I don't have to have 2 services - // for dealing with codemirror - IvyCodeMirrorService.reopen({ - dom: service('dom'), - modes: function() { - return MODES; - }, - lint: function() { - return lint(...arguments); - }, - getEditor: function(element) { - return get(this, 'dom').element('textarea + div', element).CodeMirror; - }, - }); } export default { diff --git a/ui-v2/app/initializers/power-select.js b/ui-v2/app/initializers/power-select.js new file mode 100644 index 0000000000..30ae44dd6c --- /dev/null +++ b/ui-v2/app/initializers/power-select.js @@ -0,0 +1,15 @@ +import { get } from '@ember/object'; +export function initialize(application) { + const PowerSelectComponent = application.resolveRegistration('component:power-select'); + PowerSelectComponent.reopen({ + updateState: function(changes) { + if (!get(this, 'isDestroyed')) { + return this._super(changes); + } + }, + }); +} + +export default { + initialize, +}; diff --git a/ui-v2/app/initializers/search.js b/ui-v2/app/initializers/search.js new file mode 100644 index 0000000000..d5a80cbe32 --- /dev/null +++ b/ui-v2/app/initializers/search.js @@ -0,0 +1,40 @@ +import intention from 'consul-ui/search/filters/intention'; +import token from 'consul-ui/search/filters/token'; +import policy from 'consul-ui/search/filters/policy'; +import role from 'consul-ui/search/filters/role'; +import kv from 'consul-ui/search/filters/kv'; +import acl from 'consul-ui/search/filters/acl'; +import node from 'consul-ui/search/filters/node'; +// service instance +import nodeService from 'consul-ui/search/filters/node/service'; +import serviceNode from 'consul-ui/search/filters/service/node'; +import service from 'consul-ui/search/filters/service'; + +import filterableFactory from 'consul-ui/utils/search/filterable'; +const filterable = filterableFactory(); +export function initialize(application) { + // Service-less injection using private properties at a per-project level + const Builder = application.resolveRegistration('service:search'); + const searchables = { + intention: intention(filterable), + token: token(filterable), + acl: acl(filterable), + policy: policy(filterable), + role: role(filterable), + kv: kv(filterable), + healthyNode: node(filterable), + unhealthyNode: node(filterable), + serviceInstance: serviceNode(filterable), + nodeservice: nodeService(filterable), + service: service(filterable), + }; + Builder.reopen({ + searchable: function(name) { + return searchables[name]; + }, + }); +} + +export default { + initialize, +}; diff --git a/ui-v2/app/instance-initializers/event-source.js b/ui-v2/app/instance-initializers/event-source.js new file mode 100644 index 0000000000..05f661abb5 --- /dev/null +++ b/ui-v2/app/instance-initializers/event-source.js @@ -0,0 +1,102 @@ +import env from 'consul-ui/env'; + +export function initialize(container) { + if (env('CONSUL_UI_DISABLE_REALTIME')) { + return; + } + ['node', 'coordinate', 'session', 'service', 'proxy'] + .map(function(item) { + // create repositories that return a promise resolving to an EventSource + return { + service: `repository/${item}/event-source`, + extend: 'repository/type/event-source', + // Inject our original respository that is used by this class + // within the callable of the EventSource + services: { + content: `repository/${item}`, + }, + }; + }) + .concat( + ['dc', 'policy', 'role'].map(function(item) { + // create repositories that return a promise resolving to an EventSource + return { + service: `repository/${item}/component`, + extend: 'repository/type/component', + // Inject our original respository that is used by this class + // within the callable of the EventSource + services: { + content: `repository/${item}`, + }, + }; + }) + ) + .concat([ + // These are the routes where we overwrite the 'default' + // repo service. Default repos are repos that return a promise resolving to + // an ember-data record or recordset + { + route: 'dc/nodes/index', + services: { + repo: 'repository/node/event-source', + }, + }, + { + route: 'dc/nodes/show', + services: { + repo: 'repository/node/event-source', + coordinateRepo: 'repository/coordinate/event-source', + sessionRepo: 'repository/session/event-source', + }, + }, + { + route: 'dc/services/index', + services: { + repo: 'repository/service/event-source', + }, + }, + { + route: 'dc/services/show', + services: { + repo: 'repository/service/event-source', + }, + }, + { + route: 'dc/services/instance', + services: { + repo: 'repository/service/event-source', + proxyRepo: 'repository/proxy/event-source', + }, + }, + { + service: 'form', + services: { + role: 'repository/role/component', + policy: 'repository/policy/component', + }, + }, + ]) + .forEach(function(definition) { + if (typeof definition.extend !== 'undefined') { + // Create the class instances that we need + container.register( + `service:${definition.service}`, + container.resolveRegistration(`service:${definition.extend}`).extend({}) + ); + } + Object.keys(definition.services).forEach(function(name) { + const servicePath = definition.services[name]; + // inject its dependencies, this could probably detect the type + // but hardcode this for the moment + if (typeof definition.route !== 'undefined') { + container.inject(`route:${definition.route}`, name, `service:${servicePath}`); + } else { + container.inject(`service:${definition.service}`, name, `service:${servicePath}`); + } + }); + }); +} + +export default { + initialize, +}; diff --git a/ui-v2/app/mixins/click-outside.js b/ui-v2/app/mixins/click-outside.js index 92c2d89639..b21ec55071 100644 --- a/ui-v2/app/mixins/click-outside.js +++ b/ui-v2/app/mixins/click-outside.js @@ -1,16 +1,19 @@ import Mixin from '@ember/object/mixin'; - +import { inject as service } from '@ember/service'; import { next } from '@ember/runloop'; import { get } from '@ember/object'; -const isOutside = function(element, e) { + +// TODO: Potentially move this to dom service +const isOutside = function(element, e, doc = document) { if (element) { - const isRemoved = !e.target || !document.contains(e.target); + const isRemoved = !e.target || !doc.contains(e.target); const isInside = element === e.target || element.contains(e.target); return !isRemoved && !isInside; } else { return false; } }; + const handler = function(e) { const el = get(this, 'element'); if (isOutside(el, e)) { @@ -18,6 +21,7 @@ const handler = function(e) { } }; export default Mixin.create({ + dom: service('dom'), init: function() { this._super(...arguments); this.handler = handler.bind(this); @@ -26,12 +30,14 @@ export default Mixin.create({ onblur: function() {}, didInsertElement: function() { this._super(...arguments); + const doc = get(this, 'dom').document(); next(this, () => { - document.addEventListener('click', this.handler); + doc.addEventListener('click', this.handler); }); }, willDestroyElement: function() { this._super(...arguments); - document.removeEventListener('click', this.handler); + const doc = get(this, 'dom').document(); + doc.removeEventListener('click', this.handler); }, }); diff --git a/ui-v2/app/mixins/policy/as-many.js b/ui-v2/app/mixins/policy/as-many.js new file mode 100644 index 0000000000..37d07a494b --- /dev/null +++ b/ui-v2/app/mixins/policy/as-many.js @@ -0,0 +1,70 @@ +import { REQUEST_CREATE, REQUEST_UPDATE } from 'consul-ui/adapters/application'; + +import Mixin from '@ember/object/mixin'; +import { get } from '@ember/object'; + +import minimizeModel from 'consul-ui/utils/minimizeModel'; + +const normalizeServiceIdentities = function(items) { + return (items || []).map(function(item) { + const policy = { + template: 'service-identity', + Name: item.ServiceName, + }; + if (typeof item.Datacenters !== 'undefined') { + policy.Datacenters = item.Datacenters; + } + return policy; + }); +}; +const normalizePolicies = function(items) { + return (items || []).map(function(item) { + return { + template: '', + ...item, + }; + }); +}; +const serializeServiceIdentities = function(items) { + return items + .filter(function(item) { + return get(item, 'template') === 'service-identity'; + }) + .map(function(item) { + const identity = { + ServiceName: get(item, 'Name'), + }; + if (get(item, 'Datacenters')) { + identity.Datacenters = get(item, 'Datacenters'); + } + return identity; + }); +}; +const serializePolicies = function(items) { + return items.filter(function(item) { + return get(item, 'template') === ''; + }); +}; + +export default Mixin.create({ + handleSingleResponse: function(url, response, primary, slug) { + response.Policies = normalizePolicies(response.Policies).concat( + normalizeServiceIdentities(response.ServiceIdentities) + ); + return this._super(url, response, primary, slug); + }, + dataForRequest: function(params) { + const data = this._super(...arguments); + const name = params.type.modelName; + switch (params.requestType) { + case REQUEST_UPDATE: + // falls through + case REQUEST_CREATE: + // ServiceIdentities serialization must happen first, or a copy taken + data[name].ServiceIdentities = serializeServiceIdentities(data[name].Policies); + data[name].Policies = minimizeModel(serializePolicies(data[name].Policies)); + break; + } + return data; + }, +}); diff --git a/ui-v2/app/mixins/role/as-many.js b/ui-v2/app/mixins/role/as-many.js new file mode 100644 index 0000000000..8fce74eb41 --- /dev/null +++ b/ui-v2/app/mixins/role/as-many.js @@ -0,0 +1,28 @@ +import { REQUEST_CREATE, REQUEST_UPDATE } from 'consul-ui/adapters/application'; + +import Mixin from '@ember/object/mixin'; + +import minimizeModel from 'consul-ui/utils/minimizeModel'; + +export default Mixin.create({ + handleSingleResponse: function(url, response, primary, slug) { + ['Roles'].forEach(function(prop) { + if (typeof response[prop] === 'undefined' || response[prop] === null) { + response[prop] = []; + } + }); + return this._super(url, response, primary, slug); + }, + dataForRequest: function(params) { + const name = params.type.modelName; + const data = this._super(...arguments); + switch (params.requestType) { + case REQUEST_UPDATE: + // falls through + case REQUEST_CREATE: + data[name].Roles = minimizeModel(data[name].Roles); + break; + } + return data; + }, +}); diff --git a/ui-v2/app/mixins/role/with-actions.js b/ui-v2/app/mixins/role/with-actions.js new file mode 100644 index 0000000000..fc75a41913 --- /dev/null +++ b/ui-v2/app/mixins/role/with-actions.js @@ -0,0 +1,4 @@ +import Mixin from '@ember/object/mixin'; +import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; + +export default Mixin.create(WithBlockingActions, {}); diff --git a/ui-v2/app/mixins/with-event-source.js b/ui-v2/app/mixins/with-event-source.js new file mode 100644 index 0000000000..f315f32f0b --- /dev/null +++ b/ui-v2/app/mixins/with-event-source.js @@ -0,0 +1,45 @@ +import Mixin from '@ember/object/mixin'; +import { computed as catchable } from 'consul-ui/computed/catchable'; +import purify from 'consul-ui/utils/computed/purify'; + +import WithListeners from 'consul-ui/mixins/with-listeners'; +const PREFIX = '_'; +export default Mixin.create(WithListeners, { + setProperties: function(model) { + const _model = {}; + Object.keys(model).forEach(key => { + // here (see comment below on deleting) + if (typeof this[key] !== 'undefined' && this[key].isDescriptor) { + _model[`${PREFIX}${key}`] = model[key]; + const meta = this.constructor.metaForProperty(key) || {}; + if (typeof meta.catch === 'function') { + if (typeof _model[`${PREFIX}${key}`].addEventListener === 'function') { + this.listen(_model[`_${key}`], 'error', meta.catch.bind(this)); + } + } + } else { + _model[key] = model[key]; + } + }); + return this._super(_model); + }, + reset: function(exiting) { + if (exiting) { + Object.keys(this).forEach(prop => { + if (this[prop] && typeof this[prop].close === 'function') { + this[prop].close(); + // ember doesn't delete on 'resetController' by default + // right now we only call reset when we are exiting, therefore a full + // setProperties will be called the next time we enter the Route so this + // is ok for what we need and means that the above conditional works + // as expected (see 'here' comment above) + delete this[prop]; + } + }); + } + return this._super(...arguments); + }, +}); +export const listen = purify(catchable, function(props) { + return props.map(item => `${PREFIX}${item}`); +}); diff --git a/ui-v2/app/mixins/with-filtering.js b/ui-v2/app/mixins/with-filtering.js index 42dc8c888d..26d08940d9 100644 --- a/ui-v2/app/mixins/with-filtering.js +++ b/ui-v2/app/mixins/with-filtering.js @@ -15,7 +15,7 @@ const toKeyValue = function(el) { }; export default Mixin.create({ filters: {}, - filtered: computed('items', 'filters', function() { + filtered: computed('items.[]', 'filters', function() { const filters = get(this, 'filters'); return get(this, 'items').filter(item => { return this.filter(item, filters); diff --git a/ui-v2/app/mixins/with-health-filtering.js b/ui-v2/app/mixins/with-health-filtering.js index e18bab7d21..ee20846897 100644 --- a/ui-v2/app/mixins/with-health-filtering.js +++ b/ui-v2/app/mixins/with-health-filtering.js @@ -1,25 +1,6 @@ import Mixin from '@ember/object/mixin'; import WithFiltering from 'consul-ui/mixins/with-filtering'; -import { computed, get } from '@ember/object'; -import ucfirst from 'consul-ui/utils/ucfirst'; -const countStatus = function(items, status) { - if (status === '') { - return get(items, 'length'); - } - const key = `Checks${ucfirst(status)}`; - return items.reduce(function(prev, item, i, arr) { - const num = get(item, key); - return ( - prev + - (typeof num !== 'undefined' - ? num - : get(item, 'Checks').filter(function(item) { - return item.Status === status; - }).length) || 0 - ); - }, 0); -}; export default Mixin.create(WithFiltering, { queryParams: { status: { @@ -29,22 +10,4 @@ export default Mixin.create(WithFiltering, { as: 'filter', }, }, - healthFilters: computed('items', function() { - const items = get(this, 'items'); - const objs = ['', 'passing', 'warning', 'critical'].map(function(item) { - const count = countStatus(items, item); - return { - count: count, - label: `${item === '' ? 'All' : ucfirst(item)} (${count.toLocaleString()})`, - value: item, - }; - }); - objs[0].label = `All (${objs - .slice(1) - .reduce(function(prev, item, i, arr) { - return prev + item.count; - }, 0) - .toLocaleString()})`; - return objs; - }), }); diff --git a/ui-v2/app/mixins/with-listeners.js b/ui-v2/app/mixins/with-listeners.js new file mode 100644 index 0000000000..4a3ac20196 --- /dev/null +++ b/ui-v2/app/mixins/with-listeners.js @@ -0,0 +1,36 @@ +import Controller from '@ember/controller'; +import Component from '@ember/component'; +import Mixin from '@ember/object/mixin'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; + +export default Mixin.create({ + dom: service('dom'), + init: function() { + this._super(...arguments); + this._listeners = get(this, 'dom').listeners(); + let teardown = ['willDestroy']; + if (this instanceof Component) { + teardown = ['willDestroyElement']; + } else if (this instanceof Controller) { + if (typeof this.reset === 'function') { + teardown.push('reset'); + } + } + teardown.forEach(method => { + const destroy = this[method]; + this[method] = function() { + if (typeof destroy === 'function') { + destroy.apply(this, arguments); + } + this.removeListeners(); + }; + }); + }, + listen: function(target, event, handler) { + return this._listeners.add(...arguments); + }, + removeListeners: function() { + return this._listeners.remove(...arguments); + }, +}); diff --git a/ui-v2/app/mixins/with-resizing.js b/ui-v2/app/mixins/with-resizing.js index 473306844e..d35aaac8f4 100644 --- a/ui-v2/app/mixins/with-resizing.js +++ b/ui-v2/app/mixins/with-resizing.js @@ -1,11 +1,12 @@ import Mixin from '@ember/object/mixin'; +import { inject as service } from '@ember/service'; import { get } from '@ember/object'; import { assert } from '@ember/debug'; export default Mixin.create({ + dom: service('dom'), resize: function(e) { assert('with-resizing.resize needs to be overridden', false); }, - win: window, init: function() { this._super(...arguments); this.handler = e => { @@ -17,14 +18,18 @@ export default Mixin.create({ }, didInsertElement: function() { this._super(...arguments); - get(this, 'win').addEventListener('resize', this.handler, false); + get(this, 'dom') + .viewport() + .addEventListener('resize', this.handler, false); this.didAppear(); }, didAppear: function() { - this.handler({ target: get(this, 'win') }); + this.handler({ target: get(this, 'dom').viewport() }); }, willDestroyElement: function() { - get(this, 'win').removeEventListener('resize', this.handler, false); + get(this, 'dom') + .viewport() + .removeEventListener('resize', this.handler, false); this._super(...arguments); }, }); diff --git a/ui-v2/app/mixins/with-searching.js b/ui-v2/app/mixins/with-searching.js new file mode 100644 index 0000000000..2a18a75601 --- /dev/null +++ b/ui-v2/app/mixins/with-searching.js @@ -0,0 +1,32 @@ +import Mixin from '@ember/object/mixin'; +import { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; +import WithListeners from 'consul-ui/mixins/with-listeners'; +/** + * WithSearching mostly depends on a `searchParams` object which must be set + * inside the `init` function. The naming and usage of this is modelled on + * `queryParams` but in contrast cannot _yet_ be 'hung' of the Controller + * object, it MUST be set in the `init` method. + * Reasons: As well as producing a eslint error, it can also be 'shared' amongst + * child Classes of the component. It is not clear _yet_ whether mixing this in + * avoids this and is something to be looked at in future to slightly improve DX + * Please also see: + * https://emberjs.com/api/ember/2.12/classes/Ember.Object/properties?anchor=mergedProperties + * + */ +export default Mixin.create(WithListeners, { + builder: service('search'), + init: function() { + this._super(...arguments); + const params = this.searchParams || {}; + this.searchables = {}; + Object.keys(params).forEach(type => { + const key = params[type]; + this.searchables[type] = get(this, 'builder').searchable(type); + this.listen(this.searchables[type], 'change', e => { + const value = e.target.value; + set(this, key, value === '' ? null : value); + }); + }); + }, +}); diff --git a/ui-v2/app/models/node.js b/ui-v2/app/models/node.js index cbe4272a58..be33ca7cf6 100644 --- a/ui-v2/app/models/node.js +++ b/ui-v2/app/models/node.js @@ -21,6 +21,7 @@ export default Model.extend({ Datacenter: attr('string'), Segment: attr(), Coord: attr(), + meta: attr(), hasStatus: function(status) { return hasStatus(get(this, 'Checks'), status); }, diff --git a/ui-v2/app/models/policy.js b/ui-v2/app/models/policy.js index 0bbf498278..d002d57cb8 100644 --- a/ui-v2/app/models/policy.js +++ b/ui-v2/app/models/policy.js @@ -24,6 +24,10 @@ const model = Model.extend({ Datacenters: attr(), CreateIndex: attr('number'), ModifyIndex: attr('number'), + + template: attr('string', { + defaultValue: '', + }), }); export const ATTRS = writable(model, ['Name', 'Description', 'Rules', 'Datacenters']); export default model; diff --git a/ui-v2/app/models/proxy.js b/ui-v2/app/models/proxy.js new file mode 100644 index 0000000000..3e4fb58228 --- /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'), + ServiceProxy: attr(), +}); diff --git a/ui-v2/app/models/role.js b/ui-v2/app/models/role.js new file mode 100644 index 0000000000..a1b1e7d021 --- /dev/null +++ b/ui-v2/app/models/role.js @@ -0,0 +1,34 @@ +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'), + Name: attr('string', { + defaultValue: '', + }), + Description: attr('string', { + defaultValue: '', + }), + Policies: attr({ + defaultValue: function() { + return []; + }, + }), + ServiceIdentities: attr({ + defaultValue: function() { + return []; + }, + }), + // frontend only for ordering where CreateIndex can't be used + CreateTime: attr('date'), + // + Datacenter: attr('string'), + // TODO: Figure out whether we need this or not + Datacenters: attr(), + Hash: attr('string'), + CreateIndex: attr('number'), + ModifyIndex: attr('number'), +}); diff --git a/ui-v2/app/models/service.js b/ui-v2/app/models/service.js index cf98df3114..3d7e425928 100644 --- a/ui-v2/app/models/service.js +++ b/ui-v2/app/models/service.js @@ -30,6 +30,7 @@ export default Model.extend({ Node: attr(), Service: attr(), Checks: attr(), + meta: attr(), passing: computed('ChecksPassing', 'Checks', function() { let num = 0; // TODO: use typeof diff --git a/ui-v2/app/models/token.js b/ui-v2/app/models/token.js index 48e2b469e7..117b6dc796 100644 --- a/ui-v2/app/models/token.js +++ b/ui-v2/app/models/token.js @@ -8,6 +8,7 @@ export const SLUG_KEY = 'AccessorID'; const model = Model.extend({ [PRIMARY_KEY]: attr('string'), [SLUG_KEY]: attr('string'), + IDPName: attr('string'), SecretID: attr('string'), // Legacy Type: attr('string'), @@ -27,7 +28,18 @@ const model = Model.extend({ return []; }, }), + Roles: attr({ + defaultValue: function() { + return []; + }, + }), + ServiceIdentities: attr({ + defaultValue: function() { + return []; + }, + }), CreateTime: attr('date'), + Hash: attr('string'), CreateIndex: attr('number'), ModifyIndex: attr('number'), }); @@ -39,6 +51,7 @@ export const ATTRS = writable(model, [ 'Local', 'Description', 'Policies', + 'Roles', // SecretID isn't writable but we need it to identify an // update via the old API, see TokenAdapter dataForRequest 'SecretID', diff --git a/ui-v2/app/router.js b/ui-v2/app/router.js index 0a7a3bef82..50234f27d7 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: { @@ -71,6 +74,15 @@ export const routes = { _options: { path: '/create' }, }, }, + roles: { + _options: { path: '/roles' }, + edit: { + _options: { path: '/:id' }, + }, + create: { + _options: { path: '/create' }, + }, + }, tokens: { _options: { path: '/tokens' }, edit: { @@ -88,9 +100,9 @@ export const routes = { _options: { path: '/' }, }, // The settings page is global. - // settings: { - // _options: { path: '/setting' }, - // }, + settings: { + _options: { path: '/setting' }, + }, notfound: { _options: { path: '/*path' }, }, diff --git a/ui-v2/app/routes/application.js b/ui-v2/app/routes/application.js index 3b85e9cfba..61f78c2274 100644 --- a/ui-v2/app/routes/application.js +++ b/ui-v2/app/routes/application.js @@ -4,12 +4,14 @@ import { hash } from 'rsvp'; import { get } from '@ember/object'; import { next } from '@ember/runloop'; import { Promise } from 'rsvp'; + import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; -const $html = document.documentElement; -const removeLoading = function() { - return $html.classList.remove('ember-loading'); + +const removeLoading = function($from) { + return $from.classList.remove('ember-loading'); }; export default Route.extend(WithBlockingActions, { + dom: service('dom'), init: function() { this._super(...arguments); }, @@ -17,20 +19,21 @@ export default Route.extend(WithBlockingActions, { settings: service('settings'), actions: { loading: function(transition, originRoute) { + const $root = get(this, 'dom').root(); let dc = null; if (originRoute.routeName !== 'dc') { const model = this.modelFor('dc') || { dcs: null, dc: { Name: null } }; dc = get(this, 'repo').getActive(model.dc.Name, model.dcs); } hash({ - loading: !$html.classList.contains('ember-loading'), + loading: !$root.classList.contains('ember-loading'), dc: dc, }).then(model => { next(() => { const controller = this.controllerFor('application'); controller.setProperties(model); transition.promise.finally(function() { - removeLoading(); + removeLoading($root); controller.setProperties({ loading: false, dc: model.dc, @@ -74,6 +77,7 @@ export default Route.extend(WithBlockingActions, { if (error.status === '') { error.message = 'Error'; } + const $root = get(this, 'dom').root(); hash({ error: error, dc: @@ -85,13 +89,13 @@ export default Route.extend(WithBlockingActions, { dcs: model && model.dcs ? model.dcs : [], }) .then(model => { - removeLoading(); + removeLoading($root); next(() => { this.controllerFor('error').setProperties(model); }); }) .catch(e => { - removeLoading(); + removeLoading($root); next(() => { this.controllerFor('error').setProperties({ error: error }); }); diff --git a/ui-v2/app/routes/dc.js b/ui-v2/app/routes/dc.js index 6b8f466a46..b795f8f773 100644 --- a/ui-v2/app/routes/dc.js +++ b/ui-v2/app/routes/dc.js @@ -19,7 +19,6 @@ export default Route.extend({ }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/acls/create.js b/ui-v2/app/routes/dc/acls/create.js index 315a07fb60..a424260e88 100644 --- a/ui-v2/app/routes/dc/acls/create.js +++ b/ui-v2/app/routes/dc/acls/create.js @@ -22,7 +22,6 @@ export default Route.extend(WithAclActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, deactivate: function() { diff --git a/ui-v2/app/routes/dc/acls/edit.js b/ui-v2/app/routes/dc/acls/edit.js index 565f90bbe7..d016c15bf8 100644 --- a/ui-v2/app/routes/dc/acls/edit.js +++ b/ui-v2/app/routes/dc/acls/edit.js @@ -16,7 +16,6 @@ export default Route.extend(WithAclActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/acls/index.js b/ui-v2/app/routes/dc/acls/index.js index b36a9e3527..b7e521b8e3 100644 --- a/ui-v2/app/routes/dc/acls/index.js +++ b/ui-v2/app/routes/dc/acls/index.js @@ -36,7 +36,6 @@ export default Route.extend(WithAclActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/acls/policies/edit.js b/ui-v2/app/routes/dc/acls/policies/edit.js index 3e09d345e8..4df3c090fa 100644 --- a/ui-v2/app/routes/dc/acls/policies/edit.js +++ b/ui-v2/app/routes/dc/acls/policies/edit.js @@ -8,7 +8,6 @@ import WithPolicyActions from 'consul-ui/mixins/policy/with-actions'; export default SingleRoute.extend(WithPolicyActions, { repo: service('repository/policy'), tokenRepo: service('repository/token'), - datacenterRepo: service('repository/dc'), model: function(params) { const dc = this.modelFor('dc').dc.Name; const tokenRepo = get(this, 'tokenRepo'); @@ -16,7 +15,6 @@ export default SingleRoute.extend(WithPolicyActions, { return hash({ ...model, ...{ - datacenters: get(this, 'datacenterRepo').findAll(), items: tokenRepo.findByPolicy(get(model.item, 'ID'), dc).catch(function(e) { switch (get(e, 'errors.firstObject.status')) { case '403': @@ -31,7 +29,6 @@ export default SingleRoute.extend(WithPolicyActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/acls/policies/index.js b/ui-v2/app/routes/dc/acls/policies/index.js index 6ab9d1a704..094b4a1457 100644 --- a/ui-v2/app/routes/dc/acls/policies/index.js +++ b/ui-v2/app/routes/dc/acls/policies/index.js @@ -23,7 +23,6 @@ export default Route.extend(WithPolicyActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/acls/roles/create.js b/ui-v2/app/routes/dc/acls/roles/create.js new file mode 100644 index 0000000000..1316244636 --- /dev/null +++ b/ui-v2/app/routes/dc/acls/roles/create.js @@ -0,0 +1,6 @@ +import Route from './edit'; +import CreatingRoute from 'consul-ui/mixins/creating-route'; + +export default Route.extend(CreatingRoute, { + templateName: 'dc/acls/roles/edit', +}); diff --git a/ui-v2/app/routes/dc/acls/roles/edit.js b/ui-v2/app/routes/dc/acls/roles/edit.js new file mode 100644 index 0000000000..f6496bf1d0 --- /dev/null +++ b/ui-v2/app/routes/dc/acls/roles/edit.js @@ -0,0 +1,34 @@ +import SingleRoute from 'consul-ui/routing/single'; +import { inject as service } from '@ember/service'; +import { hash } from 'rsvp'; +import { get } from '@ember/object'; + +import WithRoleActions from 'consul-ui/mixins/role/with-actions'; + +export default SingleRoute.extend(WithRoleActions, { + repo: service('repository/role'), + tokenRepo: service('repository/token'), + model: function(params) { + const dc = this.modelFor('dc').dc.Name; + const tokenRepo = get(this, 'tokenRepo'); + return this._super(...arguments).then(model => { + return hash({ + ...model, + ...{ + items: tokenRepo.findByRole(get(model.item, 'ID'), dc).catch(function(e) { + switch (get(e, 'errors.firstObject.status')) { + case '403': + case '401': + // do nothing the SingleRoute will have caught it already + return; + } + throw e; + }), + }, + }); + }); + }, + setupController: function(controller, model) { + controller.setProperties(model); + }, +}); diff --git a/ui-v2/app/routes/dc/acls/roles/index.js b/ui-v2/app/routes/dc/acls/roles/index.js new file mode 100644 index 0000000000..078e5109b5 --- /dev/null +++ b/ui-v2/app/routes/dc/acls/roles/index.js @@ -0,0 +1,28 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { hash } from 'rsvp'; +import { get } from '@ember/object'; + +import WithRoleActions from 'consul-ui/mixins/role/with-actions'; + +export default Route.extend(WithRoleActions, { + repo: service('repository/role'), + queryParams: { + s: { + as: 'filter', + replace: true, + }, + }, + model: function(params) { + const repo = get(this, 'repo'); + return hash({ + ...repo.status({ + items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name), + }), + isLoading: false, + }); + }, + setupController: function(controller, model) { + controller.setProperties(model); + }, +}); diff --git a/ui-v2/app/routes/dc/acls/tokens/edit.js b/ui-v2/app/routes/dc/acls/tokens/edit.js index 8371f86ab5..8109df5f33 100644 --- a/ui-v2/app/routes/dc/acls/tokens/edit.js +++ b/ui-v2/app/routes/dc/acls/tokens/edit.js @@ -1,105 +1,24 @@ import SingleRoute from 'consul-ui/routing/single'; import { inject as service } from '@ember/service'; import { hash } from 'rsvp'; -import { set, get } from '@ember/object'; -import updateArrayObject from 'consul-ui/utils/update-array-object'; +import { get } from '@ember/object'; import WithTokenActions from 'consul-ui/mixins/token/with-actions'; -const ERROR_PARSE_RULES = 'Failed to parse ACL rules'; -const ERROR_NAME_EXISTS = 'Invalid Policy: A Policy with Name'; export default SingleRoute.extend(WithTokenActions, { repo: service('repository/token'), - policyRepo: service('repository/policy'), - datacenterRepo: service('repository/dc'), settings: service('settings'), model: function(params, transition) { - const dc = this.modelFor('dc').dc.Name; - const policyRepo = get(this, 'policyRepo'); return this._super(...arguments).then(model => { return hash({ ...model, ...{ - // TODO: I only need these to create a new policy - datacenters: get(this, 'datacenterRepo').findAll(), - policy: this.getEmptyPolicy(), token: get(this, 'settings').findBySlug('token'), - items: policyRepo.findAllByDatacenter(dc).catch(function(e) { - switch (get(e, 'errors.firstObject.status')) { - case '403': - case '401': - // do nothing the SingleRoute will have caught it already - return; - } - throw e; - }), }, }); }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, - getEmptyPolicy: function() { - const dc = this.modelFor('dc').dc.Name; - return get(this, 'policyRepo').create({ Datacenter: dc }); - }, - actions: { - // TODO: Some of this could potentially be moved to the repo services - loadPolicy: function(item, items) { - const repo = get(this, 'policyRepo'); - const dc = this.modelFor('dc').dc.Name; - const slug = get(item, repo.getSlugKey()); - repo.findBySlug(slug, dc).then(item => { - updateArrayObject(items, item, repo.getSlugKey()); - }); - }, - remove: function(item, items) { - return items.removeObject(item); - }, - clearPolicy: function() { - // TODO: I should be able to reset the ember-data object - // back to it original state? - // possibly Forms could know how to create - const controller = get(this, 'controller'); - controller.setProperties({ - policy: this.getEmptyPolicy(), - }); - }, - createPolicy: function(item, policies, success) { - get(this, 'policyRepo') - .persist(item) - .then(item => { - set(item, 'CreateTime', new Date().getTime()); - policies.pushObject(item); - return item; - }) - .then(function() { - success(); - }) - .catch(err => { - if (typeof err.errors !== 'undefined') { - const error = err.errors[0]; - let prop; - let message = error.detail; - switch (true) { - case message.indexOf(ERROR_PARSE_RULES) === 0: - prop = 'Rules'; - message = error.detail; - break; - case message.indexOf(ERROR_NAME_EXISTS) === 0: - prop = 'Name'; - message = message.substr(ERROR_NAME_EXISTS.indexOf(':') + 1); - break; - } - if (prop) { - item.addError(prop, message); - } - } else { - throw err; - } - }); - }, - }, }); diff --git a/ui-v2/app/routes/dc/acls/tokens/index.js b/ui-v2/app/routes/dc/acls/tokens/index.js index 757871e013..2f3e21afcb 100644 --- a/ui-v2/app/routes/dc/acls/tokens/index.js +++ b/ui-v2/app/routes/dc/acls/tokens/index.js @@ -36,7 +36,6 @@ export default Route.extend(WithTokenActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/intentions/create.js b/ui-v2/app/routes/dc/intentions/create.js index a52a8429d5..cf49eac252 100644 --- a/ui-v2/app/routes/dc/intentions/create.js +++ b/ui-v2/app/routes/dc/intentions/create.js @@ -32,7 +32,6 @@ export default Route.extend(WithIntentionActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, deactivate: function() { diff --git a/ui-v2/app/routes/dc/intentions/edit.js b/ui-v2/app/routes/dc/intentions/edit.js index 16b085dbe9..138ead24e5 100644 --- a/ui-v2/app/routes/dc/intentions/edit.js +++ b/ui-v2/app/routes/dc/intentions/edit.js @@ -26,7 +26,6 @@ export default Route.extend(WithAclActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/intentions/index.js b/ui-v2/app/routes/dc/intentions/index.js index ec41bf54aa..750f11c0cc 100644 --- a/ui-v2/app/routes/dc/intentions/index.js +++ b/ui-v2/app/routes/dc/intentions/index.js @@ -19,7 +19,6 @@ export default Route.extend(WithIntentionActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/kv/create.js b/ui-v2/app/routes/dc/kv/create.js index 44ec169e58..d63ce89338 100644 --- a/ui-v2/app/routes/dc/kv/create.js +++ b/ui-v2/app/routes/dc/kv/create.js @@ -24,7 +24,6 @@ export default Route.extend(WithKvActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, deactivate: function() { diff --git a/ui-v2/app/routes/dc/kv/edit.js b/ui-v2/app/routes/dc/kv/edit.js index 170e512f6a..44c13a5f15 100644 --- a/ui-v2/app/routes/dc/kv/edit.js +++ b/ui-v2/app/routes/dc/kv/edit.js @@ -32,7 +32,6 @@ export default Route.extend(WithKvActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/kv/index.js b/ui-v2/app/routes/dc/kv/index.js index 21ef89dc05..ad2a8a7b73 100644 --- a/ui-v2/app/routes/dc/kv/index.js +++ b/ui-v2/app/routes/dc/kv/index.js @@ -55,7 +55,6 @@ export default Route.extend(WithKvActions, { }, }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/nodes/index.js b/ui-v2/app/routes/dc/nodes/index.js index 5488397458..8cbd56ecdc 100644 --- a/ui-v2/app/routes/dc/nodes/index.js +++ b/ui-v2/app/routes/dc/nodes/index.js @@ -17,7 +17,6 @@ export default Route.extend({ }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/dc/nodes/show.js b/ui-v2/app/routes/dc/nodes/show.js index 9d9f344588..ad7bf3b029 100644 --- a/ui-v2/app/routes/dc/nodes/show.js +++ b/ui-v2/app/routes/dc/nodes/show.js @@ -1,17 +1,14 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { hash } from 'rsvp'; -import { get, set } from '@ember/object'; +import { get } from '@ember/object'; -import distance from 'consul-ui/utils/distance'; -import tomographyFactory from 'consul-ui/utils/tomography'; import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; -const tomography = tomographyFactory(distance); - export default Route.extend(WithBlockingActions, { repo: service('repository/node'), sessionRepo: service('repository/session'), + coordinateRepo: service('repository/coordinate'), queryParams: { s: { as: 'filter', @@ -20,28 +17,14 @@ export default Route.extend(WithBlockingActions, { }, model: function(params) { const dc = this.modelFor('dc').dc.Name; - const repo = get(this, 'repo'); - const sessionRepo = get(this, 'sessionRepo'); + const name = params.name; return hash({ - item: repo.findBySlug(params.name, dc), - }).then(function(model) { - // TODO: Consider loading this after initial page load - const coordinates = get(model.item, 'Coordinates'); - return hash({ - ...model, - ...{ - tomography: - get(coordinates, 'length') > 1 - ? tomography(params.name, coordinates.map(item => get(item, 'data'))) - : null, - items: get(model.item, 'Services'), - sessions: sessionRepo.findByNode(get(model.item, 'Node'), dc), - }, - }); + item: get(this, 'repo').findBySlug(name, dc), + tomography: get(this, 'coordinateRepo').findAllByNode(name, dc), + sessions: get(this, 'sessionRepo').findByNode(name, dc), }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, actions: { @@ -53,7 +36,9 @@ export default Route.extend(WithBlockingActions, { const node = get(item, 'Node'); return repo.remove(item).then(() => { return repo.findByNode(node, dc).then(function(sessions) { - set(controller, 'sessions', sessions); + controller.setProperties({ + sessions: sessions, + }); }); }); }, 'delete'); diff --git a/ui-v2/app/routes/dc/services/index.js b/ui-v2/app/routes/dc/services/index.js index 7b74816dbd..7d479a6adc 100644 --- a/ui-v2/app/routes/dc/services/index.js +++ b/ui-v2/app/routes/dc/services/index.js @@ -10,15 +10,33 @@ export default Route.extend({ as: 'filter', replace: true, }, + // temporary support of old style status + status: { + as: 'status', + }, }, model: function(params) { const repo = get(this, 'repo'); + let terms = params.s || ''; + // we check for the old style `status` variable here + // and convert it to the new style filter=status:critical + let status = params.status; + if (status) { + status = `status:${status}`; + if (terms.indexOf(status) === -1) { + terms = terms + .split('\n') + .concat(status) + .join('\n') + .trim(); + } + } return hash({ + terms: terms !== '' ? terms.split('\n') : [], items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name), }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); 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 0000000000..2fb9eb30fd --- /dev/null +++ b/ui-v2/app/routes/dc/services/instance.js @@ -0,0 +1,31 @@ +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) { + // this will not be run in a blocking loop, but this is ok as + // its highly unlikely that a service will suddenly change to being a + // connect-proxy or vice versa so leave as is for now + return hash({ + proxy: + get(model.item, 'Kind') === 'connect-proxy' + ? null + : proxyRepo.findInstanceBySlug(params.id, params.name, dc), + ...model, + }); + }); + }, + setupController: function(controller, model) { + controller.setProperties(model); + }, +}); diff --git a/ui-v2/app/routes/dc/services/show.js b/ui-v2/app/routes/dc/services/show.js index bf5fa0d657..361b0e15df 100644 --- a/ui-v2/app/routes/dc/services/show.js +++ b/ui-v2/app/routes/dc/services/show.js @@ -5,6 +5,7 @@ import { get } from '@ember/object'; export default Route.extend({ repo: service('repository/service'), + settings: service('settings'), queryParams: { s: { as: 'filter', @@ -13,19 +14,13 @@ export default Route.extend({ }, model: function(params) { const repo = get(this, 'repo'); + const settings = get(this, 'settings'); return hash({ item: repo.findBySlug(params.name, this.modelFor('dc').dc.Name), - }).then(function(model) { - return { - ...model, - ...{ - items: model.item.Nodes, - }, - }; + urls: settings.findBySlug('urls'), }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, }); diff --git a/ui-v2/app/routes/settings.js b/ui-v2/app/routes/settings.js index c5f9701b61..d62cb6b9c8 100644 --- a/ui-v2/app/routes/settings.js +++ b/ui-v2/app/routes/settings.js @@ -3,8 +3,8 @@ import { inject as service } from '@ember/service'; import { hash } from 'rsvp'; import { get } from '@ember/object'; -import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; -export default Route.extend(WithBlockingActions, { +export default Route.extend({ + client: service('client/http'), repo: service('settings'), dcRepo: service('repository/dc'), model: function(params) { @@ -21,11 +21,14 @@ export default Route.extend(WithBlockingActions, { }); }, setupController: function(controller, model) { - this._super(...arguments); controller.setProperties(model); }, - // overwrite afterUpdate and afterDelete hooks - // to avoid the default 'return to listing page' - afterUpdate: function() {}, - afterDelete: function() {}, + actions: { + update: function(item) { + if (!get(item, 'client.blocking')) { + get(this, 'client').abort(); + } + get(this, 'repo').persist(item); + }, + }, }); diff --git a/ui-v2/app/routing/single.js b/ui-v2/app/routing/single.js index c0ff0efc93..6ac63ad791 100644 --- a/ui-v2/app/routing/single.js +++ b/ui-v2/app/routing/single.js @@ -17,6 +17,7 @@ export default Route.extend({ const create = this.isCreate(...arguments); return hash({ isLoading: false, + dc: dc, create: create, ...repo.status({ item: create diff --git a/ui-v2/app/search/filters/acl.js b/ui-v2/app/search/filters/acl.js new file mode 100644 index 0000000000..b68e31ec37 --- /dev/null +++ b/ui-v2/app/search/filters/acl.js @@ -0,0 +1,14 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'Name') + .toLowerCase() + .indexOf(sLower) !== -1 || + get(item, 'ID') + .toLowerCase() + .indexOf(sLower) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/intention.js b/ui-v2/app/search/filters/intention.js new file mode 100644 index 0000000000..cf71a6d86c --- /dev/null +++ b/ui-v2/app/search/filters/intention.js @@ -0,0 +1,15 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const source = get(item, 'SourceName').toLowerCase(); + const destination = get(item, 'DestinationName').toLowerCase(); + const sLower = s.toLowerCase(); + const allLabel = 'All Services (*)'.toLowerCase(); + return ( + source.indexOf(sLower) !== -1 || + destination.indexOf(sLower) !== -1 || + (source === '*' && allLabel.indexOf(sLower) !== -1) || + (destination === '*' && allLabel.indexOf(sLower) !== -1) + ); + }); +} diff --git a/ui-v2/app/search/filters/kv.js b/ui-v2/app/search/filters/kv.js new file mode 100644 index 0000000000..ffef18f3c9 --- /dev/null +++ b/ui-v2/app/search/filters/kv.js @@ -0,0 +1,10 @@ +import { get } from '@ember/object'; +import rightTrim from 'consul-ui/utils/right-trim'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const key = rightTrim(get(item, 'Key'), '/') + .split('/') + .pop(); + return key.toLowerCase().indexOf(s.toLowerCase()) !== -1; + }); +} diff --git a/ui-v2/app/search/filters/node.js b/ui-v2/app/search/filters/node.js new file mode 100644 index 0000000000..6ac9c302f6 --- /dev/null +++ b/ui-v2/app/search/filters/node.js @@ -0,0 +1,11 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'Node') + .toLowerCase() + .indexOf(sLower) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/node/service.js b/ui-v2/app/search/filters/node/service.js new file mode 100644 index 0000000000..255c238eaf --- /dev/null +++ b/ui-v2/app/search/filters/node/service.js @@ -0,0 +1,21 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const term = s.toLowerCase(); + return ( + get(item, 'Service') + .toLowerCase() + .indexOf(term) !== -1 || + get(item, 'ID') + .toLowerCase() + .indexOf(term) !== -1 || + (get(item, 'Tags') || []).some(function(item) { + return item.toLowerCase().indexOf(term) !== -1; + }) || + get(item, 'Port') + .toString() + .toLowerCase() + .indexOf(term) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/policy.js b/ui-v2/app/search/filters/policy.js new file mode 100644 index 0000000000..3144ac6e26 --- /dev/null +++ b/ui-v2/app/search/filters/policy.js @@ -0,0 +1,14 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'Name') + .toLowerCase() + .indexOf(sLower) !== -1 || + get(item, 'Description') + .toLowerCase() + .indexOf(sLower) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/role.js b/ui-v2/app/search/filters/role.js new file mode 100644 index 0000000000..3144ac6e26 --- /dev/null +++ b/ui-v2/app/search/filters/role.js @@ -0,0 +1,14 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'Name') + .toLowerCase() + .indexOf(sLower) !== -1 || + get(item, 'Description') + .toLowerCase() + .indexOf(sLower) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/service.js b/ui-v2/app/search/filters/service.js new file mode 100644 index 0000000000..a5c6547bc7 --- /dev/null +++ b/ui-v2/app/search/filters/service.js @@ -0,0 +1,34 @@ +import { get } from '@ember/object'; +import ucfirst from 'consul-ui/utils/ucfirst'; +const find = function(obj, term) { + if (Array.isArray(obj)) { + return obj.some(function(item) { + return find(item, term); + }); + } + return obj.toLowerCase().indexOf(term) !== -1; +}; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const term = s.toLowerCase(); + let status; + switch (true) { + case term.startsWith('service:'): + return find(get(item, 'Name'), term.substr(8)); + case term.startsWith('tag:'): + return find(get(item, 'Tags') || [], term.substr(4)); + case term.startsWith('status:'): + status = term.substr(7); + switch (term.substr(7)) { + case 'warning': + case 'critical': + case 'passing': + return get(item, `Checks${ucfirst(status)}`) > 0; + default: + return false; + } + default: + return find(get(item, 'Name'), term) || find(get(item, 'Tags') || [], term); + } + }); +} diff --git a/ui-v2/app/search/filters/service/node.js b/ui-v2/app/search/filters/service/node.js new file mode 100644 index 0000000000..5f12724218 --- /dev/null +++ b/ui-v2/app/search/filters/service/node.js @@ -0,0 +1,14 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const term = s.toLowerCase(); + return ( + get(item, 'Node.Node') + .toLowerCase() + .indexOf(term) !== -1 || + get(item, 'Service.ID') + .toLowerCase() + .indexOf(term) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/token.js b/ui-v2/app/search/filters/token.js new file mode 100644 index 0000000000..f8f5f1c3ef --- /dev/null +++ b/ui-v2/app/search/filters/token.js @@ -0,0 +1,21 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'AccessorID') + .toLowerCase() + .indexOf(sLower) !== -1 || + // TODO: Check if Name can go, it was just for legacy + get(item, 'Name') + .toLowerCase() + .indexOf(sLower) !== -1 || + get(item, 'Description') + .toLowerCase() + .indexOf(sLower) !== -1 || + (get(item, 'Policies') || []).some(function(item) { + return item.Name.toLowerCase().indexOf(sLower) !== -1; + }) + ); + }); +} diff --git a/ui-v2/app/serializers/application.js b/ui-v2/app/serializers/application.js index d7468ee8f3..defe01efe8 100644 --- a/ui-v2/app/serializers/application.js +++ b/ui-v2/app/serializers/application.js @@ -1,20 +1,61 @@ import Serializer from 'ember-data/serializers/rest'; +import { get } from '@ember/object'; +import { + HEADERS_SYMBOL as HTTP_HEADERS_SYMBOL, + HEADERS_INDEX as HTTP_HEADERS_INDEX, +} from 'consul-ui/utils/http/consul'; export default Serializer.extend({ // this could get confusing if you tried to override // say `normalizeQueryResponse` // TODO: consider creating a method for each one of the `normalize...Response` family normalizeResponse: function(store, primaryModelClass, payload, id, requestType) { + // Pick the meta/headers back off the payload and cleanup + // before we go through serializing + const headers = payload[HTTP_HEADERS_SYMBOL] || {}; + delete payload[HTTP_HEADERS_SYMBOL]; + const normalizedPayload = this.normalizePayload(payload, id, requestType); + // put the meta onto the response, here this is ok + // as JSON-API allows this and our specific data is now in + // response[primaryModelClass.modelName] + // so we aren't in danger of overwriting anything + // (which was the reason for the Symbol-like property earlier) + // use a method modelled on ember-data methods so we have the opportunity to + // do this on a per-model level + const meta = this.normalizeMeta( + store, + primaryModelClass, + headers, + normalizedPayload, + id, + requestType + ); + if (requestType === 'queryRecord') { + normalizedPayload.meta = meta; + } return this._super( store, primaryModelClass, { - [primaryModelClass.modelName]: this.normalizePayload(payload, id, requestType), + meta: meta, + [primaryModelClass.modelName]: normalizedPayload, }, id, requestType ); }, + normalizeMeta: function(store, primaryModelClass, headers, payload, id, requestType) { + const meta = { + cursor: headers[HTTP_HEADERS_INDEX], + date: headers['date'], + }; + if (requestType === 'query') { + meta.ids = payload.map(item => { + return get(item, this.primaryKey); + }); + } + return meta; + }, normalizePayload: function(payload, id, requestType) { return payload; }, diff --git a/ui-v2/app/serializers/proxy.js b/ui-v2/app/serializers/proxy.js new file mode 100644 index 0000000000..7c3c5c42e0 --- /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/serializers/role.js b/ui-v2/app/serializers/role.js new file mode 100644 index 0000000000..4a43dd24f7 --- /dev/null +++ b/ui-v2/app/serializers/role.js @@ -0,0 +1,5 @@ +import Serializer from './application'; +import { PRIMARY_KEY } from 'consul-ui/models/role'; +export default Serializer.extend({ + primaryKey: PRIMARY_KEY, +}); diff --git a/ui-v2/app/services/client/http.js b/ui-v2/app/services/client/http.js new file mode 100644 index 0000000000..fa452d5009 --- /dev/null +++ b/ui-v2/app/services/client/http.js @@ -0,0 +1,90 @@ +import Service, { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; +import { Promise } from 'rsvp'; + +import getObjectPool from 'consul-ui/utils/get-object-pool'; +import Request from 'consul-ui/utils/http/request'; + +const dispose = function(request) { + if (request.headers()['content-type'] === 'text/event-stream') { + const xhr = request.connection(); + // unsent and opened get aborted + // headers and loading means wait for it + // to finish for the moment + if (xhr.readyState) { + switch (xhr.readyState) { + case 0: + case 1: + xhr.abort(); + break; + } + } + } + return request; +}; +export default Service.extend({ + dom: service('dom'), + init: function() { + this._super(...arguments); + let protocol = 'http/1.1'; + try { + protocol = performance.getEntriesByType('resource').find(item => { + // isCurrent is added in initializers/client and is used + // to ensure we use the consul-ui.js src to sniff what the protocol + // is. Based on the assumption that whereever this script is it's + // likely to be the same as the xmlhttprequests + return item.initiatorType === 'script' && this.isCurrent(item.name); + }).nextHopProtocol; + } catch (e) { + // pass through + } + let maxConnections; + // http/2, http2+QUIC/39 and SPDY don't have connection limits + switch (true) { + case protocol.indexOf('h2') === 0: + case protocol.indexOf('hq') === 0: + case protocol.indexOf('spdy') === 0: + break; + default: + // generally 6 are available + // reserve 1 for traffic that we can't manage + maxConnections = 5; + break; + } + set(this, 'connections', getObjectPool(dispose, maxConnections)); + if (typeof maxConnections !== 'undefined') { + set(this, 'maxConnections', maxConnections); + const doc = get(this, 'dom').document(); + // when the user hides the tab, abort all connections + doc.addEventListener('visibilitychange', e => { + if (e.target.hidden) { + get(this, 'connections').purge(); + } + }); + } + }, + abort: function(id = null) { + get(this, 'connections').purge(); + }, + whenAvailable: function(e) { + const doc = get(this, 'dom').document(); + // if we are using a connection limited protocol and the user has hidden the tab (hidden browser/tab switch) + // any aborted errors should restart + if (typeof get(this, 'maxConnections') !== 'undefined' && doc.hidden) { + return new Promise(function(resolve) { + doc.addEventListener('visibilitychange', function listen(event) { + doc.removeEventListener('visibilitychange', listen); + resolve(e); + }); + }); + } + return Promise.resolve(e); + }, + request: function(options, xhr) { + const request = new Request(options.type, options.url, { body: options.data || {} }, xhr); + return get(this, 'connections').acquire(request, request.getId()); + }, + complete: function() { + return get(this, 'connections').release(...arguments); + }, +}); diff --git a/ui-v2/app/services/code-mirror/linter.js b/ui-v2/app/services/code-mirror/linter.js new file mode 100644 index 0000000000..f607ce3026 --- /dev/null +++ b/ui-v2/app/services/code-mirror/linter.js @@ -0,0 +1,33 @@ +import Service, { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +import lint from 'consul-ui/utils/editor/lint'; +const MODES = [ + { + name: 'JSON', + mime: 'application/json', + mode: 'javascript', + ext: ['json', 'map'], + alias: ['json5'], + }, + { + name: 'HCL', + mime: 'text/x-ruby', + mode: 'ruby', + ext: ['rb'], + alias: ['jruby', 'macruby', 'rake', 'rb', 'rbx'], + }, + { name: 'YAML', mime: 'text/x-yaml', mode: 'yaml', ext: ['yaml', 'yml'], alias: ['yml'] }, +]; + +export default Service.extend({ + dom: service('dom'), + modes: function() { + return MODES; + }, + lint: function() { + return lint(...arguments); + }, + getEditor: function(element) { + return get(this, 'dom').element('textarea + div', element).CodeMirror; + }, +}); diff --git a/ui-v2/app/services/dom-buffer.js b/ui-v2/app/services/dom-buffer.js index 9cb647c4fb..fe58d5fa36 100644 --- a/ui-v2/app/services/dom-buffer.js +++ b/ui-v2/app/services/dom-buffer.js @@ -15,7 +15,9 @@ export default Service.extend(Evented, { return dom; }, remove: function(name) { - buffer[name].remove(); - delete buffer[name]; + if (typeof buffer[name] !== 'undefined') { + buffer[name].remove(); + delete buffer[name]; + } }, }); diff --git a/ui-v2/app/services/dom.js b/ui-v2/app/services/dom.js index 4c59831da5..740406cb59 100644 --- a/ui-v2/app/services/dom.js +++ b/ui-v2/app/services/dom.js @@ -2,24 +2,44 @@ import Service from '@ember/service'; import { getOwner } from '@ember/application'; import { get } from '@ember/object'; +// selecting import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; -// TODO: Move to utils/dom -import getComponentFactory from 'consul-ui/utils/get-component-factory'; +// TODO: sibling and closest seem to have 'PHP-like' guess the order arguments +// ie. one `string, element` and the other has `element, string` +// see if its possible to standardize +import sibling from 'consul-ui/utils/dom/sibling'; +import closest from 'consul-ui/utils/dom/closest'; +import getComponentFactory from 'consul-ui/utils/dom/get-component-factory'; + +// events import normalizeEvent from 'consul-ui/utils/dom/normalize-event'; +import createListeners from 'consul-ui/utils/dom/create-listeners'; +import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor'; // ember-eslint doesn't like you using a single $ so use double // use $_ for components const $$ = qsaFactory(); let $_; +const clickFirstAnchor = clickFirstAnchorFactory(closest); export default Service.extend({ doc: document, + win: window, init: function() { this._super(...arguments); $_ = getComponentFactory(getOwner(this)); }, - normalizeEvent: function() { - return normalizeEvent(...arguments); + document: function() { + return get(this, 'doc'); }, + viewport: function() { + return get(this, 'win'); + }, + // TODO: should this be here? Needs a better name at least + clickFirstAnchor: clickFirstAnchor, + closest: closest, + sibling: sibling, + normalizeEvent: normalizeEvent, + listeners: createListeners, root: function() { return get(this, 'doc').documentElement; }, @@ -30,9 +50,11 @@ export default Service.extend({ }, elementsByTagName: function(name, context) { context = typeof context === 'undefined' ? get(this, 'doc') : context; - return context.getElementByTagName(name); + return context.getElementsByTagName(name); }, elements: function(selector, context) { + // don't ever be tempted to [...$$()] here + // it should return a NodeList return $$(selector, context); }, element: function(selector, context) { @@ -48,7 +70,18 @@ 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) { + return [...this.elements(selector, context)] + .map(function(item) { + return $_(item); + }) + .filter(function(item) { + return item != null; + }); + }, }); diff --git a/ui-v2/app/services/form.js b/ui-v2/app/services/form.js index 933f332d9a..137abe76b5 100644 --- a/ui-v2/app/services/form.js +++ b/ui-v2/app/services/form.js @@ -1,10 +1,21 @@ -import Service from '@ember/service'; +import Service, { inject as service } from '@ember/service'; import builderFactory from 'consul-ui/utils/form/builder'; const builder = builderFactory(); export default Service.extend({ // a `get` method is added via the form initializer // see initializers/form.js + + // TODO: Temporarily add these here until something else needs + // dynamic repos + role: service('repository/role'), + policy: service('repository/policy'), + // + init: function() { + this._super(...arguments); + this.forms = []; + }, build: function(obj, name) { return builder(...arguments); }, + form: function() {}, }); diff --git a/ui-v2/app/services/lazy-proxy.js b/ui-v2/app/services/lazy-proxy.js new file mode 100644 index 0000000000..f9e34f77df --- /dev/null +++ b/ui-v2/app/services/lazy-proxy.js @@ -0,0 +1,32 @@ +import Service from '@ember/service'; +import { get } from '@ember/object'; + +export default Service.extend({ + shouldProxy: function(content, method) { + return false; + }, + init: function() { + this._super(...arguments); + const content = get(this, 'content'); + for (let prop in content) { + if (typeof content[prop] === 'function') { + if (this.shouldProxy(content, prop)) { + this[prop] = function() { + const cb = this.execute(content, prop); + if (typeof cb.then !== 'undefined') { + return cb.then(method => { + return method.apply(this, arguments); + }); + } else { + return cb.apply(this, arguments); + } + }; + } else if (typeof this[prop] !== 'function') { + this[prop] = function() { + return content[prop](...arguments); + }; + } + } + } + }, +}); diff --git a/ui-v2/app/services/repository.js b/ui-v2/app/services/repository.js index f7fe5ac0e1..13eaf7f780 100644 --- a/ui-v2/app/services/repository.js +++ b/ui-v2/app/services/repository.js @@ -14,16 +14,24 @@ export default Service.extend({ }, // store: service('store'), - findAllByDatacenter: function(dc) { - return get(this, 'store').query(this.getModelName(), { + findAllByDatacenter: function(dc, configuration = {}) { + const query = { dc: dc, - }); + }; + if (typeof configuration.cursor !== 'undefined') { + query.index = configuration.cursor; + } + return get(this, 'store').query(this.getModelName(), query); }, - findBySlug: function(slug, dc) { - return get(this, 'store').queryRecord(this.getModelName(), { - id: slug, + findBySlug: function(slug, dc, configuration = {}) { + const query = { dc: dc, - }); + id: slug, + }; + if (typeof configuration.cursor !== 'undefined') { + query.index = configuration.cursor; + } + return get(this, 'store').queryRecord(this.getModelName(), query); }, create: function(obj) { // TODO: This should probably return a Promise diff --git a/ui-v2/app/services/repository/coordinate.js b/ui-v2/app/services/repository/coordinate.js index f6f5c1d8e6..75b3f2f1f7 100644 --- a/ui-v2/app/services/repository/coordinate.js +++ b/ui-v2/app/services/repository/coordinate.js @@ -1,8 +1,23 @@ +import { get } from '@ember/object'; import RepositoryService from 'consul-ui/services/repository'; +import tomographyFactory from 'consul-ui/utils/tomography'; +import distance from 'consul-ui/utils/distance'; +const tomography = tomographyFactory(distance); + const modelName = 'coordinate'; export default RepositoryService.extend({ getModelName: function() { return modelName; }, + findAllByNode: function(node, dc, configuration) { + return this.findAllByDatacenter(dc, configuration).then(function(coordinates) { + let results = {}; + if (get(coordinates, 'length') > 1) { + results = tomography(node, coordinates.map(item => get(item, 'data'))); + } + results.meta = get(coordinates, 'meta'); + return results; + }); + }, }); diff --git a/ui-v2/app/services/repository/kv.js b/ui-v2/app/services/repository/kv.js index 6185c9022e..c3c4b1900d 100644 --- a/ui-v2/app/services/repository/kv.js +++ b/ui-v2/app/services/repository/kv.js @@ -13,7 +13,7 @@ export default RepositoryService.extend({ return PRIMARY_KEY; }, // this one gives you the full object so key,values and meta - findBySlug: function(key, dc) { + findBySlug: function(key, dc, configuration = {}) { if (isFolder(key)) { const id = JSON.stringify([dc, key]); let item = get(this, 'store').peekRecord(this.getModelName(), id); @@ -24,23 +24,31 @@ export default RepositoryService.extend({ } return Promise.resolve(item); } - return get(this, 'store').queryRecord(this.getModelName(), { + const query = { id: key, dc: dc, - }); + }; + if (typeof configuration.cursor !== 'undefined') { + query.index = configuration.cursor; + } + return get(this, 'store').queryRecord(this.getModelName(), query); }, // this one only gives you keys // https://www.consul.io/api/kv.html - findAllBySlug: function(key, dc) { + findAllBySlug: function(key, dc, configuration = {}) { if (key === '/') { key = ''; } + const query = { + id: key, + dc: dc, + separator: '/', + }; + if (typeof configuration.cursor !== 'undefined') { + query.index = configuration.cursor; + } return this.get('store') - .query(this.getModelName(), { - id: key, - dc: dc, - separator: '/', - }) + .query(this.getModelName(), query) .then(function(items) { return items.filter(function(item) { return key !== get(item, 'Key'); diff --git a/ui-v2/app/services/repository/node.js b/ui-v2/app/services/repository/node.js index 0d7eb57344..eec4f211d3 100644 --- a/ui-v2/app/services/repository/node.js +++ b/ui-v2/app/services/repository/node.js @@ -1,20 +1,9 @@ import RepositoryService from 'consul-ui/services/repository'; import { inject as service } from '@ember/service'; -import { get } from '@ember/object'; const modelName = 'node'; export default RepositoryService.extend({ coordinates: service('repository/coordinate'), getModelName: function() { return modelName; }, - findBySlug: function(slug, dc) { - return this._super(...arguments).then(node => { - return get(this, 'coordinates') - .findAllByDatacenter(dc) - .then(function(res) { - node.Coordinates = res; - return node; - }); - }); - }, }); diff --git a/ui-v2/app/services/repository/policy.js b/ui-v2/app/services/repository/policy.js index 76df14ca89..f9a506d023 100644 --- a/ui-v2/app/services/repository/policy.js +++ b/ui-v2/app/services/repository/policy.js @@ -1,14 +1,15 @@ -import Service, { inject as service } from '@ember/service'; +import RepositoryService from 'consul-ui/services/repository'; import { get } from '@ember/object'; -import { typeOf } from '@ember/utils'; -import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy'; import { Promise } from 'rsvp'; import statusFactory from 'consul-ui/utils/acls-status'; import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error'; +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy'; + const isValidServerError = isValidServerErrorFactory(); const status = statusFactory(isValidServerError, Promise); const MODEL_NAME = 'policy'; -export default Service.extend({ + +export default RepositoryService.extend({ getModelName: function() { return MODEL_NAME; }, @@ -18,43 +19,20 @@ export default Service.extend({ getSlugKey: function() { return SLUG_KEY; }, - store: service('store'), status: function(obj) { return status(obj); }, + persist: function(item) { + // only if a policy doesn't have a template, save it + // right now only ServiceIdentities have templates and + // are not saveable themselves (but can be saved to a Role/Token) + switch (get(item, 'template')) { + case '': + return item.save(); + } + return Promise.resolve(item); + }, translate: function(item) { return get(this, 'store').translate('policy', get(item, 'Rules')); }, - findAllByDatacenter: function(dc) { - return get(this, 'store').query('policy', { - dc: dc, - }); - }, - findBySlug: function(slug, dc) { - return get(this, 'store').queryRecord('policy', { - id: slug, - dc: dc, - }); - }, - create: function(obj) { - return get(this, 'store').createRecord('policy', obj); - }, - persist: function(item) { - return item.save(); - }, - remove: function(obj) { - let item = obj; - if (typeof obj.destroyRecord === 'undefined') { - item = obj.get('data'); - } - if (typeOf(item) === 'object') { - item = get(this, 'store').peekRecord('policy', item[PRIMARY_KEY]); - } - return item.destroyRecord().then(item => { - return get(this, 'store').unloadRecord(item); - }); - }, - invalidate: function() { - get(this, 'store').unloadAll('policy'); - }, }); diff --git a/ui-v2/app/services/repository/proxy.js b/ui-v2/app/services/repository/proxy.js new file mode 100644 index 0000000000..6daeee8c54 --- /dev/null +++ b/ui-v2/app/services/repository/proxy.js @@ -0,0 +1,40 @@ +import RepositoryService from 'consul-ui/services/repository'; +import { PRIMARY_KEY } from 'consul-ui/models/proxy'; +import { get, set } 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) { + let res = {}; + if (get(items, 'length') > 0) { + let instance = items.findBy('ServiceProxy.DestinationServiceID', id); + if (instance) { + res = instance; + } else { + instance = items.findBy('ServiceProxy.DestinationServiceName', slug); + if (instance) { + res = instance; + } + } + } + set(res, 'meta', get(items, 'meta')); + return res; + }); + }, +}); diff --git a/ui-v2/app/services/repository/role.js b/ui-v2/app/services/repository/role.js new file mode 100644 index 0000000000..265e83fb38 --- /dev/null +++ b/ui-v2/app/services/repository/role.js @@ -0,0 +1,24 @@ +import RepositoryService from 'consul-ui/services/repository'; +import { Promise } from 'rsvp'; +import statusFactory from 'consul-ui/utils/acls-status'; +import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error'; +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/role'; + +const isValidServerError = isValidServerErrorFactory(); +const status = statusFactory(isValidServerError, Promise); +const MODEL_NAME = 'role'; + +export default RepositoryService.extend({ + getModelName: function() { + return MODEL_NAME; + }, + getPrimaryKey: function() { + return PRIMARY_KEY; + }, + getSlugKey: function() { + return SLUG_KEY; + }, + status: function(obj) { + return status(obj); + }, +}); diff --git a/ui-v2/app/services/repository/service.js b/ui-v2/app/services/repository/service.js index 5654c3a618..472c6ae670 100644 --- a/ui-v2/app/services/repository/service.js +++ b/ui-v2/app/services/repository/service.js @@ -7,16 +7,58 @@ 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'); + if (nodes.length === 0) { + // TODO: Add an store.error("404", "message") or similar + // or move all this to serializer + const e = new Error(); + e.errors = [ + { + status: '404', + title: 'Not found', + }, + ]; + throw e; + } + 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); + set(service, 'meta', get(item, 'meta')); + 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 == ''; + }); + set(service, 'meta', get(item, 'meta')); + return service; + } + // TODO: Add an store.error("404", "message") or similar + // or move all this to serializer + const e = new Error(); + e.errors = [ + { + status: '404', + title: 'Unable to find instance', + }, + ]; + throw e; + }); }, }); diff --git a/ui-v2/app/services/repository/session.js b/ui-v2/app/services/repository/session.js index 9dace862dd..aba309469f 100644 --- a/ui-v2/app/services/repository/session.js +++ b/ui-v2/app/services/repository/session.js @@ -8,11 +8,15 @@ export default RepositoryService.extend({ getModelName: function() { return modelName; }, - findByNode: function(node, dc) { - return get(this, 'store').query(this.getModelName(), { + findByNode: function(node, dc, configuration = {}) { + const query = { id: node, dc: dc, - }); + }; + if (typeof configuration.cursor !== 'undefined') { + query.index = configuration.cursor; + } + return get(this, 'store').query(this.getModelName(), query); }, // TODO: Why Key? Probably should be findBySlug like the others findByKey: function(slug, dc) { diff --git a/ui-v2/app/services/repository/token.js b/ui-v2/app/services/repository/token.js index 4aabe95c97..bf4358d229 100644 --- a/ui-v2/app/services/repository/token.js +++ b/ui-v2/app/services/repository/token.js @@ -1,14 +1,15 @@ -import Service, { inject as service } from '@ember/service'; +import RepositoryService from 'consul-ui/services/repository'; import { get } from '@ember/object'; -import { typeOf } from '@ember/utils'; -import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token'; import { Promise } from 'rsvp'; +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token'; import statusFactory from 'consul-ui/utils/acls-status'; import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error'; + const isValidServerError = isValidServerErrorFactory(); const status = statusFactory(isValidServerError, Promise); const MODEL_NAME = 'token'; -export default Service.extend({ + +export default RepositoryService.extend({ getModelName: function() { return MODEL_NAME; }, @@ -48,39 +49,10 @@ export default Service.extend({ dc: dc, }); }, - // TODO: RepositoryService - store: service('store'), - findAllByDatacenter: function(dc) { + findByRole: function(id, dc) { return get(this, 'store').query(this.getModelName(), { + role: id, dc: dc, }); }, - findBySlug: function(slug, dc) { - return get(this, 'store').queryRecord(this.getModelName(), { - id: slug, - dc: dc, - }); - }, - create: function(obj) { - // TODO: This should probably return a Promise - return get(this, 'store').createRecord(this.getModelName(), obj); - }, - persist: function(item) { - return item.save(); - }, - remove: function(obj) { - let item = obj; - if (typeof obj.destroyRecord === 'undefined') { - item = obj.get('data'); - } - if (typeOf(item) === 'object') { - item = get(this, 'store').peekRecord(this.getModelName(), item[this.getPrimaryKey()]); - } - return item.destroyRecord().then(item => { - return get(this, 'store').unloadRecord(item); - }); - }, - invalidate: function() { - get(this, 'store').unloadAll(this.getModelName()); - }, }); diff --git a/ui-v2/app/services/repository/type/component.js b/ui-v2/app/services/repository/type/component.js new file mode 100644 index 0000000000..5721e49c52 --- /dev/null +++ b/ui-v2/app/services/repository/type/component.js @@ -0,0 +1,16 @@ +import LazyProxyService from 'consul-ui/services/lazy-proxy'; + +import { fromPromise, proxy } from 'consul-ui/utils/dom/event-source'; +export default LazyProxyService.extend({ + shouldProxy: function(content, method) { + return method.indexOf('find') === 0 || method === 'persist'; + }, + execute: function(repo, findOrPersist) { + return function() { + return proxy( + fromPromise(repo[findOrPersist](...arguments)), + findOrPersist.indexOf('All') === -1 ? {} : [] + ); + }; + }, +}); diff --git a/ui-v2/app/services/repository/type/event-source.js b/ui-v2/app/services/repository/type/event-source.js new file mode 100644 index 0000000000..7c4423835e --- /dev/null +++ b/ui-v2/app/services/repository/type/event-source.js @@ -0,0 +1,80 @@ +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; + +import LazyProxyService from 'consul-ui/services/lazy-proxy'; + +import { cache as createCache, BlockingEventSource } from 'consul-ui/utils/dom/event-source'; + +const createProxy = function(repo, find, settings, cache, serialize = JSON.stringify) { + // proxied find*..(id, dc) + const client = get(this, 'client'); + return function() { + const key = `${repo.getModelName()}.${find}.${serialize([...arguments])}`; + const _args = arguments; + const newPromisedEventSource = cache; + return newPromisedEventSource( + function(configuration) { + // take a copy of the original arguments + let args = [..._args]; + if (configuration.settings.enabled) { + // ...and only add our current cursor/configuration if we are blocking + args = args.concat([configuration]); + } + // original find... with configuration now added + return repo[find](...args) + .then(res => { + if (!configuration.settings.enabled) { + // blocking isn't enabled, immediately close + this.close(); + } + return res; + }) + .catch(function(e) { + // setup the aborted connection restarting + // this should happen here to avoid cache deletion + const status = get(e, 'errors.firstObject.status'); + if (status === '0') { + // Any '0' errors (abort) should possibly try again, depending upon the circumstances + // whenAvailable returns a Promise that resolves when the client is available + // again + return client.whenAvailable(e); + } + throw e; + }); + }, + { + key: key, + type: BlockingEventSource, + settings: { + enabled: settings.blocking, + }, + } + ); + }; +}; +let cache = null; +export default LazyProxyService.extend({ + store: service('store'), + settings: service('settings'), + wait: service('timeout'), + client: service('client/http'), + init: function() { + this._super(...arguments); + if (cache === null) { + cache = createCache({}); + } + }, + willDestroy: function() { + cache = null; + }, + shouldProxy: function(content, method) { + return method.indexOf('find') === 0; + }, + execute: function(repo, find) { + return get(this, 'settings') + .findBySlug('client') + .then(settings => { + return createProxy.bind(this)(repo, find, settings, cache); + }); + }, +}); diff --git a/ui-v2/app/services/search.js b/ui-v2/app/services/search.js new file mode 100644 index 0000000000..5962a60852 --- /dev/null +++ b/ui-v2/app/services/search.js @@ -0,0 +1,9 @@ +import Service from '@ember/service'; +export default Service.extend({ + searchable: function() { + return { + addEventListener: function() {}, + removeEventListener: function() {}, + }; + }, +}); diff --git a/ui-v2/app/styles/app.scss b/ui-v2/app/styles/app.scss index ce7dc4ba16..7617caf8a4 100644 --- a/ui-v2/app/styles/app.scss +++ b/ui-v2/app/styles/app.scss @@ -4,7 +4,13 @@ @import 'base/reset/index'; @import 'variables/index'; +/*TODO: Move this to its own local component*/ @import 'ember-power-select'; +#ember-basic-dropdown-wormhole { + z-index: 510; + position: relative; +} +/**/ @import 'components/index'; @import 'core/typography'; diff --git a/ui-v2/app/styles/base/color/base-variables.scss b/ui-v2/app/styles/base/color/base-variables.scss index b31e096c9a..accc1466d4 100644 --- a/ui-v2/app/styles/base/color/base-variables.scss +++ b/ui-v2/app/styles/base/color/base-variables.scss @@ -1,133 +1,133 @@ -$brand-gray-050: #f5f6f7; -$brand-gray-100: #e1e4e7; -$brand-gray-200: #cdd3d7; -$brand-gray-300: #b9c1c7; -$brand-gray-400: #a5b0b7; -$brand-gray-500: #919fa8; -$brand-gray-600: #77838a; -$brand-gray-700: #5d666b; -$brand-gray-800: #42494d; -$brand-gray-900: #282c2e; -$brand-magenta-050: #f9ebf2; -$brand-magenta-100: #efc4d8; -$brand-magenta-200: #e59ebe; -$brand-magenta-300: #da77a4; -$brand-magenta-400: #d0508a; -$brand-magenta-500: #c62a71; -$brand-magenta-600: #9e2159; -$brand-magenta-700: #7d1a47; -$brand-magenta-800: #5a1434; -$brand-magenta-900: #360c1f; -$brand-blue-050: #f0f5ff; -$brand-blue-100: #bfd4ff; -$brand-blue-200: #8ab1ff; -$brand-blue-300: #5b92ff; -$brand-blue-400: #387aff; -$brand-blue-500: #1563ff; -$brand-blue-600: #0f4fd1; -$brand-blue-700: #0e40a3; -$brand-blue-800: #0a2d74; -$brand-blue-900: #061b46; -$brand-purple-050: #eeedfc; -$brand-purple-100: #d5d2f7; -$brand-purple-200: #aea7f2; -$brand-purple-300: #8d83ed; -$brand-purple-400: #7568e8; -$brand-purple-500: #5c4ee5; -$brand-purple-600: #4c40bc; -$brand-purple-700: #3b3292; -$brand-purple-800: #2a2469; -$brand-purple-900: #1a163f; -$brand-teal-050: #ebf8f3; -$brand-teal-100: #c3ecdc; -$brand-teal-200: #9bdfc5; -$brand-teal-300: #74d3ae; -$brand-teal-400: #4cc679; -$brand-teal-500: #25ba81; -$brand-teal-600: #1f996a; -$brand-teal-700: #187753; -$brand-teal-800: #11553b; -$brand-teal-900: #0b3324; -$brand-cyan-050: #e7f8ff; -$brand-cyan-100: #b9ecff; -$brand-cyan-200: #8be0ff; -$brand-cyan-300: #5cd3ff; -$brand-cyan-400: #2ec7ff; -$brand-cyan-500: #00bbff; -$brand-cyan-600: #009fd9; -$brand-cyan-700: #0077a3; -$brand-cyan-800: #005574; -$brand-cyan-900: #003346; -$ui-gray-1: #191a1c; -$ui-gray-2: #323538; -$ui-gray-3: #4c4f54; -$ui-gray-4: #656a70; -$ui-gray-5: #7f858d; -$ui-gray-6: #9a9ea5; -$ui-gray-7: #b4b8bc; -$ui-gray-8: #d0d2d5; -$ui-gray-9: #ebecee; -$ui-gray-10: #f3f4f6; -$ui-gray-050: #f7f8fa; -$ui-gray-100: #ebeef2; -$ui-gray-200: #dce0e6; -$ui-gray-300: #bac1cc; -$ui-gray-400: #8e96a3; -$ui-gray-500: #6f7682; -$ui-gray-600: #626873; -$ui-gray-700: #525761; -$ui-gray-800: #373a42; -$ui-gray-900: #1f2124; -$ui-green-050: #ecf7ed; -$ui-green-100: #c6e9c9; -$ui-green-200: #a0dba5; -$ui-green-300: #7acc81; -$ui-green-400: #54be5d; -$ui-green-500: #2eb039; -$ui-green-600: #26912f; -$ui-green-700: #1e7125; -$ui-green-800: #15501a; -$ui-green-900: #0d3010; -$ui-blue-050: #f0f5ff; -$ui-blue-100: #bfd4ff; -$ui-blue-200: #8ab1ff; -$ui-blue-300: #5b92ff; -$ui-blue-400: #387aff; -$ui-blue-500: #1563ff; -$ui-blue-600: #0f4fd1; -$ui-blue-700: #0e40a3; -$ui-blue-800: #0a2d74; -$ui-blue-900: #061b46; -$ui-red-050: #f9ecee; -$ui-red-100: #efc7cc; -$ui-red-200: #e5a2aa; -$ui-red-300: #db7d88; -$ui-red-400: #d15866; -$ui-red-500: #c73445; -$ui-red-600: #a32b39; -$ui-red-700: #7f222c; -$ui-red-800: #5b1820; -$ui-red-900: #370f13; -$ui-orange-050: #fef4ec; -$ui-orange-100: #fde0c8; -$ui-orange-200: #fccca4; -$ui-orange-300: #fbb77f; -$ui-orange-400: #faa35b; -$ui-orange-500: #fa8f37; -$ui-orange-600: #cd762e; -$ui-orange-700: #a05c23; -$ui-orange-800: #724119; -$ui-orange-900: #45270f; -$ui-yellow-050: #fffbed; -$ui-yellow-100: #fdeeba; -$ui-yellow-200: #fce48c; -$ui-yellow-300: #fbd95e; -$ui-yellow-400: #face30; -$ui-yellow-500: #fac402; -$ui-yellow-600: #cda102; -$ui-yellow-700: #a07d02; -$ui-yellow-800: #725a01; -$ui-yellow-900: #453601; +$steel-050: #f5f6f7; +$steel-100: #e1e4e7; +$steel-200: #cdd3d7; +$steel-300: #b9c1c7; +$steel-400: #a5b0b7; +$steel-500: #919fa8; +$steel-600: #77838a; +$steel-700: #5d666b; +$steel-800: #42494d; +$steel-900: #282c2e; +$magenta-050: #f9ebf2; +$magenta-100: #efc4d8; +$magenta-200: #e59ebe; +$magenta-300: #da77a4; +$magenta-400: #d0508a; +$magenta-500: #c62a71; +$magenta-600: #9e2159; +$magenta-700: #7d1a47; +$magenta-800: #5a1434; +$magenta-900: #360c1f; +$cobalt-050: #f0f5ff; +$cobalt-100: #bfd4ff; +$cobalt-200: #8ab1ff; +$cobalt-300: #5b92ff; +$cobalt-400: #387aff; +$cobalt-500: #1563ff; +$cobalt-600: #0f4fd1; +$cobalt-700: #0e40a3; +$cobalt-800: #0a2d74; +$cobalt-900: #061b46; +$indigo-050: #eeedfc; +$indigo-100: #d5d2f7; +$indigo-200: #aea7f2; +$indigo-300: #8d83ed; +$indigo-400: #7568e8; +$indigo-500: #5c4ee5; +$indigo-600: #4c40bc; +$indigo-700: #3b3292; +$indigo-800: #2a2469; +$indigo-900: #1a163f; +$teal-050: #ebf8f3; +$teal-100: #c3ecdc; +$teal-200: #9bdfc5; +$teal-300: #74d3ae; +$teal-400: #4cc697; +$teal-500: #25ba81; +$teal-600: #1f996a; +$teal-700: #187753; +$teal-800: #11553b; +$teal-900: #0b3324; +$cyan-050: #e7f8ff; +$cyan-100: #b9ecff; +$cyan-200: #8be0ff; +$cyan-300: #5cd3ff; +$cyan-400: #2ec7ff; +$cyan-500: #00bbff; +$cyan-600: #009fd9; +$cyan-700: #0077a3; +$cyan-800: #005574; +$cyan-900: #003346; +$gray-1: #191a1c; +$gray-2: #323538; +$gray-3: #4c4f54; +$gray-4: #656a70; +$gray-5: #7f858d; +$gray-6: #9a9ea5; +$gray-7: #b4b8bc; +$gray-8: #d0d2d5; +$gray-9: #ebecee; +$gray-10: #f3f4f6; +$gray-050: #f7f8fa; +$gray-100: #ebeef2; +$gray-200: #dce0e6; +$gray-300: #bac1cc; +$gray-400: #8e96a3; +$gray-500: #6f7682; +$gray-600: #626873; +$gray-700: #525761; +$gray-800: #373a42; +$gray-900: #1f2124; +$green-050: #ecf7ed; +$green-100: #c6e9c9; +$green-200: #a0dba5; +$green-300: #7acc81; +$green-400: #54be5d; +$green-500: #2eb039; +$green-600: #26912f; +$green-700: #1e7125; +$green-800: #15501a; +$green-900: #0d3010; +$blue-050: #f0f5ff; +$blue-100: #bfd4ff; +$blue-200: #8ab1ff; +$blue-300: #5b92ff; +$blue-400: #387aff; +$blue-500: #1563ff; +$blue-600: #0f4fd1; +$blue-700: #0e40a3; +$blue-800: #0a2d74; +$blue-900: #061b46; +$red-050: #f9ecee; +$red-100: #efc7cc; +$red-200: #e5a2aa; +$red-300: #db7d88; +$red-400: #d15866; +$red-500: #c73445; +$red-600: #a32b39; +$red-700: #7f222c; +$red-800: #5b1820; +$red-900: #370f13; +$orange-050: #fef4ec; +$orange-100: #fde0c8; +$orange-200: #fccca4; +$orange-300: #fbb77f; +$orange-400: #faa35b; +$orange-500: #fa8f37; +$orange-600: #cd762e; +$orange-700: #a05c23; +$orange-800: #724119; +$orange-900: #45270f; +$yellow-050: #fffbed; +$yellow-100: #fdeeba; +$yellow-200: #fce48c; +$yellow-300: #fbd95e; +$yellow-400: #face30; +$yellow-500: #fac402; +$yellow-600: #cda102; +$yellow-700: #a07d02; +$yellow-800: #725a01; +$yellow-900: #453601; $transparent: transparent; -$ui-white: #fff; -$ui-black: #000; +$white: #fff; +$black: #000; diff --git a/ui-v2/app/styles/base/color/frame-placeholders.scss b/ui-v2/app/styles/base/color/frame-placeholders.scss index e4843ed790..fa1aa302a4 100644 --- a/ui-v2/app/styles/base/color/frame-placeholders.scss +++ b/ui-v2/app/styles/base/color/frame-placeholders.scss @@ -5,82 +5,153 @@ %frame-border-000 { /* same as decor-border-000 - but need to think about being able to import color on its own*/ border-style: solid; - // border-width: 0; } -%frame-yellow-500 { + +/* possibly filter bar */ +/* modal close button */ +%frame-gray-050 { @extend %frame-border-000; - background-color: $ui-yellow-050; - border-color: $ui-color-warning; + background-color: $gray-050; + border-color: $gray-300; + color: $gray-400; } -%frame-green-500 { +/* modal main content */ +%frame-gray-100 { @extend %frame-border-000; - background-color: $ui-green-050; - border-color: $ui-green-500; - color: $ui-green-500; + background-color: $white; + border-color: $gray-300; + color: $gray-600; /* wasn't set */ +} +/* hover */ +%frame-gray-200 { + @extend %frame-border-000; + background-color: $transparent; + border-color: $gray-700; + color: $gray-800; +} +%frame-gray-300 { + @extend %frame-border-000; + background-color: $white; + border-color: $gray-700; + color: $gray-800; +} +/* button */ +/**/ +%frame-gray-400 { + @extend %frame-border-000; + background-color: $gray-050; + border-color: $gray-300; + color: $gray-800; } %frame-gray-500 { @extend %frame-border-000; - background-color: $ui-gray-050; - border-color: $ui-gray-300; - color: $ui-gray-400; + background-color: $gray-050; + border-color: $gray-300; + color: $gray-400; +} + +/* tabs */ +%frame-gray-600 { + @extend %frame-border-000; + background-color: $gray-100; + border-color: $gray-500; + color: $gray-500; +} +/* active */ +%frame-gray-700 { + @extend %frame-border-000; + background-color: $gray-200; + border-color: $gray-700; + color: $gray-800; +} +/* modal bars */ +%frame-gray-800 { + @extend %frame-border-000; + background-color: $gray-050; + border-color: $gray-300; + color: $gray-900; +} +%frame-gray-900 { + @extend %frame-border-000; + background-color: $gray-100; + border-color: $gray-300; + color: $gray-900; +} + +%frame-yellow-500 { + @extend %frame-border-000; + background-color: $yellow-050; + border-color: $color-warning; +} +%frame-green-500 { + @extend %frame-border-000; + background-color: $green-050; + border-color: $green-500; + color: $green-500; +} +%frame-blue-500 { + @extend %frame-border-000; + background-color: $blue-050; + border-color: $blue-500; + color: $blue-800; } %frame-blue-600 { @extend %frame-border-000; - background-color: $ui-blue-200; - border-color: $ui-gray-400; - color: $ui-blue-050; + background-color: $blue-200; + border-color: $gray-400; + color: $blue-050; } %frame-blue-700 { @extend %frame-border-000; - background-color: $ui-blue-400; - border-color: $ui-blue-800; - color: $ui-white; + background-color: $blue-400; + border-color: $blue-800; + color: $white; } %frame-blue-800 { @extend %frame-border-000; - background-color: $ui-blue-500; - border-color: $ui-blue-800; - color: $ui-white; + background-color: $blue-500; + border-color: $blue-800; + color: $white; } %frame-blue-900 { @extend %frame-border-000; - background-color: $ui-blue-700; - border-color: $ui-blue-800; - color: $ui-white; + background-color: $blue-700; + border-color: $blue-800; + color: $white; } %frame-red-300 { @extend %frame-border-000; - background-color: $ui-white; - border-color: $ui-red-500; - color: $ui-red-500; + background-color: $white; + border-color: $red-500; + color: $red-500; } %frame-red-500 { @extend %frame-border-000; - background-color: $ui-red-050; - border-color: $ui-red-500; - color: $ui-red-800; + background-color: $red-050; + border-color: $red-500; + color: $red-800; } %frame-red-600 { @extend %frame-border-000; - background-color: $ui-red-200; - border-color: $ui-gray-400; - color: $ui-white; + background-color: $red-200; + border-color: $gray-400; + color: $white; } %frame-red-700 { @extend %frame-border-000; - background-color: $ui-red-500; - border-color: $ui-red-800; - color: $ui-white; + background-color: $red-500; + border-color: $red-800; + color: $white; } %frame-red-900 { @extend %frame-border-000; - background-color: $ui-red-700; - border-color: $ui-red-800; - color: $ui-white; + background-color: $red-700; + border-color: $red-800; + color: $white; } %frame-magenta-300 { @extend %frame-border-000; - background-color: $ui-white; - border-color: $brand-magenta-600; - color: $brand-magenta-600; + background-color: $white; + border-color: $magenta-600; + color: $magenta-600; } diff --git a/ui-v2/app/styles/base/color/semantic-variables.scss b/ui-v2/app/styles/base/color/semantic-variables.scss index 32cb0fa149..14d545e43c 100644 --- a/ui-v2/app/styles/base/color/semantic-variables.scss +++ b/ui-v2/app/styles/base/color/semantic-variables.scss @@ -1,12 +1,12 @@ -$ui-color-primary: $ui-blue-500; -$ui-color-dangerous: $ui-red-500; -$ui-color-primary-disabled: $ui-blue-500; -$ui-color-transparent: transparent; -$ui-color-neutral: $ui-gray-500; -$ui-color-action: $ui-blue-500; -$ui-color-info: $ui-blue-500; -$ui-color-success: $ui-green-500; -$ui-color-failure: $ui-red-500; -$ui-color-danger: $ui-red-500; -$ui-color-warning: $ui-yellow-500; -$ui-color-alert: $ui-orange-500; +$color-primary: $blue-500; +$color-dangerous: $red-500; +$color-primary-disabled: $blue-500; +$color-transparent: transparent; +$color-neutral: $gray-500; +$color-action: $blue-500; +$color-info: $blue-500; +$color-success: $green-500; +$color-failure: $red-500; +$color-danger: $red-500; +$color-warning: $yellow-500; +$color-alert: $orange-500; diff --git a/ui-v2/app/styles/base/decoration/base-placeholders.scss b/ui-v2/app/styles/base/decoration/base-placeholders.scss index 42a6acd54c..69bc7f7f71 100644 --- a/ui-v2/app/styles/base/decoration/base-placeholders.scss +++ b/ui-v2/app/styles/base/decoration/base-placeholders.scss @@ -1,3 +1,14 @@ +%visually-hidden { + position: absolute !important; + height: 1px; + width: 1px; + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); +} +%visually-hidden-text { + text-indent: -9000px; + font-size: 0; +} %decor-border-000 { border-style: solid; border-width: 0; diff --git a/ui-v2/app/styles/base/icons/base-placeholders.scss b/ui-v2/app/styles/base/icons/base-placeholders.scss new file mode 100644 index 0000000000..6e49492576 --- /dev/null +++ b/ui-v2/app/styles/base/icons/base-placeholders.scss @@ -0,0 +1,13 @@ +%with-icon { + background-repeat: no-repeat; + background-position: center; +} +%as-pseudo { + display: inline-block; + content: ''; + visibility: visible; + background-size: contain; + width: 1.2em; + height: 1.2em; + vertical-align: text-top; +} \ No newline at end of file diff --git a/ui-v2/app/styles/base/icons/base-variables.scss b/ui-v2/app/styles/base/icons/base-variables.scss new file mode 100644 index 0000000000..1be5ffae1b --- /dev/null +++ b/ui-v2/app/styles/base/icons/base-variables.scss @@ -0,0 +1,99 @@ +$alert-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$alert-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$alert-triangle-svg: url('data:image/svg+xml;charset=UTF-8,'); +$arrow-down-svg: url('data:image/svg+xml;charset=UTF-8,'); +$arrow-left-svg: url('data:image/svg+xml;charset=UTF-8,'); +$arrow-right-svg: url('data:image/svg+xml;charset=UTF-8,'); +$arrow-up-svg: url('data:image/svg+xml;charset=UTF-8,'); +$calendar-svg: url('data:image/svg+xml;charset=UTF-8,'); +$cancel-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$cancel-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$cancel-plain-svg: url('data:image/svg+xml;charset=UTF-8,'); +$cancel-square-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$cancel-square-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$caret-down-svg: url('data:image/svg+xml;charset=UTF-8,'); +$caret-up-svg: url('data:image/svg+xml;charset=UTF-8,'); +$check-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$check-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$check-plain-svg: url('data:image/svg+xml;charset=UTF-8,'); +$chevron-down-2-svg: url('data:image/svg+xml;charset=UTF-8,'); +$chevron-down-svg: url('data:image/svg+xml;charset=UTF-8,'); +$chevron-left-svg: url('data:image/svg+xml;charset=UTF-8,'); +$chevron-right-svg: url('data:image/svg+xml;charset=UTF-8,'); +$chevron-up-svg: url('data:image/svg+xml;charset=UTF-8,'); +$chevron-svg: url('data:image/svg+xml;charset=UTF-8,'); +$clock-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$clock-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$code-svg: url('data:image/svg+xml;charset=UTF-8,'); +$consul-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,'); +$copy-action-svg: url('data:image/svg+xml;charset=UTF-8,'); +$copy-success-svg: url('data:image/svg+xml;charset=UTF-8,'); +$disabled-svg: url('data:image/svg+xml;charset=UTF-8,'); +$download-svg: url('data:image/svg+xml;charset=UTF-8,'); +$edit-svg: url('data:image/svg+xml;charset=UTF-8,'); +$exit-svg: url('data:image/svg+xml;charset=UTF-8,'); +$expand-less-svg: url('data:image/svg+xml;charset=UTF-8,'); +$expand-more-svg: url('data:image/svg+xml;charset=UTF-8,'); +$eye-svg: url('data:image/svg+xml;charset=UTF-8,'); +$file-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$file-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$filter-svg: url('data:image/svg+xml;charset=UTF-8,'); +$flag-svg: url('data:image/svg+xml;charset=UTF-8,'); +$folder-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$folder-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$git-branch-svg: url('data:image/svg+xml;charset=UTF-8,'); +$git-commit-svg: url('data:image/svg+xml;charset=UTF-8,'); +$git-pull-request-svg: url('data:image/svg+xml;charset=UTF-8,'); +$hashicorp-logo-svg: url('data:image/svg+xml;charset=UTF-8,'); +$help-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$help-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$history-svg: url('data:image/svg+xml;charset=UTF-8,'); +$info-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$info-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$kubernetes-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,'); +$link-svg: url('data:image/svg+xml;charset=UTF-8,'); +$loading-svg: url('data:image/svg+xml;charset=UTF-8,'); +$lock-closed-svg: url('data:image/svg+xml;charset=UTF-8,'); +$lock-disabled-svg: url('data:image/svg+xml;charset=UTF-8,'); +$lock-open-svg: url('data:image/svg+xml;charset=UTF-8,'); +$menu-svg: url('data:image/svg+xml;charset=UTF-8,'); +$minus-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$minus-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$minus-plain-svg: url('data:image/svg+xml;charset=UTF-8,'); +$minus-square-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$minus-svg: url('data:image/svg+xml;charset=UTF-8,'); +$more-horizontal-svg: url('data:image/svg+xml;charset=UTF-8,'); +$more-vertical-svg: url('data:image/svg+xml;charset=UTF-8,'); +$nomad-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,'); +$plus-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$plus-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$plus-plain-svg: url('data:image/svg+xml;charset=UTF-8,'); +$plus-square-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$queue-svg: url('data:image/svg+xml;charset=UTF-8,'); +$refresh-svg: url('data:image/svg+xml;charset=UTF-8,'); +$run-svg: url('data:image/svg+xml;charset=UTF-8,'); +$search-svg: url('data:image/svg+xml;charset=UTF-8,'); +$service-identity-svg: url('data:image/svg+xml;charset=UTF-8,'); +$settings-svg: url('data:image/svg+xml;charset=UTF-8,'); +$star-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$star-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$star-svg: url('data:image/svg+xml;charset=UTF-8,'); +$sub-arrow-left-svg: url('data:image/svg+xml;charset=UTF-8,'); +$sub-arrow-right-svg: url('data:image/svg+xml;charset=UTF-8,'); +$swap-horizontal-svg: url('data:image/svg+xml;charset=UTF-8,'); +$swap-vertical-svg: url('data:image/svg+xml;charset=UTF-8,'); +$terraform-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,'); +$tier-enterprise-svg: url('data:image/svg+xml;charset=UTF-8,'); +$tier-oss-svg: url('data:image/svg+xml;charset=UTF-8,'); +$trash-svg: url('data:image/svg+xml;charset=UTF-8,'); +$tune-svg: url('data:image/svg+xml;charset=UTF-8,'); +$unfold-less-svg: url('data:image/svg+xml;charset=UTF-8,'); +$unfold-more-svg: url('data:image/svg+xml;charset=UTF-8,'); +$upload-svg: url('data:image/svg+xml;charset=UTF-8,'); +$user-organization-svg: url('data:image/svg+xml;charset=UTF-8,'); +$user-plain-svg: url('data:image/svg+xml;charset=UTF-8,'); +$user-square-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); +$user-square-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$user-team-svg: url('data:image/svg+xml;charset=UTF-8,'); +$visibility-hide-svg: url('data:image/svg+xml;charset=UTF-8,'); +$visibility-show-svg: url('data:image/svg+xml;charset=UTF-8,'); diff --git a/ui-v2/app/styles/base/icons/icon-placeholders.scss b/ui-v2/app/styles/base/icons/icon-placeholders.scss new file mode 100644 index 0000000000..f6d41b9308 --- /dev/null +++ b/ui-v2/app/styles/base/icons/icon-placeholders.scss @@ -0,0 +1,494 @@ +%with-alert-circle-fill-icon { + @extend %with-icon; + background-image: $alert-circle-fill-svg; +} + +%with-alert-circle-outline-icon { + @extend %with-icon; + background-image: $alert-circle-outline-svg; +} + +%with-alert-triangle-icon { + @extend %with-icon; + background-image: $alert-triangle-svg; +} + +%with-arrow-down-icon { + @extend %with-icon; + background-image: $arrow-down-svg; +} + +%with-arrow-left-icon { + @extend %with-icon; + background-image: $arrow-left-svg; +} + +%with-arrow-right-icon { + @extend %with-icon; + background-image: $arrow-right-svg; +} + +%with-arrow-up-icon { + @extend %with-icon; + background-image: $arrow-up-svg; +} + +%with-calendar-icon { + @extend %with-icon; + background-image: $calendar-svg; +} + +%with-cancel-circle-fill-icon { + @extend %with-icon; + background-image: $cancel-circle-fill-svg; +} + +%with-cancel-circle-outline-icon { + @extend %with-icon; + background-image: $cancel-circle-outline-svg; +} + +%with-cancel-plain-icon { + @extend %with-icon; + background-image: $cancel-plain-svg; +} + +%with-cancel-square-fill-icon { + @extend %with-icon; + background-image: $cancel-square-fill-svg; +} + +%with-cancel-square-outline-icon { + @extend %with-icon; + background-image: $cancel-square-outline-svg; +} + +%with-caret-down-icon { + @extend %with-icon; + background-image: $caret-down-svg; +} + +%with-caret-up-icon { + @extend %with-icon; + background-image: $caret-up-svg; +} + +%with-check-circle-fill-icon { + @extend %with-icon; + background-image: $check-circle-fill-svg; +} + +%with-check-circle-outline-icon { + @extend %with-icon; + background-image: $check-circle-outline-svg; +} + +%with-check-plain-icon { + @extend %with-icon; + background-image: $check-plain-svg; +} + +%with-chevron-down-2-icon { + @extend %with-icon; + background-image: $chevron-down-2-svg; +} + +%with-chevron-down-icon { + @extend %with-icon; + background-image: $chevron-down-svg; +} + +%with-chevron-left-icon { + @extend %with-icon; + background-image: $chevron-left-svg; +} + +%with-chevron-right-icon { + @extend %with-icon; + background-image: $chevron-right-svg; +} + +%with-chevron-up-icon { + @extend %with-icon; + background-image: $chevron-up-svg; +} + +%with-chevron-icon { + @extend %with-icon; + background-image: $chevron-svg; +} + +%with-clock-fill-icon { + @extend %with-icon; + background-image: $clock-fill-svg; +} + +%with-clock-outline-icon { + @extend %with-icon; + background-image: $clock-outline-svg; +} + +%with-code-icon { + @extend %with-icon; + background-image: $code-svg; +} + +%with-consul-logo-color-icon { + @extend %with-icon; + background-image: $consul-logo-color-svg; +} + +%with-copy-action-icon { + @extend %with-icon; + background-image: $copy-action-svg; +} + +%with-copy-success-icon { + @extend %with-icon; + background-image: $copy-success-svg; +} + +%with-disabled-icon { + @extend %with-icon; + background-image: $disabled-svg; +} + +%with-download-icon { + @extend %with-icon; + background-image: $download-svg; +} + +%with-edit-icon { + @extend %with-icon; + background-image: $edit-svg; +} + +%with-exit-icon { + @extend %with-icon; + background-image: $exit-svg; +} + +%with-expand-less-icon { + @extend %with-icon; + background-image: $expand-less-svg; +} + +%with-expand-more-icon { + @extend %with-icon; + background-image: $expand-more-svg; +} + +%with-eye-icon { + @extend %with-icon; + background-image: $eye-svg; +} + +%with-file-fill-icon { + @extend %with-icon; + background-image: $file-fill-svg; +} + +%with-file-outline-icon { + @extend %with-icon; + background-image: $file-outline-svg; +} + +%with-filter-icon { + @extend %with-icon; + background-image: $filter-svg; +} + +%with-flag-icon { + @extend %with-icon; + background-image: $flag-svg; +} + +%with-folder-fill-icon { + @extend %with-icon; + background-image: $folder-fill-svg; +} + +%with-folder-outline-icon { + @extend %with-icon; + background-image: $folder-outline-svg; +} + +%with-git-branch-icon { + @extend %with-icon; + background-image: $git-branch-svg; +} + +%with-git-commit-icon { + @extend %with-icon; + background-image: $git-commit-svg; +} + +%with-git-pull-request-icon { + @extend %with-icon; + background-image: $git-pull-request-svg; +} + +%with-hashicorp-logo-icon { + @extend %with-icon; + background-image: $hashicorp-logo-svg; +} + +%with-help-circle-fill-icon { + @extend %with-icon; + background-image: $help-circle-fill-svg; +} + +%with-help-circle-outline-icon { + @extend %with-icon; + background-image: $help-circle-outline-svg; +} + +%with-history-icon { + @extend %with-icon; + background-image: $history-svg; +} + +%with-info-circle-fill-icon { + @extend %with-icon; + background-image: $info-circle-fill-svg; +} + +%with-info-circle-outline-icon { + @extend %with-icon; + background-image: $info-circle-outline-svg; +} + +%with-kubernetes-logo-color-icon { + @extend %with-icon; + background-image: $kubernetes-logo-color-svg; +} + +%with-link-icon { + @extend %with-icon; + background-image: $link-svg; +} + +%with-loading-icon { + @extend %with-icon; + background-image: $loading-svg; +} + +%with-lock-closed-icon { + @extend %with-icon; + background-image: $lock-closed-svg; +} + +%with-lock-disabled-icon { + @extend %with-icon; + background-image: $lock-disabled-svg; +} + +%with-lock-open-icon { + @extend %with-icon; + background-image: $lock-open-svg; +} + +%with-menu-icon { + @extend %with-icon; + background-image: $menu-svg; +} + +%with-minus-circle-fill-icon { + @extend %with-icon; + background-image: $minus-circle-fill-svg; +} + +%with-minus-circle-outline-icon { + @extend %with-icon; + background-image: $minus-circle-outline-svg; +} + +%with-minus-plain-icon { + @extend %with-icon; + background-image: $minus-plain-svg; +} + +%with-minus-square-fill-icon { + @extend %with-icon; + background-image: $minus-square-fill-svg; +} + +%with-minus-icon { + @extend %with-icon; + background-image: $minus-svg; +} + +%with-more-horizontal-icon { + @extend %with-icon; + background-image: $more-horizontal-svg; +} + +%with-more-vertical-icon { + @extend %with-icon; + background-image: $more-vertical-svg; +} + +%with-nomad-logo-color-icon { + @extend %with-icon; + background-image: $nomad-logo-color-svg; +} + +%with-plus-circle-fill-icon { + @extend %with-icon; + background-image: $plus-circle-fill-svg; +} + +%with-plus-circle-outline-icon { + @extend %with-icon; + background-image: $plus-circle-outline-svg; +} + +%with-plus-plain-icon { + @extend %with-icon; + background-image: $plus-plain-svg; +} + +%with-plus-square-fill-icon { + @extend %with-icon; + background-image: $plus-square-fill-svg; +} + +%with-queue-icon { + @extend %with-icon; + background-image: $queue-svg; +} + +%with-refresh-icon { + @extend %with-icon; + background-image: $refresh-svg; +} + +%with-run-icon { + @extend %with-icon; + background-image: $run-svg; +} + +%with-search-icon { + @extend %with-icon; + background-image: $search-svg; +} + +%with-service-identity-icon { + @extend %with-icon; + background-image: $service-identity-svg; +} + +%with-settings-icon { + @extend %with-icon; + background-image: $settings-svg; +} + +%with-star-fill-icon { + @extend %with-icon; + background-image: $star-fill-svg; +} + +%with-star-outline-icon { + @extend %with-icon; + background-image: $star-outline-svg; +} + +%with-star-icon { + @extend %with-icon; + background-image: $star-svg; +} + +%with-sub-arrow-left-icon { + @extend %with-icon; + background-image: $sub-arrow-left-svg; +} + +%with-sub-arrow-right-icon { + @extend %with-icon; + background-image: $sub-arrow-right-svg; +} + +%with-swap-horizontal-icon { + @extend %with-icon; + background-image: $swap-horizontal-svg; +} + +%with-swap-vertical-icon { + @extend %with-icon; + background-image: $swap-vertical-svg; +} + +%with-terraform-logo-color-icon { + @extend %with-icon; + background-image: $terraform-logo-color-svg; +} + +%with-tier-enterprise-icon { + @extend %with-icon; + background-image: $tier-enterprise-svg; +} + +%with-tier-oss-icon { + @extend %with-icon; + background-image: $tier-oss-svg; +} + +%with-trash-icon { + @extend %with-icon; + background-image: $trash-svg; +} + +%with-tune-icon { + @extend %with-icon; + background-image: $tune-svg; +} + +%with-unfold-less-icon { + @extend %with-icon; + background-image: $unfold-less-svg; +} + +%with-unfold-more-icon { + @extend %with-icon; + background-image: $unfold-more-svg; +} + +%with-upload-icon { + @extend %with-icon; + background-image: $upload-svg; +} + +%with-user-organization-icon { + @extend %with-icon; + background-image: $user-organization-svg; +} + +%with-user-plain-icon { + @extend %with-icon; + background-image: $user-plain-svg; +} + +%with-user-square-fill-icon { + @extend %with-icon; + background-image: $user-square-fill-svg; +} + +%with-user-square-outline-icon { + @extend %with-icon; + background-image: $user-square-outline-svg; +} + +%with-user-team-icon { + @extend %with-icon; + background-image: $user-team-svg; +} + +%with-visibility-hide-icon { + @extend %with-icon; + background-image: $visibility-hide-svg; +} + +%with-visibility-show-icon { + @extend %with-icon; + background-image: $visibility-show-svg; +} diff --git a/ui-v2/app/styles/base/icons/index.scss b/ui-v2/app/styles/base/icons/index.scss index 9d228363d3..17b18ac3c6 100644 --- a/ui-v2/app/styles/base/icons/index.scss +++ b/ui-v2/app/styles/base/icons/index.scss @@ -1,14 +1,3 @@ -$star-svg: url('data:image/svg+xml;charset=UTF-8,'); -$eye-svg: url('data:image/svg+xml;charset=UTF-8,'); - -$chevron-svg: url('data:image/svg+xml;charset=UTF-8,'); - -$cancel-plain-svg: url('data:image/svg+xml;charset=UTF-8,'); - -$loading-svg: url('data:image/svg+xml;charset=UTF-8,'); - -$hashicorp-svg: url('data:image/svg+xml;charset=UTF-8,'); - -$consul-color-svg: url('data:image/svg+xml;charset=UTF-8,'); -$nomad-color-svg: url('data:image/svg+xml;charset=UTF-8,'); -$terraform-color-svg: url('data:image/svg+xml;charset=UTF-8,'); +@import './base-variables'; +@import './base-placeholders'; +@import './icon-placeholders'; diff --git a/ui-v2/app/styles/base/reset/system.scss b/ui-v2/app/styles/base/reset/system.scss index 27d5d3797b..8f42d0169e 100644 --- a/ui-v2/app/styles/base/reset/system.scss +++ b/ui-v2/app/styles/base/reset/system.scss @@ -8,16 +8,16 @@ strong { color: inherit; } body { - color: $ui-gray-900; + color: $gray-900; } a { - color: $ui-color-action; + color: $color-action; } html { - background-color: $ui-white; + background-color: $white; } hr { - background-color: $ui-gray-500; + background-color: $gray-500; } html { font-size: $typo-size-000; diff --git a/ui-v2/app/styles/components/action-group/layout.scss b/ui-v2/app/styles/components/action-group/layout.scss index 06a47fb2ab..b3a6c49654 100644 --- a/ui-v2/app/styles/components/action-group/layout.scss +++ b/ui-v2/app/styles/components/action-group/layout.scss @@ -1,21 +1,20 @@ %action-group label span { display: none; } -%action-group::before { - margin-left: -1px; +%action-group-action { + width: 170px; + padding: 10px 10px; + text-align: left; } -%action-group label::after { - margin-left: 5px; -} -%action-group label::before { - margin-left: -7px; +%action-group li > * { + @extend %action-group-action; } %action-group { width: 30px; height: 30px; position: absolute; top: 8px; - right: 15px; + right: 3px; } %action-group label { display: block; @@ -30,12 +29,12 @@ /* this is actually the group */ %action-group ul { position: absolute; - right: -10px; + right: 0px; padding: 1px; } %action-group ul::before { position: absolute; - right: 18px; + right: 9px; content: ''; display: block; width: 10px; @@ -59,10 +58,6 @@ position: relative; z-index: 1; } -%action-group li a { - width: 170px; - padding: 10px 10px; -} %action-group input[type='radio'], %action-group input[type='radio'] ~ ul, %action-group input[type='radio'] ~ .with-confirmation > ul { diff --git a/ui-v2/app/styles/components/action-group/skin.scss b/ui-v2/app/styles/components/action-group/skin.scss index ac46082e59..344fa9319c 100644 --- a/ui-v2/app/styles/components/action-group/skin.scss +++ b/ui-v2/app/styles/components/action-group/skin.scss @@ -2,15 +2,16 @@ @extend %toggle-button; } %action-group input[type='radio']:checked + label:first-of-type { - background-color: $ui-gray-050; + /* frame-gray */ + background-color: $gray-050; } -%action-group label { +%action-group label, +%action-group-action { cursor: pointer; } -%action-group label::after, -%action-group label::before, -%action-group::before { - @extend %with-dot; +%action-group label:first-of-type::after { + @extend %with-more-horizontal-icon, %as-pseudo; + opacity: 0.7; } %action-group ul { border: $decor-border-100; @@ -23,13 +24,15 @@ } %action-group ul, %action-group ul::before { - border-color: $ui-color-action; + border-color: $color-action; } -%action-group li a:hover { - background-color: $ui-color-action; - color: $ui-white; +%action-group-action { + background-color: $white; +} +%action-group-action:hover { + @extend %frame-blue-800; } %action-group ul, %action-group ul::before { - background-color: $ui-white; + background-color: $white; } diff --git a/ui-v2/app/styles/components/anchors.scss b/ui-v2/app/styles/components/anchors.scss index 3d26d0764b..ad56af1bfc 100644 --- a/ui-v2/app/styles/components/anchors.scss +++ b/ui-v2/app/styles/components/anchors.scss @@ -1,12 +1,21 @@ @import './anchors/index'; %main-content a { - color: $ui-gray-900; + color: $gray-900; } -%main-content a[rel*='help'] { - @extend %with-info; +a[rel*='external'] { + @extend %with-exit; } %main-content label a[rel*='help'] { - color: $ui-gray-400; + color: $gray-400; +} +%main-content a[rel*='help']::after { + @extend %with-info-circle-outline-icon, %as-pseudo; + opacity: 0.4; +} +%main-content h2 a[rel*='help']::after { + font-size: 0.65em; + margin-top: 0.2em; + margin-left: 0.2em; } [role='tabpanel'] > p:only-child [rel*='help']::after { diff --git a/ui-v2/app/styles/components/anchors/index.scss b/ui-v2/app/styles/components/anchors/index.scss index 093b8f8936..9cd1c30b9d 100644 --- a/ui-v2/app/styles/components/anchors/index.scss +++ b/ui-v2/app/styles/components/anchors/index.scss @@ -6,5 +6,5 @@ %anchor:hover, %anchor:focus, %anchor:active { - color: $ui-color-action; + color: $color-action; } diff --git a/ui-v2/app/styles/components/app-view.scss b/ui-v2/app/styles/components/app-view.scss index 8b4c559755..7cd6562ee1 100644 --- a/ui-v2/app/styles/components/app-view.scss +++ b/ui-v2/app/styles/components/app-view.scss @@ -1,6 +1,7 @@ @import './app-view/index'; @import './filter-bar/index'; @import './buttons/index'; +@import './type-icon/index'; main { @extend %app-view; } @@ -12,11 +13,21 @@ main { } @media #{$--lt-spacious-page-header} { %app-view header .actions { - margin-top: 5px; + margin-top: 9px; } } -%app-view h1 span { +// TODO: This should be its own component +%app-view h1 span[data-tooltip] { @extend %with-external-source-icon; + // TODO: Look to remove this, right now its needed but this + // should automaticaly vertically center + margin-top: 20px; +} +%app-view h1 span.kind-proxy { + @extend %type-icon, %with-proxy; +} +%app-view h1 em { + color: $gray-600; } %app-view header .actions a, %app-view header .actions button { diff --git a/ui-v2/app/styles/components/app-view/layout.scss b/ui-v2/app/styles/components/app-view/layout.scss index 34827cc5bf..0eae559a23 100644 --- a/ui-v2/app/styles/components/app-view/layout.scss +++ b/ui-v2/app/styles/components/app-view/layout.scss @@ -9,6 +9,16 @@ float: right; display: flex; align-items: flex-start; + margin-top: 9px; +} +%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 { @@ -19,7 +29,7 @@ } %app-view h2 { padding-bottom: 0.2em; - margin-bottom: 1.1em; + margin-bottom: 0.2em; } %app-view header .actions > *:not(:last-child) { margin-right: 12px; @@ -50,3 +60,8 @@ min-height: 1em; margin-bottom: 0.4em; } +// TODO: Think about an %app-form or similar +%app-content fieldset:not(.freetext-filter) { + padding-bottom: 0.3em; + margin-bottom: 2em; +} diff --git a/ui-v2/app/styles/components/app-view/skin.scss b/ui-v2/app/styles/components/app-view/skin.scss index ab4fd115d4..431c380d43 100644 --- a/ui-v2/app/styles/components/app-view/skin.scss +++ b/ui-v2/app/styles/components/app-view/skin.scss @@ -1,21 +1,40 @@ %app-view h2, -%app-view header > div:last-of-type { - border-bottom: $decor-border-100; +%app-view fieldset { + border-bottom: $decor-border-200; } -%app-view header > div:last-of-type, -%app-view h2 { - border-color: $keyline-light; +%app-view fieldset h2 { + border-bottom: none; +} +@media #{$--horizontal-selects} { + %app-view header h1 { + border-bottom: $decor-border-200; + } +} +@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, +%app-view fieldset { + 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: $ui-gray-400; + color: $gray-400; } [role='tabpanel'] > p:only-child, -.template-error > div { - background-color: $ui-gray-050; - color: $ui-gray-400; -} +.template-error > div, %app-content > p:only-child, %app-view > div.disabled > div { - background-color: $ui-gray-050; - color: $ui-gray-400; + @extend %frame-gray-500; } diff --git a/ui-v2/app/styles/components/breadcrumbs/skin.scss b/ui-v2/app/styles/components/breadcrumbs/skin.scss index 86f88cf495..fb86355e92 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: $ui-color-action; + color: $color-action; +} +%breadcrumbs strong { + color: $gray-400; } diff --git a/ui-v2/app/styles/components/buttons.scss b/ui-v2/app/styles/components/buttons.scss index 977dc0897e..9047a583db 100644 --- a/ui-v2/app/styles/components/buttons.scss +++ b/ui-v2/app/styles/components/buttons.scss @@ -3,12 +3,13 @@ button[type='submit'], a.type-create { @extend %primary-button; } +// the :not(li)'s here avoid styling action-group buttons button[type='reset'], -button[type='button']:not(.copy-btn):not(.type-delete), +:not(li) > button[type='button']:not(.copy-btn):not(.type-delete), html.template-error div > a { @extend %secondary-button; } -button.type-delete { +:not(li) > button.type-delete { @extend %dangerous-button; } button.copy-btn { diff --git a/ui-v2/app/styles/components/buttons/skin.scss b/ui-v2/app/styles/components/buttons/skin.scss index af1af1b0b1..0ba5ebcd34 100644 --- a/ui-v2/app/styles/components/buttons/skin.scss +++ b/ui-v2/app/styles/components/buttons/skin.scss @@ -21,22 +21,23 @@ @extend %button; border-width: 1px; border-radius: $radius-small; - box-shadow: 0 3px 1px 0 rgba($ui-black, 0.12); + box-shadow: 0 3px 1px 0 rgba($black, 0.12); } /* color */ %copy-button { - color: $ui-color-action; - background-color: $ui-color-transparent; + color: $color-action; + background-color: $color-transparent; } %copy-button:hover:not(:disabled):not(:active), %copy-button:focus { - color: $ui-color-action; - background-color: $ui-gray-050; + /*frame-grey frame-blue*/ + color: $color-action; + background-color: $gray-050; } %copy-button:disabled { } %copy-button:active { - background-color: $ui-gray-200; + background-color: $gray-200; } %primary-button { @extend %frame-blue-800; @@ -52,28 +53,22 @@ @extend %frame-blue-900; } /**/ -%secondary-button { - /* %frame-gray-something */ - color: $ui-gray-800; - background-color: $ui-gray-050; - border-color: $ui-gray-300; - border-style: solid; -} %secondary-button:hover:not(:disabled):not(:active), %secondary-button:focus { - /* %frame-gray-something */ - background-color: $ui-white; - color: $ui-gray-800; - border-color: $ui-gray-700; + @extend %frame-gray-300; +} +%secondary-button { + @extend %frame-gray-400; } %secondary-button:disabled { - color: $ui-gray-600; + color: $gray-600; } %secondary-button:active { /* %frame-gray-something */ - background-color: $ui-gray-200; - color: $ui-gray-800; - border-color: $ui-gray-700; + @extend %frame-gray-700; + background-color: $gray-200; + color: $gray-800; + border-color: $gray-700; } /**/ %dangerous-button { diff --git a/ui-v2/app/styles/components/code-editor/layout.scss b/ui-v2/app/styles/components/code-editor/layout.scss index 1410a65344..a4f1865e59 100644 --- a/ui-v2/app/styles/components/code-editor/layout.scss +++ b/ui-v2/app/styles/components/code-editor/layout.scss @@ -15,8 +15,8 @@ %code-editor-syntax-select { margin-top: 1px; border: 0; - background-color: $ui-black; - color: $ui-white; + background-color: $black; + color: $white; border-left: 1px solid; border-radius: 0; } @@ -25,7 +25,10 @@ bottom: 0px; width: 100%; height: 25px; - background-color: black; + background-color: $black; content: ''; display: block; } +%code-editor > pre { + display: none; +} diff --git a/ui-v2/app/styles/components/code-editor/skin.scss b/ui-v2/app/styles/components/code-editor/skin.scss index e5d1c6395a..5053054fa6 100644 --- a/ui-v2/app/styles/components/code-editor/skin.scss +++ b/ui-v2/app/styles/components/code-editor/skin.scss @@ -12,13 +12,13 @@ $syntax-serf: #dd4e58; $syntax-packer: #1ddba3; // Our colors -$syntax-gray: lighten($ui-black, 89%); +$syntax-gray: lighten($black, 89%); $syntax-red: #ff3d3d; $syntax-green: #39b54a; $syntax-dark-gray: #535f73; $syntax-gutter-grey: #2a2f36; -$syntax-yellow: $ui-yellow-500; +$syntax-yellow: $yellow-500; .CodeMirror { max-width: 1150px; min-height: 300px; @@ -33,7 +33,7 @@ $syntax-yellow: $ui-yellow-500; background-color: #f9f9fa; border: 1px solid $syntax-light-gray; border-radius: 0; - color: lighten($ui-black, 13%); + color: lighten($black, 13%); font-family: $typo-family-mono; font-size: 13px; padding: 7px 8px 9px; @@ -42,7 +42,7 @@ $syntax-yellow: $ui-yellow-500; .cm-s-hashi { &.CodeMirror { width: 100%; - background-color: $ui-black !important; + background-color: $black !important; color: #cfd2d1 !important; border: none; font-family: $typo-family-mono; @@ -150,7 +150,7 @@ $syntax-yellow: $ui-yellow-500; .CodeMirror-matchingbracket { text-decoration: underline; - color: $ui-white !important; + color: $white !important; } } @@ -174,7 +174,7 @@ $syntax-yellow: $ui-yellow-500; } span.cm-property { - color: $ui-white; + color: $white; } span.cm-variable-2 { diff --git a/ui-v2/app/styles/components/confirmation-dialog/skin.scss b/ui-v2/app/styles/components/confirmation-dialog/skin.scss index 8df087ff55..aa50e08d36 100644 --- a/ui-v2/app/styles/components/confirmation-dialog/skin.scss +++ b/ui-v2/app/styles/components/confirmation-dialog/skin.scss @@ -1,6 +1,6 @@ table div.with-confirmation.confirming { - background-color: $ui-white; + background-color: $white; } %confirmation-dialog-inline p { - color: $ui-gray-400; + color: $gray-400; } diff --git a/ui-v2/app/styles/components/expanded-single-select/skin.scss b/ui-v2/app/styles/components/expanded-single-select/skin.scss index a03183b045..d67d29c25f 100644 --- a/ui-v2/app/styles/components/expanded-single-select/skin.scss +++ b/ui-v2/app/styles/components/expanded-single-select/skin.scss @@ -4,10 +4,10 @@ %expanded-single-select input[type='radio']:checked + *, %expanded-single-select input[type='radio']:hover + *, %expanded-single-select input[type='radio']:focus + * { - box-shadow: 0 4px 8px 0 rgba($ui-black, 0.05); + box-shadow: 0 4px 8px 0 rgba($black, 0.05); } %expanded-single-select input[type='radio']:checked + *, %expanded-single-select input[type='radio']:hover + *, %expanded-single-select input[type='radio']:focus + * { - background-color: $ui-white; + background-color: $white; } diff --git a/ui-v2/app/styles/components/feedback-dialog.scss b/ui-v2/app/styles/components/feedback-dialog.scss index 7fbeff2d72..61c93da807 100644 --- a/ui-v2/app/styles/components/feedback-dialog.scss +++ b/ui-v2/app/styles/components/feedback-dialog.scss @@ -14,7 +14,7 @@ main .with-feedback { .actions .with-feedback p::after { bottom: auto; top: -13px !important; - border-bottom: 18px solid $ui-gray-800; + border-bottom: 18px solid $gray-800; border-top: 0; } } diff --git a/ui-v2/app/styles/components/filter-bar/skin.scss b/ui-v2/app/styles/components/filter-bar/skin.scss index 518b63ec94..e981f3d2ba 100644 --- a/ui-v2/app/styles/components/filter-bar/skin.scss +++ b/ui-v2/app/styles/components/filter-bar/skin.scss @@ -20,19 +20,20 @@ } %filter-bar, %filter-bar > * { - background-color: $ui-gray-050; + /* frame-gray */ + background-color: $gray-050; } %filter-bar > *, %filter-bar label:not(:last-child) { - border-color: $ui-gray-300 !important; + border-color: $gray-300 !important; } %filter-bar [role='radiogroup'] label { - color: $ui-gray-400; + color: $gray-400; } %filter-bar .value-warning span::after { - border-color: $ui-gray-050; + border-color: $gray-050; } %filter-bar .value-warning input:checked + span::after { - border-color: $ui-white; + border-color: $white; } diff --git a/ui-v2/app/styles/components/flash-message/skin.scss b/ui-v2/app/styles/components/flash-message/skin.scss index 0c4f82f238..b06698fed8 100644 --- a/ui-v2/app/styles/components/flash-message/skin.scss +++ b/ui-v2/app/styles/components/flash-message/skin.scss @@ -5,12 +5,18 @@ %flash-message p.success strong { @extend %with-passing; } +%flash-message p.warning strong { + @extend %with-warning; +} %flash-message p.error strong { @extend %with-critical; } %flash-message p.success { @extend %frame-green-500; } +%flash-message p.warning { + @extend %frame-yellow-500; +} %flash-message p.error { @extend %frame-red-500; } diff --git a/ui-v2/app/styles/components/form-elements.scss b/ui-v2/app/styles/components/form-elements.scss index 942efbb8cb..7717f9baa8 100644 --- a/ui-v2/app/styles/components/form-elements.scss +++ b/ui-v2/app/styles/components/form-elements.scss @@ -24,7 +24,8 @@ form table, %app-content form dl { @extend %form-row; } -%app-content [role='radiogroup'] { +%app-content form:not(.filter-bar) [role='radiogroup'], +%modal-window [role='radiogroup'] { @extend %radio-group; } %radio-group label { @@ -33,6 +34,9 @@ form table, .checkbox-group { @extend %checkbox-group; } +fieldset > p { + color: $gray-400; +} %toggle + .checkbox-group { margin-top: -1em; } diff --git a/ui-v2/app/styles/components/form-elements/skin.scss b/ui-v2/app/styles/components/form-elements/skin.scss index 1b76786336..ce13bff2c6 100644 --- a/ui-v2/app/styles/components/form-elements/skin.scss +++ b/ui-v2/app/styles/components/form-elements/skin.scss @@ -23,39 +23,39 @@ border-radius: $decor-radius-100; } %form-element-error > input { - border-color: $ui-color-failure !important; + border-color: $color-failure !important; } %form-element > strong { - color: $ui-color-failure; + color: $color-failure; } %form-element > em { - color: $ui-gray-400; + color: $gray-400; } %form-element > em > code { - background-color: $ui-gray-200; - color: $brand-magenta-600; + background-color: $gray-200; + color: $magenta-600; border-radius: $decor-radius-100; } %form-element > span { - color: $ui-black; + color: $black; } %form-element [type='text'], %form-element [type='password'], %form-element textarea { - color: $ui-gray-500; + color: $gray-500; } %form-element [type='text'], %form-element [type='password'], %form-element textarea { - border-color: $ui-gray-300; + border-color: $gray-300; } %form-element [type='text']:hover, %form-element [type='password']:hover, %form-element textarea:hover { - border-color: $ui-gray-500; + border-color: $gray-500; } %form-element [type='text']:focus, %form-element [type='password']:focus, %form-element textarea:focus { - border-color: $ui-blue-500; + border-color: $blue-500; } diff --git a/ui-v2/app/styles/components/freetext-filter/skin.scss b/ui-v2/app/styles/components/freetext-filter/skin.scss index 9c1dfbc788..09ddc99862 100644 --- a/ui-v2/app/styles/components/freetext-filter/skin.scss +++ b/ui-v2/app/styles/components/freetext-filter/skin.scss @@ -7,15 +7,15 @@ border-right: $decor-border-100; } %freetext-filter { - background-color: $ui-white; + background-color: $white; } %freetext-filter span { - color: $ui-gray-300; + color: $gray-300; } %freetext-filter input { - border-color: $ui-gray-300; + border-color: $gray-300; } %freetext-filter input, %freetext-filter input::placeholder { - color: $ui-gray-400; + color: $gray-400; } 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 0000000000..ff44885a30 --- /dev/null +++ b/ui-v2/app/styles/components/healthcheck-info.scss @@ -0,0 +1,12 @@ +@import './healthcheck-info/index'; +@import './icons/index'; +tr .healthcheck-info { + @extend %healthcheck-info; +} +td span.zero { + @extend %with-no-healthchecks; + // TODO: Why isn't this in 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 0000000000..06b64a930a --- /dev/null +++ b/ui-v2/app/styles/components/healthcheck-info/layout.scss @@ -0,0 +1,27 @@ +%healthcheck-info { + display: inline-flex; +} +%healthcheck-info > * { + display: block; +} +%healthcheck-info dt { + text-indent: -9000px; +} +%healthcheck-info dt.zero { + display: none; +} +%healthcheck-info dd.zero { + visibility: hidden; +} +%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 0000000000..9b22b05f01 --- /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 550b2c992a..96216d6315 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 0000000000..bc18252196 --- /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 ef40daf375..5f1a9403de 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-output/skin.scss b/ui-v2/app/styles/components/healthcheck-output/skin.scss new file mode 100644 index 0000000000..9d26d4d663 --- /dev/null +++ b/ui-v2/app/styles/components/healthcheck-output/skin.scss @@ -0,0 +1,35 @@ +%healthcheck-output { + border-width: 1px; +} +%healthcheck-output, +%healthcheck-output pre { + border-radius: $decor-radius-100; +} +%healthcheck-output dd:first-of-type { + color: $gray-400; +} +%healthcheck-output pre { + background-color: $black; + color: $white; +} +%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-output.warning { + @extend %frame-yellow-500; + color: $gray-900; +} +%healthcheck-output.critical { + @extend %frame-red-500; + color: $gray-900; +} +%healthcheck-output.passing::before { + background-color: $color-success !important; +} +%healthcheck-output.critical::before { + background-color: $color-danger !important; +} diff --git a/ui-v2/app/styles/components/healthcheck-status/skin.scss b/ui-v2/app/styles/components/healthcheck-status/skin.scss deleted file mode 100644 index 8656904fe2..0000000000 --- a/ui-v2/app/styles/components/healthcheck-status/skin.scss +++ /dev/null @@ -1,35 +0,0 @@ -%healthcheck-status { - border-width: 1px; -} -%healthcheck-status, -%healthcheck-status pre { - border-radius: $decor-radius-100; -} -%healthcheck-status dd:first-of-type { - color: $ui-gray-400; -} -%healthcheck-status pre { - background-color: $ui-black; - color: $ui-white; -} -%healthcheck-status.passing { - /* TODO: this should be a gray frame */ - // @extend %frame-green-500; - color: $ui-gray-900; - border-color: $ui-gray-200; - border-style: solid; -} -%healthcheck-status.warning { - @extend %frame-yellow-500; - color: $ui-gray-900; -} -%healthcheck-status.critical { - @extend %frame-red-500; - color: $ui-gray-900; -} -%healthcheck-status.passing::before { - background-color: $ui-color-success !important; -} -%healthcheck-status.critical::before { - background-color: $ui-color-danger !important; -} diff --git a/ui-v2/app/styles/components/healthchecked-resource/skin.scss b/ui-v2/app/styles/components/healthchecked-resource/skin.scss index 0938e5f26f..12b5ecc86e 100644 --- a/ui-v2/app/styles/components/healthchecked-resource/skin.scss +++ b/ui-v2/app/styles/components/healthchecked-resource/skin.scss @@ -1,26 +1,26 @@ %healthchecked-resource { border: $decor-border-100; - box-shadow: 0 4px 8px 0 rgba($ui-black, 0.05); + box-shadow: 0 4px 8px 0 rgba($black, 0.05); } %healthchecked-resource li { border-top: $decor-border-100; } %healthchecked-resource, %healthchecked-resource li { - border-color: $ui-gray-200; + border-color: $gray-200; } %healthchecked-resource li.passing { - color: $ui-color-success; + color: $color-success; } %healthchecked-resource li.warning { - color: $ui-color-alert; + color: $color-alert; } %healthchecked-resource li.critical { - color: $ui-color-failure; + color: $color-failure; } %healthchecked-resource:hover, %healthchecked-resource:focus { - box-shadow: 0 8px 10px 0 rgba($ui-black, 0.1); + box-shadow: 0 8px 10px 0 rgba($black, 0.1); } %healthchecked-resource { border-radius: $radius-small; @@ -29,5 +29,5 @@ @extend %with-no-healthchecks; } %healthchecked-resource ul:empty::before { - color: $ui-gray-400; + color: $gray-400; } diff --git a/ui-v2/app/styles/components/icons/index.scss b/ui-v2/app/styles/components/icons/index.scss index b17f57590a..7ff829e43a 100644 --- a/ui-v2/app/styles/components/icons/index.scss +++ b/ui-v2/app/styles/components/icons/index.scss @@ -54,6 +54,7 @@ %with-folder { text-indent: 30px; } +%with-proxy, %with-hashicorp, %with-folder, %with-chevron, @@ -66,18 +67,27 @@ display: inline-block; } %with-hashicorp { - background-color: $ui-white; + background-color: $white; } %with-hashicorp::before { @extend %pseudo-icon; opacity: 0.45; - background-image: $hashicorp-svg; + background-image: $hashicorp-logo-svg; background-size: cover; width: 20px; height: 20px; left: -25px; margin-top: -10px; - background-color: $ui-color-transparent; + background-color: $color-transparent; +} +%with-proxy::before { + @extend %pseudo-icon; + background-image: url('data:image/svg+xml;charset=UTF-8,'); + width: 18px; + height: 18px; + left: 3px; + margin-top: -9px; + background-color: $color-transparent; } %with-clipboard { padding-left: 38px !important; @@ -89,16 +99,15 @@ height: 17px; left: 12px; margin-top: -8px; - background-color: $ui-color-transparent; + background-color: $color-transparent; } %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: $ui-color-transparent; + font-size: 0.7rem; } %with-folder::before { @extend %pseudo-icon; @@ -108,7 +117,7 @@ margin-top: -6px; left: 2px; background-image: url('data:image/svg+xml;charset=UTF-8,'); - background-color: $ui-color-transparent; + background-color: $color-transparent; } %with-magnifier { position: relative; @@ -126,7 +135,7 @@ border: 0.05em solid; border-radius: 0.35em; border-color: currentColor; - background-color: $ui-color-transparent; + background-color: $color-transparent; } %with-magnifier::after { @extend %pseudo-icon; @@ -140,15 +149,12 @@ height: 0.05em; transform: rotate(45deg); } -%with-info { - position: relative; - padding-right: 23px; -} -%with-info::after { - @extend %pseudo-icon; - right: 0; - background-image: url('data:image/svg+xml;charset=UTF-8,'); - background-color: $ui-color-transparent; +%with-exit::after { + @extend %pseudo-icon-bg-img; + top: 3px; + right: -8px; + background-image: $exit-svg; + background-color: $color-transparent; width: 16px; height: 16px; } @@ -188,7 +194,7 @@ } %with-inverted-tick { @extend %pseudo-icon; - background-color: $ui-color-transparent; + background-color: $color-transparent; background-image: url('data:image/svg+xml;charset=UTF-8,'); height: 20px !important; width: 16px !important; @@ -214,7 +220,7 @@ background-image: url('data:image/svg+xml;charset=UTF-8,'); width: 16px; height: 14px; - background-color: $ui-color-transparent; + background-color: $color-transparent; } %with-right-arrow-grey { @extend %pseudo-icon; @@ -225,7 +231,7 @@ background-image: url('data:image/svg+xml;charset=UTF-8,'); width: 16px; height: 16px; - background-color: $ui-color-transparent; + background-color: $color-transparent; } %with-deny-icon-grey { @extend %pseudo-icon; @@ -243,7 +249,7 @@ } %with-warning::before { @extend %with-warning-icon-orange; - background-color: $ui-color-transparent; + background-color: $color-transparent; } %with-critical::before { @extend %with-cross; diff --git a/ui-v2/app/styles/components/index.scss b/ui-v2/app/styles/components/index.scss index f461f7bea1..3493041c17 100644 --- a/ui-v2/app/styles/components/index.scss +++ b/ui-v2/app/styles/components/index.scss @@ -16,9 +16,12 @@ @import './app-view'; @import './product'; -@import './healthcheck-status'; +@import './tag-list'; +@import './healthcheck-output'; +@import './healthcheck-info'; @import './healthchecked-resource'; @import './freetext-filter'; +@import './phrase-editor'; @import './filter-bar'; @import './tomography-graph'; @import './action-group'; diff --git a/ui-v2/app/styles/components/modal-dialog/layout.scss b/ui-v2/app/styles/components/modal-dialog/layout.scss index ee3ed9d167..fb12c1c666 100644 --- a/ui-v2/app/styles/components/modal-dialog/layout.scss +++ b/ui-v2/app/styles/components/modal-dialog/layout.scss @@ -5,7 +5,7 @@ overflow: hidden; } %modal-dialog { - z-index: 10000; + z-index: 500; position: fixed; left: 0; top: 0; @@ -46,6 +46,8 @@ padding-right: 15px; } %modal-window > div { + overflow-y: auto; + max-height: 80vh; padding: 20px 23px; } %modal-window > footer, diff --git a/ui-v2/app/styles/components/modal-dialog/skin.scss b/ui-v2/app/styles/components/modal-dialog/skin.scss index bd901019de..a97a159dcc 100644 --- a/ui-v2/app/styles/components/modal-dialog/skin.scss +++ b/ui-v2/app/styles/components/modal-dialog/skin.scss @@ -1,21 +1,18 @@ %modal-dialog > label { - background-color: rgba($ui-white, 0.9); + background-color: rgba($white, 0.9); } %modal-window { - box-shadow: 2px 8px 8px 0 rgba($ui-black, 0.1); + box-shadow: 2px 8px 8px 0 rgba($black, 0.1); } %modal-window { /*%frame-gray-000*/ - background-color: $ui-white; + background-color: $white; border: $decor-border-100; - border-color: $ui-gray-300; + border-color: $gray-300; } %modal-window > footer, %modal-window > header { - /*%frame-gray-000*/ - border: 0 solid; - background-color: $ui-gray-050; - border-color: $ui-gray-300; + @extend %frame-gray-800; } %modal-window > footer { border-top-width: 1px; @@ -34,9 +31,9 @@ background-size: 80%; cursor: pointer; - /*%frame-gray-000*/ - background-color: $ui-gray-050; + /*%frame-gray-050??*/ + background-color: $gray-050; border: $decor-border-100; - border-color: $ui-gray-300; + border-color: $gray-300; border-radius: $decor-radius-100; } diff --git a/ui-v2/app/styles/components/notice/layout.scss b/ui-v2/app/styles/components/notice/layout.scss index 2e101a21f1..f2e9a9a777 100644 --- a/ui-v2/app/styles/components/notice/layout.scss +++ b/ui-v2/app/styles/components/notice/layout.scss @@ -2,6 +2,7 @@ position: relative; padding: 1em; padding-left: 45px; + margin-bottom: 1em; } %notice::before { position: absolute; diff --git a/ui-v2/app/styles/components/notice/skin.scss b/ui-v2/app/styles/components/notice/skin.scss index fcba275786..c2488b3b2e 100644 --- a/ui-v2/app/styles/components/notice/skin.scss +++ b/ui-v2/app/styles/components/notice/skin.scss @@ -10,46 +10,31 @@ @extend %notice; } %notice-success { - @extend %with-passing; - background-color: $ui-green-050; - border-color: $ui-color-success; - color: $ui-green-700; -} -%notice-success::before { - color: $ui-color-success; + @extend %frame-green-500, %with-passing; } %notice-info { - /* %frame-blue-000*/ - border-style: solid; /*TODO: this can go once we are using a frame*/ - @extend %with-passing; /* needs a better info button*/ - background-color: $ui-blue-050; - border-color: $ui-color-action; /* TODO: change to info */ - color: $ui-blue-700; + @extend %frame-blue-500, %with-passing; /* needs a better info button*/ } %notice-highlight { - /* %frame-blue-000*/ - border-style: solid; /*TODO: this can go once we are using a frame*/ - @extend %with-star; - border-color: $ui-gray-300; - background-color: $ui-gray-050; + @extend %frame-gray-800, %with-star; } -%notice-info::before { - color: $ui-color-action; /* change to info */ +%notice-warning { + @extend %frame-yellow-500, %with-warning; +} +%notice-error { + @extend %frame-red-500, %with-critical; } %notice-highlight::before { /* %with-star, bigger */ width: 16px; height: 16px; } -%notice-warning { - @extend %frame-yellow-500, %with-warning; +%notice-success::before { + color: $color-success; } -%notice-error { - @extend %with-critical; - background-color: $ui-red-050; - border-color: $ui-color-failure; - color: $ui-red-600; +%notice-info::before { + color: $color-action; /* change to info */ } %notice-error::before { - color: $ui-color-failure; + color: $color-failure; } diff --git a/ui-v2/app/styles/components/phrase-editor.scss b/ui-v2/app/styles/components/phrase-editor.scss new file mode 100644 index 0000000000..b7852453a1 --- /dev/null +++ b/ui-v2/app/styles/components/phrase-editor.scss @@ -0,0 +1,4 @@ +@import './phrase-editor/index'; +.phrase-editor { + @extend %phrase-editor; +} diff --git a/ui-v2/app/styles/components/phrase-editor/index.scss b/ui-v2/app/styles/components/phrase-editor/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/styles/components/phrase-editor/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/phrase-editor/layout.scss b/ui-v2/app/styles/components/phrase-editor/layout.scss new file mode 100644 index 0000000000..aa7e5c5ded --- /dev/null +++ b/ui-v2/app/styles/components/phrase-editor/layout.scss @@ -0,0 +1,50 @@ +%phrase-editor { + display: flex; + margin-top: 14px; + margin-bottom: 5px; +} +%phrase-editor ul { + overflow: hidden; +} +%phrase-editor li { + @extend %pill; + float: left; + margin-right: 4px; +} +%phrase-editor span { + display: none; +} +%phrase-editor label { + flex-grow: 1; +} +%phrase-editor input { + width: 100%; + height: 33px; + padding: 8px 10px; + box-sizing: border-box; +} +%phrase-editor button::before { + // TODO: Look at why this isn't automatically centering + vertical-align: inherit !important; +} +@media #{$--horizontal-selects} { + %phrase-editor { + margin-top: 14px; + } + %phrase-editor ul { + padding-top: 5px; + padding-left: 5px; + } + %phrase-editor input { + padding-left: 3px; + } +} +@media #{$--lt-horizontal-selects} { + %phrase-editor { + margin-top: 9px; + } + %phrase-editor label { + display: block; + margin-top: 5px; + } +} diff --git a/ui-v2/app/styles/components/phrase-editor/skin.scss b/ui-v2/app/styles/components/phrase-editor/skin.scss new file mode 100644 index 0000000000..78046d57bc --- /dev/null +++ b/ui-v2/app/styles/components/phrase-editor/skin.scss @@ -0,0 +1,18 @@ +@media #{$--horizontal-selects} { + %phrase-editor { + border: 1px solid $gray-300; + border-radius: 2px; + } + %phrase-editor input:focus { + outline: 0; + } +} +@media #{$--lt-horizontal-selects} { + %phrase-editor label { + border: 1px solid $gray-300; + border-radius: 2px; + } +} +%phrase-editor input { + -webkit-appearance: none; +} diff --git a/ui-v2/app/styles/components/pill.scss b/ui-v2/app/styles/components/pill.scss index 4d8f0673a1..8b2771a6dd 100644 --- a/ui-v2/app/styles/components/pill.scss +++ b/ui-v2/app/styles/components/pill.scss @@ -1,4 +1,45 @@ @import './pill/index'; -td strong { +td strong, +%tag-list span { @extend %pill; + margin-right: 3px; +} +// For the moment pills with classes are iconed ones +%pill:not([class]) { + @extend %frame-gray-900; +} +%pill[class] { + padding-left: 0; + margin-right: 16px; +} +%pill[class]::before { + @extend %as-pseudo; + margin-right: 3px; +} +%pill.policy::before { + @extend %with-file-fill-icon; + opacity: 0.3; +} +%pill.policy-management::before { + @extend %with-star-icon; +} +%pill.policy-service-identity::before { + @extend %with-service-identity-icon; +} +%pill.role::before { + @extend %with-user-plain-icon; + opacity: 0.3; +} + +// TODO: These are related to the pill icons, but also to the tables +// All of this icon assigning stuff should probably go in the eventual +// refactored /components/icons.scss file + +td.policy-service-identity a::after { + @extend %with-service-identity-icon, %as-pseudo; + margin-left: 3px; +} +td.policy-management a::after { + @extend %with-star-icon, %as-pseudo; + margin-left: 3px; } diff --git a/ui-v2/app/styles/components/pill/layout.scss b/ui-v2/app/styles/components/pill/layout.scss index fceb4a5a55..325dc503fb 100644 --- a/ui-v2/app/styles/components/pill/layout.scss +++ b/ui-v2/app/styles/components/pill/layout.scss @@ -2,3 +2,7 @@ display: inline-block; padding: 1px 5px; } +%pill button { + padding: 0; + margin-right: 3px; +} diff --git a/ui-v2/app/styles/components/pill/skin.scss b/ui-v2/app/styles/components/pill/skin.scss index 6efe90d8a4..a0ca72d168 100644 --- a/ui-v2/app/styles/components/pill/skin.scss +++ b/ui-v2/app/styles/components/pill/skin.scss @@ -1,4 +1,13 @@ %pill { - background-color: $ui-gray-100; border-radius: $radius-small; } +%pill button { + background-color: transparent; + font-size: 0; + cursor: pointer; +} +%pill button::before { + @extend %with-cancel-plain-icon, %as-pseudo; + width: 10px; + height: 10px; +} diff --git a/ui-v2/app/styles/components/product.scss b/ui-v2/app/styles/components/product.scss index 4207add1a2..90fa382b25 100644 --- a/ui-v2/app/styles/components/product.scss +++ b/ui-v2/app/styles/components/product.scss @@ -10,23 +10,23 @@ html.template-loading main > div { @extend %loader; } %loader circle { - fill: $brand-magenta-100; + fill: $magenta-100; } %main-header::before { - background-color: $brand-magenta-600; + background-color: $magenta-600; } %header-nav a, %header-nav-toggle-button { - color: $brand-magenta-050; + color: $magenta-050; } @media #{$--lt-horizontal-nav} { %header-nav-panel { - background-color: $brand-magenta-600; + background-color: $magenta-600; } } @media #{$--horizontal-nav} { %header-nav > ul > li:not(:first-child).is-active > a { - background-color: $brand-magenta-800; + background-color: $magenta-800; } } #wrapper > footer { @@ -41,7 +41,7 @@ html.template-loading main > div { /* toggleable toolbar for short screens */ [for='toolbar-toggle'] { @extend %with-magnifier; - color: $ui-blue-500; + color: $blue-500; width: 20px; height: 20px; margin-left: 15px; diff --git a/ui-v2/app/styles/components/product/footer.scss b/ui-v2/app/styles/components/product/footer.scss index 25d61c6342..6192d6e24e 100644 --- a/ui-v2/app/styles/components/product/footer.scss +++ b/ui-v2/app/styles/components/product/footer.scss @@ -5,11 +5,11 @@ border-top: $decor-border-100; } %footer { - border-color: $ui-gray-200; - background-color: $ui-white; + border-color: $gray-200; + background-color: $white; } %footer > * { - color: $ui-gray-400; + color: $gray-400; } %footer { display: flex; diff --git a/ui-v2/app/styles/components/product/header-nav.scss b/ui-v2/app/styles/components/product/header-nav.scss index 85e55abf58..5773502c59 100644 --- a/ui-v2/app/styles/components/product/header-nav.scss +++ b/ui-v2/app/styles/components/product/header-nav.scss @@ -21,20 +21,20 @@ border-bottom: $decor-border-100; } %header-drop-nav li { - border-color: $ui-gray-300; + border-color: $gray-300; } %header-drop-nav { - border-color: $ui-gray-300; - background-color: $ui-white; + border-color: $gray-300; + background-color: $white; } %header-drop-nav a { - color: $ui-gray-900 !important; + color: $gray-900 !important; } %header-nav > ul > li > a:hover, %header-nav > ul > li > a:focus, %header-nav > ul > li > a:active, %header-nav > ul > li.is-active > a { - color: $ui-white; + color: $white; } %header-nav-toggle-button::before, %header-nav-toggle-button::after, @@ -71,7 +71,7 @@ %header-nav-panel { box-sizing: border-box; padding: 15px 35px; - z-index: 10000; + z-index: 499; text-align: right; } %header-nav-toggle-button { @@ -79,7 +79,7 @@ right: 0px; width: 100px; height: 40px; - z-index: 2; + z-index: 200; cursor: pointer; } %header-nav-panel { @@ -88,7 +88,7 @@ height: 100%; position: absolute; top: 0px; - z-index: 3; + z-index: 300; padding: 0; padding-top: 15px; right: -100%; @@ -99,7 +99,7 @@ padding: 15px 35px; } %header-nav-toggle:checked + label { - background-color: rgba($ui-black, 0.4); + background-color: rgba($black, 0.4); width: 100vw; height: 100%; left: 0; @@ -125,7 +125,7 @@ %header-drop-nav a:focus, %header-drop-nav a:active, %header-drop-nav a.selected { - background-color: $ui-gray-050; + background-color: $gray-050; } %header-nav a { display: block; @@ -167,7 +167,7 @@ %header-drop-nav { display: block; position: absolute; - z-index: 100; + z-index: 400; } %header-drop-nav a { text-align: left; diff --git a/ui-v2/app/styles/components/table.scss b/ui-v2/app/styles/components/table.scss index 7f497a3b43..4d3fd154a6 100644 --- a/ui-v2/app/styles/components/table.scss +++ b/ui-v2/app/styles/components/table.scss @@ -1,55 +1,49 @@ @import './icons/index'; @import './table/index'; +@import './type-icon/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 .kind-proxy { + @extend %type-icon, %with-proxy; + text-indent: -9000px !important; + width: 24px; + transform: scale(0.7); } -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: $ui-gray-400; -} -table:not(.sessions) tr { +table:not(.sessions) tbody tr { cursor: pointer; } table:not(.sessions) td:first-child { padding: 0; } -td dt.passing, -td dt.passing + dd { - color: $ui-color-success; -} -td dt.warning, -td dt.warning + dd { - color: $ui-color-alert; -} -td dt.critical, -td dt.critical + dd { - color: $ui-color-failure; -} /* Header Tooltips/Icon*/ th { overflow: visible; } th span { @extend %tooltip; - @extend %with-info; - margin-left: 12px; - top: 3px; - width: 23px; - height: 15px; + margin-left: 2px; + vertical-align: text-top; } -th span:after { - left: -8px; +th span::after { + @extend %with-info-circle-outline-icon, %as-pseudo; + opacity: 0.6; } th span em::after { @extend %tooltip-tail; @@ -69,3 +63,31 @@ th span:hover em::after, th span:hover em { @extend %blink-in-fade-out-active; } +/* ideally these would be in route css files, but left here as they */ +/* accomplish the same thing (hide non-essential columns for tables) */ +@media #{$--lt-medium-table} { + /* Policy > Datacenters */ + html.template-policy.template-list tr > :nth-child(2) { + display: none; + } + html.template-service.template-list tr > :nth-child(2) { + display: none; + } +} +@media #{$--lt-wide-table} { + html.template-intention.template-list tr > :nth-last-child(2) { + display: none; + } + html.template-service.template-list tr > :last-child { + display: none; + } + html.template-node.template-show #services tr > :last-child { + display: none; + } + html.template-node.template-show #lock-sessions tr > :not(:first-child):not(:last-child) { + display: none; + } + html.template-node.template-show #lock-sessions td:last-child { + padding: 0; + } +} diff --git a/ui-v2/app/styles/components/table/layout.scss b/ui-v2/app/styles/components/table/layout.scss index 23301b423e..da345c6153 100644 --- a/ui-v2/app/styles/components/table/layout.scss +++ b/ui-v2/app/styles/components/table/layout.scss @@ -2,7 +2,7 @@ table { width: 100%; } %table-actions { - width: 60px; + width: 60px !important; } th.actions input { display: none; @@ -10,38 +10,29 @@ th.actions input { th.actions { text-align: right; } -td.actions .with-confirmation.confirming { - position: absolute; - bottom: 4px; - right: 1px; +table tr { + display: flex; } -td.actions .with-confirmation.confirming p { - margin-bottom: 1em; +table td { + display: inline-flex; + align-items: center; + height: 50px; +} +table td a { + display: block; } table caption { text-align: left; margin-bottom: 0.8em; } -td > button, -td > .with-confirmation > button { - position: relative; - top: -6px; -} table th { padding-bottom: 0.6em; } -table td, -table td a { - padding: 0.9em 0; -} -table th, +table th:not(.actions), table td:not(.actions), table td a { padding-right: 0.9em; } -table td a { - display: block; -} th, td:not(.actions), td:not(.actions) a { @@ -49,76 +40,9 @@ td:not(.actions) a { text-overflow: ellipsis; 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 { display: none; } } -/* ideally these would be in route css files, but left here as they */ -/* accomplish the same thing (hide non-essential columns for tables) */ -@media #{$--lt-medium-table} { - /* Policy > Datacenters */ - html.template-policy.template-list tr > :nth-child(2) { - display: none; - } -} -@media #{$--lt-wide-table} { - html.template-intention.template-list tr > :nth-last-child(2) { - display: none; - } - html.template-service.template-list tr > :last-child { - display: none; - } - html.template-node.template-show #services tr > :last-child { - display: none; - } - html.template-node.template-show #lock-sessions tr > :not(:first-child):not(:last-child) { - display: none; - } - html.template-node.template-show #lock-sessions td:last-child { - padding: 0; - } - html.template-node.template-show #lock-sessions td:last-child button { - float: right; - } -} diff --git a/ui-v2/app/styles/components/table/skin.scss b/ui-v2/app/styles/components/table/skin.scss index d6582bb793..ceac149fbc 100644 --- a/ui-v2/app/styles/components/table/skin.scss +++ b/ui-v2/app/styles/components/table/skin.scss @@ -3,11 +3,21 @@ td { border-bottom: $decor-border-100; } th { - color: $ui-gray-400 !important; -} -th { - border-color: $keyline-dark; + border-color: $gray-300; } td { - border-color: $keyline-mid; + border-color: $gray-200; + color: $gray-500; +} +th, +td strong { + color: $gray-600; +} +// TODO: Add to native selector `tbody th` - will involve moving all +// current th's to `thead th` and changing the templates +%tbody-th { + color: $gray-900; +} +td:first-child { + @extend %tbody-th; } diff --git a/ui-v2/app/styles/components/tabs.scss b/ui-v2/app/styles/components/tabs.scss index b0c08a7f8c..64a1b9138e 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 9588b870eb..7b20b1aa1f 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 7a5eeda62f..1538bcf0d8 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; } @@ -8,17 +17,16 @@ border-bottom: $decor-border-200; } %tab-nav a { - border-color: $ui-color-transparent; - color: $ui-gray-500; + border-color: $color-transparent; + color: $gray-500; } %tab-nav li:not(.selected) a:hover, %tab-nav li:not(.selected) a:focus, %tab-nav li:not(.selected) a:active { /* %frame-gray-something */ - border-color: $ui-color-transparent; - background-color: $ui-gray-100; + border-color: $color-transparent; + background-color: $gray-100; } %tab-nav .selected a { @extend %frame-magenta-300; } - diff --git a/ui-v2/app/styles/components/tabular-collection.scss b/ui-v2/app/styles/components/tabular-collection.scss index c9745e787f..da5a034506 100644 --- a/ui-v2/app/styles/components/tabular-collection.scss +++ b/ui-v2/app/styles/components/tabular-collection.scss @@ -3,10 +3,6 @@ table.dom-recycling { @extend %dom-recycling-table; } /* project specific */ -%dom-recycling-table { - /* minimum of 4x50px heigh rows plus top/bottom margins*/ - min-height: 249px; -} %dom-recycling-table tbody { /* tbodys are all absolute so,*/ /* make room for the header */ @@ -35,34 +31,60 @@ 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; +table tr > *:nth-last-child(2):first-child, +table tr > *:nth-last-child(2):first-child ~ * { + width: calc(100% / 2); } +table tr > *:nth-last-child(3):first-child, +table tr > *:nth-last-child(3):first-child ~ * { + width: calc(100% / 3); +} +table tr > *:nth-last-child(4):first-child, +table tr > *:nth-last-child(4):first-child ~ * { + width: calc(100% / 4); +} +table tr > *:nth-last-child(5):first-child, +table tr > *:nth-last-child(5):first-child ~ * { + width: calc(100% / 5); +} + +table.has-actions tr > .actions { + @extend %table-actions; +} +table.has-actions tr > *:nth-last-child(2):first-child, +table.has-actions tr > *:nth-last-child(2):first-child ~ * { + width: calc(100% - 60px); +} +table.has-actions tr > *:nth-last-child(3):first-child, +table.has-actions tr > *:nth-last-child(3):first-child ~ * { + width: calc(50% - 30px); +} +table.has-actions tr > *:nth-last-child(4):first-child, +table.has-actions tr > *:nth-last-child(4):first-child ~ * { + width: calc(33% - 20px); +} +table.has-actions tr > *:nth-last-child(5):first-child, +table.has-actions tr > *:nth-last-child(5):first-child ~ * { + width: calc(25% - 15px); +} + /*TODO: trs only live in tables, get rid of table */ html.template-service.template-list main table tr { @extend %services-row; } -html.template-intention.template-list main table tr { - @extend %intentions-row; -} -html.template-kv.template-list main table tr { - @extend %kvs-row; -} -html.template-acl.template-list main table tr { - @extend %acls-row; -} -html.template-policy.template-list main table tr { - @extend %policies-row; +html.template-service.template-show #instances table tr { + @extend %instances-row; } html.template-token.template-list main table tr { @extend %tokens-row; } +html.template-role.template-list main table tr { + @extend %roles-row; +} html.template-policy.template-edit [role='dialog'] table tr, -html.template-policy.template-edit main table tr { +html.template-policy.template-edit main table tr, +html.template-role.template-edit [role='dialog'] table tr, +html.template-role.template-edit main table.token-list tr { @extend %tokens-minimal-row; } html.template-token.template-list main table tr td.me, @@ -70,12 +92,54 @@ html.template-token.template-list main table tr td.me ~ td, html.template-token.template-list main table tr th { @extend %tokens-your-row; } -html.template-node.template-show main table tr { - @extend %node-services-row; -} html.template-node.template-show main table.sessions tr { @extend %node-sessions-row; } +// this will get auto calculated later in tabular-collection.js +// keeping this here for reference +// %services-row > * { +// (100% / 2) - (160px / 2) +// width: calc(50% - 160px); +// } +%services-row > *:nth-child(2) { + width: 100px; +} +%services-row > * { + width: auto; +} +%instances-row > * { + width: calc(100% / 5); +} +%tokens-row > *:first-child, +%tokens-minimal-row > *:not(last-child), +%tokens-row > *:nth-child(2), +%tokens-your-row:nth-last-child(2) { + width: 120px; +} +%tokens-row > *:nth-child(3) { + width: calc(30% - 150px); +} +%tokens-row > *:nth-child(4) { + width: calc(70% - 150px); +} +%tokens-your-row:nth-child(4) { + width: calc(70% - 270px) !important; +} +%tokens-row > *:last-child { + @extend %table-actions; +} +%tokens-minimal-row > *:last-child { + width: calc(100% - 240px) !important; +} + +%roles-row > *:nth-child(1), +%roles-row > *:nth-child(2) { + width: calc(22% - 20px) !important; +} +%roles-row > *:nth-child(3) { + width: calc(56% - 20px) !important; +} + @media #{$--horizontal-session-list} { %node-sessions-row > * { // (100% / 7) - (300px / 6) - (120px / 6) @@ -106,37 +170,6 @@ html.template-node.template-show main table.sessions tr { display: none; } } -%intentions-row > * { - width: calc(25% - 15px); -} -%intentions-row > *:last-child { - @extend %table-actions; -} -%acls-row > * { - width: calc(50% - 30px); -} -%acls-row > *:last-child { - @extend %table-actions; -} -%tokens-row > *:first-child, -%tokens-minimal-row > *:not(last-child), -%tokens-row > *:nth-child(2), -%tokens-your-row:nth-last-child(2) { - width: 120px; -} -%tokens-row > *:nth-child(3), -%tokens-row > *:nth-child(4) { - width: calc(50% - 150px); -} -%tokens-your-row:nth-child(4) { - width: calc(50% - 270px) !important; -} -%tokens-row > *:last-child { - @extend %table-actions; -} -%tokens-minimal-row > *:last-child { - width: calc(100% - 240px); -} @media #{$--lt-medium-table} { /* Token > Policies */ /* Token > Your Token */ @@ -146,29 +179,10 @@ 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; } -} - -%kvs-row > *:first-child { - width: calc(100% - 60px); -} -%kvs-row > *:last-child { - @extend %table-actions; -} -%node-services-row > * { - width: 33%; -} -%policies-row > * { - width: calc(33% - 20px); -} -%policies-row > *:last-child { - @extend %table-actions; -} -// this will get auto calculated later in tabular-collection.js -// keeping this here for reference -// %services-row > * { -// (100% / 2) - (160px / 2) -// width: calc(50% - 160px); -// } -%services-row > * { - width: auto; + html.template-service.template-show #instances tr > :nth-child(3) { + display: none; + } + %instances-row > * { + width: calc(100% / 4); + } } diff --git a/ui-v2/app/styles/components/tabular-details/layout.scss b/ui-v2/app/styles/components/tabular-details/layout.scss index 2f0a958fb2..1cf49f1cb3 100644 --- a/ui-v2/app/styles/components/tabular-details/layout.scss +++ b/ui-v2/app/styles/components/tabular-details/layout.scss @@ -1,8 +1,4 @@ /* TODO: rename: %details-table */ -%tabular-details { - width: 100%; - table-layout: fixed; -} %tabular-details tr > .actions { @extend %table-actions; position: relative; @@ -14,54 +10,48 @@ @extend %toggle-button; pointer-events: auto; position: absolute; + top: 8px; } %tabular-details td > label { @extend %tabular-details-toggle-button; - /*TODO: This needs to be figured out with %toggle-button/%action-group */ - top: 8px; - right: 15px; + right: 2px; +} +%tabular-details tr:nth-child(even) td { + height: auto; + position: relative; + display: table-cell; } %tabular-details tr:nth-child(even) td > * { display: none; } -%tabular-details tr:nth-child(odd) td { - width: calc(50% - 30px); -} -%tabular-details tr:nth-child(odd) td:last-child { - width: 60px; -} %tabular-detail > label { @extend %tabular-details-toggle-button; - top: 8px; - right: 24px; + right: 11px; } %tabular-details tr:nth-child(even) td > input:checked + * { display: block; } %tabular-details td:only-child { overflow: visible; - position: relative; + width: 100%; } + +// detail %tabular-detail { position: relative; left: -10px; right: -10px; width: calc(100% + 20px); - margin-top: -48px; + margin-top: -51px; pointer-events: none; - overflow: hidden; } %tabular-detail { padding: 10px; } -%tabular-detail::before { +%tabular-detail::after { content: ''; display: block; - height: 1px; - position: absolute; - top: -2px; - left: 0; - width: 100%; + clear: both; } %tabular-detail > div { pointer-events: auto; diff --git a/ui-v2/app/styles/components/tabular-details/skin.scss b/ui-v2/app/styles/components/tabular-details/skin.scss index f3b7f38b09..643c26fe6d 100644 --- a/ui-v2/app/styles/components/tabular-details/skin.scss +++ b/ui-v2/app/styles/components/tabular-details/skin.scss @@ -3,18 +3,30 @@ } %tabular-details td:only-child { cursor: default; + border: 0; } %tabular-detail { - border: 1px solid $ui-gray-300; + border: 1px solid $gray-300; border-radius: $decor-radius-100; - box-shadow: 0 8px 10px 0 rgba($ui-black, 0.1); + box-shadow: 0 8px 10px 0 rgba($black, 0.1); margin-bottom: 20px; } %tabular-detail::before, %tabular-detail > div, %tabular-detail > label { - background-color: $ui-white; + background-color: $white; } %tabular-detail > label::before { transform: rotate(180deg); } +// this is here as its a fake border +%tabular-detail::before { + background: $gray-200; + content: ''; + display: block; + height: 1px; + position: absolute; + bottom: -20px; + left: 10px; + width: calc(100% - 20px); +} 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 0000000000..6bc2ea8e70 --- /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 0000000000..bc18252196 --- /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 0000000000..e1f28e6402 --- /dev/null +++ b/ui-v2/app/styles/components/tag-list/layout.scss @@ -0,0 +1,14 @@ +%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 { + display: inline-flex; + padding-left: 0; +} +%tag-list dd > * { + margin-right: 3px; +} 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 0000000000..e69de29bb2 diff --git a/ui-v2/app/styles/components/toggle-button/skin.scss b/ui-v2/app/styles/components/toggle-button/skin.scss index 759b2e430f..bc993a5566 100644 --- a/ui-v2/app/styles/components/toggle-button/skin.scss +++ b/ui-v2/app/styles/components/toggle-button/skin.scss @@ -4,8 +4,8 @@ } %toggle-button:hover, %toggle-button:focus { - background-color: $ui-gray-050; + background-color: $gray-050; } %toggle-button:active { - background-color: $ui-gray-100; + background-color: $gray-100; } diff --git a/ui-v2/app/styles/components/toggle/skin.scss b/ui-v2/app/styles/components/toggle/skin.scss index df2500819f..80c0bf3687 100644 --- a/ui-v2/app/styles/components/toggle/skin.scss +++ b/ui-v2/app/styles/components/toggle/skin.scss @@ -15,16 +15,16 @@ @extend %toggle-negative; } %toggle label span { - color: $ui-gray-900; + color: $gray-900; } %toggle label span::after { - background-color: $ui-white; + background-color: $white; } %toggle label input:checked + span::before, %toggle-negative label input + span::before { - background-color: $ui-blue-500; + background-color: $blue-500; } %toggle label span::before, %toggle-negative label input:checked + span::before { - background-color: $ui-gray-300; + background-color: $gray-300; } diff --git a/ui-v2/app/styles/components/tomography-graph.scss b/ui-v2/app/styles/components/tomography-graph.scss index b76b8bccb1..b61f62776c 100644 --- a/ui-v2/app/styles/components/tomography-graph.scss +++ b/ui-v2/app/styles/components/tomography-graph.scss @@ -1,31 +1,31 @@ .tomography .background { - fill: $ui-gray-050; + fill: $gray-050; } .tomography .axis { fill: none; - stroke: $ui-gray-300; + stroke: $gray-300; stroke-dasharray: 4 4; } .tomography .border { fill: none; - stroke: $ui-gray-300; + stroke: $gray-300; } .tomography .point { - stroke: $ui-gray-400; - fill: $brand-magenta-600; + stroke: $gray-400; + fill: $magenta-600; } .tomography .lines line { - stroke: $brand-magenta-600; + stroke: $magenta-600; } .tomography .lines line:hover { - stroke: $ui-gray-300; + stroke: $gray-300; stroke-width: 2px; } .tomography .tick line { - stroke: $ui-gray-300; + stroke: $gray-300; } .tomography .tick text { font-size: $typo-size-600; text-anchor: start; - color: $ui-gray-900; + color: $gray-900; } diff --git a/ui-v2/app/styles/components/type-icon/index.scss b/ui-v2/app/styles/components/type-icon/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/styles/components/type-icon/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/type-icon/layout.scss b/ui-v2/app/styles/components/type-icon/layout.scss new file mode 100644 index 0000000000..a88b65d79e --- /dev/null +++ b/ui-v2/app/styles/components/type-icon/layout.scss @@ -0,0 +1,5 @@ +%type-icon { + display: inline-block; + text-indent: 20px; + padding: 3px; +} diff --git a/ui-v2/app/styles/components/type-icon/skin.scss b/ui-v2/app/styles/components/type-icon/skin.scss new file mode 100644 index 0000000000..b6d0be1129 --- /dev/null +++ b/ui-v2/app/styles/components/type-icon/skin.scss @@ -0,0 +1,6 @@ +%type-icon { + border-radius: 4px; + + background: $gray-100; + color: $gray-400; +} diff --git a/ui-v2/app/styles/components/with-tooltip.scss b/ui-v2/app/styles/components/with-tooltip.scss index df1a62d51a..c4e536ff12 100644 --- a/ui-v2/app/styles/components/with-tooltip.scss +++ b/ui-v2/app/styles/components/with-tooltip.scss @@ -1,4 +1,4 @@ @import './with-tooltip/index'; -%app-view h1 span { +%app-view h1 span[data-tooltip] { @extend %with-pseudo-tooltip; } diff --git a/ui-v2/app/styles/components/with-tooltip/index.scss b/ui-v2/app/styles/components/with-tooltip/index.scss index 3ee08c67cb..bca4c98e29 100644 --- a/ui-v2/app/styles/components/with-tooltip/index.scss +++ b/ui-v2/app/styles/components/with-tooltip/index.scss @@ -12,7 +12,7 @@ %with-pseudo-tooltip { text-indent: -9000px; font-size: 0; - top: -9px; + top: -7px; } %with-pseudo-tooltip::after, diff --git a/ui-v2/app/styles/components/with-tooltip/layout.scss b/ui-v2/app/styles/components/with-tooltip/layout.scss index c3a3193dbd..130823941b 100644 --- a/ui-v2/app/styles/components/with-tooltip/layout.scss +++ b/ui-v2/app/styles/components/with-tooltip/layout.scss @@ -3,6 +3,7 @@ display: inline-flex; justify-content: center; align-items: center; + vertical-align: text-top; } %tooltip-bubble, %tooltip-tail { @@ -30,5 +31,5 @@ background-color: transparent !important; border-left: 9px solid transparent; border-right: 9px solid transparent; - border-top: 18px solid $ui-gray-800; + border-top: 18px solid $gray-800; } diff --git a/ui-v2/app/styles/components/with-tooltip/skin.scss b/ui-v2/app/styles/components/with-tooltip/skin.scss index f8fbf8ac1c..b4a17d7c5a 100644 --- a/ui-v2/app/styles/components/with-tooltip/skin.scss +++ b/ui-v2/app/styles/components/with-tooltip/skin.scss @@ -1,9 +1,9 @@ %tooltip-bubble, %tooltip-tail { - color: $ui-white; - background-color: $ui-gray-800; + color: $white; + background-color: $gray-800; } %tooltip-bubble { border-radius: $decor-radius-200; - box-shadow: 0 3px 1px 0 rgba($ui-black, 0.12); + box-shadow: 0 3px 1px 0 rgba($black, 0.12); } diff --git a/ui-v2/app/styles/core/typography.scss b/ui-v2/app/styles/core/typography.scss index 4107ebf9f2..8b5eeeed2c 100644 --- a/ui-v2/app/styles/core/typography.scss +++ b/ui-v2/app/styles/core/typography.scss @@ -1,8 +1,8 @@ %button { font-family: $typo-family-sans; } -main p, -%modal-window p { +main p:not(:last-child), +%modal-window p:not(:last-child) { margin-bottom: 1em; } %button, @@ -11,6 +11,7 @@ main p, %form-element [type='password'] { line-height: 1.5; } +h3, %radio-group label { line-height: 1.25; } @@ -24,88 +25,109 @@ main p, %footer { letter-spacing: -0.05em; } -th, -button, -td strong, -td:first-child, + +%button { + font-family: $typo-family-sans; +} +/* Weighting */ h1, %app-content div > dt, %header-drop-nav .is-active { font-weight: $typo-weight-bold; } h2, +h3, +fieldset > header, +caption, %header-nav, %healthchecked-resource header span, -%healthcheck-status dt, +%healthcheck-output dt, %copy-button, %app-content div > dl > dt, -td a { - font-weight: $typo-weight-semibold; -} +%tbody-th, %form-element > span, -%toggle label span, -caption { +%toggle label span { font-weight: $typo-weight-semibold; } %button { font-weight: $typo-weight-semibold !important; } -th, -%breadcrumbs a, -%action-group a, -%tab-nav, -%tooltip-bubble { - font-weight: $typo-weight-medium; -} main label a[rel*='help'], -td:first-child em, %pill, +%tbody-th em, %form-element > strong, -%healthchecked-resource strong { +%healthchecked-resource strong, +%app-view h1 em { font-weight: $typo-weight-normal; } +th, +td strong, +%breadcrumbs li > *, +%action-group-action, +%tab-nav, +%tooltip-bubble, +%type-icon { + font-weight: $typo-weight-medium; +} + +/* Styling */ %form-element > em, -td:first-child em, -%healthchecked-resource header em { +%tbody-th em, +%healthchecked-resource header em, +%app-view h1 em { font-style: normal; } + +/* Sizing */ %footer > * { font-size: inherit; } + h1 { font-size: $typo-header-100; } -h2, -%header-drop-nav .is-active { +h2 { + font-size: $typo-header-200; +} +h3 { + font-size: $typo-header-300; +} +%healthcheck-info dt, +%header-drop-nav .is-active, +%app-view h1 em { font-size: $typo-size-500; } body, pre code, input, textarea, -td { +%action-group-action, +%tbody-th { font-size: $typo-size-600; } th, +td, caption, .type-dialog, %form-element > span, %tooltip-bubble, %healthchecked-resource strong, -%footer { +%footer, +%type-icon { font-size: $typo-size-700; } %toggle label span { font-size: $typo-size-700 !important; } -%app-content > p:only-child, -[role='tabpanel'] > p:only-child, -%app-view > div.disabled > div, +fieldset > p, .template-error > div, +[role='tabpanel'] > p:only-child, +.with-confirmation p, +%app-content > p:only-child, +%app-view > div.disabled > div, %button, %form-element > em, %form-element > strong, -.with-confirmation p, %feedback-dialog-inline p { font-size: $typo-size-800; } diff --git a/ui-v2/app/styles/routes/dc/acls/index.scss b/ui-v2/app/styles/routes/dc/acls/index.scss index bf3fb279ba..849ed0337e 100644 --- a/ui-v2/app/styles/routes/dc/acls/index.scss +++ b/ui-v2/app/styles/routes/dc/acls/index.scss @@ -10,7 +10,7 @@ td a.is-management::after { .template-policy.template-list main header .actions, .template-token.template-list main header .actions { position: relative; - top: 50px; + top: 45px; } } @@ -29,3 +29,10 @@ td a.is-management::after { margin-top: 0; } } +[name='role[state]'], +[name='role[state]'] + * { + display: none; +} +[name='role[state]']:checked + * { + display: block; +} diff --git a/ui-v2/app/styles/routes/dc/acls/tokens/index.scss b/ui-v2/app/styles/routes/dc/acls/tokens/index.scss index 0715d3371b..4ad7cd7ba8 100644 --- a/ui-v2/app/styles/routes/dc/acls/tokens/index.scss +++ b/ui-v2/app/styles/routes/dc/acls/tokens/index.scss @@ -1,20 +1,18 @@ -.template-token.template-edit [for='new-policy-toggle'] { +// TODO: Move this out of here and into probably modal +.type-dialog { @extend %anchor; cursor: pointer; float: right; } -%pill.policy-management { - @extend %with-star; -} %token-yours { text-indent: 20px; - color: $ui-blue-500; + color: $blue-500; padding-left: 15px; } %token-yours::after { @extend %with-tick; border-radius: 100%; - background-color: $ui-blue-500; + background-color: $blue-500; } .me ~ :nth-last-child(2) { @extend %token-yours; @@ -28,6 +26,3 @@ .template-token.template-edit dl { @extend %form-row; } -.template-token.template-edit dd .with-feedback { - top: -5px; -} diff --git a/ui-v2/app/styles/routes/dc/intention/index.scss b/ui-v2/app/styles/routes/dc/intention/index.scss index ce502d3dfb..d10ca5debb 100644 --- a/ui-v2/app/styles/routes/dc/intention/index.scss +++ b/ui-v2/app/styles/routes/dc/intention/index.scss @@ -7,5 +7,5 @@ html.template-intention.template-list td.intent-deny strong { visibility: hidden; } html.template-intention.template-list td.destination { - font-weight: $typo-weight-semibold; + @extend %tbody-th; } diff --git a/ui-v2/app/styles/routes/dc/nodes/index.scss b/ui-v2/app/styles/routes/dc/nodes/index.scss index f8400db878..2bb95dfe96 100644 --- a/ui-v2/app/styles/routes/dc/nodes/index.scss +++ b/ui-v2/app/styles/routes/dc/nodes/index.scss @@ -1,5 +1,10 @@ -// TODO: Generalize this, also see services/index -@import '../../../components/pill/index'; -html.template-node.template-show td.tags span { - @extend %pill; +html.template-node.template-show .sessions td:last-child { + justify-content: flex-end; +} +html.template-node.template-show .sessions td:first-child { + @extend %tbody-th; +} +html.template-node.template-list .healthy h2, +html.template-node.template-list .unhealthy h2 { + margin-bottom: 0.7em; } diff --git a/ui-v2/app/styles/routes/dc/service/index.scss b/ui-v2/app/styles/routes/dc/service/index.scss index c38a957a9d..e69de29bb2 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 56895ef266..3f31e55818 100644 --- a/ui-v2/app/styles/variables/custom-query.scss +++ b/ui-v2/app/styles/variables/custom-query.scss @@ -1,9 +1,9 @@ $ideal-width: 1260px; -$--horizontal-filters: '(min-width: 850px)'; -$--lt-horizontal-filters: '(max-width: 849px)'; +$--horizontal-filters: '(min-width: 910px)'; +$--lt-horizontal-filters: '(max-width: 909px)'; -$--horizontal-selects: '(min-width: 615px)'; -$--lt-horizontal-selects: '(max-width: 614px)'; +$--horizontal-selects: '(min-width: 670px)'; +$--lt-horizontal-selects: '(max-width: 669px)'; $--horizontal-nav: '(min-width: 850px)'; $--lt-horizontal-nav: '(max-width: 849px)'; @@ -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/styles/variables/index.scss b/ui-v2/app/styles/variables/index.scss index 03b270c13a..6689805bbc 100644 --- a/ui-v2/app/styles/variables/index.scss +++ b/ui-v2/app/styles/variables/index.scss @@ -1,14 +1,9 @@ @import './custom-query'; -$gray: $ui-gray-200; -$ui-gray-025: #fafbfc; +$gray: $gray-200; +$gray-025: #fafbfc; $magenta-800-no-hash: 5a1434; -$keyline-light: $ui-gray-100; // h1 -$keyline-mid: $ui-gray-200; // td, footer -$keyline-dark: $ui-gray-300; // th -$keyline-darker: $ui-gray-400; - // decoration // undecided $radius-small: $decor-radius-100; diff --git a/ui-v2/app/templates/components/acl-filter.hbs b/ui-v2/app/templates/components/acl-filter.hbs index e6d24e39a3..44595ea10d 100644 --- a/ui-v2/app/templates/components/acl-filter.hbs +++ b/ui-v2/app/templates/components/acl-filter.hbs @@ -1,4 +1,4 @@ {{!
}} - {{freetext-filter onchange=(action onchange) value=search placeholder="Search by name/token"}} + {{freetext-filter searchable=searchable value=search placeholder="Search by name/token"}} {{radio-group name="type" value=type items=filters onchange=(action onchange)}} {{!
}} diff --git a/ui-v2/app/templates/components/app-view.hbs b/ui-v2/app/templates/components/app-view.hbs index 52bcfc61fc..9ac7c7106b 100644 --- a/ui-v2/app/templates/components/app-view.hbs +++ b/ui-v2/app/templates/components/app-view.hbs @@ -5,7 +5,12 @@ {{#flash-message flash=flash as |component flash|}} {{! flashes automatically ucfirst the type }} -

{{if (eq component.flashType 'Success') 'Success!' 'Error!'}} {{#yield-slot 'notification' (block-params (lowercase component.flashType) (lowercase flash.action) flash.item )}}{{yield}}{{/yield-slot}}

+

+ + {{component.flashType}}! + + {{#yield-slot 'notification' (block-params (lowercase component.flashType) (lowercase flash.action) flash.item )}}{{yield}}{{/yield-slot}} +

{{/flash-message}} {{/each}}
diff --git a/ui-v2/app/templates/components/catalog-filter.hbs b/ui-v2/app/templates/components/catalog-filter.hbs index d69ecbad0a..5185b0921e 100644 --- a/ui-v2/app/templates/components/catalog-filter.hbs +++ b/ui-v2/app/templates/components/catalog-filter.hbs @@ -1,4 +1,9 @@ {{!
}} - {{freetext-filter value=search placeholder="Search by name" onchange=(action onchange)}} - {{radio-group name="status" value=status items=filters onchange=(action onchange)}} + {{freetext-filter searchable=searchable value=search placeholder="Search by name"}} + {{radio-group name="status" value=status items=(array + (hash label='All (Any Status)' value='' ) + (hash label='Critical Checks' value='critical') + (hash label='Warning Checks' value='warning') + (hash label='Passing Checks' value='passing') + ) onchange=(action onchange)}} {{!
}} diff --git a/ui-v2/app/templates/components/changeable-set.hbs b/ui-v2/app/templates/components/changeable-set.hbs new file mode 100644 index 0000000000..c074009d5f --- /dev/null +++ b/ui-v2/app/templates/components/changeable-set.hbs @@ -0,0 +1,6 @@ +{{yield}} +{{#if (gt items.length 0)}} + {{#yield-slot 'set' (block-params items)}}{{yield}}{{/yield-slot}} +{{else}} + {{#yield-slot 'empty'}}{{yield}}{{/yield-slot}} +{{/if}} \ No newline at end of file diff --git a/ui-v2/app/templates/components/child-selector.hbs b/ui-v2/app/templates/components/child-selector.hbs new file mode 100644 index 0000000000..610c32915e --- /dev/null +++ b/ui-v2/app/templates/components/child-selector.hbs @@ -0,0 +1,21 @@ +{{yield}} + {{#yield-slot 'create'}}{{yield}}{{/yield-slot}} + +{{#if (gt items.length 0)}} + {{#yield-slot 'set'}}{{yield}}{{/yield-slot}} +{{else}} + +{{/if}} \ No newline at end of file diff --git a/ui-v2/app/templates/components/code-editor.hbs b/ui-v2/app/templates/components/code-editor.hbs index 4ebd81644b..dd6b9ae5e9 100644 --- a/ui-v2/app/templates/components/code-editor.hbs +++ b/ui-v2/app/templates/components/code-editor.hbs @@ -1,18 +1,18 @@ {{ivy-codemirror value=value - readonly=readonly name=name class=class options=options valueUpdated=(action onkeyup) }} -{{#if (not syntax)}} -{{#power-select - onchange=(action onchange) - selected=mode - searchEnabled=false - options=modes as |mode| -}} - {{mode.name}} -{{/power-select}} +
{{yield}}
+{{#if (and (not readonly) (not syntax))}} + {{#power-select + onchange=(action 'change') + selected=mode + searchEnabled=false + options=modes as |mode| + }} + {{mode.name}} + {{/power-select}} {{/if}} diff --git a/ui-v2/app/templates/components/form-component.hbs b/ui-v2/app/templates/components/form-component.hbs new file mode 100644 index 0000000000..fb5c4b157d --- /dev/null +++ b/ui-v2/app/templates/components/form-component.hbs @@ -0,0 +1 @@ +{{yield}} \ No newline at end of file diff --git a/ui-v2/app/templates/components/hashicorp-consul.hbs b/ui-v2/app/templates/components/hashicorp-consul.hbs index 916d69343c..4c7ba823ef 100644 --- a/ui-v2/app/templates/components/hashicorp-consul.hbs +++ b/ui-v2/app/templates/components/hashicorp-consul.hbs @@ -42,13 +42,11 @@
@@ -59,7 +57,7 @@ {{modal-layer}} \ No newline at end of file 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 0000000000..982d4c2c86 --- /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 0000000000..babca35d74 --- /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 0000000000..05a75e40a2 --- /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 05a75e40a2..383f67386c 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/intention-filter.hbs b/ui-v2/app/templates/components/intention-filter.hbs index 39b815e44e..cb7c999171 100644 --- a/ui-v2/app/templates/components/intention-filter.hbs +++ b/ui-v2/app/templates/components/intention-filter.hbs @@ -1,4 +1,4 @@ {{!
}} - {{freetext-filter onchange=(action onchange) value=search placeholder="Search by Source or Destination"}} + {{freetext-filter searchable=searchable value=search placeholder="Search by Source or Destination"}} {{radio-group name="action" value=action items=filters onchange=(action onchange)}} {{!
}} \ No newline at end of file diff --git a/ui-v2/app/templates/components/modal-dialog.hbs b/ui-v2/app/templates/components/modal-dialog.hbs index 5ee2ba9e4b..4f7357efca 100644 --- a/ui-v2/app/templates/components/modal-dialog.hbs +++ b/ui-v2/app/templates/components/modal-dialog.hbs @@ -1,5 +1,5 @@ {{yield}} - +
diff --git a/ui-v2/app/templates/components/phrase-editor.hbs b/ui-v2/app/templates/components/phrase-editor.hbs new file mode 100644 index 0000000000..480849c5f9 --- /dev/null +++ b/ui-v2/app/templates/components/phrase-editor.hbs @@ -0,0 +1,11 @@ +
    + {{#each items as |item index|}} +
  • + {{item}} +
  • + {{/each}} +
+ \ No newline at end of file diff --git a/ui-v2/app/templates/components/policy-form.hbs b/ui-v2/app/templates/components/policy-form.hbs new file mode 100644 index 0000000000..4b77af4af7 --- /dev/null +++ b/ui-v2/app/templates/components/policy-form.hbs @@ -0,0 +1,76 @@ +{{yield}} +
+ {{#yield-slot 'template'}} + {{else}} +
+ Policy or service identity? +
+

+ A Service Identity is default policy with a configurable service name. This saves you some time and effort you're using Consul for Connect features. +

+ {{! this should use radio-group }} +
+ {{#each templates as |template|}} + + {{/each}} +
+ {{/yield-slot}} + + +
+ Valid datacenters + +
+{{#if isScoped }} +
+ {{#each datacenters as |dc| }} + + {{/each}} + {{#each item.Datacenters as |dc| }} +{{#if (not (find-by 'Name' dc datacenters))}} + +{{/if}} + {{/each}} +
+{{/if}} +{{#if (eq item.template '') }} + +{{/if}} +
+ diff --git a/ui-v2/app/templates/components/policy-selector.hbs b/ui-v2/app/templates/components/policy-selector.hbs new file mode 100644 index 0000000000..6f58b1d0a5 --- /dev/null +++ b/ui-v2/app/templates/components/policy-selector.hbs @@ -0,0 +1,89 @@ +{{#child-selector repo=repo dc=dc type="policy" placeholder="Search for policy" items=items}} + {{yield}} + {{#block-slot 'label'}} + Apply an existing policy + {{/block-slot}} + {{#block-slot 'create'}} + {{#yield-slot 'trigger'}} + {{yield}} + {{else}} + + {{!TODO: potentially call trigger something else}} + {{!the modal has to go here so that if you provide a slot to trigger it doesn't get rendered}} + {{#modal-dialog data-test-policy-form name="new-policy-toggle"}} + {{#block-slot 'header'}} +

New Policy

+ {{/block-slot}} + {{#block-slot 'body'}} + {{policy-form form=form dc=dc}} + {{/block-slot}} + {{#block-slot 'actions' as |close|}} + + + {{/block-slot}} + {{/modal-dialog}} + {{/yield-slot}} + {{/block-slot}} + {{#block-slot 'option' as |option|}} + {{option.Name}} + {{/block-slot}} + {{#block-slot 'set'}} + {{#tabular-details + data-test-policies + onchange=(action 'loadItem') + items=(sort-by 'CreateTime:desc' 'Name:asc' items) as |item index| + }} + {{#block-slot 'header'}} + Name + Datacenters + {{/block-slot}} + {{#block-slot 'row'}} + +{{#if item.ID }} + {{item.Name}} +{{else}} + {{item.Name}} +{{/if}} + + + {{if (not item.isSaving) (join ', ' (policy/datacenters item)) 'Saving...'}} + + {{/block-slot}} + {{#block-slot 'details'}} + +
+ {{#confirmation-dialog message='Are you sure you want to remove this policy from this token?'}} + {{#block-slot 'action' as |confirm|}} + + {{/block-slot}} + {{#block-slot 'dialog' as |execute cancel message|}} +

+ {{message}} +

+ + + + {{/block-slot}} + {{/confirmation-dialog}} +
+ {{/block-slot}} + {{/tabular-details}} + + {{/block-slot}} +{{/child-selector}} diff --git a/ui-v2/app/templates/components/role-form.hbs b/ui-v2/app/templates/components/role-form.hbs new file mode 100644 index 0000000000..14c6571ef4 --- /dev/null +++ b/ui-v2/app/templates/components/role-form.hbs @@ -0,0 +1,26 @@ +{{yield}} +
+ + +
+{{!TODO: temporary policies id, look at the inception token modals and get rid of id="policies" and use something else}} +
+

Policies

+ {{#yield-slot 'policy' (block-params item)}} + {{yield}} + {{else}} + {{policy-selector dc=dc items=item.Policies}} + {{/yield-slot}} +
diff --git a/ui-v2/app/templates/components/role-selector.hbs b/ui-v2/app/templates/components/role-selector.hbs new file mode 100644 index 0000000000..0300e23e0f --- /dev/null +++ b/ui-v2/app/templates/components/role-selector.hbs @@ -0,0 +1,106 @@ +{{#modal-dialog data-test-role-form onclose=(action (mut state) 'role') name="new-role-toggle"}} + {{#block-slot 'header'}} +{{#if (eq state 'role')}} +

New Role

+{{else}} +

New Policy

+{{/if}} + {{/block-slot}} + {{#block-slot 'body'}} + + + {{#role-form form=form dc=dc}} + {{#block-slot 'policy'}} + + {{#policy-selector source=source dc=dc items=item.Policies}} + {{#block-slot 'trigger'}} + + {{/block-slot}} + {{/policy-selector}} + + {{/block-slot}} + {{/role-form}} + + + {{policy-form data-test-policy-form name="role[policy]" form=policyForm dc=dc}} + + {{/block-slot}} + {{#block-slot 'actions' as |close|}} + +{{#if (eq state 'role')}} + + +{{else}} + + +{{/if}} + + {{/block-slot}} +{{/modal-dialog}} + +{{#child-selector repo=repo dc=dc type="role" placeholder="Search for role" items=items}} + {{#block-slot 'label'}} + Apply an existing role + {{/block-slot}} + {{#block-slot 'create'}} + + + {{/block-slot}} + {{#block-slot 'option' as |option|}} + {{option.Name}} + {{/block-slot}} + {{#block-slot 'set'}} + {{#tabular-collection + data-test-roles + rows=5 + items=(sort-by 'CreateTime:desc' 'Name:asc' items) as |item index| + }} + {{#block-slot 'header'}} + Name + Description + {{/block-slot}} + {{#block-slot 'row'}} + + {{item.Name}} + + + {{item.Description}} + + {{/block-slot}} + {{#block-slot 'actions' as |index change checked|}} + {{#confirmation-dialog confirming=false index=index message="Are you sure you want to remove this Role?"}} + {{#block-slot 'action' as |confirm|}} + {{#action-group index=index onchange=(action change) checked=(if (eq checked index) 'checked')}} +
    +
  • + Edit +
  • +
  • + +
  • +
+ {{/action-group}} + {{/block-slot}} + {{#block-slot 'dialog' as |execute cancel message name|}} + {{delete-confirmation message=message execute=execute cancel=cancel}} + {{/block-slot}} + {{/confirmation-dialog}} + {{/block-slot}} + {{/tabular-collection}} + + {{/block-slot}} +{{/child-selector}} diff --git a/ui-v2/app/templates/components/service-identity.hbs b/ui-v2/app/templates/components/service-identity.hbs new file mode 100644 index 0000000000..281697d116 --- /dev/null +++ b/ui-v2/app/templates/components/service-identity.hbs @@ -0,0 +1,12 @@ +service "{{name}}" { + policy = "write" +} +service "{{name}}-sidecar-proxy" { + policy = "write" +} +service_prefix "" { + policy = "read" +} +node_prefix "" { + policy = "read" +} \ No newline at end of file diff --git a/ui-v2/app/templates/components/tab-nav.hbs b/ui-v2/app/templates/components/tab-nav.hbs index 746f1f46c2..63ef3dd4a5 100644 --- a/ui-v2/app/templates/components/tab-nav.hbs +++ b/ui-v2/app/templates/components/tab-nav.hbs @@ -1,7 +1,7 @@ {{!