mirror of https://github.com/status-im/consul.git
parent
54cc0820b4
commit
7d89e519a2
|
@ -19,12 +19,35 @@ You will need the following things properly installed on your computer.
|
|||
|
||||
## Running / Development
|
||||
|
||||
The source code comes with a small server that runs enough of the consul API
|
||||
as a set of mocks/fixtures to be able to run the UI without having to run
|
||||
consul.
|
||||
|
||||
* `make start-api` or `yarn start:api` (this starts a Consul API double running
|
||||
on http://localhost:3000)
|
||||
* `make start` or `yarn start` to start the ember app that connects to the
|
||||
above API double
|
||||
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
||||
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
|
||||
|
||||
To enable ACLs using the mock API, use Web Inspector to set a cookie as follows:
|
||||
|
||||
```
|
||||
CONSUL_ACLS_ENABLE=1
|
||||
```
|
||||
|
||||
This will enable the ACLs login page, to which you can login with any ACL
|
||||
token/secret.
|
||||
|
||||
You can also use a number of other cookie key/values to set various things whilst
|
||||
developing the UI, such as (but not limited to):
|
||||
|
||||
```
|
||||
CONSUL_SERVICE_COUNT=1000
|
||||
CONSUL_NODE_CODE=1000
|
||||
// etc etc
|
||||
```
|
||||
|
||||
See `./node_modules/@hashicorp/consul-api-double` for more details.
|
||||
|
||||
|
||||
### Code Generators
|
||||
|
@ -33,7 +56,7 @@ Make use of the many generators for code, try `ember help generate` for more det
|
|||
|
||||
### Running Tests
|
||||
|
||||
You do not need to run `make start-api`/`yarn run start:api` to run the tests
|
||||
Please note: You do not need to run `make start-api`/`yarn run start:api` to run the tests, but the same mock consul API is used.
|
||||
|
||||
* `make test` or `yarn run test`
|
||||
* `make test-view` or `yarn run test:view` to view the tests running in Chrome
|
||||
|
|
|
@ -48,7 +48,9 @@ export default Adapter.extend({
|
|||
});
|
||||
},
|
||||
cleanQuery: function(_query) {
|
||||
if (typeof _query.id !== 'undefined') {
|
||||
delete _query.id;
|
||||
}
|
||||
const query = { ..._query };
|
||||
delete _query[DATACENTER_QUERY_PARAM];
|
||||
return query;
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import Adapter, {
|
||||
REQUEST_CREATE,
|
||||
REQUEST_UPDATE,
|
||||
DATACENTER_QUERY_PARAM as API_DATACENTER_KEY,
|
||||
} from './application';
|
||||
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy';
|
||||
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';
|
||||
|
||||
export default Adapter.extend({
|
||||
urlForQuery: function(query, modelName) {
|
||||
return this.appendURL('acl/policies', [], this.cleanQuery(query));
|
||||
},
|
||||
urlForQueryRecord: function(query, modelName) {
|
||||
if (typeof query.id === 'undefined') {
|
||||
throw new Error('You must specify an id');
|
||||
}
|
||||
return this.appendURL('acl/policy', [query.id], this.cleanQuery(query));
|
||||
},
|
||||
urlForCreateRecord: function(modelName, snapshot) {
|
||||
return this.appendURL('acl/policy', [], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForUpdateRecord: function(id, modelName, snapshot) {
|
||||
return this.appendURL('acl/policy', [snapshot.attr(SLUG_KEY)], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForDeleteRecord: function(id, modelName, snapshot) {
|
||||
return this.appendURL('acl/policy', [snapshot.attr(SLUG_KEY)], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForTranslateRecord: function(modelName, snapshot) {
|
||||
return this.appendURL('acl/policy/translate', [], {});
|
||||
},
|
||||
dataForRequest: function(params) {
|
||||
const data = this._super(...arguments);
|
||||
switch (params.requestType) {
|
||||
case REQUEST_UPDATE:
|
||||
case REQUEST_CREATE:
|
||||
return data.policy;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
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);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,199 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import Adapter, {
|
||||
REQUEST_CREATE,
|
||||
REQUEST_UPDATE,
|
||||
DATACENTER_QUERY_PARAM as API_DATACENTER_KEY,
|
||||
} from './application';
|
||||
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token';
|
||||
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 { get } from '@ember/object';
|
||||
|
||||
const REQUEST_CLONE = 'cloneRecord';
|
||||
const REQUEST_SELF = 'querySelf';
|
||||
|
||||
export default Adapter.extend({
|
||||
store: service('store'),
|
||||
cleanQuery: function(_query) {
|
||||
const query = this._super(...arguments);
|
||||
// TODO: Make sure policy is being passed through
|
||||
delete _query.policy;
|
||||
// take off the secret for /self
|
||||
delete query.secret;
|
||||
return query;
|
||||
},
|
||||
urlForQuery: function(query, modelName) {
|
||||
return this.appendURL('acl/tokens', [], this.cleanQuery(query));
|
||||
},
|
||||
urlForQueryRecord: function(query, modelName) {
|
||||
if (typeof query.id === 'undefined') {
|
||||
throw new Error('You must specify an id');
|
||||
}
|
||||
return this.appendURL('acl/token', [query.id], this.cleanQuery(query));
|
||||
},
|
||||
urlForQuerySelf: function(query, modelName) {
|
||||
return this.appendURL('acl/token/self', [], this.cleanQuery(query));
|
||||
},
|
||||
urlForCreateRecord: function(modelName, snapshot) {
|
||||
return this.appendURL('acl/token', [], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForUpdateRecord: function(id, modelName, snapshot) {
|
||||
// If a token has Rules, use the old API
|
||||
if (typeof snapshot.attr('Rules') !== 'undefined') {
|
||||
return this.appendURL('acl/update', [], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
}
|
||||
return this.appendURL('acl/token', [snapshot.attr(SLUG_KEY)], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForDeleteRecord: function(id, modelName, snapshot) {
|
||||
return this.appendURL('acl/token', [snapshot.attr(SLUG_KEY)], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForRequest: function({ type, snapshot, requestType }) {
|
||||
switch (requestType) {
|
||||
case 'cloneRecord':
|
||||
return this.urlForCloneRecord(type.modelName, snapshot);
|
||||
case 'querySelf':
|
||||
return this.urlForQuerySelf(snapshot, type.modelName);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
urlForCloneRecord: function(modelName, snapshot) {
|
||||
return this.appendURL('acl/token', [snapshot.attr(SLUG_KEY), 'clone'], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
self: function(store, modelClass, snapshot) {
|
||||
const params = {
|
||||
store: store,
|
||||
type: modelClass,
|
||||
snapshot: snapshot,
|
||||
requestType: 'querySelf',
|
||||
};
|
||||
// _requestFor is private... but these methods aren't, until they disappear..
|
||||
const request = {
|
||||
method: this.methodForRequest(params),
|
||||
url: this.urlForRequest(params),
|
||||
headers: this.headersForRequest(params),
|
||||
data: this.dataForRequest(params),
|
||||
};
|
||||
// TODO: private..
|
||||
return this._makeRequest(request);
|
||||
},
|
||||
clone: function(store, modelClass, id, snapshot) {
|
||||
const params = {
|
||||
store: store,
|
||||
type: modelClass,
|
||||
id: id,
|
||||
snapshot: snapshot,
|
||||
requestType: 'cloneRecord',
|
||||
};
|
||||
// _requestFor is private... but these methods aren't, until they disappear..
|
||||
const request = {
|
||||
method: this.methodForRequest(params),
|
||||
url: this.urlForRequest(params),
|
||||
headers: this.headersForRequest(params),
|
||||
data: this.dataForRequest(params),
|
||||
};
|
||||
// TODO: private..
|
||||
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')
|
||||
.peekAll('token')
|
||||
.findBy('SecretID', response['ID']);
|
||||
if (item) {
|
||||
response['SecretID'] = response['ID'];
|
||||
response['AccessorID'] = get(item, 'AccessorID');
|
||||
}
|
||||
}
|
||||
return this._super(url, response, primary, slug);
|
||||
},
|
||||
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_CLONE:
|
||||
case REQUEST_CREATE:
|
||||
return HTTP_PUT;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
headersForRequest: function(params) {
|
||||
switch (params.requestType) {
|
||||
case REQUEST_SELF:
|
||||
return {
|
||||
'X-Consul-Token': params.snapshot.secret,
|
||||
};
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
dataForRequest: function(params) {
|
||||
let data = this._super(...arguments);
|
||||
switch (params.requestType) {
|
||||
case REQUEST_UPDATE:
|
||||
// If a token has Rules, use the old API
|
||||
if (typeof data.token['Rules'] !== 'undefined') {
|
||||
data.token['ID'] = data.token['SecretID'];
|
||||
data.token['Name'] = data.token['Description'];
|
||||
}
|
||||
// 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:
|
||||
return {};
|
||||
case REQUEST_CLONE:
|
||||
data = {};
|
||||
break;
|
||||
}
|
||||
// make sure we never send the SecretID
|
||||
if (data && typeof data['SecretID'] !== 'undefined') {
|
||||
delete data['SecretID'];
|
||||
}
|
||||
return data;
|
||||
},
|
||||
});
|
|
@ -1,14 +1,16 @@
|
|||
import Component from '@ember/component';
|
||||
import SlotsMixin from 'ember-block-slots';
|
||||
import { get } from '@ember/object';
|
||||
import templatize from 'consul-ui/utils/templatize';
|
||||
const $html = document.documentElement;
|
||||
const templatize = function(arr = []) {
|
||||
return arr.map(item => `template-${item}`);
|
||||
};
|
||||
export default Component.extend(SlotsMixin, {
|
||||
loading: false,
|
||||
authorized: true,
|
||||
enabled: true,
|
||||
classNames: ['app-view'],
|
||||
classNameBindings: ['enabled::disabled', 'authorized::unauthorized'],
|
||||
didReceiveAttrs: function() {
|
||||
// right now only manually added classes are hoisted to <html>
|
||||
let cls = get(this, 'class') || '';
|
||||
if (get(this, 'loading')) {
|
||||
cls += ' loading';
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
|
||||
const $$ = qsaFactory();
|
||||
export default Component.extend({
|
||||
mode: 'application/json',
|
||||
classNames: ['code-editor'],
|
||||
onkeyup: function() {},
|
||||
didAppear: function() {
|
||||
const $editor = [...$$('textarea + div', this.element)][0];
|
||||
$editor.CodeMirror.refresh();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({});
|
|
@ -0,0 +1,7 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
execute: function() {},
|
||||
cancel: function() {},
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { get } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
const append = function(content) {
|
||||
this.element.appendChild(content);
|
||||
};
|
||||
export default Component.extend({
|
||||
buffer: service('dom-buffer'),
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.append = append.bind(this);
|
||||
},
|
||||
didInsertElement: function() {
|
||||
get(this, 'buffer').on('add', this.append);
|
||||
},
|
||||
didDestroyElement: function() {
|
||||
get(this, 'buffer').off('add', this.append);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { get } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
export default Component.extend({
|
||||
buffer: service('dom-buffer'),
|
||||
getBufferName: function() {
|
||||
// TODO: Right now we are only using this for the modal layer
|
||||
// moving forwards you'll be able to name your buffers
|
||||
return 'modal';
|
||||
},
|
||||
didInsertElement: function() {
|
||||
get(this, 'buffer').add(this.getBufferName(), this.element);
|
||||
},
|
||||
didDestroyElement: function() {
|
||||
get(this, 'buffer').remove(this.getBufferName());
|
||||
},
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import Component from '@ember/component';
|
||||
import { get, set } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import qsaFactory from 'consul-ui/utils/qsa-factory';
|
||||
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
|
||||
const $$ = qsaFactory();
|
||||
|
||||
import SlotsMixin from 'ember-block-slots';
|
||||
|
|
|
@ -3,7 +3,7 @@ import Component from 'ember-collection/components/ember-collection';
|
|||
import PercentageColumns from 'ember-collection/layouts/percentage-columns';
|
||||
import style from 'ember-computed-style';
|
||||
import WithResizing from 'consul-ui/mixins/with-resizing';
|
||||
import qsaFactory from 'consul-ui/utils/qsa-factory';
|
||||
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
|
||||
const $$ = qsaFactory();
|
||||
export default Component.extend(WithResizing, {
|
||||
tagName: 'div',
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
import { get, set } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Component from 'consul-ui/components/dom-buffer';
|
||||
import SlotsMixin from 'ember-block-slots';
|
||||
import WithResizing from 'consul-ui/mixins/with-resizing';
|
||||
|
||||
import templatize from 'consul-ui/utils/templatize';
|
||||
export default Component.extend(SlotsMixin, WithResizing, {
|
||||
dom: service('dom'),
|
||||
checked: true,
|
||||
height: null,
|
||||
// dialog is a reference to the modal-dialog 'panel' so its 'window'
|
||||
dialog: null,
|
||||
overflowingClass: 'overflowing',
|
||||
onclose: function() {},
|
||||
onopen: function() {},
|
||||
_open: function(e) {
|
||||
set(this, 'checked', true);
|
||||
if (get(this, 'height') === null) {
|
||||
if (this.element) {
|
||||
const dialogPanel = get(this, 'dom').element('[role="dialog"] > div > div', this.element);
|
||||
const rect = dialogPanel.getBoundingClientRect();
|
||||
set(this, 'dialog', dialogPanel);
|
||||
set(this, 'height', rect.height);
|
||||
}
|
||||
}
|
||||
this.didAppear();
|
||||
this.onopen(e);
|
||||
},
|
||||
didAppear: function() {
|
||||
this._super(...arguments);
|
||||
if (get(this, 'checked')) {
|
||||
get(this, 'dom')
|
||||
.root()
|
||||
.classList.add(...templatize(['with-modal']));
|
||||
}
|
||||
},
|
||||
_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);
|
||||
}
|
||||
// TODO: should we make a didDisappear?
|
||||
get(this, 'dom')
|
||||
.root()
|
||||
.classList.remove(...templatize(['with-modal']));
|
||||
this.onclose(e);
|
||||
},
|
||||
didReceiveAttrs: function() {
|
||||
this._super(...arguments);
|
||||
// TODO: Why does setting name mean checked it false?
|
||||
// It's because if it has a name then it is likely to be linked
|
||||
// to HTML state rather than just being added via HTMLBars
|
||||
// and therefore likely to be immediately on the page
|
||||
// It's not our usecase just yet, but this should check the state
|
||||
// of the thing its linked to, incase that has a `checked` of true
|
||||
// right now we know ours is always false.
|
||||
if (get(this, 'name')) {
|
||||
set(this, 'checked', false);
|
||||
}
|
||||
if (this.element) {
|
||||
if (get(this, 'checked')) {
|
||||
// TODO: probably need an event here
|
||||
// possibly this.element for the target
|
||||
// or find the input
|
||||
this._open({ target: {} });
|
||||
}
|
||||
}
|
||||
},
|
||||
didInsertElement: function() {
|
||||
this._super(...arguments);
|
||||
if (get(this, 'checked')) {
|
||||
// TODO: probably need an event here
|
||||
// possibly this.element for the target
|
||||
// or find the input
|
||||
this._open({ target: {} });
|
||||
}
|
||||
},
|
||||
didDestroyElement: function() {
|
||||
this._super(...arguments);
|
||||
get(this, 'dom')
|
||||
.root()
|
||||
.classList.remove(...templatize(['with-modal']));
|
||||
},
|
||||
resize: function(e) {
|
||||
if (get(this, 'checked')) {
|
||||
const height = get(this, 'height');
|
||||
if (height !== null) {
|
||||
const dialogPanel = get(this, 'dialog');
|
||||
const overflowing = get(this, 'overflowingClass');
|
||||
if (height > e.detail.height) {
|
||||
if (!dialogPanel.classList.contains(overflowing)) {
|
||||
dialogPanel.classList.add(overflowing);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
if (dialogPanel.classList.contains(overflowing)) {
|
||||
dialogPanel.classList.remove(overflowing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
change: function(e) {
|
||||
if (e && e.target && e.target.checked) {
|
||||
this._open(e);
|
||||
} else {
|
||||
this._close();
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
get(this, 'dom').element('#modal_close').checked = true;
|
||||
this.onclose();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import Component from 'consul-ui/components/dom-buffer-flush';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
dom: service('dom'),
|
||||
actions: {
|
||||
change: function(e) {
|
||||
[...get(this, 'dom').elements('[name="modal"]')]
|
||||
.filter(function(item) {
|
||||
return item.getAttribute('id') !== 'modal_close';
|
||||
})
|
||||
.forEach(function(item) {
|
||||
item.onchange();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({});
|
|
@ -5,7 +5,11 @@ import Grid from 'ember-collection/layouts/grid';
|
|||
import SlotsMixin from 'ember-block-slots';
|
||||
import WithResizing from 'consul-ui/mixins/with-resizing';
|
||||
import style from 'ember-computed-style';
|
||||
import qsaFactory from 'consul-ui/utils/qsa-factory';
|
||||
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
|
||||
import sibling from 'consul-ui/utils/dom/sibling';
|
||||
import closest from 'consul-ui/utils/dom/closest';
|
||||
import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor';
|
||||
const clickFirstAnchor = clickFirstAnchorFactory(closest);
|
||||
|
||||
import { computed, get, set } from '@ember/object';
|
||||
/**
|
||||
|
@ -53,26 +57,6 @@ class ZIndexedGrid extends Grid {
|
|||
return style;
|
||||
}
|
||||
}
|
||||
// basic DOM closest utility to cope with no support
|
||||
// TODO: instead of degrading gracefully
|
||||
// add a while polyfill for closest
|
||||
const closest = function(sel, el) {
|
||||
try {
|
||||
return el.closest(sel);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
const sibling = function(el, name) {
|
||||
let sibling = el;
|
||||
while ((sibling = sibling.nextSibling)) {
|
||||
if (sibling.nodeType === 1) {
|
||||
if (sibling.nodeName.toLowerCase() === name) {
|
||||
return sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* The tabular-collection can contain 'actions' the UI for which
|
||||
* uses dropdown 'action groups', so a group of different actions.
|
||||
|
@ -131,11 +115,13 @@ const change = function(e) {
|
|||
};
|
||||
export default Component.extend(SlotsMixin, WithResizing, {
|
||||
tagName: 'table',
|
||||
classNames: ['dom-recycling'],
|
||||
attributeBindings: ['style'],
|
||||
width: 1150,
|
||||
height: 500,
|
||||
style: style('getStyle'),
|
||||
checked: null,
|
||||
hasCaption: false,
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.change = change.bind(this);
|
||||
|
@ -149,12 +135,13 @@ export default Component.extend(SlotsMixin, WithResizing, {
|
|||
};
|
||||
}),
|
||||
resize: function(e) {
|
||||
const $tbody = [...$$('tbody', this.element)][0];
|
||||
const $tbody = this.element;
|
||||
const $appContent = [...$$('main > div')][0];
|
||||
if ($appContent) {
|
||||
const border = 1;
|
||||
const rect = $tbody.getBoundingClientRect();
|
||||
const $footer = [...$$('footer[role="contentinfo"]')][0];
|
||||
const space = rect.top + $footer.clientHeight;
|
||||
const space = rect.top + $footer.clientHeight + border;
|
||||
const height = e.detail.height - space;
|
||||
this.set('height', Math.max(0, height));
|
||||
// TODO: The row height should auto calculate properly from the CSS
|
||||
|
@ -165,7 +152,8 @@ export default Component.extend(SlotsMixin, WithResizing, {
|
|||
},
|
||||
willRender: function() {
|
||||
this._super(...arguments);
|
||||
this.set('hasActions', this._isRegistered('actions'));
|
||||
set(this, 'hasCaption', this._isRegistered('caption'));
|
||||
set(this, 'hasActions', this._isRegistered('actions'));
|
||||
},
|
||||
// `ember-collection` bug workaround
|
||||
// https://github.com/emberjs/ember-collection/issues/138
|
||||
|
@ -285,26 +273,7 @@ export default Component.extend(SlotsMixin, WithResizing, {
|
|||
},
|
||||
actions: {
|
||||
click: function(e) {
|
||||
// click on row functionality
|
||||
// so if you click the actual row but not a link
|
||||
// find the first link and fire that instead
|
||||
const name = e.target.nodeName.toLowerCase();
|
||||
switch (name) {
|
||||
case 'input':
|
||||
case 'label':
|
||||
case 'a':
|
||||
case 'button':
|
||||
return;
|
||||
}
|
||||
const $a = closest('tr', e.target).querySelector('a');
|
||||
if ($a) {
|
||||
const click = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
$a.dispatchEvent(click);
|
||||
}
|
||||
return clickFirstAnchor(e);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import Component from '@ember/component';
|
||||
import SlotsMixin from 'ember-block-slots';
|
||||
import closest from 'consul-ui/utils/dom/closest';
|
||||
import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor';
|
||||
const clickFirstAnchor = clickFirstAnchorFactory(closest);
|
||||
|
||||
export default Component.extend(SlotsMixin, {
|
||||
onchange: function() {},
|
||||
actions: {
|
||||
click: function(e) {
|
||||
clickFirstAnchor(e);
|
||||
},
|
||||
change: function(item, e) {
|
||||
this.onchange(e, item);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
import SlotsMixin from 'ember-block-slots';
|
||||
|
||||
export default Component.extend(SlotsMixin, {
|
||||
tagName: '',
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
import Controller from './edit';
|
||||
export default Controller.extend();
|
|
@ -0,0 +1,45 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get, set } 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');
|
||||
},
|
||||
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)
|
||||
);
|
||||
set(this, 'isScoped', get(model.item, 'Datacenters.length') > 0);
|
||||
},
|
||||
actions: {
|
||||
change: function(e, value, item) {
|
||||
const form = get(this, 'form');
|
||||
const event = get(this, 'dom').normalizeEvent(e, value);
|
||||
try {
|
||||
form.handleEvent(event);
|
||||
} catch (err) {
|
||||
const target = event.target;
|
||||
switch (target.name) {
|
||||
case 'policy[isScoped]':
|
||||
set(this, 'isScoped', !get(this, 'isScoped'));
|
||||
set(this.item, 'Datacenters', null);
|
||||
break;
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get } from '@ember/object';
|
||||
import WithFiltering from 'consul-ui/mixins/with-filtering';
|
||||
export default Controller.extend(WithFiltering, {
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
filter: function(item, { s = '', type = '' }) {
|
||||
const sLower = s.toLowerCase();
|
||||
return (
|
||||
get(item, 'Name')
|
||||
.toLowerCase()
|
||||
.indexOf(sLower) !== -1 ||
|
||||
get(item, 'Description')
|
||||
.toLowerCase()
|
||||
.indexOf(sLower) !== -1
|
||||
);
|
||||
},
|
||||
actions: {},
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
import Controller from './edit';
|
||||
export default Controller.extend();
|
|
@ -0,0 +1,79 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get, set } from '@ember/object';
|
||||
export default Controller.extend({
|
||||
dom: service('dom'),
|
||||
builder: service('form'),
|
||||
isScoped: false,
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.form = get(this, 'builder').form('token');
|
||||
},
|
||||
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;
|
||||
case 'policy':
|
||||
prev[key] = this.form
|
||||
.form(key)
|
||||
.setData(prev[key])
|
||||
.getData();
|
||||
break;
|
||||
}
|
||||
return prev;
|
||||
}, model)
|
||||
);
|
||||
},
|
||||
actions: {
|
||||
sendClearPolicy: function(item) {
|
||||
set(this, 'isScoped', false);
|
||||
this.send('clearPolicy');
|
||||
},
|
||||
sendCreatePolicy: function(item, policies, success) {
|
||||
this.send('createPolicy', item, policies, success);
|
||||
},
|
||||
refreshCodeEditor: function(selector, parent) {
|
||||
if (parent.target) {
|
||||
parent = undefined;
|
||||
}
|
||||
get(this, 'dom')
|
||||
.component(selector, parent)
|
||||
.didAppear();
|
||||
},
|
||||
change: function(e, value, item) {
|
||||
const form = get(this, 'form');
|
||||
const event = get(this, 'dom').normalizeEvent(e, value);
|
||||
try {
|
||||
form.handleEvent(event);
|
||||
} catch (err) {
|
||||
const target = event.target;
|
||||
switch (target.name) {
|
||||
case 'policy[isScoped]':
|
||||
set(this, 'isScoped', !get(this, 'isScoped'));
|
||||
set(this.policy, 'Datacenters', null);
|
||||
break;
|
||||
case 'Policy':
|
||||
set(value, 'CreateTime', new Date().getTime());
|
||||
get(this, 'item.Policies').pushObject(value);
|
||||
break;
|
||||
case 'Details':
|
||||
// the Details expander toggle
|
||||
// only load on opening
|
||||
if (target.checked) {
|
||||
this.send('refreshCodeEditor', '.code-editor', target.parentNode);
|
||||
if (!get(value, 'Rules')) {
|
||||
this.send('loadPolicy', value, get(this, 'item.Policies'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get } from '@ember/object';
|
||||
import WithFiltering from 'consul-ui/mixins/with-filtering';
|
||||
export default Controller.extend(WithFiltering, {
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
filter: function(item, { s = '', type = '' }) {
|
||||
const sLower = s.toLowerCase();
|
||||
return (
|
||||
get(item, 'AccessorID')
|
||||
.toLowerCase()
|
||||
.indexOf(sLower) !== -1 ||
|
||||
get(item, 'Name')
|
||||
.toLowerCase()
|
||||
.indexOf(sLower) !== -1 ||
|
||||
get(item, 'Description')
|
||||
.toLowerCase()
|
||||
.indexOf(sLower) !== -1 ||
|
||||
(get(item, 'Policies') || []).some(function(item) {
|
||||
return item.Name.toLowerCase().indexOf(sLower) !== -1;
|
||||
})
|
||||
);
|
||||
},
|
||||
actions: {
|
||||
sendClone: function(item) {
|
||||
this.send('clone', item);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -2,7 +2,7 @@ import Controller from '@ember/controller';
|
|||
import { get, set } from '@ember/object';
|
||||
import { getOwner } from '@ember/application';
|
||||
import WithFiltering from 'consul-ui/mixins/with-filtering';
|
||||
import qsaFactory from 'consul-ui/utils/qsa-factory';
|
||||
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
|
||||
import getComponentFactory from 'consul-ui/utils/get-component-factory';
|
||||
|
||||
const $$ = qsaFactory();
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
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) {
|
||||
return form(name, {
|
||||
Datacenters: {
|
||||
type: 'array',
|
||||
},
|
||||
}).setValidators(v);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import builderFactory from 'consul-ui/utils/form/builder';
|
||||
import validations from 'consul-ui/validations/token';
|
||||
import policy from 'consul-ui/forms/policy';
|
||||
const builder = builderFactory();
|
||||
export default function(name = '', v = validations, form = builder) {
|
||||
return form(name, {})
|
||||
.setValidators(v)
|
||||
.add(policy());
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
import { get } from '@ember/object';
|
||||
export function difference(params, hash) {
|
||||
return params[0].filter(function(item) {
|
||||
return !params[1].findBy('ID', get(item, 'ID'));
|
||||
});
|
||||
}
|
||||
|
||||
export default helper(difference);
|
|
@ -0,0 +1,16 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
/**
|
||||
* Datacenters can be an array of datacenters.
|
||||
* Anything that isn't an array means 'All', even an empty array.
|
||||
*/
|
||||
export function datacenters(params, hash = {}) {
|
||||
const datacenters = get(params[0], 'Datacenters');
|
||||
if (!Array.isArray(datacenters) || datacenters.length === 0) {
|
||||
return [hash['global'] || 'All'];
|
||||
}
|
||||
return get(params[0], 'Datacenters');
|
||||
}
|
||||
|
||||
export default helper(datacenters);
|
|
@ -0,0 +1,8 @@
|
|||
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);
|
|
@ -0,0 +1,8 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
const ANONYMOUS_ID = '00000000-0000-0000-0000-000000000002';
|
||||
export function isAnonymous(params, hash) {
|
||||
return get(params[0], 'AccessorID') === ANONYMOUS_ID;
|
||||
}
|
||||
export default helper(isAnonymous);
|
|
@ -0,0 +1,9 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export function isLegacy(params, hash) {
|
||||
const token = params[0];
|
||||
return get(token, 'Legacy') || typeof get(token, 'Rules') !== 'undefined';
|
||||
}
|
||||
|
||||
export default helper(isLegacy);
|
|
@ -0,0 +1,19 @@
|
|||
import token from 'consul-ui/forms/token';
|
||||
import policy from 'consul-ui/forms/policy';
|
||||
export function initialize(application) {
|
||||
// Service-less injection using private properties at a per-project level
|
||||
const FormBuilder = application.resolveRegistration('service:form');
|
||||
const forms = {
|
||||
token: token(),
|
||||
policy: policy(),
|
||||
};
|
||||
FormBuilder.reopen({
|
||||
form: function(name) {
|
||||
return forms[name];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
export function initialize(application) {
|
||||
const IvyCodeMirrorComponent = application.resolveRegistration('component:ivy-codemirror');
|
||||
// Make sure ivy-codemirror respects/maintains a `name=""` attribute
|
||||
IvyCodeMirrorComponent.reopen({
|
||||
attributeBindings: ['name'],
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
/**
|
||||
* Used for create-type Routes
|
||||
*
|
||||
* 'repo' is standardized across the app
|
||||
* 'item' is standardized across the app
|
||||
* they could be replaced with `getRepo` and `getItem`
|
||||
*/
|
||||
export default Mixin.create({
|
||||
beforeModel: function() {
|
||||
get(this, 'repo').invalidate();
|
||||
},
|
||||
deactivate: function() {
|
||||
// TODO: This is dependent on ember-changeset
|
||||
// Change changeset to support ember-data props
|
||||
const item = get(this.controller, 'item.data');
|
||||
// TODO: Look and see if rollbackAttributes is good here
|
||||
if (get(item, 'isNew')) {
|
||||
item.destroyRecord();
|
||||
}
|
||||
},
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
|
||||
export default Mixin.create(WithBlockingActions, {});
|
|
@ -0,0 +1,61 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
import { get } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default Mixin.create(WithBlockingActions, {
|
||||
settings: service('settings'),
|
||||
actions: {
|
||||
use: function(item) {
|
||||
return get(this, 'feedback').execute(() => {
|
||||
return get(this, 'repo')
|
||||
.findBySlug(get(item, 'AccessorID'), this.modelFor('dc').dc.Name)
|
||||
.then(item => {
|
||||
return get(this, 'settings')
|
||||
.persist({
|
||||
token: {
|
||||
AccessorID: get(item, 'AccessorID'),
|
||||
SecretID: get(item, 'SecretID'),
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
// using is similar to delete in that
|
||||
// if you use from the listing page, stay on the listing page
|
||||
// whereas if you use from the detail page, take me back to the listing page
|
||||
return this.afterDelete(...arguments);
|
||||
});
|
||||
});
|
||||
}, 'use');
|
||||
},
|
||||
logout: function(item) {
|
||||
return get(this, 'feedback').execute(() => {
|
||||
return get(this, 'settings')
|
||||
.delete('token')
|
||||
.then(() => {
|
||||
// logging out is similar to delete in that
|
||||
// if you log out from the listing page, stay on the listing page
|
||||
// whereas if you logout from the detail page, take me back to the listing page
|
||||
return this.afterDelete(...arguments);
|
||||
});
|
||||
}, 'logout');
|
||||
},
|
||||
clone: function(item) {
|
||||
let cloned;
|
||||
return get(this, 'feedback').execute(() => {
|
||||
return get(this, 'repo')
|
||||
.clone(item)
|
||||
.then(item => {
|
||||
cloned = item;
|
||||
// cloning is similar to delete in that
|
||||
// if you clone from the listing page, stay on the listing page
|
||||
// whereas if you clone from another token, take me back to the listing page
|
||||
// so I can see it
|
||||
return this.afterDelete(...arguments);
|
||||
})
|
||||
.then(function() {
|
||||
return cloned;
|
||||
});
|
||||
}, 'clone');
|
||||
},
|
||||
},
|
||||
});
|
|
@ -83,7 +83,7 @@ export default Mixin.create({
|
|||
}
|
||||
);
|
||||
},
|
||||
update: function(item, parent) {
|
||||
update: function(item) {
|
||||
return get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo')
|
||||
|
@ -98,7 +98,7 @@ export default Mixin.create({
|
|||
}
|
||||
);
|
||||
},
|
||||
delete: function(item, parent) {
|
||||
delete: function(item) {
|
||||
return get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo')
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import writable from 'consul-ui/utils/model/writable';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const SLUG_KEY = 'ID';
|
||||
|
||||
const model = Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
Name: attr('string', {
|
||||
defaultValue: '',
|
||||
}),
|
||||
Description: attr('string', {
|
||||
defaultValue: '',
|
||||
}),
|
||||
Rules: attr('string', {
|
||||
defaultValue: '',
|
||||
}),
|
||||
// frontend only for ordering where CreateIndex can't be used
|
||||
CreateTime: attr('date'),
|
||||
//
|
||||
Datacenter: attr('string'),
|
||||
Datacenters: attr(),
|
||||
CreateIndex: attr('number'),
|
||||
ModifyIndex: attr('number'),
|
||||
});
|
||||
export const ATTRS = writable(model, ['Name', 'Description', 'Rules', 'Datacenters']);
|
||||
export default model;
|
|
@ -21,6 +21,7 @@ export default Model.extend({
|
|||
EnableTagOverride: attr('boolean'),
|
||||
CreateIndex: attr('number'),
|
||||
ModifyIndex: attr('number'),
|
||||
// TODO: These should be typed
|
||||
ChecksPassing: attr(),
|
||||
ChecksCritical: attr(),
|
||||
ChecksWarning: attr(),
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import writable from 'consul-ui/utils/model/writable';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const SLUG_KEY = 'AccessorID';
|
||||
|
||||
const model = Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
SecretID: attr('string'),
|
||||
// Legacy
|
||||
Type: attr('string'),
|
||||
Name: attr('string', {
|
||||
defaultValue: '',
|
||||
}),
|
||||
Rules: attr('string'),
|
||||
// End Legacy
|
||||
Legacy: attr('boolean'),
|
||||
Description: attr('string', {
|
||||
defaultValue: '',
|
||||
}),
|
||||
Datacenter: attr('string'),
|
||||
Local: attr('boolean'),
|
||||
Policies: attr({
|
||||
defaultValue: function() {
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
CreateTime: attr('date'),
|
||||
CreateIndex: attr('number'),
|
||||
ModifyIndex: attr('number'),
|
||||
});
|
||||
// Name and Rules is only for legacy tokens
|
||||
export const ATTRS = writable(model, [
|
||||
'Name',
|
||||
'Rules',
|
||||
'Type',
|
||||
'Local',
|
||||
'Description',
|
||||
'Policies',
|
||||
// SecretID isn't writable but we need it to identify an
|
||||
// update via the old API, see TokenAdapter dataForRequest
|
||||
'SecretID',
|
||||
'AccessorID',
|
||||
]);
|
||||
export default model;
|
|
@ -35,6 +35,14 @@ Router.map(function() {
|
|||
this.route('acls', { path: '/acls' }, function() {
|
||||
this.route('edit', { path: '/:id' });
|
||||
this.route('create', { path: '/create' });
|
||||
this.route('policies', { path: '/policies' }, function() {
|
||||
this.route('edit', { path: '/:id' });
|
||||
this.route('create', { path: '/create' });
|
||||
});
|
||||
this.route('tokens', { path: '/tokens' }, function() {
|
||||
this.route('edit', { path: '/:id' });
|
||||
this.route('create', { path: '/create' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -43,7 +51,7 @@ Router.map(function() {
|
|||
this.route('index', { path: '/' });
|
||||
|
||||
// The settings page is global.
|
||||
this.route('settings', { path: '/settings' });
|
||||
// this.route('settings', { path: '/settings' });
|
||||
this.route('notfound', { path: '/*path' });
|
||||
});
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ export default Route.extend({
|
|||
return true;
|
||||
},
|
||||
error: function(e, transition) {
|
||||
// TODO: Normalize all this better
|
||||
let error = {
|
||||
status: e.code || '',
|
||||
message: e.message || e.detail || 'Error',
|
||||
|
@ -46,6 +47,20 @@ export default Route.extend({
|
|||
error = e.errors[0];
|
||||
error.message = error.title || error.detail || 'Error';
|
||||
}
|
||||
// TODO: Unfortunately ember will not maintain the correct URL
|
||||
// for you i.e. when this happens the URL in your browser location bar
|
||||
// will be the URL where you clicked on the link to come here
|
||||
// not the URL where you got the 403 response
|
||||
// Currently this is dealt with a lot better with the new ACLs system, in that
|
||||
// if you get a 403 in the ACLs area, the URL is correct
|
||||
// Moving that app wide right now wouldn't be ideal, therefore simply redirect
|
||||
// to the ACLs URL instead of maintaining the actual URL, which is better than the old
|
||||
// 403 page
|
||||
// To note: Consul only gives you back a 403 if a non-existent token has been sent in the header
|
||||
// if a token has not been sent at all, it just gives you a 200 with an empty dataset
|
||||
if (error.status === '403') {
|
||||
return this.transitionTo('dc.acls.tokens');
|
||||
}
|
||||
if (error.status === '') {
|
||||
error.message = 'Error';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { get } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
|
||||
export default Route.extend(WithBlockingActions, {
|
||||
settings: service('settings'),
|
||||
feedback: service('feedback'),
|
||||
repo: service('tokens'),
|
||||
actions: {
|
||||
authorize: function(secret) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
return get(this, 'feedback').execute(() => {
|
||||
return get(this, 'repo')
|
||||
.self(secret, dc)
|
||||
.then(item => {
|
||||
get(this, 'settings')
|
||||
.persist({
|
||||
token: {
|
||||
AccessorID: get(item, 'AccessorID'),
|
||||
SecretID: secret,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.refresh();
|
||||
});
|
||||
});
|
||||
}, 'authorize');
|
||||
},
|
||||
},
|
||||
});
|
|
@ -13,6 +13,9 @@ export default Route.extend(WithAclActions, {
|
|||
replace: true,
|
||||
},
|
||||
},
|
||||
beforeModel: function(transition) {
|
||||
return this.replaceWith('dc.acls.tokens');
|
||||
},
|
||||
model: function(params) {
|
||||
return hash({
|
||||
isLoading: false,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import Route from './edit';
|
||||
import CreatingRoute from 'consul-ui/mixins/creating-route';
|
||||
|
||||
export default Route.extend(CreatingRoute, {
|
||||
templateName: 'dc/acls/policies/edit',
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import SingleRoute from 'consul-ui/routing/single';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import WithPolicyActions from 'consul-ui/mixins/policy/with-actions';
|
||||
|
||||
export default SingleRoute.extend(WithPolicyActions, {
|
||||
repo: service('policies'),
|
||||
tokensRepo: service('tokens'),
|
||||
datacenterRepo: service('dc'),
|
||||
model: function(params) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const tokensRepo = get(this, 'tokensRepo');
|
||||
return this._super(...arguments).then(model => {
|
||||
return hash({
|
||||
...model,
|
||||
...{
|
||||
datacenters: get(this, 'datacenterRepo').findAll(),
|
||||
items: tokensRepo.findByPolicy(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) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import WithPolicyActions from 'consul-ui/mixins/policy/with-actions';
|
||||
|
||||
export default Route.extend(WithPolicyActions, {
|
||||
repo: service('policies'),
|
||||
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) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Route from './edit';
|
||||
import CreatingRoute from 'consul-ui/mixins/creating-route';
|
||||
|
||||
export default Route.extend(CreatingRoute, {
|
||||
templateName: 'dc/acls/tokens/edit',
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
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 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('tokens'),
|
||||
policiesRepo: service('policies'),
|
||||
datacenterRepo: service('dc'),
|
||||
settings: service('settings'),
|
||||
model: function(params, transition) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const policiesRepo = get(this, 'policiesRepo');
|
||||
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: policiesRepo.findAllByDatacenter(dc).catch(function(e) {
|
||||
switch (get(e, 'errors.firstObject.status')) {
|
||||
case '403':
|
||||
case '401':
|
||||
// do nothing the SingleRoute will have caught it already
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
getEmptyPolicy: function() {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
return get(this, 'policiesRepo').create({ Datacenter: dc });
|
||||
},
|
||||
actions: {
|
||||
// TODO: Some of this could potentially be moved to the repo services
|
||||
loadPolicy: function(item, items) {
|
||||
const repo = get(this, 'policiesRepo');
|
||||
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, 'policiesRepo')
|
||||
.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;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
import WithTokenActions from 'consul-ui/mixins/token/with-actions';
|
||||
export default Route.extend(WithTokenActions, {
|
||||
repo: service('tokens'),
|
||||
settings: service('settings'),
|
||||
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,
|
||||
token: get(this, 'settings').findBySlug('token'),
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -34,7 +34,13 @@ export default Route.extend(WithKvActions, {
|
|||
...model,
|
||||
...{
|
||||
items: repo.findAllBySlug(get(model.parent, 'Key'), dc).catch(e => {
|
||||
const status = get(e, 'errors.firstObject.status');
|
||||
switch (status) {
|
||||
case '403':
|
||||
return this.transitionTo('dc.acls.tokens');
|
||||
default:
|
||||
return this.transitionTo('dc.kv.index');
|
||||
}
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,8 +5,8 @@ import { get } from '@ember/object';
|
|||
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
export default Route.extend(WithBlockingActions, {
|
||||
dcRepo: service('dc'),
|
||||
repo: service('settings'),
|
||||
dcRepo: service('dc'),
|
||||
model: function(params) {
|
||||
return hash({
|
||||
item: get(this, 'repo').findAll(),
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { get } from '@ember/object';
|
||||
import { assert } from '@ember/debug';
|
||||
import { Promise, hash } from 'rsvp';
|
||||
export default Route.extend({
|
||||
// repo: service('repositoryName'),
|
||||
isCreate: function(params, transition) {
|
||||
return transition.targetName.split('.').pop() === 'create';
|
||||
},
|
||||
model: function(params, transition) {
|
||||
const repo = get(this, 'repo');
|
||||
assert(
|
||||
"`repo` is undefined, please define RepositoryService using `repo: service('repositoryName')`",
|
||||
typeof repo !== 'undefined'
|
||||
);
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const create = this.isCreate(...arguments);
|
||||
return hash({
|
||||
isLoading: false,
|
||||
create: create,
|
||||
...repo.status({
|
||||
item: create
|
||||
? Promise.resolve(repo.create({ Datacenter: dc }))
|
||||
: repo.findBySlug(params.id, dc),
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import Serializer from './application';
|
||||
import { PRIMARY_KEY, ATTRS } from 'consul-ui/models/policy';
|
||||
|
||||
export default Serializer.extend({
|
||||
primaryKey: PRIMARY_KEY,
|
||||
attrs: ATTRS,
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import Serializer from './application';
|
||||
import { PRIMARY_KEY, ATTRS } from 'consul-ui/models/token';
|
||||
|
||||
export default Serializer.extend({
|
||||
primaryKey: PRIMARY_KEY,
|
||||
attrs: ATTRS,
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
import Service from '@ember/service';
|
||||
import Evented from '@ember/object/evented';
|
||||
const buffer = {};
|
||||
export default Service.extend(Evented, {
|
||||
// TODO: Consider renaming this and/or
|
||||
// `delete`ing the buffer (but not the DOM element)
|
||||
// flush should flush, but maybe being able to re-flush
|
||||
// after you've flushed could be handy
|
||||
flush: function(name) {
|
||||
return buffer[name];
|
||||
},
|
||||
add: function(name, dom) {
|
||||
this.trigger('add', dom);
|
||||
buffer[name] = dom;
|
||||
return dom;
|
||||
},
|
||||
remove: function(name) {
|
||||
buffer[name].remove();
|
||||
delete buffer[name];
|
||||
},
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import Service from '@ember/service';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
|
||||
// TODO: Move to utils/dom
|
||||
import getComponentFactory from 'consul-ui/utils/get-component-factory';
|
||||
import normalizeEvent from 'consul-ui/utils/dom/normalize-event';
|
||||
|
||||
// ember-eslint doesn't like you using a single $ so use double
|
||||
// use $_ for components
|
||||
const $$ = qsaFactory();
|
||||
let $_;
|
||||
export default Service.extend({
|
||||
doc: document,
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
$_ = getComponentFactory(getOwner(this));
|
||||
},
|
||||
normalizeEvent: function() {
|
||||
return normalizeEvent(...arguments);
|
||||
},
|
||||
root: function() {
|
||||
return get(this, 'doc').documentElement;
|
||||
},
|
||||
// TODO: Should I change these to use the standard names
|
||||
// even though they don't have a standard signature (querySelector*)
|
||||
elementById: function(id) {
|
||||
return get(this, 'doc').getElementById(id);
|
||||
},
|
||||
elementsByTagName: function(name, context) {
|
||||
context = typeof context === 'undefined' ? get(this, 'doc') : context;
|
||||
return context.getElementByTagName(name);
|
||||
},
|
||||
elements: function(selector, context) {
|
||||
return $$(selector, context);
|
||||
},
|
||||
element: function(selector, context) {
|
||||
if (selector.substr(0, 1) === '#') {
|
||||
return this.elementById(selector.substr(1));
|
||||
}
|
||||
// TODO: This can just use querySelector
|
||||
return [...$$(selector, context)][0];
|
||||
},
|
||||
// ember components aren't strictly 'dom-like'
|
||||
// but if you think of them as a web component 'shim'
|
||||
// then it makes more sense to think of them as part of the dom
|
||||
// with traditional/standard web components you wouldn't actually need this
|
||||
// method as you could just get to their methods from the dom element
|
||||
component: function(selector, context) {
|
||||
// TODO: support passing a dom element, when we need to do that
|
||||
return $_(this.element(selector, context));
|
||||
},
|
||||
});
|
|
@ -7,6 +7,12 @@ const TYPE_ERROR = 'error';
|
|||
const defaultStatus = function(type, obj) {
|
||||
return type;
|
||||
};
|
||||
const notificationDefaults = function() {
|
||||
return {
|
||||
timeout: 6000,
|
||||
extendedTimeout: 300,
|
||||
};
|
||||
};
|
||||
export default Service.extend({
|
||||
notify: service('flashMessages'),
|
||||
logger: service('logger'),
|
||||
|
@ -18,23 +24,29 @@ export default Service.extend({
|
|||
return (
|
||||
handle()
|
||||
//TODO: pass this through to getAction..
|
||||
.then(target => {
|
||||
.then(item => {
|
||||
// TODO right now the majority of `item` is a Transition
|
||||
// but you can resolve an object
|
||||
notify.add({
|
||||
...notificationDefaults(),
|
||||
type: getStatus(TYPE_SUCCESS),
|
||||
// here..
|
||||
action: getAction(),
|
||||
item: item,
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
get(this, 'logger').execute(e);
|
||||
if (e.name === 'TransitionAborted') {
|
||||
notify.add({
|
||||
...notificationDefaults(),
|
||||
type: getStatus(TYPE_SUCCESS),
|
||||
// and here
|
||||
action: getAction(),
|
||||
});
|
||||
} else {
|
||||
notify.add({
|
||||
...notificationDefaults(),
|
||||
type: getStatus(TYPE_ERROR, e),
|
||||
action: getAction(),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import 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
|
||||
build: function(obj, name) {
|
||||
return builder(...arguments);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
import Service, { inject as service } from '@ember/service';
|
||||
import { get } from '@ember/object';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy';
|
||||
import { Promise } from 'rsvp';
|
||||
import statusFactory from 'consul-ui/utils/acls-status';
|
||||
const status = statusFactory(Promise);
|
||||
const MODEL_NAME = 'policy';
|
||||
export default Service.extend({
|
||||
getModelName: function() {
|
||||
return MODEL_NAME;
|
||||
},
|
||||
getPrimaryKey: function() {
|
||||
return PRIMARY_KEY;
|
||||
},
|
||||
getSlugKey: function() {
|
||||
return SLUG_KEY;
|
||||
},
|
||||
store: service('store'),
|
||||
status: function(obj) {
|
||||
return status(obj);
|
||||
},
|
||||
translate: function(item) {
|
||||
return get(this, 'store').translate('policy', get(item, 'Rules'));
|
||||
},
|
||||
findAllByDatacenter: function(dc) {
|
||||
return get(this, 'store').query('policy', {
|
||||
dc: dc,
|
||||
});
|
||||
},
|
||||
findBySlug: function(slug, dc) {
|
||||
return get(this, 'store').queryRecord('policy', {
|
||||
id: slug,
|
||||
dc: dc,
|
||||
});
|
||||
},
|
||||
create: function(obj) {
|
||||
return get(this, 'store').createRecord('policy', obj);
|
||||
},
|
||||
persist: function(item) {
|
||||
return item.save();
|
||||
},
|
||||
remove: function(obj) {
|
||||
let item = obj;
|
||||
if (typeof obj.destroyRecord === 'undefined') {
|
||||
item = obj.get('data');
|
||||
}
|
||||
if (typeOf(item) === 'object') {
|
||||
item = get(this, 'store').peekRecord('policy', item[PRIMARY_KEY]);
|
||||
}
|
||||
return item.destroyRecord().then(item => {
|
||||
return get(this, 'store').unloadRecord(item);
|
||||
});
|
||||
},
|
||||
invalidate: function() {
|
||||
get(this, 'store').unloadAll('policy');
|
||||
},
|
||||
});
|
|
@ -1,38 +1,46 @@
|
|||
import Service from '@ember/service';
|
||||
import { Promise } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import getStorage from 'consul-ui/utils/storage/local-storage';
|
||||
const SCHEME = 'consul';
|
||||
const storage = getStorage(SCHEME);
|
||||
export default Service.extend({
|
||||
// TODO: change name
|
||||
storage: window.localStorage,
|
||||
storage: storage,
|
||||
findHeaders: function() {
|
||||
// TODO: if possible this should be a promise
|
||||
const token = get(this, 'storage').getItem('token');
|
||||
// TODO: Actually this has nothing to do with settings it should be in the adapter,
|
||||
// which probably can't work with a promise based interface :(
|
||||
const token = get(this, 'storage').getValue('token');
|
||||
// TODO: The old UI always sent ?token=
|
||||
// replicate the old functionality here
|
||||
// but remove this to be cleaner if its not necessary
|
||||
return {
|
||||
'X-Consul-Token': token === null ? '' : token,
|
||||
'X-Consul-Token': typeof token.SecretID === 'undefined' ? '' : token.SecretID,
|
||||
};
|
||||
},
|
||||
findAll: function(key) {
|
||||
const token = get(this, 'storage').getItem('token');
|
||||
return Promise.resolve({ token: token === null ? '' : token });
|
||||
return Promise.resolve(get(this, 'storage').all());
|
||||
},
|
||||
findBySlug: function(slug) {
|
||||
// TODO: Force localStorage to always be strings...
|
||||
// const value = get(this, 'storage').getItem(slug);
|
||||
return Promise.resolve(get(this, 'storage').getItem(slug));
|
||||
return Promise.resolve(get(this, 'storage').getValue(slug));
|
||||
},
|
||||
persist: function(obj) {
|
||||
const storage = get(this, 'storage');
|
||||
Object.keys(obj).forEach((item, i) => {
|
||||
// TODO: ...everywhere
|
||||
storage.setItem(item, obj[item]);
|
||||
storage.setValue(item, obj[item]);
|
||||
});
|
||||
return Promise.resolve(obj);
|
||||
},
|
||||
delete: function(obj) {
|
||||
return Promise.resolve(get(this, 'storage').removeItem('token'));
|
||||
// TODO: Loop through and delete the specified keys
|
||||
if (!Array.isArray(obj)) {
|
||||
obj = [obj];
|
||||
}
|
||||
const storage = get(this, 'storage');
|
||||
const item = obj.reduce(function(prev, item, i, arr) {
|
||||
storage.removeValue(item);
|
||||
return prev;
|
||||
}, {});
|
||||
return Promise.resolve(item);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import Store from 'ember-data/store';
|
||||
|
||||
// TODO: These only exist for ACLs, should probably make sure they fail
|
||||
// nicely if you aren't on ACLs for good DX
|
||||
export default Store.extend({
|
||||
// cloning immediately refreshes the view
|
||||
clone: function(modelName, id) {
|
||||
|
@ -16,4 +18,9 @@ export default Store.extend({
|
|||
);
|
||||
// TODO: See https://github.com/emberjs/data/blob/7b8019818526a17ee72747bd3c0041354e58371a/addon/-private/system/promise-proxies.js#L68
|
||||
},
|
||||
self: function(modelName, token) {
|
||||
// TODO: no normalization, type it properly for the moment
|
||||
const adapter = this.adapterFor(modelName);
|
||||
return adapter.self(this, { modelName: modelName }, token);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import Service, { inject as service } from '@ember/service';
|
||||
import { get } from '@ember/object';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token';
|
||||
import { Promise } from 'rsvp';
|
||||
import statusFactory from 'consul-ui/utils/acls-status';
|
||||
const status = statusFactory(Promise);
|
||||
const MODEL_NAME = 'token';
|
||||
export default Service.extend({
|
||||
getModelName: function() {
|
||||
return MODEL_NAME;
|
||||
},
|
||||
getPrimaryKey: function() {
|
||||
return PRIMARY_KEY;
|
||||
},
|
||||
getSlugKey: function() {
|
||||
return SLUG_KEY;
|
||||
},
|
||||
status: function(obj) {
|
||||
return status(obj);
|
||||
},
|
||||
self: function(secret, dc) {
|
||||
return get(this, 'store').self(this.getModelName(), {
|
||||
secret: secret,
|
||||
dc: dc,
|
||||
});
|
||||
},
|
||||
clone: function(item) {
|
||||
return get(this, 'store').clone(this.getModelName(), get(item, PRIMARY_KEY));
|
||||
},
|
||||
findByPolicy: function(id, dc) {
|
||||
return get(this, 'store').query(this.getModelName(), {
|
||||
policy: id,
|
||||
dc: dc,
|
||||
});
|
||||
},
|
||||
// TODO: RepositoryService
|
||||
store: service('store'),
|
||||
findAllByDatacenter: function(dc) {
|
||||
return get(this, 'store').query(this.getModelName(), {
|
||||
dc: dc,
|
||||
});
|
||||
},
|
||||
findBySlug: function(slug, dc) {
|
||||
return get(this, 'store').queryRecord(this.getModelName(), {
|
||||
id: slug,
|
||||
dc: dc,
|
||||
});
|
||||
},
|
||||
create: function(obj) {
|
||||
// TODO: This should probably return a Promise
|
||||
return get(this, 'store').createRecord(this.getModelName(), obj);
|
||||
},
|
||||
persist: function(item) {
|
||||
return item.save();
|
||||
},
|
||||
remove: function(obj) {
|
||||
let item = obj;
|
||||
if (typeof obj.destroyRecord === 'undefined') {
|
||||
item = obj.get('data');
|
||||
}
|
||||
if (typeOf(item) === 'object') {
|
||||
item = get(this, 'store').peekRecord(this.getModelName(), item[this.getPrimaryKey()]);
|
||||
}
|
||||
return item.destroyRecord().then(item => {
|
||||
return get(this, 'store').unloadRecord(item);
|
||||
});
|
||||
},
|
||||
invalidate: function() {
|
||||
get(this, 'store').unloadAll(this.getModelName());
|
||||
},
|
||||
});
|
|
@ -6,32 +6,7 @@
|
|||
|
||||
@import 'ember-power-select';
|
||||
|
||||
@import 'components/breadcrumbs';
|
||||
@import 'components/anchors';
|
||||
@import 'components/buttons';
|
||||
@import 'components/tabs';
|
||||
@import 'components/pill';
|
||||
@import 'components/table';
|
||||
@import 'components/form-elements';
|
||||
|
||||
@import 'components/tabular-collection';
|
||||
@import 'components/list-collection';
|
||||
|
||||
@import 'components/product';
|
||||
|
||||
@import 'components/healthcheck-status';
|
||||
@import 'components/healthchecked-resource';
|
||||
@import 'components/freetext-filter';
|
||||
@import 'components/filter-bar';
|
||||
@import 'components/tomography-graph';
|
||||
@import 'components/action-group';
|
||||
@import 'components/flash-message';
|
||||
@import 'components/code-editor';
|
||||
@import 'components/confirmation-dialog';
|
||||
@import 'components/feedback-dialog';
|
||||
@import 'components/notice';
|
||||
@import 'components/with-tooltip';
|
||||
|
||||
@import 'components/index';
|
||||
@import 'core/typography';
|
||||
@import 'core/layout';
|
||||
|
||||
|
@ -40,3 +15,5 @@
|
|||
@import 'routes/dc/intention/index';
|
||||
@import 'routes/dc/kv/index';
|
||||
@import 'routes/dc/acls/index';
|
||||
@import 'routes/dc/acls/tokens/index';
|
||||
@import 'routes/dc/acls/policies/index';
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
$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>');
|
||||
$eye-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="16" height="8" viewBox="0 0 16 8" xmlns="http://www.w3.org/2000/svg"><path d="M10.229 1.301A3.493 3.493 0 0 1 11.5 4a3.493 3.493 0 0 1-1.271 2.699c1.547-.431 3.008-1.326 4.393-2.699-1.385-1.373-2.846-2.268-4.393-2.699zM5.771 6.7A3.493 3.493 0 0 1 4.5 4c0-1.086.495-2.057 1.271-2.699C4.224 1.732 2.763 2.627 1.378 4c1.385 1.373 2.846 2.268 4.393 2.699zM8 8C5.054 8 2.388 6.667 0 4c2.388-2.667 5.054-4 8-4 2.946 0 5.612 1.333 8 4-2.388 2.667-5.054 4-8 4zm.965-4.25a1 1 0 1 0 .07-2 1 1 0 0 0-.07 2z" fill="%237C8896" fill-rule="nonzero"/></svg>');
|
||||
|
||||
$chevron-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="10" height="6" viewBox="0 0 10 6" xmlns="http://www.w3.org/2000/svg"><path d="M5.001 3.515L8.293.287a1.014 1.014 0 0 1 1.414 0 .967.967 0 0 1 0 1.386L5.71 5.595a1.014 1.014 0 0 1-1.414 0L.293 1.674a.967.967 0 0 1 0-1.387 1.014 1.014 0 0 1 1.414 0l3.294 3.228z" fill="%23000" fill-rule="nonzero"/></svg>');
|
||||
|
||||
$cancel-plain-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="24" height="24" viewport="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="%23373a42"/></svg>');
|
||||
|
||||
$loading-svg: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" class="structure-icon-loading"><style>.structure-icon-loading-base{opacity:.1}.structure-icon-loading-progress{animation:structure-icon-loading-fancy-spin 3s infinite linear;opacity:.25;stroke-dasharray:0 44;stroke-dashoffset:0;stroke-linecap:round;transform-origin:50% 50%}@keyframes structure-icon-loading-fancy-spin{0%{stroke-dasharray:0 44;stroke-dashoffset:0}25%{stroke-dasharray:33 11;stroke-dashoffset:-40}50%{stroke-dasharray:0 44;stroke-dashoffset:-110}75%{stroke-dasharray:33 11;stroke-dashoffset:-150}to{stroke-dasharray:0 44;stroke-dashoffset:-220}}@keyframes structure-icon-loading-simple-spin{0%{transform:rotate(0deg)}to{transform:rotate(360deg)}}</style><defs><path stroke="%23fff" stroke-width="3" fill="none" id="structure-icon-loading" d="M12 5l6 3v8l-6 3-6-3V8z"/></defs><use xlink:href="%23structure-icon-loading" class="structure-icon-loading-base"/><use xlink:href="%23structure-icon-loading" class="structure-icon-loading-progress"/></svg>');
|
||||
|
||||
$hashicorp-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 107 114" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"><path d="M44.54 0L0 25.69V87.41l16.73 9.66V35.35L44.54 19.3z"/><path d="M62.32 0v49.15H44.54V30.81L27.8 40.47v62.97l16.74 9.68V64.11h17.78v18.22l16.73-9.66V9.66z"/><path d="M62.32 113.14l44.54-25.69V25.73l-16.74-9.66v61.72l-27.8 16.05z"/></svg>');
|
||||
|
||||
$consul-color-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M8.693 10.707a1.862 1.862 0 1 1-.006-3.724 1.862 1.862 0 0 1 .006 3.724" fill="%23961D59"/><path d="M12.336 9.776a.853.853 0 1 1 0-1.707.853.853 0 0 1 0 1.707M15.639 10.556a.853.853 0 1 1 .017-.07c-.01.022-.01.044-.017.07M14.863 8.356a.855.855 0 0 1-.925-1.279.855.855 0 0 1 1.559.255c.024.11.027.222.009.333a.821.821 0 0 1-.642.691M17.977 10.467a.849.849 0 1 1-1.67-.296.849.849 0 0 1 .982-.692c.433.073.74.465.709.905a.221.221 0 0 0-.016.076M17.286 8.368a.853.853 0 1 1-.279-1.684.853.853 0 0 1 .279 1.684M16.651 13.371a.853.853 0 1 1-1.492-.828.853.853 0 0 1 1.492.828M16.325 5.631a.853.853 0 1 1-.84-1.485.853.853 0 0 1 .84 1.485" fill="%23D62783"/><path d="M8.842 17.534c-4.798 0-8.687-3.855-8.687-8.612C.155 4.166 4.045.31 8.842.31a8.645 8.645 0 0 1 5.279 1.77l-1.056 1.372a6.987 6.987 0 0 0-7.297-.709 6.872 6.872 0 0 0 0 12.356 6.987 6.987 0 0 0 7.297-.709l1.056 1.374a8.66 8.66 0 0 1-5.279 1.77z" fill="%23D62783" fill-rule="nonzero"/></g></svg>');
|
||||
|
|
|
@ -33,3 +33,10 @@
|
|||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
%user-select-text {
|
||||
-webkit-touch-callout: default;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
|
|
@ -74,7 +74,9 @@ fieldset {
|
|||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
a {
|
||||
a,
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
hr {
|
||||
|
|
|
@ -3,10 +3,10 @@ $typo-family-sans: BlinkMacSystemFont, -apple-system, 'Segoe UI', 'Roboto', 'Oxy
|
|||
$typo-family-mono: monospace;
|
||||
$typo-size-000: 16px;
|
||||
$typo-size-100: 3.5rem;
|
||||
$typo-size-200: 2.5rem;
|
||||
$typo-size-300: 2.2rem;
|
||||
$typo-size-400: 1.5rem;
|
||||
$typo-size-500: 1.125rem;
|
||||
$typo-size-200: 1.8rem;
|
||||
$typo-size-300: 1.3rem;
|
||||
$typo-size-400: 1.2rem;
|
||||
$typo-size-500: 1rem;
|
||||
$typo-size-600: 0.875rem;
|
||||
$typo-size-700: 0.8125rem;
|
||||
$typo-size-800: 0.75rem;
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
@import './base-variables';
|
||||
@import './semantic-variables';
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
$typo-header-100: $typo-size-200;
|
||||
$typo-header-200: $typo-size-300;
|
||||
$typo-header-300: $typo-size-500;
|
|
@ -27,6 +27,7 @@
|
|||
z-index: -1;
|
||||
top: 0;
|
||||
}
|
||||
/* this is actually the group */
|
||||
%action-group ul {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
|
@ -71,6 +72,6 @@
|
|||
%action-group input[type='radio']:checked ~ .with-confirmation > ul {
|
||||
display: block;
|
||||
}
|
||||
%action-group input[type='radio']:checked ~ label[for="actions_close"] {
|
||||
%action-group input[type='radio']:checked ~ label[for='actions_close'] {
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
%action-group label:first-of-type {
|
||||
@extend %toggle-button;
|
||||
}
|
||||
%action-group input[type='radio']:checked + label:first-of-type {
|
||||
background-color: $ui-gray-050;
|
||||
}
|
||||
%action-group label {
|
||||
border-radius: $radius-small;
|
||||
cursor: pointer;
|
||||
}
|
||||
%action-group label::after,
|
||||
%action-group label::before,
|
||||
%action-group::before {
|
||||
@extend %with-dot;
|
||||
}
|
||||
%action-group ul {
|
||||
border: $decor-border-100;
|
||||
border-radius: $radius-small;
|
||||
|
@ -15,13 +25,6 @@
|
|||
%action-group ul::before {
|
||||
border-color: $ui-color-action;
|
||||
}
|
||||
%action-group input[type='radio']:checked + label:first-of-type,
|
||||
%action-group label:first-of-type:hover {
|
||||
background-color: $ui-gray-050;
|
||||
}
|
||||
%action-group label:first-of-type:active {
|
||||
background-color: $ui-gray-100;
|
||||
}
|
||||
%action-group li a:hover {
|
||||
background-color: $ui-color-action;
|
||||
color: $ui-white;
|
||||
|
@ -30,8 +33,3 @@
|
|||
%action-group ul::before {
|
||||
background-color: $ui-white;
|
||||
}
|
||||
%action-group label::after,
|
||||
%action-group label::before,
|
||||
%action-group::before {
|
||||
@extend %with-dot;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
@import './anchors/index';
|
||||
main a {
|
||||
%main-content a {
|
||||
color: $ui-gray-900;
|
||||
}
|
||||
main a[rel*='help'] {
|
||||
%main-content a[rel*='help'] {
|
||||
@extend %with-info;
|
||||
}
|
||||
main label a[rel*='help'] {
|
||||
%main-content label a[rel*='help'] {
|
||||
color: $ui-gray-400;
|
||||
}
|
||||
|
||||
[role='tabpanel'] > p:only-child [rel*='help']::after {
|
||||
content: none;
|
||||
}
|
||||
main p a,
|
||||
main dd a {
|
||||
%main-content p a,
|
||||
%main-content dd a {
|
||||
@extend %anchor;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
@import './app-view/index';
|
||||
@import './filter-bar/index';
|
||||
@import './buttons/index';
|
||||
main {
|
||||
@extend %app-view;
|
||||
}
|
||||
%app-view > div > div {
|
||||
@extend %app-content;
|
||||
}
|
||||
%app-view header form {
|
||||
@extend %filter-bar;
|
||||
}
|
||||
@media #{$--lt-spacious-page-header} {
|
||||
%app-view header .actions {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
%app-view h1 span {
|
||||
@extend %with-external-source-icon;
|
||||
}
|
||||
%app-view header .actions a,
|
||||
%app-view header .actions button {
|
||||
@extend %button-compact;
|
||||
}
|
||||
%app-content div > dl {
|
||||
@extend %form-row;
|
||||
}
|
||||
[role='tabpanel'] > p:only-child,
|
||||
.template-error > div,
|
||||
%app-content > p:only-child,
|
||||
%app-view > div.disabled > div,
|
||||
%app-view.empty > div {
|
||||
@extend %app-content-empty;
|
||||
}
|
||||
[role='tabpanel'] > *:first-child {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
%app-view > div.disabled > div {
|
||||
margin-top: 0 !important;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
|
@ -0,0 +1,52 @@
|
|||
/* layout */
|
||||
%app-view header > div:last-of-type > div:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
%app-view {
|
||||
position: relative;
|
||||
}
|
||||
%app-view header .actions {
|
||||
float: right;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
/* units */
|
||||
%app-view {
|
||||
margin-top: 50px;
|
||||
}
|
||||
%app-view header + div > *:first-child {
|
||||
margin-top: 1.8em;
|
||||
}
|
||||
%app-view h2 {
|
||||
padding-bottom: 0.2em;
|
||||
margin-bottom: 1.1em;
|
||||
}
|
||||
%app-view header .actions > *:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
// content
|
||||
%app-content div > dl > dt {
|
||||
position: absolute;
|
||||
}
|
||||
%app-content div > dl {
|
||||
position: relative;
|
||||
}
|
||||
%app-content-empty {
|
||||
margin-top: 0;
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
%app-content form:not(:last-child) {
|
||||
margin-bottom: 2.2em;
|
||||
}
|
||||
%app-content div > dl > dt {
|
||||
width: 140px;
|
||||
}
|
||||
%app-content div > dl > dd {
|
||||
padding-left: 140px;
|
||||
}
|
||||
%app-content div > dl > * {
|
||||
min-height: 1em;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
%app-view h2,
|
||||
%app-view header > div:last-of-type {
|
||||
border-bottom: $decor-border-100;
|
||||
}
|
||||
%app-view header > div:last-of-type,
|
||||
%app-view h2 {
|
||||
border-color: $keyline-light;
|
||||
}
|
||||
%app-content div > dl > dd {
|
||||
color: $ui-gray-400;
|
||||
}
|
||||
[role='tabpanel'] > p:only-child,
|
||||
.template-error > div {
|
||||
background-color: $ui-gray-050;
|
||||
color: $ui-gray-400;
|
||||
}
|
||||
%app-content > p:only-child,
|
||||
%app-view > div.disabled > div {
|
||||
background-color: $ui-gray-050;
|
||||
color: $ui-gray-400;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
%breadcrumbs {
|
||||
position: absolute;
|
||||
top: -35px; // %app-view:margin-top - 15px;
|
||||
top: -38px; // %app-view:margin-top - 15px;
|
||||
}
|
||||
%breadcrumbs ol {
|
||||
display: flex;
|
||||
|
|
|
@ -1,17 +1,40 @@
|
|||
%button {
|
||||
position: relative;
|
||||
}
|
||||
%button .progress.indeterminate {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -12px;
|
||||
margin-top: -12px;
|
||||
}
|
||||
%button:disabled .progress + * {
|
||||
visibility: hidden;
|
||||
}
|
||||
%button:empty {
|
||||
padding-right: 0 !important;
|
||||
padding-left: 18px !important;
|
||||
margin-right: 5px;
|
||||
}
|
||||
%button:empty::before {
|
||||
left: 1px;
|
||||
}
|
||||
%button:not(:empty) {
|
||||
display: inline-flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: calc(0.375em - 1px) calc(2.2em - 1px);
|
||||
height: 2.5em;
|
||||
height: 2.55em;
|
||||
min-width: 100px;
|
||||
}
|
||||
%button:not(:last-child) {
|
||||
margin-right: 7px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
%button-compact {
|
||||
// @extend %button;
|
||||
padding-left: calc(1.75em - 1px);
|
||||
padding-right: calc(1.75em - 1px);
|
||||
height: 2.1em;
|
||||
padding-left: calc(1.6em - 1px) !important;
|
||||
padding-right: calc(1.6em - 1px) !important;
|
||||
padding-top: calc(0.35em - 1px) !important;
|
||||
height: 2.3em !important;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
}
|
||||
%copy-button {
|
||||
@extend %button, %with-clipboard;
|
||||
min-height: 17px;
|
||||
}
|
||||
%copy-button:not(:empty) {
|
||||
padding-left: 38px !important;
|
||||
}
|
||||
%primary-button,
|
||||
%secondary-button,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
|
@ -0,0 +1,9 @@
|
|||
%checkbox-group span {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
min-width: 50px;
|
||||
}
|
||||
%checkbox-group label {
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
%checkbox-group label {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
div.with-confirmation {
|
||||
@extend %confirmation-dialog, %confirmation-dialog-inline;
|
||||
}
|
||||
table div.with-confirmation.confirming {
|
||||
table td > div.with-confirmation.confirming {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
float: right;
|
||||
}
|
||||
%confirmation-dialog-inline p {
|
||||
margin-right: 1em;
|
||||
margin-right: 12px;
|
||||
padding-left: 12px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
%confirmation-dialog-inline {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import './layout';
|
|
@ -0,0 +1,13 @@
|
|||
%dom-recycling-table {
|
||||
position: relative;
|
||||
}
|
||||
%dom-recycling-table tr {
|
||||
display: flex;
|
||||
}
|
||||
%dom-recycling-table tr > * {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
%dom-recycling-table tbody {
|
||||
/* important required as ember-collection will inline an overflow: visible*/
|
||||
overflow-x: hidden !important;
|
||||
}
|
|
@ -2,3 +2,6 @@
|
|||
.flash-message {
|
||||
@extend %flash-message;
|
||||
}
|
||||
%flash-message.exiting {
|
||||
@extend %blink-in-fade-out;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,38 @@
|
|||
/*TODO: This remains a mix of form-elements */
|
||||
/* form-elements should probably be a collection of these */
|
||||
@import './form-elements/index';
|
||||
@import './toggle/index';
|
||||
.type-toggle {
|
||||
@extend %toggle;
|
||||
}
|
||||
@import './radio-group/index';
|
||||
@import './checkbox-group/index';
|
||||
label span {
|
||||
@extend %user-select-none;
|
||||
}
|
||||
.has-error {
|
||||
@extend %form-element-error;
|
||||
}
|
||||
%app-content .type-text,
|
||||
%app-content .type-toggle {
|
||||
%modal-dialog .type-text,
|
||||
%app-content .type-text {
|
||||
@extend %form-element;
|
||||
}
|
||||
.type-toggle {
|
||||
@extend %form-element, %toggle;
|
||||
}
|
||||
%form-element,
|
||||
%radio-group,
|
||||
%checkbox-group,
|
||||
form table,
|
||||
%app-content form dl {
|
||||
@extend %form-row;
|
||||
}
|
||||
%app-content [role='radiogroup'] {
|
||||
@extend %radio-group;
|
||||
}
|
||||
%radio-group label {
|
||||
@extend %form-element;
|
||||
}
|
||||
.checkbox-group {
|
||||
@extend %checkbox-group;
|
||||
}
|
||||
%toggle + .checkbox-group {
|
||||
margin-top: -1em;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
%form-row {
|
||||
margin-bottom: 1.4em;
|
||||
}
|
||||
%form-element {
|
||||
@extend %form-row;
|
||||
}
|
||||
%form-element,
|
||||
%form-element > em,
|
||||
%form-element > span,
|
||||
%form-element textarea {
|
||||
display: block;
|
||||
}
|
||||
%form-element a {
|
||||
display: inline;
|
||||
}
|
||||
%form-element > em > code {
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -18,6 +27,10 @@
|
|||
%form-element > span {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
%form-element > span + em {
|
||||
margin-top: -0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
%form-element textarea {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
|
@ -47,30 +60,3 @@
|
|||
%form-element > span {
|
||||
margin-bottom: 0.4em !important;
|
||||
}
|
||||
%form-element,
|
||||
%radio-group {
|
||||
margin-bottom: 1.55em;
|
||||
}
|
||||
%radio-group {
|
||||
overflow: hidden;
|
||||
}
|
||||
%radio-group label {
|
||||
float: left;
|
||||
}
|
||||
%radio-group label > span {
|
||||
float: right;
|
||||
}
|
||||
%radio-group {
|
||||
padding-left: 1px;
|
||||
}
|
||||
%radio-group label:not(:last-child) {
|
||||
margin-right: 25px;
|
||||
}
|
||||
%radio-group label > span {
|
||||
margin-left: 1em;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
%radio-group label,
|
||||
%radio-group label > span {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
%radio-group label {
|
||||
@extend %form-element;
|
||||
%form-element > strong {
|
||||
@extend %with-error;
|
||||
}
|
||||
%form-element-error > input,
|
||||
%form-element-error > textarea {
|
||||
|
@ -10,7 +10,7 @@
|
|||
%form-element textarea {
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
box-shadow: inset 0 4px 1px rgba(0, 0, 0, .06);
|
||||
box-shadow: inset 0 4px 1px rgba(0, 0, 0, 0.06);
|
||||
border-radius: $decor-radius-100;
|
||||
border: $decor-border-100;
|
||||
}
|
||||
|
@ -25,6 +25,9 @@
|
|||
%form-element-error > input {
|
||||
border-color: $ui-color-failure !important;
|
||||
}
|
||||
%form-element > strong {
|
||||
color: $ui-color-failure;
|
||||
}
|
||||
%form-element > em {
|
||||
color: $ui-gray-400;
|
||||
}
|
||||
|
|
|
@ -1,27 +1,34 @@
|
|||
/*TODO: The old pseudo-icon was to specific */
|
||||
/* make a temporary one with the -- prefix */
|
||||
/* to make it more reusable temporarily */
|
||||
%bg-icon {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
%--pseudo-icon {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-position: center;
|
||||
}
|
||||
%pseudo-icon-bg-img {
|
||||
@extend %--pseudo-icon;
|
||||
position: relative;
|
||||
background-size: contain;
|
||||
background-color: transparent;
|
||||
}
|
||||
%pseudo-icon-css {
|
||||
@extend %--pseudo-icon;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-top: -0.6em;
|
||||
background-color: currentColor;
|
||||
}
|
||||
/* %pseudo-icon-mask, %pseudo-icon-overlay ?*/
|
||||
%pseudo-icon {
|
||||
@extend %pseudo-icon-css;
|
||||
}
|
||||
|
@ -145,6 +152,36 @@
|
|||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
/*TODO: All chevrons need merging */
|
||||
%with-chevron-down::before {
|
||||
@extend %pseudo-icon-bg-img;
|
||||
background-image: $chevron-svg;
|
||||
width: 10px;
|
||||
height: 6px;
|
||||
}
|
||||
%with-star-before::before,
|
||||
%with-star-after::after {
|
||||
@extend %pseudo-icon-bg-img;
|
||||
background-image: $star-svg;
|
||||
width: 10px;
|
||||
height: 9px;
|
||||
}
|
||||
%with-star-before::before {
|
||||
padding-right: 12px;
|
||||
}
|
||||
%with-star-after::after {
|
||||
padding-left: 22px;
|
||||
}
|
||||
%with-star {
|
||||
@extend %with-star-before;
|
||||
}
|
||||
%with-eye::before {
|
||||
@extend %pseudo-icon-bg-img;
|
||||
background-image: $eye-svg;
|
||||
width: 16px;
|
||||
height: 8px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
%with-tick {
|
||||
@extend %pseudo-icon;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="10" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M8.95 0L10 .985 3.734 8 0 4.737l.924-1.11 2.688 2.349z" fill="%23FFF"/></svg>');
|
||||
|
@ -216,3 +253,11 @@
|
|||
@extend %with-minus;
|
||||
border-radius: 20%;
|
||||
}
|
||||
%with-error {
|
||||
position: relative;
|
||||
padding-left: 18px;
|
||||
}
|
||||
%with-error::before {
|
||||
@extend %with-cross;
|
||||
margin-top: -0.5em;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
@import './breadcrumbs';
|
||||
@import './anchors';
|
||||
@import './progress';
|
||||
@import './buttons';
|
||||
@import './toggle-button';
|
||||
@import './secret-button';
|
||||
@import './tabs';
|
||||
@import './pill';
|
||||
@import './table';
|
||||
@import './form-elements';
|
||||
|
||||
@import './tabular-details';
|
||||
@import './tabular-collection';
|
||||
@import './list-collection';
|
||||
|
||||
@import './app-view';
|
||||
@import './product';
|
||||
|
||||
@import './healthcheck-status';
|
||||
@import './healthchecked-resource';
|
||||
@import './freetext-filter';
|
||||
@import './filter-bar';
|
||||
@import './tomography-graph';
|
||||
@import './action-group';
|
||||
@import './flash-message';
|
||||
@import './code-editor';
|
||||
@import './confirmation-dialog';
|
||||
@import './feedback-dialog';
|
||||
@import './modal-dialog';
|
||||
@import './notice';
|
||||
@import './with-tooltip';
|
|
@ -0,0 +1,13 @@
|
|||
@import './modal-dialog/index';
|
||||
[role='dialog'] {
|
||||
@extend %modal-dialog;
|
||||
}
|
||||
input[name='modal'] {
|
||||
@extend %modal-control;
|
||||
}
|
||||
html.template-with-modal {
|
||||
@extend %with-modal;
|
||||
}
|
||||
%modal-dialog table {
|
||||
min-height: 149px;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
|
@ -0,0 +1,70 @@
|
|||
%modal-dialog > div > div {
|
||||
@extend %modal-window;
|
||||
}
|
||||
%with-modal {
|
||||
overflow: hidden;
|
||||
}
|
||||
%modal-dialog {
|
||||
z-index: 10000;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
%modal-control,
|
||||
%modal-control + * {
|
||||
display: none;
|
||||
}
|
||||
%modal-control:checked + * {
|
||||
display: block;
|
||||
}
|
||||
%modal-dialog > label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
%modal-dialog > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
%modal-window.overflowing {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
%modal-window {
|
||||
max-width: 855px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
%modal-window > * {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
%modal-window > div {
|
||||
padding: 20px 23px;
|
||||
}
|
||||
%modal-window > footer,
|
||||
%modal-window > header {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
%modal-window table {
|
||||
height: 150px !important;
|
||||
}
|
||||
%modal-window tbody {
|
||||
max-height: 100px;
|
||||
}
|
||||
%modal-window > header {
|
||||
position: relative;
|
||||
}
|
||||
%modal-window > header [for='modal_close'] {
|
||||
float: right;
|
||||
text-indent: -9000px;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
%modal-dialog > label {
|
||||
background-color: rgba($ui-white, 0.9);
|
||||
}
|
||||
%modal-window {
|
||||
box-shadow: 2px 8px 8px 0 rgba($ui-black, 0.1);
|
||||
}
|
||||
%modal-window {
|
||||
/*%frame-gray-000*/
|
||||
background-color: $ui-white;
|
||||
border: $decor-border-100;
|
||||
border-color: $ui-gray-300;
|
||||
}
|
||||
%modal-window > footer,
|
||||
%modal-window > header {
|
||||
/*%frame-gray-000*/
|
||||
border: 0 solid;
|
||||
background-color: $ui-gray-050;
|
||||
border-color: $ui-gray-300;
|
||||
}
|
||||
%modal-window > footer {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
%modal-window > header {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
%modal-window.warning > header {
|
||||
@extend %with-warning;
|
||||
text-indent: 20px;
|
||||
}
|
||||
|
||||
%modal-window > header [for='modal_close'] {
|
||||
@extend %bg-icon;
|
||||
background-image: $cancel-plain-svg;
|
||||
background-size: 80%;
|
||||
|
||||
cursor: pointer;
|
||||
/*%frame-gray-000*/
|
||||
background-color: $ui-gray-050;
|
||||
border: $decor-border-100;
|
||||
border-color: $ui-gray-300;
|
||||
border-radius: $decor-radius-100;
|
||||
}
|
|
@ -2,3 +2,9 @@
|
|||
.notice.warning {
|
||||
@extend %notice-warning;
|
||||
}
|
||||
.notice.info {
|
||||
@extend %notice-info;
|
||||
}
|
||||
.notice.policy-management {
|
||||
@extend %notice-highlight;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
%notice::before {
|
||||
left: 20px;
|
||||
top: 18px;
|
||||
margin-top: 0;
|
||||
}
|
||||
%notice {
|
||||
position: relative;
|
||||
padding: 1em;
|
||||
padding-left: 45px;
|
||||
}
|
||||
%notice::before {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 18px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue