web: cleanup messages implementation (#386)
* web: rebuild messages without template objects * web: show error message when ws connection fails * web: show error message when siteshell page not found * web: fix spinner size for loading * web: fix linting error
This commit is contained in:
		| @ -3,7 +3,7 @@ | |||||||
| {% load i18n %} | {% load i18n %} | ||||||
|  |  | ||||||
| {% block body %} | {% block body %} | ||||||
| <ak-messages></ak-messages> | <ak-message-container></ak-message-container> | ||||||
| <div class="pf-c-page"> | <div class="pf-c-page"> | ||||||
|     <a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">{% trans 'Skip to content' %}</a> |     <a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">{% trans 'Skip to content' %}</a> | ||||||
|     {% block page_content %} |     {% block page_content %} | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| {% load static %} | {% load static %} | ||||||
| {% load i18n %} | {% load i18n %} | ||||||
|  |  | ||||||
| <ak-messages></ak-messages> | <ak-message-container></ak-message-container> | ||||||
|  |  | ||||||
| <header class="pf-c-login__main-header"> | <header class="pf-c-login__main-header"> | ||||||
|     <h1 class="pf-c-title pf-m-3xl"> |     <h1 class="pf-c-title pf-m-3xl"> | ||||||
|  | |||||||
| @ -28,7 +28,7 @@ | |||||||
|         </filter> |         </filter> | ||||||
|     </svg> |     </svg> | ||||||
| </div> | </div> | ||||||
| <ak-messages></ak-messages> | <ak-message-container></ak-message-container> | ||||||
| <div class="pf-c-login"> | <div class="pf-c-login"> | ||||||
|     <div class="pf-c-login__container"> |     <div class="pf-c-login__container"> | ||||||
|         <header class="pf-c-login__header"> |         <header class="pf-c-login__header"> | ||||||
|  | |||||||
							
								
								
									
										53
									
								
								web/src/elements/messages/Message.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								web/src/elements/messages/Message.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | |||||||
|  | import { customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||||
|  |  | ||||||
|  | export interface APIMessage { | ||||||
|  |     level_tag: string; | ||||||
|  |     tags?: string; | ||||||
|  |     message: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const LEVEL_ICON_MAP: { [key: string]: string } = { | ||||||
|  |     error: "fas fa-exclamation-circle", | ||||||
|  |     warning: "fas fa-exclamation-triangle", | ||||||
|  |     success: "fas fa-check-circle", | ||||||
|  |     info: "fas fa-info", | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @customElement("ak-message") | ||||||
|  | export class Message extends LitElement { | ||||||
|  |  | ||||||
|  |     @property({attribute: false}) | ||||||
|  |     message?: APIMessage; | ||||||
|  |  | ||||||
|  |     @property({type: Number}) | ||||||
|  |     removeAfter = 3000; | ||||||
|  |  | ||||||
|  |     @property({attribute: false}) | ||||||
|  |     onRemove?: (m: APIMessage) => void; | ||||||
|  |  | ||||||
|  |     createRenderRoot(): ShadowRoot | Element { | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     firstUpdated(): void { | ||||||
|  |         setTimeout(() => { | ||||||
|  |             if (!this.message) return; | ||||||
|  |             if (!this.onRemove) return; | ||||||
|  |             this.onRemove(this.message); | ||||||
|  |         }, this.removeAfter); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render(): TemplateResult { | ||||||
|  |         return html`<li class="pf-c-alert-group__item"> | ||||||
|  |             <div class="pf-c-alert pf-m-${this.message?.level_tag} ${this.message?.level_tag === "error" ? "pf-m-danger" : ""}"> | ||||||
|  |                 <div class="pf-c-alert__icon"> | ||||||
|  |                     <i class="${this.message ? LEVEL_ICON_MAP[this.message.level_tag] : ""}"></i> | ||||||
|  |                 </div> | ||||||
|  |                 <p class="pf-c-alert__title"> | ||||||
|  |                     ${this.message?.message} | ||||||
|  |                 </p> | ||||||
|  |             </div> | ||||||
|  |         </li>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @ -1,27 +1,25 @@ | |||||||
| import { LitElement, html, customElement, TemplateResult } from "lit-element"; | import { gettext } from "django"; | ||||||
| import { DefaultClient } from "../api/client"; | import { LitElement, html, customElement, TemplateResult, property } from "lit-element"; | ||||||
|  | import { DefaultClient } from "../../api/client"; | ||||||
|  | import "./Message"; | ||||||
|  | import { APIMessage } from "./Message"; | ||||||
| 
 | 
 | ||||||
| const LEVEL_ICON_MAP: { [key: string]: string } = { | export function showMessage(message: APIMessage): void { | ||||||
|     error: "fas fa-exclamation-circle", |     const container = document.querySelector<MessageContainer>("ak-message-container"); | ||||||
|     warning: "fas fa-exclamation-triangle", |     if (!container) { | ||||||
|     success: "fas fa-check-circle", |         throw new Error("failed to find message container"); | ||||||
|     info: "fas fa-info", |     } | ||||||
| }; |     container.messages.push(message); | ||||||
| 
 |     container.requestUpdate(); | ||||||
| const ID = function (prefix: string) { |  | ||||||
|     return prefix + Math.random().toString(36).substr(2, 9); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| interface Message { |  | ||||||
|     level_tag: string; |  | ||||||
|     tags: string; |  | ||||||
|     message: string; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @customElement("ak-messages") | @customElement("ak-message-container") | ||||||
| export class Messages extends LitElement { | export class MessageContainer extends LitElement { | ||||||
|     url = DefaultClient.makeUrl(["root", "messages"]); |     url = DefaultClient.makeUrl(["root", "messages"]); | ||||||
| 
 | 
 | ||||||
|  |     @property({attribute: false}) | ||||||
|  |     messages: APIMessage[] = []; | ||||||
|  | 
 | ||||||
|     messageSocket?: WebSocket; |     messageSocket?: WebSocket; | ||||||
|     retryDelay = 200; |     retryDelay = 200; | ||||||
| 
 | 
 | ||||||
| @ -52,6 +50,12 @@ export class Messages extends LitElement { | |||||||
|         }); |         }); | ||||||
|         this.messageSocket.addEventListener("close", (e) => { |         this.messageSocket.addEventListener("close", (e) => { | ||||||
|             console.debug(`authentik/messages: closed ws connection: ${e}`); |             console.debug(`authentik/messages: closed ws connection: ${e}`); | ||||||
|  |             if (this.retryDelay > 3000) { | ||||||
|  |                 showMessage({ | ||||||
|  |                     level_tag: "error", | ||||||
|  |                     message: gettext("Connection error, reconnecting...") | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|             setTimeout(() => { |             setTimeout(() => { | ||||||
|                 console.debug(`authentik/messages: reconnecting ws in ${this.retryDelay}ms`); |                 console.debug(`authentik/messages: reconnecting ws in ${this.retryDelay}ms`); | ||||||
|                 this.connect(); |                 this.connect(); | ||||||
| @ -60,7 +64,8 @@ export class Messages extends LitElement { | |||||||
|         }); |         }); | ||||||
|         this.messageSocket.addEventListener("message", (e) => { |         this.messageSocket.addEventListener("message", (e) => { | ||||||
|             const data = JSON.parse(e.data); |             const data = JSON.parse(e.data); | ||||||
|             this.renderMessage(data); |             this.messages.push(data); | ||||||
|  |             this.requestUpdate(); | ||||||
|         }); |         }); | ||||||
|         this.messageSocket.addEventListener("error", (e) => { |         this.messageSocket.addEventListener("error", (e) => { | ||||||
|             console.warn(`authentik/messages: error ${e}`); |             console.warn(`authentik/messages: error ${e}`); | ||||||
| @ -75,38 +80,25 @@ export class Messages extends LitElement { | |||||||
|         console.debug("authentik/messages: fetching messages over direct api"); |         console.debug("authentik/messages: fetching messages over direct api"); | ||||||
|         return fetch(this.url) |         return fetch(this.url) | ||||||
|             .then((r) => r.json()) |             .then((r) => r.json()) | ||||||
|             .then((r: Message[]) => { |             .then((r: APIMessage[]) => { | ||||||
|                 r.forEach((m: Message) => { |                 r.forEach((m: APIMessage) => { | ||||||
|                     this.renderMessage(m); |                     this.messages.push(m); | ||||||
|  |                     this.requestUpdate(); | ||||||
|                 }); |                 }); | ||||||
|             }); |             }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     renderMessage(message: Message): void { |  | ||||||
|         const container = <HTMLElement>this.querySelector(".pf-c-alert-group"); |  | ||||||
|         if (!container) { |  | ||||||
|             console.warn("authentik/messages: failed to find container"); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         const id = ID("ak-message"); |  | ||||||
|         const el = document.createElement("template"); |  | ||||||
|         el.innerHTML = `<li id=${id} class="pf-c-alert-group__item">
 |  | ||||||
|             <div class="pf-c-alert pf-m-${message.level_tag} ${message.level_tag === "error" ? "pf-m-danger" : ""}"> |  | ||||||
|                 <div class="pf-c-alert__icon"> |  | ||||||
|                     <i class="${LEVEL_ICON_MAP[message.level_tag]}"></i> |  | ||||||
|                 </div> |  | ||||||
|                 <p class="pf-c-alert__title"> |  | ||||||
|                     ${message.message} |  | ||||||
|                 </p> |  | ||||||
|             </div> |  | ||||||
|         </li>`;
 |  | ||||||
|         setTimeout(() => { |  | ||||||
|             this.querySelector(`#${id}`)?.remove(); |  | ||||||
|         }, 1500); |  | ||||||
|         container.appendChild(el.content.firstChild!); // eslint-disable-line
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         return html`<ul class="pf-c-alert-group pf-m-toast"></ul>`; |         return html`<ul class="pf-c-alert-group pf-m-toast">
 | ||||||
|  |             ${this.messages.map((m) => { | ||||||
|  |         return html`<ak-message
 | ||||||
|  |                     .message=${m} | ||||||
|  |                     .onRemove=${(m: APIMessage) => { | ||||||
|  |         this.messages = this.messages.filter((v) => v !== m); | ||||||
|  |         this.requestUpdate(); | ||||||
|  |     }}> | ||||||
|  |                 </ak-message>`; | ||||||
|  |     })} | ||||||
|  |         </ul>`;
 | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -39,7 +39,7 @@ | |||||||
|         <script src="/static/dist/main.js" type="module"></script> |         <script src="/static/dist/main.js" type="module"></script> | ||||||
|     </head> |     </head> | ||||||
|     <body> |     <body> | ||||||
|         <ak-messages></ak-messages> |         <ak-message-container></ak-message-container> | ||||||
|         <div class="pf-c-page"> |         <div class="pf-c-page"> | ||||||
|             <a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content" |             <a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content" | ||||||
|                 >Skip to content</a |                 >Skip to content</a | ||||||
|  | |||||||
| @ -2,8 +2,8 @@ import { gettext } from "django"; | |||||||
| import { html, LitElement, TemplateResult } from "lit-element"; | import { html, LitElement, TemplateResult } from "lit-element"; | ||||||
| import { SidebarItem } from "../elements/sidebar/Sidebar"; | import { SidebarItem } from "../elements/sidebar/Sidebar"; | ||||||
|  |  | ||||||
| import "../elements/Messages"; |  | ||||||
| import "../elements/router/RouterOutlet"; | import "../elements/router/RouterOutlet"; | ||||||
|  | import "../elements/messages/MessageContainer"; | ||||||
|  |  | ||||||
| export abstract class Interface extends LitElement { | export abstract class Interface extends LitElement { | ||||||
|  |  | ||||||
| @ -14,7 +14,7 @@ export abstract class Interface extends LitElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         return html`<ak-messages></ak-messages> |         return html`<ak-message-container></ak-message-container> | ||||||
|             <div class="pf-c-page"> |             <div class="pf-c-page"> | ||||||
|                 <a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">${gettext("Skip to content")}</a> |                 <a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">${gettext("Skip to content")}</a> | ||||||
|                 <ak-sidebar class="pf-c-page__sidebar" .items=${this.sidebar}> |                 <ak-sidebar class="pf-c-page__sidebar" .items=${this.sidebar}> | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ import "./elements/EmptyState"; | |||||||
| import "./elements/cards/AggregateCard"; | import "./elements/cards/AggregateCard"; | ||||||
| import "./elements/cards/AggregatePromiseCard"; | import "./elements/cards/AggregatePromiseCard"; | ||||||
| import "./elements/CodeMirror"; | import "./elements/CodeMirror"; | ||||||
| import "./elements/Messages"; | import "./elements/messages/MessageContainer"; | ||||||
| import "./elements/Spinner"; | import "./elements/Spinner"; | ||||||
| import "./elements/Tabs"; | import "./elements/Tabs"; | ||||||
| import "./elements/router/RouterOutlet"; | import "./elements/router/RouterOutlet"; | ||||||
|  | |||||||
| @ -6,6 +6,8 @@ import SpinnerStyle from "@patternfly/patternfly/components/Spinner/spinner.css" | |||||||
| // @ts-ignore | // @ts-ignore | ||||||
| import BackdropStyle from "@patternfly/patternfly/components/Backdrop/backdrop.css"; | import BackdropStyle from "@patternfly/patternfly/components/Backdrop/backdrop.css"; | ||||||
| import { SpinnerSize } from "../../elements/Spinner"; | import { SpinnerSize } from "../../elements/Spinner"; | ||||||
|  | import { showMessage } from "../../elements/messages/MessageContainer"; | ||||||
|  | import { gettext } from "django"; | ||||||
|  |  | ||||||
| @customElement("ak-site-shell") | @customElement("ak-site-shell") | ||||||
| export class SiteShell extends LitElement { | export class SiteShell extends LitElement { | ||||||
| @ -64,6 +66,10 @@ export class SiteShell extends LitElement { | |||||||
|                 } |                 } | ||||||
|                 console.debug(`authentik/site-shell: Request failed ${this._url}`); |                 console.debug(`authentik/site-shell: Request failed ${this._url}`); | ||||||
|                 window.location.hash = "#/"; |                 window.location.hash = "#/"; | ||||||
|  |                 showMessage({ | ||||||
|  |                     level_tag: "error", | ||||||
|  |                     message: gettext(`Request failed: ${r.statusText}`), | ||||||
|  |                 }); | ||||||
|                 throw new Error("Request failed"); |                 throw new Error("Request failed"); | ||||||
|             }) |             }) | ||||||
|             .then((r) => r.text()) |             .then((r) => r.text()) | ||||||
| @ -115,7 +121,7 @@ export class SiteShell extends LitElement { | |||||||
|             html`<div class="pf-c-backdrop"> |             html`<div class="pf-c-backdrop"> | ||||||
|                     <div class="pf-l-bullseye"> |                     <div class="pf-l-bullseye"> | ||||||
|                         <div class="pf-l-bullseye__item"> |                         <div class="pf-l-bullseye__item"> | ||||||
|                             <ak-spinner size=${SpinnerSize.Large}></ak-spinner> |                             <ak-spinner size=${SpinnerSize.XLarge}></ak-spinner> | ||||||
|                         </div> |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div>` |                 </div>` | ||||||
|  | |||||||
| @ -42,7 +42,7 @@ export function loading<T>(v: T, actual: TemplateResult): TemplateResult { | |||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <div class="pf-l-bullseye"> |                 <div class="pf-l-bullseye"> | ||||||
|                     <div class="pf-l-bullseye__item"> |                     <div class="pf-l-bullseye__item"> | ||||||
|                         <ak-spinner size="${SpinnerSize.Large}"></ak-spinner> |                         <ak-spinner size="${SpinnerSize.XLarge}"></ak-spinner> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L