Skip to content

Commit

Permalink
ui: Logout button (#7604)
Browse files Browse the repository at this point in the history
* ui: Logout button

This commit adds an easier way to logout of the UI using a logout button

Notes:

- Added a Logout button to the main navigation when you are logged in,
meaning you have easy access to a way to log out of the UI.
- Changed all wording to use 'Log in/out' vocabulary instad of 'stop
using'.
- The logout button opens a panel to show you your current ACL
token and a logout button in order to logout.
- When using legacy ACLs we don't show the current ACL token as legacy
ACLs tokens only have secret values, whereas the new ACLs use a
non-secret ID plus a secret ID (that we don't show).
- We also added a new `<EmptyState />` component to use for all our
empty states. We currently only use this for the ACLs disabled screen to
provide more outgoing links to more readind material/documentation to
help you to understand and enable ACLs.
- The `<DataSink />` component is the sibling to our `<DataSource />`
component and whilst is much simpler (as it doesn't require polling
support), its tries to use the same code patterns for consistencies
sake.
- We had a fun problem with ember-data's `store.unloadAll` here, and in
the end went with `store.init` to empty the ember-data store instead due
to timing issues.
- We've tried to use already existing patterns in the Consul UI here
such as our preexisting `feedback` service, although these are likely to
change in the future. The thinking here is to add this feature with as
little change as possible.

Overall this is a precursor to a much larger piece of work centered on
auth in the UI. We figured this was a feature complete piece of work as
it is and thought it was worthwhile to PR as a feature on its own, which
also means the larger piece of work will be a smaller scoped PR also.
  • Loading branch information
johncowen authored and John Cowen committed May 12, 2020
1 parent 32a619a commit 4bf1dae
Show file tree
Hide file tree
Showing 47 changed files with 730 additions and 183 deletions.
45 changes: 38 additions & 7 deletions ui-v2/app/components/app-view/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,45 @@
<header>
{{#each flashMessages.queue as |flash|}}
<FlashMessage @flash={{flash}} as |component flash|>
{{! flashes automatically ucfirst the type }}
{{#let (lowercase component.flashType) (lowercase flash.action) as |status type|}}
{{! flashes automatically ucfirst the type }}

<p data-notification class="{{lowercase component.flashType}} notification-{{lowercase flash.action}}">
<strong>
{{component.flashType}}!
</strong>
<YieldSlot @name="notification" @params={{block-params (lowercase component.flashType) (lowercase flash.action) flash.item}}>{{yield}}</YieldSlot>
</p>
<p data-notification class={{concat status ' notification-' type}}>
<strong>
{{capitalize status}}!
</strong>
{{#yield-slot name="notification" params=(block-params status type flash.item)}}
{{yield}}
{{#if (eq type 'logout')}}
{{#if (eq status 'success') }}
You are now logged out.
{{else}}
There was an error logging out.
{{/if}}
{{else if (eq type 'authorize')}}
{{#if (eq status 'success') }}
You are now logged in.
{{else}}
There was an error, please check your SecretID/Token
{{/if}}
{{/if}}
{{else}}
{{#if (eq type 'logout')}}
{{#if (eq status 'success') }}
You are now logged out.
{{else}}
There was an error logging out.
{{/if}}
{{else if (eq type 'authorize')}}
{{#if (eq status 'success') }}
You are now logged in.
{{else}}
There was an error, please check your SecretID/Token
{{/if}}
{{/if}}
{{/yield-slot}}
</p>
{{/let}}
</FlashMessage>
{{/each}}
<div>
Expand Down
62 changes: 62 additions & 0 deletions ui-v2/app/components/data-sink/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## DataSink

```handlebars
<DataSink
@sink="/dc/nspace/intentions/{{intentions.uid}}"
@onchange={{action (mut items) value="data"}}
@onerror={{action (mut error) value="error"}}
as |api|
></DataSink>
```

### Arguments

| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `sink` | `String` | | The location of the sink, this should map to a string based URI |
| `data` | `Object` | | The data to be saved to the current instance, null or an empty string means remove |
| `onchange` | `Function` | | The action to fire when the data has arrived to the sink. Emits an Event-like object with a `data` property containing the data, if the data was deleted this is `undefined`. |
| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |

### Methods/Actions/api

| Method/Action | Description |
| --- | --- |
| `open` | Manually add or remove fom the data sink |

The component takes a `sink` or an identifier (a uri) for the location of a sink and then emits `onchange` events whenever that data has been arrived to the sink (whether persisted or removed). If an error occurs whilst listening for data changes, an `onerror` event is emitted.

Behind the scenes in the Consul UI we map URIs back to our `ember-data` backed `Repositories` meaning we can essentially redesign the URIs used for our data to more closely fit our needs. For example we currently require that **all** HTTP API URIs begin with `/dc/nspace/` values whether they require them or not.

`DataSink` is not just restricted to HTTP API data, and can be configured to listen for data changes using a variety of methods and sources. For example we have also configured `DataSink` to send data to `LocalStorage` using the `settings://` pseudo-protocol in the URI (See examples below).


### Examples

```handlebars
<DataSink @src="/dc/nspace/intentions/{{intention.uid}}"
@onchange={{action (mut item) value="data"}}
@onerror={{action (mut error) value="error"}}
as |api|
>
<button type="button" onclick={{action api.open (hash Name="New Name")}}>Create/Update</button>
<button type="button" onclick={{action api.open null}}>Delete</button>
</DataSink>
{{item.Name}}
```

```handlebars
<DataSink @src="/dc/nspace/intentions/{{intention.uid}}"
@data=(hash Name="New Name")
@onchange={{action (mut item) value="data"}}
@onerror={{action (mut error) value="error"}}
></DataSink>
{{item.Name}}
```

### See

- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)

---
4 changes: 4 additions & 0 deletions ui-v2/app/components/data-sink/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{yield (hash
open=(action 'open')
state=state
)}}
105 changes: 105 additions & 0 deletions ui-v2/app/components/data-sink/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { set, get, computed } from '@ember/object';

import { once } from 'consul-ui/utils/dom/event-source';

export default Component.extend({
tagName: '',

service: service('data-sink/service'),
dom: service('dom'),
logger: service('logger'),

onchange: function(e) {},
onerror: function(e) {},

state: computed('instance', 'instance.{dirtyType,isSaving}', function() {
let id;
const isSaving = get(this, 'instance.isSaving');
const dirtyType = get(this, 'instance.dirtyType');
if (typeof isSaving === 'undefined' && typeof dirtyType === 'undefined') {
id = 'idle';
} else {
switch (dirtyType) {
case 'created':
id = isSaving ? 'creating' : 'create';
break;
case 'updated':
id = isSaving ? 'updating' : 'update';
break;
case 'deleted':
case undefined:
id = isSaving ? 'removing' : 'remove';
break;
}
id = `active.${id}`;
}
return {
matches: name => id.indexOf(name) !== -1,
};
}),

init: function() {
this._super(...arguments);
this._listeners = this.dom.listeners();
},
willDestroy: function() {
this._super(...arguments);
this._listeners.remove();
},
source: function(cb) {
const source = once(cb);
const error = err => {
set(this, 'instance', undefined);
try {
this.onerror(err);
this.logger.execute(err);
} catch (err) {
this.logger.execute(err);
}
};
this._listeners.add(source, {
message: e => {
try {
set(this, 'instance', undefined);
this.onchange(e);
} catch (err) {
error(err);
}
},
error: e => error(e),
});
return source;
},
didInsertElement: function() {
this._super(...arguments);
if (typeof this.data !== 'undefined') {
this.actions.open.apply(this, [this.data]);
}
},
persist: function(data, instance) {
set(this, 'instance', this.service.prepare(this.sink, data, instance));
this.source(() => this.service.persist(this.sink, this.instance));
},
remove: function(instance) {
set(this, 'instance', this.service.prepare(this.sink, null, instance));
this.source(() => this.service.remove(this.sink, this.instance));
},
actions: {
open: function(data, instance) {
if (instance instanceof Event) {
instance = undefined;
}
if (typeof data === 'undefined') {
throw new Error('You must specify data to save, or null to remove');
}
// potentially allow {} and "" as 'remove' flags
if (data === null || data === '') {
this.remove(instance);
} else {
this.persist(data, instance);
}
},
},
});
21 changes: 21 additions & 0 deletions ui-v2/app/components/empty-state/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{yield}}
<div class="empty-state" ...attributes>
<header>
{{#yield-slot name="header"}}
{{yield}}
{{/yield-slot}}
{{#yield-slot name="subheader"}}
{{yield}}
{{/yield-slot}}
</header>
<p>
{{#yield-slot name="body"}}
{{yield}}
{{/yield-slot}}
</p>
{{#yield-slot name="actions"}}
<ul>
{{yield}}
</ul>
{{/yield-slot}}
</div>
6 changes: 6 additions & 0 deletions ui-v2/app/components/empty-state/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Component from '@ember/component';
import Slotted from 'block-slots';

export default Component.extend(Slotted, {
tagName: '',
});
38 changes: 38 additions & 0 deletions ui-v2/app/components/hashicorp-consul/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,44 @@
<li data-test-main-nav-settings class={{if (is-href 'settings') 'is-active'}}>
<a href={{href-to 'settings'}}>Settings</a>
</li>
{{#if (env 'CONSUL_ACLS_ENABLED')}}
<DataSource
@src="settings://consul:token"
@onchange={{action "changeToken" value="data"}}
/>
<DataSink
@sink="settings://consul:token"
as |tokenSink|>
{{#if (not-eq token.AccessorID undefined)}}
<li data-test-main-nav-auth>
<PopoverMenu @position="right">
<BlockSlot @name="trigger">
Logout
</BlockSlot>
<BlockSlot @name="menu">
{{#if token.AccessorID}}
<li role="none">
<dl>
<dt>
<span>My ACL Token</span><br />
AccessorID
</dt>
<dd>
{{substr token.AccessorID -8}}
</dd>
</dl>
</li>
<li role="separator"></li>
{{/if}}
<li class="dangerous" role="none">
<button type="button" tabindex="-1" role="menuitem" onclick={{action tokenSink.open null}}>Logout</button>
</li>
</BlockSlot>
</PopoverMenu>
</li>
{{/if}}
</DataSink>
{{/if}}
</ul>
</nav>
</div>
Expand Down
Loading

0 comments on commit 4bf1dae

Please sign in to comment.