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/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 fbf87a180d..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'), + 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/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/modal-dialog.js b/ui-v2/app/components/modal-dialog.js index cb934102a0..3d95b77f3a 100644 --- a/ui-v2/app/components/modal-dialog.js +++ b/ui-v2/app/components/modal-dialog.js @@ -38,9 +38,11 @@ export default DomBufferComponent.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') @@ -108,7 +110,7 @@ export default DomBufferComponent.extend(SlotsMixin, WithResizing, { if (get(e, 'target.checked')) { this._open(e); } else { - this._close(); + this._close(e); } }, close: function() { 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/tabular-collection.js b/ui-v2/app/components/tabular-collection.js index 286f12599f..c4d2e1c7a8 100644 --- a/ui-v2/app/components/tabular-collection.js +++ b/ui-v2/app/components/tabular-collection.js @@ -80,13 +80,16 @@ const change = function(e) { // therefore we don't need to calculate if (e.currentTarget.getAttribute('id') !== 'actions_close') { const dom = get(this, 'dom'); + const $tr = dom.closest('tr', e.currentTarget); const $group = dom.sibling(e.currentTarget, 'ul'); - const $footer = dom.element('footer[role="contentinfo"]'); 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 { @@ -111,6 +114,7 @@ const change = function(e) { export default CollectionComponent.extend(SlotsMixin, WithResizing, { tagName: 'table', classNames: ['dom-recycling'], + classNameBindings: ['hasActions'], attributeBindings: ['style'], width: 1150, rowHeight: 50, @@ -128,13 +132,14 @@ export default CollectionComponent.extend(SlotsMixin, WithResizing, { }, getStyle: computed('rowHeight', '_items', 'maxRows', 'maxHeight', function() { const maxRows = get(this, 'rows'); - let rows = get(this._items || [], 'length'); + 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; } - const height = get(this, 'rowHeight') * rows + 29; return { - height: Math.min(get(this, 'maxHeight'), height), + height: height, }; }), resize: function(e) { diff --git a/ui-v2/app/components/tabular-details.js b/ui-v2/app/components/tabular-details.js index 50d70b796f..5112ffffdc 100644 --- a/ui-v2/app/components/tabular-details.js +++ b/ui-v2/app/components/tabular-details.js @@ -19,8 +19,8 @@ export default Component.extend(SlotsMixin, { click: function(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/controllers/dc/acls/policies/edit.js b/ui-v2/app/controllers/dc/acls/policies/edit.js index 9c15643242..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 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.item, 'Datacenters', null); - break; - default: - throw err; - } - } - }, }, }); 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 f400502603..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,33 +17,12 @@ 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 event = get(this, 'dom').normalizeEvent(e, value); const form = get(this, 'form'); @@ -52,24 +31,6 @@ export default Controller.extend({ } 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/forms/acl.js b/ui-v2/app/forms/acl.js index 7a7051ef6b..a6c2503046 100644 --- a/ui-v2/app/forms/acl.js +++ b/ui-v2/app/forms/acl.js @@ -1,6 +1,6 @@ import validations from 'consul-ui/validations/acl'; 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); } diff --git a/ui-v2/app/forms/intention.js b/ui-v2/app/forms/intention.js index 40e8e155e5..3d4139fd0f 100644 --- a/ui-v2/app/forms/intention.js +++ b/ui-v2/app/forms/intention.js @@ -1,6 +1,6 @@ import validations from 'consul-ui/validations/intention'; 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); } diff --git a/ui-v2/app/forms/kv.js b/ui-v2/app/forms/kv.js index c993fab64c..a68d5f1b8f 100644 --- a/ui-v2/app/forms/kv.js +++ b/ui-v2/app/forms/kv.js @@ -1,6 +1,6 @@ import validations from 'consul-ui/validations/kv'; 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); } 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 b5cc218812..03f3f1d431 100644 --- a/ui-v2/app/forms/token.js +++ b/ui-v2/app/forms/token.js @@ -1,9 +1,9 @@ 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/form.js b/ui-v2/app/initializers/form.js index 1c9c24b5ce..3596e2411e 100644 --- a/ui-v2/app/initializers/form.js +++ b/ui-v2/app/initializers/form.js @@ -1,21 +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 = { - kv: kv(), - acl: acl(), - token: token(), - policy: policy(), - intention: intention(), + 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/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 index ebdd48c926..d5a80cbe32 100644 --- a/ui-v2/app/initializers/search.js +++ b/ui-v2/app/initializers/search.js @@ -1,6 +1,7 @@ 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'; @@ -19,6 +20,7 @@ export function initialize(application) { token: token(filterable), acl: acl(filterable), policy: policy(filterable), + role: role(filterable), kv: kv(filterable), healthyNode: node(filterable), unhealthyNode: node(filterable), diff --git a/ui-v2/app/instance-initializers/event-source.js b/ui-v2/app/instance-initializers/event-source.js index 3ad5c102d0..05f661abb5 100644 --- a/ui-v2/app/instance-initializers/event-source.js +++ b/ui-v2/app/instance-initializers/event-source.js @@ -17,6 +17,20 @@ export function initialize(container) { }, }; }) + .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 @@ -54,6 +68,13 @@ export function initialize(container) { proxyRepo: 'repository/proxy/event-source', }, }, + { + service: 'form', + services: { + role: 'repository/role/component', + policy: 'repository/policy/component', + }, + }, ]) .forEach(function(definition) { if (typeof definition.extend !== 'undefined') { 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/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/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/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 764bc5f358..50234f27d7 100644 --- a/ui-v2/app/router.js +++ b/ui-v2/app/router.js @@ -74,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: { diff --git a/ui-v2/app/routes/dc/acls/policies/edit.js b/ui-v2/app/routes/dc/acls/policies/edit.js index 0940813c4f..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': 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 384a521337..8109df5f33 100644 --- a/ui-v2/app/routes/dc/acls/tokens/edit.js +++ b/ui-v2/app/routes/dc/acls/tokens/edit.js @@ -1,38 +1,19 @@ 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; - }), }, }); }); @@ -40,65 +21,4 @@ export default SingleRoute.extend(WithTokenActions, { setupController: function(controller, model) { 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/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/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/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/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 index 18b8a86560..f9e34f77df 100644 --- a/ui-v2/app/services/lazy-proxy.js +++ b/ui-v2/app/services/lazy-proxy.js @@ -12,9 +12,14 @@ export default Service.extend({ if (typeof content[prop] === 'function') { if (this.shouldProxy(content, prop)) { this[prop] = function() { - return this.execute(content, prop).then(method => { - return method.apply(this, arguments); - }); + 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() { diff --git a/ui-v2/app/services/repository/policy.js b/ui-v2/app/services/repository/policy.js index 74787266a9..f9a506d023 100644 --- a/ui-v2/app/services/repository/policy.js +++ b/ui-v2/app/services/repository/policy.js @@ -22,6 +22,16 @@ export default RepositoryService.extend({ 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')); }, 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/token.js b/ui-v2/app/services/repository/token.js index e12896bf8b..bf4358d229 100644 --- a/ui-v2/app/services/repository/token.js +++ b/ui-v2/app/services/repository/token.js @@ -49,4 +49,10 @@ export default RepositoryService.extend({ dc: dc, }); }, + findByRole: function(id, dc) { + return get(this, 'store').query(this.getModelName(), { + role: id, + dc: dc, + }); + }, }); 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/search.js b/ui-v2/app/services/search.js index 5a1c491cc1..5962a60852 100644 --- a/ui-v2/app/services/search.js +++ b/ui-v2/app/services/search.js @@ -1,2 +1,9 @@ import Service from '@ember/service'; -export default Service.extend({}); +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/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 index a708206da9..6e49492576 100644 --- a/ui-v2/app/styles/base/icons/base-placeholders.scss +++ b/ui-v2/app/styles/base/icons/base-placeholders.scss @@ -7,8 +7,7 @@ content: ''; visibility: visible; background-size: contain; -} -%with-cancel-plain-icon { - @extend %with-icon; - background-image: $cancel-plain-svg; -} + 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 index e63eeb0642..1be5ffae1b 100644 --- a/ui-v2/app/styles/base/icons/base-variables.scss +++ b/ui-v2/app/styles/base/icons/base-variables.scss @@ -73,10 +73,11 @@ $queue-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,'); +$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,'); 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 3fe0d277f0..17b18ac3c6 100644 --- a/ui-v2/app/styles/base/icons/index.scss +++ b/ui-v2/app/styles/base/icons/index.scss @@ -1,2 +1,3 @@ @import './base-variables'; @import './base-placeholders'; +@import './icon-placeholders'; diff --git a/ui-v2/app/styles/components/action-group/layout.scss b/ui-v2/app/styles/components/action-group/layout.scss index b5f0549321..b3a6c49654 100644 --- a/ui-v2/app/styles/components/action-group/layout.scss +++ b/ui-v2/app/styles/components/action-group/layout.scss @@ -9,21 +9,12 @@ %action-group li > * { @extend %action-group-action; } -%action-group::before { - margin-left: -1px; -} -%action-group label::after { - margin-left: 5px; -} -%action-group label::before { - margin-left: -7px; -} %action-group { width: 30px; height: 30px; position: absolute; top: 8px; - right: 15px; + right: 3px; } %action-group label { display: block; @@ -38,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; diff --git a/ui-v2/app/styles/components/action-group/skin.scss b/ui-v2/app/styles/components/action-group/skin.scss index 7ef8d43773..344fa9319c 100644 --- a/ui-v2/app/styles/components/action-group/skin.scss +++ b/ui-v2/app/styles/components/action-group/skin.scss @@ -9,10 +9,9 @@ %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; diff --git a/ui-v2/app/styles/components/anchors.scss b/ui-v2/app/styles/components/anchors.scss index a4ac20ee7b..af16dbd1b6 100644 --- a/ui-v2/app/styles/components/anchors.scss +++ b/ui-v2/app/styles/components/anchors.scss @@ -2,12 +2,13 @@ %main-content a { color: $gray-900; } -%main-content a[rel*='help'] { - @extend %with-info; -} %main-content label a[rel*='help'] { color: $gray-400; } +%main-content a[rel*='help']::after { + @extend %with-info-circle-outline-icon, %as-pseudo; + opacity: 0.4; +} [role='tabpanel'] > p:only-child [rel*='help']::after { content: none; diff --git a/ui-v2/app/styles/components/code-editor/layout.scss b/ui-v2/app/styles/components/code-editor/layout.scss index d2dbfac845..a4f1865e59 100644 --- a/ui-v2/app/styles/components/code-editor/layout.scss +++ b/ui-v2/app/styles/components/code-editor/layout.scss @@ -29,3 +29,6 @@ content: ''; display: block; } +%code-editor > pre { + display: none; +} diff --git a/ui-v2/app/styles/components/form-elements.scss b/ui-v2/app/styles/components/form-elements.scss index c0bd6fa6e0..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 form:not(.filter-bar) [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/healthcheck-info.scss b/ui-v2/app/styles/components/healthcheck-info.scss index a249d32c49..ff44885a30 100644 --- a/ui-v2/app/styles/components/healthcheck-info.scss +++ b/ui-v2/app/styles/components/healthcheck-info.scss @@ -1,11 +1,11 @@ @import './healthcheck-info/index'; @import './icons/index'; -tr dl { +tr .healthcheck-info { @extend %healthcheck-info; } td span.zero { @extend %with-no-healthchecks; - // TODO: Why isn't this is layout? + // TODO: Why isn't this in layout? display: block; text-indent: 20px; color: $gray-400; diff --git a/ui-v2/app/styles/components/healthcheck-info/layout.scss b/ui-v2/app/styles/components/healthcheck-info/layout.scss index 0f084db303..06b64a930a 100644 --- a/ui-v2/app/styles/components/healthcheck-info/layout.scss +++ b/ui-v2/app/styles/components/healthcheck-info/layout.scss @@ -1,23 +1,18 @@ %healthcheck-info { - display: flex; - height: 100%; - float: left; + 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 { - text-indent: -9000px; -} -%healthcheck-info dt.warning { - overflow: visible; -} %healthcheck-info dt.warning::before { top: 7px; } diff --git a/ui-v2/app/styles/components/icons/index.scss b/ui-v2/app/styles/components/icons/index.scss index 13e9751672..5f3dfdb460 100644 --- a/ui-v2/app/styles/components/icons/index.scss +++ b/ui-v2/app/styles/components/icons/index.scss @@ -149,18 +149,6 @@ 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: $color-transparent; - width: 16px; - height: 16px; -} /*TODO: All chevrons need merging */ %with-chevron-down::before { @extend %pseudo-icon-bg-img; diff --git a/ui-v2/app/styles/components/modal-dialog/layout.scss b/ui-v2/app/styles/components/modal-dialog/layout.scss index 222ffd4129..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; diff --git a/ui-v2/app/styles/components/pill.scss b/ui-v2/app/styles/components/pill.scss index af1809c339..8b2771a6dd 100644 --- a/ui-v2/app/styles/components/pill.scss +++ b/ui-v2/app/styles/components/pill.scss @@ -2,4 +2,44 @@ 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/skin.scss b/ui-v2/app/styles/components/pill/skin.scss index 169bda915f..a0ca72d168 100644 --- a/ui-v2/app/styles/components/pill/skin.scss +++ b/ui-v2/app/styles/components/pill/skin.scss @@ -1,5 +1,4 @@ %pill { - @extend %frame-gray-900; border-radius: $radius-small; } %pill button { diff --git a/ui-v2/app/styles/components/product/header-nav.scss b/ui-v2/app/styles/components/product/header-nav.scss index 29458807f2..5773502c59 100644 --- a/ui-v2/app/styles/components/product/header-nav.scss +++ b/ui-v2/app/styles/components/product/header-nav.scss @@ -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%; @@ -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 1a98bbc998..4d3fd154a6 100644 --- a/ui-v2/app/styles/components/table.scss +++ b/ui-v2/app/styles/components/table.scss @@ -24,10 +24,9 @@ td .kind-proxy { @extend %type-icon, %with-proxy; text-indent: -9000px !important; width: 24px; - margin-top: -8px; transform: scale(0.7); } -table:not(.sessions) tr { +table:not(.sessions) tbody tr { cursor: pointer; } table:not(.sessions) td:first-child { @@ -39,14 +38,12 @@ th { } 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; @@ -66,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 6cf8ef1ccd..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:first-child 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,43 +40,9 @@ td:not(.actions) a { text-overflow: ellipsis; overflow: hidden; } - /* 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) */ -/* TODO: Move these to component/table.scss for the moment */ -/* Also mixed with things in component/tabular-collection.scss move those also */ -@media #{$--lt-medium-table} { - /* Policy > Datacenters */ - html.template-policy.template-list tr > :nth-child(2) { - 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; - } - 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 9ab4efc978..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: $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/tabular-collection.scss b/ui-v2/app/styles/components/tabular-collection.scss index 1398d62e26..da5a034506 100644 --- a/ui-v2/app/styles/components/tabular-collection.scss +++ b/ui-v2/app/styles/components/tabular-collection.scss @@ -31,6 +31,43 @@ table.dom-recycling { /* using: */ /* calc(<100% divided by number of non-fixed width cells> - ) */ +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; @@ -38,26 +75,16 @@ html.template-service.template-list main table tr { html.template-service.template-show #instances table tr { @extend %instances-row; } -html.template-instance.template-show #upstreams table tr { - @extend %upstreams-row; -} -html.template-intention.template-list main table tr { - @extend %intentions-row; -} -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-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, @@ -65,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) @@ -101,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 */ @@ -148,37 +186,3 @@ html.template-node.template-show main table.sessions tr { width: calc(100% / 4); } } - -%kvs-row > *:first-child { - width: calc(100% - 60px); -} -%kvs-row > *:last-child { - @extend %table-actions; -} -%node-services-row > * { - width: calc(100% / 3); -} -%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 > *:nth-child(2) { - width: 100px; -} -%services-row > * { - width: auto; -} -%instances-row > * { - width: calc(100% / 5); -} -%upstreams-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 7a7faae958..643c26fe6d 100644 --- a/ui-v2/app/styles/components/tabular-details/skin.scss +++ b/ui-v2/app/styles/components/tabular-details/skin.scss @@ -3,6 +3,7 @@ } %tabular-details td:only-child { cursor: default; + border: 0; } %tabular-detail { border: 1px solid $gray-300; @@ -18,3 +19,14 @@ %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/layout.scss b/ui-v2/app/styles/components/tag-list/layout.scss index 2590f8b4c1..e1f28e6402 100644 --- a/ui-v2/app/styles/components/tag-list/layout.scss +++ b/ui-v2/app/styles/components/tag-list/layout.scss @@ -6,5 +6,9 @@ // 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/with-tooltip/layout.scss b/ui-v2/app/styles/components/with-tooltip/layout.scss index 773cbb1e35..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 { diff --git a/ui-v2/app/styles/core/typography.scss b/ui-v2/app/styles/core/typography.scss index 35792cc434..134d709094 100644 --- a/ui-v2/app/styles/core/typography.scss +++ b/ui-v2/app/styles/core/typography.scss @@ -1,6 +1,3 @@ -%button { - font-family: $typo-family-sans; -} main p, %modal-window p { margin-bottom: 1em; @@ -24,33 +21,42 @@ 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, +fieldset > header, +caption, %header-nav, %healthchecked-resource header span, %healthcheck-output dt, %copy-button, %app-content div > dl > dt, -td:first-child 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; } +main label a[rel*='help'], +%pill, +%tbody-th em, +%form-element > strong, +%healthchecked-resource strong, +%app-view h1 em { + font-weight: $typo-weight-normal; +} th, +td strong, %breadcrumbs li > *, %action-group-action, %tab-nav, @@ -58,20 +64,16 @@ th, %type-icon { font-weight: $typo-weight-medium; } -main label a[rel*='help'], -td:first-child em, -%pill, -%form-element > strong, -%healthchecked-resource strong, -%app-view h1 em { - font-weight: $typo-weight-normal; -} + +/* Styling */ %form-element > em, -td:first-child em, +%tbody-th em, %healthchecked-resource header em, %app-view h1 em { font-style: normal; } + +/* Sizing */ %footer > * { font-size: inherit; } @@ -79,20 +81,23 @@ h1 { font-size: $typo-header-100; } h2, +%healthcheck-info dt, %header-drop-nav .is-active, %app-view h1 em { font-size: $typo-size-500; } body, -%action-group-action, fieldset h2, +fieldset > header, pre code, input, textarea, -td { +%action-group-action, +%tbody-th { font-size: $typo-size-600; } th, +td, caption, .type-dialog, %form-element > span, @@ -105,14 +110,15 @@ caption, %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..99270f734e 100644 --- a/ui-v2/app/styles/routes/dc/acls/index.scss +++ b/ui-v2/app/styles/routes/dc/acls/index.scss @@ -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 dbd72fdd6f..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,11 +1,9 @@ -.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: $blue-500; @@ -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..de3b376ceb 100644 --- a/ui-v2/app/styles/routes/dc/nodes/index.scss +++ b/ui-v2/app/styles/routes/dc/nodes/index.scss @@ -1,5 +1,6 @@ -// 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; } diff --git a/ui-v2/app/styles/variables/index.scss b/ui-v2/app/styles/variables/index.scss index a5f40df0c7..6689805bbc 100644 --- a/ui-v2/app/styles/variables/index.scss +++ b/ui-v2/app/styles/variables/index.scss @@ -4,11 +4,6 @@ $gray-025: #fafbfc; $magenta-800-no-hash: 5a1434; -$keyline-light: $gray-100; // h1 -$keyline-mid: $gray-200; // td, footer -$keyline-dark: $gray-300; // th -$keyline-darker: $gray-400; - // decoration // undecided $radius-small: $decor-radius-100; 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/healthcheck-info.hbs b/ui-v2/app/templates/components/healthcheck-info.hbs index 13b62ac08c..982d4c2c86 100644 --- a/ui-v2/app/templates/components/healthcheck-info.hbs +++ b/ui-v2/app/templates/components/healthcheck-info.hbs @@ -1,7 +1,7 @@ {{#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}} 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 @@ {{!