mirror of https://github.com/status-im/consul.git
Merge pull request #6966 from hashicorp/ui-staging
ui: UI Release Merge (ui-staging merge)
This commit is contained in:
commit
95a71bba04
|
@ -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} && \
|
||||
|
|
|
@ -5,6 +5,5 @@
|
|||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false,
|
||||
"proxy": "http://localhost:3000"
|
||||
"disableAnalytics": false
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
8
|
||||
10
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
`;
|
||||
},
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
`;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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}
|
||||
`;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
`;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
`;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
`;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
`;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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() {}) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {},
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
import Slotted from 'block-slots';
|
||||
|
||||
export default Component.extend(Slotted, {
|
||||
tagName: '',
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
});
|
|
@ -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: '/',
|
||||
}
|
||||
);
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({});
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
import Controller from './edit';
|
||||
export default Controller.extend();
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
}),
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'));
|
||||
}
|
|
@ -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>')`,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 '';
|
||||
});
|
|
@ -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)}`;
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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');
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
|
||||
export default Mixin.create(WithBlockingActions, {});
|
|
@ -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(() => {
|
||||
|
|
|
@ -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(),
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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') || '');
|
||||
|
|
|
@ -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'),
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -11,4 +11,6 @@ export default Model.extend({
|
|||
Node: attr('string'),
|
||||
ServiceProxy: attr(),
|
||||
SyncTime: attr('number'),
|
||||
Datacenter: attr('string'),
|
||||
Namespace: attr('string'),
|
||||
});
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -28,6 +28,7 @@ export default Model.extend({
|
|||
ChecksWarning: attr(),
|
||||
Nodes: attr(),
|
||||
Datacenter: attr('string'),
|
||||
Namespace: attr('string'),
|
||||
Node: attr(),
|
||||
Service: attr(),
|
||||
Checks: attr(),
|
||||
|
|
|
@ -20,5 +20,6 @@ export default Model.extend({
|
|||
},
|
||||
}),
|
||||
Datacenter: attr('string'),
|
||||
Namespace: attr('string'),
|
||||
SyncTime: attr('number'),
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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'),
|
||||
});
|
||||
|
|
|
@ -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()),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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()),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
|
@ -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 });
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue