Skip to content

Commit

Permalink
UI: New ACLs (#4789)
Browse files Browse the repository at this point in the history
UI to accompany the new ACLs APIs
  • Loading branch information
johncowen authored and pearkes committed Oct 19, 2018
1 parent 54cc082 commit 7d89e51
Show file tree
Hide file tree
Showing 301 changed files with 6,542 additions and 678 deletions.
27 changes: 25 additions & 2 deletions ui-v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
4 changes: 3 additions & 1 deletion ui-v2/app/adapters/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export default Adapter.extend({
});
},
cleanQuery: function(_query) {
delete _query.id;
if (typeof _query.id !== 'undefined') {
delete _query.id;
}
const query = { ..._query };
delete _query[DATACENTER_QUERY_PARAM];
return query;
Expand Down
73 changes: 73 additions & 0 deletions ui-v2/app/adapters/policy.js
Original file line number Diff line number Diff line change
@@ -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);
},
});
199 changes: 199 additions & 0 deletions ui-v2/app/adapters/token.js
Original file line number Diff line number Diff line change
@@ -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;
},
});
8 changes: 5 additions & 3 deletions ui-v2/app/components/app-view.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
8 changes: 7 additions & 1 deletion ui-v2/app/components/code-editor.js
Original file line number Diff line number Diff line change
@@ -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();
},
});
3 changes: 3 additions & 0 deletions ui-v2/app/components/copy-button-feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@ember/component';

export default Component.extend({});
7 changes: 7 additions & 0 deletions ui-v2/app/components/delete-confirmation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Component from '@ember/component';

export default Component.extend({
tagName: '',
execute: function() {},
cancel: function() {},
});
19 changes: 19 additions & 0 deletions ui-v2/app/components/dom-buffer-flush.js
Original file line number Diff line number Diff line change
@@ -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);
},
});
Loading

0 comments on commit 7d89e51

Please sign in to comment.