diff --git a/.circleci/config.yml b/.circleci/config.yml
index 54798f322a66..d7b74955c5e0 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -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
diff --git a/ui/package.json b/ui/package.json
index 4c22878ed202..06b57506c0da 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -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": {
diff --git a/ui/packages/consul-acls/package.json b/ui/packages/consul-acls/package.json
new file mode 100644
index 000000000000..b2a513fb8235
--- /dev/null
+++ b/ui/packages/consul-acls/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "consul-acls",
+ "version": "0.1.0",
+ "private": true
+}
diff --git a/ui/packages/consul-acls/vendor/consul-acls/routes.js b/ui/packages/consul-acls/vendor/consul-acls/routes.js
new file mode 100644
index 000000000000..73696c2dde50
--- /dev/null
+++ b/ui/packages/consul-acls/vendor/consul-acls/routes.js
@@ -0,0 +1,16 @@
+(function(data) {
+ const appNameJS = data.appName.split('-')
+ .map((item, i) => i ? `${item.substr(0, 1).toUpperCase()}${item.substr(1)}` : item)
+ .join('');
+ data[`${appNameJS}Routes`] = JSON.stringify({
+ dc: {
+ acls: {
+ tokens: {
+ _options: {
+ abilities: ['read tokens'],
+ },
+ },
+ },
+ },
+ });
+})(document.currentScript.dataset);
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/form/README.mdx b/ui/packages/consul-partitions/app/components/consul/partition/form/README.mdx
new file mode 100644
index 000000000000..6f72a5cdef01
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/form/README.mdx
@@ -0,0 +1,24 @@
+# Consul::Partition::Form
+
+```hbs preview-template
+
+
+
+
+
+```
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/form/index.hbs b/ui/packages/consul-partitions/app/components/consul/partition/form/index.hbs
new file mode 100644
index 000000000000..7795f03c1f62
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/form/index.hbs
@@ -0,0 +1,126 @@
+
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/list/README.mdx b/ui/packages/consul-partitions/app/components/consul/partition/list/README.mdx
new file mode 100644
index 000000000000..326465ee9031
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/list/README.mdx
@@ -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
+
+
+
+```
+
+
+### 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)
+
+---
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/list/index.hbs b/ui/packages/consul-partitions/app/components/consul/partition/list/index.hbs
new file mode 100644
index 000000000000..1dcd6023e439
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/list/index.hbs
@@ -0,0 +1,63 @@
+
+
+{{#if item.DeletedAt}}
+
+ Deleting {{item.Name}}...
+
+{{else}}
+ {{item.Name}}
+{{/if}}
+
+
+{{#if item.Description}}
+
+ Description
+
+ {{item.Description}}
+
+
+{{/if}}
+
+
+{{#if (not item.DeletedAt)}}
+
+
+
+{{#if (can "write partition" item=item)}}
+ Edit
+{{else}}
+ View
+{{/if}}
+
+
+ {{#if (can "delete partition" item=item)}}
+
+
+ Delete
+
+
+
+
+ Confirm delete
+
+
+
+ Are you sure you want to delete this partition?
+
+
+
+ Delete
+
+
+
+
+ {{/if}}
+
+{{/if}}
+
+
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/list/test-support.js b/ui/packages/consul-partitions/app/components/consul/partition/list/test-support.js
new file mode 100644
index 000000000000..c6ec1c45214a
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/list/test-support.js
@@ -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']),
+ });
+};
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/notifications/README.mdx b/ui/packages/consul-partitions/app/components/consul/partition/notifications/README.mdx
new file mode 100644
index 000000000000..e84187b9d0f0
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/notifications/README.mdx
@@ -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
+
+ Provide a widget to change the @type
+
+
+ create
+ update
+ delete
+
+
+
+ Provide a widget to change the @status
+
+
+ success
+ error
+
+
+
+ Show the notification text
+
+
+
+
+
+```
+
+
+
+## See
+
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/notifications/index.hbs b/ui/packages/consul-partitions/app/components/consul/partition/notifications/index.hbs
new file mode 100644
index 000000000000..14eb0c41a7d0
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/notifications/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 }}
+ {{concat '(' (if error.status (concat error.status ': ')) error.detail ')'}}
+ {{/if}}
+{{/let}}
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/search-bar/README.mdx b/ui/packages/consul-partitions/app/components/consul/partition/search-bar/README.mdx
new file mode 100644
index 000000000000..484b1116c8ce
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/search-bar/README.mdx
@@ -0,0 +1,30 @@
+# Consul::Partition::SearchBar
+
+Searchbar tailored for searching Partitions. Follows our more generic
+'*::SearchBar' component interface.
+
+```hbs preview-template
+
+```
+
+## See
+
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/search-bar/index.hbs b/ui/packages/consul-partitions/app/components/consul/partition/search-bar/index.hbs
new file mode 100644
index 000000000000..1d2999c9b4a9
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/search-bar/index.hbs
@@ -0,0 +1,98 @@
+
+ <: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|}}
+
+
+ {{key}}
+ {{value}}
+
+
+{{/let}}
+
+
+ <:search as |search|>
+
+
+
+
+ {{t "common.search.searchproperty"}}
+
+
+
+ {{#let components.Optgroup components.Option as |Optgroup Option|}}
+ {{#each @filter.searchproperty.default as |prop|}}
+
+ {{t (concat "common.consul." (lowercase prop))}}
+
+ {{/each}}
+ {{/let}}
+
+
+
+
+ <:sort as |search|>
+
+
+
+ {{#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}}
+
+
+
+ {{#let components.Optgroup components.Option as |Optgroup Option|}}
+
+ {{t "common.sort.alpha.asc"}}
+ {{t "common.sort.alpha.desc"}}
+
+ {{/let}}
+
+
+
+
\ No newline at end of file
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/selector/README.mdx b/ui/packages/consul-partitions/app/components/consul/partition/selector/README.mdx
new file mode 100644
index 000000000000..f802edb1d952
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/selector/README.mdx
@@ -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 `` element.
+- For the moment, make sure you have enabled partitions using developer debug
+ cookies.
+
+```hbs preview-template
+
+```
+
+
+## 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)
+
+---
diff --git a/ui/packages/consul-partitions/app/components/consul/partition/selector/index.hbs b/ui/packages/consul-partitions/app/components/consul/partition/selector/index.hbs
new file mode 100644
index 000000000000..078f360359d1
--- /dev/null
+++ b/ui/packages/consul-partitions/app/components/consul/partition/selector/index.hbs
@@ -0,0 +1,53 @@
+{{#if (can "choose partitions")}}
+
+
+
+ {{@partition}}
+
+
+ {{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}}
+
+ {{#each (reject-by 'DeletedAt' @partitions) as |item|}}
+
+
+ {{item.Name}}
+
+
+ {{/each}}
+ {{#if (can 'manage partitions')}}
+
+
+
+ Manage Admin Partitions
+
+
+ {{/if}}
+ {{/let}}
+
+
+
+{{/if}}
+
diff --git a/ui/packages/consul-partitions/app/templates/dc/partitions/edit.hbs b/ui/packages/consul-partitions/app/templates/dc/partitions/edit.hbs
new file mode 100644
index 000000000000..019ca28bfc2e
--- /dev/null
+++ b/ui/packages/consul-partitions/app/templates/dc/partitions/edit.hbs
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+{{#let
+
+ route.params.dc
+ route.params.partition
+ route.params.nspace
+
+ loader.data
+ loader.data.isNew
+as |dc partition nspace item create|}}
+
+
+
+
+
+
+ All Partitions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{/let}}
+
+
+
diff --git a/ui/packages/consul-partitions/app/templates/dc/partitions/index.hbs b/ui/packages/consul-partitions/app/templates/dc/partitions/index.hbs
new file mode 100644
index 000000000000..cf905bfd5d4d
--- /dev/null
+++ b/ui/packages/consul-partitions/app/templates/dc/partitions/index.hbs
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+ {{#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|}}
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+ {{#if (gt items.length 0)}}
+
+ {{/if}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{#if (gt items.length 0)}}
+ No partitions found
+ {{else}}
+ Welcome to Partitions
+ {{/if}}
+
+
+
+
+ {{#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}}
+
+
+
+
+ Documentation on partitions
+
+
+ Read the guide
+
+
+
+
+
+
+
+
+
+ {{/let}}
+
+
+
diff --git a/ui/packages/consul-partitions/package.json b/ui/packages/consul-partitions/package.json
new file mode 100644
index 000000000000..c10a455e5bfa
--- /dev/null
+++ b/ui/packages/consul-partitions/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "consul-partitions",
+ "version": "0.1.0",
+ "private": true
+}
diff --git a/ui/packages/consul-partitions/vendor/consul-partitions/routes.js b/ui/packages/consul-partitions/vendor/consul-partitions/routes.js
new file mode 100644
index 000000000000..d84e1768e93d
--- /dev/null
+++ b/ui/packages/consul-partitions/vendor/consul-partitions/routes.js
@@ -0,0 +1,36 @@
+(function(data) {
+ const appNameJS = data.appName.split('-')
+ .map((item, i) => i ? `${item.substr(0, 1).toUpperCase()}${item.substr(1)}` : item)
+ .join('');
+ data[`${appNameJS}Routes`] = JSON.stringify({
+ 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'],
+ },
+ },
+ },
+ },
+ });
+})(document.currentScript.dataset);
diff --git a/ui/packages/consul-ui/.docfy-config.js b/ui/packages/consul-ui/.docfy-config.js
index cf6d67d01267..a3970301ca3c 100644
--- a/ui/packages/consul-ui/.docfy-config.js
+++ b/ui/packages/consul-ui/.docfy-config.js
@@ -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: {
diff --git a/ui/packages/consul-ui/app/abilities/base.js b/ui/packages/consul-ui/app/abilities/base.js
index 58254c6f1cac..8294273a2d70 100644
--- a/ui/packages/consul-ui/app/abilities/base.js
+++ b/ui/packages/consul-ui/app/abilities/base.js
@@ -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') {
diff --git a/ui/packages/consul-ui/app/abilities/nspace.js b/ui/packages/consul-ui/app/abilities/nspace.js
index 0271a5b3e559..befbefe29281 100644
--- a/ui/packages/consul-ui/app/abilities/nspace.js
+++ b/ui/packages/consul-ui/app/abilities/nspace.js
@@ -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;
}
diff --git a/ui/packages/consul-ui/app/abilities/partition.js b/ui/packages/consul-ui/app/abilities/partition.js
index aea6ccfffc85..034907ab3dfd 100644
--- a/ui/packages/consul-ui/app/abilities/partition.js
+++ b/ui/packages/consul-ui/app/abilities/partition.js
@@ -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;
}
diff --git a/ui/packages/consul-ui/app/abilities/service.js b/ui/packages/consul-ui/app/abilities/service.js
index 4c78ab0b306f..5e806bd6f76d 100644
--- a/ui/packages/consul-ui/app/abilities/service.js
+++ b/ui/packages/consul-ui/app/abilities/service.js
@@ -2,4 +2,8 @@ import BaseAbility from './base';
export default class ServiceAbility extends BaseAbility {
resource = 'service';
+
+ get isLinkable() {
+ return this.item.InstanceCount > 0;
+ }
}
diff --git a/ui/packages/consul-ui/app/abilities/upstream.js b/ui/packages/consul-ui/app/abilities/upstream.js
new file mode 100644
index 000000000000..da1991c8e15c
--- /dev/null
+++ b/ui/packages/consul-ui/app/abilities/upstream.js
@@ -0,0 +1,9 @@
+import BaseAbility from './base';
+
+export default class UpstreamAbility extends BaseAbility {
+ resource = 'upstream';
+
+ get isLinkable() {
+ return this.item.InstanceCount > 0;
+ }
+}
diff --git a/ui/packages/consul-ui/app/adapters/partition.js b/ui/packages/consul-ui/app/adapters/partition.js
index bbcdd3a72889..7c056c67ca48 100644
--- a/ui/packages/consul-ui/app/adapters/partition.js
+++ b/ui/packages/consul-ui/app/adapters/partition.js
@@ -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 {
@@ -25,4 +26,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,
+ }}
+ `;
+ }
}
diff --git a/ui/packages/consul-ui/app/components/consul/nspace/list/index.hbs b/ui/packages/consul-ui/app/components/consul/nspace/list/index.hbs
index a309d64d9d23..8221de231155 100644
--- a/ui/packages/consul-ui/app/components/consul/nspace/list/index.hbs
+++ b/ui/packages/consul-ui/app/components/consul/nspace/list/index.hbs
@@ -2,7 +2,7 @@
class="consul-nspace-list"
...attributes
@items={{@items}}
- @linkable={{action this.isLinkable}}
+ @linkable="linkable nspace"
as |item|>
{{#if item.DeletedAt}}
diff --git a/ui/packages/consul-ui/app/components/consul/nspace/list/index.js b/ui/packages/consul-ui/app/components/consul/nspace/list/index.js
deleted file mode 100644
index e00c5b8a0b2a..000000000000
--- a/ui/packages/consul-ui/app/components/consul/nspace/list/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Component from '@glimmer/component';
-
-export default class ConsulNspaceList extends Component {
- isLinkable(item) {
- return !item.DeletedAt;
- }
-}
diff --git a/ui/packages/consul-ui/app/components/consul/service/list/index.hbs b/ui/packages/consul-ui/app/components/consul/service/list/index.hbs
index 0cef5caf1a3e..fd5eaa5688bd 100644
--- a/ui/packages/consul-ui/app/components/consul/service/list/index.hbs
+++ b/ui/packages/consul-ui/app/components/consul/service/list/index.hbs
@@ -2,7 +2,7 @@
class="consul-service-list"
...attributes
@items={{@items}}
- @linkable={{action this.isLinkable}}
+ @linkable="linkable service"
as |item index|
>
diff --git a/ui/packages/consul-ui/app/components/consul/service/list/index.js b/ui/packages/consul-ui/app/components/consul/service/list/index.js
deleted file mode 100644
index b90fa5ee416b..000000000000
--- a/ui/packages/consul-ui/app/components/consul/service/list/index.js
+++ /dev/null
@@ -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;
- }
-}
diff --git a/ui/packages/consul-ui/app/components/consul/upstream/list/index.hbs b/ui/packages/consul-ui/app/components/consul/upstream/list/index.hbs
index 8ce4b213e55e..14ba0027fc4e 100644
--- a/ui/packages/consul-ui/app/components/consul/upstream/list/index.hbs
+++ b/ui/packages/consul-ui/app/components/consul/upstream/list/index.hbs
@@ -2,7 +2,7 @@
class="consul-upstream-list"
...attributes
@items={{@items}}
- @linkable={{action this.isLinkable}}
+ @linkable="linkable upstream"
as |item index|>
{{#if (gt item.InstanceCount 0)}}
diff --git a/ui/packages/consul-ui/app/components/consul/upstream/list/index.js b/ui/packages/consul-ui/app/components/consul/upstream/list/index.js
deleted file mode 100644
index b90fa5ee416b..000000000000
--- a/ui/packages/consul-ui/app/components/consul/upstream/list/index.js
+++ /dev/null
@@ -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;
- }
-}
diff --git a/ui/packages/consul-ui/app/components/data-form/index.js b/ui/packages/consul-ui/app/components/data-form/index.js
index b0ce2eced666..b7db7af669a1 100644
--- a/ui/packages/consul-ui/app/components/data-form/index.js
+++ b/ui/packages/consul-ui/app/components/data-form/index.js
@@ -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
diff --git a/ui/packages/consul-ui/app/components/data-writer/index.hbs b/ui/packages/consul-ui/app/components/data-writer/index.hbs
index 2120f5e4a24b..61fbb07998d8 100644
--- a/ui/packages/consul-ui/app/components/data-writer/index.hbs
+++ b/ui/packages/consul-ui/app/components/data-writer/index.hbs
@@ -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}}
diff --git a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs
index 83ad29ad9569..8919db05a49e 100644
--- a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs
+++ b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs
@@ -48,61 +48,13 @@
-
- {{#if (can "choose partitions")}}
-
-
-
- {{@partition}}
-
-
- {{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}}
-
- {{!FIXME: Do partitions do the same as namespace deletion? }}
- {{#each (reject-by 'DeletedAt' this.partitions) as |item|}}
-
-
- {{item.Name}}
-
-
- {{/each}}
- {{#if (and false (can 'manage partitions'))}}
-
-
-
- Manage Admin Partitions
-
-
- {{/if}}
- {{/let}}
-
-
-
- {{/if}}
-
+
{{#if (can "choose nspaces")}}
{{yield cell.item cell.index}}
@@ -49,10 +46,7 @@
{{yield item index}}
diff --git a/ui/packages/consul-ui/app/forms/partition.js b/ui/packages/consul-ui/app/forms/partition.js
new file mode 100644
index 000000000000..eac1aebf49fe
--- /dev/null
+++ b/ui/packages/consul-ui/app/forms/partition.js
@@ -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);
+}
diff --git a/ui/packages/consul-ui/app/locations/fsm-with-optional.js b/ui/packages/consul-ui/app/locations/fsm-with-optional.js
index 8027b65c1884..5d5eeb19bda5 100644
--- a/ui/packages/consul-ui/app/locations/fsm-with-optional.js
+++ b/ui/packages/consul-ui/app/locations/fsm-with-optional.js
@@ -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
diff --git a/ui/packages/consul-ui/app/models/partition.js b/ui/packages/consul-ui/app/models/partition.js
index 5a9385794882..6e2ac8533576 100644
--- a/ui/packages/consul-ui/app/models/partition.js
+++ b/ui/packages/consul-ui/app/models/partition.js
@@ -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 ""
diff --git a/ui/packages/consul-ui/app/routing/route.js b/ui/packages/consul-ui/app/routing/route.js
index 9ec09a47beb2..ed3cdb256f8a 100644
--- a/ui/packages/consul-ui/app/routing/route.js
+++ b/ui/packages/consul-ui/app/routing/route.js
@@ -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
diff --git a/ui/packages/consul-ui/app/services/data-sink/protocols/http.js b/ui/packages/consul-ui/app/services/data-sink/protocols/http.js
index 724b686a949f..e43f04cc496a 100644
--- a/ui/packages/consul-ui/app/services/data-sink/protocols/http.js
+++ b/ui/packages/consul-ui/app/services/data-sink/protocols/http.js
@@ -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) {
diff --git a/ui/packages/consul-ui/app/services/form.js b/ui/packages/consul-ui/app/services/form.js
index 80cff14f3c38..faec09a6e84e 100644
--- a/ui/packages/consul-ui/app/services/form.js
+++ b/ui/packages/consul-ui/app/services/form.js
@@ -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 {
diff --git a/ui/packages/consul-ui/app/services/repository.js b/ui/packages/consul-ui/app/services/repository.js
index 4758c7f1dfb6..47e094f9db06 100644
--- a/ui/packages/consul-ui/app/services/repository.js
+++ b/ui/packages/consul-ui/app/services/repository.js
@@ -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('repository/permission') permissions;
diff --git a/ui/packages/consul-ui/app/services/repository/nspace.js b/ui/packages/consul-ui/app/services/repository/nspace.js
index 472808416807..0327c4810956 100644
--- a/ui/packages/consul-ui/app/services/repository/nspace.js
+++ b/ui/packages/consul-ui/app/services/repository/nspace.js
@@ -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) {
diff --git a/ui/packages/consul-ui/app/services/repository/partition.js b/ui/packages/consul-ui/app/services/repository/partition.js
index 2832466e5ca0..002db6f385dd 100644
--- a/ui/packages/consul-ui/app/services/repository/partition.js
+++ b/ui/packages/consul-ui/app/services/repository/partition.js
@@ -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 = '') {
diff --git a/ui/packages/consul-ui/app/sort/comparators/partition.js b/ui/packages/consul-ui/app/sort/comparators/partition.js
new file mode 100644
index 000000000000..1ec2b1a99c71
--- /dev/null
+++ b/ui/packages/consul-ui/app/sort/comparators/partition.js
@@ -0,0 +1,3 @@
+export default ({ properties }) => key => {
+ return properties(['Name'])(key);
+};
diff --git a/ui/packages/consul-ui/app/styles/debug.scss b/ui/packages/consul-ui/app/styles/debug.scss
index fb1a9e18c5a7..13182829c878 100644
--- a/ui/packages/consul-ui/app/styles/debug.scss
+++ b/ui/packages/consul-ui/app/styles/debug.scss
@@ -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);
diff --git a/ui/packages/consul-ui/ember-cli-build.js b/ui/packages/consul-ui/ember-cli-build.js
index 0ede17347c9b..8df8900be733 100644
--- a/ui/packages/consul-ui/ember-cli-build.js
+++ b/ui/packages/consul-ui/ember-cli-build.js
@@ -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'];
@@ -17,6 +24,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',
@@ -61,11 +78,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`))
+ ), {
+ 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 +139,11 @@ module.exports = function(defaults, $ = process.env) {
},
}
);
+ apps.forEach(item => {
+ app.import(`vendor/${item.name}/routes.js`, {
+ outputFile: `assets/${item.name}/routes.js`,
+ });
+ });
// Use `app.import` to add additional libraries to the generated
// output files.
//
@@ -163,9 +195,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',
});
diff --git a/ui/packages/consul-ui/lib/startup/templates/body.html.js b/ui/packages/consul-ui/lib/startup/templates/body.html.js
index cafa6fb7c68b..36d664c26bf0 100644
--- a/ui/packages/consul-ui/lib/startup/templates/body.html.js
+++ b/ui/packages/consul-ui/lib/startup/templates/body.html.js
@@ -45,19 +45,35 @@ ${
environment === 'production'
? `
{{if .ACLsEnabled}}
-
+
+{{end}}
+{{if .PartitionsEnabled}}
+
{{end}}
`
: `
`
}
diff --git a/ui/packages/consul-ui/mock-api/v1/partition/_ b/ui/packages/consul-ui/mock-api/v1/partition/_
new file mode 100644
index 000000000000..f8ad7132c931
--- /dev/null
+++ b/ui/packages/consul-ui/mock-api/v1/partition/_
@@ -0,0 +1,6 @@
+{
+ "Name": "${location.pathname.get(2)}",
+ "Description": "${fake.lorem.sentence()}",
+ "CreateIndex": 12,
+ "ModifyIndex": 16
+}
diff --git a/ui/packages/consul-ui/package.json b/ui/packages/consul-ui/package.json
index a4e9383c36bc..5794a92f3a3a 100644
--- a/ui/packages/consul-ui/package.json
+++ b/ui/packages/consul-ui/package.json
@@ -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",
diff --git a/ui/packages/consul-ui/vendor/acls/routes.js b/ui/packages/consul-ui/vendor/acls/routes.js
deleted file mode 100644
index b553c9e93d31..000000000000
--- a/ui/packages/consul-ui/vendor/acls/routes.js
+++ /dev/null
@@ -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'],
- },
- },
- },
- },
- });
-})();