ui: Namespace Support (#6639)

Adds namespace support to the UI:

1. Namespace CRUD/management
2. Show Namespace in relevant areas (intentions, upstreams)
3. Main navigation bar improvements
4. Logic/integration to interact with a new `internal/acl/authorize` endpoint
This commit is contained in:
John Cowen 2019-12-17 18:47:37 +00:00 committed by John Cowen
parent f06c3adca5
commit 7044aa52c8
226 changed files with 4521 additions and 1830 deletions

View File

@ -2,6 +2,8 @@ import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './applica
import { SLUG_KEY } from 'consul-ui/models/acl';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
// The old ACL system doesn't support the `ns=` query param
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, index }) {
// https://www.consul.io/api/acl.html#list-acls

View File

@ -1,10 +1,22 @@
import Adapter from './http';
import { inject as service } from '@ember/service';
import config from 'consul-ui/config/environment';
export const DATACENTER_QUERY_PARAM = 'dc';
export const NSPACE_QUERY_PARAM = 'ns';
export default Adapter.extend({
repo: service('settings'),
client: service('client/http'),
formatNspace: function(nspace) {
if (config.CONSUL_NSPACES_ENABLED) {
return nspace !== '' ? { [NSPACE_QUERY_PARAM]: nspace } : undefined;
}
},
formatDatacenter: function(dc) {
return {
[DATACENTER_QUERY_PARAM]: dc,
};
},
// TODO: kinda protected for the moment
// decide where this should go either read/write from http
// should somehow use this or vice versa

View File

@ -1,4 +1,5 @@
import Adapter from './application';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, index }) {
return request`

View File

@ -1,4 +1,5 @@
import Adapter from 'ember-data/adapter';
import AdapterError from '@ember-data/adapter/error';
import {
AbortError,
TimeoutError,
@ -8,9 +9,7 @@ import {
NotFoundError,
ConflictError,
InvalidError,
AdapterError,
} from 'ember-data/adapters/errors';
// TODO: This is a little skeleton cb function
// is to be replaced soon with something slightly more involved
const responder = function(response) {

View File

@ -1,6 +1,10 @@
import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { SLUG_KEY } from 'consul-ui/models/intention';
// Intentions use SourceNS and DestinationNS properties for namespacing
// so we don't need to add the `?ns=` anywhere here
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, index, id }) {
return request`
@ -24,14 +28,30 @@ export default Adapter.extend({
return request`
POST /v1/connect/intentions?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
${serialized}
${{
SourceNS: serialized.SourceNS,
DestinationNS: serialized.DestinationNS,
SourceName: serialized.SourceName,
DestinationName: serialized.DestinationName,
SourceType: serialized.SourceType,
Action: serialized.Action,
Description: serialized.Description,
}}
`;
},
requestForUpdateRecord: function(request, serialized, data) {
return request`
PUT /v1/connect/intentions/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
${serialized}
${{
SourceNS: serialized.SourceNS,
DestinationNS: serialized.DestinationNS,
SourceName: serialized.SourceName,
DestinationName: serialized.DestinationName,
SourceType: serialized.SourceType,
Action: serialized.Action,
Description: serialized.Description,
}}
`;
},
requestForDeleteRecord: function(request, serialized, data) {

View File

@ -1,46 +1,62 @@
import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application';
import Adapter from './application';
import isFolder from 'consul-ui/utils/isFolder';
import keyToArray from 'consul-ui/utils/keyToArray';
import { SLUG_KEY } from 'consul-ui/models/kv';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { NSPACE_KEY } from 'consul-ui/models/nspace';
// TODO: Update to use this.formatDatacenter()
const API_KEYS_KEY = 'keys';
export default Adapter.extend({
requestForQuery: function(request, { dc, index, id, separator }) {
requestForQuery: function(request, { dc, ns, index, id, separator }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/kv/${keyToArray(id)}?${{ [API_KEYS_KEY]: null, dc, separator }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForQueryRecord: function(request, { dc, index, id }) {
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/kv/${keyToArray(id)}?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
// TODO: Should we replace text/plain here with x-www-form-encoded?
// See https://github.com/hashicorp/consul/issues/3804
requestForCreateRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/kv/${keyToArray(data[SLUG_KEY])}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/kv/${keyToArray(data[SLUG_KEY])}?${params}
Content-Type: text/plain; charset=utf-8
${serialized}
`;
},
requestForUpdateRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/kv/${keyToArray(data[SLUG_KEY])}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/kv/${keyToArray(data[SLUG_KEY])}?${params}
Content-Type: text/plain; charset=utf-8
${serialized}
@ -51,11 +67,13 @@ export default Adapter.extend({
if (isFolder(data[SLUG_KEY])) {
recurse = null;
}
return request`
DELETE /v1/kv/${keyToArray(data[SLUG_KEY])}?${{
[API_DATACENTER_KEY]: data[DATACENTER_KEY],
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
recurse,
}}
};
return request`
DELETE /v1/kv/${keyToArray(data[SLUG_KEY])}?${params}
`;
},
});

View File

@ -1,4 +1,5 @@
import Adapter from './application';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, index, id }) {
return request`

View File

@ -0,0 +1,82 @@
import Adapter from './application';
import { SLUG_KEY } from 'consul-ui/models/nspace';
// namespaces aren't categorized by datacenter, therefore no dc
export default Adapter.extend({
requestForQuery: function(request, { index }) {
return request`
GET /v1/namespaces
${{ index }}
`;
},
requestForQueryRecord: function(request, { index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an name');
}
return request`
GET /v1/namespace/${id}
${{ index }}
`;
},
requestForCreateRecord: function(request, serialized, data) {
return request`
PUT /v1/namespace/${data[SLUG_KEY]}
${{
Name: serialized.Name,
Description: serialized.Description,
ACLs: {
PolicyDefaults: serialized.ACLs.PolicyDefaults.map(item => ({ ID: item.ID })),
RoleDefaults: serialized.ACLs.RoleDefaults.map(item => ({ ID: item.ID })),
},
}}
`;
},
requestForUpdateRecord: function(request, serialized, data) {
return request`
PUT /v1/namespace/${data[SLUG_KEY]}
${{
Description: serialized.Description,
ACLs: {
PolicyDefaults: serialized.ACLs.PolicyDefaults.map(item => ({ ID: item.ID })),
RoleDefaults: serialized.ACLs.RoleDefaults.map(item => ({ ID: item.ID })),
},
}}
`;
},
requestForDeleteRecord: function(request, serialized, data) {
return request`
DELETE /v1/namespace/${data[SLUG_KEY]}
`;
},
requestForAuthorize: function(request, { dc, ns, index }) {
return request`
POST /v1/internal/acl/authorize?${{ dc, ns, index }}
${[
{
Resource: 'operator',
Access: 'write',
},
]}
`;
},
authorize: function(store, type, id, snapshot) {
return this.request(
function(adapter, request, serialized, unserialized) {
return adapter.requestForAuthorize(request, serialized, unserialized);
},
function(serializer, respond, serialized, unserialized) {
// Completely skip the serializer here
return respond(function(headers, body) {
return body;
});
},
snapshot,
type.modelName
);
},
});

View File

@ -1,29 +1,41 @@
import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application';
import Adapter from './application';
import { SLUG_KEY } from 'consul-ui/models/policy';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { NSPACE_KEY } from 'consul-ui/models/nspace';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, index, id }) {
requestForQuery: function(request, { dc, ns, index, id }) {
return request`
GET /v1/acl/policies?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForQueryRecord: function(request, { dc, index, id }) {
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/acl/policy/${id}?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForCreateRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/acl/policy?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/acl/policy?${params}
${{
Name: serialized.Name,
@ -34,8 +46,12 @@ export default Adapter.extend({
`;
},
requestForUpdateRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/acl/policy/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/acl/policy/${data[SLUG_KEY]}?${params}
${{
Name: serialized.Name,
@ -46,8 +62,12 @@ export default Adapter.extend({
`;
},
requestForDeleteRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
DELETE /v1/acl/policy/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
DELETE /v1/acl/policy/${data[SLUG_KEY]}?${params}
`;
},
});

View File

@ -1,13 +1,17 @@
import Adapter from './application';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, index, id }) {
requestForQuery: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/catalog/connect/${id}?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
});

View File

@ -1,29 +1,41 @@
import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application';
import Adapter from './application';
import { SLUG_KEY } from 'consul-ui/models/role';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { NSPACE_KEY } from 'consul-ui/models/nspace';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, index, id }) {
requestForQuery: function(request, { dc, ns, index, id }) {
return request`
GET /v1/acl/roles?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForQueryRecord: function(request, { dc, index, id }) {
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/acl/role/${id}?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForCreateRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/acl/role?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/acl/role?${params}
${{
Name: serialized.Name,
@ -35,8 +47,12 @@ export default Adapter.extend({
`;
},
requestForUpdateRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/acl/role/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/acl/role/${data[SLUG_KEY]}?${params}
${{
Name: serialized.Name,
@ -48,8 +64,12 @@ export default Adapter.extend({
`;
},
requestForDeleteRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
DELETE /v1/acl/role/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
DELETE /v1/acl/role/${data[SLUG_KEY]}?${params}
`;
},
});

View File

@ -1,20 +1,27 @@
import Adapter from './application';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, index }) {
requestForQuery: function(request, { dc, ns, index }) {
return request`
GET /v1/internal/ui/services?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForQueryRecord: function(request, { dc, index, id }) {
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/health/service/${id}?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
});

View File

@ -1,31 +1,44 @@
import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { SLUG_KEY } from 'consul-ui/models/session';
import Adapter from './application';
import { SLUG_KEY } from 'consul-ui/models/session';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { NSPACE_KEY } from 'consul-ui/models/nspace';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, index, id }) {
requestForQuery: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/session/node/${id}?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForQueryRecord: function(request, { dc, index, id }) {
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/session/info/${id}?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForDeleteRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/session/destroy/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/session/destroy/${data[SLUG_KEY]}?${params}
`;
},
});

View File

@ -1,31 +1,44 @@
import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application';
import Adapter from './application';
import { inject as service } from '@ember/service';
import { SLUG_KEY } from 'consul-ui/models/token';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { NSPACE_KEY } from 'consul-ui/models/nspace';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
store: service('store'),
requestForQuery: function(request, { dc, index, role, policy }) {
requestForQuery: function(request, { dc, ns, index, role, policy }) {
return request`
GET /v1/acl/tokens?${{ role, policy, dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForQueryRecord: function(request, { dc, index, id }) {
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/acl/token/${id}?${{ dc }}
${{ index }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
requestForCreateRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/acl/token?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/acl/token?${params}
${{
Description: serialized.Description,
@ -44,14 +57,19 @@ export default Adapter.extend({
// If a token has Rules, use the old API
if (typeof data['Rules'] !== 'undefined') {
// https://www.consul.io/api/acl/legacy.html#update-acl-token
// as we are using the old API we don't need to specify a nspace
return request`
PUT /v1/acl/update?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/acl/update?${this.formatDatacenter(data[DATACENTER_KEY])}
${serialized}
`;
}
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/acl/token/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
PUT /v1/acl/token/${data[SLUG_KEY]}?${params}
${{
Description: serialized.Description,
@ -63,8 +81,12 @@ export default Adapter.extend({
`;
},
requestForDeleteRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
DELETE /v1/acl/token/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
DELETE /v1/acl/token/${data[SLUG_KEY]}?${params}
`;
},
requestForSelf: function(request, serialized, { dc, index, secret }) {
@ -77,15 +99,18 @@ export default Adapter.extend({
${{ index }}
`;
},
requestForCloneRecord: function(request, serialized, unserialized) {
requestForCloneRecord: function(request, serialized, data) {
// this uses snapshots
const id = unserialized[SLUG_KEY];
const dc = unserialized[DATACENTER_KEY];
const id = data[SLUG_KEY];
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return request`
PUT /v1/acl/token/${id}/clone?${{ [API_DATACENTER_KEY]: dc }}
PUT /v1/acl/token/${id}/clone?${params}
`;
},
// TODO: self doesn't get passed a snapshot right now
@ -94,11 +119,11 @@ export default Adapter.extend({
// plus we can't create Snapshots as they are private, see services/store.js
self: function(store, type, id, unserialized) {
return this.request(
function(adapter, request, serialized, unserialized) {
return adapter.requestForSelf(request, serialized, unserialized);
function(adapter, request, serialized, data) {
return adapter.requestForSelf(request, serialized, data);
},
function(serializer, respond, serialized, unserialized) {
return serializer.respondForQueryRecord(respond, serialized, unserialized);
function(serializer, respond, serialized, data) {
return serializer.respondForQueryRecord(respond, serialized, data);
},
unserialized,
type.modelName
@ -106,16 +131,18 @@ export default Adapter.extend({
},
clone: function(store, type, id, snapshot) {
return this.request(
function(adapter, request, serialized, unserialized) {
return adapter.requestForCloneRecord(request, serialized, unserialized);
function(adapter, request, serialized, data) {
return adapter.requestForCloneRecord(request, serialized, data);
},
function(serializer, respond, serialized, unserialized) {
(serializer, respond, serialized, data) => {
// here we just have to pass through the dc (like when querying)
// eventually the id is created with this dc value and the id talen from the
// eventually the id is created with this dc value and the id taken from the
// json response of `acls/token/*/clone`
return serializer.respondForQueryRecord(respond, {
[API_DATACENTER_KEY]: unserialized[SLUG_KEY],
});
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
...this.formatNspace(data[NSPACE_KEY]),
};
return serializer.respondForQueryRecord(respond, params);
},
snapshot,
type.modelName

View File

@ -0,0 +1,134 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { set } from '@ember/object';
import { next } from '@ember/runloop';
const TAB = 9;
const ENTER = 13;
const ESC = 27;
const SPACE = 32;
const END = 35;
const HOME = 36;
const ARROW_UP = 38;
const ARROW_DOWN = 40;
const keys = {
vertical: {
[ARROW_DOWN]: function($items, i = -1) {
return (i + 1) % $items.length;
},
[ARROW_UP]: function($items, i = 0) {
if (i === 0) {
return $items.length - 1;
} else {
return i - 1;
}
},
[HOME]: function($items, i) {
return 0;
},
[END]: function($items, i) {
return $items.length - 1;
},
},
horizontal: {},
};
const COMPONENT_ID = 'component-aria-menu-';
export default Component.extend({
tagName: '',
dom: service('dom'),
guid: '',
expanded: false,
direction: 'vertical',
init: function() {
this._super(...arguments);
set(this, 'guid', this.dom.guid(this));
this._listeners = this.dom.listeners();
},
didInsertElement: function() {
// TODO: How do you detect whether the children have changed?
// For now we know that these elements exist and never change
this.$menu = this.dom.element(`#${COMPONENT_ID}menu-${this.guid}`);
const labelledBy = this.$menu.getAttribute('aria-labelledby');
this.$trigger = this.dom.element(`#${labelledBy}`);
},
willDestroyElement: function() {
this._super(...arguments);
this._listeners.remove();
},
actions: {
keypress: function(e) {
// If the event is from the trigger and its not an opening/closing
// key then don't do anything
if (![ENTER, SPACE, ARROW_UP, ARROW_DOWN].includes(e.keyCode)) {
return;
}
e.stopPropagation();
// Also we may do this but not need it if we return early below
// although once we add support for [A-Za-z] it unlikely we won't use
// the keypress
// ^menuitem supports menuitemradio and menuitemcheckbox
// TODO: We need to use > somehow here so we don't select submenus
const $items = [...this.dom.elements('[role^="menuitem"]', this.$menu)];
if (!this.expanded) {
this.$trigger.dispatchEvent(new MouseEvent('click'));
if (e.keyCode === ENTER || e.keyCode === SPACE) {
$items[0].focus();
return;
}
}
// this will prevent anything happening if you haven't pushed a
// configurable key
if (typeof keys[this.direction][e.keyCode] === 'undefined') {
return;
}
const $focused = this.dom.element('[role="menuitem"]:focus', this.$menu);
let i;
if ($focused) {
i = $items.findIndex(function($item) {
return $item === $focused;
});
}
const $next = $items[keys[this.direction][e.keyCode]($items, i)];
$next.focus();
},
// TODO: The argument here needs to change to an event
// see toggle-button.change
change: function(open) {
if (open) {
this.actions.open.apply(this, []);
} else {
this.actions.close.apply(this, []);
}
},
close: function(e) {
this._listeners.remove();
set(this, 'expanded', false);
// TODO: Find a better way to do this without using next
// This is needed so when you press shift tab to leave the menu
// and go to the previous button, it doesn't focus the trigger for
// the menu itself
next(() => {
this.$trigger.removeAttribute('tabindex');
});
},
open: function(e) {
set(this, 'expanded', true);
// Take the trigger out of the tabbing whilst the menu is open
this.$trigger.setAttribute('tabindex', '-1');
this._listeners.add(this.dom.document(), {
keydown: e => {
// Keep focus on the trigger when you close via ESC
if (e.keyCode === ESC) {
this.$trigger.focus();
}
if (e.keyCode === TAB || e.keyCode === ESC) {
this.$trigger.dispatchEvent(new MouseEvent('click'));
return;
}
this.actions.keypress.apply(this, [e]);
},
});
},
},
});

View File

@ -56,7 +56,7 @@ export default Component.extend(SlotsMixin, WithListeners, {
},
open: function() {
if (!get(this, 'allOptions.closed')) {
set(this, 'allOptions', this.repo.findAllByDatacenter(this.dc));
set(this, 'allOptions', this.repo.findAllByDatacenter(this.dc, this.nspace));
}
},
save: function(item, items, success = function() {}) {

View File

@ -1,7 +0,0 @@
import Component from '@ember/component';
import WithClickOutside from 'consul-ui/mixins/click-outside';
export default Component.extend(WithClickOutside, {
tagName: 'ul',
onchange: function() {},
});

View File

@ -1,23 +1,26 @@
import Component from '@ember/component';
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
export default Component.extend({
dom: service('dom'),
isDropdownVisible: false,
didInsertElement: function() {
this.dom.root().classList.remove('template-with-vertical-menu');
},
// TODO: Right now this is the only place where we need permissions
// but we are likely to need it elsewhere, so probably need a nice helper
canManageNspaces: computed('permissions', function() {
return (
typeof (this.permissions || []).find(function(item) {
return item.Resource === 'operator' && item.Access === 'write' && item.Allow;
}) !== 'undefined'
);
}),
actions: {
dropdown: function(e) {
if (get(this, 'dcs.length') > 0) {
set(this, 'isDropdownVisible', !this.isDropdownVisible);
}
},
change: function(e) {
const dom = this.dom;
const win = dom.viewport();
const $root = dom.root();
const $body = dom.element('body');
const win = this.dom.viewport();
const $root = this.dom.root();
const $body = this.dom.element('body');
if (e.target.checked) {
$root.classList.add('template-with-vertical-menu');
$body.style.height = $root.style.height = win.innerHeight + 'px';

View File

@ -73,12 +73,10 @@ export default ChildSelectorComponent.extend({
}
// potentially the item could change between load, so we don't check
// anything to see if its already loaded here
const repo = this.repo;
// TODO: Temporarily add dc here, will soon be serialized onto the policy itself
const dc = this.dc;
const slugKey = repo.getSlugKey();
const slugKey = this.repo.getSlugKey();
const slug = get(value, slugKey);
updateArrayObject(items, repo.findBySlug(slug, dc), slugKey, slug);
updateArrayObject(items, this.repo.findBySlug(slug, this.dc, this.nspace), slugKey, slug);
}
},
},

View File

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

View File

@ -0,0 +1,54 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { set } from '@ember/object';
export default Component.extend({
dom: service('dom'),
// TODO(octane): Remove when we can move to glimmer components
// so we aren't using ember-test-selectors
// supportsDataTestProperties: true,
// the above doesn't seem to do anything so still need to find a way
// to pass through data-test-properties
tagName: '',
// TODO: reserved for the moment but we don't need it yet
onblur: null,
onchange: function() {},
init: function() {
this._super(...arguments);
this.guid = this.dom.guid(this);
this._listeners = this.dom.listeners();
},
didInsertElement: function() {
this._super(...arguments);
// TODO(octane): move to ref
set(this, 'input', this.dom.element(`#toggle-button-${this.guid}`));
set(this, 'label', this.input.nextElementSibling);
},
willDestroyElement: function() {
this._super(...arguments);
this._listeners.remove();
},
actions: {
click: function(e) {
e.preventDefault();
this.input.checked = !this.input.checked;
this.actions.change.apply(this, [e]);
},
change: function(e) {
if (this.input.checked) {
// default onblur event
this._listeners.remove();
this._listeners.add(this.dom.document(), 'click', e => {
if (this.dom.isOutside(this.label, e.target)) {
this.input.checked = false;
// TODO: This should be an event
this.onchange(this.input.checked);
this._listeners.remove();
}
});
}
// TODO: This should be an event
this.onchange(this.input.checked);
},
},
});

View File

@ -9,17 +9,27 @@ export default Controller.extend({
this.form = this.builder.form('intention');
},
setProperties: function(model) {
const sourceName = get(model.item, 'SourceName');
const destinationName = get(model.item, 'DestinationName');
let source = model.items.findBy('Name', sourceName);
let destination = model.items.findBy('Name', destinationName);
let source = model.services.findBy('Name', model.item.SourceName);
if (!source) {
source = { Name: sourceName };
model.items = [source].concat(model.items);
source = { Name: model.item.SourceName };
model.services = [source].concat(model.services);
}
let destination = model.services.findBy('Name', model.item.DestinationName);
if (!destination) {
destination = { Name: destinationName };
model.items = [destination].concat(model.items);
destination = { Name: model.item.DestinationName };
model.services = [destination].concat(model.services);
}
let sourceNS = model.nspaces.findBy('Name', model.item.SourceNS);
if (!sourceNS) {
sourceNS = { Name: model.item.SourceNS };
model.nspaces = [sourceNS].concat(model.nspaces);
}
let destinationNS = model.nspaces.findBy('Name', model.item.DestinationNS);
if (!destinationNS) {
destinationNS = { Name: model.item.DestinationNS };
model.nspaces = [destinationNS].concat(model.nspaces);
}
this._super({
...model,
@ -27,6 +37,8 @@ export default Controller.extend({
item: this.form.setData(model.item).getData(),
SourceName: source,
DestinationName: destination,
SourceNS: sourceNS,
DestinationNS: destinationNS,
},
});
},
@ -35,34 +47,25 @@ export default Controller.extend({
return template.replace(/{{term}}/g, term);
},
isUnique: function(term) {
return !this.items.findBy('Name', term);
return !this.services.findBy('Name', term);
},
change: function(e, value, item) {
const event = this.dom.normalizeEvent(e, value);
const form = this.form;
const target = event.target;
let name;
let selected;
let match;
let name, selected, match;
switch (target.name) {
case 'SourceName':
case 'DestinationName':
case 'SourceNS':
case 'DestinationNS':
name = selected = target.value;
// Names can be selected Service EmberObjects or typed in strings
// if its not a string, use the `Name` from the Service EmberObject
if (typeof name !== 'string') {
name = get(target.value, 'Name');
}
// see if the name is already in the list
match = this.items.filterBy('Name', name);
if (match.length === 0) {
// if its not make a new 'fake' Service that doesn't exist yet
// and add it to the possible services to make an intention between
selected = { Name: name };
const items = [selected].concat(this.items.toArray());
set(this, 'items', items);
}
// mutate the value with the string name
// which will be handled by the form
target.value = name;
@ -71,6 +74,23 @@ export default Controller.extend({
// the current selection
// basically the difference between
// `item.DestinationName` and just `DestinationName`
// see if the name is already in the list
match = this.services.filterBy('Name', name);
if (match.length === 0) {
// if its not make a new 'fake' Service that doesn't exist yet
// and add it to the possible services to make an intention between
selected = { Name: name };
switch (target.name) {
case 'SourceName':
case 'DestinationName':
set(this, 'services', [selected].concat(this.services.toArray()));
break;
case 'SourceNS':
case 'DestinationNS':
set(this, 'nspaces', [selected].concat(this.nspaces.toArray()));
break;
}
}
set(this, target.name, selected);
break;
}

View File

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

View File

@ -0,0 +1,38 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
dom: service('dom'),
builder: service('form'),
init: function() {
this._super(...arguments);
this.form = this.builder.form('nspace');
},
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)
);
},
actions: {
change: function(e, value, item) {
const event = this.dom.normalizeEvent(e, value);
const form = this.form;
try {
form.handleEvent(event);
} catch (err) {
const target = event.target;
switch (target.name) {
default:
throw err;
}
}
},
},
});

View File

@ -0,0 +1,23 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithEventSource, WithSearching, {
queryParams: {
s: {
as: 'filter',
},
},
init: function() {
this.searchParams = {
nspace: 's',
};
this._super(...arguments);
},
searchable: computed('items.[]', function() {
return get(this, 'searchables.nspace')
.add(this.items)
.search(this.terms);
}),
});

View File

@ -1,5 +1,61 @@
import config from './config/environment';
export default function(str) {
const user = window.localStorage.getItem(str);
return user !== null ? user : config[str];
}
import _config from './config/environment';
const doc = document;
const getDevEnvVars = function() {
return doc.cookie
.split(';')
.filter(item => item !== '')
.map(item => item.trim().split('='));
};
const getUserEnvVar = function(str) {
return window.localStorage.getItem(str);
};
// TODO: Look at `services/client` for pulling
// HTTP headers in here so we can let things be controlled
// via HTTP proxies, for example turning off blocking
// queries if its a busy cluster
// const getOperatorEnvVars = function() {}
// TODO: Not necessarily here but the entire app should
// use the `env` export not the `default` one
// but we might also change the name of this file, so wait for that first
export const env = function(str) {
let user = null;
switch (str) {
case 'CONSUL_UI_DISABLE_REALTIME':
case 'CONSUL_UI_DISABLE_ANCHOR_SELECTION':
case 'CONSUL_UI_REALTIME_RUNNER':
user = getUserEnvVar(str);
break;
}
// We use null here instead of an undefined check
// as localStorage will return null if not set
return user !== null ? user : _config[str];
};
export const config = function(key) {
let $;
switch (_config.environment) {
case 'development':
case 'staging':
case 'test':
$ = getDevEnvVars().reduce(function(prev, [key, value]) {
const val = !!JSON.parse(String(value).toLowerCase());
switch (key) {
case 'CONSUL_ACLS_ENABLE':
prev['CONSUL_ACLS_ENABLED'] = val;
break;
case 'CONSUL_NSPACES_ENABLE':
prev['CONSUL_NSPACES_ENABLED'] = val;
break;
default:
prev[key] = value;
}
return prev;
}, {});
if (typeof $[key] !== 'undefined') {
return $[key];
}
break;
}
return _config[key];
};
export default env;

View File

@ -0,0 +1,9 @@
import validations from 'consul-ui/validations/nspace';
import builderFactory from 'consul-ui/utils/form/builder';
const builder = builderFactory();
export default function(container, name = '', v = validations, form = builder) {
return form(name, {})
.setValidators(v)
.add(container.form('policy'))
.add(container.form('role'));
}

View File

@ -1,7 +1,9 @@
import { helper } from '@ember/component/helper';
import $ from 'consul-ui/config/environment';
import { config } from 'consul-ui/env';
// TODO: env actually uses config values not env values
// see `app/env` for the renaming TODO's also
export function env([name, def = ''], hash) {
return $[name] != null ? $[name] : def;
return config(name) != null ? config(name) : def;
}
export default helper(env);

View File

@ -0,0 +1,39 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { hrefTo } from 'consul-ui/helpers/href-to';
const getRouteParams = function(route, params = {}) {
return route.paramNames.map(function(item) {
if (typeof params[item] !== 'undefined') {
return params[item];
}
return route.params[item];
});
};
export default Helper.extend({
router: service('router'),
compute([params], hash) {
let current = this.router.currentRoute;
let parent;
let atts = getRouteParams(current, params);
// walk up the entire route/s replacing any instances
// of the specified params with the values specified
while ((parent = current.parent)) {
atts = atts.concat(getRouteParams(parent, params));
current = parent;
}
let route = this.router.currentRoute.name;
// TODO: this is specific to consul/nspaces
// 'ideally' we could try and do this elsewhere
// not super important though.
// This will turn an URL that has no nspace (/ui/dc-1/nodes) into one
// that does have a namespace (/ui/~nspace/dc-1/nodes) if you href-mut with
// a nspace parameter
if (typeof params.nspace !== 'undefined' && !route.startsWith('nspace.')) {
route = `nspace.${route}`;
atts.push(params.nspace);
}
//
return hrefTo(this, this.router, [route, ...atts.reverse()], hash);
},
});

View File

@ -1,29 +1,42 @@
// This helper requires `ember-href-to` for the moment at least
// It's similar code but allows us to check on the type of route
// (dynamic or wildcard) and encode or not depending on the type
import { inject as service } from '@ember/service';
import Helper from '@ember/component/helper';
import { hrefTo } from 'ember-href-to/helpers/href-to';
import { hrefTo as _hrefTo } from 'ember-href-to/helpers/href-to';
import wildcard from 'consul-ui/utils/routing/wildcard';
import { routes } from 'consul-ui/router';
const isWildcard = wildcard(routes);
export const hrefTo = function(owned, router, [targetRouteName, ...rest], namedArgs) {
if (isWildcard(targetRouteName)) {
rest = rest.map(function(item, i) {
return item
.split('/')
.map(encodeURIComponent)
.join('/');
});
}
if (namedArgs.params) {
return _hrefTo(owned, namedArgs.params);
} else {
// we don't check to see if nspaces are enabled here as routes
// with beginning with 'nspace' only exist if nspaces are enabled
// this globally converts non-nspaced href-to's to nspace aware
// href-to's only if you are within a namespace
if (router.currentRouteName.startsWith('nspace.') && targetRouteName.startsWith('dc.')) {
targetRouteName = `nspace.${targetRouteName}`;
}
return _hrefTo(owned, [targetRouteName, ...rest]);
}
};
export default Helper.extend({
compute([targetRouteName, ...rest], namedArgs) {
if (isWildcard(targetRouteName)) {
rest = rest.map(function(item, i) {
return item
.split('/')
.map(encodeURIComponent)
.join('/');
});
}
if (namedArgs.params) {
return hrefTo(this, namedArgs.params);
} else {
return hrefTo(this, [targetRouteName, ...rest]);
}
router: service('router'),
compute(params, hash) {
return hrefTo(this, this.router, params, hash);
},
});

View File

@ -6,8 +6,11 @@ import { observer } from '@ember/object';
export default Helper.extend({
router: service('router'),
compute(params) {
return this.router.isActive(...params);
compute([targetRouteName, ...rest]) {
if (this.router.currentRouteName.startsWith('nspace.') && targetRouteName.startsWith('dc.')) {
targetRouteName = `nspace.${targetRouteName}`;
}
return this.router.isActive(...[targetRouteName, ...rest]);
},
onURLChange: observer('router.currentURL', function() {
this.recompute();

View File

@ -6,6 +6,7 @@ import token from 'consul-ui/forms/token';
import policy from 'consul-ui/forms/policy';
import role from 'consul-ui/forms/role';
import intention from 'consul-ui/forms/intention';
import nspace from 'consul-ui/forms/nspace';
export function initialize(application) {
// Service-less injection using private properties at a per-project level
@ -17,6 +18,7 @@ export function initialize(application) {
policy: policy,
role: role,
intention: intention,
nspace: nspace,
};
FormBuilder.reopen({
form: function(name) {

View File

@ -0,0 +1,61 @@
import Route from '@ember/routing/route';
import { config } from 'consul-ui/env';
import { routes } from 'consul-ui/router';
import flat from 'flat';
let initialize = function() {};
Route.reopen(
['modelFor', 'transitionTo', 'replaceWith', 'paramsFor'].reduce(function(prev, item) {
prev[item] = function(routeName, ...rest) {
const isNspaced = this.routeName.startsWith('nspace.');
if (routeName === 'nspace') {
if (isNspaced || this.routeName === 'nspace') {
return this._super(...arguments);
} else {
return {
nspace: '~',
};
}
}
if (isNspaced && routeName.startsWith('dc')) {
return this._super(...[`nspace.${routeName}`, ...rest]);
}
return this._super(...arguments);
};
return prev;
}, {})
);
if (config('CONSUL_NSPACES_ENABLED')) {
const dotRe = /\./g;
initialize = function(container) {
const all = Object.keys(flat(routes))
.filter(function(item) {
return item.startsWith('dc');
})
.map(function(item) {
return item.replace('._options.path', '').replace(dotRe, '/');
});
all.forEach(function(item) {
let route = container.resolveRegistration(`route:${item}`);
if (!route) {
item = `${item}/index`;
route = container.resolveRegistration(`route:${item}`);
}
route.reopen({
templateName: item
.replace('/root-create', '/create')
.replace('/create', '/edit')
.replace('/folder', '/index'),
});
container.register(`route:nspace/${item}`, route);
const controller = container.resolveRegistration(`controller:${item}`);
if (controller) {
container.register(`controller:nspace/${item}`, controller);
}
});
};
}
export default {
initialize,
};

View File

@ -9,6 +9,7 @@ import node from 'consul-ui/search/filters/node';
import nodeService from 'consul-ui/search/filters/node/service';
import serviceNode from 'consul-ui/search/filters/service/node';
import service from 'consul-ui/search/filters/service';
import nspace from 'consul-ui/search/filters/nspace';
import filterableFactory from 'consul-ui/utils/search/filterable';
const filterable = filterableFactory();
@ -27,6 +28,7 @@ export function initialize(application) {
serviceInstance: serviceNode(filterable),
nodeservice: nodeService(filterable),
service: service(filterable),
nspace: nspace(filterable),
};
Builder.reopen({
searchable: function(name) {

View File

@ -1,10 +1,11 @@
import env from 'consul-ui/env';
import env, { config } from 'consul-ui/env';
export function initialize(container) {
if (env('CONSUL_UI_DISABLE_REALTIME')) {
return;
}
['node', 'coordinate', 'session', 'service', 'proxy']
.concat(config('CONSUL_NSPACES_ENABLED') ? ['nspace/enabled'] : [])
.map(function(item) {
// create repositories that return a promise resolving to an EventSource
return {
@ -76,6 +77,18 @@ export function initialize(container) {
},
},
])
.concat(
config('CONSUL_NSPACES_ENABLED')
? [
{
route: 'dc/nspaces/index',
services: {
repo: 'repository/nspace/enabled/event-source',
},
},
]
: []
)
.forEach(function(definition) {
if (typeof definition.extend !== 'undefined') {
// Create the class instances that we need
@ -90,6 +103,9 @@ export function initialize(container) {
// but hardcode this for the moment
if (typeof definition.route !== 'undefined') {
container.inject(`route:${definition.route}`, name, `service:${servicePath}`);
if (config('CONSUL_NSPACES_ENABLED') && definition.route.startsWith('dc/')) {
container.inject(`route:nspace/${definition.route}`, name, `service:${servicePath}`);
}
} else {
container.inject(`service:${definition.service}`, name, `service:${servicePath}`);
}

View File

@ -0,0 +1,28 @@
import { config } from 'consul-ui/env';
export function initialize(container) {
if (config('CONSUL_NSPACES_ENABLED')) {
['dc', 'settings', 'dc.intentions.edit', 'dc.intentions.create'].forEach(function(item) {
container.inject(`route:${item}`, 'nspacesRepo', 'service:repository/nspace/enabled');
container.inject(`route:nspace.${item}`, 'nspacesRepo', 'service:repository/nspace/enabled');
});
container.inject('route:application', 'nspacesRepo', 'service:repository/nspace/enabled');
container
.lookup('service:dom')
.root()
.classList.add('has-nspaces');
}
// FIXME: This needs to live in its own initializer, either:
// 1. Make it be about adding classes to the root dom node
// 2. Make it be about config and things to do on initialization re: config
// If we go with 1 then we need to move both this and the above nspaces class
if (config('CONSUL_ACLS_ENABLED')) {
container
.lookup('service:dom')
.root()
.classList.add('has-acls');
}
}
export default {
initialize,
};

View File

@ -1,42 +0,0 @@
import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember/service';
import { next } from '@ember/runloop';
// TODO: Potentially move this to dom service
const isOutside = function(element, e, doc = document) {
if (element) {
const isRemoved = !e.target || !doc.contains(e.target);
const isInside = element === e.target || element.contains(e.target);
return !isRemoved && !isInside;
} else {
return false;
}
};
const handler = function(e) {
const el = this.element;
if (isOutside(el, e)) {
this.onblur(e);
}
};
export default Mixin.create({
dom: service('dom'),
init: function() {
this._super(...arguments);
this.handler = handler.bind(this);
},
onchange: function() {},
onblur: function() {},
didInsertElement: function() {
this._super(...arguments);
const doc = this.dom.document();
next(this, () => {
doc.addEventListener('click', this.handler);
});
},
willDestroyElement: function() {
this._super(...arguments);
const doc = this.dom.document();
doc.removeEventListener('click', this.handler);
},
});

View File

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

View File

@ -1,10 +1,9 @@
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({
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
Description: attr('string'),
@ -15,8 +14,11 @@ const model = Model.extend({
Precedence: attr('number'),
SourceType: attr('string', { defaultValue: 'consul' }),
Action: attr('string', { defaultValue: 'deny' }),
// These are in the API response but up until now
// aren't used for anything
DefaultAddr: attr('string'),
DefaultPort: attr('number'),
//
Meta: attr(),
Datacenter: attr('string'),
CreatedAt: attr('date'),
@ -24,12 +26,3 @@ const model = Model.extend({
CreateIndex: attr('number'),
ModifyIndex: attr('number'),
});
// TODO: Remove this in favour of just specifying it in the Adapter
export const ATTRS = writable(model, [
'Action',
'SourceName',
'DestinationName',
'SourceType',
'Description',
]);
export default model;

View File

@ -22,6 +22,7 @@ export default Model.extend({
ModifyIndex: attr('number'),
Session: attr('string'),
Datacenter: attr('string'),
Namespace: attr('string'),
isFolder: computed('Key', function() {
return isFolder(get(this, 'Key') || '');

View File

@ -0,0 +1,20 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export const PRIMARY_KEY = 'Name';
// keep this for consistency
export const SLUG_KEY = 'Name';
export const NSPACE_KEY = 'Namespace';
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
Description: attr('string', { defaultValue: '' }),
// TODO: Is there some sort of date we can use here
DeletedAt: attr('string'),
ACLs: attr(undefined, function() {
return { defaultValue: { PolicyDefaults: [], RoleDefaults: [] } };
}),
SyncTime: attr('number'),
});

View File

@ -20,6 +20,7 @@ export default Model.extend({
CreateTime: attr('date'),
//
Datacenter: attr('string'),
Namespace: attr('string'),
Datacenters: attr(),
CreateIndex: attr('number'),
ModifyIndex: attr('number'),

View File

@ -11,4 +11,6 @@ export default Model.extend({
Node: attr('string'),
ServiceProxy: attr(),
SyncTime: attr('number'),
Datacenter: attr('string'),
Namespace: attr('string'),
});

View File

@ -26,6 +26,7 @@ export default Model.extend({
CreateTime: attr('date'),
//
Datacenter: attr('string'),
Namespace: attr('string'),
// TODO: Figure out whether we need this or not
Datacenters: attr(),
Hash: attr('string'),

View File

@ -28,6 +28,7 @@ export default Model.extend({
ChecksWarning: attr(),
Nodes: attr(),
Datacenter: attr('string'),
Namespace: attr('string'),
Node: attr(),
Service: attr(),
Checks: attr(),

View File

@ -20,5 +20,6 @@ export default Model.extend({
},
}),
Datacenter: attr('string'),
Namespace: attr('string'),
SyncTime: attr('number'),
});

View File

@ -21,6 +21,7 @@ export default Model.extend({
defaultValue: '',
}),
Datacenter: attr('string'),
Namespace: attr('string'),
Local: attr('boolean'),
Policies: attr({
defaultValue: function() {

View File

@ -1,16 +1,16 @@
import EmberRouter from '@ember/routing/router';
import config from './config/environment';
import { config } from 'consul-ui/env';
import walk from 'consul-ui/utils/routing/walk';
const Router = EmberRouter.extend({
location: config.locationType,
rootURL: config.rootURL,
location: config('locationType'),
rootURL: config('rootURL'),
});
export const routes = {
// Our parent datacenter resource sets the namespace
// for the entire application
dc: {
_options: { path: ':dc' },
_options: { path: '/:dc' },
// Services represent a consul service
services: {
_options: { path: '/services' },
@ -107,4 +107,19 @@ export const routes = {
_options: { path: '/*path' },
},
};
if (config('CONSUL_NSPACES_ENABLED')) {
routes.dc.nspaces = {
_options: { path: '/namespaces' },
edit: {
_options: { path: '/:name' },
},
create: {
_options: { path: '/create' },
},
};
routes.nspace = {
_options: { path: '/:nspace' },
dc: routes.dc,
};
}
export default Router.map(walk(routes));

View File

@ -14,6 +14,7 @@ export default Route.extend(WithBlockingActions, {
init: function() {
this._super(...arguments);
},
nspacesRepo: service('repository/nspace/disabled'),
repo: service('repository/dc'),
settings: service('settings'),
actions: {
@ -27,6 +28,7 @@ export default Route.extend(WithBlockingActions, {
hash({
loading: !$root.classList.contains('ember-loading'),
dc: dc,
nspace: this.nspacesRepo.getActive(),
}).then(model => {
next(() => {
const controller = this.controllerFor('application');
@ -81,8 +83,8 @@ export default Route.extend(WithBlockingActions, {
error.status.toString().indexOf('5') !== 0
? this.repo.getActive()
: model && model.dc
? model.dc
: { Name: 'Error' },
? model.dc
: { Name: 'Error' },
dcs: model && model.dcs ? model.dcs : [],
})
.then(model => {

View File

@ -1,21 +1,70 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get } from '@ember/object';
// TODO: We should potentially move all these nspace related things
// up a level to application.js
const findActiveNspace = function(nspaces, nspace) {
let found = nspaces.find(function(item) {
return item.Name === nspace.Name;
});
if (typeof found === 'undefined') {
// if we can't find the nspace that was saved
// try default
found = nspaces.find(function(item) {
return item.Name === 'default';
});
// if there is no default just choose the first
if (typeof found === 'undefined') {
found = nspaces.firstObject;
}
}
return found;
};
export default Route.extend({
repo: service('repository/dc'),
settings: service('settings'),
nspacesRepo: service('repository/nspace/disabled'),
settingsRepo: service('settings'),
model: function(params) {
const repo = this.repo;
const nspacesRepo = this.nspacesRepo;
const settingsRepo = this.settingsRepo;
return hash({
dcs: repo.findAll(),
}).then(function(model) {
return hash({
...model,
...{
dc: repo.findBySlug(params.dc, model.dcs),
},
nspaces: nspacesRepo.findAll(),
nspace: nspacesRepo.getActive(),
token: settingsRepo.findBySlug('token'),
})
.then(function(model) {
return hash({
...model,
...{
dc: repo.findBySlug(params.dc, model.dcs),
// if there is only 1 namespace then use that
// otherwise find the namespace object that corresponds
// to the active one
nspace:
model.nspaces.length > 1
? findActiveNspace(model.nspaces, model.nspace)
: model.nspaces.firstObject,
},
});
})
.then(function(model) {
if (get(model, 'token.SecretID')) {
return hash({
...model,
...{
// When disabled nspaces is [], so nspace is undefined
permissions: nspacesRepo.authorize(params.dc, get(model, 'nspace.Name')),
},
});
} else {
return model;
}
});
});
},
setupController: function(controller, model) {
controller.setProperties(model);

View File

@ -3,11 +3,12 @@ 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, {
router: service('router'),
settings: service('settings'),
feedback: service('feedback'),
repo: service('repository/token'),
actions: {
authorize: function(secret) {
authorize: function(secret, nspace) {
const dc = this.modelFor('dc').dc.Name;
return this.feedback.execute(() => {
return this.repo.self(secret, dc).then(item => {
@ -16,6 +17,7 @@ export default Route.extend(WithBlockingActions, {
token: {
AccessorID: get(item, 'AccessorID'),
SecretID: secret,
Namespace: get(item, 'Namespace'),
},
})
.then(item => {
@ -29,7 +31,15 @@ export default Route.extend(WithBlockingActions, {
return false;
});
} else {
this.refresh();
if (get(item, 'token.Namespace') !== nspace) {
let routeName = this.router.currentRouteName;
if (!routeName.startsWith('nspace')) {
routeName = `nspace.${routeName}`;
}
return this.transitionTo(`${routeName}`, `~${get(item, 'token.Namespace')}`, dc);
} else {
this.refresh();
}
}
});
});

View File

@ -1,7 +1,7 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get, set } from '@ember/object';
import { get } from '@ember/object';
import WithAclActions from 'consul-ui/mixins/acl/with-actions';
@ -12,8 +12,9 @@ export default Route.extend(WithAclActions, {
this.repo.invalidate();
},
model: function(params) {
this.item = this.repo.create();
set(this.item, 'Datacenter', this.modelFor('dc').dc.Name);
this.item = this.repo.create({
Datacenter: this.modelFor('dc').dc.Name,
});
return hash({
create: true,
isLoading: false,

View File

@ -10,12 +10,13 @@ export default SingleRoute.extend(WithPolicyActions, {
tokenRepo: service('repository/token'),
model: function(params) {
const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1);
const tokenRepo = this.tokenRepo;
return this._super(...arguments).then(model => {
return hash({
...model,
...{
items: tokenRepo.findByPolicy(get(model.item, 'ID'), dc).catch(function(e) {
items: tokenRepo.findByPolicy(get(model.item, 'ID'), dc, nspace).catch(function(e) {
switch (get(e, 'errors.firstObject.status')) {
case '403':
case '401':

View File

@ -13,10 +13,12 @@ export default Route.extend(WithPolicyActions, {
},
},
model: function(params) {
const repo = this.repo;
return hash({
...repo.status({
items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name),
...this.repo.status({
items: this.repo.findAllByDatacenter(
this.modelFor('dc').dc.Name,
this.modelFor('nspace').nspace.substr(1)
),
}),
isLoading: false,
});

View File

@ -10,12 +10,13 @@ export default SingleRoute.extend(WithRoleActions, {
tokenRepo: service('repository/token'),
model: function(params) {
const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1);
const tokenRepo = this.tokenRepo;
return this._super(...arguments).then(model => {
return hash({
...model,
...{
items: tokenRepo.findByRole(get(model.item, 'ID'), dc).catch(function(e) {
items: tokenRepo.findByRole(get(model.item, 'ID'), dc, nspace).catch(function(e) {
switch (get(e, 'errors.firstObject.status')) {
case '403':
case '401':

View File

@ -13,10 +13,12 @@ export default Route.extend(WithRoleActions, {
},
},
model: function(params) {
const repo = this.repo;
return hash({
...repo.status({
items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name),
...this.repo.status({
items: this.repo.findAllByDatacenter(
this.modelFor('dc').dc.Name,
this.modelFor('nspace').nspace.substr(1)
),
}),
isLoading: false,
});

View File

@ -24,11 +24,14 @@ export default Route.extend(WithTokenActions, {
});
},
model: function(params) {
const repo = this.repo;
return hash({
...repo.status({
items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name),
...this.repo.status({
items: this.repo.findAllByDatacenter(
this.modelFor('dc').dc.Name,
this.modelFor('nspace').nspace.substr(1)
),
}),
nspace: this.modelFor('nspace').nspace.substr(1),
isLoading: false,
token: this.settings.findBySlug('token'),
});

View File

@ -1,32 +1,38 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get, set } from '@ember/object';
import { get } from '@ember/object';
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
// TODO: This route and the edit Route need merging somehow
export default Route.extend(WithIntentionActions, {
templateName: 'dc/intentions/edit',
repo: service('repository/intention'),
servicesRepo: service('repository/service'),
nspacesRepo: service('repository/nspace/disabled'),
beforeModel: function() {
this.repo.invalidate();
},
model: function(params) {
this.item = this.repo.create();
set(this.item, 'Datacenter', this.modelFor('dc').dc.Name);
const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1);
this.item = this.repo.create({
Datacenter: dc,
});
return hash({
create: true,
isLoading: false,
item: this.item,
items: this.servicesRepo.findAllByDatacenter(this.modelFor('dc').dc.Name),
intents: ['allow', 'deny'],
services: this.servicesRepo.findAllByDatacenter(dc, nspace),
nspaces: this.nspacesRepo.findAll(),
}).then(function(model) {
return {
...model,
...{
items: [{ Name: '*' }].concat(
model.items.toArray().filter(item => get(item, 'Kind') !== 'connect-proxy')
services: [{ Name: '*' }].concat(
model.services.toArray().filter(item => get(item, 'Kind') !== 'connect-proxy')
),
nspaces: [{ Name: '*' }].concat(model.nspaces.toArray()),
},
};
});

View File

@ -3,24 +3,32 @@ import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import WithAclActions from 'consul-ui/mixins/intention/with-actions';
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
export default Route.extend(WithAclActions, {
// TODO: This route and the create Route need merging somehow
export default Route.extend(WithIntentionActions, {
repo: service('repository/intention'),
servicesRepo: service('repository/service'),
nspacesRepo: service('repository/nspace/disabled'),
model: function(params) {
const dc = this.modelFor('dc').dc.Name;
// We load all of your services that you are able to see here
// as even if it doesn't exist in the namespace you are targetting
// you may want to add it after you've added the intention
const nspace = '*';
return hash({
isLoading: false,
item: this.repo.findBySlug(params.id, this.modelFor('dc').dc.Name),
items: this.servicesRepo.findAllByDatacenter(this.modelFor('dc').dc.Name),
intents: ['allow', 'deny'],
item: this.repo.findBySlug(params.id, dc, nspace),
services: this.servicesRepo.findAllByDatacenter(dc, nspace),
nspaces: this.nspacesRepo.findAll(),
}).then(function(model) {
return {
...model,
...{
items: [{ Name: '*' }].concat(
model.items.toArray().filter(item => get(item, 'Kind') !== 'connect-proxy')
services: [{ Name: '*' }].concat(
model.services.toArray().filter(item => get(item, 'Kind') !== 'connect-proxy')
),
nspaces: [{ Name: '*' }].concat(model.nspaces.toArray()),
},
};
});

View File

@ -14,7 +14,10 @@ export default Route.extend(WithIntentionActions, {
},
model: function(params) {
return hash({
items: this.repo.findAllByDatacenter(this.modelFor('dc').dc.Name),
items: this.repo.findAllByDatacenter(
this.modelFor('dc').dc.Name,
this.modelFor('nspace').nspace.substr(1)
),
});
},
setupController: function(controller, model) {

View File

@ -1,7 +1,7 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get, set } from '@ember/object';
import { get } from '@ember/object';
import WithKvActions from 'consul-ui/mixins/kv/with-actions';
export default Route.extend(WithKvActions, {
@ -12,15 +12,17 @@ export default Route.extend(WithKvActions, {
},
model: function(params) {
const key = params.key || '/';
const repo = this.repo;
const dc = this.modelFor('dc').dc.Name;
this.item = repo.create();
set(this.item, 'Datacenter', dc);
const nspace = this.modelFor('nspace').nspace.substr(1);
this.item = this.repo.create({
Datacenter: dc,
Namespace: nspace,
});
return hash({
create: true,
isLoading: false,
item: this.item,
parent: repo.findBySlug(key, dc),
parent: this.repo.findBySlug(key, dc, nspace),
});
},
setupController: function(controller, model) {

View File

@ -12,11 +12,11 @@ export default Route.extend(WithKvActions, {
model: function(params) {
const key = params.key;
const dc = this.modelFor('dc').dc.Name;
const repo = this.repo;
const nspace = this.modelFor('nspace').nspace.substr(1);
return hash({
isLoading: false,
parent: repo.findBySlug(ascend(key, 1) || '/', dc),
item: repo.findBySlug(key, dc),
parent: this.repo.findBySlug(ascend(key, 1) || '/', dc, nspace),
item: this.repo.findBySlug(key, dc, nspace),
session: null,
}).then(model => {
// TODO: Consider loading this after initial page load
@ -25,7 +25,7 @@ export default Route.extend(WithKvActions, {
return hash({
...model,
...{
session: this.sessionRepo.findByKey(session, dc),
session: this.sessionRepo.findByKey(session, dc, nspace),
},
});
}

View File

@ -25,15 +25,15 @@ export default Route.extend(WithKvActions, {
model: function(params) {
let key = params.key || '/';
const dc = this.modelFor('dc').dc.Name;
const repo = this.repo;
const nspace = this.modelFor('nspace').nspace.substr(1);
return hash({
isLoading: false,
parent: repo.findBySlug(key, dc),
parent: this.repo.findBySlug(key, dc, nspace),
}).then(model => {
return hash({
...model,
...{
items: repo.findAllBySlug(get(model.parent, 'Key'), dc).catch(e => {
items: this.repo.findAllBySlug(get(model.parent, 'Key'), dc, nspace).catch(e => {
const status = get(e, 'errors.firstObject.status');
switch (status) {
case '403':

View File

@ -13,7 +13,7 @@ export default Route.extend({
model: function(params) {
const dc = this.modelFor('dc').dc.Name;
return hash({
items: this.repo.findAllByDatacenter(dc),
items: this.repo.findAllByDatacenter(dc, this.modelFor('nspace').nspace.substr(1)),
leader: this.repo.findByLeader(dc),
});
},

View File

@ -1,7 +1,6 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
@ -17,11 +16,12 @@ export default Route.extend(WithBlockingActions, {
},
model: function(params) {
const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1);
const name = params.name;
return hash({
item: this.repo.findBySlug(name, dc),
item: this.repo.findBySlug(name, dc, nspace),
sessions: this.sessionRepo.findByNode(name, dc, nspace),
tomography: this.coordinateRepo.findAllByNode(name, dc),
sessions: this.sessionRepo.findByNode(name, dc),
});
},
setupController: function(controller, model) {
@ -30,12 +30,11 @@ export default Route.extend(WithBlockingActions, {
actions: {
invalidateSession: function(item) {
const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1);
const controller = this.controller;
const repo = this.sessionRepo;
return this.feedback.execute(() => {
const node = get(item, 'Node');
return repo.remove(item).then(() => {
return repo.findByNode(node, dc).then(function(sessions) {
return this.sessionRepo.remove(item).then(() => {
return this.sessionRepo.findByNode(item.Node, dc, nspace).then(function(sessions) {
controller.setProperties({
sessions: sessions,
});

View File

@ -0,0 +1,14 @@
import Route from './edit';
import CreatingRoute from 'consul-ui/mixins/creating-route';
export default Route.extend(CreatingRoute, {
templateName: 'dc/nspaces/edit',
beforeModel: function() {
// we need to skip CreatingRoute.beforeModel here
// TODO(octane): ideally we'd like to call Route.beforeModel
// but its not clear how to do that with old ember
// maybe it will be more normal with modern ember
// up until now we haven't been calling super here anyway
// so this is probably ok until we can skip a parent super
},
});

View File

@ -0,0 +1,35 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import WithNspaceActions from 'consul-ui/mixins/nspace/with-actions';
export default Route.extend(WithNspaceActions, {
repo: service('repository/nspace'),
isCreate: function(params, transition) {
return transition.targetName.split('.').pop() === 'create';
},
model: function(params, transition) {
const repo = this.repo;
const create = this.isCreate(...arguments);
const dc = this.modelFor('dc').dc.Name;
return hash({
isLoading: false,
create: create,
dc: dc,
item: create
? Promise.resolve(
repo.create({
ACLs: {
PolicyDefaults: [],
RoleDefaults: [],
},
})
)
: repo.findBySlug(params.name),
});
},
setupController: function(controller, model) {
controller.setProperties(model);
},
});

View File

@ -0,0 +1,23 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import WithNspaceActions from 'consul-ui/mixins/nspace/with-actions';
export default Route.extend(WithNspaceActions, {
repo: service('repository/nspace'),
queryParams: {
s: {
as: 'filter',
replace: true,
},
},
model: function(params) {
return hash({
items: this.repo.findAll(),
isLoading: false,
});
},
setupController: function(controller, model) {
controller.setProperties(model);
},
});

View File

@ -15,7 +15,6 @@ export default Route.extend({
},
},
model: function(params) {
const repo = this.repo;
let terms = params.s || '';
// we check for the old style `status` variable here
// and convert it to the new style filter=status:critical
@ -32,7 +31,10 @@ export default Route.extend({
}
return hash({
terms: terms !== '' ? terms.split('\n') : [],
items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name),
items: this.repo.findAllByDatacenter(
this.modelFor('dc').dc.Name,
this.modelFor('nspace').nspace.substr(1)
),
});
},
setupController: function(controller, model) {

View File

@ -7,12 +7,11 @@ export default Route.extend({
repo: service('repository/service'),
proxyRepo: service('repository/proxy'),
model: function(params) {
const repo = this.repo;
const proxyRepo = this.proxyRepo;
const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1);
return hash({
item: repo.findInstanceBySlug(params.id, params.node, params.name, dc),
}).then(function(model) {
item: this.repo.findInstanceBySlug(params.id, params.node, params.name, dc, nspace),
}).then(model => {
// this will not be run in a blocking loop, but this is ok as
// its highly unlikely that a service will suddenly change to being a
// connect-proxy or vice versa so leave as is for now
@ -21,7 +20,7 @@ export default Route.extend({
// proxies and mesh-gateways can't have proxies themselves so don't even look
['connect-proxy', 'mesh-gateway'].includes(get(model.item, 'Kind'))
? null
: proxyRepo.findInstanceBySlug(params.id, params.node, params.name, dc),
: this.proxyRepo.findInstanceBySlug(params.id, params.node, params.name, dc, nspace),
...model,
});
});

View File

@ -12,12 +12,10 @@ export default Route.extend({
},
},
model: function(params) {
const repo = this.repo;
const settings = this.settings;
const dc = this.modelFor('dc').dc.Name;
return hash({
item: repo.findBySlug(params.name, dc),
urls: settings.findBySlug('urls'),
item: this.repo.findBySlug(params.name, dc, this.modelFor('nspace').nspace.substr(1)),
urls: this.settings.findBySlug('urls'),
dc: dc,
});
},

View File

@ -0,0 +1,28 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
export default Route.extend({
repo: service('repository/dc'),
router: service('router'),
model: function(params) {
return hash({
item: this.repo.getActive(),
nspace: params.nspace,
});
},
afterModel: function(params) {
// We need to redirect if someone doesn't specify
// the section they want, but not redirect if the 'section' is
// specified (i.e. /dc-1/ vs /dc-1/services)
// check how many parts are in the URL to figure this out
// if there is a better way to do this then would be good to change
if (this.router.currentURL.split('/').length < 4) {
if (!params.nspace.startsWith('~')) {
this.transitionTo('dc.services', params.nspace);
} else {
this.transitionTo('nspace.dc.services', params.nspace, params.item.Name);
}
}
},
});

View File

@ -7,10 +7,13 @@ export default Route.extend({
client: service('client/http'),
repo: service('settings'),
dcRepo: service('repository/dc'),
nspacesRepo: service('repository/nspace/disabled'),
model: function(params) {
return hash({
item: this.repo.findAll(),
dcs: this.dcRepo.findAll(),
nspaces: this.nspacesRepo.findAll(),
nspace: this.nspacesRepo.getActive(),
}).then(model => {
if (typeof get(model.item, 'client.blocking') === 'undefined') {
set(model, 'item.client', { blocking: true });

View File

@ -13,15 +13,22 @@ export default Route.extend({
typeof repo !== 'undefined'
);
const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1);
const create = this.isCreate(...arguments);
return hash({
isLoading: false,
dc: dc,
nspace: nspace,
create: create,
...repo.status({
item: create
? Promise.resolve(repo.create({ Datacenter: dc }))
: repo.findBySlug(params.id, dc),
? Promise.resolve(
repo.create({
Datacenter: dc,
Namespace: nspace,
})
)
: repo.findBySlug(params.id, dc, nspace),
}),
});
},

View File

@ -0,0 +1,20 @@
import { get } from '@ember/object';
export default function(filterable) {
return filterable(function(item, { s = '' }) {
const sLower = s.toLowerCase();
return (
get(item, 'Name')
.toLowerCase()
.indexOf(sLower) !== -1 ||
get(item, 'Description')
.toLowerCase()
.indexOf(sLower) !== -1 ||
(get(item, 'ACLs.PolicyDefaults') || []).some(function(item) {
return item.Name.toLowerCase().indexOf(sLower) !== -1;
}) ||
(get(item, 'ACLs.RoleDefaults') || []).some(function(item) {
return item.Name.toLowerCase().indexOf(sLower) !== -1;
})
);
});
}

View File

@ -4,10 +4,15 @@ import { set } from '@ember/object';
import {
HEADERS_SYMBOL as HTTP_HEADERS_SYMBOL,
HEADERS_INDEX as HTTP_HEADERS_INDEX,
HEADERS_DATACENTER as HTTP_HEADERS_DATACENTER,
HEADERS_NAMESPACE as HTTP_HEADERS_NAMESPACE,
} from 'consul-ui/utils/http/consul';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { NSPACE_KEY } from 'consul-ui/models/nspace';
import createFingerprinter from 'consul-ui/utils/create-fingerprinter';
const DEFAULT_NSPACE = 'default';
const map = function(obj, cb) {
if (!Array.isArray(obj)) {
return [obj].map(cb)[0];
@ -15,26 +20,40 @@ const map = function(obj, cb) {
return obj.map(cb);
};
const attachHeaders = function(headers, body) {
const attachHeaders = function(headers, body, query = {}) {
// lowercase everything incase we get browser inconsistencies
const lower = {};
Object.keys(headers).forEach(function(key) {
lower[key.toLowerCase()] = headers[key];
});
// Add a 'pretend' Datacenter/Nspace header, they are not headers
// the come from the request but we add them here so we can use them later
// for store reconciliation
if (typeof query.dc !== 'undefined') {
lower[HTTP_HEADERS_DATACENTER.toLowerCase()] = query.dc;
}
lower[HTTP_HEADERS_NAMESPACE.toLowerCase()] =
typeof query.ns !== 'undefined' ? query.ns : DEFAULT_NSPACE;
//
body[HTTP_HEADERS_SYMBOL] = lower;
return body;
};
export default Serializer.extend({
fingerprint: createFingerprinter(DATACENTER_KEY),
attachHeaders: attachHeaders,
fingerprint: createFingerprinter(DATACENTER_KEY, NSPACE_KEY, DEFAULT_NSPACE),
respondForQuery: function(respond, query) {
return respond((headers, body) =>
attachHeaders(headers, map(body, this.fingerprint(this.primaryKey, this.slugKey, query.dc)))
attachHeaders(
headers,
map(body, this.fingerprint(this.primaryKey, this.slugKey, query.dc)),
query
)
);
},
respondForQueryRecord: function(respond, query) {
return respond((headers, body) =>
attachHeaders(headers, this.fingerprint(this.primaryKey, this.slugKey, query.dc)(body))
attachHeaders(headers, this.fingerprint(this.primaryKey, this.slugKey, query.dc)(body), query)
);
},
respondForCreateRecord: function(respond, serialized, data) {
@ -54,6 +73,10 @@ export default Serializer.extend({
const primaryKey = this.primaryKey;
return respond((headers, body) => {
// If updates are true use the info we already have
// TODO: We may aswell avoid re-fingerprinting here if we are just
// going to reuse data then its already fingerprinted and as the response
// is true we don't have anything changed so the old fingerprint stays the same
// as long as nothing in the fingerprint has been edited (the namespace?)
if (body === true) {
body = data;
}
@ -116,6 +139,8 @@ export default Serializer.extend({
normalizeMeta: function(store, primaryModelClass, headers, payload, id, requestType) {
const meta = {
cursor: headers[HTTP_HEADERS_INDEX],
dc: headers[HTTP_HEADERS_DATACENTER.toLowerCase()],
nspace: headers[HTTP_HEADERS_NAMESPACE.toLowerCase()],
};
if (requestType === 'query') {
meta.date = this.timestamp();

View File

@ -1,8 +1,7 @@
import Serializer from './application';
import { PRIMARY_KEY, SLUG_KEY, ATTRS } from 'consul-ui/models/intention';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/intention';
export default Serializer.extend({
primaryKey: PRIMARY_KEY,
slugKey: SLUG_KEY,
attrs: ATTRS,
});

View File

@ -2,6 +2,8 @@ import Serializer from './application';
import { inject as service } from '@ember/service';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/kv';
import { NSPACE_KEY } from 'consul-ui/models/nspace';
import { NSPACE_QUERY_PARAM as API_NSPACE_KEY } from 'consul-ui/adapters/application';
import removeNull from 'consul-ui/utils/remove-null';
export default Serializer.extend({
@ -25,6 +27,7 @@ export default Serializer.extend({
body.map(item => {
return {
[this.slugKey]: item,
[NSPACE_KEY]: query[API_NSPACE_KEY],
};
})
);

View File

@ -0,0 +1,63 @@
import Serializer from './application';
import { get } from '@ember/object';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/nspace';
export default Serializer.extend({
primaryKey: PRIMARY_KEY,
slugKey: SLUG_KEY,
respondForQuery: function(respond, serialized, data) {
return respond((headers, body) => {
return this.attachHeaders(
headers,
body.map(function(item) {
if (get(item, 'ACLs.PolicyDefaults')) {
item.ACLs.PolicyDefaults = item.ACLs.PolicyDefaults.map(function(item) {
item.template = '';
return item;
});
}
// Both of these might come though unset so we make sure
// we at least have an empty array here so we can add
// children to them if we need to whilst saving nspaces
['PolicyDefaults', 'RoleDefaults'].forEach(function(prop) {
if (typeof item.ACLs === 'undefined') {
item.ACLs = [];
}
if (typeof item.ACLs[prop] === 'undefined') {
item.ACLs[prop] = [];
}
});
return item;
})
);
});
},
respondForQueryRecord: function(respond, serialized, data) {
// We don't attachHeaders here yet, mainly because we don't use
// blocking queries on form views yet, and by the time we do
// Serializers should have been refactored to not use attachHeaders
return respond((headers, body) => {
return body;
});
},
respondForCreateRecord: function(respond, serialized, data) {
return respond((headers, body) => {
// The data properties sent to be saved in the backend
// or the same ones that we receive back if its successfull
// therefore we can just ignore the result and avoid ember-data
// syncing problems
return {};
});
},
respondForUpdateRecord: function(respond, serialized, data) {
return respond((headers, body) => {
return body;
});
},
respondForDeleteRecord: function(respond, serialized, data) {
return respond((headers, body) => {
// Deletes only need the primaryKey/uid returning
return body;
});
},
});

View File

@ -1,5 +1,6 @@
import Serializer from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/service';
import { get } from '@ember/object';
export default Serializer.extend({
primaryKey: PRIMARY_KEY,
@ -8,7 +9,14 @@ export default Serializer.extend({
// Name is added here from the query, which is used to make the uid
// Datacenter gets added in the ApplicationSerializer
return this._super(
cb => respond((headers, body) => cb(headers, { Name: query.id, Nodes: body })),
cb =>
respond((headers, body) => {
return cb(headers, {
Name: query.id,
Namespace: get(body, 'firstObject.Service.Namespace'),
Nodes: body,
});
}),
query
);
},

View File

@ -92,14 +92,26 @@ export default Service.extend({
return prev;
}, -1);
if (doubleBreak !== -1) {
body = values.splice(doubleBreak).reduce(function(prev, item) {
if (typeof item !== 'string') {
return {
...prev,
...item,
};
} else {
return item;
// This merges request bodies together, so you can specify multiple bodies
// in the request and it will merge them together.
// Turns out we never actually do this, so it might be worth removing as it complicates
// matters slightly as we assumed post bodies would be an object.
// This actually works as it just uses the value of the first object, if its an array
// it concats
body = values.splice(doubleBreak).reduce(function(prev, item, i) {
switch (true) {
case Array.isArray(item):
if (i === 0) {
prev = [];
}
return prev.concat(item);
case typeof item !== 'string':
return {
...prev,
...item,
};
default:
return item;
}
}, body);
}

View File

@ -1,5 +1,6 @@
import Service from '@ember/service';
import { getOwner } from '@ember/application';
import { guidFor } from '@ember/object/internals';
// selecting
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
@ -8,6 +9,7 @@ import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
// see if its possible to standardize
import sibling from 'consul-ui/utils/dom/sibling';
import closest from 'consul-ui/utils/dom/closest';
import isOutside from 'consul-ui/utils/dom/is-outside';
import getComponentFactory from 'consul-ui/utils/dom/get-component-factory';
// events
@ -33,10 +35,14 @@ export default Service.extend({
viewport: function() {
return this.win;
},
guid: function(el) {
return guidFor(el);
},
// TODO: should this be here? Needs a better name at least
clickFirstAnchor: clickFirstAnchor,
closest: closest,
sibling: sibling,
isOutside: isOutside,
normalizeEvent: normalizeEvent,
listeners: createListeners,
root: function() {

View File

@ -13,18 +13,20 @@ export default Service.extend({
},
//
store: service('store'),
findAllByDatacenter: function(dc, configuration = {}) {
findAllByDatacenter: function(dc, nspace, configuration = {}) {
const query = {
dc: dc,
ns: nspace,
};
if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor;
}
return this.store.query(this.getModelName(), query);
},
findBySlug: function(slug, dc, configuration = {}) {
findBySlug: function(slug, dc, nspace, configuration = {}) {
const query = {
dc: dc,
ns: nspace,
id: slug,
};
if (typeof configuration.cursor !== 'undefined') {
@ -44,6 +46,9 @@ export default Service.extend({
if (typeof obj.destroyRecord === 'undefined') {
item = obj.get('data');
}
// TODO: Change this to use vanilla JS
// I think this was originally looking for a plain object
// as opposed to an ember one
if (typeOf(item) === 'object') {
item = this.store.peekRecord(this.getModelName(), item[this.getPrimaryKey()]);
}
@ -52,6 +57,7 @@ export default Service.extend({
});
},
invalidate: function() {
// TODO: This should probably return a Promise
this.store.unloadAll(this.getModelName());
},
});

View File

@ -10,8 +10,8 @@ export default RepositoryService.extend({
getModelName: function() {
return modelName;
},
findAllByNode: function(node, dc, configuration) {
return this.findAllByDatacenter(dc, configuration).then(function(coordinates) {
findAllByNode: function(node, dc, nspace, configuration) {
return this.findAllByDatacenter(dc, nspace, configuration).then(function(coordinates) {
let results = {};
if (get(coordinates, 'length') > 1) {
results = tomography(node, coordinates.map(item => get(item, 'data')));

View File

@ -2,6 +2,7 @@ import RepositoryService from 'consul-ui/services/repository';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
import Error from '@ember/error';
import { Promise } from 'rsvp';
const modelName = 'dc';
export default RepositoryService.extend({
@ -11,6 +12,7 @@ export default RepositoryService.extend({
},
findAll: function() {
return this.store.findAll(this.getModelName()).then(function(items) {
// TODO: Move to view/template
return items.sortBy('Name');
});
},

View File

@ -1,7 +1,7 @@
import RepositoryService from 'consul-ui/services/repository';
import { Promise } from 'rsvp';
import isFolder from 'consul-ui/utils/isFolder';
import { get, set } from '@ember/object';
import { get } from '@ember/object';
import { PRIMARY_KEY } from 'consul-ui/models/kv';
const modelName = 'kv';
@ -13,20 +13,26 @@ export default RepositoryService.extend({
return PRIMARY_KEY;
},
// this one gives you the full object so key,values and meta
findBySlug: function(key, dc, configuration = {}) {
findBySlug: function(key, dc, nspace, configuration = {}) {
if (isFolder(key)) {
const id = JSON.stringify([dc, key]);
// TODO: This very much shouldn't be here,
// needs to eventually use ember-datas generateId thing
// in the meantime at least our fingerprinter
const id = JSON.stringify([nspace, dc, key]);
let item = this.store.peekRecord(this.getModelName(), id);
if (!item) {
item = this.create();
set(item, 'Key', key);
set(item, 'Datacenter', dc);
item = this.create({
Key: key,
Datacenter: dc,
Namespace: nspace,
});
}
return Promise.resolve(item);
}
const query = {
id: key,
dc: dc,
ns: nspace,
};
if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor;
@ -35,13 +41,14 @@ export default RepositoryService.extend({
},
// this one only gives you keys
// https://www.consul.io/api/kv.html
findAllBySlug: function(key, dc, configuration = {}) {
findAllBySlug: function(key, dc, nspace, configuration = {}) {
if (key === '/') {
key = '';
}
const query = {
id: key,
dc: dc,
ns: nspace,
separator: '/',
};
if (typeof configuration.cursor !== 'undefined') {
@ -55,7 +62,13 @@ export default RepositoryService.extend({
});
})
.catch(e => {
if (e.errors && e.errors[0] && e.errors[0].status == '404') {
// TODO: Double check this was loose on purpose, its probably as we were unsure of
// type of ember-data error.Status at first, we could probably change this
// to `===` now
if (get(e, 'errors.firstObject.status') == '404') {
// TODO: This very much shouldn't be here,
// needs to eventually use ember-datas generateId thing
// in the meantime at least our fingerprinter
const id = JSON.stringify([dc, key]);
const record = this.store.peekRecord(this.getModelName(), id);
if (record) {

View File

@ -0,0 +1,15 @@
import RepositoryService from 'consul-ui/services/repository';
const modelName = 'nspace';
export default RepositoryService.extend({
getModelName: function() {
return modelName;
},
findAll: function(configuration = {}) {
const query = {};
if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor;
}
return this.store.query(this.getModelName(), query);
},
});

View File

@ -0,0 +1,21 @@
import RepositoryService from 'consul-ui/services/repository';
import { Promise } from 'rsvp';
const modelName = 'nspace';
const DEFAULT_NSPACE = 'default';
export default RepositoryService.extend({
getModelName: function() {
return modelName;
},
findAll: function(configuration = {}) {
return Promise.resolve([]);
},
getActive: function() {
return {
Name: DEFAULT_NSPACE,
};
},
authorize: function(dc, nspace) {
return Promise.resolve([]);
},
});

View File

@ -0,0 +1,73 @@
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
import { config } from 'consul-ui/env';
import RepositoryService from 'consul-ui/services/repository';
const modelName = 'nspace';
export default RepositoryService.extend({
router: service('router'),
settings: service('settings'),
getModelName: function() {
return modelName;
},
findAll: function(configuration = {}) {
const query = {};
if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor;
}
return this.store.query(this.getModelName(), query);
},
authorize: function(dc, nspace) {
if (!config('CONSUL_ACLS_ENABLED')) {
return Promise.resolve([
{
Resource: 'operator',
Access: 'write',
Allow: true,
},
]);
}
return this.store.authorize(this.getModelName(), { dc: dc, ns: nspace }).catch(function(e) {
return [];
});
},
getActive: function() {
let routeParams = {};
// this is only populated before the model hook as fired,
// it is then deleted after the model hook has finished
const infos = get(this, 'router._router.currentState.router.activeTransition.routeInfos');
if (typeof infos !== 'undefined') {
infos.forEach(function(item) {
Object.keys(item.params).forEach(function(prop) {
routeParams[prop] = item.params[prop];
});
});
} else {
// this is only populated after the model hook has finished
//
const current = get(this, 'router.currentRoute');
if (current) {
const nspacedRoute = current.find(function(item, i, arr) {
return item.paramNames.includes('nspace');
});
if (typeof nspacedRoute !== 'undefined') {
routeParams.nspace = nspacedRoute.params.nspace;
}
}
}
return this.settings
.findBySlug('nspace')
.then(function(nspace) {
// If we can't figure out the nspace from the URL use
// the previously saved nspace and if thats not there
// then just use default
return routeParams.nspace || nspace || '~default';
})
.then(nspace => this.settings.persist({ nspace: nspace }))
.then(function(item) {
return {
Name: item.nspace.substr(1),
};
});
},
});

View File

@ -9,9 +9,10 @@ export default RepositoryService.extend({
getPrimaryKey: function() {
return PRIMARY_KEY;
},
findAllBySlug: function(slug, dc, configuration = {}) {
findAllBySlug: function(slug, dc, nspace, configuration = {}) {
const query = {
id: slug,
ns: nspace,
dc: dc,
};
if (typeof configuration.cursor !== 'undefined') {
@ -19,8 +20,8 @@ export default RepositoryService.extend({
}
return this.store.query(this.getModelName(), query);
},
findInstanceBySlug: function(id, node, slug, dc, configuration) {
return this.findAllBySlug(slug, dc, configuration).then(function(items) {
findInstanceBySlug: function(id, node, slug, dc, nspace, configuration) {
return this.findAllBySlug(slug, dc, nspace, configuration).then(function(items) {
let res = {};
if (get(items, 'length') > 0) {
let instance = items.filterBy('ServiceProxy.DestinationServiceID', id).findBy('Node', node);

View File

@ -7,6 +7,7 @@ export default RepositoryService.extend({
},
findBySlug: function(slug, dc) {
return this._super(...arguments).then(function(item) {
// TODO: Move this to the Serializer
const nodes = get(item, 'Nodes');
if (nodes.length === 0) {
// TODO: Add an store.error("404", "message") or similar
@ -21,6 +22,7 @@ export default RepositoryService.extend({
throw e;
}
const service = get(nodes, 'firstObject');
// TODO: Use [...new Set()] instead of uniq
const tags = nodes
.reduce(function(prev, item) {
return prev.concat(get(item, 'Service.Tags') || []);
@ -29,11 +31,13 @@ export default RepositoryService.extend({
set(service, 'Tags', tags);
set(service, 'Nodes', nodes);
set(service, 'meta', get(item, 'meta'));
set(service, 'Namespace', get(item, 'Namespace'));
return service;
});
},
findInstanceBySlug: function(id, node, slug, dc, configuration) {
return this.findBySlug(slug, dc, configuration).then(function(item) {
// TODO: Move this to the Serializer
// Loop through all the service instances and pick out the one
// that has the same service id AND node name
// node names are unique per datacenter
@ -50,6 +54,7 @@ export default RepositoryService.extend({
return item.ServiceID == '';
});
set(service, 'meta', get(item, 'meta'));
set(service, 'Namespace', get(item, 'Namespace'));
return service;
}
// TODO: Add an store.error("404", "message") or similar

View File

@ -7,10 +7,11 @@ export default RepositoryService.extend({
getModelName: function() {
return modelName;
},
findByNode: function(node, dc, configuration = {}) {
findByNode: function(node, dc, nspace, configuration = {}) {
const query = {
id: node,
dc: dc,
ns: nspace,
};
if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor;
@ -18,7 +19,7 @@ export default RepositoryService.extend({
return this.store.query(this.getModelName(), query);
},
// TODO: Why Key? Probably should be findBySlug like the others
findByKey: function(slug, dc) {
return this.findBySlug(slug, dc);
findByKey: function(slug, dc, nspace) {
return this.findBySlug(...arguments);
},
});

View File

@ -43,16 +43,18 @@ export default RepositoryService.extend({
clone: function(item) {
return this.store.clone(this.getModelName(), get(item, PRIMARY_KEY));
},
findByPolicy: function(id, dc) {
findByPolicy: function(id, dc, nspace) {
return this.store.query(this.getModelName(), {
policy: id,
dc: dc,
ns: nspace,
});
},
findByRole: function(id, dc) {
findByRole: function(id, dc, nspace) {
return this.store.query(this.getModelName(), {
role: id,
dc: dc,
ns: nspace,
});
},
});

View File

@ -17,10 +17,20 @@ const createProxy = function(repo, find, settings, cache, serialize = JSON.strin
const meta = get(event.data || {}, 'meta') || {};
if (typeof meta.date !== 'undefined') {
// unload anything older than our current sync date/time
const checkNspace = meta.nspace !== '';
store.peekAll(repo.getModelName()).forEach(function(item) {
const date = get(item, 'SyncTime');
if (typeof date !== 'undefined' && date != meta.date) {
store.unloadRecord(item);
const dc = get(item, 'Datacenter');
if (dc === meta.dc) {
if (checkNspace) {
const nspace = get(item, 'Namespace');
if (nspace !== meta.namespace) {
return;
}
}
const date = get(item, 'SyncTime');
if (typeof date !== 'undefined' && date != meta.date) {
store.unloadRecord(item);
}
}
});
}

View File

@ -31,4 +31,10 @@ export default Store.extend({
const adapter = this.adapterFor(modelName);
return adapter.queryLeader(this, { modelName: modelName }, null, query);
},
// TODO: This one is only for ACL, should fail nicely if you call it
// for anything other than ACLs for good DX
authorize: function(modelName, query = {}) {
// TODO: no normalization, type it properly for the moment
return this.adapterFor(modelName).authorize(this, { modelName: modelName }, null, query);
},
});

View File

@ -35,7 +35,7 @@
padding-bottom: calc(0.4em - 1px) !important;
}
%internal-button {
padding: 0.75rem 1rem;
padding: 0.9em 1em;
text-align: center;
display: inline-block;
box-sizing: border-box;

View File

@ -95,6 +95,9 @@
@extend %frame-red-900;
}
%internal-button {
color: $gray-900;
}
%internal-button-dangerous {
@extend %frame-red-300;
}
@ -102,7 +105,7 @@
@extend %frame-red-700;
}
%internal-button-intent {
background-color: $gray-100;
background-color: $gray-050;
}
%internal-button:focus,
%internal-button:hover {

View File

@ -9,6 +9,9 @@
%stats-card li {
border-color: $gray-200;
}
%stats-card a {
color: $gray-900;
}
%stats-card,
%stats-card header::before {
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.05);

View File

@ -26,6 +26,9 @@
%table td a {
display: block;
}
%table td.no-actions ~ .actions {
display: none;
}
%table td:not(.actions),
%table td:not(.actions) > *:only-child {
overflow-x: hidden;
@ -50,3 +53,11 @@
%table td a {
padding-right: 0.9em;
}
%table tbody td em {
display: block;
font-style: normal;
font-weight: normal;
}
%table tbody td em {
color: $gray-500;
}

View File

@ -15,7 +15,9 @@
}
/* TODO: Add to native selector `tbody th` - will involve moving all
* current th's to `thead th` and changing the templates
* at whichpoint we can probably remove the %table a selector from here
*/
%table a,
%tbody-th {
color: $gray-900;
}

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