UI: ACL Roles (#5635)

Adds support for ACL Roles and Service Identities CRUD, along with necessary changes to Tokens, and the CSS improvements required.

Also includes refinements/improvements for easier testing of deeply nested components.

1. ember-data adapter/serializer/model triplet for Roles
2. repository, form/validations and searching filter for Roles
3. Moves potentially, repeated, or soon to to repeated functionality
into a mixin (mainly for 'many policy' relationships)
4. A few styling tweaks for little edge cases around roles
5. Router additions, Route, Controller and templates for Roles

Also see: 

* UI: ACL Roles cont. plus Service Identities (#5661 and #5720)
This commit is contained in:
John Cowen 2019-05-01 19:09:29 +01:00 committed by John Cowen
parent 08c5b376e7
commit 81f209d71e
208 changed files with 4143 additions and 946 deletions

View file

@ -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;
},
});

View file

@ -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:

View file

@ -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:
}
},
},
});

View file

@ -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);
},
},
});

View file

@ -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;
}
},
},
});

View file

@ -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() {

View file

@ -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') });
}
},
},
});

View file

@ -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);
}
},
},
});

View file

@ -0,0 +1,6 @@
import FormComponent from './form-component';
export default FormComponent.extend({
type: 'role',
name: 'role',
classNames: ['role-form'],
});

View file

@ -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);
}
},
},
});

View file

@ -0,0 +1,5 @@
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});

View file

@ -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) {

View file

@ -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);
},
},
});

View file

@ -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;
}
}
},
},
});

View file

@ -0,0 +1,2 @@
import Controller from './edit';
export default Controller.extend();

View file

@ -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)
);
},
});

View file

@ -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: {},
});

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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',

8
ui-v2/app/forms/role.js Normal file
View file

@ -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'));
}

View file

@ -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'));
}

View file

@ -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);

View file

@ -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);

View file

@ -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;
},
});
}

View file

@ -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,
};

View file

@ -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),

View file

@ -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') {

View file

@ -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;
},
});

View file

@ -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;
},
});

View file

@ -0,0 +1,4 @@
import Mixin from '@ember/object/mixin';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default Mixin.create(WithBlockingActions, {});

View file

@ -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;

34
ui-v2/app/models/role.js Normal file
View file

@ -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'),
});

View file

@ -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',

View file

@ -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: {

View file

@ -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':

View file

@ -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',
});

View file

@ -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);
},
});

View file

@ -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);
},
});

View file

@ -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;
}
});
},
},
});

View file

@ -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

View file

@ -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
);
});
}

View file

@ -0,0 +1,5 @@
import Serializer from './application';
import { PRIMARY_KEY } from 'consul-ui/models/role';
export default Serializer.extend({
primaryKey: PRIMARY_KEY,
});

View file

@ -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() {},
});

View file

@ -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() {

View file

@ -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'));
},

View file

@ -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);
},
});

View file

@ -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,
});
},
});

View file

@ -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 ? {} : []
);
};
},
});

View file

@ -1,2 +1,9 @@
import Service from '@ember/service';
export default Service.extend({});
export default Service.extend({
searchable: function() {
return {
addEventListener: function() {},
removeEventListener: function() {},
};
},
});

View file

@ -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';

View file

@ -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;

View file

@ -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;
}

View file

@ -73,10 +73,11 @@ $queue-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xm
$refresh-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="%23000"/></svg>');
$run-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" class="structure-icon-run"><style>.structure-icon-run {animation: structure-icon-run-simple-spin 1s infinite linear;}.structure-icon-run-progress {animation: structure-icon-run-fancy-spin 3s infinite linear;fill: transparent;opacity: 0.66;stroke-dasharray: 16 16;transform-origin: 50% 50%;}@keyframes structure-icon-run-fancy-spin {0% {stroke-dasharray: 4 32;}50% {stroke-dasharray: 24 8;}50% {stroke-dasharray: 4 32;}50% {stroke-dasharray: 24 8;}100% {stroke-dasharray: 4 32;}}@keyframes structure-icon-run-simple-spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}</style><g fill="none" fill-rule="evenodd"><circle cx="12" cy="12" r="8" stroke="%23000" stroke-width="4"/><circle cx="12" cy="12" r="5" stroke="currentColor" stroke-width="2" class="structure-icon-run-progress"/><circle cx="12" cy="12" r="4" fill="currentColor"/></g></svg>');
$search-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" fill="%23000"/></svg>');
$service-identity-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M6.5 13a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm11-3a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm-4 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7z" id="a"/></defs><use fill="%239E2159" xlink:href="%23a" fill-rule="evenodd"/></svg>');
$settings-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65A.488.488 0 0 0 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z" fill="%23000"/></svg>');
$star-fill-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" fill="%23000"/></svg>');
$star-outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z" fill="%23000"/></svg>');
$star-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="10" height="9" viewBox="0 0 10 9" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M5 7.196L7.575 8.75l-.683-2.93 2.275-1.97-2.996-.254L5 .833 3.83 3.596.832 3.85l2.275 1.97-.683 2.93z"/></defs><use fill="%23FAC402" xlink:href="%23a" fill-rule="evenodd"/></svg>');
$star-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="10" height="9" viewBox="0 0 10 9" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M5 7.196L7.575 8.75l-.683-2.93 2.275-1.97-2.996-.254L5 .833 3.83 3.596.832 3.85l2.275 1.97-.683 2.93z"/></defs><use fill="%239E2159" xlink:href="%23a" fill-rule="evenodd"/></svg>');
$sub-arrow-left-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M11 9l1.42 1.42L8.83 14H18V4h2v12H8.83l3.59 3.58L11 21l-6-6z" fill="%23000"/></svg>');
$sub-arrow-right-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 15l-6 6-1.42-1.42L15.17 16H4V4h2v10h9.17l-3.59-3.58L13 9z" fill="%23000"/></svg>');
$swap-horizontal-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z" fill="%23000"/></svg>');

View file

@ -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;
}

View file

@ -1,2 +1,3 @@
@import './base-variables';
@import './base-placeholders';
@import './icon-placeholders';

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -29,3 +29,6 @@
content: '';
display: block;
}
%code-editor > pre {
display: none;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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,<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><circle stroke="%23BBC4D1" fill="%23FFF" cx="7" cy="7" r="7"/><path fill="%236A7786" d="M6.15 4.677V3.233h1.56v1.444zM6.15 11.374V6.35h1.56v5.023z"/></svg>');
background-color: $color-transparent;
width: 16px;
height: 16px;
}
/*TODO: All chevrons need merging */
%with-chevron-down::before {
@extend %pseudo-icon-bg-img;

View file

@ -5,7 +5,7 @@
overflow: hidden;
}
%modal-dialog {
z-index: 10000;
z-index: 500;
position: fixed;
left: 0;
top: 0;

View file

@ -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;
}

View file

@ -1,5 +1,4 @@
%pill {
@extend %frame-gray-900;
border-radius: $radius-small;
}
%pill button {

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -31,6 +31,43 @@ table.dom-recycling {
/* using: */
/* calc(<100% divided by number of non-fixed width cells> - <sum of widths of fixed cells 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);
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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;
}

View file

@ -3,6 +3,7 @@
display: inline-flex;
justify-content: center;
align-items: center;
vertical-align: text-top;
}
%tooltip-bubble,
%tooltip-tail {

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -0,0 +1,21 @@
{{yield}}
{{#yield-slot 'create'}}{{yield}}{{/yield-slot}}
<label class="type-text">
<span>{{#yield-slot 'label'}}{{yield}}{{/yield-slot}}</span>
{{#power-select
onopen=(action 'open')
search=(action 'search')
options=options
loadingMessage="Loading..."
searchMessage="No possible options"
searchPlaceholder=placeholder
onchange=(action 'change' 'items[]' items) as |item|
}}
{{#yield-slot 'option' (block-params item)}}{{yield}}{{/yield-slot}}
{{/power-select}}
</label>
{{#if (gt items.length 0)}}
{{#yield-slot 'set'}}{{yield}}{{/yield-slot}}
{{else}}
{{/if}}

View file

@ -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}}
<pre><code>{{yield}}</code></pre>
{{#if (and (not readonly) (not syntax))}}
{{#power-select
onchange=(action 'change')
selected=mode
searchEnabled=false
options=modes as |mode|
}}
{{mode.name}}
{{/power-select}}
{{/if}}

View file

@ -0,0 +1 @@
{{yield}}

View file

@ -1,7 +1,7 @@
{{#if (and (lt passing 1) (lt warning 1) (lt critical 1) )}}
<span title="No Healthchecks" class="zero">0</span>
{{else}}
<dl>
<dl class="healthcheck-info">
{{healthcheck-status width=passingWidth name='passing' value=passing}}
{{healthcheck-status width=warningWidth name='warning' value=warning}}
{{healthcheck-status width=criticalWidth name='critical' value=critical}}

View file

@ -0,0 +1,76 @@
{{yield}}
<fieldset>
{{#yield-slot 'template'}}
{{else}}
<header>
Policy or service identity?
</header>
<p>
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.
</p>
{{! this should use radio-group }}
<div role="radiogroup" class={{if item.error.Type ' has-error'}}>
{{#each templates as |template|}}
<label>
<span>{{template.name}}</span>
<input data-test-radiobutton={{concat 'template_' template.template}} type="radio" name="{{name}}[template]" value={{template.template}} checked={{eq item.template template.template}} onchange={{action (changeset-set item 'template') value='target.value'}}/>
</label>
{{/each}}
</div>
{{/yield-slot}}
<label class="type-text{{if item.error.Name ' has-error'}}">
<span>Name</span>
<input type="text" value={{item.Name}} name="{{name}}[Name]" autofocus="autofocus" oninput={{action 'change'}} />
<em>
Maximum 128 characters. May only include letters (uppercase and/or lowercase) and/or numbers. Must be unique.
</em>
{{#if item.error.Name}}
<strong>{{item.error.Name.validation}}</strong>
{{/if}}
</label>
<label class="type-text">
<span>Rules <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span>
{{#if (eq item.template '') }}
{{code-editor syntax='hcl' class=(if item.error.Rules 'error') name=(concat name '[Rules]') value=item.Rules onkeyup=(action 'change' (concat name '[Rules]'))}}
{{#if item.error.Rules}}
<strong>{{item.error.Rules.validation}}</strong>
{{/if}}
{{else}}
{{#code-editor name=(concat name '[Rules]') syntax='hcl' oninput=(action 'change' (concat name '[Rules]'))}}
{{~component 'service-identity' name=item.Name~}}
{{/code-editor}}
{{/if}}
</label>
<div class="type-toggle">
<span>Valid datacenters</span>
<label>
<input type="checkbox" name="{{name}}[isScoped]" checked={{if (not isScoped) 'checked' }} onchange={{action 'change'}} />
<span>All</span>
</label>
</div>
{{#if isScoped }}
<div class="checkbox-group" role="group">
{{#each datacenters as |dc| }}
<label class="type-checkbox">
<input type="checkbox" name="{{name}}[Datacenters]" value={{dc.Name}} checked={{if (contains dc.Name item.Datacenters) 'checked' }} onchange={{action 'change'}} />
<span>{{dc.Name}}</span>
</label>
{{/each}}
{{#each item.Datacenters as |dc| }}
{{#if (not (find-by 'Name' dc datacenters))}}
<label class="type-checkbox">
<input type="checkbox" name="{{name}}[Datacenters]" value={{dc}} checked="checked" onchange={{action 'change'}} />
<span>{{dc}}</span>
</label>
{{/if}}
{{/each}}
</div>
{{/if}}
{{#if (eq item.template '') }}
<label class="type-text">
<span>Description (Optional)</span>
<textarea name="{{name}}[Description]" value={{item.Description}} oninput={{action 'change'}}></textarea>
</label>
{{/if}}
</fieldset>

View file

@ -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}}
<label class="type-dialog" for="new-policy-toggle">
<span>Create new policy</span>
</label>
{{!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'}}
<h2>New Policy</h2>
{{/block-slot}}
{{#block-slot 'body'}}
{{policy-form form=form dc=dc}}
{{/block-slot}}
{{#block-slot 'actions' as |close|}}
<button type="submit" {{action 'save' item items (queue (action close) (action 'reset'))}} disabled={{if (or item.isSaving item.isPristine item.isInvalid) 'disabled'}}>
{{#if item.isSaving }}
<div class="progress indeterminate"></div>
{{/if}}
<span>Create and apply</span>
</button>
<button type="reset" disabled={{if item.isSaving 'disabled'}} {{action (queue (action close) (action 'reset'))}}>Cancel</button>
{{/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'}}
<th>Name</th>
<th>Datacenters</th>
{{/block-slot}}
{{#block-slot 'row'}}
<td class={{policy/typeof item}}>
{{#if item.ID }}
<a href={{href-to 'dc.acls.policies.edit' item.ID}}>{{item.Name}}</a>
{{else}}
<a name={{item.Name}}>{{item.Name}}</a>
{{/if}}
</td>
<td>
{{if (not item.isSaving) (join ', ' (policy/datacenters item)) 'Saving...'}}
</td>
{{/block-slot}}
{{#block-slot 'details'}}
<label class="type-text">
<span>Rules <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span>
{{#if (eq item.template 'default')}}
{{code-editor syntax='hcl' readonly=true value=item.Rules}}
{{else}}
{{#code-editor syntax='hcl' readonly=true}}
{{~component 'service-identity' name=item.Name~}}
{{/code-editor}}
{{/if}}
</label>
<div>
{{#confirmation-dialog message='Are you sure you want to remove this policy from this token?'}}
{{#block-slot 'action' as |confirm|}}
<button data-test-delete type="button" class="type-delete" {{action confirm 'remove' item items}}>Remove</button>
{{/block-slot}}
{{#block-slot 'dialog' as |execute cancel message|}}
<p>
{{message}}
</p>
<button type="button" class="type-delete" {{action execute}}>Confirm remove</button>
<button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
{{/block-slot}}
{{/confirmation-dialog}}
</div>
{{/block-slot}}
{{/tabular-details}}
{{/block-slot}}
{{/child-selector}}

View file

@ -0,0 +1,26 @@
{{yield}}
<fieldset>
<label class="type-text{{if item.error.Name ' has-error'}}">
<span>Name</span>
<input type="text" value={{item.Name}} name="role[Name]" autofocus="autofocus" oninput={{action 'change'}} />
<em>
Maximum 256 characters. May only include letters (uppercase and/or lowercase) and/or numbers. Must be unique.
</em>
{{#if item.error.Name}}
<strong>{{item.error.Name.validation}}</strong>
{{/if}}
</label>
<label class="type-text">
<span>Description (Optional)</span>
<textarea name="role[Description]" value={{item.Description}} oninput={{action 'change'}}></textarea>
</label>
</fieldset>
{{!TODO: temporary policies id, look at the inception token modals and get rid of id="policies" and use something else}}
<fieldset id="policies" class="policies">
<h2>Policies</h2>
{{#yield-slot 'policy' (block-params item)}}
{{yield}}
{{else}}
{{policy-selector dc=dc items=item.Policies}}
{{/yield-slot}}
</fieldset>

View file

@ -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')}}
<h2>New Role</h2>
{{else}}
<h2>New Policy</h2>
{{/if}}
{{/block-slot}}
{{#block-slot 'body'}}
<input id="{{name}}_state_role" type="radio" name="{{name}}[state]" value="role" checked={{eq state 'role'}} onchange={{action 'change'}} />
{{#role-form form=form dc=dc}}
{{#block-slot 'policy'}}
{{#policy-selector source=source dc=dc items=item.Policies}}
{{#block-slot 'trigger'}}
<label for="{{name}}_state_policy" data-test-create-policy class="type-dialog">
<span>Create new policy</span>
</label>
{{/block-slot}}
{{/policy-selector}}
{{/block-slot}}
{{/role-form}}
<input id="{{name}}_state_policy" type="radio" name="{{name}}[state]" value="policy" checked={{eq state 'policy'}} onchange={{action 'change'}} />
{{policy-form data-test-policy-form name="role[policy]" form=policyForm dc=dc}}
{{/block-slot}}
{{#block-slot 'actions' as |close|}}
{{#if (eq state 'role')}}
<button type="submit" {{action 'save' item items (queue (action close) (action 'reset'))}} disabled={{if (or item.isSaving item.isPristine item.isInvalid) 'disabled'}}>
{{#if item.isSaving }}
<div class="progress indeterminate"></div>
{{/if}}
<span>Create and apply</span>
</button>
<button type="reset" disabled={{if item.isSaving 'disabled'}} {{action (queue (action close) (action 'reset'))}}>Cancel</button>
{{else}}
<button type="submit" {{action 'dispatch' 'save' (array policy item.Policies (action (mut state) 'role'))}} disabled={{if (or policy.isSaving policy.isPristine policy.isInvalid) 'disabled'}}>
{{#if policy.isSaving }}
<div class="progress indeterminate"></div>
{{/if}}
<span>Create and apply</span>
</button>
<button type="reset" disabled={{if policy.isSaving 'disabled'}} {{action (mut state) 'role'}}>Cancel</button>
{{/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'}}
<label class="type-dialog" for="new-role-toggle">
<span>Create new role</span>
</label>
{{/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'}}
<th>Name</th>
<th>Description</th>
{{/block-slot}}
{{#block-slot 'row'}}
<td>
<a href={{href-to 'dc.acls.roles.edit' item.ID}}>{{item.Name}}</a>
</td>
<td>
{{item.Description}}
</td>
{{/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')}}
<ul>
<li>
<a data-test-edit href={{href-to 'dc.acls.roles.edit' item.ID}}>Edit</a>
</li>
<li>
<button type="button" class="type-delete" data-test-delete {{action confirm 'remove' item items}}>Remove</button>
</li>
</ul>
{{/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}}

View file

@ -0,0 +1,12 @@
service "{{name}}" {
policy = "write"
}
service "{{name}}-sidecar-proxy" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
node_prefix "" {
policy = "read"
}

View file

@ -1,7 +1,7 @@
{{!<nav role="tablist">}}
<ul>
{{#each items as |item|}}
<li class={{if (eq selected (if item.label (slugify item.label) (slugify item))) 'selected'}}>
<li class={{if (or item.selected (eq selected (if item.label (slugify item.label) (slugify item)))) 'selected'}}>
<label for="radiogroup_{{name}}_{{if item.label (slugify item.label) (slugify item)}}" data-test-radiobutton="{{name}}_{{if item.label (slugify item.label) (slugify item)}}" role="tab" aria-controls="radiogroup_{{name}}_{{if item.label (slugify item.label) (slugify item)}}_panel">
{{#if item.href }}
<a href="{{ item.href }}">{{ item.label }}</a>

View file

@ -12,12 +12,12 @@
</thead>
{{#ember-native-scrollable tagName='tbody' content-size=_contentSize scroll-left=_scrollLeft scroll-top=_scrollTop scrollChange=(action "scrollChange") clientSizeChange=(action "clientSizeChange")}}
<tr></tr>
{{~#each _cells as |cell|~}}
{{~#each _cells as |cell index|~}}
<tr data-test-tabular-row style={{{cell.style}}} onclick={{action 'click'}}>
{{#yield-slot 'row'}}{{yield cell.item cell.index}}{{/yield-slot}}
{{#yield-slot 'row'}}{{yield cell.item index}}{{/yield-slot}}
{{#if hasActions }}
<td class="actions">
{{#yield-slot 'actions' (block-params (concat cell.index) change checked)}}{{yield cell.item cell.index}}{{/yield-slot}}
{{#yield-slot 'actions' (block-params (concat index) change checked)}}{{yield cell.item index}}{{/yield-slot}}
</td>
{{/if}}
</tr>

View file

@ -1,5 +1,5 @@
{{yield}}
<table class="with-details">
<table class="with-details has-actions">
<thead>
<tr>
{{#yield-slot 'header'}}{{yield}}{{/yield-slot}}
@ -16,7 +16,7 @@
</tr>
<tr>
<td colspan="3">
<input type="checkbox" value={{index}} name={{name}} id={{concat inputId index}} onchange={{action 'change' item}} />
<input type="checkbox" checked={{ not (is-empty item.closed) }} value={{index}} name={{name}} id={{concat inputId index}} onchange={{action 'change' item items}} />
<div>
<label for={{concat inputId index}}><span>Hide details</span></label>
<div>

View file

@ -2,6 +2,7 @@
{{#if (gt items.length 0)}}
{{#tabular-collection
data-test-tokens
class='token-list'
items=(sort-by 'AccessorID:asc' items) as |item index|
}}
{{#if caption}}

View file

@ -3,11 +3,17 @@
(hash
label='Tokens'
href=(href-to 'dc.acls.tokens')
selected=(is-href 'dc.acls.tokens')
)
(hash
label='Roles'
href=(href-to 'dc.acls.roles')
selected=(is-href 'dc.acls.roles')
)
(hash
label='Policies'
href=(href-to 'dc.acls.policies')
selected=(is-href 'dc.acls.policies')
)
)
selected=(if (is-href 'dc.acls.policies') 'policies' 'tokens')
}}

View file

@ -1,49 +0,0 @@
<fieldset>
<label class="type-text{{if item.error.Name ' has-error'}}">
<span>Name</span>
{{input value=item.Name name='policy[Name]' autofocus='autofocus'}}
<em>
Maximum 128 characters. May only include letters (uppercase and/or lowercase) and/or numbers. Must be unique.
</em>
{{#if item.error.Name}}
<strong>{{item.error.Name.validation}}</strong>
{{/if}}
</label>
<label class="type-text">
<span>Rules <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span>
{{code-editor id="policy_rules" syntax='hcl' class=(if item.error.Rules 'error') name='policy[Rules]' value=item.Rules onkeyup=(action 'change' 'policy[Rules]')}}
{{#if item.error.Rules}}
<strong>{{item.error.Rules.validation}}</strong>
{{/if}}
</label>
<div class="type-toggle">
<span>Valid datacenters</span>
<label>
<input type="checkbox" name="policy[isScoped]" checked={{if (not isScoped) 'checked' }} onchange={{action 'change'}} />
<span>All</span>
</label>
</div>
{{#if isScoped }}
<div class="checkbox-group" role="group">
{{#each datacenters as |dc| }}
<label class="type-checkbox">
<input type="checkbox" name="policy[Datacenters]" value={{dc.Name}} checked={{if (contains dc.Name item.Datacenters) 'checked' }} onchange={{action 'change'}} />
<span>{{dc.Name}}</span>
</label>
{{/each}}
{{#each item.Datacenters as |dc| }}
{{#if (not (find-by 'Name' dc datacenters))}}
<label class="type-checkbox">
<input type="checkbox" name="policy[Datacenters]" value={{dc}} checked="checked" onchange={{action 'change'}} />
<span>{{dc}}</span>
</label>
{{/if}}
{{/each}}
</div>
{{/if}}
<label class="type-text">
<span>Description (Optional)</span>
<textarea name="policy[Description]" value={{item.Description}} oninput={{action 'change'}}></textarea>
</label>
</fieldset>

View file

@ -1,5 +1,8 @@
<form>
{{partial 'dc/acls/policies/fieldsets'}}
{{#policy-form form=form item=item}}
{{!don't show template selection here, i.e. Service Identity}}
{{block-slot 'template'}}
{{/policy-form}}
{{#if (not create) }}
{{token-list caption="Applied to the following tokens:" items=items}}
{{/if}}
@ -18,13 +21,13 @@
{{/block-slot}}
{{#block-slot 'dialog' as |execute cancel message|}}
{{#if (gt items.length 0)}}
{{#modal-dialog onclose=(action cancel)}}
{{#modal-dialog data-test-delete-modal onclose=(action cancel)}}
{{#block-slot 'header'}}
<h2>Policy in Use</h2>
{{/block-slot}}
{{#block-slot 'body'}}
<p>
This Policy is currently in use. If you choose to delete this Policy, it will be removed from the following <strong>{{items.length}} Tokens</strong>:
This Policy is currently in use. If you choose to delete this Policy, it will be removed from the following <strong>{{items.length}} Tokens</strong>:
</p>
{{token-list items=items target='_blank'}}
<p>

Some files were not shown because too many files have changed in this diff Show more