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:
Jens L 2020-12-12 20:46:02 +01:00 committed by GitHub
parent 488e8f769a
commit 0a874c98cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 108 additions and 57 deletions

View File

@ -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 %}

View File

@ -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">

View File

@ -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">

View 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>`;
}
}

View File

@ -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>`;
} }
} }

View File

@ -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

View File

@ -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}>

View File

@ -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";

View File

@ -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>`

View File

@ -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>