diff --git a/ui/packages/consul-ui/app/components/consul/peer/components.scss b/ui/packages/consul-ui/app/components/consul/peer/components.scss new file mode 100644 index 000000000..d4fdbe081 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/peer/components.scss @@ -0,0 +1,76 @@ +%pill-pending::before, +%pill-establishing::before, +%pill-active::before, +%pill-failing::before, +%pill-terminated::before, +%pill-deleting::before { + --icon-size: icon-000; + content: ''; +} +%pill-pending, +%pill-establishing, +%pill-active, +%pill-failing, +%pill-terminated, +%pill-deleting { + font-weight: var(--typo-weight-medium); + font-size: var(--typo-size-700); +} + +%pill-pending::before { + --icon-name: icon-running; + --icon-color: rgb(var(--tone-gray-800)); +} +%pill-pending { + background-color: rgb(var(--tone-strawberry-050)); + color: rgb(var(--tone-strawberry-500)); +} + +%pill-establishing::before { + --icon-name: icon-running; + --icon-color: rgb(var(--tone-gray-800)); +} +%pill-establishing { + background-color: rgb(var(--tone-blue-050)); + color: rgb(var(--tone-blue-500)); +} + +%pill-active::before { + --icon-name: icon-check; + --icon-color: rgb(var(--tone-green-800)); +} +%pill-active { + background-color: rgb(var(--tone-green-050)); + color: rgb(var(--tone-green-600)); +} + +%pill-failing::before { + --icon-name: icon-x; + --icon-color: rgb(var(--tone-red-500)); +} +%pill-failing { + background-color: rgb(var(--tone-red-050)); + color: rgb(var(--tone-red-500)); +} + +%pill-terminated::before { + --icon-name: icon-x-square; + --icon-color: rgb(var(--tone-gray-800)); +} +%pill-terminated { + background-color: rgb(var(--tone-gray-150)); + color: rgb(var(--tone-gray-800)); +} + + +%pill-deleting::before { + --icon-name: icon-loading; + --icon-color: rgb(var(--tone-green-800)); +} +%pill-deleting { + background-color: rgb(var(--tone-yellow-050)); + color: rgb(var(--tone-yellow-800)); +} + + + diff --git a/ui/packages/consul-ui/app/components/consul/peer/index.scss b/ui/packages/consul-ui/app/components/consul/peer/index.scss new file mode 100644 index 000000000..9fdaee0ec --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/peer/index.scss @@ -0,0 +1,4 @@ +@import './components'; + +@import './search-bar'; + diff --git a/ui/packages/consul-ui/app/components/consul/peer/list/README.mdx b/ui/packages/consul-ui/app/components/consul/peer/list/README.mdx new file mode 100644 index 000000000..f3a4694e5 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/peer/list/README.mdx @@ -0,0 +1,26 @@ +# Consul::Peer::List + +A presentational component for rendering Consul Peers + +```hbs preview-template + + + +``` + + +## Arguments + +| Argument/Attribute | Type | Default | Description | +| --- | --- | --- | --- | +| `items` | `array` | | An array of Peers | +| `ondelete` | `function` | | An action to execute when the `Delete` action is clicked | + +## See + +- [Template Source Code](./index.hbs) + +--- diff --git a/ui/packages/consul-ui/app/components/consul/peer/list/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/list/index.hbs new file mode 100644 index 000000000..249c5a976 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/peer/list/index.hbs @@ -0,0 +1,93 @@ + + +{{#if (can 'delete peer' item=item)}} + + {{item.Name}} + +{{else}} +

+ {{item.Name}} +

+{{/if}} +
+ +
+ + +
+ {{t 'routes.dc.peers.index.detail.imported.count' + count=(format-number item.ImportedServiceCount) + }} +
+ +
+ {{t 'routes.dc.peers.index.detail.exported.count' + count=(format-number item.ExportedServiceCount) + }} +
+ +
+
+ +{{#if (can 'delete peer' item=item)}} + + + + + View + + + + + Delete + + + + + Confirm delete + + +

+ Are you sure you want to delete this peer? +

+
+ + + Delete + + +
+
+
+
+{{/if}} +
+
+ diff --git a/ui/packages/consul-ui/app/components/consul/peer/notifications/README.mdx b/ui/packages/consul-ui/app/components/consul/peer/notifications/README.mdx new file mode 100644 index 000000000..a4a9fcc6d --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/peer/notifications/README.mdx @@ -0,0 +1,19 @@ +# Consul::Peer::Notifications + +A Notification component specifically for Peers. This is only a component as we currently use this in two places and if we need to add more types we can do so in one place. + +We currently one have one 'remove' type due to the fact that Peers can't use the default 'delete' notification as they get 'marked for deletion' instead. + +```hbs preview-template + +``` + + + +## See + +- [Template Source Code](./index.hbs) + +--- diff --git a/ui/packages/consul-ui/app/components/consul/peer/notifications/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/notifications/index.hbs new file mode 100644 index 000000000..972c5c10e --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/peer/notifications/index.hbs @@ -0,0 +1,16 @@ +{{#if (eq @type 'remove')}} + + + Success! + + +

+ Your Peer has been marked for deletion. +

+
+
+{{/if}} diff --git a/ui/packages/consul-ui/app/components/consul/peer/search-bar/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/search-bar/index.hbs index 07b0ae51d..b7fbf40ff 100644 --- a/ui/packages/consul-ui/app/components/consul/peer/search-bar/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/peer/search-bar/index.hbs @@ -64,6 +64,42 @@ as |key value|}} + <:filter as |search|> + + + + {{t "components.consul.peer.search-bar.state.name"}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + {{#each + (get + (require '/models/peer' + path='schema' + from='/components/consul/peer/search-bar' + ) + 'State.allowedValues' + ) as |upperState|}} + {{#let + (string-to-lower-case upperState) + as |state|}} + + {{/let}} + {{/each}} + {{/let}} + + + <:sort as |search|> {{#let components.Optgroup components.Option as |Optgroup Option|}} + + + + diff --git a/ui/packages/consul-ui/app/components/consul/peer/search-bar/index.scss b/ui/packages/consul-ui/app/components/consul/peer/search-bar/index.scss new file mode 100644 index 000000000..0e9e3c1ff --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/peer/search-bar/index.scss @@ -0,0 +1,24 @@ +.consul-peer-search-bar { + li button span { + @extend %pill-500; + } + .value-pending span { + @extend %pill-pending; + } + .value-establishing span { + @extend %pill-establishing; + } + .value-active span { + @extend %pill-active; + } + .value-failing span { + @extend %pill-failing; + } + .value-terminated span { + @extend %pill-terminated; + } + .value-deleting span { + @extend %pill-deleting; + } +} + diff --git a/ui/packages/consul-ui/app/filter/predicates/peer.js b/ui/packages/consul-ui/app/filter/predicates/peer.js new file mode 100644 index 000000000..7bafa7fa2 --- /dev/null +++ b/ui/packages/consul-ui/app/filter/predicates/peer.js @@ -0,0 +1,10 @@ +export default { + state: { + pending: (item, value) => item.State.toLowerCase() === value, + establishing: (item, value) => item.State.toLowerCase() === value, + active: (item, value) => item.State.toLowerCase() === value, + failing: (item, value) => item.State.toLowerCase() === value, + terminated: (item, value) => item.State.toLowerCase() === value, + deleting: (item, value) => item.State.toLowerCase() === value, + }, +}; diff --git a/ui/packages/consul-ui/app/helpers/require.js b/ui/packages/consul-ui/app/helpers/require.js index 59f360658..c3b39e869 100644 --- a/ui/packages/consul-ui/app/helpers/require.js +++ b/ui/packages/consul-ui/app/helpers/require.js @@ -12,12 +12,15 @@ const container = new Map(); // `css` already has a caching mechanism under the hood so rely on that, plus // we get the advantage of laziness here, i.e. we only call css as and when we // need to -export default helper(([path = ''], { from }) => { - const fullPath = resolve(`${appName}${from}`, path); +export default helper(([path = ''], options) => { + let fullPath = resolve(`${appName}${options.from}`, path); + if(path.charAt(0) === '/') { + fullPath = `${appName}${fullPath}`; + } let module; if(require.has(fullPath)) { - module = require(fullPath).default; + module = require(fullPath)[options.export || 'default']; } else { throw new Error(`Unable to resolve '${fullPath}' does the file exist?`) } @@ -27,7 +30,7 @@ export default helper(([path = ''], { from }) => { return module(css); case fullPath.endsWith('.xstate'): return module; - default: { + case fullPath.endsWith('.element'): { if(container.has(fullPath)) { return container.get(fullPath); } @@ -35,5 +38,7 @@ export default helper(([path = ''], { from }) => { container.set(fullPath, component); return component; } + default: + return module; } }); diff --git a/ui/packages/consul-ui/app/models/peer.js b/ui/packages/consul-ui/app/models/peer.js index 89c70177e..5b2c6a01a 100644 --- a/ui/packages/consul-ui/app/models/peer.js +++ b/ui/packages/consul-ui/app/models/peer.js @@ -1,5 +1,18 @@ import Model, { attr } from '@ember-data/model'; +export const schema = { + State: { + defaultValue: 'PENDING', + allowedValues: [ + 'PENDING', + 'ESTABLISHING', + 'ACTIVE', + 'FAILING', + 'TERMINATED', + 'DELETING' + ], + }, +}; export default class Peer extends Model { @attr('string') uri; @attr() meta; diff --git a/ui/packages/consul-ui/app/routes/dc/peers/index.js b/ui/packages/consul-ui/app/routes/dc/peers/index.js index 5b0973e29..5281a07f5 100644 --- a/ui/packages/consul-ui/app/routes/dc/peers/index.js +++ b/ui/packages/consul-ui/app/routes/dc/peers/index.js @@ -3,6 +3,7 @@ import Route from 'consul-ui/routing/route'; export default class PeersRoute extends Route { queryParams = { sortBy: 'sort', + state: 'state', searchproperty: { as: 'searchproperty', empty: [['Name']], 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 21467648b..9a99eb796 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 @@ -2,11 +2,14 @@ import Service, { inject as service } from '@ember/service'; import { setProperties } from '@ember/object'; export default class HttpService extends Service { + @service('client/http') client; + @service('settings') settings; @service('repository/intention') intention; @service('repository/kv') kv; @service('repository/nspace') nspace; @service('repository/partition') partition; + @service('repository/peer') peer; @service('repository/session') session; prepare(sink, data, instance) { @@ -24,12 +27,16 @@ export default class HttpService extends Service { persist(sink, instance) { const [, , , , model] = sink.split('/'); const repo = this[model]; - return repo.persist(instance); + return this.client.request( + request => repo.persist(instance, request) + ); } remove(sink, instance) { const [, , , , model] = sink.split('/'); const repo = this[model]; - return repo.remove(instance); + return this.client.request( + request => repo.remove(instance, request) + ); } } diff --git a/ui/packages/consul-ui/app/services/filter.js b/ui/packages/consul-ui/app/services/filter.js index 1ba36ec48..bf78151a7 100644 --- a/ui/packages/consul-ui/app/services/filter.js +++ b/ui/packages/consul-ui/app/services/filter.js @@ -10,6 +10,7 @@ import intention from 'consul-ui/filter/predicates/intention'; import token from 'consul-ui/filter/predicates/token'; import policy from 'consul-ui/filter/predicates/policy'; import authMethod from 'consul-ui/filter/predicates/auth-method'; +import peer from 'consul-ui/filter/predicates/peer'; const predicates = { service: andOr(service), @@ -21,6 +22,7 @@ const predicates = { intention: andOr(intention), token: andOr(token), policy: andOr(policy), + peer: andOr(peer), }; export default class FilterService extends Service { diff --git a/ui/packages/consul-ui/app/services/repository/peer.js b/ui/packages/consul-ui/app/services/repository/peer.js index 5154a7250..fdd8b762f 100644 --- a/ui/packages/consul-ui/app/services/repository/peer.js +++ b/ui/packages/consul-ui/app/services/repository/peer.js @@ -70,4 +70,30 @@ export default class PeerService extends RepositoryService { }; }); } + + async remove(item, request) { + // soft delete + // we just return the item we want to delete + // but mark it as DELETING ourselves as the request is successfull + // and we don't have blocking queries here to get immediate updates + return (await request` + DELETE /v1/peering/${item.Name} + `)((headers, body, cache) => { + const partition = item.Partition; + const ns = item.Namespace; + const dc = item.Datacenter; + return { + meta: { + version: 2, + }, + body: cache( + { + ...item, + State: 'DELETING' + }, + uri => uri`peer:///${partition}/${ns}/${dc}/peer/${item.Name}` + ) + }; + }); + } } diff --git a/ui/packages/consul-ui/app/sort/comparators/peer.js b/ui/packages/consul-ui/app/sort/comparators/peer.js index 1ec2b1a99..912d23140 100644 --- a/ui/packages/consul-ui/app/sort/comparators/peer.js +++ b/ui/packages/consul-ui/app/sort/comparators/peer.js @@ -1,3 +1,26 @@ -export default ({ properties }) => key => { +import { schema } from 'consul-ui/models/peer'; + +export default ({ properties }) => (key = 'State:asc') => { + if (key.startsWith('State:')) { + return function(itemA, itemB) { + const [, dir] = key.split(':'); + let a, b; + if (dir === 'asc') { + b = itemA; + a = itemB; + } else { + a = itemA; + b = itemB; + } + switch (true) { + case schema.State.allowedValues.indexOf(a.State) < schema.State.allowedValues.indexOf(b.State): + return 1; + case schema.State.allowedValues.indexOf(a.State) > schema.State.allowedValues.indexOf(b.State): + return -1; + case schema.State.allowedValues.indexOf(a.State) === schema.State.allowedValues.indexOf(b.State): + return 0; + } + }; + } return properties(['Name'])(key); }; diff --git a/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss b/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss index ecac9fc4a..74559e892 100644 --- a/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss +++ b/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss @@ -501,7 +501,7 @@ // @import './rotate-ccw/index.scss'; // @import './rotate-cw/index.scss'; // @import './rss/index.scss'; -// @import './running/index.scss'; +@import './running/index.scss'; // @import './save/index.scss'; // @import './scissors/index.scss'; // @import './search/index.scss'; @@ -620,7 +620,7 @@ // @import './x-diamond-fill/index.scss'; // @import './x-hexagon/index.scss'; // @import './x-hexagon-fill/index.scss'; -// @import './x-square/index.scss'; +@import './x-square/index.scss'; // @import './x-square-fill/index.scss'; // @import './youtube/index.scss'; // @import './youtube-color/index.scss'; diff --git a/ui/packages/consul-ui/app/styles/components.scss b/ui/packages/consul-ui/app/styles/components.scss index ad39f2418..234b6793a 100644 --- a/ui/packages/consul-ui/app/styles/components.scss +++ b/ui/packages/consul-ui/app/styles/components.scss @@ -103,7 +103,8 @@ @import 'consul-ui/components/topology-metrics/series'; @import 'consul-ui/components/topology-metrics/stats'; @import 'consul-ui/components/topology-metrics/status'; +@import 'consul-ui/components/consul/intention/list/table'; +@import 'consul-ui/components/consul/peer'; @import 'consul-ui/components/peerings/badge'; @import 'consul-ui/components/consul/node/peer-info'; -@import 'consul-ui/components/consul/intention/list/table'; @import 'consul-ui/components/consul/service/peer-info'; diff --git a/ui/packages/consul-ui/app/templates/dc/peers/index.hbs b/ui/packages/consul-ui/app/templates/dc/peers/index.hbs index 9b040c12f..c9fd41e36 100644 --- a/ui/packages/consul-ui/app/templates/dc/peers/index.hbs +++ b/ui/packages/consul-ui/app/templates/dc/peers/index.hbs @@ -20,11 +20,15 @@ {{#let (hash - value=(or sortBy "Name:asc") + value=(or sortBy "State:asc") change=(action (mut sortBy) value="target.selected") ) (hash + state=(hash + value=(if state (split state ',') undefined) + change=(action (mut state) value="target.selectedItems") + ) searchproperty=(hash value=(if (not-eq searchproperty undefined) (split searchproperty ',') @@ -58,111 +62,86 @@ as |sort filters items|}} - - + @label="Peer" + @ondelete={{refresh-route}} + as |writer|> + + + + + + - - -

{{item.Name}}

-
- -
- + -
- {{t 'routes.dc.peers.index.detail.imported.count' - count=(format-number item.ImportedServiceCount) - }} -
- -
- {{t 'routes.dc.peers.index.detail.exported.count' - count=(format-number item.ExportedServiceCount) - }} -
- -
-
- - {{#if (can 'delete peer' item=item)}} - - - {{#if true}} - - - View - - - {{/if}} - - {{/if}} - -
- -
- - {{!-- TODO: do we need to check permissions here or will we receive an error automatically? --}} - - -

- {{#if (gt items.length 0)}} - No peers found - {{else}} - Welcome to Peers - {{/if}} -

-
- - {{#if (gt items.length 0)}} - No peers where found matching that search, or you may not have access to view the peers you are searching for. - {{else}} - Peering allows an admin partition in one datacenter to communicate with a partition in a different - datacenter. There don't seem to be any peers for this admin partition, or you may not have - peering:read permissions to - access this view. - {{/if}} - - - - - -
-
-
+
+ + {{!-- TODO: do we need to check permissions here or will we receive an error automatically? --}} + + +

+ {{#if (gt items.length 0)}} + No peers found + {{else}} + Welcome to Peers + {{/if}} +

+
+ + {{#if (gt items.length 0)}} + No peers where found matching that search, or you may not have access to view the peers you are searching for. + {{else}} + Peering allows an admin partition in one datacenter to communicate with a partition in a different + datacenter. There don't seem to be any peers for this admin partition, or you may not have + peering:read permissions to + access this view. + {{/if}} + + + + + +
+
+
+
+ - {{/let}} diff --git a/ui/packages/consul-ui/translations/components/consul/en-us.yaml b/ui/packages/consul-ui/translations/components/consul/en-us.yaml index 48975d53d..0809332f4 100644 --- a/ui/packages/consul-ui/translations/components/consul/en-us.yaml +++ b/ui/packages/consul-ui/translations/components/consul/en-us.yaml @@ -1,3 +1,19 @@ +peer: + search-bar: + state: + name: Status + options: + pending: Pending + establishing: Establishing + active: Active + failing: Failing + terminated: Terminated + deleting: Deleting + sort: + state: + name: Status + asc: Pending to Deleting + desc: Deleting to Pending service: search-bar: kind: Service Type