web: update to new formatting rules, make eslint warnings fail ci

This commit is contained in:
Jens Langhammer 2020-12-01 17:27:19 +01:00
parent 7195b77606
commit e6391b64f0
33 changed files with 192 additions and 259 deletions

View file

@ -25,7 +25,7 @@ class ChannelsStorage(FallbackStorage):
uid, uid,
{ {
"type": "event.update", "type": "event.update",
"levelTag": message.level_tag, "level_tag": message.level_tag,
"tags": message.tags, "tags": message.tags,
"message": message.message, "message": message.message,
}, },

View file

@ -3,7 +3,7 @@
"scripts": { "scripts": {
"build": "rollup -c ./rollup.config.js", "build": "rollup -c ./rollup.config.js",
"watch": "rollup -c -w", "watch": "rollup -c -w",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx" "lint": "eslint . --max-warnings 0"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1", "@fortawesome/fontawesome-free": "^5.15.1",

View file

@ -1,4 +1,4 @@
import { DefaultClient, PBResponse } from "./client"; import { DefaultClient, PBResponse, QueryArguments } from "./client";
export class Application { export class Application {
pk: string; pk: string;
@ -21,7 +21,7 @@ export class Application {
return DefaultClient.fetch<Application>(["core", "applications", slug]); return DefaultClient.fetch<Application>(["core", "applications", slug]);
} }
static list(filter?: { [key: string]: any }): Promise<PBResponse<Application>> { static list(filter?: QueryArguments): Promise<PBResponse<Application>> {
return DefaultClient.fetch<PBResponse<Application>>(["core", "applications"], filter); return DefaultClient.fetch<PBResponse<Application>>(["core", "applications"], filter);
} }
} }

View file

@ -2,8 +2,12 @@ import { NotFoundError, RequestError } from "./errors";
export const VERSION = "v2beta"; export const VERSION = "v2beta";
export interface QueryArguments {
[key: string]: number | string | boolean;
}
export class Client { export class Client {
makeUrl(url: string[], query?: { [key: string]: string }): string { makeUrl(url: string[], query?: QueryArguments): string {
let builtUrl = `/api/${VERSION}/${url.join("/")}/`; let builtUrl = `/api/${VERSION}/${url.join("/")}/`;
if (query) { if (query) {
const queryString = Object.keys(query) const queryString = Object.keys(query)
@ -14,7 +18,7 @@ export class Client {
return builtUrl; return builtUrl;
} }
fetch<T>(url: string[], query?: { [key: string]: any }): Promise<T> { fetch<T>(url: string[], query?: QueryArguments): Promise<T> {
const finalUrl = this.makeUrl(url, query); const finalUrl = this.makeUrl(url, query);
return fetch(finalUrl) return fetch(finalUrl)
.then((r) => { .then((r) => {

View file

@ -1,7 +1,7 @@
export interface Policy { export interface Policy {
pk: string; pk: string;
name: string; name: string;
[key: string]: any; [key: string]: unknown;
} }
export interface PolicyBinding { export interface PolicyBinding {

View file

@ -6,5 +6,6 @@ import PFAddons from "@patternfly/patternfly/patternfly-addons.css";
import FA from "@fortawesome/fontawesome-free/css/fontawesome.css"; import FA from "@fortawesome/fontawesome-free/css/fontawesome.css";
// @ts-ignore // @ts-ignore
import PBGlobal from "../passbook.css"; import PBGlobal from "../passbook.css";
import { CSSResult } from "lit-element";
export const COMMON_STYLES = [PF, PFAddons, FA, PBGlobal]; export const COMMON_STYLES: CSSResult[] = [PF, PFAddons, FA, PBGlobal];

2
web/src/django.d.ts vendored
View file

@ -6,5 +6,5 @@ declare namespace django {
function ngettext(singular: string, plural: string, count: number): string; function ngettext(singular: string, plural: string, count: number): string;
function gettext_noop(msgid: string): string; function gettext_noop(msgid: string): string;
function pgettext(context: string, msgid: string): string; function pgettext(context: string, msgid: string): string;
function interpolate(fmt: string, obj: any, named: boolean): string; function interpolate(fmt: string, obj: unknown, named: boolean): string;
} }

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import Chart from "chart.js"; import Chart from "chart.js";
interface TickValue { interface TickValue {
@ -11,10 +11,10 @@ export class AdminLoginsChart extends LitElement {
@property() @property()
url = ""; url = "";
chart: any; chart?: Chart;
static get styles() { static get styles(): CSSResult[] {
return css` return [css`
:host { :host {
position: relative; position: relative;
height: 100%; height: 100%;
@ -26,7 +26,7 @@ export class AdminLoginsChart extends LitElement {
width: 100px; width: 100px;
height: 100px; height: 100px;
} }
`; `];
} }
constructor() { constructor() {
@ -38,14 +38,21 @@ export class AdminLoginsChart extends LitElement {
}); });
} }
firstUpdated() { firstUpdated(): void {
fetch(this.url) fetch(this.url)
.then((r) => r.json()) .then((r) => r.json())
.catch((e) => console.error(e)) .catch((e) => console.error(e))
.then((r) => { .then((r) => {
const ctx = (<HTMLCanvasElement>this.shadowRoot?.querySelector("canvas")).getContext( const canvas = <HTMLCanvasElement>this.shadowRoot?.querySelector("canvas");
"2d" if (!canvas) {
)!; console.warn("Failed to get canvas element");
return false;
}
const ctx = canvas.getContext("2d");
if (!ctx) {
console.warn("failed to get 2d context");
return false;
}
this.chart = new Chart(ctx, { this.chart = new Chart(ctx, {
type: "bar", type: "bar",
data: { data: {
@ -102,7 +109,7 @@ export class AdminLoginsChart extends LitElement {
}); });
} }
render() { render(): TemplateResult {
return html`<canvas></canvas>`; return html`<canvas></canvas>`;
} }
} }

View file

@ -1,4 +1,4 @@
import { customElement, html, LitElement, property } from "lit-element"; import { customElement, LitElement, property } from "lit-element";
// @ts-ignore // @ts-ignore
import CodeMirror from "codemirror"; import CodeMirror from "codemirror";
@ -17,11 +17,11 @@ export class CodeMirrorTextarea extends LitElement {
editor?: CodeMirror.EditorFromTextArea; editor?: CodeMirror.EditorFromTextArea;
createRenderRoot() { createRenderRoot() : ShadowRoot | Element {
return this; return this;
} }
firstUpdated() { firstUpdated(): void {
const textarea = this.querySelector("textarea"); const textarea = this.querySelector("textarea");
if (!textarea) { if (!textarea) {
return; return;
@ -33,7 +33,7 @@ export class CodeMirrorTextarea extends LitElement {
readOnly: this.readOnly, readOnly: this.readOnly,
autoRefresh: true, autoRefresh: true,
}); });
this.editor.on("blur", (e) => { this.editor.on("blur", () => {
this.editor?.save(); this.editor?.save();
}); });
} }

View file

@ -1,91 +0,0 @@
import { LitElement, html, customElement, property } from "lit-element";
interface ComparisonHash {
[key: string]: (a: any, b: any) => boolean;
}
@customElement("fetch-fill-slot")
export class FetchFillSlot extends LitElement {
@property()
url = "";
@property()
key = "";
@property()
value = "";
comparison(slotName: string) {
const comparisonOperatorsHash = <ComparisonHash>{
"<": function (a: any, b: any) {
return a < b;
},
">": function (a: any, b: any) {
return a > b;
},
">=": function (a: any, b: any) {
return a >= b;
},
"<=": function (a: any, b: any) {
return a <= b;
},
"==": function (a: any, b: any) {
return a == b;
},
"!=": function (a: any, b: any) {
return a != b;
},
"===": function (a: any, b: any) {
return a === b;
},
"!==": function (a: any, b: any) {
return a !== b;
},
};
const tokens = slotName.split(" ");
if (tokens.length < 3) {
throw new Error("nah");
}
let a: any = tokens[0];
if (a === "value") {
a = this.value;
} else {
a = parseInt(a, 10);
}
let b: any = tokens[2];
if (b === "value") {
b = this.value;
} else {
b = parseInt(b, 10);
}
const comp = tokens[1];
if (!(comp in comparisonOperatorsHash)) {
throw new Error("Invalid comparison");
}
return comparisonOperatorsHash[comp](a, b);
}
firstUpdated() {
fetch(this.url)
.then((r) => r.json())
.then((r) => r[this.key])
.then((r) => (this.value = r));
}
render() {
if (this.value === undefined) {
return html`<slot></slot>`;
}
let selectedSlot = "";
this.querySelectorAll("[slot]").forEach((slot) => {
const comp = slot.getAttribute("slot")!;
if (this.comparison(comp)) {
selectedSlot = comp;
}
});
this.querySelectorAll("[data-value]").forEach((dv) => {
dv.textContent = this.value;
});
return html`<slot name=${selectedSlot}></slot>`;
}
}

View file

@ -1,4 +1,4 @@
import { LitElement, html, customElement, property } from "lit-element"; import { LitElement, html, customElement, property, TemplateResult } from "lit-element";
const LEVEL_ICON_MAP: { [key: string]: string } = { const LEVEL_ICON_MAP: { [key: string]: string } = {
error: "fas fa-exclamation-circle", error: "fas fa-exclamation-circle",
@ -12,7 +12,7 @@ const ID = function (prefix: string) {
}; };
interface Message { interface Message {
levelTag: string; level_tag: string;
tags: string; tags: string;
message: string; message: string;
} }
@ -25,7 +25,7 @@ export class Messages extends LitElement {
messageSocket?: WebSocket; messageSocket?: WebSocket;
retryDelay = 200; retryDelay = 200;
createRenderRoot() { createRenderRoot(): ShadowRoot | Element {
return this; return this;
} }
@ -38,16 +38,16 @@ export class Messages extends LitElement {
} }
} }
firstUpdated() { firstUpdated(): void {
this.fetchMessages(); this.fetchMessages();
} }
connect() { connect(): void {
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${ const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
window.location.host window.location.host
}/ws/client/`; }/ws/client/`;
this.messageSocket = new WebSocket(wsUrl); this.messageSocket = new WebSocket(wsUrl);
this.messageSocket.addEventListener("open", (e) => { this.messageSocket.addEventListener("open", () => {
console.debug(`passbook/messages: connected to ${wsUrl}`); console.debug(`passbook/messages: connected to ${wsUrl}`);
}); });
this.messageSocket.addEventListener("close", (e) => { this.messageSocket.addEventListener("close", (e) => {
@ -71,32 +71,29 @@ export class Messages extends LitElement {
/* Fetch messages which were stored in the session. /* Fetch messages which were stored in the session.
* This mostly gets messages which were created when the user arrives/leaves the site * This mostly gets messages which were created when the user arrives/leaves the site
* and especially the login flow */ * and especially the login flow */
fetchMessages() { fetchMessages(): Promise<void> {
console.debug("passbook/messages: fetching messages over direct api"); console.debug("passbook/messages: fetching messages over direct api");
return fetch(this.url) return fetch(this.url)
.then((r) => r.json()) .then((r) => r.json())
.then((r) => { .then((r: Message[]) => {
r.forEach((m: any) => { r.forEach((m: Message) => {
const message = <Message>{ this.renderMessage(m);
levelTag: m.level_tag,
tags: m.tags,
message: m.message,
};
this.renderMessage(message);
}); });
}); });
} }
renderMessage(message: Message) { renderMessage(message: Message): void {
const container = <HTMLElement>this.querySelector(".pf-c-alert-group")!; const container = <HTMLElement>this.querySelector(".pf-c-alert-group");
if (!container) {
console.warn("passbook/messages: failed to find container");
return;
}
const id = ID("pb-message"); const id = ID("pb-message");
const el = document.createElement("template"); const el = document.createElement("template");
el.innerHTML = `<li id=${id} class="pf-c-alert-group__item"> el.innerHTML = `<li id=${id} class="pf-c-alert-group__item">
<div class="pf-c-alert pf-m-${message.levelTag} ${ <div class="pf-c-alert pf-m-${message.level_tag} ${message.level_tag === "error" ? "pf-m-danger" : ""}">
message.levelTag === "error" ? "pf-m-danger" : ""
}">
<div class="pf-c-alert__icon"> <div class="pf-c-alert__icon">
<i class="${LEVEL_ICON_MAP[message.levelTag]}"></i> <i class="${LEVEL_ICON_MAP[message.level_tag]}"></i>
</div> </div>
<p class="pf-c-alert__title"> <p class="pf-c-alert__title">
${message.message} ${message.message}
@ -106,10 +103,10 @@ export class Messages extends LitElement {
setTimeout(() => { setTimeout(() => {
this.querySelector(`#${id}`)?.remove(); this.querySelector(`#${id}`)?.remove();
}, 1500); }, 1500);
container.appendChild(el.content.firstChild!); container.appendChild(el.content.firstChild!); // eslint-disable-line
} }
render() { 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"></ul>`;
} }
} }

View file

@ -1,5 +1,5 @@
import { gettext } from "django"; import { gettext } from "django";
import { customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import SpinnerStyle from "@patternfly/patternfly/components/Spinner/spinner.css"; import SpinnerStyle from "@patternfly/patternfly/components/Spinner/spinner.css";
@ -15,7 +15,7 @@ export class Spinner extends LitElement {
@property() @property()
size: SpinnerSize = SpinnerSize.Medium; size: SpinnerSize = SpinnerSize.Medium;
static get styles() { static get styles(): CSSResult[] {
return [SpinnerStyle]; return [SpinnerStyle];
} }

View file

@ -1,4 +1,4 @@
import { LitElement, html, customElement, property } from "lit-element"; import { LitElement, html, customElement, property, CSSResult, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import TabsStyle from "@patternfly/patternfly/components/Tabs/tabs.css"; import TabsStyle from "@patternfly/patternfly/components/Tabs/tabs.css";
// @ts-ignore // @ts-ignore
@ -10,12 +10,23 @@ export class Tabs extends LitElement {
@property() @property()
currentPage?: string; currentPage?: string;
static get styles() { static get styles(): CSSResult[] {
return [GlobalsStyle, TabsStyle]; return [GlobalsStyle, TabsStyle];
} }
render() { renderTab(page: Element): TemplateResult {
const pages = Array.from(this.querySelectorAll("[slot]")!); const slot = page.attributes.getNamedItem("slot")?.value;
return html` <li class="pf-c-tabs__item ${slot === this.currentPage ? CURRENT_CLASS : ""}">
<button class="pf-c-tabs__link" @click=${() => { this.currentPage = slot; }}>
<span class="pf-c-tabs__item-text">
${page.attributes.getNamedItem("tab-title")?.value}
</span>
</button>
</li>`;
}
render(): TemplateResult {
const pages = Array.from(this.querySelectorAll("[slot]"));
if (!this.currentPage) { if (!this.currentPage) {
if (pages.length < 1) { if (pages.length < 1) {
return html`<h1>no tabs defined</h1>`; return html`<h1>no tabs defined</h1>`;
@ -24,25 +35,7 @@ export class Tabs extends LitElement {
} }
return html`<div class="pf-c-tabs"> return html`<div class="pf-c-tabs">
<ul class="pf-c-tabs__list"> <ul class="pf-c-tabs__list">
${pages.map((page) => { ${pages.map((page) => this.renderTab(page))}
const slot = page.attributes.getNamedItem("slot")?.value;
return html` <li
class="pf-c-tabs__item ${slot === this.currentPage
? CURRENT_CLASS
: ""}"
>
<button
class="pf-c-tabs__link"
@click=${() => {
this.currentPage = slot;
}}
>
<span class="pf-c-tabs__item-text">
${page.attributes.getNamedItem("tab-title")?.value}
</span>
</button>
</li>`;
})}
</ul> </ul>
</div> </div>
<slot name="${this.currentPage}"></slot>`; <slot name="${this.currentPage}"></slot>`;

View file

@ -1,5 +1,5 @@
import { getCookie } from "../../utils"; import { getCookie } from "../../utils";
import { customElement, html, property } from "lit-element"; import { customElement, property } from "lit-element";
import { ERROR_CLASS, SUCCESS_CLASS } from "../../constants"; import { ERROR_CLASS, SUCCESS_CLASS } from "../../constants";
import { SpinnerButton } from "./SpinnerButton"; import { SpinnerButton } from "./SpinnerButton";
@ -8,21 +8,26 @@ export class ActionButton extends SpinnerButton {
@property() @property()
url = ""; url = "";
callAction() { callAction(): void {
if (this.isRunning === true) { if (this.isRunning === true) {
return; return;
} }
this.setLoading(); this.setLoading();
const csrftoken = getCookie("passbook_csrf"); const csrftoken = getCookie("passbook_csrf");
if (!csrftoken) {
console.debug("No csrf token in cookie");
this.setDone(ERROR_CLASS);
return;
}
const request = new Request(this.url, { const request = new Request(this.url, {
headers: { "X-CSRFToken": csrftoken! }, headers: { "X-CSRFToken": csrftoken },
}); });
fetch(request, { fetch(request, {
method: "POST", method: "POST",
mode: "same-origin", mode: "same-origin",
}) })
.then((r) => r.json()) .then((r) => r.json())
.then((r) => { .then(() => {
this.setDone(SUCCESS_CLASS); this.setDone(SUCCESS_CLASS);
}) })
.catch(() => { .catch(() => {

View file

@ -1,18 +1,18 @@
import { customElement, html, LitElement } from "lit-element"; import { customElement, html, LitElement, TemplateResult } from "lit-element";
@customElement("pb-dropdown") @customElement("pb-dropdown")
export class DropdownButton extends LitElement { export class DropdownButton extends LitElement {
constructor() { constructor() {
super(); super();
const menu = <HTMLElement>this.querySelector(".pf-c-dropdown__menu")!; const menu = <HTMLElement>this.querySelector(".pf-c-dropdown__menu");
this.querySelectorAll("button.pf-c-dropdown__toggle").forEach((btn) => { this.querySelectorAll("button.pf-c-dropdown__toggle").forEach((btn) => {
btn.addEventListener("click", (e) => { btn.addEventListener("click", () => {
menu.hidden = !menu.hidden; menu.hidden = !menu.hidden;
}); });
}); });
} }
render() { render(): TemplateResult {
return html`<slot></slot>`; return html`<slot></slot>`;
} }
} }

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import ModalBoxStyle from "@patternfly/patternfly/components/ModalBox/modal-box.css"; import ModalBoxStyle from "@patternfly/patternfly/components/ModalBox/modal-box.css";
// @ts-ignore // @ts-ignore
@ -22,7 +22,7 @@ export class ModalButton extends LitElement {
@property() @property()
open = false; open = false;
static get styles() { static get styles(): CSSResult[] {
return [ return [
css` css`
:host { :host {
@ -49,7 +49,7 @@ export class ModalButton extends LitElement {
}); });
} }
updateHandlers() { updateHandlers(): void {
// Ensure links close the modal // Ensure links close the modal
this.querySelectorAll<HTMLAnchorElement>("[slot=modal] a").forEach((a) => { this.querySelectorAll<HTMLAnchorElement>("[slot=modal] a").forEach((a) => {
if (a.target == "_blank") { if (a.target == "_blank") {
@ -63,7 +63,7 @@ export class ModalButton extends LitElement {
}); });
// Make name field update slug field // Make name field update slug field
this.querySelectorAll<HTMLInputElement>("input[name=name]").forEach((input) => { this.querySelectorAll<HTMLInputElement>("input[name=name]").forEach((input) => {
input.addEventListener("input", (e) => { input.addEventListener("input", () => {
const form = input.closest("form"); const form = input.closest("form");
if (form === null) { if (form === null) {
return; return;
@ -90,7 +90,12 @@ export class ModalButton extends LitElement {
}) })
.then((data) => { .then((data) => {
if (data.indexOf("csrfmiddlewaretoken") !== -1) { if (data.indexOf("csrfmiddlewaretoken") !== -1) {
this.querySelector("[slot=modal]")!.innerHTML = data; const modalSlot = this.querySelector("[slot=modal]");
if (!modalSlot) {
console.debug("passbook/modalbutton: modal slot not found?");
return;
}
modalSlot.innerHTML = data;
console.debug("passbook/modalbutton: re-showing form"); console.debug("passbook/modalbutton: re-showing form");
this.updateHandlers(); this.updateHandlers();
} else { } else {
@ -110,7 +115,7 @@ export class ModalButton extends LitElement {
}); });
} }
onClick(e: MouseEvent) { onClick(): void {
if (!this.href) { if (!this.href) {
this.updateHandlers(); this.updateHandlers();
this.open = true; this.open = true;
@ -121,7 +126,11 @@ export class ModalButton extends LitElement {
}) })
.then((r) => r.text()) .then((r) => r.text())
.then((t) => { .then((t) => {
this.querySelector("[slot=modal]")!.innerHTML = t; const modalSlot = this.querySelector("[slot=modal]");
if (!modalSlot) {
return;
}
modalSlot.innerHTML = t;
this.updateHandlers(); this.updateHandlers();
this.open = true; this.open = true;
this.querySelectorAll<SpinnerButton>("pb-spinner-button").forEach((sb) => { this.querySelectorAll<SpinnerButton>("pb-spinner-button").forEach((sb) => {
@ -134,7 +143,7 @@ export class ModalButton extends LitElement {
} }
} }
renderModal() { renderModal(): TemplateResult {
return html`<div class="pf-c-backdrop"> return html`<div class="pf-c-backdrop">
<div class="pf-l-bullseye"> <div class="pf-l-bullseye">
<div <div
@ -158,8 +167,8 @@ export class ModalButton extends LitElement {
</div>`; </div>`;
} }
render() { render(): TemplateResult {
return html` <slot name="trigger" @click=${(e: any) => this.onClick(e)}></slot> return html` <slot name="trigger" @click=${() => this.onClick()}></slot>
${this.open ? this.renderModal() : ""}`; ${this.open ? this.renderModal() : ""}`;
} }
} }

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css"; import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
// @ts-ignore // @ts-ignore
@ -15,7 +15,7 @@ export class SpinnerButton extends LitElement {
@property() @property()
form?: string; form?: string;
static get styles() { static get styles(): CSSResult[] {
return [ return [
GlobalsStyle, GlobalsStyle,
ButtonStyle, ButtonStyle,
@ -35,13 +35,13 @@ export class SpinnerButton extends LitElement {
this.classList.add(PRIMARY_CLASS); this.classList.add(PRIMARY_CLASS);
} }
setLoading() { setLoading(): void {
this.isRunning = true; this.isRunning = true;
this.classList.add(PROGRESS_CLASS); this.classList.add(PROGRESS_CLASS);
this.requestUpdate(); this.requestUpdate();
} }
setDone(statusClass: string) { setDone(statusClass: string): void {
this.isRunning = false; this.isRunning = false;
this.classList.remove(PROGRESS_CLASS); this.classList.remove(PROGRESS_CLASS);
this.classList.replace(PRIMARY_CLASS, statusClass); this.classList.replace(PRIMARY_CLASS, statusClass);
@ -52,7 +52,7 @@ export class SpinnerButton extends LitElement {
}, 1000); }, 1000);
} }
callAction() { callAction(): void {
if (this.isRunning === true) { if (this.isRunning === true) {
return; return;
} }
@ -64,7 +64,7 @@ export class SpinnerButton extends LitElement {
this.setLoading(); this.setLoading();
} }
render() { render(): TemplateResult {
return html`<button return html`<button
class="pf-c-button pf-m-progress ${this.classList.toString()}" class="pf-c-button pf-m-progress ${this.classList.toString()}"
@click=${() => this.callAction()} @click=${() => this.callAction()}

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css"; import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
// @ts-ignore // @ts-ignore
@ -14,7 +14,7 @@ export class TokenCopyButton extends LitElement {
@property() @property()
buttonClass: string = PRIMARY_CLASS; buttonClass: string = PRIMARY_CLASS;
static get styles() { static get styles(): CSSResult[] {
return [ return [
GlobalsStyle, GlobalsStyle,
ButtonStyle, ButtonStyle,
@ -27,7 +27,7 @@ export class TokenCopyButton extends LitElement {
]; ];
} }
onClick() { onClick(): void {
if (!this.identifier) { if (!this.identifier) {
this.buttonClass = ERROR_CLASS; this.buttonClass = ERROR_CLASS;
setTimeout(() => { setTimeout(() => {
@ -45,7 +45,7 @@ export class TokenCopyButton extends LitElement {
}); });
} }
render() { render(): TemplateResult {
return html`<button @click=${() => this.onClick()} class="pf-c-button ${this.buttonClass}"> return html`<button @click=${() => this.onClick()} class="pf-c-button ${this.buttonClass}">
<slot></slot> <slot></slot>
</button>`; </button>`;

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import PageStyle from "@patternfly/patternfly/components/Page/page.css"; import PageStyle from "@patternfly/patternfly/components/Page/page.css";
// @ts-ignore // @ts-ignore
@ -23,7 +23,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
{ {
name: "Monitor", name: "Monitor",
path: ["/audit/audit/"], path: ["/audit/audit/"],
condition: (sb: Sidebar) => { condition: (sb: Sidebar): boolean => {
return sb.user?.is_superuser || false; return sb.user?.is_superuser || false;
}, },
}, },
@ -123,7 +123,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
path: ["/administration/tokens/"], path: ["/administration/tokens/"],
}, },
], ],
condition: (sb: Sidebar) => { condition: (sb: Sidebar): boolean => {
return sb.user?.is_superuser || false; return sb.user?.is_superuser || false;
}, },
}, },
@ -137,7 +137,7 @@ export class Sidebar extends LitElement {
@property() @property()
user?: User; user?: User;
static get styles() { static get styles(): CSSResult[] {
return [ return [
GlobalsStyle, GlobalsStyle,
PageStyle, PageStyle,
@ -169,7 +169,7 @@ export class Sidebar extends LitElement {
super(); super();
User.me().then((u) => (this.user = u)); User.me().then((u) => (this.user = u));
this.activePath = window.location.hash.slice(1, Infinity); this.activePath = window.location.hash.slice(1, Infinity);
window.addEventListener("hashchange", (e) => { window.addEventListener("hashchange", () => {
this.activePath = window.location.hash.slice(1, Infinity); this.activePath = window.location.hash.slice(1, Infinity);
}); });
} }
@ -200,7 +200,7 @@ export class Sidebar extends LitElement {
</li>`; </li>`;
} }
render() { render(): TemplateResult {
return html`<div class="pf-c-page__sidebar-body"> return html`<div class="pf-c-page__sidebar-body">
<nav class="pf-c-nav" aria-label="Global"> <nav class="pf-c-nav" aria-label="Global">
<ul class="pf-c-nav__list"> <ul class="pf-c-nav__list">

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import PageStyle from "@patternfly/patternfly/components/Page/page.css"; import PageStyle from "@patternfly/patternfly/components/Page/page.css";
// @ts-ignore // @ts-ignore
@ -8,6 +8,10 @@ import { Config } from "../../api/config";
export const DefaultConfig: Config = { export const DefaultConfig: Config = {
branding_logo: " /static/dist/assets/images/logo.svg", branding_logo: " /static/dist/assets/images/logo.svg",
branding_title: "passbook", branding_title: "passbook",
error_reporting_enabled: false,
error_reporting_environment: "",
error_reporting_send_pii: false,
}; };
@customElement("pb-sidebar-brand") @customElement("pb-sidebar-brand")
@ -15,7 +19,7 @@ export class SidebarBrand extends LitElement {
@property() @property()
config: Config = DefaultConfig; config: Config = DefaultConfig;
static get styles() { static get styles(): CSSResult[] {
return [ return [
GlobalsStyle, GlobalsStyle,
PageStyle, PageStyle,
@ -45,7 +49,7 @@ export class SidebarBrand extends LitElement {
Config.get().then((c) => (this.config = c)); Config.get().then((c) => (this.config = c));
} }
render() { render(): TemplateResult {
if (!this.config) { if (!this.config) {
return html``; return html``;
} }

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import NavStyle from "@patternfly/patternfly/components/Nav/nav.css"; import NavStyle from "@patternfly/patternfly/components/Nav/nav.css";
// @ts-ignore // @ts-ignore
@ -12,7 +12,7 @@ export class SidebarUser extends LitElement {
@property() @property()
user?: User; user?: User;
static get styles() { static get styles(): CSSResult[] {
return [ return [
fa, fa,
NavStyle, NavStyle,
@ -44,7 +44,7 @@ export class SidebarUser extends LitElement {
]; ];
} }
render() { render(): TemplateResult {
if (!this.user) { if (!this.user) {
return html``; return html``;
} }

View file

@ -1,7 +1,8 @@
import { gettext } from "django"; import { gettext } from "django";
import { html, LitElement, property, TemplateResult } from "lit-element"; import { CSSResult, html, LitElement, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client"; import { PBResponse } from "../../api/client";
import { COMMON_STYLES } from "../../common/styles"; import { COMMON_STYLES } from "../../common/styles";
import { htmlFromString } from "../../utils";
export abstract class Table<T> extends LitElement { export abstract class Table<T> extends LitElement {
abstract apiEndpoint(page: number): Promise<PBResponse<T>>; abstract apiEndpoint(page: number): Promise<PBResponse<T>>;
@ -14,11 +15,11 @@ export abstract class Table<T> extends LitElement {
@property() @property()
page = 1; page = 1;
static get styles() { static get styles(): CSSResult[] {
return [COMMON_STYLES]; return COMMON_STYLES;
} }
public fetch() { public fetch(): void {
this.apiEndpoint(this.page).then((r) => { this.apiEndpoint(this.page).then((r) => {
this.data = r; this.data = r;
this.page = r.pagination.current; this.page = r.pagination.current;
@ -57,11 +58,11 @@ export abstract class Table<T> extends LitElement {
}) })
); );
fullRow.push("</tr>"); fullRow.push("</tr>");
return html(<any>fullRow); return htmlFromString(...fullRow);
}); });
} }
renderTable() { renderTable(): TemplateResult {
if (!this.data) { if (!this.data) {
this.fetch(); this.fetch();
} }
@ -85,9 +86,7 @@ export abstract class Table<T> extends LitElement {
<table class="pf-c-table pf-m-compact pf-m-grid-md"> <table class="pf-c-table pf-m-compact pf-m-grid-md">
<thead> <thead>
<tr role="row"> <tr role="row">
${this.columns().map( ${this.columns().map((col) => html`<th role="columnheader" scope="col">${gettext(col)}</th>`)}
(col) => html`<th role="columnheader" scope="col">${gettext(col)}</th>`
)}
</tr> </tr>
</thead> </thead>
<tbody role="rowgroup"> <tbody role="rowgroup">
@ -102,7 +101,7 @@ export abstract class Table<T> extends LitElement {
</div>`; </div>`;
} }
render() { render(): TemplateResult {
return this.renderTable(); return this.renderTable();
} }
} }

View file

@ -1,4 +1,4 @@
import { html } from "lit-html"; import { html, TemplateResult } from "lit-html";
import { Table } from "./Table"; import { Table } from "./Table";
export abstract class TablePage<T> extends Table<T> { export abstract class TablePage<T> extends Table<T> {
@ -6,7 +6,7 @@ export abstract class TablePage<T> extends Table<T> {
abstract pageDescription(): string; abstract pageDescription(): string;
abstract pageIcon(): string; abstract pageIcon(): string;
render() { render(): TemplateResult {
return html`<section class="pf-c-page__main-section pf-m-light"> return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content"> <div class="pf-c-content">
<h1> <h1>

View file

@ -1,17 +1,17 @@
import { customElement, html, LitElement, property } from "lit-element"; import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { Table } from "./Table"; import { Table } from "./Table";
import { COMMON_STYLES } from "../../common/styles"; import { COMMON_STYLES } from "../../common/styles";
@customElement("pb-table-pagination") @customElement("pb-table-pagination")
export class TablePagination extends LitElement { export class TablePagination extends LitElement {
@property() @property()
table?: Table<any>; table?: Table<unknown>;
static get styles() { static get styles(): CSSResult[] {
return [COMMON_STYLES]; return COMMON_STYLES;
} }
previousHandler() { previousHandler(): void {
if (!this.table?.data?.pagination.previous) { if (!this.table?.data?.pagination.previous) {
console.debug("passbook/tables: no previous"); console.debug("passbook/tables: no previous");
return; return;
@ -19,7 +19,7 @@ export class TablePagination extends LitElement {
this.table.page = this.table?.data?.pagination.previous; this.table.page = this.table?.data?.pagination.previous;
} }
nextHandler() { nextHandler(): void {
if (!this.table?.data?.pagination.next) { if (!this.table?.data?.pagination.next) {
console.debug("passbook/tables: no next"); console.debug("passbook/tables: no next");
return; return;
@ -27,7 +27,7 @@ export class TablePagination extends LitElement {
this.table.page = this.table?.data?.pagination.next; this.table.page = this.table?.data?.pagination.next;
} }
render() { render(): TemplateResult {
return html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md"> return html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md"> <div class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div class="pf-c-options-menu"> <div class="pf-c-options-menu">
@ -43,9 +43,7 @@ export class TablePagination extends LitElement {
<div class="pf-c-pagination__nav-control pf-m-prev"> <div class="pf-c-pagination__nav-control pf-m-prev">
<button <button
class="pf-c-button pf-m-plain" class="pf-c-button pf-m-plain"
@click=${() => { @click=${() => {this.previousHandler();}}
this.previousHandler();
}}
disabled="${this.table?.data?.pagination.previous ? "true" : "false"}" disabled="${this.table?.data?.pagination.previous ? "true" : "false"}"
aria-label="{% trans 'Go to previous page' %}" aria-label="{% trans 'Go to previous page' %}"
> >
@ -55,9 +53,7 @@ export class TablePagination extends LitElement {
<div class="pf-c-pagination__nav-control pf-m-next"> <div class="pf-c-pagination__nav-control pf-m-next">
<button <button
class="pf-c-button pf-m-plain" class="pf-c-button pf-m-plain"
@click=${() => { @click=${() => {this.nextHandler();}}
this.nextHandler();
}}
disabled="${this.table?.data?.pagination.next ? "true" : "false"}" disabled="${this.table?.data?.pagination.next ? "true" : "false"}"
aria-label="{% trans 'Go to next page' %}" aria-label="{% trans 'Go to next page' %}"
> >

View file

@ -7,7 +7,6 @@ import "./elements/buttons/ModalButton";
import "./elements/buttons/SpinnerButton"; import "./elements/buttons/SpinnerButton";
import "./elements/buttons/TokenCopyButton"; import "./elements/buttons/TokenCopyButton";
import "./elements/CodeMirror"; import "./elements/CodeMirror";
import "./elements/FetchFillSlot";
import "./elements/Messages"; import "./elements/Messages";
import "./elements/sidebar/Sidebar"; import "./elements/sidebar/Sidebar";
import "./elements/sidebar/SidebarBrand"; import "./elements/sidebar/SidebarBrand";

View file

@ -1,5 +1,5 @@
import { gettext } from "django"; import { gettext } from "django";
import { customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import { AdminOverview } from "../api/admin_overview"; import { AdminOverview } from "../api/admin_overview";
import { DefaultClient } from "../api/client"; import { DefaultClient } from "../api/client";
@ -17,7 +17,7 @@ export class AggregateCard extends LitElement {
@property() @property()
headerLink?: string; headerLink?: string;
static get styles() { static get styles(): CSSResult[] {
return COMMON_STYLES; return COMMON_STYLES;
} }
@ -61,7 +61,7 @@ export class AdminOverviewPage extends LitElement {
@property() @property()
data?: AdminOverview; data?: AdminOverview;
static get styles() { static get styles(): CSSResult[] {
return COMMON_STYLES; return COMMON_STYLES;
} }

View file

@ -49,7 +49,7 @@ export class FlowShellCard extends LitElement {
async updateCard(data: Response): Promise<void> { async updateCard(data: Response): Promise<void> {
switch (data.type) { switch (data.type) {
case ResponseType.redirect: case ResponseType.redirect:
window.location.assign(data.to!); window.location.assign(data.to || "");
break; break;
case ResponseType.template: case ResponseType.template:
this.flowBody = data.body; this.flowBody = data.body;

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { Application } from "../api/application"; import { Application } from "../api/application";
import { PBResponse } from "../api/client"; import { PBResponse } from "../api/client";
import { COMMON_STYLES } from "../common/styles"; import { COMMON_STYLES } from "../common/styles";
@ -9,7 +9,7 @@ export class ApplicationViewPage extends LitElement {
@property() @property()
apps?: PBResponse<Application>; apps?: PBResponse<Application>;
static get styles() { static get styles(): CSSResult[] {
return COMMON_STYLES.concat( return COMMON_STYLES.concat(
css` css`
img.pf-icon { img.pf-icon {

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import CodeMirrorStyle from "codemirror/lib/codemirror.css"; import CodeMirrorStyle from "codemirror/lib/codemirror.css";
// @ts-ignore // @ts-ignore
@ -61,15 +61,16 @@ export const ROUTES: Route[] = [
class RouteMatch { class RouteMatch {
route: Route; route: Route;
arguments?: RegExpExecArray; arguments: { [key: string]: string; };
fullUrl?: string; fullUrl?: string;
constructor(route: Route) { constructor(route: Route) {
this.route = route; this.route = route;
this.arguments = {};
} }
render(): TemplateResult { render(): TemplateResult {
return this.route.render(this.arguments!.groups || {}); return this.route.render(this.arguments);
} }
toString(): string { toString(): string {
@ -85,7 +86,7 @@ export class RouterOutlet extends LitElement {
@property() @property()
defaultUrl?: string; defaultUrl?: string;
static get styles() { static get styles(): CSSResult[] {
return [ return [
CodeMirrorStyle, CodeMirrorStyle,
CodeMirrorTheme, CodeMirrorTheme,
@ -110,7 +111,7 @@ export class RouterOutlet extends LitElement {
navigate(): void { navigate(): void {
let activeUrl = window.location.hash.slice(1, Infinity); let activeUrl = window.location.hash.slice(1, Infinity);
if (activeUrl === "") { if (activeUrl === "") {
activeUrl = this.defaultUrl!; activeUrl = this.defaultUrl || "/";
window.location.hash = `#${activeUrl}`; window.location.hash = `#${activeUrl}`;
console.debug(`passbook/router: set to ${window.location.hash}`); console.debug(`passbook/router: set to ${window.location.hash}`);
return; return;
@ -121,7 +122,7 @@ export class RouterOutlet extends LitElement {
const match = route.url.exec(activeUrl); const match = route.url.exec(activeUrl);
if (match != null) { if (match != null) {
matchedRoute = new RouteMatch(route); matchedRoute = new RouteMatch(route);
matchedRoute.arguments = match; matchedRoute.arguments = match.groups || {};
matchedRoute.fullUrl = activeUrl; matchedRoute.fullUrl = activeUrl;
console.debug(`passbook/router: found match ${matchedRoute}`); console.debug(`passbook/router: found match ${matchedRoute}`);
return true; return true;
@ -136,7 +137,7 @@ export class RouterOutlet extends LitElement {
</pb-site-shell>` </pb-site-shell>`
); );
matchedRoute = new RouteMatch(route); matchedRoute = new RouteMatch(route);
matchedRoute.arguments = route.url.exec(activeUrl)!; matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};
matchedRoute.fullUrl = activeUrl; matchedRoute.fullUrl = activeUrl;
} }
this.current = matchedRoute; this.current = matchedRoute;

View file

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
// @ts-ignore // @ts-ignore
import BullseyeStyle from "@patternfly/patternfly/layouts/Bullseye/bullseye.css"; import BullseyeStyle from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
// @ts-ignore // @ts-ignore
@ -19,7 +19,7 @@ export class SiteShell extends LitElement {
@property() @property()
loading = false; loading = false;
static get styles() { static get styles(): CSSResult[] {
return [ return [
css` css`
:host, :host,
@ -44,7 +44,7 @@ export class SiteShell extends LitElement {
]; ];
} }
loadContent() { loadContent(): void {
if (!this._url) { if (!this._url) {
return; return;
} }
@ -60,7 +60,11 @@ export class SiteShell extends LitElement {
}) })
.then((r) => r.text()) .then((r) => r.text())
.then((t) => { .then((t) => {
this.querySelector("[slot=body]")!.innerHTML = t; const bodySlot = this.querySelector("[slot=body]");
if (!bodySlot) {
return;
}
bodySlot.innerHTML = t;
}) })
.then(() => { .then(() => {
// Ensure anchors only change the hash // Ensure anchors only change the hash
@ -73,12 +77,13 @@ export class SiteShell extends LitElement {
const qs = url.search || ""; const qs = url.search || "";
a.href = `#${url.pathname}${qs}`; a.href = `#${url.pathname}${qs}`;
} catch (e) { } catch (e) {
console.debug(`passbook/site-shell: error ${e}`);
a.href = `#${a.href}`; a.href = `#${a.href}`;
} }
}); });
// Create refresh buttons // Create refresh buttons
this.querySelectorAll("[role=pb-refresh]").forEach((rt) => { this.querySelectorAll("[role=pb-refresh]").forEach((rt) => {
rt.addEventListener("click", (e) => { rt.addEventListener("click", () => {
this.loadContent(); this.loadContent();
}); });
}); });
@ -87,7 +92,7 @@ export class SiteShell extends LitElement {
f.addEventListener("submit", (e) => { f.addEventListener("submit", (e) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(f); const formData = new FormData(f);
const qs = new URLSearchParams(<any>(<unknown>formData)).toString(); const qs = new URLSearchParams((<any>formData)).toString(); // eslint-disable-line
window.location.hash = `#${this._url}?${qs}`; window.location.hash = `#${this._url}?${qs}`;
}); });
}); });
@ -97,7 +102,7 @@ export class SiteShell extends LitElement {
}); });
} }
render() { render(): TemplateResult {
return html` ${this.loading ? return html` ${this.loading ?
html`<div class="pf-c-backdrop"> html`<div class="pf-c-backdrop">
<div class="pf-l-bullseye"> <div class="pf-l-bullseye">

View file

@ -29,10 +29,10 @@ export class ApplicationList extends TablePage<Application> {
row(item: Application): string[] { row(item: Application): string[] {
return [ return [
item.name!, item.name,
item.slug!, item.slug,
item.provider!.toString(), item.provider.toString(),
item.provider!.toString(), item.provider.toString(),
` `
<pb-modal-button href="administration/policies/bindings/${item.pk}/update/"> <pb-modal-button href="administration/policies/bindings/${item.pk}/update/">
<pb-spinner-button slot="trigger" class="pf-m-secondary"> <pb-spinner-button slot="trigger" class="pf-m-secondary">

View file

@ -1,5 +1,5 @@
import { gettext } from "django"; import { gettext } from "django";
import { css, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { Application } from "../../api/application"; import { Application } from "../../api/application";
import { DefaultClient, PBResponse } from "../../api/client"; import { DefaultClient, PBResponse } from "../../api/client";
import { PolicyBinding } from "../../api/policy_binding"; import { PolicyBinding } from "../../api/policy_binding";
@ -13,7 +13,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
apiEndpoint(page: number): Promise<PBResponse<PolicyBinding>> { apiEndpoint(page: number): Promise<PBResponse<PolicyBinding>> {
return DefaultClient.fetch<PBResponse<PolicyBinding>>(["policies", "bindings"], { return DefaultClient.fetch<PBResponse<PolicyBinding>>(["policies", "bindings"], {
target: this.target!, target: this.target || "",
ordering: "order", ordering: "order",
page: page, page: page,
}); });
@ -62,7 +62,7 @@ export class ApplicationViewPage extends LitElement {
@property() @property()
application?: Application; application?: Application;
static get styles(): any[] { static get styles(): CSSResult[] {
return COMMON_STYLES.concat( return COMMON_STYLES.concat(
css` css`
img.pf-icon { img.pf-icon {
@ -95,12 +95,10 @@ export class ApplicationViewPage extends LitElement {
</div> </div>
</div> </div>
<div class="pf-c-card__body"> <div class="pf-c-card__body">
${this.application ? ${this.application ? html`
html`
<pb-admin-logins-chart <pb-admin-logins-chart
url="${DefaultClient.makeUrl(["core", "applications", this.application?.slug, "metrics"])}"> url="${DefaultClient.makeUrl(["core", "applications", this.application?.slug, "metrics"])}">
</pb-admin-logins-chart>` </pb-admin-logins-chart>`: ""}
: ""}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,3 +1,5 @@
import { html, TemplateResult } from "lit-html";
export function getCookie(name: string): string | undefined { export function getCookie(name: string): string | undefined {
let cookieValue = undefined; let cookieValue = undefined;
if (document.cookie && document.cookie !== "") { if (document.cookie && document.cookie !== "") {
@ -28,3 +30,7 @@ export function truncate(input?: string, max = 10): string {
return array.slice(0, max).join(" ") + ellipsis; return array.slice(0, max).join(" ") + ellipsis;
} }
export function htmlFromString(...strings: string[]): TemplateResult {
return html({ raw: strings, ...strings } as TemplateStringsArray);
}