ui: Notifications re-organization/re-style (#11577)

- Moves where they appear up to the <App /> component.
- Instead of a <Notification /> wrapping component to move whatever you use for a notification up to where they need to appear (via ember-cli-flash), we now use a {{notification}} modifier now we have modifiers.
- Global notifications/flashes are no longer special styles of their own. You just use the {{notification}} modifier to hoist whatever component/element you want up to the top of the page. This means we can re-use our existing <Notice /> component for all our global UI notifications (this is the user visible change here)
This commit is contained in:
John Cowen 2021-11-24 18:14:07 +00:00 committed by GitHub
parent f605689154
commit 124fa8f168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 618 additions and 396 deletions

3
.changelog/11577.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Update global notification styling
```

View File

@ -8,9 +8,7 @@ state: needs-love
the app chrome), every 'top level main section/template' should have one of the app chrome), every 'top level main section/template' should have one of
these. these.
It contains legacy authorization code (that can probably be removed now), and This component will potentially be renamed to `Page` or `View` or similar now
our flash messages (that should be moved to the `<App />` or `<HashicorpConsul
/>` component and potentially be renamed to `Page` or `View` or similar now
that we don't need two words. that we don't need two words.
Other than that it provides the basic layout/slots for our main title, search Other than that it provides the basic layout/slots for our main title, search
@ -86,20 +84,12 @@ breadcrumbs and back again.
</figure> </figure>
``` ```
## Arguments
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `authorized` | `Boolean` | `true` | Whether the View is authorized or not |
| `enabled` | `Boolean` | `true` | Whether ACLs are enabled or not |
## Slots ## Slots
| Name | Description | | Name | Description |
| --- | --- | | --- | --- |
| `header` | The main title of the page, you probably want to put a `<h1>` in here | | `header` | The main title of the page, you probably want to put a `<h1>` in here |
| `content` | The main content of the page, and potentially an `<Outlet />` somewhere | | `content` | The main content of the page, and potentially an `<Outlet />` somewhere |
| `notification` | Old style notifications, also see `<Notification />` |
| `breadcrumbs` | Any breadcrumbs, you probably want an `ol/li/a` in here | | `breadcrumbs` | Any breadcrumbs, you probably want an `ol/li/a` in here |
| `actions` | Any actions relevant for the entire page, probably using `<Action />` | | `actions` | Any actions relevant for the entire page, probably using `<Action />` |
| `nav` | Secondary navigation goes in here, also see `<TabNav />` | | `nav` | Secondary navigation goes in here, also see `<TabNav />` |

View File

@ -4,74 +4,23 @@
> >
{{yield}} {{yield}}
<header> <header>
{{#each flashMessages.queue as |flash|}}
<FlashMessage @flash={{flash}} as |component flash|>
{{#if flash.dom}}
{{{flash.dom}}}
{{else}}
{{#let (lowercase component.flashType) (lowercase flash.action) as |status type|}}
{{! flashes automatically ucfirst the type }}
<p data-notification role="alert" class={{concat status ' notification-' type}}>
<strong>
{{capitalize status}}!
</strong>
{{#yield-slot name="notification" params=(block-params status type flash.item flash.error)}}
{{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}}
{{/if}}
</FlashMessage>
{{/each}}
<div> <div>
<div> <div>
{{#if authorized}}
<nav aria-label="Breadcrumb" data-test-breadcrumbs> <nav aria-label="Breadcrumb" data-test-breadcrumbs>
<YieldSlot @name="breadcrumbs"> <YieldSlot @name="breadcrumbs">
{{document-attrs class="with-breadcrumbs"}} {{document-attrs class="with-breadcrumbs"}}
{{yield}} {{yield}}
</YieldSlot> </YieldSlot>
</nav> </nav>
{{/if}}
<div class="title"> <div class="title">
<YieldSlot @name="header"> <YieldSlot @name="header">
{{yield}} {{yield}}
</YieldSlot> </YieldSlot>
<div class="actions"> <div class="actions">
{{#if authorized}}
<YieldSlot @name="actions"> <YieldSlot @name="actions">
<PortalTarget @name="app-view-actions" /> <PortalTarget @name="app-view-actions" />
{{yield}} {{yield}}
</YieldSlot> </YieldSlot>
{{/if}}
</div> </div>
</div> </div>
<YieldSlot @name="nav"> <YieldSlot @name="nav">
@ -79,42 +28,12 @@
</YieldSlot> </YieldSlot>
</div> </div>
</div> </div>
{{#if authorized}}
<YieldSlot @name="toolbar"> <YieldSlot @name="toolbar">
<input type="checkbox" id="toolbar-toggle" /> <input type="checkbox" id="toolbar-toggle" />
{{yield}} {{yield}}
</YieldSlot> </YieldSlot>
{{/if}}
</header> </header>
<div> <div>
{{#if (not enabled) }}
<EmptyState data-test-acls-disabled>
<BlockSlot @name="header">
<h2>Welcome to ACLs</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
ACLs are not enabled in this Consul cluster. We strongly encourage the use of ACLs in production environments for the best security practices.
</p>
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}/acl/index.html" rel="noopener noreferrer" target="_blank">Read the documentation</a>
</li>
<li class="learn-link">
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/security-networking/production-acls" rel="noopener noreferrer" target="_blank">Follow the guide</a>
</li>
</BlockSlot>
</EmptyState>
{{else if (not authorized)}}
<ErrorState
@error={{hash
status='403'
}}
@login={{login}}
/>
{{else}}
<YieldSlot @name="content">{{yield}}</YieldSlot> <YieldSlot @name="content">{{yield}}</YieldSlot>
{{/if}}
</div> </div>
</div> </div>

View File

@ -2,6 +2,4 @@ import Component from '@ember/component';
import SlotsMixin from 'block-slots'; import SlotsMixin from 'block-slots';
export default Component.extend(SlotsMixin, { export default Component.extend(SlotsMixin, {
tagName: '', tagName: '',
authorized: true,
enabled: true,
}); });

View File

@ -1,5 +1,6 @@
{{#let (hash {{#let (hash
main=(concat guid '-main') main=(concat guid '-main')
Notification=(component 'app/notification')
) as |exported|}} ) as |exported|}}
<div <div
@ -13,7 +14,7 @@
> >
<PortalTarget <PortalTarget
@name="app-before-skip-links" @name="app-before-skip-links"
@mutiple={{true}} @multiple={{true}}
></PortalTarget> ></PortalTarget>
<a href={{concat '#' exported.main}}>{{t 'components.app.skip_to_content'}}</a> <a href={{concat '#' exported.main}}>{{t 'components.app.skip_to_content'}}</a>
{{!-- {{!--
@ -25,7 +26,7 @@
--}} --}}
<PortalTarget <PortalTarget
@name="app-after-skip-links" @name="app-after-skip-links"
@mutiple={{true}} @multiple={{true}}
></PortalTarget> ></PortalTarget>
</div> </div>
@ -78,6 +79,13 @@
</div> </div>
</header> </header>
<main id={{concat guid '-main'}}> <main id={{concat guid '-main'}}>
<div class="notifications">
{{yield exported to="notifications"}}
<PortalTarget
@name="app-notifications"
@multiple={{true}}
></PortalTarget>
</div>
{{yield exported to="main"}} {{yield exported to="main"}}
</main> </main>
<footer <footer

View File

@ -1,6 +1,33 @@
.app .skip-links { .app .skip-links {
@extend %skip-links; @extend %skip-links;
} }
.app .notifications {
@extend %app-notifications;
}
%app-notifications {
display: flex;
flex-direction: column;
align-items: center;
position: fixed;
z-index: 50;
top: -45px;
left: 0;
pointer-events: none;
}
%app-notifications .app-notification > * {
min-width: 400px;
}
%app-notifications .app-notification {
@extend %with-transition-500;
transition-property: opacity;
width: fit-content;
max-width: 80%;
pointer-events: auto;
}
[role='banner'] { [role='banner'] {
@extend %main-header-horizontal; @extend %main-header-horizontal;
} }
@ -32,6 +59,7 @@
@extend %main-nav-horizontal-action-active; @extend %main-nav-horizontal-action-active;
} }
%main-nav-sidebar, %main-nav-sidebar,
%main-notifications,
main { main {
@extend %transition-pushover; @extend %transition-pushover;
} }
@ -39,34 +67,50 @@ main {
transition-property: left; transition-property: left;
z-index: 10; z-index: 10;
} }
%app-notifications,
main { main {
margin-top: var(--chrome-height, 64px); margin-top: var(--chrome-height, 64px);
transition-property: margin-left; transition-property: margin-left;
} }
%app-notifications {
transition-property: margin-left, width;
}
@media #{$--sidebar-open} { @media #{$--sidebar-open} {
%main-nav-horizontal-toggle ~ main .notifications {
width: calc(100% - var(--chrome-width));
}
%main-nav-horizontal-toggle:checked ~ main .notifications {
width: 100%;
}
%main-nav-horizontal-toggle + header > div > nav:first-of-type { %main-nav-horizontal-toggle + header > div > nav:first-of-type {
left: 0; left: 0;
} }
%main-nav-horizontal-toggle:checked + header > div > nav:first-of-type { %main-nav-horizontal-toggle:checked + header > div > nav:first-of-type {
left: calc(var(--chrome-width, 300px) * -1); left: calc(var(--chrome-width, 300px) * -1);
} }
%main-nav-horizontal-toggle ~ main .notifications,
%main-nav-horizontal-toggle ~ main, %main-nav-horizontal-toggle ~ main,
%main-nav-horizontal-toggle ~ footer { %main-nav-horizontal-toggle ~ footer {
margin-left: var(--chrome-width, 300px); margin-left: var(--chrome-width, 300px);
} }
%main-nav-horizontal-toggle:checked ~ main .notifications,
%main-nav-horizontal-toggle:checked ~ main, %main-nav-horizontal-toggle:checked ~ main,
%main-nav-horizontal-toggle:checked ~ footer { %main-nav-horizontal-toggle:checked ~ footer {
margin-left: 0; margin-left: 0;
} }
} }
@media #{$--lt-sidebar-open} { @media #{$--lt-sidebar-open} {
%main-nav-horizontal-toggle ~ main .notifications {
width: 100%;
}
%main-nav-horizontal-toggle:checked + header > div > nav:first-of-type { %main-nav-horizontal-toggle:checked + header > div > nav:first-of-type {
left: 0; left: 0;
} }
%main-nav-horizontal-toggle + header > div > nav:first-of-type { %main-nav-horizontal-toggle + header > div > nav:first-of-type {
left: calc(var(--chrome-width, 300px) * -1); left: calc(var(--chrome-width, 300px) * -1);
} }
%main-nav-horizontal-toggle ~ main .notifications,
%main-nav-horizontal-toggle ~ main, %main-nav-horizontal-toggle ~ main,
%main-nav-horizontal-toggle ~ footer { %main-nav-horizontal-toggle ~ footer {
margin-left: 0; margin-left: 0;

View File

@ -0,0 +1,19 @@
<div
class="app-notification"
...attributes
{{style
(array
(array 'opacity' '1')
(array 'transition-delay' (concat @delay 'ms'))
)
}}
{{style
(array
(array 'opacity' (if @sticky '1' '0'))
)
delay=0
}}
>
{{yield}}
</div>

View File

@ -14,21 +14,45 @@
@onsubmit={{action this.onsubmit}} @onsubmit={{action this.onsubmit}}
as |api|> as |api|>
<BlockSlot @name="error" as |Notification|> <BlockSlot @name="error" as |after|>
<Notification>
<p data-notification role="alert" class="error notification-update">
{{#if (string-starts-with api.error.detail 'duplicate intention found:')}} {{#if (string-starts-with api.error.detail 'duplicate intention found:')}}
<strong>Intention exists</strong> <Notice
{{notification
after=(action after)
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header>
<strong>Intention exists!</strong>
</notice.Header>
<notice.Body>
<p>
An intention already exists for this Source-Destination pair. Please enter a different combination of Services, or search the intentions to edit an existing intention. An intention already exists for this Source-Destination pair. Please enter a different combination of Services, or search the intentions to edit an existing intention.
</p>
</notice.Body>
</Notice>
{{else}} {{else}}
<Notice
{{notification
after=(action after)
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header>
<strong>Error!</strong> <strong>Error!</strong>
</notice.Header>
<notice.Body>
<p>
There was an error saving your intention. There was an error saving your intention.
{{#if (and api.error.status api.error.detail)}} {{#if (and api.error.status api.error.detail)}}
<br />{{api.error.status}}: {{api.error.detail}} <br />{{api.error.status}}: {{api.error.detail}}
{{/if}} {{/if}}
{{/if}}
</p> </p>
</Notification> </notice.Body>
</Notice>
{{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="form"> <BlockSlot @name="form">

View File

@ -49,16 +49,26 @@
<State @matches={{array "idle" "disconnected"}}> <State @matches={{array "idle" "disconnected"}}>
<State @matches="disconnected"> <State @matches="disconnected">
{{#yield-slot name="disconnected" params=(block-params (component 'notification' after=(action dispatch "RESET")))}} {{#yield-slot name="disconnected" params=(block-params (action dispatch "RESET"))}}
{{yield api}} {{yield api}}
{{else}} {{else}}
{{#if (not eq error.status '401')}} {{#if (not eq error.status '401')}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="warning notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
An error was returned whilst loading this data, refresh to try again. An error was returned whilst loading this data, refresh to try again.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{/if}} {{/if}}
{{/yield-slot}} {{/yield-slot}}
</State> </State>

View File

@ -33,46 +33,89 @@
</State> </State>
<State @matches="removed"> <State @matches="removed">
{{#yield-slot name="removed" params=(block-params (component 'notification' after=(queue (action dispatch "RESET") (action ondelete))))}} {{#let
(queue (action dispatch "RESET") (action ondelete))
as |after|}}
{{#yield-slot name="removed" params=(block-params after)}}
{{yield api}} {{yield api}}
{{else}} {{else}}
<Notification @after={{queue (action dispatch "RESET") (action ondelete)}}> <Notice
<p data-notification role="alert" class="success notification-delete"> {{notification
after=(action after)
}}
class="notification-delete"
@type="success"
as |notice|>
<notice.Header>
<strong>Success!</strong> <strong>Success!</strong>
</notice.Header>
<notice.Body>
<p>
Your {{or label type}} has been deleted. Your {{or label type}} has been deleted.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{/yield-slot}} {{/yield-slot}}
{{/let}}
</State> </State>
<State @matches="persisted"> <State @matches="persisted">
<Notification @after={{action onchange}}> {{#let
{{#yield-slot name="persisted"}} (action onchange)
as |after|}}
{{#yield-slot name="persisted" params=(block-params after)}}
{{yield api}} {{yield api}}
{{else}} {{else}}
<p data-notification role="alert" class="success notification-update"> <Notice
{{notification
after=(action after)
}}
class="notification-update"
@type="success"
as |notice|>
<notice.Header>
<strong>Success!</strong> <strong>Success!</strong>
</notice.Header>
<notice.Body>
<p>
Your {{or label type}} has been saved. Your {{or label type}} has been saved.
</p> </p>
</notice.Body>
</Notice>
{{/yield-slot}} {{/yield-slot}}
</Notification> {{/let}}
</State> </State>
<State @matches="error"> <State @matches="error">
{{#yield-slot name="error" params=(block-params (component 'notification' after=(action dispatch "RESET")))}} {{#let
(action dispatch "RESET")
as |after|}}
{{#yield-slot name="error" params=(block-params after)}}
{{yield api}} {{yield api}}
{{else}} {{else}}
<Notification @after={{action dispatch "RESET"}}> <Notice
<p data-notification role="alert" class="error notification-update"> {{notification
after=(action after)
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header>
<strong>Error!</strong> <strong>Error!</strong>
</notice.Header>
<notice.Body>
<p>
There was an error saving your {{or label type}}. There was an error saving your {{or label type}}.
{{#if (and api.error.status api.error.detail)}} {{#if (and api.error.status api.error.detail)}}
<br />{{api.error.status}}: {{api.error.detail}} <br />{{api.error.status}}: {{api.error.detail}}
{{/if}} {{/if}}
</p> </p>
</Notification> </notice.Body>
</Notice>
{{/yield-slot}} {{/yield-slot}}
{{/let}}
</State> </State>
<YieldSlot @name="content"> <YieldSlot @name="content">
{{yield api}} {{yield api}}
</YieldSlot> </YieldSlot>

View File

@ -1,35 +0,0 @@
---
class: css
state: needs-love
---
# flash-message
CSS component for styling our flash messages
```hbs preview-template
<div class="flash-message">
<p
role="alert"
class={{or this.type 'success'}}
>
<strong>
{{capitalize (or this.type 'success')}}!
</strong>
</p>
</div>
<figure>
<figcaption>Provide a widget to change the <code>class</code></figcaption>
<select
onchange={{action (mut this.type) value="target.value"}}
>
<option>success</option>
<option>warning</option>
<option>error</option>
<option>exists</option>
</select>
</figure>
```

View File

@ -1,19 +0,0 @@
@import './skin';
@import './layout';
.flash-message {
@extend %flash-message;
}
%flash-message.exiting {
@extend %blink-in-fade-out;
}
/* This is for the flash message that appears */
/* when you save an intention that already exists */
/* once we have refactored app-view with data-source with nicer */
/* flash message usage we should be able to remove this */
%flash-message p.exists strong::before {
@extend %with-cancel-square-fill-mask;
color: rgb(var(--tone-red-500));
}
%flash-message p.exists {
@extend %frame-red-500;
}

View File

@ -1,12 +0,0 @@
%flash-message {
display: flex;
position: relative;
z-index: 6;
justify-content: center;
margin: 0 15%;
}
%flash-message p {
top: -46px;
position: absolute;
padding: 9px 15px;
}

View File

@ -1,28 +0,0 @@
%flash-message p {
border-width: 1px;
border-radius: var(--decor-radius-100);
}
%flash-message p strong::before {
@extend %as-pseudo;
}
%flash-message p.success strong::before {
@extend %with-check-circle-fill-mask;
color: rgb(var(--tone-green-500));
}
%flash-message p.warning strong::before {
@extend %with-alert-triangle-mask;
color: rgb(var(--tone-orange-500));
}
%flash-message p.error strong::before {
@extend %with-cancel-square-fill-mask;
color: rgb(var(--tone-red-500));
}
%flash-message p.success {
@extend %frame-green-500;
}
%flash-message p.warning {
@extend %frame-yellow-500;
}
%flash-message p.error {
@extend %frame-red-500;
}

View File

@ -3,7 +3,81 @@
class="hashicorp-consul" class="hashicorp-consul"
...attributes ...attributes
> >
<:notifications as |app|>
{{#each flashMessages.queue as |flash|}}
<app.Notification
@delay={{sub flash.timeout flash.extendedTimeout}}
@sticky={{flash.sticky}}
>
{{#if flash.dom}}
{{{flash.dom}}}
{{else}}
{{#let (lowercase flash.type) (lowercase flash.action) as |status type|}}
<Notice
role="alert"
class={{concat status ' notification-' type}}
data-notification
@type={{status}}
as |notice|>
<notice.Header>
<strong>
{{capitalize status}}!
</strong>
</notice.Header>
<notice.Body>
<p>
{{#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}}
{{else}}
{{#if (eq flash.model 'token')}}
<Consul::Token::Notifications
@type={{type}}
@status={{status}}
@item={{flash.item}}
@error={{flash.error}}
/>
{{else if (eq flash.model 'role')}}
<Consul::Role::Notifications
@type={{type}}
@status={{status}}
@item={{flash.item}}
@error={{flash.error}}
/>
{{else if (eq flash.model 'policy')}}
<Consul::Policy::Notifications
@type={{type}}
@status={{status}}
@item={{flash.item}}
@error={{flash.error}}
/>
{{else if (eq flash.model 'nspace')}}
<Consul::Nspace::Notifications
@type={{type}}
@status={{status}}
@item={{flash.item}}
@error={{flash.error}}
/>
{{/if}}
{{/if}}
</p>
</notice.Body>
</Notice>
{{/let}}
{{/if}}
</app.Notification>
{{/each}}
</:notifications>
<:home-nav> <:home-nav>
<a <a
href={{href-to 'index'}} href={{href-to 'index'}}

View File

@ -1,7 +1,10 @@
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { inject as service } from '@ember/service';
export default class HashiCorpConsul extends Component { export default class HashiCorpConsul extends Component {
@service('flashMessages') flashMessages;
@action @action
open() { open() {
this.authForm.focus(); this.authForm.focus();

View File

@ -24,9 +24,12 @@
background-color: rgb(var(--tone-green-050)); background-color: rgb(var(--tone-green-050));
border-color: rgb(var(--tone-green-500)); border-color: rgb(var(--tone-green-500));
} }
%notice-success header * {
color: rgb(var(--tone-green-800));
}
%notice-info { %notice-info {
border-color: rgb(var(--tone-blue-100)); border-color: rgb(var(--tone-blue-100));
background-color: rgb(var(--tone-gray-010)); background-color: rgb(var(--tone-blue-010));
} }
%notice-info header * { %notice-info header * {
color: rgb(var(--tone-blue-700)); color: rgb(var(--tone-blue-700));
@ -36,7 +39,7 @@
border-color: rgb(var(--tone-gray-300)); border-color: rgb(var(--tone-gray-300));
} }
%notice-info header * { %notice-info header * {
color: rgb(var(--tone-gray-700)); color: rgb(var(--tone-blue-700));
} }
%notice-warning { %notice-warning {
border-color: rgb(var(--tone-yellow-100)); border-color: rgb(var(--tone-yellow-100));
@ -49,6 +52,9 @@
background-color: rgb(var(--tone-red-050)); background-color: rgb(var(--tone-red-050));
border-color: rgb(var(--tone-red-500)); border-color: rgb(var(--tone-red-500));
} }
%notice-error header * {
color: rgb(var(--tone-red-500));
}
%notice-success::before { %notice-success::before {
@extend %with-check-circle-fill-mask; @extend %with-check-circle-fill-mask;
color: rgb(var(--tone-green-500)); color: rgb(var(--tone-green-500));

View File

@ -1,3 +0,0 @@
<div id={{guid}}>
{{yield}}
</div>

View File

@ -1,40 +0,0 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({
tagName: '',
notify: service('flashMessages'),
dom: service('dom'),
oncomplete: function() {},
init: function() {
this._super(...arguments);
this.guid = this.dom.guid(this);
},
didInsertElement: function() {
const $el = this.dom.element(`#${this.guid}`);
const options = {
timeout: 6000,
extendedTimeout: 300,
dom: $el.innerHTML,
};
if (this.sticky) {
options.sticky = true;
}
$el.remove();
this.notify.clearMessages();
if (typeof this.after === 'function') {
Promise.resolve(this.after())
.catch(e => {
if (e.name !== 'TransitionAborted') {
throw e;
}
})
.then(res => {
this.notify.add(options);
});
} else {
this.notify.add(options);
}
},
});

View File

@ -1,6 +1,8 @@
import Mixin from '@ember/object/mixin'; import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { set, get } from '@ember/object'; import { set, get } from '@ember/object';
import { singularize } from 'ember-inflector';
/** With Blocking Actions /** With Blocking Actions
* This mixin contains common write actions (Create Update Delete) for routes. * This mixin contains common write actions (Create Update Delete) for routes.
* It could also be an Route to extend but decoration seems to be more sense right now. * It could also be an Route to extend but decoration seems to be more sense right now.
@ -25,7 +27,11 @@ export default Mixin.create({
const route = this; const route = this;
set(this, 'feedback', { set(this, 'feedback', {
execute: function(cb, type, error) { execute: function(cb, type, error) {
return feedback.execute(cb, type, error, route.controller); const temp = route.routeName.split('.');
temp.pop();
const routeName = singularize(temp.pop());
return feedback.execute(cb, type, error, routeName);
}, },
}); });
}, },

View File

@ -0,0 +1,37 @@
import Modifier from 'ember-modifier';
import { inject as service } from '@ember/service';
export default class NotificationModifier extends Modifier {
@service('flashMessages') notify;
didInstall() {
this.element.setAttribute('role', 'alert');
this.element.dataset['notification'] = null;
const options = {
timeout: 6000,
extendedTimeout: 300,
...this.args.named,
};
options.dom = this.element.outerHTML;
this.element.remove();
this.notify.clearMessages();
if (typeof options.after === 'function') {
Promise.resolve().then(_ => options.after())
.catch(e => {
if (e.name !== 'TransitionAborted') {
throw e;
}
})
.then(res => {
this.notify.add(options);
});
} else {
this.notify.add(options);
}
}
willDestroy() {
if(this.args.named.sticky) {
this.notify.clearMessages();
}
}
}

View File

@ -0,0 +1,34 @@
# notification
Consul UIs notification modifier is used to 'hoist' DOM elements into the
global notification area for the UI. The most common usage will be something
like the below:
```hbs preview-template
<figure>
<figcaption>Attach a Warning notice to the top of the app ^^^</figcaption>
<Notice
class="notification-update"
@type="warning"
{{notification
sticky=true
}}
as |notice|>
<notice.Header>
<strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
This service has been deregistered and no longer exists in the catalog.
</p>
</notice.Body>
</Notice>
</figure>
```
Currently this is backed by `ember-cli-flash` and the named options are
currently options that can be accepted by `ember-cli-flash`. Be aware that this
is likely to change in the future.

View File

@ -0,0 +1,52 @@
import Modifier from 'ember-modifier';
import { assert } from '@ember/debug';
export default class StyleModifier extends Modifier {
setStyles(newStyles = []) {
const rulesToRemove = this._oldStyles || new Set();
if(!Array.isArray(newStyles)) {
newStyles = Object.entries(newStyles)
}
newStyles.forEach(([property, value]) => {
assert(
`Your given value for property '${property}' is ${value} (${typeof value}). Accepted types are string and undefined. Please change accordingly.`,
typeof value === 'undefined' || typeof value === 'string'
);
// priority must be specified as separate argument
// value must not contain "!important"
let priority = '';
if (value.length > 0 && value.includes('!important')) {
priority = 'important';
value = value.replace('!important', '');
}
// update CSSOM
this.element.style.setProperty(property, value, priority);
// should not remove rules that have been updated in this cycle
rulesToRemove.delete(property);
});
// remove rules that were present in last cycle but aren't present in this one
rulesToRemove.forEach((rule) => this.element.style.removeProperty(rule));
// cache styles that in this rendering cycle for the next one
this._oldStyles = new Set(newStyles.map((e) => e[0]));
}
didReceiveArguments() {
if(typeof this.args.named.delay !== 'undefined') {
setTimeout(
_ => {
if(typeof this !== this.args.positional[0]) {
this.setStyles(this.args.positional[0]);
}
},
this.args.named.delay
)
} else {
this.setStyles(this.args.positional[0]);
}
}
}

View File

@ -10,6 +10,7 @@ const notificationDefaults = function() {
return { return {
timeout: 6000, timeout: 6000,
extendedTimeout: 300, extendedTimeout: 300,
destroyOnClick: true
}; };
}; };
export default class FeedbackService extends Service { export default class FeedbackService extends Service {
@ -23,7 +24,7 @@ export default class FeedbackService extends Service {
}; };
} }
success(item, action, status = defaultStatus) { success(item, action, status = defaultStatus, model) {
const getAction = callableType(action); const getAction = callableType(action);
const getStatus = callableType(status); const getStatus = callableType(status);
// returning exactly `false` for a feedback action means even though // returning exactly `false` for a feedback action means even though
@ -38,11 +39,12 @@ export default class FeedbackService extends Service {
// here.. // here..
action: getAction(), action: getAction(),
item: item, item: item,
model: model
}); });
} }
} }
error(e, action, status = defaultStatus) { error(e, action, status = defaultStatus, model) {
const getAction = callableType(action); const getAction = callableType(action);
const getStatus = callableType(status); const getStatus = callableType(status);
this.notify.clearMessages(); this.notify.clearMessages();
@ -53,6 +55,7 @@ export default class FeedbackService extends Service {
type: getStatus(TYPE_SUCCESS), type: getStatus(TYPE_SUCCESS),
// and here // and here
action: getAction(), action: getAction(),
model: model
}); });
} else { } else {
this.notify.add({ this.notify.add({
@ -60,17 +63,18 @@ export default class FeedbackService extends Service {
type: getStatus(TYPE_ERROR, e), type: getStatus(TYPE_ERROR, e),
action: getAction(), action: getAction(),
error: e, error: e,
model: model
}); });
} }
} }
async execute(handle, action, status) { async execute(handle, action, status, routeName) {
let result; let result;
try { try {
result = await handle(); result = await handle();
this.success(result, action, status); this.success(result, action, status, routeName);
} catch (e) { } catch (e) {
this.error(e, action, status); this.error(e, action, status, routeName);
} }
} }
} }

View File

@ -17,7 +17,6 @@
@import 'consul-ui/components/empty-state'; @import 'consul-ui/components/empty-state';
@import 'consul-ui/components/expanded-single-select'; @import 'consul-ui/components/expanded-single-select';
@import 'consul-ui/components/form-elements'; @import 'consul-ui/components/form-elements';
@import 'consul-ui/components/flash-message';
@import 'consul-ui/components/icon-definition'; @import 'consul-ui/components/icon-definition';
@import 'consul-ui/components/list-row'; @import 'consul-ui/components/list-row';
@import 'consul-ui/components/inline-alert'; @import 'consul-ui/components/inline-alert';

View File

@ -38,14 +38,6 @@ as |dc partition nspace id item create|}}
<AppView <AppView
@login={{route.model.app.login.open}} @login={{route.model.app.login.open}}
> >
<BlockSlot @name="notification" as |status type item error|>
<Consul::Policy::Notifications
@type={{type}}
@status={{status}}
@item={{item}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="breadcrumbs"> <BlockSlot @name="breadcrumbs">
<ol> <ol>
<li><a data-test-back href={{href-to 'dc.acls.policies'}}>All Policies</a></li> <li><a data-test-back href={{href-to 'dc.acls.policies'}}>All Policies</a></li>

View File

@ -55,14 +55,6 @@ as |route|>
<AppView <AppView
@login={{route.model.app.login.open}} @login={{route.model.app.login.open}}
> >
<BlockSlot @name="notification" as |status type item error|>
<Consul::Policy::Notifications
@type={{type}}
@status={{status}}
@item={{item}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="header"> <BlockSlot @name="header">
<h1> <h1>
<route.Title @title="Policies" /> <route.Title @title="Policies" />

View File

@ -36,14 +36,6 @@ as |dc partition nspace item create|}}
<AppView <AppView
@login={{route.model.app.login.open}} @login={{route.model.app.login.open}}
> >
<BlockSlot @name="notification" as |status type item error|>
<Consul::Role::Notifications
@type={{type}}
@status={{status}}
@item={{item}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="breadcrumbs"> <BlockSlot @name="breadcrumbs">
<ol> <ol>
<li><a data-test-back href={{href-to 'dc.acls.roles'}}>All Roles</a></li> <li><a data-test-back href={{href-to 'dc.acls.roles'}}>All Roles</a></li>

View File

@ -49,14 +49,6 @@ as |route|>
<AppView <AppView
@login={{route.model.app.login.open}} @login={{route.model.app.login.open}}
> >
<BlockSlot @name="notification" as |status type item error|>
<Consul::Role::Notifications
@type={{type}}
@status={{status}}
@item={{item}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="header"> <BlockSlot @name="header">
<h1> <h1>
<route.Title @title="Roles" /> <route.Title @title="Roles" />

View File

@ -36,14 +36,6 @@ as |dc partition nspace item create|}}
<AppView <AppView
@login={{route.model.app.login.open}} @login={{route.model.app.login.open}}
> >
<BlockSlot @name="notification" as |status type item error|>
<Consul::Token::Notifications
@type={{type}}
@status={{status}}
@item={{item}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="breadcrumbs"> <BlockSlot @name="breadcrumbs">
<ol> <ol>
<li><a data-test-back href={{href-to 'dc.acls.tokens'}}>All Tokens</a></li> <li><a data-test-back href={{href-to 'dc.acls.tokens'}}>All Tokens</a></li>

View File

@ -52,14 +52,6 @@ as |route|>
<AppView <AppView
@login={{route.model.app.login.open}} @login={{route.model.app.login.open}}
> >
<BlockSlot @name="notification" as |status type item error|>
<Consul::Token::Notifications
@type={{type}}
@status={{status}}
@item={{item}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="header"> <BlockSlot @name="header">
<h1> <h1>
<route.Title @title="Tokens" /> <route.Title @title="Tokens" />

View File

@ -28,28 +28,58 @@ as |route|>
@login={{route.model.app.login.open}} @login={{route.model.app.login.open}}
/> />
</BlockSlot> </BlockSlot>
<BlockSlot @name="disconnected" as |Notification|> <BlockSlot @name="disconnected" as |after|>
{{#if (eq loader.error.status "404")}} {{#if (eq loader.error.status "404")}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="warning notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
This KV or parent of this KV was deleted. This KV or parent of this KV was deleted.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{else if (eq loader.error.status "403")}} {{else if (eq loader.error.status "403")}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="error notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header>
<strong>Error!</strong> <strong>Error!</strong>
</notice.Header>
<notice.Body>
<p>
You no longer have access to this KV. You no longer have access to this KV.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{else}} {{else}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="warning notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
An error was returned whilst loading this data, refresh to try again. An error was returned whilst loading this data, refresh to try again.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>

View File

@ -26,28 +26,58 @@ as |route|>
/> />
</BlockSlot> </BlockSlot>
<BlockSlot @name="disconnected" as |Notification|> <BlockSlot @name="disconnected" as |after|>
{{#if (eq loader.error.status "404")}} {{#if (eq loader.error.status "404")}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="warning notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
This node no longer exists in the catalog. This node no longer exists in the catalog.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{else if (eq loader.error.status "403")}} {{else if (eq loader.error.status "403")}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="error notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header>
<strong>Error!</strong> <strong>Error!</strong>
</notice.Header>
<notice.Body>
<p>
You no longer have access to this node You no longer have access to this node
</p> </p>
</Notification> </notice.Body>
</Notice>
{{else}} {{else}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="warning notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
An error was returned whilst loading this data, refresh to try again. An error was returned whilst loading this data, refresh to try again.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="loaded"> <BlockSlot @name="loaded">

View File

@ -21,28 +21,58 @@ as |route|>
/> />
</BlockSlot> </BlockSlot>
<BlockSlot @name="disconnected" as |Notification|> <BlockSlot @name="disconnected" as |after|>
{{#if (eq loader.error.status "404")}} {{#if (eq loader.error.status "404")}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="warning notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
This service has been deregistered and no longer exists in the catalog. This service has been deregistered and no longer exists in the catalog.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{else if (eq loader.error.status "403")}} {{else if (eq loader.error.status "403")}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="error notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header>
<strong>Error!</strong> <strong>Error!</strong>
</notice.Header>
<notice.Body>
<p>
You no longer have access to this service You no longer have access to this service
</p> </p>
</Notification> </notice.Body>
</Notice>
{{else}} {{else}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="warning notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
An error was returned whilst loading this data, refresh to try again. An error was returned whilst loading this data, refresh to try again.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>

View File

@ -19,28 +19,58 @@ as |route|>
/> />
</BlockSlot> </BlockSlot>
<BlockSlot @name="disconnected" as |Notification|> <BlockSlot @name="disconnected" as |after|>
{{#if (eq loader.error.status "404")}} {{#if (eq loader.error.status "404")}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="warning notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
This service has been deregistered and no longer exists in the catalog. This service has been deregistered and no longer exists in the catalog.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{else if (eq loader.error.status "403")}} {{else if (eq loader.error.status "403")}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="error notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header>
<strong>Error!</strong> <strong>Error!</strong>
</notice.Header>
<notice.Body>
<p>
You no longer have access to this service You no longer have access to this service
</p> </p>
</Notification> </notice.Body>
</Notice>
{{else}} {{else}}
<Notification @sticky={{true}}> <Notice
<p data-notification role="alert" class="warning notification-update"> {{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
An error was returned whilst loading this data, refresh to try again. An error was returned whilst loading this data, refresh to try again.
</p> </p>
</Notification> </notice.Body>
</Notice>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>

View File

@ -2,6 +2,20 @@
{{document-attrs class="is-debug"}} {{document-attrs class="is-debug"}}
<App class="docs" id="wrapper"> <App class="docs" id="wrapper">
<:notifications as |app|>
{{#each flashMessages.queue as |flash|}}
{{#if flash.dom}}
<app.Notification
@delay={{sub flash.timeout flash.extendedTimeout}}
@sticky={{flash.sticky}}
>
{{{flash.dom}}}
</app.Notification>
{{/if}}
{{/each}}
</:notifications>
<:main-nav> <:main-nav>
<DocfyOutput as |node|> <DocfyOutput as |node|>