Merge pull request #6966 from hashicorp/ui-staging

ui: UI Release Merge (ui-staging merge)
This commit is contained in:
John Cowen 2019-12-20 15:30:43 +00:00 committed by GitHub
commit 95a71bba04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
388 changed files with 8175 additions and 2379 deletions

View File

@ -1,9 +1,9 @@
ARG ALPINE_VERSION=3.8
ARG ALPINE_VERSION=3.9
FROM alpine:${ALPINE_VERSION}
ARG NODEJS_VERSION=8.14.0-r0
ARG NODEJS_VERSION=10.14.2-r0
ARG MAKE_VERSION=4.2.1-r2
ARG YARN_VERSION=1.13
ARG YARN_VERSION=1.19.1
RUN apk update && \
apk add nodejs=${NODEJS_VERSION} nodejs-npm=${NODEJS_VERSION} make=${MAKE_VERSION} && \

View File

@ -5,6 +5,5 @@
Setting `disableAnalytics` to true will prevent any data from being sent.
*/
"disableAnalytics": false,
"proxy": "http://localhost:3000"
"disableAnalytics": false
}

View File

@ -1 +1 @@
8
10

View File

@ -29,6 +29,9 @@ build: deps
start: deps
yarn run start
start-consul: deps
yarn run start:consul
start-api: deps
yarn run start:api

View File

@ -21,14 +21,11 @@ All tooling scripts below primarily use `make` which in turn call node package s
## Running / Development
The source code comes with a small server that runs enough of the consul API
The source code comes with a small development mode 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
* `make start` or `yarn start` to start the ember app
* Visit your app at [http://localhost:4200](http://localhost:4200).
To enable ACLs using the mock API, use Web Inspector to set a cookie as follows:
@ -51,6 +48,20 @@ CONSUL_NODE_CODE=1000
See `./node_modules/@hashicorp/consul-api-double` for more details.
If you wish to run the UI code against a running consul instance, uncomment the `proxy`
line in `.ember-cli` to point ember-cli to your consul instance.
You can also run the UI against a normal Consul installation.
`make start-consul` or `yarn run start:consul` will use the `CONSUL_HTTP_ADDR`
environment variable to locate the Consul installation. If that it not set
`start-consul` will use `http://localhost:8500`.
Example usage:
```
CONSUL_HTTP_ADDR=http://10.0.0.1:8500 make start-consul
```
### Code Generators

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

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

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,43 +1,73 @@
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}
${serialized}
${{
Name: serialized.Name,
Description: serialized.Description,
Rules: serialized.Rules,
Datacenters: serialized.Datacenters,
}}
`;
},
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}
${serialized}
${{
Name: serialized.Name,
Description: serialized.Description,
Rules: serialized.Rules,
Datacenters: serialized.Datacenters,
}}
`;
},
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,135 @@
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-';
// ^menuitem supports menuitemradio and menuitemcheckbox
const MENU_ITEMS = '[role^="menuitem"]';
export default Component.extend({
tagName: '',
dom: service('dom'),
guid: '',
expanded: false,
orientation: '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
// TODO: We need to use > somehow here so we don't select submenus
const $items = [...this.dom.elements(MENU_ITEMS, 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.orientation][e.keyCode] === 'undefined') {
return;
}
const $focused = this.dom.element(`${MENU_ITEMS}:focus`, this.$menu);
let i;
if ($focused) {
i = $items.findIndex(function($item) {
return $item === $focused;
});
}
const $next = $items[keys[this.orientation][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

@ -3,7 +3,6 @@ import Component from '@ember/component';
import SlotsMixin from 'block-slots';
import { set } from '@ember/object';
import { inject as service } from '@ember/service';
const cancel = function() {
set(this, 'confirming', false);
@ -15,25 +14,10 @@ const confirm = function() {
const [action, ...args] = arguments;
set(this, 'actionName', action);
set(this, 'arguments', args);
if (this._isRegistered('dialog')) {
set(this, 'confirming', true);
} else {
this._confirm
.execute(this.message)
.then(confirmed => {
if (confirmed) {
this.execute();
}
})
.catch(function() {
return this.error.execute(...arguments);
});
}
set(this, 'confirming', true);
};
export default Component.extend(SlotsMixin, {
classNameBindings: ['confirming'],
_confirm: service('confirm'),
error: service('error'),
classNames: ['with-confirmation'],
message: 'Are you sure?',
confirming: false,

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

@ -0,0 +1,288 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { set, get, computed } from '@ember/object';
import { next } from '@ember/runloop';
const getNodesByType = function(nodes = {}, type) {
return Object.values(nodes).filter(item => item.Type === type);
};
const targetsToFailover = function(targets, a) {
let type;
const Targets = targets.map(function(b) {
// TODO: this isn't going to work past namespace for services
// with dots in the name
const [aRev, bRev] = [a, b].map(item => item.split('.').reverse());
const types = ['Datacenter', 'Namespace', 'Service', 'Subset'];
return bRev.find(function(item, i) {
const res = item !== aRev[i];
if (res) {
type = types[i];
}
return res;
});
});
return {
Type: type,
Targets: Targets,
};
};
const getNodeResolvers = function(nodes = {}) {
const failovers = getFailovers(nodes);
const resolvers = {};
Object.keys(nodes).forEach(function(key) {
const node = nodes[key];
if (node.Type === 'resolver' && !failovers.includes(key.split(':').pop())) {
resolvers[node.Name] = node;
}
});
return resolvers;
};
const getTargetResolvers = function(dc, nspace = 'default', targets = [], nodes = {}) {
const resolvers = {};
Object.values(targets).forEach(item => {
let node = nodes[item.ID];
if (node) {
if (typeof resolvers[item.Service] === 'undefined') {
resolvers[item.Service] = {
ID: item.ID,
Name: item.Service,
Children: [],
Failover: null,
Redirect: null,
};
}
const resolver = resolvers[item.Service];
let failoverable = resolver;
if (item.ServiceSubset) {
failoverable = item;
// TODO: Sometimes we have set the resolvers ID to the ID of the
// subset this just shifts the subset of the front of the URL for the moment
const temp = item.ID.split('.');
temp.shift();
resolver.ID = temp.join('.');
resolver.Children.push(item);
}
if (typeof node.Resolver.Failover !== 'undefined') {
// TODO: Figure out if we can get rid of this
/* eslint ember/no-side-effects: "warn" */
set(failoverable, 'Failover', targetsToFailover(node.Resolver.Failover.Targets, item.ID));
} else {
const res = targetsToFailover([node.Resolver.Target], `service.${nspace}.${dc}`);
if (res.Type === 'Datacenter' || res.Type === 'Namespace') {
resolver.Children.push(item);
set(failoverable, 'Redirect', true);
}
}
}
});
return Object.values(resolvers);
};
const getFailovers = function(nodes = {}) {
const failovers = [];
Object.values(nodes)
.filter(item => item.Type === 'resolver')
.forEach(function(item) {
(get(item, 'Resolver.Failover.Targets') || []).forEach(failover => {
failovers.push(failover);
});
});
return failovers;
};
export default Component.extend({
dom: service('dom'),
ticker: service('ticker'),
dataStructs: service('data-structs'),
classNames: ['discovery-chain'],
classNameBindings: ['active'],
isDisplayed: false,
selectedId: '',
x: 0,
y: 0,
tooltip: '',
activeTooltip: false,
init: function() {
this._super(...arguments);
this._listeners = this.dom.listeners();
this._viewportlistener = this.dom.listeners();
},
didInsertElement: function() {
this._super(...arguments);
this._viewportlistener.add(
this.dom.isInViewport(this.element, bool => {
set(this, 'isDisplayed', bool);
if (this.isDisplayed) {
this.addPathListeners();
} else {
this.ticker.destroy(this);
}
})
);
},
didReceiveAttrs: function() {
this._super(...arguments);
if (this.element) {
this.addPathListeners();
}
},
willDestroyElement: function() {
this._super(...arguments);
this._listeners.remove();
this._viewportlistener.remove();
this.ticker.destroy(this);
},
splitters: computed('chain.Nodes', function() {
return getNodesByType(get(this, 'chain.Nodes'), 'splitter').map(function(item) {
set(item, 'ID', `splitter:${item.Name}`);
return item;
});
}),
routers: computed('chain.Nodes', function() {
// Right now there should only ever be one 'Router'.
return getNodesByType(get(this, 'chain.Nodes'), 'router');
}),
routes: computed('chain', 'routers', function() {
const routes = get(this, 'routers').reduce(function(prev, item) {
return prev.concat(
item.Routes.map(function(route, i) {
return {
...route,
ID: `route:${item.Name}-${JSON.stringify(route.Definition.Match.HTTP)}`,
};
})
);
}, []);
if (routes.length === 0) {
let nextNode = `resolver:${this.chain.ServiceName}.${this.chain.Namespace}.${this.chain.Datacenter}`;
const splitterID = `splitter:${this.chain.ServiceName}`;
if (typeof this.chain.Nodes[splitterID] !== 'undefined') {
nextNode = splitterID;
}
routes.push({
Default: true,
ID: `route:${this.chain.ServiceName}`,
Name: this.chain.ServiceName,
Definition: {
Match: {
HTTP: {
PathPrefix: '/',
},
},
},
NextNode: nextNode,
});
}
return routes;
}),
nodeResolvers: computed('chain.Nodes', function() {
return getNodeResolvers(get(this, 'chain.Nodes'));
}),
resolvers: computed('nodeResolvers.[]', 'chain.Targets', function() {
return getTargetResolvers(
this.chain.Datacenter,
this.chain.Namespace,
get(this, 'chain.Targets'),
this.nodeResolvers
);
}),
graph: computed('chain.Nodes', function() {
const graph = this.dataStructs.graph();
Object.entries(get(this, 'chain.Nodes')).forEach(function([key, item]) {
switch (item.Type) {
case 'splitter':
item.Splits.forEach(function(splitter) {
graph.addLink(`splitter:${item.Name}`, splitter.NextNode);
});
break;
case 'router':
item.Routes.forEach(function(route, i) {
graph.addLink(
`route:${item.Name}-${JSON.stringify(route.Definition.Match.HTTP)}`,
route.NextNode
);
});
break;
}
});
return graph;
}),
selected: computed('selectedId', 'graph', function() {
if (this.selectedId === '' || !this.dom.element(`#${this.selectedId}`)) {
return {};
}
const getTypeFromId = function(id) {
return id.split(':').shift();
};
const id = this.selectedId;
const type = getTypeFromId(id);
const nodes = [id];
const edges = [];
this.graph.forEachLinkedNode(id, (linkedNode, link) => {
nodes.push(linkedNode.id);
edges.push(`${link.fromId}>${link.toId}`);
this.graph.forEachLinkedNode(linkedNode.id, (linkedNode, link) => {
const nodeType = getTypeFromId(linkedNode.id);
if (type !== nodeType && type !== 'splitter' && nodeType !== 'splitter') {
nodes.push(linkedNode.id);
edges.push(`${link.fromId}>${link.toId}`);
}
});
});
return {
nodes: nodes.map(item => `#${CSS.escape(item)}`),
edges: edges.map(item => `#${CSS.escape(item)}`),
};
}),
width: computed('isDisplayed', 'chain.{Nodes,Targets}', function() {
return this.element.offsetWidth;
}),
height: computed('isDisplayed', 'chain.{Nodes,Targets}', function() {
return this.element.offsetHeight;
}),
// TODO(octane): ember has trouble adding mouse events to svg elements whilst giving
// the developer access to the mouse event therefore we just use JS to add our events
// revisit this post Octane
addPathListeners: function() {
// TODO: Figure out if we can remove this next
next(() => {
this._listeners.remove();
[...this.dom.elements('path.split', this.element)].forEach(item => {
this._listeners.add(item, {
mouseover: e => this.actions.showSplit.apply(this, [e]),
mouseout: e => this.actions.hideSplit.apply(this, [e]),
});
});
});
// TODO: currently don't think there is a way to listen
// for an element being removed inside a component, possibly
// using IntersectionObserver. It's a tiny detail, but we just always
// remove the tooltip on component update as its so tiny, ideal
// the tooltip would stay if there was no change to the <path>
// set(this, 'activeTooltip', false);
},
actions: {
showSplit: function(e) {
this.setProperties({
x: e.offsetX,
y: e.offsetY - 5,
tooltip: e.target.dataset.percentage,
activeTooltip: true,
});
},
hideSplit: function(e = null) {
set(this, 'activeTooltip', false);
},
click: function(e) {
const id = e.currentTarget.getAttribute('id');
if (id === this.selectedId) {
set(this, 'active', false);
set(this, 'selectedId', '');
} else {
set(this, 'active', true);
set(this, 'selectedId', id);
}
},
},
});

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,5 @@
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});

View File

@ -0,0 +1,29 @@
import Component from '@ember/component';
import { get, computed } from '@ember/object';
export default Component.extend({
tagName: '',
path: computed('item', function() {
if (get(this, 'item.Default')) {
return {
type: 'Default',
value: '/',
};
}
return Object.entries(get(this, 'item.Definition.Match.HTTP') || {}).reduce(
function(prev, [key, value]) {
if (key.toLowerCase().startsWith('path')) {
return {
type: key.replace('Path', ''),
value: value,
};
}
return prev;
},
{
type: 'Prefix',
value: '/',
}
);
}),
});

View File

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

View File

@ -82,7 +82,7 @@ const change = function(e) {
const dom = get(this, 'dom');
const $tr = dom.closest('tr', e.currentTarget);
const $group = dom.sibling(e.currentTarget, 'ul');
const $group = dom.sibling(e.currentTarget, 'div');
const groupRect = $group.getBoundingClientRect();
const groupBottom = groupRect.top + $group.clientHeight;

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

@ -19,6 +19,7 @@ export default Controller.extend(WithEventSource, WithSearching, {
// This method is called immediately after `Route::setupController`, and done here rather than there
// as this is a variable used purely for view level things, if the view was different we might not
// need this variable
set(this, 'selectedTab', 'instances');
},
item: listen('item').catch(function(e) {

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,6 +1,11 @@
import { helper } from '@ember/component/helper';
// TODO: Look at ember-inline-svg
const cssVars = {
'--decor-border-100': '1px solid',
'--decor-border-200': '2px solid',
'--decor-radius-300': '3px',
'--white': '#FFF',
'--gray-500': '#6f7682',
'--kubernetes-color-svg': `url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 21 20" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" stroke="%23FFF" fill="none"><path d="M10.21 1.002a1.241 1.241 0 0 0-.472.12L3.29 4.201a1.225 1.225 0 0 0-.667.83l-1.591 6.922a1.215 1.215 0 0 0 .238 1.035l4.463 5.55c.234.29.59.46.964.46l7.159-.002c.375 0 .73-.168.964-.459l4.462-5.55c.234-.292.322-.673.238-1.036l-1.593-6.921a1.225 1.225 0 0 0-.667-.83l-6.45-3.08a1.242 1.242 0 0 0-.598-.12z" fill="%23326CE5"/><path d="M10.275 3.357c-.213 0-.386.192-.386.429v.11c.005.136.035.24.052.367.033.27.06.492.043.7a.421.421 0 0 1-.125.2l-.01.163a4.965 4.965 0 0 0-3.22 1.548 6.47 6.47 0 0 1-.138-.099c-.07.01-.139.03-.23-.022-.172-.117-.33-.277-.52-.47-.087-.093-.15-.181-.254-.27L5.4 5.944a.46.46 0 0 0-.269-.101.372.372 0 0 0-.307.136c-.133.167-.09.422.094.57l.006.003.08.065c.11.08.21.122.32.187.231.142.422.26.574.403.06.063.07.175.078.223l.123.11a4.995 4.995 0 0 0-.787 3.483l-.162.047c-.042.055-.103.141-.166.167-.198.063-.422.086-.692.114-.126.01-.236.004-.37.03-.03.005-.07.016-.103.023l-.003.001-.006.002c-.228.055-.374.264-.327.47.047.206.27.331.498.282h.006c.003-.001.005-.003.008-.003l.1-.022c.131-.036.227-.088.346-.133.255-.092.467-.168.673-.198.086-.007.177.053.222.078l.168-.029a5.023 5.023 0 0 0 2.226 2.78l-.07.168c.025.065.053.154.034.218-.075.195-.203.4-.35.628-.07.106-.142.188-.206.309l-.05.104c-.099.212-.026.456.165.548.191.092.43-.005.532-.218h.001v-.001c.015-.03.036-.07.048-.098.055-.126.073-.233.111-.354.102-.257.159-.526.3-.694.038-.046.1-.063.166-.08l.087-.159a4.987 4.987 0 0 0 3.562.01l.083.148c.066.021.138.032.197.12.105.179.177.391.265.648.038.121.057.229.112.354.012.029.033.069.048.099.102.213.341.311.533.219.19-.092.264-.337.164-.549l-.05-.104c-.064-.12-.136-.202-.207-.307-.146-.23-.267-.419-.342-.613-.032-.1.005-.163.03-.228-.015-.017-.047-.111-.065-.156a5.023 5.023 0 0 0 2.225-2.8l.165.03c.058-.039.112-.088.216-.08.206.03.418.106.673.198.12.045.215.098.347.133.028.008.068.015.1.022l.007.002.006.001c.229.05.45-.076.498-.282.047-.206-.1-.415-.327-.47l-.112-.027c-.134-.025-.243-.019-.37-.03-.27-.027-.494-.05-.692-.113-.081-.031-.139-.128-.167-.167l-.156-.046a4.997 4.997 0 0 0-.804-3.474l.137-.123c.006-.069.001-.142.073-.218.151-.143.343-.261.574-.404.11-.064.21-.106.32-.187.025-.018.06-.047.086-.068.185-.148.227-.403.094-.57-.133-.166-.39-.182-.575-.034-.027.02-.062.048-.086.068-.104.09-.168.178-.255.27-.19.194-.348.355-.52.471-.075.044-.185.029-.235.026l-.146.104A5.059 5.059 0 0 0 10.7 5.328a9.325 9.325 0 0 1-.009-.172c-.05-.048-.11-.09-.126-.193-.017-.208.011-.43.044-.7.018-.126.047-.23.053-.367l-.001-.11c0-.237-.173-.429-.386-.429zM9.79 6.351l-.114 2.025-.009.004a.34.34 0 0 1-.54.26l-.003.002-1.66-1.177A3.976 3.976 0 0 1 9.79 6.351zm.968 0a4.01 4.01 0 0 1 2.313 1.115l-1.65 1.17-.006-.003a.34.34 0 0 1-.54-.26h-.003L10.76 6.35zm-3.896 1.87l1.516 1.357-.002.008a.34.34 0 0 1-.134.585l-.001.006-1.944.561a3.975 3.975 0 0 1 .565-2.516zm6.813.001a4.025 4.025 0 0 1 .582 2.51l-1.954-.563-.001-.008a.34.34 0 0 1-.134-.585v-.004l1.507-1.35zm-3.712 1.46h.62l.387.483-.139.602-.557.268-.56-.269-.138-.602.387-.482zm1.99 1.652a.339.339 0 0 1 .08.005l.002-.004 2.01.34a3.98 3.98 0 0 1-1.609 2.022l-.78-1.885.002-.003a.34.34 0 0 1 .296-.475zm-3.375.008a.34.34 0 0 1 .308.474l.005.007-.772 1.866a3.997 3.997 0 0 1-1.604-2.007l1.993-.339.003.005a.345.345 0 0 1 .067-.006zm1.683.817a.338.338 0 0 1 .312.179h.008l.982 1.775a3.991 3.991 0 0 1-2.57-.002l.979-1.772h.001a.34.34 0 0 1 .288-.18z" stroke-width=".25" fill="%23FFF"/></g></svg>')`,
'--terraform-color-svg': `url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 16 18" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="%235C4EE5" d="M5.51 3.15l4.886 2.821v5.644L5.509 8.792z"/><path fill="%234040B2" d="M10.931 5.971v5.644l4.888-2.823V3.15z"/><path fill="%235C4EE5" d="M.086 0v5.642l4.887 2.823V2.82zM5.51 15.053l4.886 2.823v-5.644l-4.887-2.82z"/></g></svg>')`,
'--nomad-color-svg': `url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 16 18" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path fill="%231F9967" d="M11.569 6.871v2.965l-2.064 1.192-1.443-.894v7.74l.04.002 7.78-4.47V4.48h-.145z"/><path fill="%2325BA81" d="M7.997 0L.24 4.481l5.233 3.074 1.06-.645 2.57 1.435v-2.98l2.465-1.481v2.987l4.314-2.391v-.011z"/><path fill="%2325BA81" d="M7.02 9.54v2.976l-2.347 1.488V8.05l.89-.548L.287 4.48.24 4.48v8.926l7.821 4.467v-7.74z"/></g></svg>')`,

View File

@ -0,0 +1,28 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default Helper.extend({
dom: service('dom'),
compute: function([selector, id], hash) {
const $el = this.dom.element(selector);
const $refs = [$el.offsetParent, $el];
// TODO: helper probably needs to accept a `reference=` option
// with a selector to use as reference/root
if (selector.startsWith('#resolver:')) {
$refs.unshift($refs[0].offsetParent);
}
return $refs.reduce(
function(prev, item) {
prev.x += item.offsetLeft;
prev.y += item.offsetTop;
return prev;
},
{
x: 0,
y: 0,
height: $el.offsetHeight,
width: $el.offsetWidth,
}
);
},
});

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.currentRouteName;
// 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('dc.')) {
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

@ -0,0 +1,18 @@
import { helper } from '@ember/component/helper';
export default helper(function routeMatch([params] /*, hash*/) {
const keys = Object.keys(params);
switch (true) {
case keys.includes('Present'):
return `${params.Invert ? `NOT ` : ``}present`;
case keys.includes('Exact'):
return `${params.Invert ? `NOT ` : ``}exactly matching "${params.Exact}"`;
case keys.includes('Prefix'):
return `${params.Invert ? `NOT ` : ``}prefixed by "${params.Prefix}"`;
case keys.includes('Suffix'):
return `${params.Invert ? `NOT ` : ``}suffixed by "${params.Suffix}"`;
case keys.includes('Regex'):
return `${params.Invert ? `NOT ` : ``}matching the regex "${params.Regex}"`;
}
return '';
});

View File

@ -0,0 +1,41 @@
import { helper } from '@ember/component/helper';
// arguments should be a list of {x: numLike, y: numLike} points
// numLike meaning they should be numbers (or numberlike strings i.e. "1" vs 1)
const curve = function() {
const args = [...arguments];
// our arguments are destination first control points last
// SVGs are control points first destination last
// we 'shift,push' to turn that around and then map
// through the values to convert it to 'x y, x y' etc
// whether the curve is cubic-bezier (C) or quadratic-bezier (Q)
// then depends on the amount of control points
// `Q|C x y, x y, x y` etc
return `${arguments.length > 2 ? `C` : `Q`} ${args
.concat(args.shift())
.map(p => Object.values(p).join(' '))
.join(',')}`;
};
const move = function(d) {
return `
M ${d.x} ${d.y}
`;
};
export default helper(function([dest], hash) {
const src = hash.src || { x: 0, y: 0 };
const type = hash.type || 'cubic';
let args = [
dest,
{
x: (src.x + dest.x) / 2,
y: src.y,
},
];
if (type === 'cubic') {
args.push({
x: args[1].x,
y: dest.y,
});
}
return `${move(src)}${curve(...args)}`;
});

View File

@ -0,0 +1,9 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default Helper.extend({
ticker: service('ticker'),
compute: function([props, id], hash) {
return this.ticker.tweenTo(props, id);
},
});

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']
['node', 'coordinate', 'session', 'service', 'proxy', 'discovery-chain']
.concat(config('CONSUL_NSPACES_ENABLED') ? ['nspace/enabled'] : [])
.map(function(item) {
// create repositories that return a promise resolving to an EventSource
return {
@ -59,6 +60,7 @@ export function initialize(container) {
route: 'dc/services/show',
services: {
repo: 'repository/service/event-source',
chainRepo: 'repository/discovery-chain/event-source',
},
},
{
@ -76,6 +78,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 +104,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');
}
// TODO: 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

@ -10,9 +10,11 @@ import { get } from '@ember/object';
*/
export default Mixin.create({
beforeModel: function() {
this._super(...arguments);
this.repo.invalidate();
},
deactivate: function() {
this._super(...arguments);
// TODO: This is dependent on ember-changeset
// Change changeset to support ember-data props
const item = get(this.controller, 'item.data');

View File

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

View File

@ -9,13 +9,18 @@ export default Mixin.create(WithBlockingActions, {
use: function(item) {
return this.feedback.execute(() => {
return this.repo
.findBySlug(get(item, 'AccessorID'), this.modelFor('dc').dc.Name)
.findBySlug(
get(item, 'AccessorID'),
this.modelFor('dc').dc.Name,
this.modelFor('nspace').nspace.substr(1)
)
.then(item => {
return this.settings
.persist({
token: {
AccessorID: get(item, 'AccessorID'),
SecretID: get(item, 'SecretID'),
Namespace: get(item, 'Namespace'),
},
})
.then(() => {

View File

@ -0,0 +1,12 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'ServiceName';
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
Datacenter: attr('string'),
Chain: attr(),
meta: attr(),
});

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,11 +26,3 @@ const model = Model.extend({
CreateIndex: attr('number'),
ModifyIndex: attr('number'),
});
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

@ -1,11 +1,10 @@
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'),
Name: attr('string', {
@ -21,6 +20,7 @@ const model = Model.extend({
CreateTime: attr('date'),
//
Datacenter: attr('string'),
Namespace: attr('string'),
Datacenters: attr(),
CreateIndex: attr('number'),
ModifyIndex: attr('number'),
@ -29,5 +29,3 @@ const model = Model.extend({
defaultValue: '',
}),
});
export const ATTRS = writable(model, ['Name', 'Description', 'Rules', 'Datacenters']);
export default model;

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

@ -1,11 +1,10 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import writable from 'consul-ui/utils/model/writable';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'AccessorID';
const model = Model.extend({
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
IDPName: attr('string'),
@ -22,6 +21,7 @@ const model = Model.extend({
defaultValue: '',
}),
Datacenter: attr('string'),
Namespace: attr('string'),
Local: attr('boolean'),
Policies: attr({
defaultValue: function() {
@ -43,18 +43,3 @@ const model = Model.extend({
CreateIndex: attr('number'),
ModifyIndex: attr('number'),
});
// Name and Rules is only for legacy tokens
export const ATTRS = writable(model, [
'Name',
'Rules',
'Type',
'Local',
'Description',
'Policies',
'Roles',
// SecretID isn't writable but we need it to identify an
// update via the old API, see TokenAdapter dataForRequest
'SecretID',
'AccessorID',
]);
export default model;

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

@ -11,9 +11,7 @@ const removeLoading = function($from) {
};
export default Route.extend(WithBlockingActions, {
dom: service('dom'),
init: function() {
this._super(...arguments);
},
nspacesRepo: service('repository/nspace/disabled'),
repo: service('repository/dc'),
settings: service('settings'),
actions: {
@ -27,6 +25,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');
@ -77,16 +76,20 @@ export default Route.extend(WithBlockingActions, {
const $root = this.dom.root();
hash({
error: error,
nspace: this.nspacesRepo.getActive(),
dc:
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 => {
removeLoading($root);
model.nspaces = [model.nspace];
// we can't use setupController as we received an error
// so we do it manually instead
next(() => {
this.controllerFor('error').setProperties(model);
});

View File

@ -1,23 +1,105 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { hash, Promise } 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);
},
actions: {
// TODO: This will eventually be deprecated please see
// https://deprecations.emberjs.com/v3.x/#toc_deprecate-router-events
willTransition: function(transition) {
this._super(...arguments);
if (
typeof transition !== 'undefined' &&
(transition.from.name.endsWith('nspaces.create') ||
transition.from.name.startsWith('nspace.dc.acls.tokens'))
) {
// Only when we create, reload the nspaces in the main menu to update them
// as we don't block for those
// And also when we [Use] a token reload the nspaces that you are able to see,
// including your permissions for being able to manage namespaces
// Potentially we should just do this on every single transition
// but then we would need to check to see if nspaces are enabled
Promise.all([
this.nspacesRepo.findAll(),
this.nspacesRepo.authorize(
get(this.controller, 'dc.Name'),
get(this.controller, 'nspace.Name')
),
]).then(([nspaces, permissions]) => {
if (typeof this.controller !== 'undefined') {
this.controller.setProperties({
nspaces: nspaces,
permissions: permissions,
});
}
});
}
},
},
});

View File

@ -1,13 +1,15 @@
import Route from '@ember/routing/route';
import { get } from '@ember/object';
import { config } from 'consul-ui/env';
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 +18,7 @@ export default Route.extend(WithBlockingActions, {
token: {
AccessorID: get(item, 'AccessorID'),
SecretID: secret,
Namespace: get(item, 'Namespace'),
},
})
.then(item => {
@ -29,7 +32,17 @@ export default Route.extend(WithBlockingActions, {
return false;
});
} else {
this.refresh();
// TODO: Ideally we wouldn't need to use config() at a route level
// transitionTo should probably remove it instead if NSPACES aren't enabled
if (config('CONSUL_NSPACES_ENABLED') && 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

@ -4,6 +4,7 @@ import { hash } from 'rsvp';
export default Route.extend({
repo: service('repository/service'),
chainRepo: service('repository/discovery-chain'),
settings: service('settings'),
queryParams: {
s: {
@ -12,12 +13,12 @@ export default Route.extend({
},
},
model: function(params) {
const repo = this.repo;
const settings = this.settings;
const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1);
return hash({
item: repo.findBySlug(params.name, dc),
urls: settings.findBySlug('urls'),
item: this.repo.findBySlug(params.name, dc, nspace),
chain: this.chainRepo.findBySlug(params.name, dc, nspace),
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),
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;
}
@ -65,9 +88,12 @@ export default Serializer.extend({
const primaryKey = this.primaryKey;
return respond((headers, body) => {
// Deletes only need the primaryKey/uid returning
// and they need the slug key AND potential namespace in order to
// create the correct uid/fingerprint
return {
[primaryKey]: this.fingerprint(primaryKey, slugKey, data[DATACENTER_KEY])({
[slugKey]: data[slugKey],
[NSPACE_KEY]: data[NSPACE_KEY],
})[primaryKey],
};
});
@ -116,6 +142,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

@ -0,0 +1,17 @@
import Serializer from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/discovery-chain';
export default Serializer.extend({
primaryKey: PRIMARY_KEY,
slugKey: SLUG_KEY,
respondForQueryRecord: function(respond, query) {
return this._super(function(cb) {
return respond(function(headers, body) {
return cb(headers, {
...body,
[SLUG_KEY]: body.Chain[SLUG_KEY],
});
});
}, query);
},
});

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

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