web: re-format with prettier
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -1,4 +1,12 @@ | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
|  | ||||
| import CodeMirror from "codemirror"; | ||||
| import "codemirror/addon/display/autorefresh"; | ||||
| @ -13,13 +21,13 @@ import "codemirror/mode/python/python.js"; | ||||
| import CodeMirrorStyle from "codemirror/lib/codemirror.css"; | ||||
| import CodeMirrorTheme from "codemirror/theme/monokai.css"; | ||||
| import CodeMirrorDialogStyle from "codemirror/addon/dialog/dialog.css"; | ||||
| import CodeMirrorShowHintStyle from  "codemirror/addon/hint/show-hint.css"; | ||||
| import CodeMirrorShowHintStyle from "codemirror/addon/hint/show-hint.css"; | ||||
| import { ifDefined } from "lit-html/directives/if-defined"; | ||||
| import YAML from "yaml"; | ||||
|  | ||||
| @customElement("ak-codemirror") | ||||
| export class CodeMirrorTextarea extends LitElement { | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     readOnly = false; | ||||
|  | ||||
|     @property() | ||||
| @ -83,11 +91,17 @@ export class CodeMirrorTextarea extends LitElement { | ||||
|     } | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [CodeMirrorStyle, CodeMirrorTheme, CodeMirrorDialogStyle, CodeMirrorShowHintStyle, css` | ||||
|             .CodeMirror-wrap pre { | ||||
|                 word-break: break-word !important; | ||||
|             } | ||||
|         `]; | ||||
|         return [ | ||||
|             CodeMirrorStyle, | ||||
|             CodeMirrorTheme, | ||||
|             CodeMirrorDialogStyle, | ||||
|             CodeMirrorShowHintStyle, | ||||
|             css` | ||||
|                 .CodeMirror-wrap pre { | ||||
|                     word-break: break-word !important; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     firstUpdated(): void { | ||||
| @ -102,7 +116,7 @@ export class CodeMirrorTextarea extends LitElement { | ||||
|             readOnly: this.readOnly, | ||||
|             autoRefresh: true, | ||||
|             lineWrapping: true, | ||||
|             value: this._value | ||||
|             value: this._value, | ||||
|         }); | ||||
|         this.editor.on("blur", () => { | ||||
|             this.editor?.save(); | ||||
|  | ||||
| @ -4,34 +4,36 @@ import AKGlobal from "../authentik.css"; | ||||
|  | ||||
| @customElement("ak-divider") | ||||
| export class Divider extends LitElement { | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, AKGlobal, css` | ||||
|             .separator { | ||||
|                 display: flex; | ||||
|                 align-items: center; | ||||
|                 text-align: center; | ||||
|             } | ||||
|         return [ | ||||
|             PFBase, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 .separator { | ||||
|                     display: flex; | ||||
|                     align-items: center; | ||||
|                     text-align: center; | ||||
|                 } | ||||
|  | ||||
|             .separator::before, | ||||
|             .separator::after { | ||||
|                 content: ''; | ||||
|                 flex: 1; | ||||
|                 border-bottom: 1px solid var(--pf-global--Color--100); | ||||
|             } | ||||
|                 .separator::before, | ||||
|                 .separator::after { | ||||
|                     content: ""; | ||||
|                     flex: 1; | ||||
|                     border-bottom: 1px solid var(--pf-global--Color--100); | ||||
|                 } | ||||
|  | ||||
|             .separator:not(:empty)::before { | ||||
|                 margin-right: .25em; | ||||
|             } | ||||
|                 .separator:not(:empty)::before { | ||||
|                     margin-right: 0.25em; | ||||
|                 } | ||||
|  | ||||
|             .separator:not(:empty)::after { | ||||
|                 margin-left: .25em; | ||||
|             } | ||||
|         `]; | ||||
|                 .separator:not(:empty)::after { | ||||
|                     margin-left: 0.25em; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="separator"><slot></slot></div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -8,14 +8,13 @@ import { PFSize } from "./Spinner"; | ||||
|  | ||||
| @customElement("ak-empty-state") | ||||
| export class EmptyState extends LitElement { | ||||
|  | ||||
|     @property({type: String}) | ||||
|     @property({ type: String }) | ||||
|     icon = ""; | ||||
|  | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     loading = false; | ||||
|  | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     fullHeight = false; | ||||
|  | ||||
|     @property() | ||||
| @ -28,14 +27,16 @@ export class EmptyState extends LitElement { | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="pf-c-empty-state ${this.fullHeight && "pf-m-full-height"}"> | ||||
|             <div class="pf-c-empty-state__content"> | ||||
|                 ${this.loading ? | ||||
|                     html`<div class="pf-c-empty-state__icon"> | ||||
|                         <ak-spinner size=${PFSize.XLarge}></ak-spinner> | ||||
|                     </div>`: | ||||
|                     html`<i class="pf-icon fa ${this.icon || "fa-question-circle"} pf-c-empty-state__icon" aria-hidden="true"></i>`} | ||||
|                 <h1 class="pf-c-title pf-m-lg"> | ||||
|                     ${this.header} | ||||
|                 </h1> | ||||
|                 ${this.loading | ||||
|                     ? html`<div class="pf-c-empty-state__icon"> | ||||
|                           <ak-spinner size=${PFSize.XLarge}></ak-spinner> | ||||
|                       </div>` | ||||
|                     : html`<i | ||||
|                           class="pf-icon fa ${this.icon || | ||||
|                           "fa-question-circle"} pf-c-empty-state__icon" | ||||
|                           aria-hidden="true" | ||||
|                       ></i>`} | ||||
|                 <h1 class="pf-c-title pf-m-lg">${this.header}</h1> | ||||
|                 <div class="pf-c-empty-state__body"> | ||||
|                     <slot name="body"></slot> | ||||
|                 </div> | ||||
| @ -45,5 +46,4 @@ export class EmptyState extends LitElement { | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -4,7 +4,6 @@ import PFExpandableSection from "../../node_modules/@patternfly/patternfly/compo | ||||
|  | ||||
| @customElement("ak-expand") | ||||
| export class Expand extends LitElement { | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     expanded = false; | ||||
|  | ||||
| @ -20,16 +19,22 @@ export class Expand extends LitElement { | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="pf-c-expandable-section ${this.expanded ? "pf-m-expanded" : ""}"> | ||||
|             <button type="button" class="pf-c-expandable-section__toggle" aria-expanded="${this.expanded}" @click=${() => { | ||||
|                 this.expanded = !this.expanded; | ||||
|             }}> | ||||
|             <button | ||||
|                 type="button" | ||||
|                 class="pf-c-expandable-section__toggle" | ||||
|                 aria-expanded="${this.expanded}" | ||||
|                 @click=${() => { | ||||
|                     this.expanded = !this.expanded; | ||||
|                 }} | ||||
|             > | ||||
|                 <span class="pf-c-expandable-section__toggle-icon"> | ||||
|                     <i class="fas fa-angle-right" aria-hidden="true"></i> | ||||
|                 </span> | ||||
|                 <span class="pf-c-expandable-section__toggle-text">${this.expanded ? t`${this.textOpen}` : t`${this.textClosed}`}</span> | ||||
|                 <span class="pf-c-expandable-section__toggle-text" | ||||
|                     >${this.expanded ? t`${this.textOpen}` : t`${this.textClosed}`}</span | ||||
|                 > | ||||
|             </button> | ||||
|             <slot ?hidden=${!this.expanded} class="pf-c-expandable-section__content"></slot> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -12,7 +12,6 @@ export enum PFColor { | ||||
|  | ||||
| @customElement("ak-label") | ||||
| export class Label extends LitElement { | ||||
|  | ||||
|     @property() | ||||
|     color: PFColor = PFColor.Grey; | ||||
|  | ||||
| @ -45,11 +44,14 @@ export class Label extends LitElement { | ||||
|         return html`<span class="pf-c-label ${this.color}"> | ||||
|             <span class="pf-c-label__content"> | ||||
|                 <span class="pf-c-label__icon"> | ||||
|                     <i class="fas ${this.text ? "fa-fw" : ""} ${this.icon || this.getDefaultIcon()}" aria-hidden="true"></i> | ||||
|                     <i | ||||
|                         class="fas ${this.text ? "fa-fw" : ""} ${this.icon || | ||||
|                         this.getDefaultIcon()}" | ||||
|                         aria-hidden="true" | ||||
|                     ></i> | ||||
|                 </span> | ||||
|                 ${this.text || ""} | ||||
|             </span> | ||||
|         </span>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import AKGlobal from "../authentik.css"; | ||||
| @ -10,19 +18,18 @@ import { EventsApi } from "../../api/dist"; | ||||
|  | ||||
| @customElement("ak-page-header") | ||||
| export class PageHeader extends LitElement { | ||||
|  | ||||
|     @property() | ||||
|     icon?: string; | ||||
|  | ||||
|     @property({type: Boolean}) | ||||
|     iconImage = false | ||||
|     @property({ type: Boolean }) | ||||
|     iconImage = false; | ||||
|  | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     hasNotifications = false; | ||||
|  | ||||
|     @property() | ||||
|     set header(value: string) { | ||||
|         tenant().then(tenant => { | ||||
|         tenant().then((tenant) => { | ||||
|             if (value !== "") { | ||||
|                 document.title = `${value} - ${tenant.brandingTitle}`; | ||||
|             } else { | ||||
| @ -42,33 +49,40 @@ export class PageHeader extends LitElement { | ||||
|     _header = ""; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, PFPage, PFContent, AKGlobal, css` | ||||
|             :host { | ||||
|                 display: flex; | ||||
|                 flex-direction: row; | ||||
|                 min-height: 114px; | ||||
|             } | ||||
|             .pf-c-button.pf-m-plain { | ||||
|                 background-color: var(--pf-c-page__main-section--m-light--BackgroundColor); | ||||
|                 border-radius: 0px; | ||||
|             } | ||||
|             .pf-c-page__main-section { | ||||
|                 flex-grow: 1; | ||||
|                 display: flex; | ||||
|                 flex-direction: column; | ||||
|                 justify-content: center; | ||||
|             } | ||||
|             img.pf-icon { | ||||
|                 max-height: 24px; | ||||
|             } | ||||
|             .sidebar-trigger, | ||||
|             .notification-trigger { | ||||
|                 font-size: 24px; | ||||
|             } | ||||
|             .notification-trigger.has-notifications { | ||||
|                 color: #2B9AF3; | ||||
|             } | ||||
|         `]; | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFButton, | ||||
|             PFPage, | ||||
|             PFContent, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 :host { | ||||
|                     display: flex; | ||||
|                     flex-direction: row; | ||||
|                     min-height: 114px; | ||||
|                 } | ||||
|                 .pf-c-button.pf-m-plain { | ||||
|                     background-color: var(--pf-c-page__main-section--m-light--BackgroundColor); | ||||
|                     border-radius: 0px; | ||||
|                 } | ||||
|                 .pf-c-page__main-section { | ||||
|                     flex-grow: 1; | ||||
|                     display: flex; | ||||
|                     flex-direction: column; | ||||
|                     justify-content: center; | ||||
|                 } | ||||
|                 img.pf-icon { | ||||
|                     max-height: 24px; | ||||
|                 } | ||||
|                 .sidebar-trigger, | ||||
|                 .notification-trigger { | ||||
|                     font-size: 24px; | ||||
|                 } | ||||
|                 .notification-trigger.has-notifications { | ||||
|                     color: #2b9af3; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderIcon(): TemplateResult { | ||||
| @ -82,50 +96,51 @@ export class PageHeader extends LitElement { | ||||
|     } | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({ | ||||
|             seen: false, | ||||
|             ordering: "-created", | ||||
|             pageSize: 1, | ||||
|         }).then(r => { | ||||
|             this.hasNotifications = r.pagination.count > 0; | ||||
|         }); | ||||
|         new EventsApi(DEFAULT_CONFIG) | ||||
|             .eventsNotificationsList({ | ||||
|                 seen: false, | ||||
|                 ordering: "-created", | ||||
|                 pageSize: 1, | ||||
|             }) | ||||
|             .then((r) => { | ||||
|                 this.hasNotifications = r.pagination.count > 0; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<button | ||||
|             class="sidebar-trigger pf-c-button pf-m-plain" | ||||
|             @click=${() => { | ||||
|                 this.dispatchEvent( | ||||
|                     new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||
|                         bubbles: true, | ||||
|                         composed: true, | ||||
|                     }) | ||||
|                 ); | ||||
|             }}> | ||||
|             <i class="fas fa-bars"></i> | ||||
|         </button> | ||||
|         <section class="pf-c-page__main-section pf-m-light"> | ||||
|             <div class="pf-c-content"> | ||||
|                 <h1> | ||||
|                     ${this.renderIcon()} | ||||
|                     ${this.header} | ||||
|                 </h1> | ||||
|                 ${this.description ? | ||||
|                     html`<p>${this.description}</p>` : html``} | ||||
|             </div> | ||||
|         </section> | ||||
|         <button | ||||
|             class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications ? "has-notifications" : ""}" | ||||
|             @click=${() => { | ||||
|                 this.dispatchEvent( | ||||
|                     new CustomEvent(EVENT_NOTIFICATION_TOGGLE, { | ||||
|                         bubbles: true, | ||||
|                         composed: true, | ||||
|                     }) | ||||
|                 ); | ||||
|             }}> | ||||
|             <i class="fas fa-bell"></i> | ||||
|         </button>`; | ||||
|                 class="sidebar-trigger pf-c-button pf-m-plain" | ||||
|                 @click=${() => { | ||||
|                     this.dispatchEvent( | ||||
|                         new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||
|                             bubbles: true, | ||||
|                             composed: true, | ||||
|                         }), | ||||
|                     ); | ||||
|                 }} | ||||
|             > | ||||
|                 <i class="fas fa-bars"></i> | ||||
|             </button> | ||||
|             <section class="pf-c-page__main-section pf-m-light"> | ||||
|                 <div class="pf-c-content"> | ||||
|                     <h1>${this.renderIcon()} ${this.header}</h1> | ||||
|                     ${this.description ? html`<p>${this.description}</p>` : html``} | ||||
|                 </div> | ||||
|             </section> | ||||
|             <button | ||||
|                 class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications | ||||
|                     ? "has-notifications" | ||||
|                     : ""}" | ||||
|                 @click=${() => { | ||||
|                     this.dispatchEvent( | ||||
|                         new CustomEvent(EVENT_NOTIFICATION_TOGGLE, { | ||||
|                             bubbles: true, | ||||
|                             composed: true, | ||||
|                         }), | ||||
|                     ); | ||||
|                 }} | ||||
|             > | ||||
|                 <i class="fas fa-bell"></i> | ||||
|             </button>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -20,13 +20,13 @@ export class Spinner extends LitElement { | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<span | ||||
|                 class="pf-c-spinner ${this.size.toString()}" | ||||
|                 role="progressbar" | ||||
|                 aria-valuetext="${t`Loading...`}"> | ||||
|                 <span class="pf-c-spinner__clipper"></span> | ||||
|                 <span class="pf-c-spinner__lead-ball"></span> | ||||
|                 <span class="pf-c-spinner__tail-ball"></span> | ||||
|             </span>`; | ||||
|             class="pf-c-spinner ${this.size.toString()}" | ||||
|             role="progressbar" | ||||
|             aria-valuetext="${t`Loading...`}" | ||||
|         > | ||||
|             <span class="pf-c-spinner__clipper"></span> | ||||
|             <span class="pf-c-spinner__lead-ball"></span> | ||||
|             <span class="pf-c-spinner__tail-ball"></span> | ||||
|         </span>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| import { LitElement, html, customElement, property, CSSResult, TemplateResult, css } from "lit-element"; | ||||
| import { | ||||
|     LitElement, | ||||
|     html, | ||||
|     customElement, | ||||
|     property, | ||||
|     CSSResult, | ||||
|     TemplateResult, | ||||
|     css, | ||||
| } from "lit-element"; | ||||
| import { ifDefined } from "lit-html/directives/if-defined"; | ||||
| import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css"; | ||||
| import PFGlobal from "@patternfly/patternfly/patternfly-base.css"; | ||||
| @ -11,24 +19,29 @@ export class Tabs extends LitElement { | ||||
|     @property() | ||||
|     currentPage?: string; | ||||
|  | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     vertical = false; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFGlobal, PFTabs, AKGlobal, css` | ||||
|             ::slotted(*) { | ||||
|                 flex-grow: 2; | ||||
|             } | ||||
|             :host([vertical]) { | ||||
|                 display: flex; | ||||
|             } | ||||
|             :host([vertical]) .pf-c-tabs { | ||||
|                 width: auto !important; | ||||
|             } | ||||
|             :host([vertical]) .pf-c-tabs__list { | ||||
|                 height: 100%; | ||||
|             } | ||||
|         `]; | ||||
|         return [ | ||||
|             PFGlobal, | ||||
|             PFTabs, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 ::slotted(*) { | ||||
|                     flex-grow: 2; | ||||
|                 } | ||||
|                 :host([vertical]) { | ||||
|                     display: flex; | ||||
|                 } | ||||
|                 :host([vertical]) .pf-c-tabs { | ||||
|                     width: auto !important; | ||||
|                 } | ||||
|                 :host([vertical]) .pf-c-tabs__list { | ||||
|                     height: 100%; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     observer: MutationObserver; | ||||
| @ -42,7 +55,11 @@ export class Tabs extends LitElement { | ||||
|  | ||||
|     connectedCallback(): void { | ||||
|         super.connectedCallback(); | ||||
|         this.observer.observe(this, { attributes: true, childList: true, subtree: true }); | ||||
|         this.observer.observe(this, { | ||||
|             attributes: true, | ||||
|             childList: true, | ||||
|             subtree: true, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     disconnectedCallback(): void { | ||||
| @ -61,9 +78,7 @@ export class Tabs extends LitElement { | ||||
|         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.onClick(slot)}> | ||||
|                 <span class="pf-c-tabs__item-text"> | ||||
|                     ${page.getAttribute("data-tab-title")} | ||||
|                 </span> | ||||
|                 <span class="pf-c-tabs__item-text"> ${page.getAttribute("data-tab-title")} </span> | ||||
|             </button> | ||||
|         </li>`; | ||||
|     } | ||||
|  | ||||
| @ -5,10 +5,11 @@ import { MessageLevel } from "../messages/Message"; | ||||
|  | ||||
| @customElement("ak-action-button") | ||||
| export class ActionButton extends SpinnerButton { | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
|     apiRequest: () => Promise<any> = () => { throw new Error(); }; | ||||
|     apiRequest: () => Promise<any> = () => { | ||||
|         throw new Error(); | ||||
|     }; | ||||
|  | ||||
|     callAction = (): Promise<void> => { | ||||
|         this.setLoading(); | ||||
| @ -16,13 +17,13 @@ export class ActionButton extends SpinnerButton { | ||||
|             if (e instanceof Error) { | ||||
|                 showMessage({ | ||||
|                     level: MessageLevel.error, | ||||
|                     message: e.toString() | ||||
|                     message: e.toString(), | ||||
|                 }); | ||||
|             } else { | ||||
|                 e.text().then(t => { | ||||
|                 e.text().then((t) => { | ||||
|                     showMessage({ | ||||
|                         level: MessageLevel.error, | ||||
|                         message: t | ||||
|                         message: t, | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFModalBox from "@patternfly/patternfly/components/ModalBox/modal-box.css"; | ||||
| @ -35,11 +43,25 @@ export class ModalButton extends LitElement { | ||||
|     @property() | ||||
|     size: PFSize = PFSize.Large; | ||||
|  | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     open = false; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, PFModalBox, PFForm, PFTitle, PFFormControl, PFBullseye, PFBackdrop, PFPage, PFCard, PFContent, AKGlobal, MODAL_BUTTON_STYLES]; | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFButton, | ||||
|             PFModalBox, | ||||
|             PFForm, | ||||
|             PFTitle, | ||||
|             PFFormControl, | ||||
|             PFBullseye, | ||||
|             PFBackdrop, | ||||
|             PFPage, | ||||
|             PFCard, | ||||
|             PFContent, | ||||
|             AKGlobal, | ||||
|             MODAL_BUTTON_STYLES, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
| @ -53,7 +75,7 @@ export class ModalButton extends LitElement { | ||||
|     } | ||||
|  | ||||
|     resetForms(): void { | ||||
|         this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach(form => { | ||||
|         this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach((form) => { | ||||
|             if ("resetForm" in form) { | ||||
|                 form?.resetForm(); | ||||
|             } | ||||
| @ -62,7 +84,7 @@ export class ModalButton extends LitElement { | ||||
|  | ||||
|     onClick(): void { | ||||
|         this.open = true; | ||||
|         this.querySelectorAll("*").forEach(child => { | ||||
|         this.querySelectorAll("*").forEach((child) => { | ||||
|             if ("requestUpdate" in child) { | ||||
|                 (child as LitElement).requestUpdate(); | ||||
|             } | ||||
| @ -70,17 +92,13 @@ export class ModalButton extends LitElement { | ||||
|     } | ||||
|  | ||||
|     renderModalInner(): TemplateResult { | ||||
|         return html`<slot name='modal'></slot>`; | ||||
|         return html`<slot name="modal"></slot>`; | ||||
|     } | ||||
|  | ||||
|     renderModal(): TemplateResult { | ||||
|         return html`<div class="pf-c-backdrop"> | ||||
|             <div class="pf-l-bullseye"> | ||||
|                 <div | ||||
|                     class="pf-c-modal-box ${this.size}" | ||||
|                     role="dialog" | ||||
|                     aria-modal="true" | ||||
|                 > | ||||
|                 <div class="pf-c-modal-box ${this.size}" role="dialog" aria-modal="true"> | ||||
|                     <button | ||||
|                         @click=${() => (this.open = false)} | ||||
|                         class="pf-c-button pf-m-plain" | ||||
| @ -99,5 +117,4 @@ export class ModalButton extends LitElement { | ||||
|         return html` <slot name="trigger" @click=${() => this.onClick()}></slot> | ||||
|             ${this.open ? this.renderModal() : ""}`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css"; | ||||
| @ -8,7 +16,7 @@ import { ERROR_CLASS, PRIMARY_CLASS, PROGRESS_CLASS, SUCCESS_CLASS } from "../.. | ||||
|  | ||||
| @customElement("ak-spinner-button") | ||||
| export class SpinnerButton extends LitElement { | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     isRunning = false; | ||||
|  | ||||
|     @property() | ||||
| @ -60,17 +68,20 @@ export class SpinnerButton extends LitElement { | ||||
|                 } | ||||
|                 this.setLoading(); | ||||
|                 if (this.callAction) { | ||||
|                     this.callAction().then(() => { | ||||
|                         this.setDone(SUCCESS_CLASS); | ||||
|                     }).catch(() => { | ||||
|                         this.setDone(ERROR_CLASS); | ||||
|                     }); | ||||
|                     this.callAction() | ||||
|                         .then(() => { | ||||
|                             this.setDone(SUCCESS_CLASS); | ||||
|                         }) | ||||
|                         .catch(() => { | ||||
|                             this.setDone(ERROR_CLASS); | ||||
|                         }); | ||||
|                 } | ||||
|             }}> | ||||
|             }} | ||||
|         > | ||||
|             ${this.isRunning | ||||
|                 ? html` <span class="pf-c-button__progress"> | ||||
|                             <ak-spinner size=${PFSize.Medium}></ak-spinner> | ||||
|                         </span>` | ||||
|                       <ak-spinner size=${PFSize.Medium}></ak-spinner> | ||||
|                   </span>` | ||||
|                 : ""} | ||||
|             <slot></slot> | ||||
|         </button>`; | ||||
|  | ||||
| @ -17,23 +17,25 @@ export class TokenCopyButton extends ActionButton { | ||||
|         if (!this.identifier) { | ||||
|             return Promise.reject(); | ||||
|         } | ||||
|         return new CoreApi(DEFAULT_CONFIG).coreTokensViewKeyRetrieve({ | ||||
|             identifier: this.identifier | ||||
|         }).then((token) => { | ||||
|             if (!token.key) { | ||||
|                 return Promise.reject(); | ||||
|             } | ||||
|             return navigator.clipboard.writeText(token.key).then(() => { | ||||
|                 this.buttonClass = SUCCESS_CLASS; | ||||
|                 setTimeout(() => { | ||||
|                     this.buttonClass = PRIMARY_CLASS; | ||||
|                 }, 1500); | ||||
|         return new CoreApi(DEFAULT_CONFIG) | ||||
|             .coreTokensViewKeyRetrieve({ | ||||
|                 identifier: this.identifier, | ||||
|             }) | ||||
|             .then((token) => { | ||||
|                 if (!token.key) { | ||||
|                     return Promise.reject(); | ||||
|                 } | ||||
|                 return navigator.clipboard.writeText(token.key).then(() => { | ||||
|                     this.buttonClass = SUCCESS_CLASS; | ||||
|                     setTimeout(() => { | ||||
|                         this.buttonClass = PRIMARY_CLASS; | ||||
|                     }, 1500); | ||||
|                 }); | ||||
|             }) | ||||
|             .catch((err: Response | undefined) => { | ||||
|                 return err?.json().then((errResp) => { | ||||
|                     throw new Error(errResp["detail"]); | ||||
|                 }); | ||||
|             }); | ||||
|         }).catch((err: Response | undefined) => { | ||||
|             return err?.json().then(errResp => { | ||||
|                 throw new Error(errResp["detail"]); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
| import { ifDefined } from "lit-html/directives/if-defined"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| @ -17,22 +25,24 @@ export class AggregateCard extends LitElement { | ||||
|     headerLink?: string; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFCard, PFFlex, AKGlobal].concat([css` | ||||
|             .pf-c-card.pf-c-card-aggregate { | ||||
|                 height: 100%; | ||||
|             } | ||||
|             .pf-c-card__header { | ||||
|                 flex-wrap: nowrap; | ||||
|             } | ||||
|             .center-value { | ||||
|                 font-size: var(--pf-global--icon--FontSize--lg); | ||||
|                 text-align: center; | ||||
|                 color: var(--pf-global--Color--100); | ||||
|             } | ||||
|             .subtext { | ||||
|                 font-size: var(--pf-global--FontSize--sm); | ||||
|             } | ||||
|         `]); | ||||
|         return [PFBase, PFCard, PFFlex, AKGlobal].concat([ | ||||
|             css` | ||||
|                 .pf-c-card.pf-c-card-aggregate { | ||||
|                     height: 100%; | ||||
|                 } | ||||
|                 .pf-c-card__header { | ||||
|                     flex-wrap: nowrap; | ||||
|                 } | ||||
|                 .center-value { | ||||
|                     font-size: var(--pf-global--icon--FontSize--lg); | ||||
|                     text-align: center; | ||||
|                     color: var(--pf-global--Color--100); | ||||
|                 } | ||||
|                 .subtext { | ||||
|                     font-size: var(--pf-global--FontSize--sm); | ||||
|                 } | ||||
|             `, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     renderInner(): TemplateResult { | ||||
| @ -40,9 +50,11 @@ export class AggregateCard extends LitElement { | ||||
|     } | ||||
|  | ||||
|     renderHeaderLink(): TemplateResult { | ||||
|         return html`${this.headerLink ? html`<a href="${this.headerLink}"> | ||||
|             <i class="fa fa-link"> </i> | ||||
|         </a>` : ""}`; | ||||
|         return html`${this.headerLink | ||||
|             ? html`<a href="${this.headerLink}"> | ||||
|                   <i class="fa fa-link"> </i> | ||||
|               </a>` | ||||
|             : ""}`; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
| @ -53,10 +65,7 @@ export class AggregateCard extends LitElement { | ||||
|                 </div> | ||||
|                 ${this.renderHeaderLink()} | ||||
|             </div> | ||||
|             <div class="pf-c-card__body center-value"> | ||||
|                 ${this.renderInner()} | ||||
|             </div> | ||||
|             <div class="pf-c-card__body center-value">${this.renderInner()}</div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -6,14 +6,14 @@ import { PFSize } from "../Spinner"; | ||||
|  | ||||
| @customElement("ak-aggregate-card-promise") | ||||
| export class AggregatePromiseCard extends AggregateCard { | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     promise?: Promise<Record<string, unknown>>; | ||||
|  | ||||
|     promiseProxy(): Promise<TemplateResult> { | ||||
|         if (!this.promise) { | ||||
|             return new Promise<TemplateResult>(() => html``); | ||||
|         } | ||||
|         return this.promise.then(s => { | ||||
|         return this.promise.then((s) => { | ||||
|             return html`<i class="fa fa-check-circle"></i> ${s.toString()}`; | ||||
|         }); | ||||
|     } | ||||
| @ -23,5 +23,4 @@ export class AggregatePromiseCard extends AggregateCard { | ||||
|             ${until(this.promiseProxy(), html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`)} | ||||
|         </p>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -6,7 +6,6 @@ import { DEFAULT_CONFIG } from "../../api/Config"; | ||||
|  | ||||
| @customElement("ak-charts-admin-login") | ||||
| export class AdminLoginsChart extends AKChart<LoginMetrics> { | ||||
|  | ||||
|     apiRequest(): Promise<LoginMetrics> { | ||||
|         return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve(); | ||||
|     } | ||||
| @ -18,26 +17,27 @@ export class AdminLoginsChart extends AKChart<LoginMetrics> { | ||||
|                     label: "Failed Logins", | ||||
|                     backgroundColor: "rgba(201, 25, 11, .5)", | ||||
|                     spanGaps: true, | ||||
|                     data: data.loginsFailedPer1h?.map((cord) => { | ||||
|                         return { | ||||
|                             x: cord.xCord || 0, | ||||
|                             y: cord.yCord || 0, | ||||
|                         }; | ||||
|                     }) || [], | ||||
|                     data: | ||||
|                         data.loginsFailedPer1h?.map((cord) => { | ||||
|                             return { | ||||
|                                 x: cord.xCord || 0, | ||||
|                                 y: cord.yCord || 0, | ||||
|                             }; | ||||
|                         }) || [], | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Successful Logins", | ||||
|                     backgroundColor: "rgba(189, 229, 184, .5)", | ||||
|                     spanGaps: true, | ||||
|                     data: data.loginsPer1h?.map((cord) => { | ||||
|                         return { | ||||
|                             x: cord.xCord || 0, | ||||
|                             y: cord.yCord || 0, | ||||
|                         }; | ||||
|                     }) || [], | ||||
|                     data: | ||||
|                         data.loginsPer1h?.map((cord) => { | ||||
|                             return { | ||||
|                                 x: cord.xCord || 0, | ||||
|                                 y: cord.yCord || 0, | ||||
|                             }; | ||||
|                         }) || [], | ||||
|                 }, | ||||
|             ] | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -6,12 +6,13 @@ import { ChartData } from "chart.js"; | ||||
|  | ||||
| @customElement("ak-charts-application-authorize") | ||||
| export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> { | ||||
|  | ||||
|     @property() | ||||
|     applicationSlug!: string; | ||||
|  | ||||
|     apiRequest(): Promise<Coordinate[]> { | ||||
|         return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetricsList({ slug: this.applicationSlug }); | ||||
|         return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetricsList({ | ||||
|             slug: this.applicationSlug, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getChartData(data: Coordinate[]): ChartData { | ||||
| @ -21,15 +22,15 @@ export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> { | ||||
|                     label: "Authorizations", | ||||
|                     backgroundColor: "rgba(189, 229, 184, .5)", | ||||
|                     spanGaps: true, | ||||
|                     data: data.map((cord) => { | ||||
|                         return { | ||||
|                             x: cord.xCord || 0, | ||||
|                             y: cord.yCord || 0, | ||||
|                         }; | ||||
|                     }) || [], | ||||
|                     data: | ||||
|                         data.map((cord) => { | ||||
|                             return { | ||||
|                                 x: cord.xCord || 0, | ||||
|                                 y: cord.yCord || 0, | ||||
|                             }; | ||||
|                         }) || [], | ||||
|                 }, | ||||
|             ] | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -6,7 +6,7 @@ import { ArcElement, BarElement } from "chart.js"; | ||||
| import { TimeScale, LinearScale } from "chart.js"; | ||||
| import "chartjs-adapter-moment"; | ||||
| import { FONT_COLOUR_DARK_MODE, FONT_COLOUR_LIGHT_MODE } from "../../pages/flows/FlowDiagram"; | ||||
| import {EVENT_REFRESH} from "../../constants"; | ||||
| import { EVENT_REFRESH } from "../../constants"; | ||||
|  | ||||
| Chart.register(Legend, Tooltip); | ||||
| Chart.register(LineController, BarController, DoughnutController); | ||||
| @ -14,7 +14,6 @@ Chart.register(ArcElement, BarElement); | ||||
| Chart.register(TimeScale, LinearScale); | ||||
|  | ||||
| export abstract class AKChart<T> extends LitElement { | ||||
|  | ||||
|     abstract apiRequest(): Promise<T>; | ||||
|     abstract getChartData(data: T): ChartData; | ||||
|  | ||||
| @ -26,15 +25,17 @@ export abstract class AKChart<T> extends LitElement { | ||||
|     fontColour = FONT_COLOUR_LIGHT_MODE; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [css` | ||||
|             .container { | ||||
|                 height: 100%; | ||||
|             } | ||||
|             canvas { | ||||
|                 width: 100px; | ||||
|                 height: 100px; | ||||
|             } | ||||
|         `]; | ||||
|         return [ | ||||
|             css` | ||||
|                 .container { | ||||
|                     height: 100%; | ||||
|                 } | ||||
|                 canvas { | ||||
|                     width: 100px; | ||||
|                     height: 100px; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
| @ -99,12 +100,14 @@ export abstract class AKChart<T> extends LitElement { | ||||
|                     chart.ctx.textBaseline = "middle"; | ||||
|                     chart.ctx.fillStyle = this.fontColour; | ||||
|  | ||||
|                     const textX = Math.round((width - chart.ctx.measureText(this.centerText).width) / 2); | ||||
|                     const textX = Math.round( | ||||
|                         (width - chart.ctx.measureText(this.centerText).width) / 2, | ||||
|                     ); | ||||
|                     const textY = height / 2; | ||||
|  | ||||
|                     chart.ctx.fillText(this.centerText, textX, textY); | ||||
|                 } | ||||
|             } | ||||
|                 }, | ||||
|             }, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| @ -116,8 +119,12 @@ export abstract class AKChart<T> extends LitElement { | ||||
|                     type: "time", | ||||
|                     display: true, | ||||
|                     ticks: { | ||||
|                         callback: function (tickValue: string | number, index: number, ticks: Tick[]): string { | ||||
|                             const valueStamp = (ticks[index]); | ||||
|                         callback: function ( | ||||
|                             tickValue: string | number, | ||||
|                             index: number, | ||||
|                             ticks: Tick[], | ||||
|                         ): string { | ||||
|                             const valueStamp = ticks[index]; | ||||
|                             const delta = Date.now() - valueStamp.value; | ||||
|                             const ago = Math.round(delta / 1000 / 3600); | ||||
|                             return `${ago} Hours ago`; | ||||
| @ -129,7 +136,7 @@ export abstract class AKChart<T> extends LitElement { | ||||
|                     grid: { | ||||
|                         color: "rgba(0, 0, 0, 0)", | ||||
|                     }, | ||||
|                     offset: true | ||||
|                     offset: true, | ||||
|                 }, | ||||
|                 y: { | ||||
|                     type: "linear", | ||||
| @ -138,7 +145,7 @@ export abstract class AKChart<T> extends LitElement { | ||||
|                     grid: { | ||||
|                         color: "rgba(0, 0, 0, 0)", | ||||
|                     }, | ||||
|                 } | ||||
|                 }, | ||||
|             }, | ||||
|         } as ChartOptions; | ||||
|     } | ||||
|  | ||||
| @ -6,8 +6,7 @@ import { ChartData } from "chart.js"; | ||||
|  | ||||
| @customElement("ak-charts-user") | ||||
| export class UserChart extends AKChart<UserMetrics> { | ||||
|  | ||||
|     @property({type: Number}) | ||||
|     @property({ type: Number }) | ||||
|     userId?: number; | ||||
|  | ||||
|     apiRequest(): Promise<UserMetrics> { | ||||
| @ -23,37 +22,39 @@ export class UserChart extends AKChart<UserMetrics> { | ||||
|                     label: "Failed Logins", | ||||
|                     backgroundColor: "rgba(201, 25, 11, .5)", | ||||
|                     spanGaps: true, | ||||
|                     data: data.loginsFailedPer1h?.map((cord) => { | ||||
|                         return { | ||||
|                             x: cord.xCord || 0, | ||||
|                             y: cord.yCord || 0, | ||||
|                         }; | ||||
|                     }) || [], | ||||
|                     data: | ||||
|                         data.loginsFailedPer1h?.map((cord) => { | ||||
|                             return { | ||||
|                                 x: cord.xCord || 0, | ||||
|                                 y: cord.yCord || 0, | ||||
|                             }; | ||||
|                         }) || [], | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Successful Logins", | ||||
|                     backgroundColor: "rgba(189, 229, 184, .5)", | ||||
|                     spanGaps: true, | ||||
|                     data: data.loginsPer1h?.map((cord) => { | ||||
|                         return { | ||||
|                             x: cord.xCord || 0, | ||||
|                             y: cord.yCord || 0, | ||||
|                         }; | ||||
|                     }) || [], | ||||
|                     data: | ||||
|                         data.loginsPer1h?.map((cord) => { | ||||
|                             return { | ||||
|                                 x: cord.xCord || 0, | ||||
|                                 y: cord.yCord || 0, | ||||
|                             }; | ||||
|                         }) || [], | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Application authorizations", | ||||
|                     backgroundColor: "rgba(43, 154, 243, .5)", | ||||
|                     spanGaps: true, | ||||
|                     data: data.authorizationsPer1h?.map((cord) => { | ||||
|                         return { | ||||
|                             x: cord.xCord || 0, | ||||
|                             y: cord.yCord || 0, | ||||
|                         }; | ||||
|                     }) || [], | ||||
|                     data: | ||||
|                         data.authorizationsPer1h?.map((cord) => { | ||||
|                             return { | ||||
|                                 x: cord.xCord || 0, | ||||
|                                 y: cord.yCord || 0, | ||||
|                             }; | ||||
|                         }) || [], | ||||
|                 }, | ||||
|             ] | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -7,11 +7,10 @@ import AKGlobal from "../../authentik.css"; | ||||
|  | ||||
| @customElement("ak-chip") | ||||
| export class Chip extends LitElement { | ||||
|  | ||||
|     @property() | ||||
|     value?: number | string; | ||||
|  | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     removable = false; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
| @ -24,16 +23,23 @@ export class Chip extends LitElement { | ||||
|                 <span class="pf-c-chip__text"> | ||||
|                     <slot></slot> | ||||
|                 </span> | ||||
|                 ${this.removable ? html`<button class="pf-c-button pf-m-plain" type="button" @click=${() => { | ||||
|                     this.dispatchEvent(new CustomEvent("remove", { | ||||
|                         bubbles: true, | ||||
|                         composed: true, | ||||
|                     })); | ||||
|                 }}> | ||||
|                     <i class="fas fa-times" aria-hidden="true"></i> | ||||
|                 </button>` : html``} | ||||
|                 ${this.removable | ||||
|                     ? html`<button | ||||
|                           class="pf-c-button pf-m-plain" | ||||
|                           type="button" | ||||
|                           @click=${() => { | ||||
|                               this.dispatchEvent( | ||||
|                                   new CustomEvent("remove", { | ||||
|                                       bubbles: true, | ||||
|                                       composed: true, | ||||
|                                   }), | ||||
|                               ); | ||||
|                           }} | ||||
|                       > | ||||
|                           <i class="fas fa-times" aria-hidden="true"></i> | ||||
|                       </button>` | ||||
|                     : html``} | ||||
|             </div> | ||||
|         </li>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -9,7 +9,6 @@ import { Chip } from "./Chip"; | ||||
|  | ||||
| @customElement("ak-chip-group") | ||||
| export class ChipGroup extends LitElement { | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFChip, PFChipGroup, PFButton, AKGlobal]; | ||||
|     } | ||||
| @ -20,7 +19,7 @@ export class ChipGroup extends LitElement { | ||||
|  | ||||
|     get value(): (string | number | undefined)[] { | ||||
|         const values: (string | number | undefined)[] = []; | ||||
|         this.querySelectorAll<Chip>("ak-chip").forEach(chip => { | ||||
|         this.querySelectorAll<Chip>("ak-chip").forEach((chip) => { | ||||
|             values.push(chip.value); | ||||
|         }); | ||||
|         return values; | ||||
| @ -28,12 +27,11 @@ export class ChipGroup extends LitElement { | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="pf-c-chip-group"> | ||||
|                 <div class="pf-c-chip-group__main"> | ||||
|                     <ul class="pf-c-chip-group__list" role="list"> | ||||
|                         <slot></slot> | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             </div>`; | ||||
|             <div class="pf-c-chip-group__main"> | ||||
|                 <ul class="pf-c-chip-group__list" role="list"> | ||||
|                     <slot></slot> | ||||
|                 </ul> | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -55,32 +55,28 @@ export class ObjectChangelog extends Table<Event> { | ||||
|         return [ | ||||
|             html`${item.action}`, | ||||
|             html`<div>${item.user?.username}</div> | ||||
|             ${item.user.on_behalf_of ? html`<small> | ||||
|                 ${t`On behalf of ${item.user.on_behalf_of.username}`} | ||||
|             </small>` : html``}`, | ||||
|                 ${item.user.on_behalf_of | ||||
|                     ? html`<small> ${t`On behalf of ${item.user.on_behalf_of.username}`} </small>` | ||||
|                     : html``}`, | ||||
|             html`<span>${item.created?.toLocaleString()}</span>`, | ||||
|             html`<span>${item.clientIp || "-"}</span>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: Event): TemplateResult { | ||||
|         return html` | ||||
|         <td role="cell" colspan="4"> | ||||
|             <div class="pf-c-table__expandable-row-content"> | ||||
|                 <ak-event-info .event=${item as EventWithContext}></ak-event-info> | ||||
|             </div> | ||||
|         </td> | ||||
|         <td></td> | ||||
|         <td></td> | ||||
|         <td></td>`; | ||||
|         return html` <td role="cell" colspan="4"> | ||||
|                 <div class="pf-c-table__expandable-row-content"> | ||||
|                     <ak-event-info .event=${item as EventWithContext}></ak-event-info> | ||||
|                 </div> | ||||
|             </td> | ||||
|             <td></td> | ||||
|             <td></td> | ||||
|             <td></td>`; | ||||
|     } | ||||
|  | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}> | ||||
|             <div slot="body"> | ||||
|                 ${t`No matching events could be found.`} | ||||
|             </div> | ||||
|             <div slot="body">${t`No matching events could be found.`}</div> | ||||
|         </ak-empty-state>`); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -29,7 +29,7 @@ export class ObjectChangelog extends Table<Event> { | ||||
|             page: page, | ||||
|             ordering: this.order, | ||||
|             pageSize: PAGE_SIZE / 2, | ||||
|             username: this.targetUser | ||||
|             username: this.targetUser, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @ -46,32 +46,28 @@ export class ObjectChangelog extends Table<Event> { | ||||
|         return [ | ||||
|             html`${item.action}`, | ||||
|             html`<div>${item.user?.username}</div> | ||||
|             ${item.user.on_behalf_of ? html`<small> | ||||
|                 ${t`On behalf of ${item.user.on_behalf_of.username}`} | ||||
|             </small>` : html``}`, | ||||
|                 ${item.user.on_behalf_of | ||||
|                     ? html`<small> ${t`On behalf of ${item.user.on_behalf_of.username}`} </small>` | ||||
|                     : html``}`, | ||||
|             html`<span>${item.created?.toLocaleString()}</span>`, | ||||
|             html`<span>${item.clientIp || "-"}</span>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: Event): TemplateResult { | ||||
|         return html` | ||||
|         <td role="cell" colspan="4"> | ||||
|             <div class="pf-c-table__expandable-row-content"> | ||||
|                 <ak-event-info .event=${item as EventWithContext}></ak-event-info> | ||||
|             </div> | ||||
|         </td> | ||||
|         <td></td> | ||||
|         <td></td> | ||||
|         <td></td>`; | ||||
|         return html` <td role="cell" colspan="4"> | ||||
|                 <div class="pf-c-table__expandable-row-content"> | ||||
|                     <ak-event-info .event=${item as EventWithContext}></ak-event-info> | ||||
|                 </div> | ||||
|             </td> | ||||
|             <td></td> | ||||
|             <td></td> | ||||
|             <td></td>`; | ||||
|     } | ||||
|  | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}> | ||||
|             <div slot="body"> | ||||
|                 ${t`No matching events could be found.`} | ||||
|             </div> | ||||
|             <div slot="body">${t`No matching events could be found.`}</div> | ||||
|         </ak-empty-state>`); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -8,7 +8,6 @@ import { showMessage } from "../messages/MessageContainer"; | ||||
|  | ||||
| @customElement("ak-forms-confirm") | ||||
| export class ConfirmationForm extends ModalButton { | ||||
|  | ||||
|     @property() | ||||
|     successMessage!: string; | ||||
|     @property() | ||||
| @ -17,23 +16,25 @@ export class ConfirmationForm extends ModalButton { | ||||
|     @property() | ||||
|     action!: string; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     onConfirm!: () => Promise<unknown>; | ||||
|  | ||||
|     confirm(): Promise<void> { | ||||
|         return this.onConfirm().then(() => { | ||||
|             this.onSuccess(); | ||||
|             this.open = false; | ||||
|             this.dispatchEvent( | ||||
|                 new CustomEvent(EVENT_REFRESH, { | ||||
|                     bubbles: true, | ||||
|                     composed: true, | ||||
|                 }) | ||||
|             ); | ||||
|         }).catch((e) => { | ||||
|             this.onError(e); | ||||
|             throw e; | ||||
|         }); | ||||
|         return this.onConfirm() | ||||
|             .then(() => { | ||||
|                 this.onSuccess(); | ||||
|                 this.open = false; | ||||
|                 this.dispatchEvent( | ||||
|                     new CustomEvent(EVENT_REFRESH, { | ||||
|                         bubbles: true, | ||||
|                         composed: true, | ||||
|                     }), | ||||
|                 ); | ||||
|             }) | ||||
|             .catch((e) => { | ||||
|                 this.onError(e); | ||||
|                 throw e; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     onSuccess(): void { | ||||
| @ -52,33 +53,34 @@ export class ConfirmationForm extends ModalButton { | ||||
|  | ||||
|     renderModalInner(): TemplateResult { | ||||
|         return html`<section class="pf-c-page__main-section pf-m-light"> | ||||
|             <div class="pf-c-content"> | ||||
|                 <h1 class="pf-c-title pf-m-2xl"> | ||||
|                     <slot name="header"></slot> | ||||
|                 </h1> | ||||
|             </div> | ||||
|         </section> | ||||
|         <section class="pf-c-page__main-section pf-m-light"> | ||||
|             <form class="pf-c-form pf-m-horizontal"> | ||||
|                 <slot class="pf-c-content" name="body"></slot> | ||||
|             </form> | ||||
|         </section> | ||||
|         <footer class="pf-c-modal-box__footer"> | ||||
|             <ak-spinner-button | ||||
|                 .callAction=${() => { | ||||
|                     return this.confirm(); | ||||
|                 }} | ||||
|                 class="pf-m-danger"> | ||||
|                 ${this.action} | ||||
|             </ak-spinner-button>  | ||||
|             <ak-spinner-button | ||||
|                 .callAction=${async () => { | ||||
|                     this.open = false; | ||||
|                 }} | ||||
|                 class="pf-m-secondary"> | ||||
|                 ${t`Cancel`} | ||||
|             </ak-spinner-button> | ||||
|         </footer>`; | ||||
|                 <div class="pf-c-content"> | ||||
|                     <h1 class="pf-c-title pf-m-2xl"> | ||||
|                         <slot name="header"></slot> | ||||
|                     </h1> | ||||
|                 </div> | ||||
|             </section> | ||||
|             <section class="pf-c-page__main-section pf-m-light"> | ||||
|                 <form class="pf-c-form pf-m-horizontal"> | ||||
|                     <slot class="pf-c-content" name="body"></slot> | ||||
|                 </form> | ||||
|             </section> | ||||
|             <footer class="pf-c-modal-box__footer"> | ||||
|                 <ak-spinner-button | ||||
|                     .callAction=${() => { | ||||
|                         return this.confirm(); | ||||
|                     }} | ||||
|                     class="pf-m-danger" | ||||
|                 > | ||||
|                     ${this.action} </ak-spinner-button | ||||
|                 >  | ||||
|                 <ak-spinner-button | ||||
|                     .callAction=${async () => { | ||||
|                         this.open = false; | ||||
|                     }} | ||||
|                     class="pf-m-secondary" | ||||
|                 > | ||||
|                     ${t`Cancel`} | ||||
|                 </ak-spinner-button> | ||||
|             </footer>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -11,42 +11,43 @@ import { until } from "lit-html/directives/until"; | ||||
|  | ||||
| @customElement("ak-forms-delete") | ||||
| export class DeleteForm extends ModalButton { | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat(PFList); | ||||
|     } | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     obj?: Record<string, unknown>; | ||||
|  | ||||
|     @property() | ||||
|     objectLabel?: string; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     usedBy?: () => Promise<UsedBy[]>; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     delete!: () => Promise<unknown>; | ||||
|  | ||||
|     confirm(): Promise<void> { | ||||
|         return this.delete().then(() => { | ||||
|             this.onSuccess(); | ||||
|             this.open = false; | ||||
|             this.dispatchEvent( | ||||
|                 new CustomEvent(EVENT_REFRESH, { | ||||
|                     bubbles: true, | ||||
|                     composed: true, | ||||
|                 }) | ||||
|             ); | ||||
|         }).catch((e) => { | ||||
|             this.onError(e); | ||||
|             throw e; | ||||
|         }); | ||||
|         return this.delete() | ||||
|             .then(() => { | ||||
|                 this.onSuccess(); | ||||
|                 this.open = false; | ||||
|                 this.dispatchEvent( | ||||
|                     new CustomEvent(EVENT_REFRESH, { | ||||
|                         bubbles: true, | ||||
|                         composed: true, | ||||
|                     }), | ||||
|                 ); | ||||
|             }) | ||||
|             .catch((e) => { | ||||
|                 this.onError(e); | ||||
|                 throw e; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     onSuccess(): void { | ||||
|         showMessage({ | ||||
|             message: t`Successfully deleted ${this.objectLabel} ${ this.obj?.name }`, | ||||
|             message: t`Successfully deleted ${this.objectLabel} ${this.obj?.name}`, | ||||
|             level: MessageLevel.success, | ||||
|         }); | ||||
|     } | ||||
| @ -66,69 +67,70 @@ export class DeleteForm extends ModalButton { | ||||
|             objName = ""; | ||||
|         } | ||||
|         return html`<section class="pf-c-page__main-section pf-m-light"> | ||||
|             <div class="pf-c-content"> | ||||
|                 <h1 class="pf-c-title pf-m-2xl"> | ||||
|                     ${t`Delete ${this.objectLabel}`} | ||||
|                 </h1> | ||||
|             </div> | ||||
|         </section> | ||||
|         <section class="pf-c-page__main-section pf-m-light"> | ||||
|             <form class="pf-c-form pf-m-horizontal"> | ||||
|                 <p> | ||||
|                     ${t`Are you sure you want to delete ${this.objectLabel} ${objName} ?`} | ||||
|                 </p> | ||||
|             </form> | ||||
|         </section> | ||||
|         ${this.usedBy ? until(this.usedBy().then(usedBy => { | ||||
|             if (usedBy.length < 1) { | ||||
|                 return html``; | ||||
|             } | ||||
|             return html` | ||||
|                 <section class="pf-c-page__main-section"> | ||||
|                     <form class="pf-c-form pf-m-horizontal"> | ||||
|                         <p> | ||||
|                             ${t`The following objects use ${objName} `} | ||||
|                         </p> | ||||
|                         <ul class="pf-c-list"> | ||||
|                             ${usedBy.map(ub => { | ||||
|                                 let consequence = ""; | ||||
|                                 switch (ub.action) { | ||||
|                                 case UsedByActionEnum.Cascade: | ||||
|                                     consequence = t`object will be DELETED`; | ||||
|                                     break; | ||||
|                                 case UsedByActionEnum.CascadeMany: | ||||
|                                     consequence = t`connecting object will be deleted`; | ||||
|                                     break; | ||||
|                                 case UsedByActionEnum.SetDefault: | ||||
|                                     consequence = t`reference will be reset to default value`; | ||||
|                                     break; | ||||
|                                 case UsedByActionEnum.SetNull: | ||||
|                                     consequence = t`reference will be set to an empty value`; | ||||
|                                     break; | ||||
|                                 } | ||||
|                                 return html`<li>${t`${ub.name} (${consequence})`}</li>`; | ||||
|                             })} | ||||
|                         </ul> | ||||
|                     </form> | ||||
|                 </section> | ||||
|             `; | ||||
|         })) : html``} | ||||
|         <footer class="pf-c-modal-box__footer"> | ||||
|             <ak-spinner-button | ||||
|                 .callAction=${() => { | ||||
|                     return this.confirm(); | ||||
|                 }} | ||||
|                 class="pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </ak-spinner-button>  | ||||
|             <ak-spinner-button | ||||
|                 .callAction=${async () => { | ||||
|                     this.open = false; | ||||
|                 }} | ||||
|                 class="pf-m-secondary"> | ||||
|                 ${t`Cancel`} | ||||
|             </ak-spinner-button> | ||||
|         </footer>`; | ||||
|                 <div class="pf-c-content"> | ||||
|                     <h1 class="pf-c-title pf-m-2xl">${t`Delete ${this.objectLabel}`}</h1> | ||||
|                 </div> | ||||
|             </section> | ||||
|             <section class="pf-c-page__main-section pf-m-light"> | ||||
|                 <form class="pf-c-form pf-m-horizontal"> | ||||
|                     <p>${t`Are you sure you want to delete ${this.objectLabel} ${objName} ?`}</p> | ||||
|                 </form> | ||||
|             </section> | ||||
|             ${this.usedBy | ||||
|                 ? until( | ||||
|                       this.usedBy().then((usedBy) => { | ||||
|                           if (usedBy.length < 1) { | ||||
|                               return html``; | ||||
|                           } | ||||
|                           return html` | ||||
|                               <section class="pf-c-page__main-section"> | ||||
|                                   <form class="pf-c-form pf-m-horizontal"> | ||||
|                                       <p>${t`The following objects use ${objName} `}</p> | ||||
|                                       <ul class="pf-c-list"> | ||||
|                                           ${usedBy.map((ub) => { | ||||
|                                               let consequence = ""; | ||||
|                                               switch (ub.action) { | ||||
|                                                   case UsedByActionEnum.Cascade: | ||||
|                                                       consequence = t`object will be DELETED`; | ||||
|                                                       break; | ||||
|                                                   case UsedByActionEnum.CascadeMany: | ||||
|                                                       consequence = t`connecting object will be deleted`; | ||||
|                                                       break; | ||||
|                                                   case UsedByActionEnum.SetDefault: | ||||
|                                                       consequence = t`reference will be reset to default value`; | ||||
|                                                       break; | ||||
|                                                   case UsedByActionEnum.SetNull: | ||||
|                                                       consequence = t`reference will be set to an empty value`; | ||||
|                                                       break; | ||||
|                                               } | ||||
|                                               return html`<li> | ||||
|                                                   ${t`${ub.name} (${consequence})`} | ||||
|                                               </li>`; | ||||
|                                           })} | ||||
|                                       </ul> | ||||
|                                   </form> | ||||
|                               </section> | ||||
|                           `; | ||||
|                       }), | ||||
|                   ) | ||||
|                 : html``} | ||||
|             <footer class="pf-c-modal-box__footer"> | ||||
|                 <ak-spinner-button | ||||
|                     .callAction=${() => { | ||||
|                         return this.confirm(); | ||||
|                     }} | ||||
|                     class="pf-m-danger" | ||||
|                 > | ||||
|                     ${t`Delete`} </ak-spinner-button | ||||
|                 >  | ||||
|                 <ak-spinner-button | ||||
|                     .callAction=${async () => { | ||||
|                         this.open = false; | ||||
|                     }} | ||||
|                     class="pf-m-secondary" | ||||
|                 > | ||||
|                     ${t`Cancel`} | ||||
|                 </ak-spinner-button> | ||||
|             </footer>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,15 @@ import "@polymer/paper-input/paper-input"; | ||||
| import "@polymer/iron-form/iron-form"; | ||||
| import { PaperInputElement } from "@polymer/paper-input/paper-input"; | ||||
| import { showMessage } from "../../elements/messages/MessageContainer"; | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| @ -18,31 +26,38 @@ import { ValidationError } from "authentik-api"; | ||||
| import { EVENT_REFRESH } from "../../constants"; | ||||
|  | ||||
| export class APIError extends Error { | ||||
|  | ||||
|     constructor(public response: ValidationError) { | ||||
|         super(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @customElement("ak-form") | ||||
| export class Form<T> extends LitElement { | ||||
|  | ||||
|     @property() | ||||
|     successMessage = ""; | ||||
|  | ||||
|     @property() | ||||
|     send!: (data: T) => Promise<unknown>; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     nonFieldErrors?: string[]; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFCard, PFButton, PFForm, PFAlert, PFInputGroup, PFFormControl, AKGlobal, css` | ||||
|             select[multiple] { | ||||
|                 height: 15em; | ||||
|             } | ||||
|         `]; | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFCard, | ||||
|             PFButton, | ||||
|             PFForm, | ||||
|             PFAlert, | ||||
|             PFInputGroup, | ||||
|             PFFormControl, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 select[multiple] { | ||||
|                     height: 15em; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     get isInViewport(): boolean { | ||||
| @ -55,25 +70,27 @@ export class Form<T> extends LitElement { | ||||
|     } | ||||
|  | ||||
|     updated(): void { | ||||
|         this.shadowRoot?.querySelectorAll<HTMLInputElement>("input[name=name]").forEach(nameInput => { | ||||
|             const form = nameInput.closest("form"); | ||||
|             if (form === null) { | ||||
|                 return; | ||||
|             } | ||||
|             const slugField = form.querySelector<HTMLInputElement>("input[name=slug]"); | ||||
|             if (!slugField) { | ||||
|                 return; | ||||
|             } | ||||
|             // Only attach handler if the slug is already equal to the name | ||||
|             // if not, they are probably completely different and shouldn't update | ||||
|             // each other | ||||
|             if (convertToSlug(nameInput.value) !== slugField.value) { | ||||
|                 return; | ||||
|             } | ||||
|             nameInput.addEventListener("input", () => { | ||||
|                 slugField.value = convertToSlug(nameInput.value); | ||||
|         this.shadowRoot | ||||
|             ?.querySelectorAll<HTMLInputElement>("input[name=name]") | ||||
|             .forEach((nameInput) => { | ||||
|                 const form = nameInput.closest("form"); | ||||
|                 if (form === null) { | ||||
|                     return; | ||||
|                 } | ||||
|                 const slugField = form.querySelector<HTMLInputElement>("input[name=slug]"); | ||||
|                 if (!slugField) { | ||||
|                     return; | ||||
|                 } | ||||
|                 // Only attach handler if the slug is already equal to the name | ||||
|                 // if not, they are probably completely different and shouldn't update | ||||
|                 // each other | ||||
|                 if (convertToSlug(nameInput.value) !== slugField.value) { | ||||
|                     return; | ||||
|                 } | ||||
|                 nameInput.addEventListener("input", () => { | ||||
|                     slugField.value = convertToSlug(nameInput.value); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -110,7 +127,7 @@ export class Form<T> extends LitElement { | ||||
|     serializeForm(form: IronFormElement): T { | ||||
|         const elements: HTMLInputElement[] = form._getSubmittableElements(); | ||||
|         const json: { [key: string]: unknown } = {}; | ||||
|         elements.forEach(element => { | ||||
|         elements.forEach((element) => { | ||||
|             const values = form._serializeElementValues(element); | ||||
|             if (element.hidden) { | ||||
|                 return; | ||||
| @ -138,54 +155,58 @@ export class Form<T> extends LitElement { | ||||
|             return; | ||||
|         } | ||||
|         const data = this.serializeForm(ironForm); | ||||
|         return this.send(data).then((r) => { | ||||
|             showMessage({ | ||||
|                 level: MessageLevel.success, | ||||
|                 message: this.getSuccessMessage() | ||||
|             }); | ||||
|             this.dispatchEvent( | ||||
|                 new CustomEvent(EVENT_REFRESH, { | ||||
|                     bubbles: true, | ||||
|                     composed: true, | ||||
|                 }) | ||||
|             ); | ||||
|             return r; | ||||
|         }).catch((ex: Response | Error) => { | ||||
|             if (ex instanceof Error) { | ||||
|                 throw ex; | ||||
|             } | ||||
|             if (ex.status > 399 && ex.status < 500) { | ||||
|                 return ex.json().then((errorMessage: ValidationError) => { | ||||
|                     if (!errorMessage) return errorMessage; | ||||
|                     if (errorMessage instanceof Error) { | ||||
|                         throw errorMessage; | ||||
|                     } | ||||
|                     // assign all input-related errors to their elements | ||||
|                     const elements: PaperInputElement[] = ironForm._getSubmittableElements(); | ||||
|                     elements.forEach((element) => { | ||||
|                         const elementName = element.name; | ||||
|                         if (!elementName) return; | ||||
|                         if (camelToSnake(elementName) in errorMessage) { | ||||
|                             element.errorMessage = errorMessage[camelToSnake(elementName)].join(", "); | ||||
|                             element.invalid = true; | ||||
|                         } | ||||
|                     }); | ||||
|                     if ("non_field_errors" in errorMessage) { | ||||
|                         this.nonFieldErrors = errorMessage["non_field_errors"]; | ||||
|                     } | ||||
|                     throw new APIError(errorMessage); | ||||
|         return this.send(data) | ||||
|             .then((r) => { | ||||
|                 showMessage({ | ||||
|                     level: MessageLevel.success, | ||||
|                     message: this.getSuccessMessage(), | ||||
|                 }); | ||||
|             } | ||||
|             throw ex; | ||||
|         }).catch((ex: Error) => { | ||||
|             // error is local or not from rest_framework | ||||
|             showMessage({ | ||||
|                 message: ex.toString(), | ||||
|                 level: MessageLevel.error, | ||||
|                 this.dispatchEvent( | ||||
|                     new CustomEvent(EVENT_REFRESH, { | ||||
|                         bubbles: true, | ||||
|                         composed: true, | ||||
|                     }), | ||||
|                 ); | ||||
|                 return r; | ||||
|             }) | ||||
|             .catch((ex: Response | Error) => { | ||||
|                 if (ex instanceof Error) { | ||||
|                     throw ex; | ||||
|                 } | ||||
|                 if (ex.status > 399 && ex.status < 500) { | ||||
|                     return ex.json().then((errorMessage: ValidationError) => { | ||||
|                         if (!errorMessage) return errorMessage; | ||||
|                         if (errorMessage instanceof Error) { | ||||
|                             throw errorMessage; | ||||
|                         } | ||||
|                         // assign all input-related errors to their elements | ||||
|                         const elements: PaperInputElement[] = ironForm._getSubmittableElements(); | ||||
|                         elements.forEach((element) => { | ||||
|                             const elementName = element.name; | ||||
|                             if (!elementName) return; | ||||
|                             if (camelToSnake(elementName) in errorMessage) { | ||||
|                                 element.errorMessage = | ||||
|                                     errorMessage[camelToSnake(elementName)].join(", "); | ||||
|                                 element.invalid = true; | ||||
|                             } | ||||
|                         }); | ||||
|                         if ("non_field_errors" in errorMessage) { | ||||
|                             this.nonFieldErrors = errorMessage["non_field_errors"]; | ||||
|                         } | ||||
|                         throw new APIError(errorMessage); | ||||
|                     }); | ||||
|                 } | ||||
|                 throw ex; | ||||
|             }) | ||||
|             .catch((ex: Error) => { | ||||
|                 // error is local or not from rest_framework | ||||
|                 showMessage({ | ||||
|                     message: ex.toString(), | ||||
|                     level: MessageLevel.error, | ||||
|                 }); | ||||
|                 // rethrow the error so the form doesn't close | ||||
|                 throw ex; | ||||
|             }); | ||||
|             // rethrow the error so the form doesn't close | ||||
|             throw ex; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
| @ -197,24 +218,24 @@ export class Form<T> extends LitElement { | ||||
|             return html``; | ||||
|         } | ||||
|         return html`<div class="pf-c-form__alert"> | ||||
|         ${this.nonFieldErrors.map(err => { | ||||
|             return html`<div class="pf-c-alert pf-m-inline pf-m-danger"> | ||||
|                 <div class="pf-c-alert__icon"> | ||||
|                     <i class="fas fa-exclamation-circle"></i> | ||||
|                 </div> | ||||
|                 <h4 class="pf-c-alert__title"> | ||||
|                     ${err} | ||||
|                 </h4> | ||||
|             </div>`; | ||||
|         })} | ||||
|             ${this.nonFieldErrors.map((err) => { | ||||
|                 return html`<div class="pf-c-alert pf-m-inline pf-m-danger"> | ||||
|                     <div class="pf-c-alert__icon"> | ||||
|                         <i class="fas fa-exclamation-circle"></i> | ||||
|                     </div> | ||||
|                     <h4 class="pf-c-alert__title">${err}</h4> | ||||
|                 </div>`; | ||||
|             })} | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
|     renderVisible(): TemplateResult { | ||||
|         return html`<iron-form | ||||
|             @iron-form-presubmit=${(ev: Event) => { this.submit(ev); }}> | ||||
|             ${this.renderNonFieldErrors()} | ||||
|             ${this.renderForm()} | ||||
|             @iron-form-presubmit=${(ev: Event) => { | ||||
|                 this.submit(ev); | ||||
|             }} | ||||
|         > | ||||
|             ${this.renderNonFieldErrors()} ${this.renderForm()} | ||||
|         </iron-form>`; | ||||
|     } | ||||
|  | ||||
| @ -224,5 +245,4 @@ export class Form<T> extends LitElement { | ||||
|         } | ||||
|         return this.renderVisible(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -6,16 +6,19 @@ import { ErrorDetail } from "authentik-api"; | ||||
|  | ||||
| @customElement("ak-form-element") | ||||
| export class FormElement extends LitElement { | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFForm, PFFormControl, css` | ||||
|             slot { | ||||
|                 display: flex; | ||||
|                 flex-direction: row; | ||||
|                 align-items: center; | ||||
|                 justify-content: space-around; | ||||
|             } | ||||
|         `]; | ||||
|         return [ | ||||
|             PFForm, | ||||
|             PFFormControl, | ||||
|             css` | ||||
|                 slot { | ||||
|                     display: flex; | ||||
|                     flex-direction: row; | ||||
|                     align-items: center; | ||||
|                     justify-content: space-around; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
| @ -28,22 +31,23 @@ export class FormElement extends LitElement { | ||||
|     errors?: ErrorDetail[]; | ||||
|  | ||||
|     updated(): void { | ||||
|         this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => { | ||||
|         this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => { | ||||
|             input.focus(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="pf-c-form__group"> | ||||
|                 <label class="pf-c-form__label"> | ||||
|                     <span class="pf-c-form__label-text">${this.label}</span> | ||||
|                     ${this.required ? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>` : html``} | ||||
|                 </label> | ||||
|                 <slot></slot> | ||||
|                 ${(this.errors || []).map((error) => { | ||||
|                         return html`<p class="pf-c-form__helper-text pf-m-error">${error.string}</p>`; | ||||
|                     })} | ||||
|             </div>`; | ||||
|             <label class="pf-c-form__label"> | ||||
|                 <span class="pf-c-form__label-text">${this.label}</span> | ||||
|                 ${this.required | ||||
|                     ? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>` | ||||
|                     : html``} | ||||
|             </label> | ||||
|             <slot></slot> | ||||
|             ${(this.errors || []).map((error) => { | ||||
|                 return html`<p class="pf-c-form__helper-text pf-m-error">${error.string}</p>`; | ||||
|             })} | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||
| @ -7,25 +15,37 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
|  | ||||
| @customElement("ak-form-group") | ||||
| export class FormGroup extends LitElement { | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     expanded = false; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFForm, PFButton, PFFormControl, AKGlobal, css` | ||||
|             slot[name=body][hidden] { | ||||
|                 display: none !important; | ||||
|             } | ||||
|         `]; | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFForm, | ||||
|             PFButton, | ||||
|             PFFormControl, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 slot[name="body"][hidden] { | ||||
|                     display: none !important; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}"> | ||||
|             <div class="pf-c-form__field-group-toggle"> | ||||
|                 <div class="pf-c-form__field-group-toggle-button"> | ||||
|                     <button class="pf-c-button pf-m-plain" type="button" aria-expanded="${this.expanded}" aria-label="Details" @click=${() => { | ||||
|                         this.expanded = !this.expanded; | ||||
|                     }}> | ||||
|                     <button | ||||
|                         class="pf-c-button pf-m-plain" | ||||
|                         type="button" | ||||
|                         aria-expanded="${this.expanded}" | ||||
|                         aria-label="Details" | ||||
|                         @click=${() => { | ||||
|                             this.expanded = !this.expanded; | ||||
|                         }} | ||||
|                     > | ||||
|                         <span class="pf-c-form__field-group-toggle-icon"> | ||||
|                             <i class="fas fa-angle-right" aria-hidden="true"></i> | ||||
|                         </span> | ||||
| @ -47,5 +67,4 @@ export class FormGroup extends LitElement { | ||||
|             <slot ?hidden=${!this.expanded} class="pf-c-form__field-group-body" name="body"></slot> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -8,17 +8,24 @@ import { t } from "@lingui/macro"; | ||||
|  | ||||
| @customElement("ak-form-element-horizontal") | ||||
| export class HorizontalFormElement extends LitElement { | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFForm, PFFormControl, AKGlobal, css` | ||||
|             .pf-c-form__group { | ||||
|                 display: grid; | ||||
|                 grid-template-columns: var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth) var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth); | ||||
|             } | ||||
|             .pf-c-form__group-label { | ||||
|                 padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop); | ||||
|             } | ||||
|         `]; | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFForm, | ||||
|             PFFormControl, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 .pf-c-form__group { | ||||
|                     display: grid; | ||||
|                     grid-template-columns: | ||||
|                         var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth) | ||||
|                         var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth); | ||||
|                 } | ||||
|                 .pf-c-form__group-label { | ||||
|                     padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop); | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
| @ -43,7 +50,7 @@ export class HorizontalFormElement extends LitElement { | ||||
|     name = ""; | ||||
|  | ||||
|     updated(): void { | ||||
|         this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => { | ||||
|         this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => { | ||||
|             input.focus(); | ||||
|         }); | ||||
|         this.querySelectorAll("*").forEach((input) => { | ||||
| @ -59,7 +66,7 @@ export class HorizontalFormElement extends LitElement { | ||||
|                     return; | ||||
|             } | ||||
|             if (this.writeOnly && !this.writeOnlyActivated) { | ||||
|                 const i = (input as HTMLInputElement); | ||||
|                 const i = input as HTMLInputElement; | ||||
|                 i.setAttribute("hidden", "true"); | ||||
|                 const handler = () => { | ||||
|                     i.removeAttribute("hidden"); | ||||
| @ -76,24 +83,36 @@ export class HorizontalFormElement extends LitElement { | ||||
|             <div class="pf-c-form__group-label"> | ||||
|                 <label class="pf-c-form__label"> | ||||
|                     <span class="pf-c-form__label-text">${this.label}</span> | ||||
|                     ${this.required ? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>` : html``} | ||||
|                     ${this.required | ||||
|                         ? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>` | ||||
|                         : html``} | ||||
|                 </label> | ||||
|             </div> | ||||
|             <div class="pf-c-form__group-control"> | ||||
|                 ${this.writeOnly && !this.writeOnlyActivated ? | ||||
|                     html`<div class="pf-c-form__horizontal-group"> | ||||
|                         <input class="pf-c-form-control" type="password" disabled value="**************"> | ||||
|                     </div>` : | ||||
|                     html``} | ||||
|                 ${this.writeOnly && !this.writeOnlyActivated | ||||
|                     ? html`<div class="pf-c-form__horizontal-group"> | ||||
|                           <input | ||||
|                               class="pf-c-form-control" | ||||
|                               type="password" | ||||
|                               disabled | ||||
|                               value="**************" | ||||
|                           /> | ||||
|                       </div>` | ||||
|                     : html``} | ||||
|                 <slot class="pf-c-form__horizontal-group"></slot> | ||||
|                 <div class="pf-c-form__horizontal-group"> | ||||
|                     ${this.writeOnly ? html`<p class="pf-c-form__helper-text" aria-live="polite">${ | ||||
|                         t`Click to change value` | ||||
|                     }</p>` : html``} | ||||
|                     ${this.invalid ? html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">${this.errorMessage}</p>` : html``} | ||||
|                     ${this.writeOnly | ||||
|                         ? html`<p class="pf-c-form__helper-text" aria-live="polite"> | ||||
|                               ${t`Click to change value`} | ||||
|                           </p>` | ||||
|                         : html``} | ||||
|                     ${this.invalid | ||||
|                         ? html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite"> | ||||
|                               ${this.errorMessage} | ||||
|                           </p>` | ||||
|                         : html``} | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -7,11 +7,10 @@ import "../buttons/SpinnerButton"; | ||||
|  | ||||
| @customElement("ak-forms-modal") | ||||
| export class ModalForm extends ModalButton { | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     closeAfterSuccessfulSubmit = true; | ||||
|  | ||||
|     confirm(): Promise<void>  { | ||||
|     confirm(): Promise<void> { | ||||
|         const form = this.querySelector<Form<unknown>>("[slot=form]"); | ||||
|         if (!form) { | ||||
|             return Promise.reject(t`No form found`); | ||||
| @ -29,39 +28,40 @@ export class ModalForm extends ModalButton { | ||||
|                 new CustomEvent(EVENT_REFRESH, { | ||||
|                     bubbles: true, | ||||
|                     composed: true, | ||||
|                 }) | ||||
|                 }), | ||||
|             ); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     renderModalInner(): TemplateResult { | ||||
|         return html`<section class="pf-c-page__main-section pf-m-light"> | ||||
|             <div class="pf-c-content"> | ||||
|                 <h1 class="pf-c-title pf-m-2xl"> | ||||
|                     <slot name="header"></slot> | ||||
|                 </h1> | ||||
|             </div> | ||||
|         </section> | ||||
|         <section class="pf-c-page__main-section pf-m-light"> | ||||
|             <slot name="form"></slot> | ||||
|         </section> | ||||
|         <footer class="pf-c-modal-box__footer"> | ||||
|             <ak-spinner-button | ||||
|                 .callAction=${() => { | ||||
|                     return this.confirm(); | ||||
|                 }} | ||||
|                 class="pf-m-primary"> | ||||
|                 <slot name="submit"></slot> | ||||
|             </ak-spinner-button>  | ||||
|             <ak-spinner-button | ||||
|                 .callAction=${async () => { | ||||
|                     this.resetForms(); | ||||
|                     this.open = false; | ||||
|                 }} | ||||
|                 class="pf-m-secondary"> | ||||
|                 ${t`Cancel`} | ||||
|             </ak-spinner-button> | ||||
|         </footer>`; | ||||
|                 <div class="pf-c-content"> | ||||
|                     <h1 class="pf-c-title pf-m-2xl"> | ||||
|                         <slot name="header"></slot> | ||||
|                     </h1> | ||||
|                 </div> | ||||
|             </section> | ||||
|             <section class="pf-c-page__main-section pf-m-light"> | ||||
|                 <slot name="form"></slot> | ||||
|             </section> | ||||
|             <footer class="pf-c-modal-box__footer"> | ||||
|                 <ak-spinner-button | ||||
|                     .callAction=${() => { | ||||
|                         return this.confirm(); | ||||
|                     }} | ||||
|                     class="pf-m-primary" | ||||
|                 > | ||||
|                     <slot name="submit"></slot> </ak-spinner-button | ||||
|                 >  | ||||
|                 <ak-spinner-button | ||||
|                     .callAction=${async () => { | ||||
|                         this.resetForms(); | ||||
|                         this.open = false; | ||||
|                     }} | ||||
|                     class="pf-m-secondary" | ||||
|                 > | ||||
|                     ${t`Cancel`} | ||||
|                 </ak-spinner-button> | ||||
|             </footer>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -3,14 +3,13 @@ import { EVENT_REFRESH } from "../../constants"; | ||||
| import { Form } from "./Form"; | ||||
|  | ||||
| export abstract class ModelForm<T, PKT extends string | number> extends Form<T> { | ||||
|  | ||||
|     abstract loadInstance(pk: PKT): Promise<T>; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     set instancePk(value: PKT) { | ||||
|         this._instancePk = value; | ||||
|         if (this.isInViewport) { | ||||
|             this.loadInstance(value).then(instance => { | ||||
|             this.loadInstance(value).then((instance) => { | ||||
|                 this.instance = instance; | ||||
|                 this.requestUpdate(); | ||||
|             }); | ||||
| @ -32,7 +31,7 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T> | ||||
|         super(); | ||||
|         this.addEventListener(EVENT_REFRESH, () => { | ||||
|             if (!this._instancePk) return; | ||||
|             this.loadInstance(this._instancePk).then(instance => { | ||||
|             this.loadInstance(this._instancePk).then((instance) => { | ||||
|                 this.instance = instance; | ||||
|             }); | ||||
|         }); | ||||
| @ -51,5 +50,4 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T> | ||||
|         } | ||||
|         return super.render(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -3,14 +3,13 @@ import { Form } from "./Form"; | ||||
|  | ||||
| @customElement("ak-proxy-form") | ||||
| export class ProxyForm extends Form<unknown> { | ||||
|  | ||||
|     @property() | ||||
|     type!: string; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     args: Record<string, unknown> = {}; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     typeMap: Record<string, string> = {}; | ||||
|  | ||||
|     submit(ev: Event): Promise<unknown> | undefined { | ||||
| @ -43,5 +42,4 @@ export class ProxyForm extends Form<unknown> { | ||||
|         } | ||||
|         return html`${el}`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -9,7 +9,7 @@ export enum MessageLevel { | ||||
|     error = "error", | ||||
|     warning = "warning", | ||||
|     success = "success", | ||||
|     info = "info" | ||||
|     info = "info", | ||||
| } | ||||
| export interface APIMessage { | ||||
|     level: MessageLevel; | ||||
| @ -27,14 +27,13 @@ const LEVEL_ICON_MAP: { [key: string]: string } = { | ||||
|  | ||||
| @customElement("ak-message") | ||||
| export class Message extends LitElement { | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     message?: APIMessage; | ||||
|  | ||||
|     @property({type: Number}) | ||||
|     @property({ type: Number }) | ||||
|     removeAfter = 8000; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     onRemove?: (m: APIMessage) => void; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
| @ -51,27 +50,34 @@ export class Message extends LitElement { | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<li class="pf-c-alert-group__item"> | ||||
|             <div class="pf-c-alert pf-m-${this.message?.level} ${this.message?.level === MessageLevel.error ? "pf-m-danger" : ""}"> | ||||
|             <div | ||||
|                 class="pf-c-alert pf-m-${this.message?.level} ${this.message?.level === | ||||
|                 MessageLevel.error | ||||
|                     ? "pf-m-danger" | ||||
|                     : ""}" | ||||
|             > | ||||
|                 <div class="pf-c-alert__icon"> | ||||
|                     <i class="${this.message ? LEVEL_ICON_MAP[this.message.level] : ""}"></i> | ||||
|                 </div> | ||||
|                 <p class="pf-c-alert__title"> | ||||
|                     ${this.message?.message} | ||||
|                 </p> | ||||
|                 ${this.message?.description && html`<div class="pf-c-alert__description"> | ||||
|                 <p class="pf-c-alert__title">${this.message?.message}</p> | ||||
|                 ${this.message?.description && | ||||
|                 html`<div class="pf-c-alert__description"> | ||||
|                     <p>${this.message.description}</p> | ||||
|                 </div>`} | ||||
|                 <div class="pf-c-alert__action"> | ||||
|                     <button class="pf-c-button pf-m-plain" type="button" @click=${() => { | ||||
|                         if (!this.message) return; | ||||
|                         if (!this.onRemove) return; | ||||
|                         this.onRemove(this.message); | ||||
|                     }}> | ||||
|                     <button | ||||
|                         class="pf-c-button pf-m-plain" | ||||
|                         type="button" | ||||
|                         @click=${() => { | ||||
|                             if (!this.message) return; | ||||
|                             if (!this.onRemove) return; | ||||
|                             this.onRemove(this.message); | ||||
|                         }} | ||||
|                     > | ||||
|                         <i class="fas fa-times" aria-hidden="true"></i> | ||||
|                     </button> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </li>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| import { LitElement, html, customElement, TemplateResult, property, CSSResult, css } from "lit-element"; | ||||
| import { | ||||
|     LitElement, | ||||
|     html, | ||||
|     customElement, | ||||
|     TemplateResult, | ||||
|     property, | ||||
|     CSSResult, | ||||
|     css, | ||||
| } from "lit-element"; | ||||
| import "./Message"; | ||||
| import { APIMessage } from "./Message"; | ||||
| import PFAlertGroup from "@patternfly/patternfly/components/AlertGroup/alert-group.css"; | ||||
| @ -17,17 +25,20 @@ export function showMessage(message: APIMessage): void { | ||||
|  | ||||
| @customElement("ak-message-container") | ||||
| export class MessageContainer extends LitElement { | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     messages: APIMessage[] = []; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFAlertGroup, css` | ||||
|             /* Fix spacing between messages */ | ||||
|             ak-message { | ||||
|                 display: block; | ||||
|             } | ||||
|         `]; | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFAlertGroup, | ||||
|             css` | ||||
|                 /* Fix spacing between messages */ | ||||
|                 ak-message { | ||||
|                     display: block; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
| @ -40,7 +51,7 @@ export class MessageContainer extends LitElement { | ||||
|  | ||||
|     // add a new message, but only if the message isn't currently shown. | ||||
|     addMessage(message: APIMessage): void { | ||||
|         const matchingMessages = this.messages.filter(m => m.message == message.message); | ||||
|         const matchingMessages = this.messages.filter((m) => m.message == message.message); | ||||
|         if (matchingMessages.length < 1) { | ||||
|             this.messages.push(message); | ||||
|         } | ||||
| @ -54,9 +65,10 @@ export class MessageContainer extends LitElement { | ||||
|                     .onRemove=${(m: APIMessage) => { | ||||
|                         this.messages = this.messages.filter((v) => v !== m); | ||||
|                         this.requestUpdate(); | ||||
|                     }}> | ||||
|                     </ak-message>`; | ||||
|                 })} | ||||
|                     }} | ||||
|                 > | ||||
|                 </ak-message>`; | ||||
|             })} | ||||
|         </ul>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -4,13 +4,12 @@ import { MessageLevel } from "./Message"; | ||||
| import { showMessage } from "./MessageContainer"; | ||||
|  | ||||
| export class MessageMiddleware implements Middleware { | ||||
|  | ||||
|     post(context: ResponseContext): Promise<Response | void> { | ||||
|         if (context.response.status >= 500) { | ||||
|             showMessage({ | ||||
|                 level: MessageLevel.error, | ||||
|                 message: t`API request failed`, | ||||
|                 description: `${context.init.method} ${context.url}: ${context.response.status}` | ||||
|                 description: `${context.init.method} ${context.url}: ${context.response.status}`, | ||||
|             }); | ||||
|         } | ||||
|         return Promise.resolve(context.response); | ||||
|  | ||||
| @ -32,7 +32,7 @@ export class APIMiddleware implements Middleware { | ||||
|             new CustomEvent(EVENT_API_DRAWER_REFRESH, { | ||||
|                 bubbles: true, | ||||
|                 composed: true, | ||||
|             }) | ||||
|             }), | ||||
|         ); | ||||
|         return Promise.resolve(context.response); | ||||
|     } | ||||
| @ -43,7 +43,6 @@ export const API_DRAWER_MIDDLEWARE = new APIMiddleware(); | ||||
|  | ||||
| @customElement("ak-api-drawer") | ||||
| export class APIDrawer extends LitElement { | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFNotificationDrawer, PFContent, PFDropdown, AKGlobal]; | ||||
|     } | ||||
| @ -58,9 +57,7 @@ export class APIDrawer extends LitElement { | ||||
|     renderItem(item: RequestInfo): TemplateResult { | ||||
|         return html`<li class="pf-c-notification-drawer__list-item pf-m-read"> | ||||
|             <div class="pf-c-notification-drawer__list-item-header"> | ||||
|                 <h2 class="pf-c-notification-drawer__list-item-header-title"> | ||||
|                     ${item.method} | ||||
|                 </h2> | ||||
|                 <h2 class="pf-c-notification-drawer__list-item-header-title">${item.method}</h2> | ||||
|             </div> | ||||
|             <p class="pf-c-notification-drawer__list-item-description">${item.path}</p> | ||||
|         </li>`; | ||||
| @ -70,17 +67,14 @@ export class APIDrawer extends LitElement { | ||||
|         return html`<div class="pf-c-drawer__body pf-m-no-padding"> | ||||
|             <div class="pf-c-notification-drawer"> | ||||
|                 <div class="pf-c-notification-drawer__header pf-c-content"> | ||||
|                     <h1> | ||||
|                         ${t`API Requests`} | ||||
|                     </h1> | ||||
|                     <h1>${t`API Requests`}</h1> | ||||
|                 </div> | ||||
|                 <div class="pf-c-notification-drawer__body"> | ||||
|                     <ul class="pf-c-notification-drawer__list"> | ||||
|                         ${API_DRAWER_MIDDLEWARE.requests.map(n => this.renderItem(n))} | ||||
|                         ${API_DRAWER_MIDDLEWARE.requests.map((n) => this.renderItem(n))} | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,13 @@ | ||||
| import { t } from "@lingui/macro"; | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
| import { EventsApi, Notification } from "authentik-api"; | ||||
| import { AKResponse } from "../../api/Client"; | ||||
| import { DEFAULT_CONFIG } from "../../api/Config"; | ||||
| @ -14,11 +22,10 @@ import { ActionToLabel } from "../../pages/events/utils"; | ||||
|  | ||||
| @customElement("ak-notification-drawer") | ||||
| export class NotificationDrawer extends LitElement { | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     notifications?: AKResponse<Notification>; | ||||
|  | ||||
|     @property({type: Number}) | ||||
|     @property({ type: Number }) | ||||
|     unread = 0; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
| @ -36,34 +43,36 @@ export class NotificationDrawer extends LitElement { | ||||
|                 .pf-c-notification-drawer__list-item-description { | ||||
|                     white-space: pre-wrap; | ||||
|                 } | ||||
|             ` | ||||
|             `, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({ | ||||
|             seen: false, | ||||
|             ordering: "-created", | ||||
|         }).then(r => { | ||||
|             this.notifications = r; | ||||
|             this.unread = r.results.length; | ||||
|         }); | ||||
|         new EventsApi(DEFAULT_CONFIG) | ||||
|             .eventsNotificationsList({ | ||||
|                 seen: false, | ||||
|                 ordering: "-created", | ||||
|             }) | ||||
|             .then((r) => { | ||||
|                 this.notifications = r; | ||||
|                 this.unread = r.results.length; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     renderItem(item: Notification): TemplateResult { | ||||
|         let level = ""; | ||||
|         switch (item.severity) { | ||||
|         case "notice": | ||||
|             level = "pf-m-info"; | ||||
|             break; | ||||
|         case "warning": | ||||
|             level = "pf-m-warning"; | ||||
|             break; | ||||
|         case "alert": | ||||
|             level = "pf-m-danger"; | ||||
|             break; | ||||
|         default: | ||||
|             break; | ||||
|             case "notice": | ||||
|                 level = "pf-m-info"; | ||||
|                 break; | ||||
|             case "warning": | ||||
|                 level = "pf-m-warning"; | ||||
|                 break; | ||||
|             case "alert": | ||||
|                 level = "pf-m-danger"; | ||||
|                 break; | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|         return html`<li class="pf-c-notification-drawer__list-item"> | ||||
|             <div class="pf-c-notification-drawer__list-item-header"> | ||||
| @ -75,26 +84,38 @@ export class NotificationDrawer extends LitElement { | ||||
|                 </h2> | ||||
|             </div> | ||||
|             <div class="pf-c-notification-drawer__list-item-action"> | ||||
|                 ${item.event && html` | ||||
|                     <a class="pf-c-dropdown__toggle pf-m-plain" href="#/events/log/${item.event?.pk}"> | ||||
|                 ${item.event && | ||||
|                 html` | ||||
|                     <a | ||||
|                         class="pf-c-dropdown__toggle pf-m-plain" | ||||
|                         href="#/events/log/${item.event?.pk}" | ||||
|                     > | ||||
|                         <i class="fas fas fa-share-square"></i> | ||||
|                     </a> | ||||
|                 `} | ||||
|                 <button class="pf-c-dropdown__toggle pf-m-plain" type="button" @click=${() => { | ||||
|                     new EventsApi(DEFAULT_CONFIG).eventsNotificationsPartialUpdate({ | ||||
|                         uuid: item.pk || "", | ||||
|                         patchedNotificationRequest: { | ||||
|                             seen: true, | ||||
|                         } | ||||
|                     }).then(() => { | ||||
|                         this.firstUpdated(); | ||||
|                     }); | ||||
|                 }}> | ||||
|                 <button | ||||
|                     class="pf-c-dropdown__toggle pf-m-plain" | ||||
|                     type="button" | ||||
|                     @click=${() => { | ||||
|                         new EventsApi(DEFAULT_CONFIG) | ||||
|                             .eventsNotificationsPartialUpdate({ | ||||
|                                 uuid: item.pk || "", | ||||
|                                 patchedNotificationRequest: { | ||||
|                                     seen: true, | ||||
|                                 }, | ||||
|                             }) | ||||
|                             .then(() => { | ||||
|                                 this.firstUpdated(); | ||||
|                             }); | ||||
|                     }} | ||||
|                 > | ||||
|                     <i class="fas fa-times"></i> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <p class="pf-c-notification-drawer__list-item-description">${item.body}</p> | ||||
|             <small class="pf-c-notification-drawer__list-item-timestamp">${item.created?.toLocaleString()}</small> | ||||
|             <small class="pf-c-notification-drawer__list-item-timestamp" | ||||
|                 >${item.created?.toLocaleString()}</small | ||||
|             > | ||||
|         </li>`; | ||||
|     } | ||||
|  | ||||
| @ -106,12 +127,8 @@ export class NotificationDrawer extends LitElement { | ||||
|             <div class="pf-c-notification-drawer"> | ||||
|                 <div class="pf-c-notification-drawer__header"> | ||||
|                     <div class="text"> | ||||
|                         <h1 class="pf-c-notification-drawer__header-title"> | ||||
|                             ${t`Notifications`} | ||||
|                         </h1> | ||||
|                         <span> | ||||
|                             ${t`${this.unread} unread`} | ||||
|                         </span> | ||||
|                         <h1 class="pf-c-notification-drawer__header-title">${t`Notifications`}</h1> | ||||
|                         <span> ${t`${this.unread} unread`} </span> | ||||
|                     </div> | ||||
|                     <div class="pf-c-notification-drawer__header-action"> | ||||
|                         <div class="pf-c-notification-drawer__header-action-close"> | ||||
| @ -121,12 +138,13 @@ export class NotificationDrawer extends LitElement { | ||||
|                                         new CustomEvent(EVENT_NOTIFICATION_TOGGLE, { | ||||
|                                             bubbles: true, | ||||
|                                             composed: true, | ||||
|                                         }) | ||||
|                                         }), | ||||
|                                     ); | ||||
|                                 }} | ||||
|                                 class="pf-c-button pf-m-plain" | ||||
|                                 type="button" | ||||
|                                 aria-label="Close"> | ||||
|                                 aria-label="Close" | ||||
|                             > | ||||
|                                 <i class="fas fa-times" aria-hidden="true"></i> | ||||
|                             </button> | ||||
|                         </div> | ||||
| @ -134,11 +152,10 @@ export class NotificationDrawer extends LitElement { | ||||
|                 </div> | ||||
|                 <div class="pf-c-notification-drawer__body"> | ||||
|                     <ul class="pf-c-notification-drawer__list"> | ||||
|                         ${this.notifications.results.map(n => this.renderItem(n))} | ||||
|                         ${this.notifications.results.map((n) => this.renderItem(n))} | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -35,30 +35,27 @@ export class UserOAuthCodeList extends Table<ExpiringBaseGrantModel> { | ||||
|  | ||||
|     row(item: ExpiringBaseGrantModel): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<a href="#/core/providers/${item.provider?.pk}"> | ||||
|                 ${item.provider?.name} | ||||
|             </a>`, | ||||
|             html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`, | ||||
|             html`${item.expires?.toLocaleString()}`, | ||||
|             html`${item.scope.join(", ")}`, | ||||
|             html` | ||||
|             <ak-forms-delete | ||||
|             html` <ak-forms-delete | ||||
|                 .obj=${item} | ||||
|                 objectLabel=${t`Authorization Code`} | ||||
|                 .usedBy=${() => { | ||||
|                     return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesUsedByList({ | ||||
|                         id: item.pk | ||||
|                         id: item.pk, | ||||
|                     }); | ||||
|                 }} | ||||
|                 .delete=${() => { | ||||
|                     return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesDestroy({ | ||||
|                         id: item.pk, | ||||
|                     }); | ||||
|                 }}> | ||||
|                 }} | ||||
|             > | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                     ${t`Delete Authorization Code`} | ||||
|                 </button> | ||||
|             </ak-forms-delete>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -42,49 +42,45 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> { | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: RefreshTokenModel): TemplateResult { | ||||
|         return html` | ||||
|         <td role="cell" colspan="4"> | ||||
|             <div class="pf-c-table__expandable-row-content"> | ||||
|                 <div class="pf-l-flex"> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${t`ID Token`}</h3> | ||||
|                         <pre>${item.idToken}</pre> | ||||
|         return html` <td role="cell" colspan="4"> | ||||
|                 <div class="pf-c-table__expandable-row-content"> | ||||
|                     <div class="pf-l-flex"> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`ID Token`}</h3> | ||||
|                             <pre>${item.idToken}</pre> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </td> | ||||
|         <td></td> | ||||
|         <td></td> | ||||
|         <td></td>`; | ||||
|             </td> | ||||
|             <td></td> | ||||
|             <td></td> | ||||
|             <td></td>`; | ||||
|     } | ||||
|  | ||||
|     row(item: RefreshTokenModel): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<a href="#/core/providers/${item.provider?.pk}"> | ||||
|                 ${item.provider?.name} | ||||
|             </a>`, | ||||
|             html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`, | ||||
|             html`${item.revoked ? t`Yes` : t`No`}`, | ||||
|             html`${item.expires?.toLocaleString()}`, | ||||
|             html`${item.scope.join(", ")}`, | ||||
|             html` | ||||
|             <ak-forms-delete | ||||
|             html` <ak-forms-delete | ||||
|                 .obj=${item} | ||||
|                 objectLabel=${t`Refresh Code`} | ||||
|                 .usedBy=${() => { | ||||
|                     return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({ | ||||
|                         id: item.pk | ||||
|                         id: item.pk, | ||||
|                     }); | ||||
|                 }} | ||||
|                 .delete=${() => { | ||||
|                     return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({ | ||||
|                         id: item.pk, | ||||
|                     }); | ||||
|                 }}> | ||||
|                 }} | ||||
|             > | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                     ${t`Delete Refresh Code`} | ||||
|                 </button> | ||||
|             </ak-forms-delete>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -3,7 +3,7 @@ import { Route } from "./Route"; | ||||
|  | ||||
| export class RouteMatch { | ||||
|     route: Route; | ||||
|     arguments: { [key: string]: string; }; | ||||
|     arguments: { [key: string]: string }; | ||||
|     fullUrl?: string; | ||||
|  | ||||
|     constructor(route: Route) { | ||||
| @ -16,6 +16,8 @@ export class RouteMatch { | ||||
|     } | ||||
|  | ||||
|     toString(): string { | ||||
|         return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify(this.arguments)}>`; | ||||
|         return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify( | ||||
|             this.arguments, | ||||
|         )}>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -6,7 +6,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| @customElement("ak-router-404") | ||||
| export class Router404 extends LitElement { | ||||
|  | ||||
|     @property() | ||||
|     url = ""; | ||||
|  | ||||
| @ -19,9 +18,7 @@ export class Router404 extends LitElement { | ||||
|             <div class="pf-c-empty-state__content"> | ||||
|                 <i class="fas fa-question-circle pf-c-empty-state__icon" aria-hidden="true"></i> | ||||
|                 <h1 class="pf-c-title pf-m-lg">${t`Not found`}</h1> | ||||
|                 <div class="pf-c-empty-state__body"> | ||||
|                     ${t`The URL "${this.url}" was not found.`} | ||||
|                 </div> | ||||
|                 <div class="pf-c-empty-state__body">${t`The URL "${this.url}" was not found.`}</div> | ||||
|                 <a href="#/" class="pf-c-button pf-m-primary" type="button">${t`Return home`}</a> | ||||
|             </div> | ||||
|         </div>`; | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
| import { Route } from "./Route"; | ||||
| import { ROUTES } from "../../routes"; | ||||
| import { RouteMatch } from "./RouteMatch"; | ||||
| @ -10,26 +18,36 @@ import { ROUTE_SEPARATOR } from "../../constants"; | ||||
| // Poliyfill for hashchange.newURL, | ||||
| // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange | ||||
| window.addEventListener("load", () => { | ||||
|     if (!window.HashChangeEvent) (function () { | ||||
|         let lastURL = document.URL; | ||||
|         window.addEventListener("hashchange", function (event) { | ||||
|             Object.defineProperty(event, "oldURL", { enumerable: true, configurable: true, value: lastURL }); | ||||
|             Object.defineProperty(event, "newURL", { enumerable: true, configurable: true, value: document.URL }); | ||||
|             lastURL = document.URL; | ||||
|         }); | ||||
|     }()); | ||||
|     if (!window.HashChangeEvent) | ||||
|         (function () { | ||||
|             let lastURL = document.URL; | ||||
|             window.addEventListener("hashchange", function (event) { | ||||
|                 Object.defineProperty(event, "oldURL", { | ||||
|                     enumerable: true, | ||||
|                     configurable: true, | ||||
|                     value: lastURL, | ||||
|                 }); | ||||
|                 Object.defineProperty(event, "newURL", { | ||||
|                     enumerable: true, | ||||
|                     configurable: true, | ||||
|                     value: document.URL, | ||||
|                 }); | ||||
|                 lastURL = document.URL; | ||||
|             }); | ||||
|         })(); | ||||
| }); | ||||
|  | ||||
| @customElement("ak-router-outlet") | ||||
| export class RouterOutlet extends LitElement { | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     current?: RouteMatch; | ||||
|  | ||||
|     @property() | ||||
|     defaultUrl?: string; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [AKGlobal, | ||||
|         return [ | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 :host { | ||||
|                     height: 100vh; | ||||
| @ -88,7 +106,7 @@ export class RouterOutlet extends LitElement { | ||||
|                 RegExp(""), | ||||
|                 html`<div class="pf-c-page__main"> | ||||
|                     <ak-router-404 url=${activeUrl}></ak-router-404> | ||||
|                 </div>` | ||||
|                 </div>`, | ||||
|             ); | ||||
|             matchedRoute = new RouteMatch(route); | ||||
|             matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {}; | ||||
|  | ||||
| @ -9,7 +9,6 @@ import "./SidebarUser"; | ||||
|  | ||||
| @customElement("ak-sidebar") | ||||
| export class Sidebar extends LitElement { | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { | ||||
|     css, | ||||
|     CSSResult, | ||||
|     customElement, | ||||
|     html, | ||||
|     LitElement, | ||||
|     property, | ||||
|     TemplateResult, | ||||
| } from "lit-element"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGlobal from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| @ -25,7 +33,7 @@ export const DefaultTenant: CurrentTenant = { | ||||
|  | ||||
| @customElement("ak-sidebar-brand") | ||||
| export class SidebarBrand extends LitElement { | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     tenant: CurrentTenant = DefaultTenant; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
| @ -45,7 +53,7 @@ export class SidebarBrand extends LitElement { | ||||
|                 } | ||||
|                 .pf-c-brand img { | ||||
|                     width: 100%; | ||||
|                     padding: 0 .5rem; | ||||
|                     padding: 0 0.5rem; | ||||
|                     height: 42px; | ||||
|                 } | ||||
|                 button.pf-c-button.sidebar-trigger { | ||||
| @ -67,28 +75,34 @@ export class SidebarBrand extends LitElement { | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         configureSentry(true); | ||||
|         tenant().then(tenant => this.tenant = tenant); | ||||
|         tenant().then((tenant) => (this.tenant = tenant)); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html` | ||||
|             ${window.innerWidth <= MIN_WIDTH ? html` | ||||
|                 <button | ||||
|                     class="sidebar-trigger pf-c-button" | ||||
|                     @click=${() => { | ||||
|                         this.dispatchEvent( | ||||
|                             new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||
|                                 bubbles: true, | ||||
|                                 composed: true, | ||||
|                             }) | ||||
|                         ); | ||||
|                     }}> | ||||
|                     <i class="fas fa-bars"></i> | ||||
|                 </button> | ||||
|             ` : html``} | ||||
|         return html` ${window.innerWidth <= MIN_WIDTH | ||||
|                 ? html` | ||||
|                       <button | ||||
|                           class="sidebar-trigger pf-c-button" | ||||
|                           @click=${() => { | ||||
|                               this.dispatchEvent( | ||||
|                                   new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||
|                                       bubbles: true, | ||||
|                                       composed: true, | ||||
|                                   }), | ||||
|                               ); | ||||
|                           }} | ||||
|                       > | ||||
|                           <i class="fas fa-bars"></i> | ||||
|                       </button> | ||||
|                   ` | ||||
|                 : html``} | ||||
|             <a href="#/" class="pf-c-page__header-brand-link"> | ||||
|                 <div class="pf-c-brand ak-brand"> | ||||
|                     <img src="${ifDefined(this.tenant.brandingLogo)}" alt="authentik icon" loading="lazy" /> | ||||
|                     <img | ||||
|                         src="${ifDefined(this.tenant.brandingLogo)}" | ||||
|                         alt="authentik icon" | ||||
|                         loading="lazy" | ||||
|                     /> | ||||
|                 </div> | ||||
|             </a>`; | ||||
|     } | ||||
|  | ||||
| @ -9,7 +9,6 @@ import { ROUTE_SEPARATOR } from "../../constants"; | ||||
|  | ||||
| @customElement("ak-sidebar-item") | ||||
| export class SidebarItem extends LitElement { | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
| @ -25,7 +24,7 @@ export class SidebarItem extends LitElement { | ||||
|                     background-color: var(--ak-accent); | ||||
|                     margin: 16px; | ||||
|                 } | ||||
|                 :host([highlight]) .pf-c-nav__item .pf-c-nav__link  { | ||||
|                 :host([highlight]) .pf-c-nav__item .pf-c-nav__link { | ||||
|                     padding-left: 0.5rem; | ||||
|                 } | ||||
|                 .pf-c-nav__link.pf-m-current::after, | ||||
| @ -92,13 +91,13 @@ export class SidebarItem extends LitElement { | ||||
|  | ||||
|     get childItems(): SidebarItem[] { | ||||
|         const children = Array.from(this.querySelectorAll<SidebarItem>("ak-sidebar-item") || []); | ||||
|         children.forEach(child => child.parent = this); | ||||
|         children.forEach((child) => (child.parent = this)); | ||||
|         return children; | ||||
|     } | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     set activeWhen(regexp: string[]) { | ||||
|         regexp.forEach(r => { | ||||
|         regexp.forEach((r) => { | ||||
|             this.activeMatchers.push(new RegExp(r)); | ||||
|         }); | ||||
|     } | ||||
| @ -110,7 +109,7 @@ export class SidebarItem extends LitElement { | ||||
|  | ||||
|     onHashChange(): void { | ||||
|         const activePath = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0]; | ||||
|         this.childItems.forEach(item => { | ||||
|         this.childItems.forEach((item) => { | ||||
|             this.expandParentRecursive(activePath, item); | ||||
|         }); | ||||
|         this.isActive = this.matchesPath(activePath); | ||||
| @ -125,7 +124,7 @@ export class SidebarItem extends LitElement { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|         return this.activeMatchers.some(v => { | ||||
|         return this.activeMatchers.some((v) => { | ||||
|             const match = v.exec(path); | ||||
|             if (match !== null) { | ||||
|                 return true; | ||||
| @ -138,7 +137,7 @@ export class SidebarItem extends LitElement { | ||||
|             item.parent.expanded = true; | ||||
|             this.requestUpdate(); | ||||
|         } | ||||
|         item.childItems.forEach(i => this.expandParentRecursive(activePath, i)); | ||||
|         item.childItems.forEach((i) => this.expandParentRecursive(activePath, i)); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
| @ -153,10 +152,16 @@ export class SidebarItem extends LitElement { | ||||
|             } | ||||
|         } | ||||
|         if (this.childItems.length > 0) { | ||||
|             return html`<li class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}"> | ||||
|                 <button class="pf-c-nav__link" aria-expanded="true" @click=${() => { | ||||
|                     this.expanded = !this.expanded; | ||||
|                 }}> | ||||
|             return html`<li | ||||
|                 class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}" | ||||
|             > | ||||
|                 <button | ||||
|                     class="pf-c-nav__link" | ||||
|                     aria-expanded="true" | ||||
|                     @click=${() => { | ||||
|                         this.expanded = !this.expanded; | ||||
|                     }} | ||||
|                 > | ||||
|                     <slot name="label"></slot> | ||||
|                     <span class="pf-c-nav__toggle"> | ||||
|                         <span class="pf-c-nav__toggle-icon"> | ||||
| @ -172,15 +177,20 @@ export class SidebarItem extends LitElement { | ||||
|             </li>`; | ||||
|         } | ||||
|         return html`<li class="pf-c-nav__item"> | ||||
|             ${this.path ? html` | ||||
|                 <a href="${this.isAbsoluteLink ? "" : "#"}${this.path}" class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}"> | ||||
|                     <slot name="label"></slot> | ||||
|                 </a> | ||||
|             ` : html` | ||||
|                 <span class="pf-c-nav__link"> | ||||
|                     <slot name="label"></slot> | ||||
|                 </span> | ||||
|             `} | ||||
|             ${this.path | ||||
|                 ? html` | ||||
|                       <a | ||||
|                           href="${this.isAbsoluteLink ? "" : "#"}${this.path}" | ||||
|                           class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}" | ||||
|                       > | ||||
|                           <slot name="label"></slot> | ||||
|                       </a> | ||||
|                   ` | ||||
|                 : html` | ||||
|                       <span class="pf-c-nav__link"> | ||||
|                           <slot name="label"></slot> | ||||
|                       </span> | ||||
|                   `} | ||||
|         </li>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -9,7 +9,6 @@ import { ifDefined } from "lit-html/directives/if-defined"; | ||||
|  | ||||
| @customElement("ak-sidebar-user") | ||||
| export class SidebarUser extends LitElement { | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
| @ -34,9 +33,16 @@ export class SidebarUser extends LitElement { | ||||
|     render(): TemplateResult { | ||||
|         return html` | ||||
|             <a href="#/user" class="pf-c-nav__link user-avatar" id="user-settings"> | ||||
|                 ${until(me().then((u) => { | ||||
|                     return html`<img class="pf-c-avatar" src="${ifDefined(u.user.avatar)}" alt="" />`; | ||||
|                 }), html``)} | ||||
|                 ${until( | ||||
|                     me().then((u) => { | ||||
|                         return html`<img | ||||
|                             class="pf-c-avatar" | ||||
|                             src="${ifDefined(u.user.avatar)}" | ||||
|                             alt="" | ||||
|                         />`; | ||||
|                     }), | ||||
|                     html``, | ||||
|                 )} | ||||
|             </a> | ||||
|             <a href="/flows/-/default/invalidation/" class="pf-c-nav__link user-logout" id="logout"> | ||||
|                 <i class="fas fa-sign-out-alt" aria-hidden="true"></i> | ||||
|  | ||||
| @ -20,7 +20,6 @@ import { EVENT_REFRESH } from "../../constants"; | ||||
| import { ifDefined } from "lit-html/directives/if-defined"; | ||||
|  | ||||
| export class TableColumn { | ||||
|  | ||||
|     title: string; | ||||
|     orderBy?: string; | ||||
|  | ||||
| @ -45,25 +44,27 @@ export class TableColumn { | ||||
|  | ||||
|     private getSortIndicator(table: Table<unknown>): string { | ||||
|         switch (table.order) { | ||||
|         case this.orderBy: | ||||
|             return "fa-long-arrow-alt-down"; | ||||
|         case `-${this.orderBy}`: | ||||
|             return "fa-long-arrow-alt-up"; | ||||
|         default: | ||||
|             return "fa-arrows-alt-v"; | ||||
|             case this.orderBy: | ||||
|                 return "fa-long-arrow-alt-down"; | ||||
|             case `-${this.orderBy}`: | ||||
|                 return "fa-long-arrow-alt-up"; | ||||
|             default: | ||||
|                 return "fa-arrows-alt-v"; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     renderSortable(table: Table<unknown>): TemplateResult { | ||||
|         return html` | ||||
|             <button class="pf-c-table__button" @click=${() => this.headerClickHandler(table)}> | ||||
|                 <div class="pf-c-table__button-content"> | ||||
|                     <span class="pf-c-table__text">${this.title}</span> | ||||
|                     <span class="pf-c-table__sort-indicator"> | ||||
|                         <i class="fas ${this.getSortIndicator(table)}"></i> | ||||
|                     </span> | ||||
|                 </div> | ||||
|             </button>`; | ||||
|         return html` <button | ||||
|             class="pf-c-table__button" | ||||
|             @click=${() => this.headerClickHandler(table)} | ||||
|         > | ||||
|             <div class="pf-c-table__button-content"> | ||||
|                 <span class="pf-c-table__text">${this.title}</span> | ||||
|                 <span class="pf-c-table__sort-indicator"> | ||||
|                     <i class="fas ${this.getSortIndicator(table)}"></i> | ||||
|                 </span> | ||||
|             </div> | ||||
|         </button>`; | ||||
|     } | ||||
|  | ||||
|     render(table: Table<unknown>): TemplateResult { | ||||
| @ -72,12 +73,14 @@ export class TableColumn { | ||||
|             scope="col" | ||||
|             class=" | ||||
|                 ${this.orderBy ? "pf-c-table__sort " : " "} | ||||
|                 ${(table.order === this.orderBy || table.order === `-${this.orderBy}`) ? "pf-m-selected " : ""} | ||||
|             "> | ||||
|                 ${table.order === this.orderBy || table.order === `-${this.orderBy}` | ||||
|                 ? "pf-m-selected " | ||||
|                 : ""} | ||||
|             " | ||||
|         > | ||||
|             ${this.orderBy ? this.renderSortable(table) : html`${this.title}`} | ||||
|         </th>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| export abstract class Table<T> extends LitElement { | ||||
| @ -99,32 +102,41 @@ export abstract class Table<T> extends LitElement { | ||||
|         return html``; | ||||
|     } | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     data?: AKResponse<T>; | ||||
|  | ||||
|     @property({type: Number}) | ||||
|     @property({ type: Number }) | ||||
|     page = 1; | ||||
|  | ||||
|     @property({type: String}) | ||||
|     @property({ type: String }) | ||||
|     order?: string; | ||||
|  | ||||
|     @property({type: String}) | ||||
|     @property({ type: String }) | ||||
|     search?: string; | ||||
|  | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     checkbox = false; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     selectedElements: T[] = []; | ||||
|  | ||||
|     @property({type: Boolean}) | ||||
|     @property({ type: Boolean }) | ||||
|     expandable = false; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     expandedRows: boolean[] = []; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFTable, PFBullseye, PFButton, PFToolbar, PFDropdown, PFPagination, AKGlobal]; | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFTable, | ||||
|             PFBullseye, | ||||
|             PFButton, | ||||
|             PFToolbar, | ||||
|             PFDropdown, | ||||
|             PFPagination, | ||||
|             AKGlobal, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
| @ -139,24 +151,23 @@ export abstract class Table<T> extends LitElement { | ||||
|             return; | ||||
|         } | ||||
|         this.isLoading = true; | ||||
|         this.apiEndpoint(this.page).then((r) => { | ||||
|             this.data = r; | ||||
|             this.page = r.pagination.current; | ||||
|             this.expandedRows = []; | ||||
|             this.isLoading = false; | ||||
|         }).catch(() => { | ||||
|             this.isLoading = false; | ||||
|         }); | ||||
|         this.apiEndpoint(this.page) | ||||
|             .then((r) => { | ||||
|                 this.data = r; | ||||
|                 this.page = r.pagination.current; | ||||
|                 this.expandedRows = []; | ||||
|                 this.isLoading = false; | ||||
|             }) | ||||
|             .catch(() => { | ||||
|                 this.isLoading = false; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     private renderLoading(): TemplateResult { | ||||
|         return html`<tr role="row"> | ||||
|             <td role="cell" colspan="25"> | ||||
|                 <div class="pf-l-bullseye"> | ||||
|                     <ak-empty-state | ||||
|                         ?loading="${true}" | ||||
|                         header=${t`Loading`}> | ||||
|                     </ak-empty-state> | ||||
|                     <ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state> | ||||
|                 </div> | ||||
|             </td> | ||||
|         </tr>`; | ||||
| @ -167,7 +178,11 @@ export abstract class Table<T> extends LitElement { | ||||
|             <tr role="row"> | ||||
|                 <td role="cell" colspan="8"> | ||||
|                     <div class="pf-l-bullseye"> | ||||
|                         ${inner ? inner : html`<ak-empty-state header="${t`No objects found.`}"></ak-empty-state>`} | ||||
|                         ${inner | ||||
|                             ? inner | ||||
|                             : html`<ak-empty-state | ||||
|                                   header="${t`No objects found.`}" | ||||
|                               ></ak-empty-state>`} | ||||
|                     </div> | ||||
|                 </td> | ||||
|             </tr> | ||||
| @ -182,40 +197,62 @@ export abstract class Table<T> extends LitElement { | ||||
|             return [this.renderEmpty()]; | ||||
|         } | ||||
|         return this.data.results.map((item: T, idx: number) => { | ||||
|             if ((this.expandedRows.length - 1) < idx) { | ||||
|             if (this.expandedRows.length - 1 < idx) { | ||||
|                 this.expandedRows[idx] = false; | ||||
|             } | ||||
|             return html`<tbody role="rowgroup" class="${this.expandedRows[idx] ? "pf-m-expanded" : ""}"> | ||||
|             return html`<tbody | ||||
|                 role="rowgroup" | ||||
|                 class="${this.expandedRows[idx] ? "pf-m-expanded" : ""}" | ||||
|             > | ||||
|                 <tr role="row"> | ||||
|                     ${this.checkbox ? html`<td class="pf-c-table__check" role="cell"> | ||||
|                         <input type="checkbox" | ||||
|                             ?checked=${this.selectedElements.indexOf(item) >= 0} | ||||
|                             @input=${(ev: InputEvent) => { | ||||
|                                 if ((ev.target as HTMLInputElement).checked) { | ||||
|                                     // Add item to selected | ||||
|                                     this.selectedElements.push(item); | ||||
|                                 } else { | ||||
|                                     // Get index of item and remove if selected | ||||
|                                     const index = this.selectedElements.indexOf(item); | ||||
|                                     if (index <= -1) return; | ||||
|                                     this.selectedElements.splice(index, 1); | ||||
|                                 } | ||||
|                                 this.requestUpdate(); | ||||
|                             }} /> | ||||
|                     </td>` : html``} | ||||
|                     ${this.expandable ? html`<td class="pf-c-table__toggle" role="cell"> | ||||
|                     <button class="pf-c-button pf-m-plain ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" @click=${() => { | ||||
|                         this.expandedRows[idx] = !this.expandedRows[idx]; | ||||
|                         this.requestUpdate(); | ||||
|                     }}> | ||||
|                         <div class="pf-c-table__toggle-icon"> <i class="fas fa-angle-down" aria-hidden="true"></i> </div> | ||||
|                     </button> | ||||
|                     </td>` : html``} | ||||
|                     ${this.checkbox | ||||
|                         ? html`<td class="pf-c-table__check" role="cell"> | ||||
|                               <input | ||||
|                                   type="checkbox" | ||||
|                                   ?checked=${this.selectedElements.indexOf(item) >= 0} | ||||
|                                   @input=${(ev: InputEvent) => { | ||||
|                                       if ((ev.target as HTMLInputElement).checked) { | ||||
|                                           // Add item to selected | ||||
|                                           this.selectedElements.push(item); | ||||
|                                       } else { | ||||
|                                           // Get index of item and remove if selected | ||||
|                                           const index = this.selectedElements.indexOf(item); | ||||
|                                           if (index <= -1) return; | ||||
|                                           this.selectedElements.splice(index, 1); | ||||
|                                       } | ||||
|                                       this.requestUpdate(); | ||||
|                                   }} | ||||
|                               /> | ||||
|                           </td>` | ||||
|                         : html``} | ||||
|                     ${this.expandable | ||||
|                         ? html`<td class="pf-c-table__toggle" role="cell"> | ||||
|                               <button | ||||
|                                   class="pf-c-button pf-m-plain ${this.expandedRows[idx] | ||||
|                                       ? "pf-m-expanded" | ||||
|                                       : ""}" | ||||
|                                   @click=${() => { | ||||
|                                       this.expandedRows[idx] = !this.expandedRows[idx]; | ||||
|                                       this.requestUpdate(); | ||||
|                                   }} | ||||
|                               > | ||||
|                                   <div class="pf-c-table__toggle-icon"> | ||||
|                                        <i class="fas fa-angle-down" aria-hidden="true"></i | ||||
|                                       >  | ||||
|                                   </div> | ||||
|                               </button> | ||||
|                           </td>` | ||||
|                         : html``} | ||||
|                     ${this.row(item).map((col) => { | ||||
|                         return html`<td role="cell">${col}</td>`; | ||||
|                     })} | ||||
|                 </tr> | ||||
|                 <tr class="pf-c-table__expandable-row ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" role="row"> | ||||
|                 <tr | ||||
|                     class="pf-c-table__expandable-row ${this.expandedRows[idx] | ||||
|                         ? "pf-m-expanded" | ||||
|                         : ""}" | ||||
|                     role="row" | ||||
|                 > | ||||
|                     <td></td> | ||||
|                     ${this.expandedRows[idx] ? this.renderExpanded(item) : html``} | ||||
|                 </tr> | ||||
| @ -230,10 +267,11 @@ export abstract class Table<T> extends LitElement { | ||||
|                     new CustomEvent(EVENT_REFRESH, { | ||||
|                         bubbles: true, | ||||
|                         composed: true, | ||||
|                     }) | ||||
|                     }), | ||||
|                 ); | ||||
|             }} | ||||
|             class="pf-c-button pf-m-primary"> | ||||
|             class="pf-c-button pf-m-primary" | ||||
|         > | ||||
|             ${t`Refresh`} | ||||
|         </button>`; | ||||
|     } | ||||
| @ -246,16 +284,20 @@ export abstract class Table<T> extends LitElement { | ||||
|         if (!this.searchEnabled()) { | ||||
|             return html``; | ||||
|         } | ||||
|         return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => { | ||||
|             this.search = value; | ||||
|             this.dispatchEvent( | ||||
|                 new CustomEvent(EVENT_REFRESH, { | ||||
|                     bubbles: true, | ||||
|                     composed: true, | ||||
|                 }) | ||||
|             ); | ||||
|         }}> | ||||
|         </ak-table-search> `; | ||||
|         return html`<ak-table-search | ||||
|                 value=${ifDefined(this.search)} | ||||
|                 .onSearch=${(value: string) => { | ||||
|                     this.search = value; | ||||
|                     this.dispatchEvent( | ||||
|                         new CustomEvent(EVENT_REFRESH, { | ||||
|                             bubbles: true, | ||||
|                             composed: true, | ||||
|                         }), | ||||
|                     ); | ||||
|                 }} | ||||
|             > | ||||
|             </ak-table-search | ||||
|             > `; | ||||
|     } | ||||
|  | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
| @ -268,20 +310,17 @@ export abstract class Table<T> extends LitElement { | ||||
|     } | ||||
|  | ||||
|     renderTable(): TemplateResult { | ||||
|         return html` | ||||
|             ${this.checkbox ? | ||||
|                 html`<ak-chip-group> | ||||
|                         ${this.selectedElements.map(el => { | ||||
|                             return html`<ak-chip>${this.renderSelectedChip(el)}</ak-chip>`; | ||||
|                         })} | ||||
|                     </ak-chip-group>`: | ||||
|                 html``} | ||||
|         return html` ${this.checkbox | ||||
|                 ? html`<ak-chip-group> | ||||
|                       ${this.selectedElements.map((el) => { | ||||
|                           return html`<ak-chip>${this.renderSelectedChip(el)}</ak-chip>`; | ||||
|                       })} | ||||
|                   </ak-chip-group>` | ||||
|                 : html``} | ||||
|             <div class="pf-c-toolbar"> | ||||
|                 <div class="pf-c-toolbar__content"> | ||||
|                     ${this.renderSearch()} | ||||
|                     <div class="pf-c-toolbar__bulk-select"> | ||||
|                         ${this.renderToolbar()} | ||||
|                     </div> | ||||
|                     <div class="pf-c-toolbar__bulk-select">${this.renderToolbar()}</div> | ||||
|                     ${this.renderToolbarAfter()} | ||||
|                     <ak-table-pagination | ||||
|                         class="pf-c-toolbar__item pf-m-pagination" | ||||
| @ -292,29 +331,36 @@ export abstract class Table<T> extends LitElement { | ||||
|                                 new CustomEvent(EVENT_REFRESH, { | ||||
|                                     bubbles: true, | ||||
|                                     composed: true, | ||||
|                                 }) | ||||
|                                 }), | ||||
|                             ); | ||||
|                         }}> | ||||
|                         }} | ||||
|                     > | ||||
|                     </ak-table-pagination> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <table class="pf-c-table pf-m-compact pf-m-grid-md pf-m-expandable"> | ||||
|                 <thead> | ||||
|                     <tr role="row"> | ||||
|                         ${this.checkbox ? html`<td class="pf-c-table__check" role="cell"> | ||||
|                             <input type="checkbox" aria-label=${t`Select all rows`} @input=${(ev: InputEvent) => { | ||||
|                                 if ((ev.target as HTMLInputElement).checked) { | ||||
|                                     this.selectedElements = this.data?.results || []; | ||||
|                                 } else { | ||||
|                                     this.selectedElements = []; | ||||
|                                 } | ||||
|                             }} /> | ||||
|                         </td>` : html``} | ||||
|                         ${this.checkbox | ||||
|                             ? html`<td class="pf-c-table__check" role="cell"> | ||||
|                                   <input | ||||
|                                       type="checkbox" | ||||
|                                       aria-label=${t`Select all rows`} | ||||
|                                       @input=${(ev: InputEvent) => { | ||||
|                                           if ((ev.target as HTMLInputElement).checked) { | ||||
|                                               this.selectedElements = this.data?.results || []; | ||||
|                                           } else { | ||||
|                                               this.selectedElements = []; | ||||
|                                           } | ||||
|                                       }} | ||||
|                                   /> | ||||
|                               </td>` | ||||
|                             : html``} | ||||
|                         ${this.expandable ? html`<td role="cell"></td>` : html``} | ||||
|                         ${this.columns().map((col) => col.render(this))} | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|                 ${(this.isLoading || !this.data) ? this.renderLoading() : this.renderRows()} | ||||
|                 ${this.isLoading || !this.data ? this.renderLoading() : this.renderRows()} | ||||
|             </table> | ||||
|             <div class="pf-c-pagination pf-m-bottom"> | ||||
|                 <ak-table-pagination | ||||
| @ -326,9 +372,10 @@ export abstract class Table<T> extends LitElement { | ||||
|                             new CustomEvent(EVENT_REFRESH, { | ||||
|                                 bubbles: true, | ||||
|                                 composed: true, | ||||
|                             }) | ||||
|                             }), | ||||
|                         ); | ||||
|                     }}> | ||||
|                     }} | ||||
|                 > | ||||
|                 </ak-table-pagination> | ||||
|             </div>`; | ||||
|     } | ||||
|  | ||||
| @ -19,7 +19,16 @@ export abstract class TableModal<T> extends Table<T> { | ||||
|     open = false; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat(PFModalBox, PFBullseye, PFContent, PFBackdrop, PFPage, PFStack, AKGlobal, MODAL_BUTTON_STYLES); | ||||
|         return super.styles.concat( | ||||
|             PFModalBox, | ||||
|             PFBullseye, | ||||
|             PFContent, | ||||
|             PFBackdrop, | ||||
|             PFPage, | ||||
|             PFStack, | ||||
|             AKGlobal, | ||||
|             MODAL_BUTTON_STYLES, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
| @ -33,7 +42,7 @@ export abstract class TableModal<T> extends Table<T> { | ||||
|     } | ||||
|  | ||||
|     resetForms(): void { | ||||
|         this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach(form => { | ||||
|         this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach((form) => { | ||||
|             if ("resetForm" in form) { | ||||
|                 form?.resetForm(); | ||||
|             } | ||||
| @ -42,7 +51,7 @@ export abstract class TableModal<T> extends Table<T> { | ||||
|  | ||||
|     onClick(): void { | ||||
|         this.open = true; | ||||
|         this.querySelectorAll("*").forEach(child => { | ||||
|         this.querySelectorAll("*").forEach((child) => { | ||||
|             if ("requestUpdate" in child) { | ||||
|                 (child as LitElement).requestUpdate(); | ||||
|             } | ||||
| @ -56,11 +65,7 @@ export abstract class TableModal<T> extends Table<T> { | ||||
|     renderModal(): TemplateResult { | ||||
|         return html`<div class="pf-c-backdrop"> | ||||
|             <div class="pf-l-bullseye"> | ||||
|                 <div | ||||
|                     class="pf-c-modal-box ${this.size}" | ||||
|                     role="dialog" | ||||
|                     aria-modal="true" | ||||
|                 > | ||||
|                 <div class="pf-c-modal-box ${this.size}" role="dialog" aria-modal="true"> | ||||
|                     <button | ||||
|                         @click=${() => (this.open = false)} | ||||
|                         class="pf-c-button pf-m-plain" | ||||
|  | ||||
| @ -19,7 +19,8 @@ export abstract class TablePage<T> extends Table<T> { | ||||
|         return html`<ak-page-header | ||||
|                 icon=${this.pageIcon()} | ||||
|                 header=${this.pageTitle()} | ||||
|                 description=${ifDefined(this.pageDescription())}> | ||||
|                 description=${ifDefined(this.pageDescription())} | ||||
|             > | ||||
|             </ak-page-header> | ||||
|             <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                 <div class="pf-c-card">${this.renderTable()}</div> | ||||
|  | ||||
| @ -8,12 +8,12 @@ import AKGlobal from "../../authentik.css"; | ||||
|  | ||||
| @customElement("ak-table-pagination") | ||||
| export class TablePagination extends LitElement { | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     pages?: AKPagination; | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     @property({ attribute: false }) | ||||
|     // eslint-disable-next-line | ||||
|     pageChangeHandler: (page: number) => void = (page: number) => {} | ||||
|     pageChangeHandler: (page: number) => void = (page: number) => {}; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, PFPagination, AKGlobal]; | ||||
| @ -33,7 +33,9 @@ export class TablePagination extends LitElement { | ||||
|                     <div class="pf-c-pagination__nav-control pf-m-prev"> | ||||
|                         <button | ||||
|                             class="pf-c-button pf-m-plain" | ||||
|                             @click=${() => { this.pageChangeHandler(this.pages?.previous || 0); }} | ||||
|                             @click=${() => { | ||||
|                                 this.pageChangeHandler(this.pages?.previous || 0); | ||||
|                             }} | ||||
|                             ?disabled="${(this.pages?.previous || 0) < 1}" | ||||
|                             aria-label="${t`Go to previous page`}" | ||||
|                         > | ||||
| @ -43,7 +45,9 @@ export class TablePagination extends LitElement { | ||||
|                     <div class="pf-c-pagination__nav-control pf-m-next"> | ||||
|                         <button | ||||
|                             class="pf-c-button pf-m-plain" | ||||
|                             @click=${() => { this.pageChangeHandler(this.pages?.next || 0); }} | ||||
|                             @click=${() => { | ||||
|                                 this.pageChangeHandler(this.pages?.next || 0); | ||||
|                             }} | ||||
|                             ?disabled="${(this.pages?.next || 0) <= 0}" | ||||
|                             aria-label="${t`Go to next page`}" | ||||
|                         > | ||||
|  | ||||
| @ -10,7 +10,6 @@ import { t } from "@lingui/macro"; | ||||
|  | ||||
| @customElement("ak-table-search") | ||||
| export class TableSearch extends LitElement { | ||||
|  | ||||
|     @property() | ||||
|     value?: string; | ||||
|  | ||||
| @ -23,25 +22,36 @@ export class TableSearch extends LitElement { | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="pf-c-toolbar__group pf-m-filter-group"> | ||||
|                 <div class="pf-c-toolbar__item pf-m-search-filter"> | ||||
|                     <form class="pf-c-input-group" method="GET" @submit=${(e: Event) => { | ||||
|             <div class="pf-c-toolbar__item pf-m-search-filter"> | ||||
|                 <form | ||||
|                     class="pf-c-input-group" | ||||
|                     method="GET" | ||||
|                     @submit=${(e: Event) => { | ||||
|                         e.preventDefault(); | ||||
|                         if (!this.onSearch) return; | ||||
|                         const el = this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]"); | ||||
|                         const el = | ||||
|                             this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]"); | ||||
|                         if (!el) return; | ||||
|                         if (el.value === "") return; | ||||
|                         this.onSearch(el?.value); | ||||
|                     }}> | ||||
|                         <input class="pf-c-form-control" name="search" type="search" placeholder=${t`Search...`} value="${ifDefined(this.value)}" @search=${(ev: Event) => { | ||||
|                     }} | ||||
|                 > | ||||
|                     <input | ||||
|                         class="pf-c-form-control" | ||||
|                         name="search" | ||||
|                         type="search" | ||||
|                         placeholder=${t`Search...`} | ||||
|                         value="${ifDefined(this.value)}" | ||||
|                         @search=${(ev: Event) => { | ||||
|                             if (!this.onSearch) return; | ||||
|                             this.onSearch((ev.target as HTMLInputElement).value); | ||||
|                         }}> | ||||
|                         <button class="pf-c-button pf-m-control" type="submit"> | ||||
|                             <i class="fas fa-search" aria-hidden="true"></i> | ||||
|                         </button> | ||||
|                     </form> | ||||
|                 </div> | ||||
|             </div>`; | ||||
|                         }} | ||||
|                     /> | ||||
|                     <button class="pf-c-button pf-m-control" type="submit"> | ||||
|                         <i class="fas fa-search" aria-hidden="true"></i> | ||||
|                     </button> | ||||
|                 </form> | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -10,7 +10,6 @@ import { DEFAULT_CONFIG } from "../../api/Config"; | ||||
|  | ||||
| @customElement("ak-user-session-list") | ||||
| export class AuthenticatedSessionList extends Table<AuthenticatedSession> { | ||||
|  | ||||
|     @property() | ||||
|     targetUser!: string; | ||||
|  | ||||
| @ -41,8 +40,7 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> { | ||||
|             html`${item.userAgent.userAgent?.family}`, | ||||
|             html`${item.userAgent.os?.family}`, | ||||
|             html`${item.expires?.toLocaleString()}`, | ||||
|             html` | ||||
|             <ak-forms-delete | ||||
|             html` <ak-forms-delete | ||||
|                 .obj=${item} | ||||
|                 objectLabel=${t`Session`} | ||||
|                 .usedBy=${() => { | ||||
| @ -54,12 +52,10 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> { | ||||
|                     return new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsDestroy({ | ||||
|                         uuid: item.uuid || "", | ||||
|                     }); | ||||
|                 }}> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                     ${t`Delete Session`} | ||||
|                 </button> | ||||
|                 }} | ||||
|             > | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-danger">${t`Delete Session`}</button> | ||||
|             </ak-forms-delete>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -36,25 +36,22 @@ export class UserConsentList extends Table<UserConsent> { | ||||
|         return [ | ||||
|             html`${item.application.name}`, | ||||
|             html`${item.expires?.toLocaleString()}`, | ||||
|             html` | ||||
|             <ak-forms-delete | ||||
|             html` <ak-forms-delete | ||||
|                 .obj=${item} | ||||
|                 objectLabel=${t`Consent`} | ||||
|                 .usedBy=${() => { | ||||
|                     return new CoreApi(DEFAULT_CONFIG).coreUserConsentUsedByList({ | ||||
|                         id: item.pk | ||||
|                         id: item.pk, | ||||
|                     }); | ||||
|                 }} | ||||
|                 .delete=${() => { | ||||
|                     return new CoreApi(DEFAULT_CONFIG).coreUserConsentDestroy({ | ||||
|                         id: item.pk, | ||||
|                     }); | ||||
|                 }}> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                     ${t`Delete Consent`} | ||||
|                 </button> | ||||
|                 }} | ||||
|             > | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-danger">${t`Delete Consent`}</button> | ||||
|             </ak-forms-delete>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer