ui: Partitions Application Layer (#11017)

* Add Partition to all our models

* Add partitions into our serializers/fingerprinting

* Make some amends to a few adapters ready for partitions

* Amend blueprints to avoid linting error

* Update all  our repositories to include partitions, also

Remove enabled/disable nspace repo and just use a nspace with
conditionals

* Ensure nspace and parition parameters always return '' no matter what

* Ensure data-sink finds the model properly

This will later be replaced by a @dataSink decorator but we are find
kicking that can down the road a little more

* Add all the new partition data layer

* Add a way to set the title of the page from inside the route

and make it accessibile via a route announcer

* Make the Consul Route the default/basic one

* Tweak nspace and partition abilities not to check the length

* Thread partition through all the components that need it

* Some ACL tweaks

* Move the entire app to use partitions

* Delete all the tests we no longer need

* Update some Unit tests to use partition

* Fix up KV title tests

* Fix up a few more acceptance tests

* Fixup and temporarily ignore some acceptance tests

* Stop using ember-cli-page-objects fillable as it doesn't seem to work

* Fix lint error

* Remove old ACL related test

* Add a tick after filling out forms

* Fix token warning modal

* Found some more places where we need a partition var

* Fixup some more acceptance tests

* Tokens still needs a repo service for CRUD

* Remove acceptance tests we no longer need

* Fixup and "FIXME ignore" a few tests

* Remove an s

* Disable blocking queries for KV to revert to previous release for now

* Fixup adapter tests to follow async/function resolving interface

* Fixup all the serializer integration tests

* Fixup service/repo integration tests

* Fixup deleting acceptance test

* Fixup some ent tests

* Make sure nspaces passes the dc through for when thats important

* ...aaaand acceptance nspaces with the extra dc param
This commit is contained in:
John Cowen 2021-09-15 19:50:11 +01:00 committed by GitHub
parent 0eb4a98fab
commit fc14a412fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
334 changed files with 3573 additions and 3295 deletions

View File

@ -16,7 +16,7 @@ export default class NspaceAbility extends BaseAbility {
}
get canChoose() {
return this.canUse && (this.nspaces || []).length > 0;
return this.canUse;
}
get canUse() {

View File

@ -16,7 +16,7 @@ export default class PartitionAbility extends BaseAbility {
}
get canChoose() {
return this.canUse && (this.partitions || []).length > 0;
return this.canUse;
}
get canUse() {

View File

@ -1,7 +1,7 @@
import Adapter from './application';
export default class BindingRuleAdapter extends Adapter {
requestForQuery(request, { dc, ns, partition, authmethod, index, id }) {
requestForQuery(request, { dc, ns, partition, authmethod, index }) {
return request`
GET /v1/acl/binding-rules?${{ dc, authmethod }}

View File

@ -21,6 +21,7 @@ export default class IntentionAdapter extends Adapter {
}
${{
partition: '',
ns: '*',
index,
filter,

View File

@ -7,11 +7,11 @@ import { SLUG_KEY } from 'consul-ui/models/kv';
const API_KEYS_KEY = 'keys';
export default class KvAdapter extends Adapter {
requestForQuery(request, { dc, ns, partition, index, id, separator }) {
async requestForQuery(request, { dc, ns, partition, index, id, separator }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
const respond = await request`
GET /v1/kv/${keyToArray(id)}?${{ [API_KEYS_KEY]: null, dc, separator }}
${{
@ -20,9 +20,11 @@ export default class KvAdapter extends Adapter {
index,
}}
`;
await respond((headers, body) => delete headers['x-consul-index']);
return respond;
}
requestForQueryRecord(request, { dc, ns, partition, index, id }) {
async requestForQueryRecord(request, { dc, ns, partition, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}

View File

@ -39,6 +39,7 @@ export default class NodeAdapter extends Adapter {
`;
}
// this does not require a partition parameter
requestForQueryLeader(request, { dc, uri }) {
return request`
GET /v1/status/leader?${{ dc }}

View File

@ -3,9 +3,9 @@ import { SLUG_KEY } from 'consul-ui/models/nspace';
// namespaces aren't categorized by datacenter, therefore no dc
export default class NspaceAdapter extends Adapter {
requestForQuery(request, { partition, index, uri }) {
requestForQuery(request, { dc, partition, index, uri }) {
return request`
GET /v1/namespaces
GET /v1/namespaces?${{ dc }}
X-Request-ID: ${uri}
${{
@ -15,12 +15,12 @@ export default class NspaceAdapter extends Adapter {
`;
}
requestForQueryRecord(request, { partition, index, id }) {
requestForQueryRecord(request, { dc, partition, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an name');
}
return request`
GET /v1/namespace/${id}
GET /v1/namespace/${id}?${{ dc }}
${{
partition,
@ -32,6 +32,7 @@ export default class NspaceAdapter extends Adapter {
requestForCreateRecord(request, serialized, data) {
return request`
PUT /v1/namespace/${data[SLUG_KEY]}?${{
dc: data.Datacenter,
partition: data.Partition,
}}
@ -49,6 +50,7 @@ export default class NspaceAdapter extends Adapter {
requestForUpdateRecord(request, serialized, data) {
return request`
PUT /v1/namespace/${data[SLUG_KEY]}?${{
dc: data.Datacenter,
partition: data.Partition,
}}
@ -65,6 +67,7 @@ export default class NspaceAdapter extends Adapter {
requestForDeleteRecord(request, serialized, data) {
return request`
DELETE /v1/namespace/${data[SLUG_KEY]}?${{
dc: data.Datacenter,
partition: data.Partition,
}}
`;

View File

@ -0,0 +1,28 @@
import Adapter from './application';
// Blocking query support for partitions is currently disabled
export default class PartitionAdapter extends Adapter {
// FIXME: Check overall hierarchy again
async requestForQuery(request, { ns, dc, index }) {
const respond = await request`
GET /v1/partitions?${{ dc }}
${{ index }}
`;
await respond((headers, body) => delete headers['x-consul-index']);
return respond;
}
async requestForQueryRecord(request, { ns, dc, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
const respond = await request`
GET /v1/partition/${id}?${{ dc }}
${{ index }}
`;
await respond((headers, body) => delete headers['x-consul-index']);
return respond;
}
}

View File

@ -21,7 +21,7 @@ export default class PermissionAdapter extends Adapter {
resources = resources.map(item => ({ ...item, Partition: partition }));
}
return request`
POST /v1/internal/acl/authorize?${{ dc, index }}
POST /v1/internal/acl/authorize?${{ dc }}
${resources}
`;

View File

@ -17,12 +17,13 @@ export default class TokenAdapter extends Adapter {
`;
}
requestForQueryRecord(request, { dc, ns, partition, index, id }) {
async requestForQueryRecord(request, { dc, ns, partition, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
const respond = await request`
GET /v1/acl/token/${id}?${{ dc }}
Cache-Control: no-store
${{
ns,
@ -30,6 +31,8 @@ export default class TokenAdapter extends Adapter {
index,
}}
`;
respond((headers, body) => delete headers['x-consul-index']);
return respond;
}
requestForCreateRecord(request, serialized, data) {

View File

@ -22,7 +22,11 @@
token=token
) (hash
AuthProfile=(component 'auth-profile' item=token)
AuthForm=(component 'auth-form' dc=dc nspace=nspace onsubmit=(action sink.open value="data"))
AuthForm=(component 'auth-form'
dc=dc
partition=partition
nspace=nspace
onsubmit=(action sink.open value="data"))
) as |api components|}}
<State @matches="authorized">
{{#yield-slot name="authorized"}}

View File

@ -77,7 +77,13 @@
{{#if (env 'CONSUL_SSO_ENABLED')}}
{{!-- This `or` can be completely removed post 1.10 as 1.10 has optional params with default values --}}
<DataSource
@src={{concat '/' (or nspace '') '/' dc '/oidc/providers'}}
@src={{uri '/${partition}/${nspace}/${dc}/oidc/providers'
(hash
partition=partition
nspace=(or nspace '')
dc=dc
)
}}
@onchange={{queue (action (mut providers) value="data")}}
@onerror={{queue (action (mut error) value="error.errors.firstObject")}}
@loading="lazy"
@ -96,9 +102,11 @@
{{/if}}
</div>
<State @matches="loading">
{{!FIXME: default partition?}}
<TokenSource
@dc={{dc}}
@nspace={{or value.Namespace nspace}}
@partition={{or value.Partition 'default'}}
@type={{if value.Name 'oidc' 'secret'}}
@value={{if value.Name value.Name value}}
@onchange={{queue (action dispatch "RESET") (action onsubmit)}}

View File

@ -9,7 +9,14 @@
<span><YieldSlot @name="label">{{yield}}</YieldSlot></span>
{{#if isOpen}}
<DataSource
@src={{concat '/' nspace '/' dc '/' (pluralize type)}}
@src={{uri '/${partition}/${nspace}/${dc}/${type}'
(hash
partition=partition
nspace=nspace
dc=dc
type=(pluralize type)
)
}}
@onchange={{action (mut allOptions) value="data"}}
/>
{{/if}}

View File

@ -0,0 +1,28 @@
<AppView>
<BlockSlot @name="header">
<h1>
Tokens
</h1>
</BlockSlot>
<BlockSlot @name="content">
<EmptyState data-test-acls-disabled>
<BlockSlot @name="header">
<h2>Welcome to ACLs</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
ACLs are not enabled in this Consul cluster. We strongly encourage the use of ACLs in production environments for the best security practices.
</p>
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}/acl/index.html" rel="noopener noreferrer" target="_blank">Read the documentation</a>
</li>
<li class="learn-link">
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/security-networking/production-acls" rel="noopener noreferrer" target="_blank">Follow the guide</a>
</li>
</BlockSlot>
</EmptyState>
</BlockSlot>
</AppView>

View File

@ -6,7 +6,7 @@ A presentational component for rendering HealthChecks.
<figure>
<figcaption>Grab some mock data...</figcaption>
<DataSource @src="/default/dc-1/node/my-node" as |source|>
<DataSource @src="/partition/default/dc-1/node/my-node" as |source|>
<figure>
<figcaption>but only show a max of 2 items for docs purposes</figcaption>

View File

@ -6,6 +6,7 @@
@type="intention"
@dc={{@dc}}
@nspace={{@nspace}}
@partition={{@partition}}
@autofill={{@autofill}}
@item={{@item}}
@src={{@src}}
@ -73,15 +74,27 @@ as |api|>
{{/let}}
<DataSource
@src={{concat '/*/' @dc '/services'}}
@src={{uri '/${partition}/*/${dc}/services'
(hash
partition=@partition
dc=@dc
)
}}
@onchange={{action this.createServices item}}
/>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (can 'use nspaces')}}
<DataSource
@src="/*/*/namespaces"
@src={{uri '/${partition}/*/${dc}/namespaces'
(hash
partition=@partition
dc=@dc
)
}}
@onchange={{action this.createNspaces item}}
/>
{{/if}}
{{#if (and api.isCreate this.isManagedByCRDs)}}
<Consul::Intention::Notice::CustomResource @type="warning" />
{{/if}}

View File

@ -1,6 +1,7 @@
<DataForm
@dc={{dc}}
@nspace={{nspace}}
@partition={{partition}}
@type="kv"
@label="key"
@autofill={{autofill}}
@ -19,11 +20,11 @@
{{#if api.isCreate}}
<label class="type-text{{if api.data.error.Key ' has-error'}}">
<span>Key or folder</span>
<input autofocus="autofocus" type="text" value={{left-trim api.data.Key parent.Key}} name="additional" oninput={{action api.change}} placeholder="Key or folder" />
<input autofocus="autofocus" type="text" value={{left-trim api.data.Key parent}} name="additional" oninput={{action api.change}} placeholder="Key or folder" />
<em>To create a folder, end a key with <code>/</code></em>
</label>
{{/if}}
{{#if (or (eq (left-trim api.data.Key parent.Key) '') (not-eq (last api.data.Key) '/'))}}
{{#if (or (eq (left-trim api.data.Key parent) '') (not-eq (last api.data.Key) '/'))}}
<div>
<div class="type-toggle">
<label>

View File

@ -26,7 +26,7 @@ export default Component.extend({
set(item, 'Value', this.encoder.execute(target.value));
break;
case 'additional':
parent = get(this, 'parent.Key');
parent = get(this, 'parent');
set(item, 'Key', `${parent !== '/' ? parent : ''}${target.value}`);
break;
case 'json':

View File

@ -1,6 +1,7 @@
<DataForm
@dc={{dc}}
@nspace={{nspace}}
@partition={{partition}}
@item={{item}}
@type="session"
@onsubmit={{action onsubmit}}

View File

@ -4,7 +4,7 @@ class: ember
# Consul::LockSession::List
```hbs preview-template
<DataSource @src="/default/dc-1/sessions/for-node/my-node" as |source|>
<DataSource @src="/partition/default/dc-1/sessions/for-node/my-node" as |source|>
<Consul::LockSession::List
@items={{source.data}}
@onInvalidate={{action (noop)}}

View File

@ -8,7 +8,7 @@ A presentational component for presenting Consul Metadata
The following example shows how to construct the required structure from the Consul API using ember-componsable-helpers' `entries` helper.
```hbs
<DataSource @src="/default/dc-1/service-instance/service-id/node-0/service-0" as |source|>
<DataSource @src="/partition/default/dc-1/service-instance/service-id/node-0/service-0" as |source|>
<Consul::Metadata::List
@items={{entries source.data.firstObject.Meta}}
/>

View File

@ -4,7 +4,7 @@ class: ember
## Consul::Nspace::List
```hbs
<DataSource @src="/default/dc-1/namespaces" as |source|>
<DataSource @src="/partition/default/dc-1/namespaces" as |source|>
<Consul::Nspace::List
@items={{source.data}}
@ondelete={{action (noop)}}

View File

@ -81,7 +81,15 @@ as |key value|}}
{{#each dcs as |dc|}}
<Option @value={{dc.Name}} @selected={{contains dc.Name @filter.datacenter.value}}>{{dc.Name}}</Option>
{{/each}}
<DataSource @src="/*/*/datacenters" @loading="lazy" @onchange={{action (mut this.dcs) value="data"}} />
<DataSource
@src={{uri "/${partition}/*/*/datacenters"
(hash
partition=@partition
)
}}
@loading="lazy"
@onchange={{action (mut this.dcs) value="data"}}
/>
{{/let}}
</BlockSlot>
</search.Select>

View File

@ -19,7 +19,7 @@ Lastly, a `SearchService` in `services/search.js` configures what is available f
<figure>
<figcaption>Get some data to search on</figcaption>
<DataSource @src="/nspace/dc-1/services" as |source|>
<DataSource @src="/partition/nspace/dc-1/services" as |source|>
<figure>
<figcaption>and show the complete set of data</figcaption>

View File

@ -1,8 +1,9 @@
<DataLoader
@items={{item}}
@src={{uri
'/${nspace}/${dc}/${type}/${src}'
'/${partition}/${nspace}/${dc}/${type}/${src}'
(hash
partition=partition
nspace=nspace
dc=dc
type=type
@ -16,8 +17,9 @@
<DataWriter
@sink={{uri
'/${nspace}/${dc}/${type}'
'/${partition}/${nspace}/${dc}/${type}'
(hash
partition=partition
nspace=nspace
dc=(or data.Datacenter dc)
type=type

View File

@ -46,7 +46,7 @@ export default Component.extend(Slotted, {
}
// mark as creating
// and autofill the new record if required
if (get(changeset, 'isNew')) {
if (get(data, 'isNew')) {
set(this, 'create', true);
changeset = Object.entries(this.autofill || {}).reduce(function(prev, [key, value]) {
set(prev, key, value);

View File

@ -0,0 +1,73 @@
# DataLoader
`<DataLoader />` works similarly to, and uses, `<DataSource />` but additionally
exposes various common states based on the status of the loading of the data.
These states are exposed as slots to enable you to easily render different
elements based on the state of the data.
Use the `@dataSource` decorator in your repositories to define URI to async
method mapping.
```javascript
class SomethingRepository extends Service {
@dataSource('/:partition/:nspace/:dc/services')
async youCouldCallItAnythingTodoWithGettingServices(params) {
console.log(params);
// {partition: "partition", nspace: "nspace", dc: "dc"}
return getTheThing(params);
}
}
```
```hbs preview-template
<DataLoader
@src="/partition/nspace/dc/services"
as |loader|>
<BlockSlot @name="loading">
Loading...
</BlockSlot>
<BlockSlot @name="error">
Error {{loader.error.status}}
</BlockSlot>
<BlockSlot @name="disconnected">
Whilst we could load the initial data, something happened subsequently that
meant we could load longer load updates to the data.
</BlockSlot>
<BlockSlot @name="loaded">
{{#each loader.data as |service|}}
{{service.Name}}<br />
{{/each}}
</BlockSlot>
</DataLoader>
```
## Attributes
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `src` | `String` | | The source to subscribe to updates to, this should map to a string based URI |
## Exports
| Name | Description |
| --- | --- |
| `data` | The loaded dataset once any data has been loaded successfully |
| `error` | The error thrown if an error is encountered whilst loading data |
## Slots
| Name | Description |
| --- | --- |
| `loading` | Rendered whilst waiting for the initial data to load. |
| `error` | If there is an error only whilst waiting for the initial data to load, this slot is rendered. |
| `disconnected` | Rendered when the initial data has already loaded, but a subsequent set of loaded data causes an error to be thrown.|
| `loaded` | Rendered once the initial data is loaded and on subsequent successful loads of data. |
## See
- [DataSource](../data-source/README.mdx)
- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)
---

View File

@ -2,6 +2,9 @@ export default {
id: 'data-loader',
initial: 'load',
on: {
OPEN: {
target: 'load',
},
ERROR: {
target: 'disconnected',
},

View File

@ -54,12 +54,14 @@
{{#yield-slot name="disconnected" params=(block-params (component 'notification' after=(action dispatch "RESET")))}}
{{yield api}}
{{else}}
{{#if (not eq error.status '401')}}
<Notification @sticky={{true}}>
<p data-notification role="alert" class="warning notification-update">
<strong>Warning!</strong>
An error was returned whilst loading this data, refresh to try again.
</p>
</Notification>
{{/if}}
{{/yield-slot}}
</State>
{{#if (eq error.status "403")}}

View File

@ -1,8 +1,22 @@
## DataSource
# DataSource
Use the `@dataSource` decorator in your repositories to define URI to async
method mapping.
```javascript
class SomethingRepository extends Service {
@dataSource('/:partition/:nspace/:dc/services')
async youCouldCallItAnythingTodoWithGettingServices(params) {
console.log(params);
// {partition: "partition", nspace: "nspace", dc: "dc"}
return getTheThing(params);
}
}
```
```hbs preview-template
<DataSource
@src="/nspace/dc/services"
@src="/partition/nspace/dc/services"
@loading="eager"
@disabled={{false}}
as |source|>
@ -12,7 +26,7 @@ as |source|>
</DataSource>
```
### Arguments
## Attributes
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
@ -34,14 +48,14 @@ Behind the scenes in the Consul UI we map URIs back to our `ember-data` backed `
`DataSource` is not just restricted to HTTP API data, and can be configured to listen for data changes using a variety of methods and sources. For example we have also configured `DataSource` to listen to `LocalStorage` changes using the `settings://` pseudo-protocol in the URI (See examples below).
### Examples
## Examples
Straightforward usage can use `mut` to easily update data within a template using an event handler approach.
```hbs
{{! listen for HTTP API changes}}
<DataSource
@src="/nspace/dc/services"
@src="/partition/nspace/dc/services"
@onchange={{action (mut items) value="data"}}
@onerror={{action (mut error) value="error"}}
/>
@ -71,7 +85,7 @@ A property approach to easily update data within a template
```hbs
{{! listen for HTTP API changes}}
<DataSource
@src="/nspace/dc/services"
@src="/partition/nspace/dc/services"
as |source|>
{{#if source.error}}
Something went wrong!
@ -101,19 +115,19 @@ DataSources can also be recursively nested for loading in series as opposed to i
{{! listen for HTTP API changes}}
<DataSource
@src="/nspace/dc/services"
@src="/partition/nspace/dc/services"
@onerror={{action (mut error) value="error"}}
as |source|>
<source.Source
@src="/nspace/dc/service/{{source.data.firstObject.Name}}"
@src="/partition/nspace/dc/service/{{source.data.firstObject.Name}}"
@onerror={{action (mut error) value="error"}}
as |source|>
{{source.data.Service.Service.Name}} <== Detailed information for the first service
<source.Source
@src="/nspace/dc/proxy/for-service/{{source.data.Service.ID}}"
@src="/partition/nspace/dc/proxy/for-service/{{source.data.Service.ID}}"
@onerror={{action (mut error) value="error"}}
@onchange={{action (mut loaded) true}}
as |source|>
@ -127,7 +141,7 @@ DataSources can also be recursively nested for loading in series as opposed to i
</DataSource>
```
### See
## See
- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)

View File

@ -89,6 +89,14 @@ export default class DataSource extends Component {
@action
disconnect() {
// FIXME? Should we be doing this here
if (
typeof this.data !== 'undefined' &&
typeof this.data.length === 'undefined' &&
typeof this.data.rollbackAttributes === 'function'
) {
this.data.rollbackAttributes();
}
this.close();
this._listeners.remove();
this._lazyListeners.remove();
@ -169,6 +177,15 @@ export default class DataSource extends Component {
}
}
}
@action
async invalidate() {
this.source.readyState = 2;
this.disconnect();
schedule('afterRender', () => {
// FIXME: Lazy data-sources
this.connect([]);
});
}
// keep this argumentless
@action

View File

@ -11,9 +11,7 @@
</:home-nav>
<:main-nav>
{{#if @dc}}
<ul>
<li
class="dcs"
data-test-datacenter-menu
@ -28,7 +26,7 @@
<BlockSlot @name="menu">
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}}
<DataSource
@src="/*/*/datacenters"
@src="/*/*/*/datacenters"
@onchange={{action (mut @dcs) value="data"}}
@loading="lazy"
/>
@ -51,8 +49,61 @@
</PopoverMenu>
</li>
{{#let (or this.nspaces @nspaces) as |nspaces|}}
{{#if (can "choose nspaces" nspaces=nspaces)}}
{{#if (can "choose partitions")}}
<li
class="partitions"
data-test-partition-menu
>
<PopoverMenu
aria-label="Admin Partition"
@position="left"
as |components api|>
<BlockSlot @name="trigger">
{{@partition}}
</BlockSlot>
<BlockSlot @name="menu">
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}}
<DataSource
@src={{uri
'/*/*/${dc}/partitions'
(hash
dc=@dc.Name
)
}}
@onchange={{action (mut this.partitions) value="data"}}
/>
{{!FIXME: Do partitions do the same as namespace deletion? }}
{{#each (reject-by 'DeletedAt' this.partitions) as |item|}}
<MenuItem
class={{if (eq @partition item.Name) 'is-active'}}
@href={{href-to '.' params=(hash
partition=item.Name
nspace=(if (gt @nspace.length 0) @nspace undefined)
)}}
>
<BlockSlot @name="label">
{{item.Name}}
</BlockSlot>
</MenuItem>
{{/each}}
{{#if (and false (can 'manage partitions'))}}
<MenuSeparator />
<MenuItem
data-test-main-nav-partitions
@href={{href-to 'dc.nspaces' @dc.Name}}
>
<BlockSlot @name="label">
Manage Admin Partitions
</BlockSlot>
</MenuItem>
{{/if}}
{{/let}}
</BlockSlot>
</PopoverMenu>
</li>
{{/if}}
{{#if (can "choose nspaces")}}
<li
class="nspaces"
data-test-nspace-menu
@ -62,7 +113,7 @@
@position="left"
as |components api|>
<BlockSlot @name="trigger">
{{@nspace.Name}}
{{@nspace}}
</BlockSlot>
{{#if (is-href 'dc.nspaces')}}
<BlockSlot @name="header">
@ -74,14 +125,23 @@
<BlockSlot @name="menu">
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}}
<DataSource
@src="/*/*/namespaces"
@src={{uri
'/${partition}/*/${dc}/namespaces'
(hash
partition=@partition
dc=@dc.Name
)
}}
@onchange={{action (mut this.nspaces) value="data"}}
@loading="lazy"
/>
{{#each (reject-by 'DeletedAt' nspaces) as |item|}}
{{#each (reject-by 'DeletedAt' this.nspaces) as |item|}}
<MenuItem
class={{if (eq @nspace.Name item.Name) 'is-active'}}
@href={{href-to '.' params=(hash nspace=item.Name)}}
class={{if (eq @nspace item.Name) 'is-active'}}
@href={{href-to '.' params=(hash
partition=(if (gt @partition.length 0) @partition undefined)
nspace=item.Name
)}}
>
<BlockSlot @name="label">
{{item.Name}}
@ -104,7 +164,6 @@
</PopoverMenu>
</li>
{{/if}}
{{/let}}
{{#if (can "read services")}}
<li data-test-main-nav-services class={{if (is-href 'dc.services' @dc.Name) 'is-active'}}>
<a href={{href-to 'dc.services' @dc.Name}}>Services</a>
@ -158,8 +217,6 @@
</li>
{{/if}}
</ul>
{{/if}}
</:main-nav>
<:complementary-nav>
@ -207,13 +264,17 @@
</PopoverMenu>
</li>
<li data-test-main-nav-settings class={{if (is-href 'settings') 'is-active'}}>
<a href={{href-to 'settings'}}>Settings</a>
<a href={{href-to 'settings' params=(hash
nspace=undefined
partition=undefined
)}}>Settings</a>
</li>
{{#if (can 'authenticate')}}
<li data-test-main-nav-auth>
<AuthDialog
@dc={{@dc.Name}}
@nspace={{@nspace.Name}}
@nspace={{@nspace}}
@partition={{@partition}}
@onchange={{this.reauthorize}} as |authDialog components|
>
{{#let components.AuthForm components.AuthProfile as |AuthForm AuthProfile|}}

View File

@ -11,6 +11,7 @@
%main-nav-vertical:not(.in-viewport) {
visibility: hidden;
}
%main-nav-vertical li.partitions,
%main-nav-vertical li.nspaces,
%main-nav-vertical li.dcs {
margin-bottom: 25px;

View File

@ -18,7 +18,7 @@
{{#each templates as |template|}}
<label data-test-radiobutton={{concat 'template_' template.template}}>
<span>{{template.name}}</span>
<input type="radio" name={{concat name '[template]'}} value={{template.template}} checked={{eq item.template template.template}} onchange={{action (changeset-set item 'template') value='target.value'}}/>
<input type="radio" name={{concat name '[template]'}} value={{template.template}} checked={{eq item.template template.template}} onchange={{action (optional (changeset-set item 'template')) value='target.value'}}/>
</label>
{{/each}}
</div>
@ -72,7 +72,8 @@
</label>
{{#if (eq item.template 'node-identity')}}
<DataSource @src="/*/*/datacenters"
<DataSource
@src={{uri '/*/*/*/datacenters'}}
@onchange={{action (mut datacenters) value="data"}}
/>
<label class="type-select" data-test-datacenter>
@ -95,7 +96,8 @@
</label>
</div>
{{#if isScoped }}
<DataSource @src="/*/*/datacenters"
<DataSource
@src={{uri '/*/*/*/datacenters'}}
@onchange={{action (mut datacenters) value="data"}}
/>

View File

@ -2,6 +2,7 @@
@disabled={{disabled}}
@repo={{repo}}
@dc={{dc}}
@partition={{partition}}
@nspace={{nspace}}
@type="policy"
@placeholder="Search for policy"
@ -38,7 +39,13 @@
<h2>New Policy</h2>
</BlockSlot>
<BlockSlot @name="body">
<PolicyForm @form={{form}} @nspace={{nspace}} @dc={{dc}} @allowServiceIdentity={{allowServiceIdentity}} />
<PolicyForm
@form={{form}}
@nspace={{nspace}}
@partition={{partition}}
@dc={{dc}}
@allowServiceIdentity={{allowServiceIdentity}}
/>
</BlockSlot>
<BlockSlot @name="actions" as |close|>
<button type="submit"
@ -79,7 +86,14 @@
<BlockSlot @name="details">
{{#if (eq item.template '')}}
<DataSource
@src={{concat '/' item.Namespace '/' dc '/policy/' item.ID}}
@src={{uri '/${partition}/${nspace}/${dc}/policy/${id}'
(hash
partition=item.Partition
nspace=item.Namespace
dc=dc
id=item.ID
)
}}
@onchange={{action (mut loadedItem) value="data"}}
@loading="lazy"
/>

View File

@ -29,6 +29,7 @@
<PolicySelector
@disabled={{not (can "write role" item=item)}}
@dc={{dc}}
@partition={{partition}}
@nspace={{nspace}}
@items={{item.Policies}}
/>

View File

@ -18,10 +18,21 @@ as |modal|>
<BlockSlot @name="body">
<input id="{{name}}_state_role" type="radio" name="{{name}}[state]" value="role" checked={{if (eq state 'role') 'checked'}} onchange={{action 'change'}} />
<RoleForm @form={{form}} @dc={{dc}} @nspace={{nspace}}>
<RoleForm
@form={{form}}
@dc={{dc}}
@nspace={{nspace}}
@partition={{partition}}
>
<BlockSlot @name="policy">
<PolicySelector @source={{source}} @dc={{dc}} @nspace={{nspace}} @items={{item.Policies}}>
<PolicySelector
@source={{source}}
@dc={{dc}}
@partition={{partition}}
@nspace={{nspace}}
@items={{item.Policies}}
>
<BlockSlot @name="trigger">
<label for="{{name}}_state_policy" data-test-create-policy class="type-dialog">
<span>Create new policy</span>
@ -33,7 +44,14 @@ as |modal|>
</RoleForm>
<input id="{{name}}_state_policy" type="radio" name="{{name}}[state]" value="policy" checked={{if (eq state 'policy') 'checked'}} onchange={{action 'change'}} />
<PolicyForm data-test-policy-form @name="role[policy]" @form={{policyForm}} @dc={{dc}} />
<PolicyForm
data-test-policy-form
@name="role[policy]"
@form={{policyForm}}
@dc={{dc}}
@nspace={{nspace}}
@partition={{partition}}
/>
</BlockSlot>
<BlockSlot @name="actions" as |close|>
@ -62,7 +80,16 @@ as |modal|>
</BlockSlot>
</ModalDialog>
<ChildSelector @disabled={{disabled}} @repo={{repo}} @dc={{dc}} @nspace={{nspace}} @type="role" @placeholder="Search for role" @items={{items}}>
<ChildSelector
@disabled={{disabled}}
@repo={{repo}}
@dc={{dc}}
@partition={{partition}}
@nspace={{nspace}}
@type="role"
@placeholder="Search for role"
@items={{items}}
>
<BlockSlot @name="label">
Apply an existing role
</BlockSlot>

View File

@ -12,8 +12,8 @@ routes.
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `String` | `undefined` | The name of the route in ember routeName format e.g. `dc.services.index`. This is generally the `routeName` variable that is available to you in all Consul UI route/page level templates.|
| `title` | `String` | `undefined` | The title for this page (eventually passed through to the `{{page-title}}` helper. This argument should be omitted if a title change isn't required. |
| `titleSeparator` | `String` | `undefined` | This can be used in the top-level route to configure the separator for the `{{page-title}}` helper |
| `title` | `String` | `undefined` | Deprecated: The title for this page (eventually passed through to the `{{page-title}}` helper. This argument should be omitted if a title change isn't required. Also see the exported `<Title />` component which is now preferred |
| `titleSeparator` | `String` | `undefined` | Deprecated: This can be used in the top-level route to configure the separator for the `{{page-title}}` helper. Also see the exported `<Title />` component which is now preferred |
## Exports
@ -21,16 +21,28 @@ routes.
| --- | --- | --- | --- |
| `model` | `Object` | `undefined` | Arbitrary hash of data passed down from the parent route/outlet |
| `params` | `Object` | `undefined` | An object/merge of **all** optional route params and normal route params |
| `Title` | `Component` | `` | An inline component to allow you to set a title within the Route component |
| `Announcer` | `Component` | `` | An inline component to allow you to specify where the route announcer is rendered. This should be at the very top of your app probably under your `<Route />` in `application.hbs` |
```hbs
<!-- application.hbs -->
<Route
@name={{routeName}}
@title="Page Title"
@titleSeparator=" - "
as |route|>
<route.Announcer />
...
</Route>
<!-- All route templates that change the title -->
<Route
@name={{routeName}}
as |route|>
<h1><route.Title @title="Page Title" /></h1>
{{route.model.dc.Name}}
</Route>
```
Every page/route template has a `routeName` variable exposed specifically to
allow you to use this to set the `@name` of the route.
The `<Title @title=""/>` component should be used to control the title of the page. This component also yields the value of the `@title` attribute allowing you to use it to avoid repeating the title of the page for things like reusing the same value for a `<h1>`. The `Title` component is one of the only components which uses an attribute to specify textual copy, this makes it hard to add further HTML elements to the value of `@title` which would not be supported in the HTML `<title>` element.

View File

@ -0,0 +1,3 @@
{{page-title @title separator=(or @separator ' - ')}}
<PortalTarget @name="route-announcer" />

View File

@ -1,11 +1,12 @@
{{did-insert this.connect}}
{{will-destroy this.disconnect}}
{{#if this.title}}
{{page-title this.title separator=@titleSeparator}}
{{/if}}
{{yield (hash
model=this.model
params=this.params
currentName=this.router.currentRoute.name
refresh=this.refresh
Title=(component "route/title")
Announcer=(component "route/announcer")
)}}

View File

@ -5,13 +5,10 @@ import { tracked } from '@glimmer/tracking';
export default class RouteComponent extends Component {
@service('routlet') routlet;
@service('router') router;
@tracked model;
get title() {
return this.args.title;
}
get params() {
return this.routlet.paramsFor(this.args.name);
}

View File

@ -0,0 +1,15 @@
{{page-title @title separator=@separator}}
{{#if (not-eq @render false)}}
{{@title}}
{{/if}}
<Portal @target="route-announcer">
<div
class="route-title"
...attributes
aria-live="assertive"
aria-atomic="true"
>
{{! Using a handlebars concat here avoid whitespace issues}}
{{concat 'Navigated to ' @title}}
</div>
</Portal>

View File

@ -0,0 +1,6 @@
%route-title {
@extend %visually-hidden;
}
.route-title {
@extend %route-title;
}

View File

@ -7,6 +7,7 @@ class: ember
<TokenSource
@dc={{dc}}
@nspace={{nspace}}
@partition={{partition}}
@type={{or 'oidc' 'secret'}}
@value={{or identifierForProvider secret}}
@onchange={{action 'change'}}
@ -20,6 +21,7 @@ class: ember
| --- | --- | --- | --- |
| `dc` | `String` | | The name of the current datacenter |
| `nspace` | `String` | | The name of the current namespace |
| `partition` | `String` | | The name of the current partition |
| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the jwt data, in this case the autorizationCode and the status |
| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |

View File

@ -1,7 +1,7 @@
<StateChart @src={{chart}} @initial={{if (eq type 'oidc') 'provider' 'secret'}} as |State Guard Action dispatch state|>
<Guard @name="isSecret" @cond={{action 'isSecret'}} />
{{!-- This `or` can be completely removed post 1.10 as 1.10 has optional params with default values --}}
{{#let (concat '/' (or nspace '') '/' dc) as |path|}}
{{#let (concat '/' (or partition '') '/' (or nspace '') '/' dc) as |path|}}
<State @matches="secret">
<DataSource
@src={{concat path '/token/self/' value}}

View File

@ -30,6 +30,7 @@
<TopologyMetrics::Stats
data-test-topology-metrics-downstream-stats
@nspace={{or item.Namespace 'default'}}
@partition={{or item.Partition 'default'}}
@dc={{item.Datacenter}}
@endpoint='downstream-summary-for-service'
@service={{@service.Service.Service}}
@ -48,6 +49,7 @@
{{#if @hasMetricsProvider}}
<TopologyMetrics::Series
@nspace={{or @service.Service.Namespace 'default'}}
@partition={{or service.Service.Partition 'default'}}
@dc={{@dc}}
@service={{@service.Service.Service}}
@protocol={{@topology.Protocol}}
@ -56,6 +58,7 @@
{{#if this.mainNotIngressService}}
<TopologyMetrics::Stats
@nspace={{or @service.Service.Namespace 'default'}}
@partition={{or service.Service.Partition 'default'}}
@dc={{@dc}}
@endpoint='summary-for-service'
@service={{@service.Service.Service}}
@ -104,6 +107,7 @@
{{!-- One of the only places in the app where it's acceptable to default to 'default' namespace --}}
<TopologyMetrics::Stats
@nspace={{or item.Namespace 'default'}}
@partition={{or item.Partition 'default'}}
@dc={{item.Datacenter}}
@endpoint='upstream-summary-for-service'
@service={{@service.Service.Service}}

View File

@ -1,9 +1,9 @@
{{#if (not @noMetricsReason)}}
<DataSource
@src={{uri
'/${nspace}/${dc}/metrics/summary-for-service/${service}/${protocol}'
@src={{uri '/${partition}/${nspace}/${dc}/metrics/summary-for-service/${service}/${protocol}'
(hash
nspace=@nspace
partition=@partition
dc=@dc
service=@service
protocol=@protocol

View File

@ -1,9 +1,10 @@
{{#if (not @noMetricsReason)}}
<DataSource
@src={{uri
'/${nspace}/${dc}/metrics/${endpoint}/${service}/${protocol}'
'/${partition}/${nspace}/${dc}/metrics/${endpoint}/${service}/${protocol}'
(hash
nspace=@nspace
partition=@partition
dc=@dc
endpoint=@endpoint
service=@service

View File

@ -5,14 +5,9 @@ import { get, action } from '@ember/object';
import transitionable from 'consul-ui/utils/routing/transitionable';
export default class ApplicationController extends Controller {
@service('router')
router;
@service('store')
store;
@service('feedback')
feedback;
@service('router') router;
@service('store') store;
@service('feedback') feedback;
// TODO: We currently do this in the controller instead of the router
// as the nspace and dc variables aren't available directly on the Route
@ -29,7 +24,7 @@ export default class ApplicationController extends Controller {
// TODO: Currently we clear cache from the ember-data store
// ideally this would be a static method of the abstract Repository class
// once we move to proper classes for services take another look at this.
this.store.clear();
this.store.invalidate();
//
const params = {};
if (e.data) {

View File

@ -7,25 +7,11 @@ export default Controller.extend({
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);
this.form.handleEvent(event);
} catch (err) {
const target = event.target;
switch (target.name) {

View File

@ -1,25 +0,0 @@
import Controller from '@ember/controller';
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
export default Controller.extend({
dom: service('dom'),
actions: {
change: function(e, value, item) {
const event = this.dom.normalizeEvent(e, value);
// TODO: Switch to using forms like the rest of the app
// setting utils/form/builder for things to be done before we
// can do that. For the moment just do things normally its a simple
// enough form at the moment
const target = event.target;
const blocking = get(this, 'item.client.blocking');
switch (target.name) {
case 'client[blocking]':
set(this, 'item.client.blocking', !blocking);
this.send('update', 'client', this.item.client);
break;
}
},
},
});

View File

@ -0,0 +1,8 @@
import Route from 'consul-ui/routing/route';
export default {
name: 'routing',
initialize(application) {
application.register('route:basic', Route);
},
};

View File

@ -13,7 +13,7 @@ export default {
let repositories = container
.get('container-debug-adapter:main')
.catalogEntriesByType('service')
.filter(item => item.startsWith('repository/'));
.filter(item => item.startsWith('repository/') || item === 'ui-config');
// during testing we get -test files in here, filter those out but only in debug envs
runInDebug(() => (repositories = repositories.filter(item => !item.endsWith('-test'))));

View File

@ -1,15 +0,0 @@
export function initialize(container) {
const env = container.lookup('service:env');
if (env.var('CONSUL_NSPACES_ENABLED')) {
// enable the nspace repo
['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');
}
}
export default {
initialize,
};

View File

@ -165,7 +165,7 @@ export default class FSMWithOptionalLocation {
optionalParams() {
let optional = this.optional || {};
return Object.keys(OPTIONAL).reduce((prev, item) => {
return ['partition', 'nspace'].reduce((prev, item) => {
let value = '';
if (typeof optional[item] !== 'undefined') {
value = optional[item].match;
@ -263,7 +263,7 @@ export default class FSMWithOptionalLocation {
optional = undefined;
}
optional = Object.values(optional || this.optional || {});
optional = optional.map(item => item.value || item, []);
optional = optional.filter(item => Boolean(item)).map(item => item.value || item, []);
temp.splice(...[1, 0].concat(optional));
url = temp.join('/');
}

View File

@ -1,26 +0,0 @@
import Mixin from '@ember/object/mixin';
import { get } from '@ember/object';
/**
* Used for create-type Routes
*
* 'repo' is standardized across the app
* 'item' is standardized across the app
* they could be replaced with `getRepo` and `getItem`
*/
export default Mixin.create({
beforeModel: function() {
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');
// TODO: Look and see if rollbackAttributes is good here
if (get(item, 'isNew')) {
item.destroyRecord();
}
},
});

View File

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

View File

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

View File

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

View File

@ -1,48 +0,0 @@
import Mixin from '@ember/object/mixin';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
import { get } from '@ember/object';
import { inject as service } from '@ember/service';
export default Mixin.create(WithBlockingActions, {
settings: service('settings'),
actions: {
use: function(item) {
return this.repo
.findBySlug({
dc: this.modelFor('dc').dc.Name,
ns: get(item, 'Namespace'),
id: get(item, 'AccessorID'),
})
.then(item => {
return this.settings.persist({
token: {
AccessorID: get(item, 'AccessorID'),
SecretID: get(item, 'SecretID'),
Namespace: get(item, 'Namespace'),
},
});
});
},
logout: function(item) {
return this.settings.delete('token');
},
clone: function(item) {
let cloned;
return this.feedback.execute(() => {
return this.repo
.clone(item)
.then(item => {
cloned = item;
// cloning is similar to delete in that
// if you clone from the listing page, stay on the listing page
// whereas if you clone from another token, take me back to the listing page
// so I can see it
return this.afterDelete(...arguments);
})
.then(function() {
return cloned;
});
}, 'clone');
},
},
});

View File

@ -1,6 +1,6 @@
import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember/service';
import { set } from '@ember/object';
import { set, get } from '@ember/object';
/** With Blocking Actions
* This mixin contains common write actions (Create Update Delete) for routes.
* It could also be an Route to extend but decoration seems to be more sense right now.
@ -18,6 +18,7 @@ import { set } from '@ember/object';
*/
export default Mixin.create({
_feedback: service('feedback'),
settings: service('settings'),
init: function() {
this._super(...arguments);
const feedback = this._feedback;
@ -107,5 +108,45 @@ export default Mixin.create({
}
);
},
use: function(item) {
return this.repo
.findBySlug({
dc: get(item, 'Datacenter'),
ns: get(item, 'Namespace'),
partition: get(item, 'Partition'),
id: get(item, 'AccessorID'),
})
.then(item => {
return this.settings.persist({
token: {
AccessorID: get(item, 'AccessorID'),
SecretID: get(item, 'SecretID'),
Namespace: get(item, 'Namespace'),
Partition: get(item, 'Partition'),
},
});
});
},
logout: function(item) {
return this.settings.delete('token');
},
clone: function(item) {
let cloned;
return this.feedback.execute(() => {
return this.repo
.clone(item)
.then(item => {
cloned = item;
// cloning is similar to delete in that
// if you clone from the listing page, stay on the listing page
// whereas if you clone from another token, take me back to the listing page
// so I can see it
return this.afterDelete(...arguments);
})
.then(function() {
return cloned;
});
}, 'clone');
},
},
});

View File

@ -12,6 +12,7 @@ export default class AuthMethod extends Model {
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('string', { defaultValue: () => '' }) Description;
@attr('string', { defaultValue: () => '' }) DisplayName;
@attr('string', { defaultValue: () => 'local' }) TokenLocality;

View File

@ -9,6 +9,7 @@ export default class BindingRule extends Model {
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('string', { defaultValue: () => '' }) Description;
@attr('string') AuthMethod;
@attr('string', { defaultValue: () => '' }) Selector;

View File

@ -10,5 +10,6 @@ export default class Coordinate extends Model {
@attr() Coord; // {Vec, Error, Adjustment, Height}
@attr('string') Segment;
@attr('string') Datacenter;
@attr('string') Partition;
@attr('number') SyncTime;
}

View File

@ -8,6 +8,7 @@ export default class DiscoveryChain extends Model {
@attr('string') ServiceName;
@attr('string') Datacenter;
// FIXME: Does this need partition?
@attr() Chain; // {}
@attr() meta; // {}
}

View File

@ -12,6 +12,7 @@ export default class Intention extends Model {
@attr('string') Datacenter;
@attr('string') Description;
// FIXME: Will we have Source/DestinationPartition?
@attr('string', { defaultValue: () => 'default' }) SourceNS;
@attr('string', { defaultValue: () => '*' }) SourceName;
@attr('string', { defaultValue: () => 'default' }) DestinationNS;

View File

@ -14,6 +14,7 @@ export default class Kv extends Model {
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('number') LockIndex;
@attr('number') Flags;
@nullValue(undefined) @attr('string') Value;

View File

@ -11,6 +11,7 @@ export default class Node extends Model {
@attr('string') ID;
@attr('string') Datacenter;
@attr('string') Partition;
@attr('string') Address;
@attr('string') Node;
@attr('number') SyncTime;

View File

@ -1,12 +1,17 @@
import Model, { attr } from '@ember-data/model';
export const PRIMARY_KEY = 'Name';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'Name';
export const NSPACE_KEY = 'Namespace';
export default class Nspace extends Model {
@attr('string') uid;
@attr('string') Name;
@attr('string') Datacenter;
@attr('string') Partition;
// Namespace is the same as Name but please don't alias as we want to keep
// mutating the response here instead
@attr('string') Namespace;
@attr('number') SyncTime;
@attr('string', { defaultValue: () => '' }) Description;

View File

@ -9,6 +9,7 @@ export default class OidcProvider extends Model {
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('string') Kind;
@attr('string') AuthURL;
@attr('string') DisplayName;

View File

@ -0,0 +1,21 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'Name';
export const PARTITION_KEY = 'Partition';
export default class PartitionModel extends Model {
@attr('string') uid;
@attr('string') Name;
@attr('string') Description;
@attr('string') Datacenter;
@attr('string') Namespace; // always ""
// Partition is the same as Name but please don't alias as we want to keep
// mutating the response here instead
@attr('string') Partition;
@attr('number') SyncTime;
@attr() meta;
}

View File

@ -11,6 +11,7 @@ export default class Policy extends Model {
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('string', { defaultValue: () => '' }) Name;
@attr('string', { defaultValue: () => '' }) Description;
@attr('string', { defaultValue: () => '' }) Rules;

View File

@ -11,6 +11,7 @@ export default class Proxy extends ServiceInstanceModel {
@attr('string') Datacenter;
@attr('string') Namespace;
// FIXME: Does this need a partition?
@attr('string') ServiceName;
@attr('string') ServiceID;
@attr('string') NodeName;

View File

@ -9,6 +9,7 @@ export default class Role extends Model {
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('string', { defaultValue: () => '' }) Name;
@attr('string', { defaultValue: () => '' }) Description;
@attr({ defaultValue: () => [] }) Policies;

View File

@ -51,6 +51,9 @@ export default class ServiceInstance extends Model {
@alias('Service.Meta') Meta;
@alias('Service.Namespace') Namespace;
// FIXME: Is parition top level or Service.Partition?
@attr('string') Partition;
@filter('Checks.@each.Kind', (item, i, arr) => item.Kind === 'service') ServiceChecks;
@filter('Checks.@each.Kind', (item, i, arr) => item.Kind === 'node') NodeChecks;

View File

@ -28,6 +28,7 @@ export default class Service extends Model {
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('string') Kind;
@attr('number') ChecksPassing;
@attr('number') ChecksCritical;

View File

@ -12,6 +12,7 @@ export default class Session extends Model {
@attr('string') Name;
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('string') Node;
@attr('string') Behavior;
@attr('string') TTL;

View File

@ -11,6 +11,7 @@ export default class Token extends Model {
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('string') IDPName;
@attr('string') SecretID;

View File

@ -10,6 +10,7 @@ export default class Topology extends Model {
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string') Partition;
@attr('string') Protocol;
@attr('boolean') FilteredByACLs;
@attr('boolean') TransparentProxy;

View File

@ -133,7 +133,7 @@ export const routes = {
abilities: ['access acls'],
},
edit: {
_options: { path: '/:id' },
_options: { path: '/:acl' },
},
create: {
_options: {
@ -174,7 +174,7 @@ export const routes = {
tokens: {
_options: {
path: '/tokens',
abilities: ['read tokens'],
abilities: env('CONSUL_ACLS_ENABLED') ? ['read tokens'] : ['access acls'],
},
edit: {
_options: { path: '/:id' },
@ -219,7 +219,7 @@ export const routes = {
_options: { path: '/setting' },
},
notfound: {
_options: { path: '/*path' },
_options: { path: '/*notfound' },
},
};
if (env('CONSUL_NSPACES_ENABLED')) {

View File

@ -1,93 +1,24 @@
import Route from 'consul-ui/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { action } from '@ember/object';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default Route.extend(WithBlockingActions, {
router: service('router'),
nspacesRepo: service('repository/nspace/disabled'),
repo: service('repository/dc'),
settings: service('settings'),
model: function() {
return hash({
router: this.router,
dcs: this.repo.findAll(),
nspaces: this.nspacesRepo.findAll().catch(function() {
return [];
}),
// these properties are added to the controller from route/dc
// as we don't have access to the dc and nspace params in the URL
// until we get to the route/dc route
// permissions also requires the dc param
// dc: null,
// nspace: null
// token: null
// permissions: null
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
actions: {
error: function(e, transition) {
// TODO: Normalize all this better
let error = {
status: e.code || e.statusCode || '',
message: e.message || e.detail || 'Error',
};
if (e.errors && e.errors[0]) {
error = e.errors[0];
error.message = error.message || error.title || error.detail || 'Error';
}
if (error.status === '') {
error.message = 'Error';
}
// Try and get the currently attempted dc, whereever that may be
let model = this.modelFor('dc') || this.modelFor('nspace.dc');
if (!model) {
const path = new URL(location.href).pathname
.substr(this.router.rootURL.length - 1)
.split('/')
.slice(1, 3);
model = {
nspace: { Name: 'default' },
};
if (path[0].startsWith('~')) {
model.nspace = {
Name: path.shift(),
};
}
model.dc = {
Name: path[0],
};
}
const app = this.modelFor('application') || {};
const dcs = app.dcs || [model.dc];
const nspaces = app.nspaces || [model.nspace];
hash({
dc:
error.status.toString().indexOf('5') !== 0
? this.repo.getActive(model.dc.Name, dcs)
: { Name: 'Error' },
dcs: dcs,
nspace: model.nspace,
nspaces: nspaces,
})
.then(model => Promise.all([model, this.repo.clearActive()]))
.then(([model]) => {
// we can't use setupController as we received an error
// so we do it manually instead
this.controllerFor('application').setProperties(model);
this.controllerFor('error').setProperties({ error: error });
})
.catch(e => {
this.controllerFor('error').setProperties({ error: error });
});
return true;
},
},
});
export default class ApplicationRoute extends Route.extend(WithBlockingActions) {
@action
error(e, transition) {
// TODO: Normalize all this better
let error = {
status: e.code || e.statusCode || '',
message: e.message || e.detail || 'Error',
};
if (e.errors && e.errors[0]) {
error = e.errors[0];
error.message = error.message || error.title || error.detail || 'Error';
}
if (error.status === '') {
error.message = 'Error';
}
this.controllerFor('application').setProperties({ error: error });
return true;
}
}

View File

@ -1,27 +1,17 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import { get, action } from '@ember/object';
// TODO: We should potentially move all these nspace related things
// up a level to application.js
export default class DcRoute extends Route {
@service('repository/dc') repo;
@service('repository/permission') permissionsRepo;
@service('repository/nspace/disabled') nspacesRepo;
@service('settings') settingsRepo;
async model(params) {
let [token, nspace, dc] = await Promise.all([
this.settingsRepo.findBySlug('token'),
this.nspacesRepo.getActive(this.optionalParams().nspace),
this.repo.findBySlug(params.dc),
]);
// When disabled nspaces is [], so nspace is undefined
const permissions = await this.permissionsRepo.findAll({
dc: params.dc,
ns: get(nspace || {}, 'Name'),
ns: this.optionalParams().nspace,
});
// the model here is actually required for the entire application
// but we need to wait until we are in this route so we know what the dc
@ -30,49 +20,10 @@ export default class DcRoute extends Route {
// We do this here instead of in setupController to prevent timing issues
// in lower routes
this.controllerFor('application').setProperties({
dc,
nspace,
token,
permissions,
});
return {
dc,
nspace,
token,
permissions,
};
}
// TODO: This will eventually be deprecated please see
// https://deprecations.emberjs.com/v3.x/#toc_deprecate-router-events
@action
willTransition(transition) {
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
const controller = this.controllerFor('application');
Promise.all([
this.nspacesRepo.findAll(),
this.permissionsRepo.findAll({
dc: get(controller, 'dc.Name'),
nspace: get(controller, 'nspace.Name'),
}),
]).then(([nspaces, permissions]) => {
if (typeof controller !== 'undefined') {
controller.setProperties({
nspaces: nspaces,
permissions: permissions,
});
}
});
}
}
}

View File

@ -1,10 +1,6 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp';
export default class IndexRoute extends Route {
@service('repository/auth-method') repo;
queryParams = {
sortBy: 'sort',
source: 'source',
@ -18,21 +14,4 @@ export default class IndexRoute extends Route {
replace: true,
},
};
model(params) {
return hash({
...this.repo.status({
items: this.repo.findAllByDatacenter({
dc: this.modelFor('dc').dc.Name,
ns: this.optionalParams().nspace,
}),
}),
searchProperties: this.queryParams.searchproperty.empty[0],
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -1,36 +0,0 @@
import { inject as service } from '@ember/service';
import SingleRoute from 'consul-ui/routing/single';
import { hash } from 'rsvp';
export default class ShowRoute extends SingleRoute {
@service('repository/auth-method') repo;
@service('repository/binding-rule') bindingRuleRepo;
model(params) {
const dc = this.modelFor('dc').dc;
const nspace = this.optionalParams().nspace;
return super.model(...arguments).then(model => {
return hash({
...model,
...{
item: this.repo.findBySlug({
id: params.id,
dc: dc.Name,
ns: nspace,
}),
bindingRules: this.bindingRuleRepo.findAllByDatacenter({
ns: nspace,
dc: dc.Name,
authmethod: params.id,
}),
},
});
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -1,16 +0,0 @@
import Route from 'consul-ui/routing/route';
export default class AuthMethodRoute extends Route {
model(params) {
const parent = this.routeName
.split('.')
.slice(0, -1)
.join('.');
return this.modelFor(parent);
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -1,16 +0,0 @@
import Route from 'consul-ui/routing/route';
export default class BindingRulesRoute extends Route {
model() {
const parent = this.routeName
.split('.')
.slice(0, -1)
.join('.');
return this.modelFor(parent);
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -1,6 +1,6 @@
import Route from 'consul-ui/routing/route';
import to from 'consul-ui/utils/routing/redirect-to';
export default Route.extend({
redirect: to('auth-method'),
});
export default class AuthMethodShowIndexRoute extends Route {
redirect = to('auth-method');
}

View File

@ -1,16 +0,0 @@
import Route from 'consul-ui/routing/route';
export default class NspaceRulesRoute extends Route {
model() {
const parent = this.routeName
.split('.')
.slice(0, -1)
.join('.');
return this.modelFor(parent);
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -1,6 +1,5 @@
import Route from './edit';
import CreatingRoute from 'consul-ui/mixins/creating-route';
export default class CreateRoute extends Route.extend(CreatingRoute) {
export default class CreateRoute extends Route {
templateName = 'dc/acls/policies/edit';
}

View File

@ -1,48 +1,8 @@
import { inject as service } from '@ember/service';
import SingleRoute from 'consul-ui/routing/single';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import Route from 'consul-ui/routing/route';
import WithPolicyActions from 'consul-ui/mixins/policy/with-actions';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default class EditRoute extends SingleRoute.extend(WithPolicyActions) {
@service('repository/policy')
repo;
@service('repository/token')
tokenRepo;
model(params) {
const dc = this.modelFor('dc').dc.Name;
const nspace = this.optionalParams().nspace;
const tokenRepo = this.tokenRepo;
return super.model(...arguments).then(model => {
return hash({
...model,
...{
routeName: this.routeName,
items: tokenRepo
.findByPolicy({
ns: nspace,
dc: dc,
id: get(model.item, 'ID'),
})
.catch(function(e) {
switch (get(e, 'errors.firstObject.status')) {
case '403':
case '401':
// do nothing the SingleRoute will have caught it already
return;
}
throw e;
}),
},
});
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
export default class EditRoute extends Route.extend(WithBlockingActions) {
@service('repository/policy') repo;
}

View File

@ -1,12 +1,10 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp';
import { inject as service } from '@ember/service';
import WithPolicyActions from 'consul-ui/mixins/policy/with-actions';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default class IndexRoute extends Route.extend(WithPolicyActions) {
export default class IndexRoute extends Route.extend(WithBlockingActions) {
@service('repository/policy') repo;
queryParams = {
sortBy: 'sort',
datacenter: {
@ -22,21 +20,4 @@ export default class IndexRoute extends Route.extend(WithPolicyActions) {
replace: true,
},
};
model(params) {
return hash({
...this.repo.status({
items: this.repo.findAllByDatacenter({
ns: this.optionalParams().nspace,
dc: this.modelFor('dc').dc.Name,
}),
}),
searchProperties: this.queryParams.searchproperty.empty[0],
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -1,6 +1,5 @@
import Route from './edit';
import CreatingRoute from 'consul-ui/mixins/creating-route';
export default class CreateRoute extends Route.extend(CreatingRoute) {
export default class CreateRoute extends Route {
templateName = 'dc/acls/roles/edit';
}

View File

@ -1,47 +1,8 @@
import { inject as service } from '@ember/service';
import SingleRoute from 'consul-ui/routing/single';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import Route from 'consul-ui/routing/route';
import WithRoleActions from 'consul-ui/mixins/role/with-actions';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default class EditRoute extends SingleRoute.extend(WithRoleActions) {
@service('repository/role')
repo;
@service('repository/token')
tokenRepo;
model(params) {
const dc = this.modelFor('dc').dc.Name;
const nspace = this.optionalParams().nspace;
const tokenRepo = this.tokenRepo;
return super.model(...arguments).then(model => {
return hash({
...model,
...{
items: tokenRepo
.findByRole({
ns: nspace,
dc: dc,
id: get(model.item, 'ID'),
})
.catch(function(e) {
switch (get(e, 'errors.firstObject.status')) {
case '403':
case '401':
// do nothing the SingleRoute will have caught it already
return;
}
throw e;
}),
},
});
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
export default class EditRoute extends Route.extend(WithBlockingActions) {
@service('repository/role') repo;
}

View File

@ -1,12 +1,10 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp';
import { inject as service } from '@ember/service';
import WithRoleActions from 'consul-ui/mixins/role/with-actions';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default class IndexRoute extends Route.extend(WithRoleActions) {
export default class IndexRoute extends Route.extend(WithBlockingActions) {
@service('repository/role') repo;
queryParams = {
sortBy: 'sort',
searchproperty: {
@ -18,21 +16,4 @@ export default class IndexRoute extends Route.extend(WithRoleActions) {
replace: true,
},
};
model(params) {
return hash({
...this.repo.status({
items: this.repo.findAllByDatacenter({
ns: this.optionalParams().nspace,
dc: this.modelFor('dc').dc.Name,
}),
}),
searchProperties: this.queryParams.searchproperty.empty[0],
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -1,6 +1,5 @@
import Route from './edit';
import CreatingRoute from 'consul-ui/mixins/creating-route';
export default class CreateRoute extends Route.extend(CreatingRoute) {
export default class CreateRoute extends Route {
templateName = 'dc/acls/tokens/edit';
}

View File

@ -1,30 +1,15 @@
import { inject as service } from '@ember/service';
import SingleRoute from 'consul-ui/routing/single';
import { hash } from 'rsvp';
import Route from 'consul-ui/routing/route';
import WithTokenActions from 'consul-ui/mixins/token/with-actions';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default class EditRoute extends SingleRoute.extend(WithTokenActions) {
@service('repository/token')
repo;
export default class EditRoute extends Route.extend(WithBlockingActions) {
@service('repository/token') repo;
@service('settings') settings;
@service('settings')
settings;
model(params, transition) {
return super.model(...arguments).then(model => {
return hash({
...model,
...{
routeName: this.routeName,
token: this.settings.findBySlug('token'),
},
});
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
async model(params, transition) {
return {
token: await this.settings.findBySlug('token'),
};
}
}

View File

@ -1,13 +1,10 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import WithTokenActions from 'consul-ui/mixins/token/with-actions';
import { inject as service } from '@ember/service';
export default class IndexRoute extends Route.extend(WithTokenActions) {
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default class IndexRoute extends Route.extend(WithBlockingActions) {
@service('repository/token') repo;
@service('settings') settings;
queryParams = {
sortBy: 'sort',
kind: 'kind',
@ -20,35 +17,4 @@ export default class IndexRoute extends Route.extend(WithTokenActions) {
replace: true,
},
};
async beforeModel(transition) {
const token = await this.settings.findBySlug('token');
// If you have a token set with AccessorID set to null (legacy mode)
// then rewrite to the old acls
if (token && get(token, 'AccessorID') === null) {
// If you return here, you get a TransitionAborted error in the tests only
// everything works fine either way checking things manually
this.replaceWith('dc.acls');
}
}
model(params) {
const nspace = this.optionalParams().nspace;
return hash({
...this.repo.status({
items: this.repo.findAllByDatacenter({
ns: nspace,
dc: this.modelFor('dc').dc.Name,
}),
}),
nspace: nspace,
token: this.settings.findBySlug('token'),
searchProperties: this.queryParams.searchproperty.empty[0],
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -1,7 +1,6 @@
import Route from 'consul-ui/routing/route';
import to from 'consul-ui/utils/routing/redirect-to';
export default class IndexRoute extends Route {
beforeModel() {
this.transitionTo('dc.services');
}
redirect = to('services');
}

View File

@ -1,4 +1,4 @@
import Route from './edit';
import Route from 'consul-ui/routing/route';
export default class CreateRoute extends Route {
templateName = 'dc/intentions/edit';

View File

@ -1,38 +0,0 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
export default class EditRoute extends Route {
@service('repository/intention') repo;
@service('env') env;
async model(params, transition) {
const dc = this.modelFor('dc').dc.Name;
const nspace = this.optionalParams().nspace;
let item;
if (typeof params.intention_id !== 'undefined') {
item = await this.repo.findBySlug({
ns: nspace,
dc: dc,
id: params.intention_id,
});
} else {
const defaultNspace = this.env.var('CONSUL_NSPACES_ENABLED') ? '*' : 'default';
item = await this.repo.create({
SourceNS: nspace || defaultNspace,
DestinationNS: nspace || defaultNspace,
Datacenter: dc,
});
}
return {
dc,
nspace,
item,
};
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

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