web: re-format with prettier

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer
2021-08-03 17:52:21 +02:00
parent 77ed25ae34
commit 2c60ec50be
218 changed files with 11696 additions and 8225 deletions

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
});
});
}

View File

@ -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() : ""}`;
}
}

View File

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

View File

@ -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"]);
});
});
}
};
}

View File

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

View File

@ -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>&nbsp;${s.toString()}`;
});
}
@ -23,5 +23,4 @@ export class AggregatePromiseCard extends AggregateCard {
${until(this.promiseProxy(), html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`)}
</p>`;
}
}

View File

@ -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,
};
}) || [],
},
]
],
};
}
}

View File

@ -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,
};
}) || [],
},
]
],
};
}
}

View File

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

View File

@ -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,
};
}) || [],
},
]
],
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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>&nbsp;
<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
>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.open = false;
}}
class="pf-m-secondary"
>
${t`Cancel`}
</ak-spinner-button>
</footer>`;
}
}

View File

@ -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>&nbsp;
<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
>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.open = false;
}}
class="pf-m-secondary"
>
${t`Cancel`}
</ak-spinner-button>
</footer>`;
}
}

View File

@ -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();
}
}

View File

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

View File

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

View File

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

View File

@ -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>&nbsp;
<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
>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.resetForms();
this.open = false;
}}
class="pf-m-secondary"
>
${t`Cancel`}
</ak-spinner-button>
</footer>`;
}
}

View File

@ -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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,6 @@ import "./SidebarUser";
@customElement("ak-sidebar")
export class Sidebar extends LitElement {
static get styles(): CSSResult[] {
return [
PFBase,

View File

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

View File

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

View File

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

View File

@ -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">&nbsp;<i class="fas fa-angle-down" aria-hidden="true"></i>&nbsp;</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">
&nbsp;<i class="fas fa-angle-down" aria-hidden="true"></i
>&nbsp;
</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>&nbsp;`;
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
>&nbsp;`;
}
// 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>`;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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