ui: Move nspace CRUD to use the same approach as partitions (#11633)

This sounds a bit 'backwards' as the end goal here is to add an improved UX to partitions, not namespaces. The reason for doing it this way is that Namespaces already has a type of 'improved UX' CRUD in that it has one to many relationship in the form when saving your namespaces (the end goal for partitions). In moving Namespaces to use the same approach as partitions we:

- Ensure the new approach works with one-to-many forms.
- Test the new approach without writing a single test (we already have a bunch of tests for namespaces which are now testing the approach used by both namespaces and partitions)

Additionally:

- Fixes issue with missing default nspace in the nspace selector
- In doing when checking to see that things where consistent between the two, I found a few little minor problems with the Admin Partition CRUD so fixed those up here also.
- Removed the old style Nspace notifications
This commit is contained in:
John Cowen 2021-12-01 11:04:02 +00:00 committed by GitHub
parent cff9356f97
commit 91383269b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 601 additions and 491 deletions

View File

@ -22,7 +22,7 @@ references:
test-results: &TEST_RESULTS_DIR /tmp/test-results
cache:
yarn: &YARN_CACHE_KEY consul-ui-v5-{{ checksum "ui/yarn.lock" }}
yarn: &YARN_CACHE_KEY consul-ui-v6-{{ checksum "ui/yarn.lock" }}
rubygem: &RUBYGEM_CACHE_KEY static-site-gems-v1-{{ checksum "Gemfile.lock" }}
environment: &ENVIRONMENT

View File

@ -11,7 +11,7 @@
"scripts": {
"doc:toc": "doctoc README.md",
"compliance": "npm-run-all compliance:*",
"compliance:licenses": "license-checker --summary --onlyAllow 'Python-2.0;Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Unicode-TOU;Unlicense;WTFPL' --excludePackages 'consul-ui@2.2.0;consul-acls@0.1.0;consul-partitions@0.1.0'"
"compliance:licenses": "license-checker --summary --onlyAllow 'Python-2.0;Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Unicode-TOU;Unlicense;WTFPL' --excludePackages 'consul-ui@2.2.0;consul-acls@0.1.0;consul-partitions@0.1.0;consul-nspaces@0.1.0'"
},
"devDependencies": {

View File

@ -0,0 +1,24 @@
# Consul::Nspace::Form
```hbs preview-template
<DataLoader @src={{
uri '/${partition}/${nspace}/${dc}/namespace/${id}'
(hash
partition='partition'
nspace='nspace'
dc='dc'
id=''
)
}}
as |loader|>
<BlockSlot @name="loaded">
<Consul::Nspace::Form
@item={{loader.data}}
@dc={{'dc-1'}}
@nspace={{'nspace'}}
@partition={{'partition'}}
@onsubmit={{noop}}
/>
</BlockSlot>
</DataLoader>
```

View File

@ -0,0 +1,169 @@
<div
class="consul-nspace-form"
...attributes
>
<DataWriter
@sink={{uri
'/${partition}/${nspace}/${dc}/nspace'
(hash
partition=''
nspace=''
dc=@item.Datacenter
)
}}
@type={{'nspace'}}
@label={{"Namespace"}}
@ondelete={{fn (if @ondelete @ondelete @onsubmit) @item}}
@onchange={{fn (optional @onsubmit) @item}}
as |writer|>
<BlockSlot @name="removed" as |after|>
<Consul::Nspace::Notifications
{{notification
after=(action after)
}}
@type="remove"
/>
</BlockSlot>
<BlockSlot @name="content">
{{#let
(not (can "write nspaces"))
@item
(hash
help='Must be a valid DNS hostname. Must contain 1-64 characters (numbers, letters, and hyphens), and must begin with a letter. Once created, this cannot be changed.'
Name=(array
(hash
test='^[a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?$'
error='Name must be a valid DNS hostname.'
)
)
)
(hash
Description=(array)
)
as |readOnly item Name Description|}}
<form
{{on 'submit' (fn writer.persist item)}}
{{disabled readOnly}}
>
<StateChart
@src={{state-chart 'validate'}}
as |State Guard ChartAction dispatch state|>
<fieldset>
{{#if (is "new nspace" item=item)}}
<TextInput
@name="Name"
@placeholder="Name"
@item={{item}}
@validations={{Name}}
@chart={{hash
state=state
dispatch=dispatch
}}
/>
{{/if}}
<TextInput
@expanded={{true}}
@name="Description"
@label="Description (Optional)"
@item={{item}}
@validations={{Description}}
@chart={{hash
state=state
dispatch=dispatch
}}
/>
</fieldset>
{{#if (can 'use acls')}}
<fieldset id="roles">
<h2>Roles</h2>
<p>
{{#if (can "write nspace" item=item)}}
By adding roles to this namespaces, you will apply them to all tokens created within this namespace.
{{else}}
The following roles are applied to all tokens created within this namespace.
{{/if}}
</p>
<RoleSelector
@dc={{@dc}}
@nspace="default"
@partition={{@partition}}
@disabled={{readOnly}}
@items={{item.ACLs.RoleDefaults}}
/>
</fieldset>
<fieldset id="policies">
<h2>Policies</h2>
<p>
{{#if (not readOnly)}}
By adding policies to this namespace, you will apply them to all tokens created within this namespace.
{{else}}
The following policies are applied to all tokens created within this namespace.
{{/if}}
</p>
<PolicySelector
@dc={{@dc}}
@nspace="default"
@partition={{@partition}}
@disabled={{readOnly}}
@allowIdentity={{false}}
@items={{item.ACLs.PolicyDefaults}}
/>
</fieldset>
{{/if}}
<div>
{{#if (and (is "new nspace" item=item) (can "create nspaces"))}}
<Action
@type="submit"
{{disabled (or (is "pristine nspace" item=item) (state-matches state "error"))}}
>
Save
</Action>
{{else if (can "write nspace" item=item)}}
<Action @type="submit">Save</Action>
{{/if}}
<Action
@type="reset"
{{on 'click' (if @oncancel (fn @oncancel item) (fn @onsubmit item))}}
>
Cancel
</Action>
{{#if (and (not (is "new nspace" item=item)) (can "delete nspace" item=item))}}
<ConfirmationDialog @message="Are you sure you want to delete this Namespace?">
<BlockSlot @name="action" as |confirm|>
<Action
data-test-delete
class="type-delete"
{{on 'click' (fn confirm (fn writer.delete item))}}
>
Delete
</Action>
</BlockSlot>
<BlockSlot @name="dialog" as |execute cancel message|>
<DeleteConfirmation
@message={{message}}
@execute={{execute}}
@cancel={{cancel}}
/>
</BlockSlot>
</ConfirmationDialog>
{{/if}}
</div>
</StateChart>
</form>
{{/let}}
</BlockSlot>
</DataWriter>
</div>

View File

@ -1,29 +1,30 @@
---
class: ember
---
## Consul::Nspace::List
# Consul::Nspace::List
```hbs
<DataSource @src="/partition/default/dc-1/namespaces" as |source|>
A presentational component for rendering Consul Namespaces
Please note:
- For the moment, make sure you have enabled nspaces using developer debug
cookies.
```hbs preview-template
<DataSource @src={{uri '/partition/default/dc-1/namespaces'}} as |source|>
<Consul::Nspace::List
@items={{source.data}}
@ondelete={{action (noop)}}
@ondelete={{noop}}
/>
</DataSource>
```
A presentational component for rendering Consul Namespaces
### Arguments
## Arguments
| Argument/Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `array` | | An array of Namespaces |
| `ondelete` | `function` | | An action to execute when the `Delete` action is clicked |
### See
## See
- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)
---

View File

@ -0,0 +1,19 @@
# Consul::Nspace::Notifications
A Notification component specifically for namespaces. This is only a component as we currently use this in two places and if we need to add more types we can do so in one place.
We currently only have one 'remove' type due to the fact that namespaces can't use the default 'delete' notification as they get 'marked for deletion' instead.
```hbs preview-template
<Consul::Nspace::Notifications
@type={{'remove'}}
/>
```
## See
- [Template Source Code](./index.hbs)
---

View File

@ -0,0 +1,16 @@
{{#if (eq @type 'remove')}}
<Notice
class="notification-delete"
@type="success"
...attributes
as |notice|>
<notice.Header>
<strong>Success!</strong>
</notice.Header>
<notice.Body>
<p>
Your Namespace has been marked for deletion.
</p>
</notice.Body>
</Notice>
{{/if}}

View File

@ -0,0 +1,30 @@
# Consul::Nspace::SearchBar
Searchbar tailored for searching namespaces. Follows our more generic
'*::SearchBar' component interface.
```hbs preview-template
<Consul::Nspace::SearchBar
@search={{this.search}}
@onsearch={{fn (mut this.search) value="target.value"}}
@sort={{hash
value='Name:asc'
change=(noop)
}}
@filter={{hash
searchproperty=(hash
value=(array)
change=(noop)
default=(array)
)
}}
/>
```
## See
- [Template Source Code](./index.hbs)
---

View File

@ -0,0 +1,40 @@
# Consul::Nspace::Selector
A conditional, autoloading, menu component specifically for making it easy to select namespaces.
Please note:
- Currently at least, you must add this inside of a `<ul>` element, as the `<li>` elements output by this component are intended to be mixed with other sibling `<li>`s from other components or template code.
- For the moment, make sure you have enabled nspaces using developer debug
cookies.
```hbs preview-template
<ul>
<Consul::Nspace::Selector
@dc={{hash
Name='dc-1'
}}
@nspace='default'
@partition='default'
@nspaces={{or this.nspaces (array)}}
@onchange={{action (mut this.nspaces) value="data"}}
/>
</ul>
```
## Arguments
| Argument/Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `dc` | `object` | | The current datacenter |
| `nspace` | `string` | | The name of the current namespace |
| `partition` | `string` | | The name of the current partition |
| `nspaces` | `array` | | A list of nspace models/objects to use for the selector |
| `onchange` | `function` | | An event handler, for when nspaces are loaded. You probably want to update `@nspaces` using this. |
## See
- [Template Source Code](./index.hbs)
---

View File

@ -0,0 +1,74 @@
{{#if (can "use nspaces")}}
{{#if (can "choose nspaces")}}
{{#let
(or @nspace 'default')
as |nspace|}}
<li
class="nspaces"
data-test-nspace-menu
>
<PopoverMenu
aria-label="Namespace"
@position="left"
as |components api|>
<BlockSlot @name="trigger">
{{nspace}}
</BlockSlot>
<BlockSlot @name="menu">
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}}
{{#if (gt @nspaces.length 0)}}
<DataSource
@src={{uri
'/${partition}/*/${dc}/namespaces'
(hash
partition=@partition
dc=@dc.Name
)
}}
@onchange={{fn (optional @onchange)}}
@loading="lazy"
/>
{{else}}
<DataSource
@src={{uri
'/${partition}/*/${dc}/namespaces'
(hash
partition=@partition
dc=@dc.Name
)
}}
@onchange={{fn (optional @onchange)}}
/>
{{/if}}
{{#each (reject-by 'DeletedAt' @nspaces) as |item|}}
<MenuItem
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}}
</BlockSlot>
</MenuItem>
{{/each}}
{{#if (can 'manage nspaces')}}
<MenuSeparator />
<MenuItem
data-test-main-nav-nspaces
@href={{href-to 'dc.nspaces' @dc.Name}}
>
<BlockSlot @name="label">
Manage Namespaces
</BlockSlot>
</MenuItem>
{{/if}}
{{/let}}
</BlockSlot>
</PopoverMenu>
</li>
{{/let}}
{{/if}}
{{/if}}

View File

@ -30,13 +30,6 @@ as |route|>
loader.data.isNew
as |dc partition nspace item create|}}
<AppView>
<BlockSlot @name="notification" as |status type item error|>
<Consul::Nspace::Notifications
@type={{type}}
@status={{status}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to 'dc.nspaces'}}>All Namespaces</a></li>
@ -50,7 +43,13 @@ as |dc partition nspace item create|}}
<BlockSlot @name="actions">
</BlockSlot>
<BlockSlot @name="content">
{{ partial 'dc/nspaces/form'}}
<Consul::Nspace::Form
@item={{item}}
@dc={{route.params.dc}}
@nspace={{route.params.nspace}}
@partition={{route.params.partition}}
@onsubmit={{transition-to 'dc.nspaces.index'}}
/>
</BlockSlot>
</AppView>
{{/let}}

View File

@ -42,13 +42,6 @@ as |route|>
as |sort filters items|}}
<AppView>
<BlockSlot @name="notification" as |status type item error|>
<Consul::Nspace::Notifications
@type={{type}}
@status={{status}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="header">
<h1>
<route.Title @title="Namespaces" />
@ -71,6 +64,27 @@ as |route|>
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<DataWriter
@sink={{uri '/${partition}/${dc}/${nspace}/nspace/'
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
)
}}
@type="nspace"
@label="Namespace"
@ondelete={{refresh-route}}
as |writer|>
<BlockSlot @name="removed" as |after|>
<Consul::Nspace::Notifications
{{notification
after=(action after)
}}
@type="remove"
/>
</BlockSlot>
<BlockSlot @name="content">
<DataCollection
@type="nspace"
@ -82,7 +96,7 @@ as |route|>
<collection.Collection>
<Consul::Nspace::List
@items={{collection.items}}
@ondelete={{route-action 'delete'}}
@ondelete={{writer.delete}}
/>
</collection.Collection>
<collection.Empty>
@ -119,6 +133,8 @@ as |route|>
</collection.Empty>
</DataCollection>
</BlockSlot>
</DataWriter>
</BlockSlot>
</AppView>
{{/let}}
</BlockSlot>

View File

@ -0,0 +1,5 @@
{
"name": "consul-nspaces",
"version": "0.1.0",
"private": true
}

View File

@ -0,0 +1,38 @@
(routes => routes({
dc: {
nspaces: {
_options: {
path: '/namespaces',
queryParams: {
sortBy: 'sort',
searchproperty: {
as: 'searchproperty',
empty: [['Name', 'Description', 'Role', 'Policy']],
},
search: {
as: 'filter',
replace: true,
},
},
abilities: ['read nspaces'],
},
edit: {
_options: { path: '/:name' },
},
create: {
_options: {
template: 'dc/nspaces/edit',
path: '/create',
abilities: ['create nspaces'],
},
},
},
},
}))(
(json, data = document.currentScript.dataset) => {
const appNameJS = data.appName.split('-')
.map((item, i) => i ? `${item.substr(0, 1).toUpperCase()}${item.substr(1)}` : item)
.join('');
data[`${appNameJS}Routes`] = JSON.stringify(json);
}
);

View File

@ -12,15 +12,25 @@
)
}}
@type={{'partition'}}
@label={{label}}
@label={{'Admin Partition'}}
@ondelete={{fn (if @ondelete @ondelete @onsubmit) @item}}
@onchange={{fn (optional @onsubmit) @item}}
as |writer|>
<BlockSlot @name="removed" as |after|>
<Consul::Partition::Notifications
{{notification
after=(action after)
}}
@type="remove"
/>
</BlockSlot>
<BlockSlot @name="content">
{{#let
(not (can "write partition"))
@item
(hash
@ -37,19 +47,18 @@
Description=(array)
)
as |item Name Description|}}
as |readOnly item Name Description|}}
<form
{{on 'submit' (fn writer.persist item)}}
{{disabled (not (can "write partition" item=item))}}
{{disabled readOnly}}
>
<StateChart
@src={{state-chart 'validate'}}
as |State Guard Action dispatch state|>
as |State Guard ChartAction dispatch state|>
<fieldset>
{{#if (is "new partition" item=item)}}
{{#if (is "new partition" item=item)}}
<TextInput
@name="Name"
@placeholder="Name"
@ -76,34 +85,33 @@ as |State Guard Action dispatch state|>
<div>
{{#if (and (is "new partition" item=item) (can "create partitions")) }}
<button
type="submit"
<Action
@type="submit"
{{disabled (or (is "pristine partition" item=item) (state-matches state "error"))}}
>
Save
</button>
{{else if (can "write partition" item=item)}}
<button type="submit">Save</button>
</Action>
{{else if (not readOnly)}}
<Action @type="submit">Save</Action>
{{/if}}
<button
type="reset"
<Action
@type="reset"
{{on 'click' (if @oncancel (fn @oncancel item) (fn @onsubmit item))}}
>
Cancel
</button>
</Action>
{{#if (and (not (is "new partition" item=item)) (can "delete partition" item=item))}}
<ConfirmationDialog @message="Are you sure you want to delete this Partition?">
<BlockSlot @name="action" as |confirm|>
<button
<Action
data-test-delete
type="button"
class="type-delete"
{{on 'click' (fn confirm (fn writer.delete item))}}
>
Delete
</button>
</Action>
</BlockSlot>
<BlockSlot @name="dialog" as |execute cancel message|>
<DeleteConfirmation

View File

@ -17,16 +17,15 @@ Please note:
```
### Arguments
## Arguments
| Argument/Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `array` | | An array of Partitions |
| `ondelete` | `function` | | An action to execute when the `Delete` action is clicked |
### See
## See
- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)
---

View File

@ -1,40 +1,13 @@
# Consul::Partition::Notifications
A Notification component specifically for Partitions (at some point will be replaced with just using `ember-intl`/`t`.
A Notification component specifically for Partitions. This is only a component as we currently use this in two places and if we need to add more types we can do so in one place.
We currently one have one 'remove' type due to the fact that Admin Partions can't use the default 'delete' notification as they get 'marked for deletion' instead.
```hbs preview-template
<figure>
<figcaption>Provide a widget to change the <code>@type</code></figcaption>
<select
{{on 'change' (action (mut this.type) value="target.value")}}
>
<option>create</option>
<option>update</option>
<option>delete</option>
</select>
</figure>
<figure>
<figcaption>Provide a widget to change the <code>@status</code></figcaption>
<select
{{on 'change' (action (mut this.success) value="target.value")}}
>
<option>success</option>
<option>error</option>
</select>
</figure>
<figure>
<figcaption>Show the notification text</figcaption>
<p>
<Consul::Partition::Notifications
@type={{or this.type 'create'}}
@status={{or this.success 'success'}}
@error={{undefined}}
@type={{'remove'}}
/>
</p>
</figure>
```

View File

@ -1,24 +1,16 @@
{{#if (eq @type 'create')}}
{{#if (eq @status 'success') }}
Your partition has been added.
{{else}}
There was an error adding your partition.
{{/if}}
{{else if (eq @type 'update') }}
{{#if (eq @status 'success') }}
Your partition has been saved.
{{else}}
There was an error saving your partition.
{{/if}}
{{ else if (eq @type 'delete')}}
{{#if (eq @status 'success') }}
Your partition has been marked for deletion.
{{else}}
There was an error deleting your partition.
{{/if}}
{{#if (eq @type 'remove')}}
<Notice
class="notification-delete"
@type="success"
...attributes
as |notice|>
<notice.Header>
<strong>Success!</strong>
</notice.Header>
<notice.Body>
<p>
Your Admin Partition has been marked for deletion.
</p>
</notice.Body>
</Notice>
{{/if}}
{{#let @error.errors.firstObject as |error|}}
{{#if error.detail }}
<br />{{concat '(' (if error.status (concat error.status ': ')) error.detail ')'}}
{{/if}}
{{/let}}

View File

@ -27,28 +27,24 @@ as |route|>
route.params.nspace
loader.data
loader.data.isNew
as |dc partition nspace item create|}}
as |dc partition nspace item|}}
<AppView>
<BlockSlot @name="notification" as |status type item error|>
<Consul::Partition::Notifications
@type={{type}}
@status={{status}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to 'dc.partitions'}}>All Partitions</a></li>
<li><a data-test-back href={{href-to 'dc.partitions'}}>All Admin Partitions</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">
<h1>
<route.Title @title={{if create "New Partition" (concat "Edit " item.Name)}} />
<route.Title
@title={{if
(is "new partition" item=item)
"New Admin Partition"
(concat "Edit " item.Name)
}}
/>
</h1>
</BlockSlot>
<BlockSlot @name="actions">
</BlockSlot>
<BlockSlot @name="content">
<Consul::Partition::Form
@ -59,7 +55,6 @@ as |dc partition nspace item create|}}
@onsubmit={{transition-to 'dc.partitions.index'}}
/>
</BlockSlot>
</AppView>
{{/let}}

View File

@ -42,13 +42,6 @@ as |route|>
as |sort filters items|}}
<AppView>
<BlockSlot @name="notification" as |status type item error|>
<Consul::Partition::Notifications
@type={{type}}
@status={{status}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="header">
<h1>
<route.Title @title="Admin Partitions" />
@ -79,8 +72,17 @@ as |route|>
)
}}
@type="partition"
@label="Admin Partition"
@ondelete={{refresh-route}}
as |writer|>
<BlockSlot @name="removed" as |after|>
<Consul::Partition::Notifications
{{notification
after=(action after)
}}
@type="remove"
/>
</BlockSlot>
<BlockSlot @name="content">
<DataCollection
@type="nspace"

View File

@ -91,6 +91,12 @@ module.exports = {
pattern: '**/README.mdx',
urlSchema: 'auto',
urlPrefix: 'docs/consul-partitions',
},
{
root: `${path.dirname(require.resolve('consul-nspaces/package.json'))}/app/components`,
pattern: '**/README.mdx',
urlSchema: 'auto',
urlPrefix: 'docs/consul-nspaces',
}
].concat(user.sources),
labels: {

View File

@ -1,24 +0,0 @@
{{#if (eq @type 'create')}}
{{#if (eq @status 'success') }}
Your namespace has been added.
{{else}}
There was an error adding your namespace.
{{/if}}
{{else if (eq @type 'update') }}
{{#if (eq @status 'success') }}
Your namespace has been saved.
{{else}}
There was an error saving your namespace.
{{/if}}
{{ else if (eq @type 'delete')}}
{{#if (eq @status 'success') }}
Your namespace has been marked for deletion.
{{else}}
There was an error deleting your namespace.
{{/if}}
{{/if}}
{{#let @error.errors.firstObject as |error|}}
{{#if error.detail }}
<br />{{concat '(' (if error.status (concat error.status ': ')) error.detail ')'}}
{{/if}}
{{/let}}

View File

@ -60,13 +60,6 @@
@item={{flash.item}}
@error={{flash.error}}
/>
{{else if (eq flash.model 'nspace')}}
<Consul::Nspace::Notifications
@type={{type}}
@status={{status}}
@item={{flash.item}}
@error={{flash.error}}
/>
{{/if}}
{{/if}}
</p>
@ -140,60 +133,13 @@
@partitions={{this.partitions}}
@onchange={{action (mut this.partitions) value="data"}}
/>
{{#if (can "choose nspaces")}}
<li
class="nspaces"
data-test-nspace-menu
>
<PopoverMenu
aria-label="Namespace"
@position="left"
as |components api|>
<BlockSlot @name="trigger">
{{@nspace}}
</BlockSlot>
<BlockSlot @name="menu">
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}}
<DataSource
@src={{uri
'/${partition}/*/${dc}/namespaces'
(hash
partition=@partition
dc=@dc.Name
)
}}
<Consul::Nspace::Selector
@dc={{@dc}}
@partition={{@partition}}
@nspace={{@nspace}}
@nspaces={{this.nspaces}}
@onchange={{action (mut this.nspaces) value="data"}}
@loading="lazy"
/>
{{#each (reject-by 'DeletedAt' this.nspaces) as |item|}}
<MenuItem
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}}
</BlockSlot>
</MenuItem>
{{/each}}
{{#if (can 'manage nspaces')}}
<MenuSeparator />
<MenuItem
data-test-main-nav-nspaces
@href={{href-to 'dc.nspaces' @dc.Name}}
>
<BlockSlot @name="label">
Manage Namespaces
</BlockSlot>
</MenuItem>
{{/if}}
{{/let}}
</BlockSlot>
</PopoverMenu>
</li>
{{/if}}
{{#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>

View File

@ -1,2 +0,0 @@
import Controller from './edit';
export default class CreateController extends Controller {}

View File

@ -1,24 +0,0 @@
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');
},
actions: {
change: function(e, value, item) {
const event = this.dom.normalizeEvent(e, value);
try {
this.form.handleEvent(event);
} catch (err) {
const target = event.target;
switch (target.name) {
default:
throw err;
}
}
},
},
});

View File

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

View File

@ -1,7 +0,0 @@
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);
}

View File

@ -231,23 +231,6 @@ export const routes = merge.all(
)
);
if (env('CONSUL_NSPACES_ENABLED')) {
routes.dc.nspaces = {
_options: {
path: '/namespaces',
abilities: ['read nspaces'],
},
edit: {
_options: { path: '/:name' },
},
create: {
_options: {
path: '/create',
abilities: ['create nspaces'],
},
},
};
}
runInDebug(() => {
// check to see if we are running docfy and if so add its routes to our
// route config

View File

@ -1,5 +0,0 @@
import Route from './edit';
export default class CreateRoute extends Route {
templateName = 'dc/nspaces/edit';
}

View File

@ -1,8 +0,0 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default class EditRoute extends Route.extend(WithBlockingActions) {
@service('repository/nspace') repo;
}

View File

@ -1,19 +0,0 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default class IndexRoute extends Route.extend(WithBlockingActions) {
@service('repository/nspace') repo;
queryParams = {
sortBy: 'sort',
searchproperty: {
as: 'searchproperty',
empty: [['Name', 'Description', 'Role', 'Policy']],
},
search: {
as: 'filter',
replace: true,
},
};
}

View File

@ -22,11 +22,14 @@ export default class BaseRoute extends Route {
.filter(item => item !== 'index')
.join('.');
const template = get(routes, `${routeName}._options.template`);
if(template) {
if (template) {
this.templateName = template;
}
const queryParams = get(routes, `${routeName}._options.queryParams`);
if(queryParams && (this.routeName === 'dc.partitions.index' || this.routeName === 'oauth-provider-debug')) {
if (
queryParams &&
['dc.partitions.index', 'dc.nspaces.index', 'oauth-provider-debug'].includes(this.routeName)
) {
this.queryParams = queryParams;
}
}

View File

@ -13,7 +13,7 @@ export default class NspaceSerializer extends Serializer {
cb(
headers,
body.map(function(item) {
item.Namespace = item.Name;
item.Namespace = '*';
item.Datacenter = query.dc;
if (get(item, 'ACLs.PolicyDefaults')) {
item.ACLs.PolicyDefaults = item.ACLs.PolicyDefaults.map(function(item) {
@ -45,7 +45,7 @@ export default class NspaceSerializer extends Serializer {
cb =>
respond((headers, body) => {
body.Datacenter = serialized.dc;
body.Namespace = body.Name;
body.Namespace = '*';
return cb(headers, body);
}),
serialized,
@ -58,7 +58,7 @@ export default class NspaceSerializer extends Serializer {
cb =>
respond((headers, body) => {
body.Datacenter = serialized.dc;
body.Namespace = body.Name;
body.Namespace = '*';
return cb(headers, body);
}),
serialized,

View File

@ -12,8 +12,8 @@ export default class PartitionSerializer extends Serializer {
return cb(
headers,
body.Partitions.map(item => {
item.Partition = item.Name;
item.Namespace = '';
item.Partition = '*';
item.Namespace = '*';
return item;
})
);

View File

@ -5,6 +5,7 @@ export default class HttpService extends Service {
@service('settings') settings;
@service('repository/intention') intention;
@service('repository/kv') kv;
@service('repository/nspace') nspace;
@service('repository/partition') partition;
@service('repository/session') session;

View File

@ -6,8 +6,6 @@ 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';
import partition from 'consul-ui/forms/partition';
const builder = builderFactory();
@ -17,8 +15,6 @@ const forms = {
policy: policy,
role: role,
intention: intention,
nspace: nspace,
partition: partition,
};
export default class FormService extends Service {

View File

@ -4,6 +4,8 @@ import RepositoryService, { softDelete } from 'consul-ui/services/repository';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/nspace';
import dataSource from 'consul-ui/decorators/data-source';
import { defaultChangeset as changeset } from 'consul-ui/utils/form/builder';
const findActiveNspace = function(nspaces, nspace) {
let found = nspaces.find(function(item) {
return item.Name === nspace.Name;
@ -24,7 +26,7 @@ const findActiveNspace = function(nspaces, nspace) {
return found;
};
const modelName = 'nspace';
export default class NspaceEnabledService extends RepositoryService {
export default class NspaceService extends RepositoryService {
@service('router') router;
@service('container') container;
@service('env') env;
@ -68,10 +70,7 @@ export default class NspaceEnabledService extends RepositoryService {
} else {
item = await super.findBySlug(...arguments);
}
return this.form
.form(this.getModelName())
.setData(item)
.getData();
return changeset(item);
}
remove(item) {

View File

@ -4,6 +4,8 @@ import RepositoryService, { softDelete } from 'consul-ui/services/repository';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/partition';
import dataSource from 'consul-ui/decorators/data-source';
import { defaultChangeset as changeset } from 'consul-ui/utils/form/builder';
const findActive = function(items, item) {
let found = items.find(function(i) {
return i.Name === item.Name;
@ -61,10 +63,7 @@ export default class PartitionRepository extends RepositoryService {
} else {
item = await super.findBySlug(...arguments);
}
return this.form
.form(this.getModelName())
.setData(item)
.getData();
return changeset(item);
}
remove(item) {

View File

@ -1,80 +0,0 @@
<form>
<fieldset
disabled={{if (not (can "write nspace" item=item)) "disabled"}}
>
{{#if create }}
<label class="type-text{{if item.error.Name ' has-error'}}">
<span>Name</span>
<input autofocus="autofocus" type="text" value={{item.Name}} name="Name" oninput={{action 'change'}} placeholder="Name" />
<em>
Must be a valid DNS hostname. Must contain 1-64 characters (numbers, letters, and hyphens), and must begin with a letter. Once created, this cannot be changed.
</em>
{{#if item.error.Name}}
<strong>{{item.error.Name.validation}}</strong>
{{/if}}
</label>
{{/if}}
<label class="type-text validate-optional">
<span>Description (Optional)</span>
<textarea name="Description" oninput={{action 'change'}}>{{item.Description}}</textarea>
</label>
</fieldset>
{{#if (env 'CONSUL_ACLS_ENABLED')}}
<fieldset id="roles">
<h2>Roles</h2>
<p>
{{#if (can "write nspace" item=item)}}
By adding roles to this namespaces, you will apply them to all tokens created within this namespace.
{{else}}
The following roles are applied to all tokens created within this namespace.
{{/if}}
</p>
<RoleSelector
@disabled={{not (can "write nspace" item=item)}}
@dc={{dc}}
@nspace="default"
@partition={{partition}}
@items={{item.ACLs.RoleDefaults}}
/>
</fieldset>
<fieldset id="policies">
<h2>Policies</h2>
<p>
{{#if (can "write nspace" item=item)}}
By adding policies to this namespaces, you will apply them to all tokens created within this namespace.
{{else}}
The following policies are applied to all tokens created within this namespace.
{{/if}}
</p>
<PolicySelector
@disabled={{not (can "write nspace" item=item)}}
@dc={{dc}}
@nspace="default"
@partition={{partition}}
@allowIdentity={{false}}
@items={{item.ACLs.PolicyDefaults}}
/>
</fieldset>
{{/if}}
<div>
{{#if (and create (can "create nspaces")) }}
<button type="submit" {{ action "create" item}} disabled={{if (or item.isPristine item.isInvalid) 'disabled'}}>Save</button>
{{else}}
{{#if (can "write nspace" item=item)}}
<button type="submit" {{ action "update" item}} disabled={{if item.isInvalid 'disabled'}}>Save</button>
{{/if}}
{{/if}}
<button type="reset" {{ action "cancel" item}}>Cancel</button>
{{# if (and (not create) (can "delete nspace" item=item) ) }}
<ConfirmationDialog @message="Are you sure you want to delete this Namespace?">
<BlockSlot @name="action" as |confirm|>
<button data-test-delete type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
</BlockSlot>
<BlockSlot @name="dialog" as |execute cancel message|>
<DeleteConfirmation @message={{message}} @execute={{execute}} @cancel={{cancel}} />
</BlockSlot>
</ConfirmationDialog>
{{/if}}
</div>
</form>

View File

@ -1,6 +1,16 @@
{{page-title 'Engineering Docs - Consul' separator=' - '}}
{{document-attrs class="is-debug"}}
{{! Tell CSS what we have enabled }}
{{#if (can "use acls")}}
{{document-attrs class="has-acls"}}
{{/if}}
{{#if (can "use nspaces")}}
{{document-attrs class="has-nspaces"}}
{{/if}}
{{#if (can "use partitions")}}
{{document-attrs class="has-partitions"}}
{{/if}}
<App class="docs" id="wrapper">
<:notifications as |app|>

View File

@ -1,10 +0,0 @@
<Route
@name={{routeName}}
as |route|>
<Outlet
@name={{routeName}}
@model={{route.model}}
as |o|>
{{outlet}}
</Outlet>
</Route>

View File

@ -6,7 +6,7 @@ import lookupValidator from 'ember-changeset-validations';
// Keep these here for now so forms are easy to make
// TODO: Probably move this to utils/form/parse-element-name
import parseElementName from 'consul-ui/utils/get-form-name-property';
const defaultChangeset = function(data, validators) {
export const defaultChangeset = function(data, validators) {
return createChangeset(data, lookupValidator(validators), validators, { changeset: Changeset });
};
/**

View File

@ -1,4 +0,0 @@
import { validateFormat } from 'ember-changeset-validations/validators';
export default {
Name: validateFormat({ regex: /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?$/ }),
};

View File

@ -28,7 +28,8 @@ module.exports = function(defaults, $ = process.env) {
const apps = [
'consul-acls',
'consul-partitions'
'consul-partitions',
'consul-nspaces'
].map(item => {
return {
name: item,

View File

@ -54,6 +54,11 @@ module.exports = {
environment: config.environment,
rootURL: config.environment === 'production' ? '{{.ContentPath}}' : config.rootURL,
config: config,
env: function(key) {
if (process.env[key]) {
return process.env[key];
}
},
};
switch (type) {
case 'head':

View File

@ -15,7 +15,7 @@ const hbs = (path, attrs = {}) =>
const BrandLoader = attrs => hbs('brand-loader/index.hbs', attrs);
const Enterprise = attrs => hbs('brand-loader/enterprise.hbs', attrs);
module.exports = ({ appName, environment, rootURL, config }) => `
module.exports = ({ appName, environment, rootURL, config, env }) => `
<noscript>
<div style="margin: 0 auto;">
<h2>JavaScript Required</h2>
@ -47,7 +47,9 @@ ${
? `
<script data-app-name="${appName}" data-${appName}-services src="${rootURL}assets/consul-ui/services-debug.js"></script>
<script data-app-name="${appName}" data-${appName}-routing src="${rootURL}assets/consul-ui/routes-debug.js"></script>
` : ``}
`
: ``
}
${
environment === 'production'
? `
@ -57,13 +59,18 @@ ${
{{if .PartitionsEnabled}}
<script data-app-name="${appName}" data-${appName}-routing src="${rootURL}assets/consul-partitions/routes.js"></script>
{{end}}
{{if .NamespacesEnabled}}
<script data-app-name="${appName}" data-${appName}-routing src="${rootURL}assets/consul-nspaces/routes.js"></script>
{{end}}
`
: `
<script>
(
function(get, obj) {
Object.entries(obj).forEach(([key, value]) => {
if(get(key)) {
if(get(key) || (key === 'CONSUL_NSPACES_ENABLE' && ${
env('CONSUL_NSPACES_ENABLED') === '1' ? `true` : `false`
})) {
document.write(\`\\x3Cscript data-app-name="${appName}" data-${appName}-routing src="${rootURL}assets/\${value}/routes.js">\\x3C/script>\`);
}
});
@ -72,7 +79,8 @@ ${
key => document.cookie.split('; ').find(item => item.startsWith(\`\${key}=\`)),
{
'CONSUL_ACLS_ENABLE': 'consul-acls',
'CONSUL_PARTITIONS_ENABLE': 'consul-partitions'
'CONSUL_PARTITIONS_ENABLE': 'consul-partitions',
'CONSUL_NSPACES_ENABLE': 'consul-nspaces'
}
);
</script>

View File

@ -81,6 +81,7 @@
"clipboard": "^2.0.4",
"consul-acls": "*",
"consul-partitions": "*",
"consul-nspaces": "*",
"css.escape": "^1.5.1",
"d3-array": "^2.8.0",
"d3-scale": "^3.2.3",

View File

@ -26,17 +26,18 @@ Feature: dc / nspaces / delete: Deleting items with confirmations, success and e
| Edit | Listing | Method | URL | Data |
| nspace | nspaces | DELETE | /v1/namespace/a-namespace?dc=datacenter | {"Name": "a-namespace"} |
--------------------------------------------------------------------------------------------------------
Scenario: Deleting a [Model] from the [Model] detail page
When I visit the [Model] page for yaml
Scenario: Deleting a nspace from the nspace detail page with success
When I visit the nspace page for yaml
---
dc: datacenter
[Slug]
namespace: a-namespace
---
And I click delete
And I click confirmDelete
Then a [Method] request was made to "[URL]"
Then a DELETE request was made to "/v1/namespace/a-namespace?dc=datacenter"
And "[data-notification]" has the "notification-delete" class
And "[data-notification]" has the "success" class
Scenario: Deleting a [Model] from the [Model] detail page with error
When I visit the [Model] page for yaml
---
dc: datacenter
@ -45,7 +46,7 @@ Feature: dc / nspaces / delete: Deleting items with confirmations, success and e
Given the url "[URL]" responds with a 500 status
And I click delete
And I click confirmDelete
And "[data-notification]" has the "notification-delete" class
And "[data-notification]" has the "notification-update" class
And "[data-notification]" has the "error" class
Where:
-------------------------------------------------------------------------------------------

View File

@ -24,8 +24,8 @@ module('Integration | Serializer | nspace', function(hooks) {
Object.assign({}, item, {
Datacenter: dc,
Partition: item.Partition || undefinedPartition,
Namespace: item.Name,
uid: `["${item.Partition}","${item.Name}","${dc}","${item.Name}"]`,
Namespace: '*',
uid: `["${item.Partition}","*","${dc}","${item.Name}"]`,
})
);
const actual = serializer.respondForQuery(

View File

@ -16,9 +16,9 @@ module('Integration | Serializer | partition', function(hooks) {
const expected = payload.Partitions.map(item =>
Object.assign({}, item, {
Datacenter: dc,
Namespace: '',
Partition: item.Name,
uid: `["${item.Name}","","${dc}","${item.Name}"]`,
Namespace: '*',
Partition: '*',
uid: `["*","*","${dc}","${item.Name}"]`,
})
);
const actual = serializer.respondForQuery(

View File

@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Controller | dc/nspaces/create', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.owner.lookup('controller:dc/nspaces/create');
assert.ok(controller);
});
});

View File

@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Controller | dc/nspaces/edit', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.owner.lookup('controller:dc/nspaces/edit');
assert.ok(controller);
});
});

View File

@ -1,11 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | dc/nspaces/create', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:dc/nspaces/create');
assert.ok(route);
});
});

View File

@ -1,11 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | dc/nspaces/edit', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:dc/nspaces/edit');
assert.ok(route);
});
});

View File

@ -1,11 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | dc/nspaces/index', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:dc/nspaces/index');
assert.ok(route);
});
});