ui: Adds initial CRUD for partitions (#11188)
* Add `is` and `test` helpers in a similar vein to `can` Adds 2 new helpers in a similar vein to ember-cans can: - `is` allows you to use vocab/phrases such as (is "something model") which calls isSomething() on the models ability. - `test` allows you to use vocab/phrases such as (test "is something model") or (test "can something model")which calls isSomething() / canSomething() on the models ability. Mostly using the is helper and the can helper. It's basically the is/can helper combined. * Adds TextInput component + related modifiers/helpers/machines/services (#11189) Adds a few new components/modifiers/helpers to aid building forms. - state-chart helper, used in lieu of a more generic approach for requiring our statecharts. - A few modifications to our existing disabled modifier. - A new 'validation' modifier, a super small form validation approach built to make use of state charts (optionally). Eventually we should be able to replace our current validation approach (ember-changeset-validations + extra deps) with this. - A new TextInput component, which is the first of our new components specifically to make it easy to build forms with validations. This is still a WIP, I left some comments in pointing out where this one would be progressed, but as we don't need the planned functionality yet, I left it where it was. All of this will be fleshed out more at a later date. Documentation is included for all of ^ * ui: Adds initial CRUD for partitions (#11190) Adds basic CRUD support for partitions. Engineering-wise probably the biggest takeaway here is that we needed to write very little javascript code to add this entire feature, and the little javascript we did need to write was very straightforwards. Everything is pretty much just HTML. Another note to make is that both ember-changeset and ember-data (model layer things) are now completely abstracted away from the view layer of the application. New components: - Consul::Partition::Form - Consul::Partition::List - Consul::Partition::Notifications - Consul::Partition::SearchBar - Consul::Partition::Selector See additional documentation here for more details New Route templates: - index.hbs partition listing/searching/filtering - edit.hbs partition editing and creation Additionally: There is some additional debug work here for better observability and to prevent any errors regarding our href-to usage when a dc is not available in our documentation site. Our softDelete functionality has been DRYed out a little to be used across two repos. isLinkable was removed from our ListCollection component for lists like upstream and service listing, and instead use our new is helper from within the ListCollection, meaning we've added a few more lighterweight templateOnly components. * ui: Exclude all debug-like files from the build (#11211) This PR adds **/*-debug.* to our test/prod excluded files (realised I needed to add test-support.js also so added that here as its more or less the same thing). Conditionally juggling ES6 static imports (specifically debug ones) for this was also getting a little hairy, so I moved it all to use the same approach as our conditional routes. All in all it brings the vendor build back down to ~430kb gzipped.
This commit is contained in:
parent
51769d1f95
commit
93b78aee53
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
ui: Added initial support for admin partition CRUD
|
||||
```
|
|
@ -22,7 +22,7 @@ references:
|
|||
test-results: &TEST_RESULTS_DIR /tmp/test-results
|
||||
|
||||
cache:
|
||||
yarn: &YARN_CACHE_KEY consul-ui-v4-{{ checksum "ui/yarn.lock" }}
|
||||
yarn: &YARN_CACHE_KEY consul-ui-v5-{{ checksum "ui/yarn.lock" }}
|
||||
rubygem: &RUBYGEM_CACHE_KEY static-site-gems-v1-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
environment: &ENVIRONMENT
|
||||
|
@ -602,7 +602,7 @@ jobs:
|
|||
|
||||
- run:
|
||||
name: install yarn packages
|
||||
command: cd ui && yarn install
|
||||
command: cd ui && yarn install && cd packages/consul-ui && yarn install
|
||||
|
||||
- save_cache:
|
||||
key: *YARN_CACHE_KEY
|
||||
|
|
|
@ -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;'"
|
||||
"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'"
|
||||
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "consul-acls",
|
||||
"version": "0.1.0",
|
||||
"private": true
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
(routes => routes({
|
||||
dc: {
|
||||
acls: {
|
||||
tokens: {
|
||||
_options: {
|
||||
abilities: ['read tokens'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))(
|
||||
(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);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,24 @@
|
|||
# Consul::Partition::Form
|
||||
|
||||
```hbs preview-template
|
||||
<DataLoader @src={{
|
||||
uri '/${partition}/${nspace}/${dc}/partition/${id}'
|
||||
(hash
|
||||
partition='partition'
|
||||
nspace='nspace'
|
||||
dc='dc'
|
||||
id=''
|
||||
)
|
||||
}}
|
||||
as |loader|>
|
||||
<BlockSlot @name="loaded">
|
||||
<Consul::Partition::Form
|
||||
@item={{loader.data}}
|
||||
@dc={{'dc-1'}}
|
||||
@nspace={{'nspace'}}
|
||||
@partition={{'partition'}}
|
||||
@onsubmit={{noop}}
|
||||
/>
|
||||
</BlockSlot>
|
||||
</DataLoader>
|
||||
```
|
|
@ -0,0 +1,126 @@
|
|||
<div
|
||||
class="consul-partition-form"
|
||||
...attributes
|
||||
>
|
||||
<DataWriter
|
||||
@sink={{uri
|
||||
'/${partition}/${nspace}/${dc}/partition'
|
||||
(hash
|
||||
partition=''
|
||||
nspace=''
|
||||
dc=@item.Datacenter
|
||||
)
|
||||
}}
|
||||
@type={{'partition'}}
|
||||
@label={{label}}
|
||||
@ondelete={{fn (if @ondelete @ondelete @onsubmit) @item}}
|
||||
@onchange={{fn (optional @onsubmit) @item}}
|
||||
as |writer|>
|
||||
|
||||
<BlockSlot @name="content">
|
||||
|
||||
{{#let
|
||||
|
||||
@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 |item Name Description|}}
|
||||
|
||||
<form
|
||||
{{on 'submit' (fn writer.persist item)}}
|
||||
{{disabled (not (can "write partition" item=item))}}
|
||||
>
|
||||
|
||||
<StateChart
|
||||
@src={{state-chart 'validate'}}
|
||||
as |State Guard Action dispatch state|>
|
||||
|
||||
<fieldset>
|
||||
{{#if (is "new partition" 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>
|
||||
|
||||
<div>
|
||||
{{#if (and (is "new partition" item=item) (can "create partitions")) }}
|
||||
<button
|
||||
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>
|
||||
{{/if}}
|
||||
|
||||
<button
|
||||
type="reset"
|
||||
{{on 'click' (if @oncancel (fn @oncancel item) (fn @onsubmit item))}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
{{#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
|
||||
data-test-delete
|
||||
type="button"
|
||||
class="type-delete"
|
||||
{{on 'click' (fn confirm (fn writer.delete item))}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</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>
|
|
@ -0,0 +1,32 @@
|
|||
# Consul::Partition::List
|
||||
|
||||
A presentational component for rendering Consul Partitions
|
||||
|
||||
Please note:
|
||||
|
||||
- For the moment, make sure you have enabled partitions using developer debug
|
||||
cookies.
|
||||
|
||||
```hbs preview-template
|
||||
<DataSource @src={{uri '/partition/default/dc-1/partitions'}} as |source|>
|
||||
<Consul::Partition::List
|
||||
@items={{source.data}}
|
||||
@ondelete={{noop}}
|
||||
/>
|
||||
</DataSource>
|
||||
```
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
- [Component Source Code](./index.js)
|
||||
- [Template Source Code](./index.hbs)
|
||||
|
||||
---
|
|
@ -0,0 +1,63 @@
|
|||
<ListCollection
|
||||
class="consul-partition-list"
|
||||
...attributes
|
||||
@items={{@items}}
|
||||
@linkable="linkable partition"
|
||||
as |item|>
|
||||
<BlockSlot @name="header">
|
||||
{{#if item.DeletedAt}}
|
||||
<p>
|
||||
Deleting {{item.Name}}...
|
||||
</p>
|
||||
{{else}}
|
||||
<a data-test-partition={{item.Name}} href={{href-to 'dc.partitions.edit' item.Name}}>{{item.Name}}</a>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="details">
|
||||
{{#if item.Description}}
|
||||
<dl>
|
||||
<dt>Description</dt>
|
||||
<dd data-test-description>
|
||||
{{item.Description}}
|
||||
</dd>
|
||||
</dl>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions" as |Actions|>
|
||||
{{#if (not item.DeletedAt)}}
|
||||
<Actions as |Action|>
|
||||
<Action data-test-edit-action @href={{href-to 'dc.partitions.edit' item.Name}}>
|
||||
<BlockSlot @name="label">
|
||||
{{#if (can "write partition" item=item)}}
|
||||
Edit
|
||||
{{else}}
|
||||
View
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
</Action>
|
||||
{{#if (can "delete partition" item=item)}}
|
||||
<Action data-test-delete-action @onclick={{action @ondelete item}} class="dangerous">
|
||||
<BlockSlot @name="label">
|
||||
Delete
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="confirmation" as |Confirmation|>
|
||||
<Confirmation class="warning">
|
||||
<BlockSlot @name="header">
|
||||
Confirm delete
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
Are you sure you want to delete this partition?
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="confirm" as |Confirm|>
|
||||
<Confirm>Delete</Confirm>
|
||||
</BlockSlot>
|
||||
</Confirmation>
|
||||
</BlockSlot>
|
||||
</Action>
|
||||
{{/if}}
|
||||
</Actions>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
</ListCollection>
|
|
@ -0,0 +1,18 @@
|
|||
export const selectors = () => ({
|
||||
['.consul-partition-list']: {
|
||||
row: {
|
||||
$: '[data-test-list-row]',
|
||||
partition: 'a',
|
||||
name: '[data-test-partition]',
|
||||
description: '[data-test-description]'
|
||||
}
|
||||
}
|
||||
});
|
||||
export const pageObject = (collection, clickable, attribute, text, actions) => () => {
|
||||
return collection('.consul-partition-list [data-test-list-row]', {
|
||||
partition: clickable('a'),
|
||||
name: attribute('data-test-partition', '[data-test-partition]'),
|
||||
description: text('[data-test-description]'),
|
||||
...actions(['edit', 'delete']),
|
||||
});
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
# Consul::Partition::Notifications
|
||||
|
||||
A Notification component specifically for Partitions (at some point will be replaced with just using `ember-intl`/`t`.
|
||||
|
||||
```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}}
|
||||
/>
|
||||
</p>
|
||||
</figure>
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
## See
|
||||
|
||||
- [Template Source Code](./index.hbs)
|
||||
|
||||
---
|
|
@ -0,0 +1,24 @@
|
|||
{{#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}}
|
||||
{{#let @error.errors.firstObject as |error|}}
|
||||
{{#if error.detail }}
|
||||
<br />{{concat '(' (if error.status (concat error.status ': ')) error.detail ')'}}
|
||||
{{/if}}
|
||||
{{/let}}
|
|
@ -0,0 +1,30 @@
|
|||
# Consul::Partition::SearchBar
|
||||
|
||||
Searchbar tailored for searching Partitions. Follows our more generic
|
||||
'*::SearchBar' component interface.
|
||||
|
||||
```hbs preview-template
|
||||
<Consul::Partition::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)
|
||||
|
||||
---
|
|
@ -0,0 +1,98 @@
|
|||
<SearchBar
|
||||
class="consul-partition-search-bar"
|
||||
...attributes
|
||||
@filter={{@filter}}
|
||||
>
|
||||
<:status as |search|>
|
||||
|
||||
{{#let
|
||||
|
||||
(t (concat "components.consul.nspace.search-bar." search.status.key)
|
||||
default=(array
|
||||
(concat "common.search." search.status.key)
|
||||
(concat "common.consul." search.status.key)
|
||||
)
|
||||
)
|
||||
|
||||
(t (concat "components.consul.nspace.search-bar." search.status.value)
|
||||
default=(array
|
||||
(concat "common.search." search.status.value)
|
||||
(concat "common.consul." search.status.value)
|
||||
(concat "common.brand." search.status.value)
|
||||
)
|
||||
)
|
||||
|
||||
as |key value|}}
|
||||
<search.RemoveFilter
|
||||
aria-label={{t "common.ui.remove" item=(concat key " " value)}}
|
||||
>
|
||||
<dl>
|
||||
<dt>{{key}}</dt>
|
||||
<dd>{{value}}</dd>
|
||||
</dl>
|
||||
</search.RemoveFilter>
|
||||
{{/let}}
|
||||
|
||||
</:status>
|
||||
<:search as |search|>
|
||||
<search.Search
|
||||
@onsearch={{action @onsearch}}
|
||||
@value={{@search}}
|
||||
@placeholder={{t "common.search.search"}}
|
||||
>
|
||||
<search.Select
|
||||
class="type-search-properties"
|
||||
@position="right"
|
||||
@onchange={{action @filter.searchproperty.change}}
|
||||
@multiple={{true}}
|
||||
@required={{true}}
|
||||
as |components|>
|
||||
<BlockSlot @name="selected">
|
||||
<span>
|
||||
{{t "common.search.searchproperty"}}
|
||||
</span>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="options">
|
||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||
{{#each @filter.searchproperty.default as |prop|}}
|
||||
<Option @value={{prop}} @selected={{contains prop @filter.searchproperty.value}}>
|
||||
{{t (concat "common.consul." (lowercase prop))}}
|
||||
</Option>
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</search.Select>
|
||||
</search.Search>
|
||||
</:search>
|
||||
<:sort as |search|>
|
||||
<search.Select
|
||||
class="type-sort"
|
||||
data-test-sort-control
|
||||
@position="right"
|
||||
@onchange={{action @sort.change}}
|
||||
@multiple={{false}}
|
||||
@required={{true}}
|
||||
as |components|>
|
||||
<BlockSlot @name="selected">
|
||||
<span>
|
||||
{{#let (from-entries (array
|
||||
(array "Name:asc" (t "common.sort.alpha.asc"))
|
||||
(array "Name:desc" (t "common.sort.alpha.desc"))
|
||||
))
|
||||
as |selectable|
|
||||
}}
|
||||
{{get selectable @sort.value}}
|
||||
{{/let}}
|
||||
</span>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="options">
|
||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||
<Optgroup @label={{t "common.consul.name"}}>
|
||||
<Option @value="Name:asc" @selected={{eq "Name:asc" @sort.value}}>{{t "common.sort.alpha.asc"}}</Option>
|
||||
<Option @value="Name:desc" @selected={{eq "Name:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option>
|
||||
</Optgroup>
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</search.Select>
|
||||
</:sort>
|
||||
</SearchBar>
|
|
@ -0,0 +1,40 @@
|
|||
# Consul::Partition::Selector
|
||||
|
||||
A conditional, autoloading, menu component specifically for making it easy to select partitions.
|
||||
|
||||
Please note:
|
||||
|
||||
- Currently at least, you must add this inside of a `<ul>` element.
|
||||
- For the moment, make sure you have enabled partitions using developer debug
|
||||
cookies.
|
||||
|
||||
```hbs preview-template
|
||||
<ul>
|
||||
<Consul::Partition::Selector
|
||||
@dc={{hash
|
||||
Name='dc-1'
|
||||
}}
|
||||
@nspace='default'
|
||||
@partition='default'
|
||||
@partitions={{or this.partitions (array)}}
|
||||
@onchange={{action (mut this.partitions) 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 |
|
||||
| `partitions` | `array` | | A list of partition models/objects to use for the selector |
|
||||
| `onchange` | `function` | | An event handler, for when partitions are loaded. You probably want to update `@partitions` using this. |
|
||||
|
||||
## See
|
||||
|
||||
- [Template Source Code](./index.hbs)
|
||||
|
||||
---
|
|
@ -0,0 +1,53 @@
|
|||
{{#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={{fn (optional @onchange)}}
|
||||
/>
|
||||
{{#each (reject-by 'DeletedAt' @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 (can 'manage partitions')}}
|
||||
<MenuSeparator />
|
||||
<MenuItem
|
||||
data-test-main-nav-partitions
|
||||
@href={{href-to 'dc.partitions.index' @dc.Name}}
|
||||
>
|
||||
<BlockSlot @name="label">
|
||||
Manage Admin Partitions
|
||||
</BlockSlot>
|
||||
</MenuItem>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</PopoverMenu>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<Route
|
||||
@name={{routeName}}
|
||||
as |route|>
|
||||
<DataLoader @src={{
|
||||
uri '/${partition}/${nspace}/${dc}/partition/${id}'
|
||||
(hash
|
||||
partition=route.params.partition
|
||||
nspace=route.params.nspace
|
||||
dc=route.params.dc
|
||||
id=(or route.params.name '')
|
||||
)
|
||||
}}
|
||||
as |loader|>
|
||||
|
||||
<BlockSlot @name="error">
|
||||
<AppError
|
||||
@error={{loader.error}}
|
||||
@login={{route.model.app.login.open}}
|
||||
/>
|
||||
</BlockSlot>
|
||||
|
||||
<BlockSlot @name="loaded">
|
||||
{{#let
|
||||
|
||||
route.params.dc
|
||||
route.params.partition
|
||||
route.params.nspace
|
||||
|
||||
loader.data
|
||||
loader.data.isNew
|
||||
as |dc partition nspace item create|}}
|
||||
<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>
|
||||
</ol>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="header">
|
||||
<h1>
|
||||
<route.Title @title={{if create "New Partition" (concat "Edit " item.Name)}} />
|
||||
</h1>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
|
||||
<Consul::Partition::Form
|
||||
@item={{item}}
|
||||
@dc={{route.params.dc}}
|
||||
@nspace={{route.params.nspace}}
|
||||
@partition={{route.params.partition}}
|
||||
@onsubmit={{transition-to 'dc.partitions.index'}}
|
||||
/>
|
||||
|
||||
|
||||
</BlockSlot>
|
||||
</AppView>
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</DataLoader>
|
||||
</Route>
|
|
@ -0,0 +1,138 @@
|
|||
<Route
|
||||
@name={{routeName}}
|
||||
as |route|>
|
||||
<DataLoader
|
||||
@src={{
|
||||
uri '/${partition}/${nspace}/${dc}/partitions'
|
||||
(hash
|
||||
partition=route.params.partition
|
||||
nspace=route.params.nspace
|
||||
dc=route.params.dc
|
||||
)}}
|
||||
as |loader|>
|
||||
|
||||
<BlockSlot @name="error">
|
||||
<AppError
|
||||
@error={{loader.error}}
|
||||
@login={{route.model.app.login.open}}
|
||||
/>
|
||||
</BlockSlot>
|
||||
|
||||
<BlockSlot @name="loaded">
|
||||
{{#let
|
||||
|
||||
(hash
|
||||
value=(or sortBy "Name:asc")
|
||||
change=(action (mut sortBy) value="target.selected")
|
||||
)
|
||||
|
||||
(hash
|
||||
searchproperty=(hash
|
||||
value=(if (not-eq searchproperty undefined)
|
||||
(split searchproperty ',')
|
||||
searchProperties
|
||||
)
|
||||
change=(action (mut searchproperty) value="target.selectedItems")
|
||||
default=searchProperties
|
||||
)
|
||||
)
|
||||
|
||||
loader.data
|
||||
|
||||
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" />
|
||||
</h1>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<a data-test-create href="{{href-to 'dc.partitions.create'}}" class="type-create">Create</a>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="toolbar">
|
||||
{{#if (gt items.length 0)}}
|
||||
<Consul::Partition::SearchBar
|
||||
@search={{search}}
|
||||
@onsearch={{action (mut search) value="target.value"}}
|
||||
|
||||
@sort={{sort}}
|
||||
|
||||
@filter={{filters}}
|
||||
/>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
<DataWriter
|
||||
@sink={{uri '/${partition}/${dc}/${nspace}/partition/'
|
||||
(hash
|
||||
partition=route.params.partition
|
||||
nspace=route.params.nspace
|
||||
dc=route.params.dc
|
||||
)
|
||||
}}
|
||||
@type="partition"
|
||||
@ondelete={{refresh-route}}
|
||||
as |writer|>
|
||||
<BlockSlot @name="content">
|
||||
<DataCollection
|
||||
@type="nspace"
|
||||
@sort={{sort.value}}
|
||||
@filters={{filters}}
|
||||
@search={{search}}
|
||||
@items={{items}}
|
||||
as |collection|>
|
||||
<collection.Collection>
|
||||
<Consul::Partition::List
|
||||
@items={{collection.items}}
|
||||
@ondelete={{writer.delete}}
|
||||
/>
|
||||
</collection.Collection>
|
||||
<collection.Empty>
|
||||
<EmptyState
|
||||
@login={{route.model.app.login.open}}
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<h2>
|
||||
{{#if (gt items.length 0)}}
|
||||
No partitions found
|
||||
{{else}}
|
||||
Welcome to Partitions
|
||||
{{/if}}
|
||||
</h2>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
{{#if (gt items.length 0)}}
|
||||
No partitions where found matching that search, or you may not have access to view the namespaces you are searching for.
|
||||
{{else}}
|
||||
There don't seem to be any partitions, or you may not have access to view partitions yet.
|
||||
{{/if}}
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<li class="docs-link">
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}/commands/FIXME" rel="noopener noreferrer" target="_blank">Documentation on partitions</a>
|
||||
</li>
|
||||
<li class="learn-link">
|
||||
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/FIXME" rel="noopener noreferrer" target="_blank">Read the guide</a>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
</collection.Empty>
|
||||
</DataCollection>
|
||||
</BlockSlot>
|
||||
</DataWriter>
|
||||
</BlockSlot>
|
||||
</AppView>
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</DataLoader>
|
||||
</Route>
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "consul-partitions",
|
||||
"version": "0.1.0",
|
||||
"private": true
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
(routes => routes({
|
||||
dc: {
|
||||
partitions: {
|
||||
_options: {
|
||||
path: '/partitions',
|
||||
queryParams: {
|
||||
sortBy: 'sort',
|
||||
searchproperty: {
|
||||
as: 'searchproperty',
|
||||
empty: [['Name', 'Description']],
|
||||
},
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
abilities: ['read partitions'],
|
||||
},
|
||||
edit: {
|
||||
_options: { path: '/:name' },
|
||||
},
|
||||
create: {
|
||||
_options: {
|
||||
template: 'dc/partitions/edit',
|
||||
path: '/create',
|
||||
abilities: ['create partitions'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))(
|
||||
(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);
|
||||
}
|
||||
);
|
|
@ -79,6 +79,12 @@ module.exports = {
|
|||
pattern: '**/README.mdx',
|
||||
urlSchema: 'auto',
|
||||
urlPrefix: 'docs/consul',
|
||||
},
|
||||
{
|
||||
root: `${path.dirname(require.resolve('consul-partitions/package.json'))}/app/components`,
|
||||
pattern: '**/README.mdx',
|
||||
urlSchema: 'auto',
|
||||
urlPrefix: 'docs/consul-partitions',
|
||||
}
|
||||
].concat(user.sources),
|
||||
labels: {
|
||||
|
|
|
@ -38,6 +38,19 @@ export default class BaseAbility extends Ability {
|
|||
this.permissions.generate(this.resource, ACCESS_WRITE, segment),
|
||||
];
|
||||
}
|
||||
// characteristics
|
||||
// TODO: Remove once we have managed to do the scroll pane refactor
|
||||
get isLinkable() {
|
||||
return true;
|
||||
}
|
||||
get isNew() {
|
||||
return this.item.isNew;
|
||||
}
|
||||
|
||||
get isPristine() {
|
||||
return this.item.isPristine;
|
||||
}
|
||||
//
|
||||
|
||||
get canRead() {
|
||||
if (typeof this.item !== 'undefined') {
|
||||
|
|
|
@ -7,6 +7,10 @@ export default class NspaceAbility extends BaseAbility {
|
|||
resource = 'operator';
|
||||
segmented = false;
|
||||
|
||||
get isLinkable() {
|
||||
return !this.item.DeletedAt;
|
||||
}
|
||||
|
||||
get canManage() {
|
||||
return this.canCreate;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import BaseAbility from './base';
|
||||
import BaseAbility from 'consul-ui/abilities/base';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class PartitionAbility extends BaseAbility {
|
||||
|
@ -7,6 +7,10 @@ export default class PartitionAbility extends BaseAbility {
|
|||
resource = 'operator';
|
||||
segmented = false;
|
||||
|
||||
get isLinkable() {
|
||||
return !this.item.DeletedAt;
|
||||
}
|
||||
|
||||
get canManage() {
|
||||
return this.canCreate;
|
||||
}
|
||||
|
|
|
@ -2,4 +2,8 @@ import BaseAbility from './base';
|
|||
|
||||
export default class ServiceAbility extends BaseAbility {
|
||||
resource = 'service';
|
||||
|
||||
get isLinkable() {
|
||||
return this.item.InstanceCount > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import BaseAbility from './base';
|
||||
|
||||
export default class UpstreamAbility extends BaseAbility {
|
||||
resource = 'upstream';
|
||||
|
||||
get isLinkable() {
|
||||
return this.item.InstanceCount > 0;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import Adapter from './application';
|
||||
import { SLUG_KEY } from 'consul-ui/models/partition';
|
||||
|
||||
// Blocking query support for partitions is currently disabled
|
||||
export default class PartitionAdapter extends Adapter {
|
||||
|
@ -24,4 +25,37 @@ export default class PartitionAdapter extends Adapter {
|
|||
await respond((headers, body) => delete headers['x-consul-index']);
|
||||
return respond;
|
||||
}
|
||||
|
||||
async requestForCreateRecord(request, serialized, data) {
|
||||
return request`
|
||||
PUT /v1/partition/${data[SLUG_KEY]}?${{
|
||||
dc: data.Datacenter,
|
||||
}}
|
||||
|
||||
${{
|
||||
Name: serialized.Name,
|
||||
Description: serialized.Description,
|
||||
}}
|
||||
`;
|
||||
}
|
||||
|
||||
async requestForUpdateRecord(request, serialized, data) {
|
||||
return request`
|
||||
PUT /v1/partition/${data[SLUG_KEY]}?${{
|
||||
dc: data.Datacenter,
|
||||
}}
|
||||
|
||||
${{
|
||||
Description: serialized.Description,
|
||||
}}
|
||||
`;
|
||||
}
|
||||
|
||||
async requestForDeleteRecord(request, serialized, data) {
|
||||
return request`
|
||||
DELETE /v1/partition/${data[SLUG_KEY]}?${{
|
||||
dc: data.Datacenter,
|
||||
}}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
class="consul-nspace-list"
|
||||
...attributes
|
||||
@items={{@items}}
|
||||
@linkable={{action this.isLinkable}}
|
||||
@linkable="linkable nspace"
|
||||
as |item|>
|
||||
<BlockSlot @name="header">
|
||||
{{#if item.DeletedAt}}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import Component from '@glimmer/component';
|
||||
|
||||
export default class ConsulNspaceList extends Component {
|
||||
isLinkable(item) {
|
||||
return !item.DeletedAt;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
class="consul-service-list"
|
||||
...attributes
|
||||
@items={{@items}}
|
||||
@linkable={{action this.isLinkable}}
|
||||
@linkable="linkable service"
|
||||
as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class ConsulServiceList extends Component {
|
||||
@action
|
||||
isLinkable(item) {
|
||||
return item.InstanceCount > 0;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
class="consul-upstream-list"
|
||||
...attributes
|
||||
@items={{@items}}
|
||||
@linkable={{action this.isLinkable}}
|
||||
@linkable="linkable upstream"
|
||||
as |item index|>
|
||||
<BlockSlot @name="header">
|
||||
{{#if (gt item.InstanceCount 0)}}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class ConsulServiceList extends Component {
|
||||
@action
|
||||
isLinkable(item) {
|
||||
return item.InstanceCount > 0;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import Component from '@ember/component';
|
|||
import { inject as service } from '@ember/service';
|
||||
import { set, get } from '@ember/object';
|
||||
import Slotted from 'block-slots';
|
||||
import { isChangeset } from 'validated-changeset';
|
||||
|
||||
export default Component.extend(Slotted, {
|
||||
tagName: '',
|
||||
|
@ -41,7 +42,7 @@ export default Component.extend(Slotted, {
|
|||
setData: function(data) {
|
||||
let changeset = data;
|
||||
// convert to a real changeset
|
||||
if (typeof this.form !== 'undefined') {
|
||||
if (!isChangeset(data) && typeof this.form !== 'undefined') {
|
||||
changeset = this.form.setData(data).getData();
|
||||
}
|
||||
// mark as creating
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
persist=(action "persist")
|
||||
delete=(queue (action (mut data)) (action dispatch "REMOVE"))
|
||||
inflight=(state-matches state (array "persisting" "removing"))
|
||||
disabled=(state-matches state (array "persisting" "removing"))
|
||||
) as |api|}}
|
||||
|
||||
{{yield api}}
|
||||
|
|
|
@ -48,60 +48,13 @@
|
|||
</BlockSlot>
|
||||
</PopoverMenu>
|
||||
</li>
|
||||
|
||||
{{#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"}}
|
||||
/>
|
||||
{{#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}}
|
||||
|
||||
<Consul::Partition::Selector
|
||||
@dc={{@dc}}
|
||||
@partition={{@partition}}
|
||||
@nspace={{@nspace}}
|
||||
@partitions={{this.partitions}}
|
||||
@onchange={{action (mut this.partitions) value="data"}}
|
||||
/>
|
||||
{{#if (can "choose nspaces")}}
|
||||
<li
|
||||
class="nspaces"
|
||||
|
|
|
@ -22,10 +22,7 @@
|
|||
<li
|
||||
data-test-list-row
|
||||
onclick={{action 'click'}} style={{{cell.style}}}
|
||||
class={{if
|
||||
(compute (action (or linkable (noop)) cell.item))
|
||||
'linkable'
|
||||
}}
|
||||
class={{if (not linkable) 'linkable' (if (is linkable item=cell.item) 'linkable')}}
|
||||
>
|
||||
<YieldSlot @name="header"><div class="header">{{yield cell.item cell.index}}</div></YieldSlot>
|
||||
<YieldSlot @name="details"><div class="detail">{{yield cell.item cell.index}}</div></YieldSlot>
|
||||
|
@ -49,10 +46,7 @@
|
|||
<li
|
||||
data-test-list-row
|
||||
onclick={{action 'click'}}
|
||||
class={{if
|
||||
(compute (action (or linkable (noop)) item))
|
||||
'linkable'
|
||||
}}
|
||||
class={{if (not linkable) 'linkable' (if (is linkable item=cell.item) 'linkable')}}
|
||||
>
|
||||
<YieldSlot @name="header"><div class="header">{{yield item index}}</div></YieldSlot>
|
||||
<YieldSlot @name="details"><div class="detail">{{yield item index}}</div></YieldSlot>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# TextInput
|
||||
|
||||
Form component to be used for entering text values, both short form and long
|
||||
form. Currently an inline component but as and when we get chance this will be
|
||||
changed to also accept slots for specifying specific parts of the component.
|
||||
|
||||
```hbs preview-template
|
||||
<TextInput
|
||||
@name="single"
|
||||
@label="Single Line Text Input"
|
||||
@item={{hash
|
||||
single=""
|
||||
}}
|
||||
@placeholder="Placeholder: Enter some single line text here"
|
||||
@help="Help me if you can, I'm feeling down"
|
||||
/>
|
||||
<hr />
|
||||
<TextInput
|
||||
@expanded={{true}}
|
||||
@name="Description"
|
||||
@label="Multiline Input"
|
||||
@item={{hash
|
||||
Description="Long form text"
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
| Argument | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `item` | `Object` | | An object whose properties are to be edited |
|
||||
| `name` | `String` | '' | An identifier for the property to be edited on the `item` |
|
||||
| `label` | `String` | `@name` | A label to use to label the text input element |
|
||||
| `placeholder` | `String` | | Equivalent to the HTML `placeholder=""` attribute |
|
||||
| `help` | `String` | | Provide some help text for the input (consider using `@validations` instead) |
|
||||
| `expanded` | `Boolean` | `false` | Whether to use an expanded textarea or just a normal single line input |
|
||||
| `validations` | `Object` | | A `validations` object to be passed to the underlying `validate` modifier |
|
||||
| `chart` | `Object` | | A StateChart object (implementing `state` and `dispatch` to be passed to the underlying `validate` modifier |
|
||||
|
||||
## See
|
||||
|
||||
- [Validate Modifier](../modifiers/validate.mdx)
|
||||
- [Template Source Code](./index.hbs)
|
||||
|
||||
---
|
|
@ -0,0 +1,48 @@
|
|||
<label
|
||||
class={{concat 'text-input' ' type-text' (if (get @chart.state.context.errors @name) ' has-error')}}
|
||||
...attributes
|
||||
>
|
||||
<span>
|
||||
{{!- add an optional slot here called <:label>-}}
|
||||
{{or @label @name}}
|
||||
</span>
|
||||
{{!- add an optional slot here called <:input>?-}}
|
||||
{{#if @expanded}}
|
||||
<textarea
|
||||
{{validate @item
|
||||
validations=@validations
|
||||
chart=@chart
|
||||
}}
|
||||
{{on 'input' (optional @oninput)}}
|
||||
name={{@name}}
|
||||
>{{or @value (get @item @name)}}</textarea>
|
||||
{{else}}
|
||||
<input
|
||||
{{validate @item
|
||||
validations=@validations
|
||||
chart=@chart
|
||||
}}
|
||||
{{on 'input' (optional @oninput)}}
|
||||
type="text"
|
||||
value={{or @value (get @item @name)}}
|
||||
name={{@name}}
|
||||
placeholder={{or @placeholder}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#let
|
||||
(or @validations.help @help)
|
||||
as |help|}}
|
||||
{{#if help}}
|
||||
{{!- add an optional slot here called <:help>?-}}
|
||||
<em>
|
||||
{{help}}
|
||||
</em>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
<State @state={{@chart.state}} @matches="error">
|
||||
{{!- add an optional slot here called <:alert/error/success>?-}}
|
||||
<strong
|
||||
role="alert"
|
||||
>{{get (get @chart.state.context.errors @name) 'message'}}</strong>
|
||||
</State>
|
||||
</label>
|
|
@ -0,0 +1,7 @@
|
|||
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);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import Helper from 'ember-can/helpers/can';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import { camelize } from '@ember/string';
|
||||
export const is = (helper, [abilityString, model], properties) => {
|
||||
let { abilityName, propertyName } = helper.can.parse(abilityString);
|
||||
let ability = helper.can.abilityFor(abilityName, model, properties);
|
||||
|
||||
if(typeof ability.getCharacteristicProperty === 'function') {
|
||||
propertyName = ability.getCharacteristicProperty(propertyName);
|
||||
} else {
|
||||
propertyName = camelize(`is-${propertyName}`);
|
||||
}
|
||||
|
||||
helper._removeAbilityObserver();
|
||||
helper._addAbilityObserver(ability, propertyName);
|
||||
|
||||
return get(ability, propertyName);
|
||||
}
|
||||
export default Helper.extend({
|
||||
compute([abilityString, model], properties) {
|
||||
return is(this, [abilityString, model], properties);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
# is
|
||||
|
||||
`{{is "something model" item=item}}` is used to perform a test on based on a
|
||||
type of model, almost the same as `ember-can` but reads better to test for a
|
||||
characteristic rather than an ability:
|
||||
|
||||
```hbs
|
||||
|
||||
{{#if (is "crd intention" item=item)}}
|
||||
I'm a CRD intention
|
||||
{{/if}}
|
||||
|
||||
```
|
||||
|
||||
Consider using the `test` helper instead.
|
||||
|
||||
## See also
|
||||
|
||||
- [`test` helper](./test.mdx)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class StateChartHelper extends Helper {
|
||||
@service('state') state;
|
||||
|
||||
compute([value], hash) {
|
||||
return this.state.stateChart(value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import Helper from 'ember-can/helpers/can';
|
||||
import { is } from 'consul-ui/helpers/is';
|
||||
|
||||
export default Helper.extend({
|
||||
compute([abilityString, model], properties) {
|
||||
switch(true) {
|
||||
case abilityString.startsWith('can '):
|
||||
return super.compute([abilityString.substr(4), model], properties);
|
||||
case abilityString.startsWith('is '):
|
||||
return is(this, [abilityString.substr(3), model], properties);
|
||||
}
|
||||
throw new Error(`${abilityString} is not supported by the 'test' helper.`);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
# test
|
||||
|
||||
`{{and (test "is something model" item=item) (test "can read model")}}` is used
|
||||
to perform a test on based on a type of model, almost the same as `ember-can`
|
||||
and the `is` helper, but is more generic and extensible whilst reading nicely:
|
||||
|
||||
```hbs
|
||||
|
||||
{{#if (and (test "is crd intention" item=item) (test "can read intentions"))}}
|
||||
I'm a CRD intention
|
||||
{{/if}}
|
||||
|
||||
```
|
||||
|
||||
Consider using this instead of the `can` and `is` helpers, as its more generic.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import Route from 'consul-ui/routing/route';
|
||||
|
||||
export default {
|
||||
name: 'routing',
|
||||
initialize(application) {
|
||||
application.register('route:basic', Route);
|
||||
},
|
||||
};
|
|
@ -1,8 +1,42 @@
|
|||
import { runInDebug } from '@ember/debug';
|
||||
import require from 'require';
|
||||
import merge from 'deepmerge';
|
||||
|
||||
const doc = document;
|
||||
const appName = 'consul-ui';
|
||||
const appNameJS = appName
|
||||
.split('-')
|
||||
.map((item, i) => (i ? `${item.substr(0, 1).toUpperCase()}${item.substr(1)}` : item))
|
||||
.join('');
|
||||
|
||||
export const services = merge.all(
|
||||
[].concat(
|
||||
...[...doc.querySelectorAll(`script[data-${appName}-services]`)].map($item =>
|
||||
JSON.parse($item.dataset[`${appNameJS}Services`])
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const inject = function(container, obj) {
|
||||
// inject all the things
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
switch(true) {
|
||||
case (typeof value.class === 'string'):
|
||||
if(require.has(value.class)) {
|
||||
container.register(key.replace('auth-provider:', 'torii-provider:'), require(value.class).default);
|
||||
} else {
|
||||
throw new Error(`Unable to locate '${value.class}'`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
export default {
|
||||
name: 'container',
|
||||
initialize(application) {
|
||||
|
||||
inject(application, services);
|
||||
|
||||
const container = application.lookup('service:container');
|
||||
// find all the services and add their classes to the container so we can
|
||||
// look instances up by class afterwards as we then resolve the
|
||||
|
|
|
@ -199,15 +199,35 @@ export default class FSMWithOptionalLocation {
|
|||
if (typeof this.router === 'undefined') {
|
||||
this.router = this.container.lookup('router:main');
|
||||
}
|
||||
const router = this.router._routerMicrolib;
|
||||
const url = router.generate(routeName, ...params, {
|
||||
queryParams: {},
|
||||
});
|
||||
let withOptional = true;
|
||||
switch (true) {
|
||||
case routeName === 'settings':
|
||||
case routeName.startsWith('docs.'):
|
||||
withOptional = false;
|
||||
break;
|
||||
}
|
||||
const router = this.router._routerMicrolib;
|
||||
let url;
|
||||
try {
|
||||
url = router.generate(routeName, ...params, {
|
||||
queryParams: {},
|
||||
});
|
||||
} catch(e) {
|
||||
if(
|
||||
!(this.router.currentRouteName.startsWith('docs') &&
|
||||
e.message.startsWith('There is no route named ')
|
||||
)
|
||||
) {
|
||||
if(this.router.currentRouteName.startsWith('docs') && routeName.startsWith('dc')) {
|
||||
params.unshift('dc-1');
|
||||
url = router.generate(routeName, ...params, {
|
||||
queryParams: {},
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return `console://${routeName} <= ${JSON.stringify(params)}`;
|
||||
}
|
||||
return this.formatURL(url, hash, withOptional);
|
||||
}
|
||||
|
@ -217,6 +237,10 @@ export default class FSMWithOptionalLocation {
|
|||
* performs an ember transition/refresh and browser location update using that
|
||||
*/
|
||||
transitionTo(url) {
|
||||
if(this.router.currentRouteName.startsWith('docs') && url.startsWith('console://')) {
|
||||
console.log(`location.transitionTo: ${url.substr(10)}`);
|
||||
return true;
|
||||
}
|
||||
const transitionURL = this.getURLForTransition(url);
|
||||
if (this._previousURL === transitionURL) {
|
||||
// probably an optional parameter change
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
export default {
|
||||
id: 'form',
|
||||
initial: 'idle',
|
||||
on: {
|
||||
RESET: [
|
||||
{
|
||||
target: 'idle',
|
||||
},
|
||||
],
|
||||
},
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
SUCCESS: [
|
||||
{
|
||||
target: 'success',
|
||||
},
|
||||
],
|
||||
ERROR: [
|
||||
{
|
||||
target: 'error',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
success: {},
|
||||
error: {},
|
||||
},
|
||||
};
|
|
@ -9,6 +9,8 @@ export default class PartitionModel extends Model {
|
|||
@attr('string') uid;
|
||||
@attr('string') Name;
|
||||
@attr('string') Description;
|
||||
// TODO: Is there some sort of date we can use here
|
||||
@attr('string') DeletedAt;
|
||||
@attr('string') Datacenter;
|
||||
|
||||
@attr('string') Namespace; // always ""
|
||||
|
|
|
@ -3,15 +3,22 @@ import { modifier } from 'ember-modifier';
|
|||
export default modifier(function enabled($element, [bool = true], hash) {
|
||||
if (['input', 'textarea', 'select', 'button'].includes($element.nodeName.toLowerCase())) {
|
||||
if (bool) {
|
||||
$element.disabled = bool;
|
||||
$element.setAttribute('disabled', bool);
|
||||
$element.setAttribute('aria-disabled', bool);
|
||||
} else {
|
||||
$element.dataset.disabled = false;
|
||||
$element.removeAttribute('disabled');
|
||||
$element.removeAttribute('aria-disabled');
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const $el of $element.querySelectorAll('input,textarea')) {
|
||||
if ($el.dataset.disabled !== 'false') {
|
||||
$el.disabled = bool;
|
||||
for (const $el of $element.querySelectorAll('input,textarea,button')) {
|
||||
if(bool && $el.dataset.disabled !== 'false') {
|
||||
$element.setAttribute('disabled', bool);
|
||||
$element.setAttribute('aria-disabled', bool);
|
||||
} else {
|
||||
$element.removeAttribute('disabled');
|
||||
$element.removeAttribute('aria-disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import Modifier from 'ember-modifier';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
class ValidationError extends Error {}
|
||||
|
||||
export default class ValidateModifier extends Modifier {
|
||||
|
||||
item = null;
|
||||
hash = null;
|
||||
|
||||
validate(value, validations = {}) {
|
||||
if(Object.keys(validations).length === 0) {
|
||||
return;
|
||||
}
|
||||
const errors = {};
|
||||
Object.entries(this.hash.validations)
|
||||
// filter out strings, for now these are helps, but ain't great if someone has a item.help
|
||||
.filter(([key, value]) => typeof value !== 'string')
|
||||
.forEach(([key, item]) => {
|
||||
// optionally set things for you
|
||||
if(this.item) {
|
||||
this.item[key] = value;
|
||||
}
|
||||
(item || []).forEach((validation) => {
|
||||
const re = new RegExp(validation.test);
|
||||
if(!re.test(value)) {
|
||||
errors[key] = new ValidationError(validation.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
const state = this.hash.chart.state;
|
||||
if(state.context == null) {
|
||||
state.context = {};
|
||||
}
|
||||
if(Object.keys(errors).length > 0) {
|
||||
state.context.errors = errors;
|
||||
this.hash.chart.dispatch("ERROR");
|
||||
} else {
|
||||
state.context.errors = null;
|
||||
this.hash.chart.dispatch("RESET");
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
reset(e) {
|
||||
if(e.target.value.length === 0) {
|
||||
const state = this.hash.chart.state;
|
||||
if(!state.context) {
|
||||
state.context = {};
|
||||
}
|
||||
if(!state.context.errors) {
|
||||
state.context.errors = {};
|
||||
}
|
||||
Object.entries(this.hash.validations)
|
||||
// filter out strings, for now these are helps, but ain't great if someone has a item.help
|
||||
.filter(([key, value]) => typeof value !== 'string')
|
||||
.forEach(([key, item]) => {
|
||||
if(typeof state.context.errors[key] !== 'undefined') {
|
||||
delete state.context.errors[key];
|
||||
}
|
||||
});
|
||||
if(Object.keys(state.context.errors).length === 0) {
|
||||
state.context.errors = null;
|
||||
this.hash.chart.dispatch("RESET");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connect([value], _hash) {
|
||||
this.element.addEventListener(
|
||||
'input',
|
||||
this.listen
|
||||
);
|
||||
this.element.addEventListener(
|
||||
'blur',
|
||||
this.reset
|
||||
);
|
||||
if(this.element.value.length > 0) {
|
||||
await Promise.resolve();
|
||||
if(this && this.element) {
|
||||
this.validate(this.element.value, this.hash.validations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
listen(e) {
|
||||
this.validate(e.target.value, this.hash.validations);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.item = null;
|
||||
this.hash = null;
|
||||
this.element.removeEventListener(
|
||||
'input',
|
||||
this.listen
|
||||
)
|
||||
this.element.removeEventListener(
|
||||
'blur',
|
||||
this.reset
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
didReceiveArguments() {
|
||||
const [value] = this.args.positional;
|
||||
const _hash = this.args.named;
|
||||
|
||||
this.item = value;
|
||||
this.hash = _hash;
|
||||
|
||||
if(typeof _hash.chart === 'undefined') {
|
||||
this.hash.chart = {
|
||||
state: {
|
||||
context: {}
|
||||
},
|
||||
dispatch: (state) => {
|
||||
switch(state) {
|
||||
case 'ERROR':
|
||||
_hash.onchange(this.hash.chart.state.context.errors);
|
||||
break;
|
||||
case 'RESET':
|
||||
_hash.onchange();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
didInstall() {
|
||||
this.connect(this.args.positional, this.args.named);
|
||||
}
|
||||
|
||||
willRemove() {
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
# validate
|
||||
|
||||
Simple validation modifier to make it super easy to add validations to your
|
||||
form elements.
|
||||
|
||||
**Please note:** You probably should be using one of our many (soon) Form
|
||||
Components like `<TextInput />` instead of using this. If you have something
|
||||
more custom that needs validation support, then read on!
|
||||
|
||||
The `validate` modifier primarily requires a `validations` argument passing to
|
||||
it. The shape of this is an object containing property/validation pairs.
|
||||
Generally you will only need to pass one of these, and in this case the
|
||||
property is also used for naming any resulting errors. For example `Name` will
|
||||
result in `{Name: 'Name error message'}` being thrown/called/passed to the
|
||||
state's context or the `onchange` event.
|
||||
|
||||
In the future we are looking to support validation based on other properties
|
||||
in the passed `item` positional argument, hence the slightly more complicated
|
||||
shape of this `validations` argument.
|
||||
|
||||
Validation objects currently contain 2 properties: `test` and `error`. `test`
|
||||
is used to provide a Regular Expression used to validate the users' input, and
|
||||
the `error` property is a humanized string which is provided to the state's
|
||||
context/onchange event. We may add support for a `success` message in the
|
||||
future for when the validation is in a successful state.
|
||||
|
||||
Please note: you should only need to use either the `chart` argument or the
|
||||
`onchange` listener, not both.
|
||||
|
||||
|
||||
```hbs preview-template
|
||||
|
||||
{{#let
|
||||
|
||||
(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.'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
as |validations|}}
|
||||
|
||||
<figure>
|
||||
<figcaption>Valid to begin with</figcaption>
|
||||
<input
|
||||
{{validate
|
||||
validations=validations
|
||||
onchange=(fn (mut this.validErrors))
|
||||
}}
|
||||
type="text"
|
||||
value={{'this-is-valid-text-add-a-space-to-see-the-validation-error'}}
|
||||
/>
|
||||
{{#if this.validErrors.Name}}
|
||||
<br /><strong>{{this.validErrors.Name.message}}</strong>
|
||||
{{/if}}
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<figcaption>Invalid to begin with</figcaption>
|
||||
<input
|
||||
{{validate
|
||||
validations=validations
|
||||
onchange=(fn (mut this.invalidErrors))
|
||||
}}
|
||||
type="text"
|
||||
value={{"not-valid-text remove-the-space"}}
|
||||
/>
|
||||
{{#if this.invalidErrors.Name}}
|
||||
<br /><strong>{{this.invalidErrors.Name.message}}</strong>
|
||||
{{/if}}
|
||||
</figure>
|
||||
|
||||
{{/let}}
|
||||
```
|
||||
|
||||
## Positional Arguments
|
||||
|
||||
| Argument | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `item` | `object` | | An object containing properties to be validated |
|
||||
|
||||
|
||||
## Named Arguments
|
||||
|
||||
| Argument | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `validations` | `object` | | Validation shaped object to use for the validation |
|
||||
| `onchange` | `object` | | A function called when the validations state changes form successful to erroneous and vice versa |
|
||||
| `chart` | `object` | | A statechart object following a state/dispatch interface to use as an alternative t onchange |
|
|
@ -0,0 +1,27 @@
|
|||
import ApplicationRoute from '../routes/application';
|
||||
|
||||
let isDebugRoute = false;
|
||||
const routeChange = function(transition) {
|
||||
isDebugRoute = transition.to.name.startsWith('docs');
|
||||
};
|
||||
|
||||
export default class DebugRoute extends ApplicationRoute {
|
||||
constructor(owner) {
|
||||
super(...arguments);
|
||||
this.router = owner.lookup('service:router');
|
||||
this.router.on('routeWillChange', routeChange);
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
if (isDebugRoute) {
|
||||
this.render('debug');
|
||||
} else {
|
||||
super.renderTemplate(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
this.router.off('routeWillChange', routeChange);
|
||||
super.willDestroy(...arguments);
|
||||
}
|
||||
}
|
|
@ -15,6 +15,22 @@ export default class BaseRoute extends Route {
|
|||
@service('repository/permission') permissions;
|
||||
@service('router') router;
|
||||
|
||||
_setRouteName() {
|
||||
super._setRouteName(...arguments);
|
||||
const routeName = this.routeName
|
||||
.split('.')
|
||||
.filter(item => item !== 'index')
|
||||
.join('.');
|
||||
const template = get(routes, `${routeName}._options.template`);
|
||||
if(template) {
|
||||
this.templateName = template;
|
||||
}
|
||||
const queryParams = get(routes, `${routeName}._options.queryParams`);
|
||||
if(queryParams && this.routeName === 'dc.partitions.index') {
|
||||
this.queryParams = queryParams;
|
||||
}
|
||||
}
|
||||
|
||||
redirect(model, transition) {
|
||||
// remove any references to index as it is the same as the root routeName
|
||||
const routeName = this.routeName
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import Oauth2CodeProvider from 'torii/providers/oauth2-code';
|
||||
const NAME = 'oidc-with-url';
|
||||
const Provider = Oauth2CodeProvider.extend({
|
||||
name: NAME,
|
||||
buildUrl: function() {
|
||||
import OAuth2CodeProvider from 'torii/providers/oauth2-code';
|
||||
export default class OAuth2CodeWithURLProvider extends OAuth2CodeProvider {
|
||||
|
||||
name = 'oidc-with-url';
|
||||
|
||||
buildUrl() {
|
||||
return this.baseUrl;
|
||||
},
|
||||
open: function(options) {
|
||||
}
|
||||
|
||||
open(options) {
|
||||
const name = this.get('name'),
|
||||
url = this.buildUrl(),
|
||||
responseParams = ['state', 'code'],
|
||||
|
@ -20,18 +22,14 @@ const Provider = Oauth2CodeProvider.extend({
|
|||
provider: name,
|
||||
};
|
||||
});
|
||||
},
|
||||
close: function() {
|
||||
}
|
||||
|
||||
close() {
|
||||
const popup = this.get('popup.remote') || {};
|
||||
if (typeof popup.close === 'function') {
|
||||
return popup.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
export function initialize(application) {
|
||||
application.register(`torii-provider:${NAME}`, Provider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
|
@ -5,6 +5,7 @@ export default class HttpService extends Service {
|
|||
@service('settings') settings;
|
||||
@service('repository/intention') intention;
|
||||
@service('repository/kv') kv;
|
||||
@service('repository/partition') partition;
|
||||
@service('repository/session') session;
|
||||
|
||||
prepare(sink, data, instance) {
|
||||
|
|
|
@ -7,6 +7,7 @@ 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,6 +18,7 @@ const forms = {
|
|||
role: role,
|
||||
intention: intention,
|
||||
nspace: nspace,
|
||||
partition: partition,
|
||||
};
|
||||
|
||||
export default class FormService extends Service {
|
||||
|
|
|
@ -1,39 +1,13 @@
|
|||
import ApplicationRoute from '../routes/application';
|
||||
import { I18nService, formatOptionsSymbol } from './i18n';
|
||||
import I18nService, { formatOptionsSymbol } from 'consul-ui/services/i18n';
|
||||
import ucfirst from 'consul-ui/utils/ucfirst';
|
||||
|
||||
import faker from 'faker';
|
||||
|
||||
let isDebugRoute = false;
|
||||
const routeChange = function(transition) {
|
||||
isDebugRoute = transition.to.name.startsWith('docs');
|
||||
};
|
||||
const DebugRoute = class extends ApplicationRoute {
|
||||
constructor(owner) {
|
||||
super(...arguments);
|
||||
this.router = owner.lookup('service:router');
|
||||
this.router.on('routeWillChange', routeChange);
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
if (isDebugRoute) {
|
||||
this.render('debug');
|
||||
} else {
|
||||
super.renderTemplate(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
this.router.off('routeWillChange', routeChange);
|
||||
super.willDestroy(...arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// we currently use HTML in translations, so anything 'word-like' with these
|
||||
// chars won't get translated
|
||||
const translator = cb => item => (!['<', '>', '='].includes(item) ? cb(item) : item);
|
||||
|
||||
class DebugI18nService extends I18nService {
|
||||
export default class DebugI18nService extends I18nService {
|
||||
formatMessage(value, formatOptions) {
|
||||
const text = super.formatMessage(...arguments);
|
||||
let locale = this.env.var('CONSUL_INTL_LOCALE');
|
||||
|
@ -82,11 +56,4 @@ class DebugI18nService extends I18nService {
|
|||
return formatOptions;
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: 'debug',
|
||||
after: 'i18n',
|
||||
initialize(application) {
|
||||
application.register('route:application', DebugRoute);
|
||||
application.register('service:intl', DebugI18nService);
|
||||
},
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@ import IntlService from 'ember-intl/services/intl';
|
|||
import { inject as service } from '@ember/service';
|
||||
|
||||
export const formatOptionsSymbol = Symbol();
|
||||
export class I18nService extends IntlService {
|
||||
export default class I18nService extends IntlService {
|
||||
@service('env') env;
|
||||
/**
|
||||
* Additionally injects selected project level environment variables into the
|
||||
|
@ -30,9 +30,3 @@ export class I18nService extends IntlService {
|
|||
};
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: 'i18n',
|
||||
initialize: function(container) {
|
||||
container.register('service:intl', I18nService);
|
||||
},
|
||||
};
|
|
@ -6,6 +6,27 @@ import { isChangeset } from 'validated-changeset';
|
|||
import HTTPError from 'consul-ui/utils/http/error';
|
||||
import { ACCESS_READ } from 'consul-ui/abilities/base';
|
||||
|
||||
export const softDelete = (repo, item) => {
|
||||
// Some deletes need to be more of a soft delete.
|
||||
// Therefore the partition still exists once we've requested a delete/removal.
|
||||
// This makes 'removing' more of a custom action rather than a standard
|
||||
// ember-data delete.
|
||||
// Here we use the same request for a delete but we bypass ember-data's
|
||||
// destroyRecord/unloadRecord and serialization so we don't get
|
||||
// ember data error messages when the UI tries to update a 'DeletedAt' property
|
||||
// on an object that ember-data is trying to delete
|
||||
const res = repo.store.adapterFor(repo.getModelName()).rpc(
|
||||
(adapter, request, serialized, unserialized) => {
|
||||
return adapter.requestForDeleteRecord(request, serialized, unserialized);
|
||||
},
|
||||
(serializer, respond, serialized, unserialized) => {
|
||||
return item;
|
||||
},
|
||||
item,
|
||||
repo.getModelName()
|
||||
);
|
||||
return res;
|
||||
}
|
||||
export default class RepositoryService extends Service {
|
||||
@service('store') store;
|
||||
@service('env') env;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { runInDebug } from '@ember/debug';
|
||||
import RepositoryService from 'consul-ui/services/repository';
|
||||
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';
|
||||
|
||||
|
@ -75,25 +75,7 @@ export default class NspaceEnabledService extends RepositoryService {
|
|||
}
|
||||
|
||||
remove(item) {
|
||||
// Namespace deletion is more of a soft delete.
|
||||
// Therefore the namespace still exists once we've requested a delete/removal.
|
||||
// This makes 'removing' more of a custom action rather than a standard
|
||||
// ember-data delete.
|
||||
// Here we use the same request for a delete but we bypass ember-data's
|
||||
// destroyRecord/unloadRecord and serialization so we don't get
|
||||
// ember data error messages when the UI tries to update a 'DeletedAt' property
|
||||
// on an object that ember-data is trying to delete
|
||||
const res = this.store.adapterFor('nspace').rpc(
|
||||
(adapter, request, serialized, unserialized) => {
|
||||
return adapter.requestForDeleteRecord(request, serialized, unserialized);
|
||||
},
|
||||
(serializer, respond, serialized, unserialized) => {
|
||||
return item;
|
||||
},
|
||||
item,
|
||||
'nspace'
|
||||
);
|
||||
return res;
|
||||
return softDelete(this, item);
|
||||
}
|
||||
|
||||
authorize(dc, nspace) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { runInDebug } from '@ember/debug';
|
||||
import RepositoryService from 'consul-ui/services/repository';
|
||||
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';
|
||||
|
||||
|
@ -27,6 +27,7 @@ const findActive = function(items, item) {
|
|||
const MODEL_NAME = 'partition';
|
||||
export default class PartitionRepository extends RepositoryService {
|
||||
@service('settings') settings;
|
||||
@service('form') form;
|
||||
@service('repository/permission') permissions;
|
||||
|
||||
getModelName() {
|
||||
|
@ -49,9 +50,25 @@ export default class PartitionRepository extends RepositoryService {
|
|||
return super.findAll(...arguments).catch(() => []);
|
||||
}
|
||||
|
||||
@dataSource('/:ns/:dc/partition/:id')
|
||||
async findBySlug() {
|
||||
return super.findBySlug(...arguments);
|
||||
@dataSource('/:partition/:ns/:dc/partition/:id')
|
||||
async findBySlug(params) {
|
||||
let item;
|
||||
if (params.id === '') {
|
||||
item = await this.create({
|
||||
Datacenter: params.dc,
|
||||
Partition: '',
|
||||
});
|
||||
} else {
|
||||
item = await super.findBySlug(...arguments);
|
||||
}
|
||||
return this.form
|
||||
.form(this.getModelName())
|
||||
.setData(item)
|
||||
.getData();
|
||||
}
|
||||
|
||||
remove(item) {
|
||||
return softDelete(this, item);
|
||||
}
|
||||
|
||||
async getActive(currentName = '') {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import StateService from 'consul-ui/services/state';
|
||||
|
||||
import validate from 'consul-ui/machines/validate.xstate';
|
||||
|
||||
export default class ChartedStateService extends StateService {
|
||||
stateCharts = {
|
||||
'validate': validate
|
||||
};
|
||||
}
|
||||
|
|
@ -4,14 +4,20 @@ import flat from 'flat';
|
|||
import { createMachine, interpret } from '@xstate/fsm';
|
||||
|
||||
export default class StateService extends Service {
|
||||
@service('logger')
|
||||
logger;
|
||||
|
||||
stateCharts = {};
|
||||
|
||||
@service('logger') logger;
|
||||
|
||||
// @xstate/fsm
|
||||
log(chart, state) {
|
||||
// this.logger.execute(`${chart.id} > ${state.value}`);
|
||||
}
|
||||
|
||||
stateChart(name) {
|
||||
return this.stateCharts[name];
|
||||
}
|
||||
|
||||
addGuards(chart, options) {
|
||||
this.guards(chart).forEach(function([path, name]) {
|
||||
// xstate/fsm has no guard lookup
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default ({ properties }) => key => {
|
||||
return properties(['Name'])(key);
|
||||
};
|
|
@ -13,14 +13,33 @@
|
|||
html.is-debug body > .brand-loader {
|
||||
display: none !important;
|
||||
}
|
||||
html.is-debug [class*='partition-'] {
|
||||
display: block !important;
|
||||
}
|
||||
html:not(.with-data-source) .data-source-debug {
|
||||
display: none;
|
||||
}
|
||||
html:not(.with-data-source) .data-source-debug {
|
||||
display: none;
|
||||
}
|
||||
%debug-box {
|
||||
color: red;
|
||||
background-color: rgb(255 255 255 / 70%);
|
||||
position: absolute;
|
||||
/* hi */
|
||||
z-index: 100000;
|
||||
}
|
||||
html.with-href-to [href^='console://']::before {
|
||||
@extend %p3;
|
||||
@extend %debug-box;
|
||||
content: attr(href);
|
||||
display: inline;
|
||||
}
|
||||
html.with-route-announcer .route-title {
|
||||
@extend %unvisually-hidden;
|
||||
}
|
||||
.data-source-debug {
|
||||
color: red;
|
||||
@extend %debug-box;
|
||||
}
|
||||
.data-source-debug input:checked + pre code::after {
|
||||
content: attr(data-json);
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
'use strict';
|
||||
const path = require('path');
|
||||
const exists = require('fs').existsSync;
|
||||
|
||||
const Funnel = require('broccoli-funnel');
|
||||
const mergeTrees = require('broccoli-merge-trees');
|
||||
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
|
||||
const utils = require('./config/utils');
|
||||
|
||||
// const BroccoliDebug = require('broccoli-debug');
|
||||
// const debug = BroccoliDebug.buildDebugCallback(`app:consul-ui`)
|
||||
|
||||
module.exports = function(defaults, $ = process.env) {
|
||||
// available environments
|
||||
// ['production', 'development', 'staging', 'test'];
|
||||
|
@ -10,6 +17,7 @@ module.exports = function(defaults, $ = process.env) {
|
|||
$ = utils.env($);
|
||||
const env = EmberApp.env();
|
||||
const prodlike = ['production', 'staging'];
|
||||
const devlike = ['development', 'staging'];
|
||||
const sourcemaps = !['production'].includes(env) && !$('BABEL_DISABLE_SOURCEMAPS', false);
|
||||
|
||||
const trees = {};
|
||||
|
@ -17,6 +25,16 @@ module.exports = function(defaults, $ = process.env) {
|
|||
const outputPaths = {};
|
||||
let excludeFiles = [];
|
||||
|
||||
const apps = [
|
||||
'consul-acls',
|
||||
'consul-partitions'
|
||||
].map(item => {
|
||||
return {
|
||||
name: item,
|
||||
path: path.dirname(require.resolve(`${item}/package.json`))
|
||||
};
|
||||
});
|
||||
|
||||
const babel = {
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
|
@ -29,6 +47,7 @@ module.exports = function(defaults, $ = process.env) {
|
|||
// exclude any component/pageobject.js files from anything but test
|
||||
excludeFiles = excludeFiles.concat([
|
||||
'components/**/pageobject.js',
|
||||
'components/**/test-support.js',
|
||||
'components/**/*.test-support.js',
|
||||
'components/**/*.test.js',
|
||||
])
|
||||
|
@ -38,6 +57,8 @@ module.exports = function(defaults, $ = process.env) {
|
|||
// exclude our debug initializer, route and template
|
||||
excludeFiles = excludeFiles.concat([
|
||||
'instance-initializers/debug.js',
|
||||
'routing/**/*-debug.js',
|
||||
'services/**/*-debug.js',
|
||||
'templates/debug.hbs',
|
||||
'components/debug/**/*.*'
|
||||
])
|
||||
|
@ -61,11 +82,21 @@ module.exports = function(defaults, $ = process.env) {
|
|||
['strip-function-call', {'strip': ['Ember.runInDebug']}]
|
||||
)
|
||||
}
|
||||
//
|
||||
|
||||
trees.app = new Funnel('app', {
|
||||
exclude: excludeFiles
|
||||
//
|
||||
trees.app = mergeTrees([
|
||||
new Funnel('app', { exclude: excludeFiles })
|
||||
].concat(
|
||||
apps.filter(item => exists(`${item.path}/app`)).map(item => new Funnel(`${item.path}/app`, {exclude: excludeFiles}))
|
||||
), {
|
||||
overwrite: true
|
||||
});
|
||||
trees.vendor = mergeTrees([
|
||||
new Funnel('vendor'),
|
||||
].concat(
|
||||
apps.map(item => new Funnel(`${item.path}/vendor`))
|
||||
));
|
||||
//
|
||||
|
||||
const app = new EmberApp(
|
||||
Object.assign({}, defaults, {
|
||||
|
@ -112,6 +143,16 @@ module.exports = function(defaults, $ = process.env) {
|
|||
},
|
||||
}
|
||||
);
|
||||
apps.forEach(item => {
|
||||
app.import(`vendor/${item.name}/routes.js`, {
|
||||
outputFile: `assets/${item.name}/routes.js`,
|
||||
});
|
||||
});
|
||||
['consul-ui/services'].concat(devlike ? ['consul-ui/services-debug'] : []).forEach(item => {
|
||||
app.import(`vendor/${item}.js`, {
|
||||
outputFile: `assets/${item}.js`,
|
||||
});
|
||||
});
|
||||
// Use `app.import` to add additional libraries to the generated
|
||||
// output files.
|
||||
//
|
||||
|
@ -163,9 +204,6 @@ module.exports = function(defaults, $ = process.env) {
|
|||
app.import('vendor/metrics-providers/prometheus.js', {
|
||||
outputFile: 'assets/metrics-providers/prometheus.js',
|
||||
});
|
||||
app.import('vendor/acls/routes.js', {
|
||||
outputFile: 'assets/acls/routes.js',
|
||||
});
|
||||
app.import('vendor/init.js', {
|
||||
outputFile: 'assets/init.js',
|
||||
});
|
||||
|
|
|
@ -41,23 +41,45 @@ ${environment === 'production' ? `{{jsonEncode .}}` : JSON.stringify(config.oper
|
|||
"codemirror/mode/yaml/yaml.js": "${rootURL}assets/codemirror/mode/yaml/yaml.js"
|
||||
}
|
||||
</script>
|
||||
<script data-app-name="${appName}" data-${appName}-services src="${rootURL}assets/consul-ui/services.js"></script>
|
||||
${
|
||||
environment === 'development' || environment === 'staging'
|
||||
? `
|
||||
<script data-app-name="${appName}" data-${appName}-services src="${rootURL}assets/consul-ui/services-debug.js"></script>
|
||||
` : ``}
|
||||
${
|
||||
environment === 'production'
|
||||
? `
|
||||
{{if .ACLsEnabled}}
|
||||
<script data-${appName}-routing src="${rootURL}assets/acls/routes.js"></script>
|
||||
<script data-app-name="${appName}" data-${appName}-routing src="${rootURL}assets/consul-acls/routes.js"></script>
|
||||
{{end}}
|
||||
{{if .PartitionsEnabled}}
|
||||
<script data-app-name="${appName}" data-${appName}-routing src="${rootURL}assets/consul-partitions/routes.js"></script>
|
||||
{{end}}
|
||||
`
|
||||
: `
|
||||
<script>
|
||||
if(document.cookie['CONSUL_ACLS_ENABLED']) {
|
||||
const appName = '${appName}';
|
||||
const appNameJS = appName.split('-').map((item, i) => i ? \`\${item.substr(0, 1).toUpperCase()}\${item.substr(1)}\` : item).join('');
|
||||
const $script = document.createElement('script');
|
||||
$script.setAttribute('src', '${rootURL}assets/acls/routes.js');
|
||||
$script.dataset[\`\${appNameJS}Routes\`] = null;
|
||||
document.body.appendChild($script);
|
||||
(
|
||||
function(get, obj) {
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
if(get(key)) {
|
||||
const appName = '${appName}';
|
||||
const appNameJS = appName.split('-').map((item, i) => i ? \`\${item.substr(0, 1).toUpperCase()}\${item.substr(1)}\` : item).join('');
|
||||
const $script = document.createElement('script');
|
||||
$script.setAttribute('data-app-name', '${appName}');
|
||||
$script.setAttribute('data-${appName}-routing', '');
|
||||
$script.setAttribute('src', \`${rootURL}assets/\${value}/routes.js\`);
|
||||
document.body.appendChild($script);
|
||||
}
|
||||
});
|
||||
}
|
||||
)(
|
||||
key => document.cookie.split('; ').find(item => item.startsWith(\`\${key}=\`)),
|
||||
{
|
||||
'CONSUL_ACLS_ENABLE': 'consul-acls',
|
||||
'CONSUL_PARTITIONS_ENABLE': 'consul-partitions'
|
||||
}
|
||||
);
|
||||
</script>
|
||||
`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"Name": "${location.pathname.get(2)}",
|
||||
"Description": "${fake.lorem.sentence()}",
|
||||
"CreateIndex": 12,
|
||||
"ModifyIndex": 16
|
||||
}
|
|
@ -74,10 +74,13 @@
|
|||
"babel-plugin-strip-function-call": "^1.0.2",
|
||||
"base64-js": "^1.3.0",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"broccoli-debug": "^0.6.5",
|
||||
"broccoli-funnel": "^3.0.3",
|
||||
"broccoli-merge-trees": "^4.2.0",
|
||||
"chalk": "^4.1.0",
|
||||
"clipboard": "^2.0.4",
|
||||
"consul-acls": "*",
|
||||
"consul-partitions": "*",
|
||||
"css.escape": "^1.5.1",
|
||||
"d3-array": "^2.8.0",
|
||||
"d3-scale": "^3.2.3",
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
(function(appNameJS = 'consulUi', doc = document) {
|
||||
const scripts = doc.getElementsByTagName('script');
|
||||
const script = scripts[scripts.length - 1];
|
||||
script.dataset[`${appNameJS}Routes`] = JSON.stringify({
|
||||
dc: {
|
||||
acls: {
|
||||
tokens: {
|
||||
_options: {
|
||||
abilities: ['read tokens'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,15 @@
|
|||
(services => services({
|
||||
"route:application": {
|
||||
"class": "consul-ui/routing/application-debug"
|
||||
},
|
||||
"service:intl": {
|
||||
"class": "consul-ui/services/i18n-debug"
|
||||
}
|
||||
}))(
|
||||
(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}Services`] = JSON.stringify(json);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,21 @@
|
|||
(services => services({
|
||||
"route:basic": {
|
||||
"class": "consul-ui/routing/route"
|
||||
},
|
||||
"service:intl": {
|
||||
"class": "consul-ui/services/i18n"
|
||||
},
|
||||
"service:state": {
|
||||
"class": "consul-ui/services/state-with-charts"
|
||||
},
|
||||
"auth-provider:oidc-with-url": {
|
||||
"class": "consul-ui/services/auth-providers/oauth2-code-with-url-provider"
|
||||
}
|
||||
}))(
|
||||
(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}Services`] = JSON.stringify(json);
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue