Compare commits
2 Commits
web/cleanu
...
web/reques
Author | SHA1 | Date | |
---|---|---|---|
c8be337414 | |||
5c85c2c9e6 |
2443
tests/wdio/package-lock.json
generated
2443
tests/wdio/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,8 +14,8 @@
|
||||
"build": "run-s build-locales esbuild:build",
|
||||
"build-proxy": "run-s build-locales esbuild:build-proxy",
|
||||
"watch": "run-s build-locales esbuild:watch",
|
||||
"lint": "cross-env NODE_OPTIONS='--max_old_space_size=16384' eslint . --max-warnings 0 --fix",
|
||||
"lint:precommit": "cross-env NODE_OPTIONS='--max_old_space_size=16384' node scripts/eslint-precommit.mjs",
|
||||
"lint": "cross-env NODE_OPTIONS='--max_old_space_size=65536' eslint . --max-warnings 0 --fix",
|
||||
"lint:precommit": "cross-env NODE_OPTIONS='--max_old_space_size=65536' node scripts/eslint-precommit.mjs",
|
||||
"lint:spelling": "node scripts/check-spelling.mjs",
|
||||
"lit-analyse": "lit-analyzer src",
|
||||
"precommit": "npm-run-all --parallel tsc lit-analyse lint:spelling --sequential lint:precommit prettier",
|
||||
|
@ -12,4 +12,8 @@ export const authentikEnterpriseContext = createContext<LicenseSummary>(
|
||||
|
||||
export const authentikBrandContext = createContext<CurrentBrand>(Symbol("authentik-brand-context"));
|
||||
|
||||
export const authentikLocalStoreContext = createContext<unknown>(
|
||||
Symbol("authentik-local-store-context"),
|
||||
);
|
||||
|
||||
export default authentikConfigContext;
|
||||
|
31
web/src/elements/utils/tryCatch.ts
Normal file
31
web/src/elements/utils/tryCatch.ts
Normal file
@ -0,0 +1,31 @@
|
||||
type TryFn<T> = () => T;
|
||||
type CatchFn<T> = (error: unknown) => T;
|
||||
|
||||
type TryCatchArgs<T> = {
|
||||
tryFn: TryFn<T>;
|
||||
catchFn?: CatchFn<T>;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isTryCatchArgs = <T>(t: any): t is TryCatchArgs<T> =>
|
||||
typeof t === "object" && "tryFn" in t && "catchFn" in t;
|
||||
|
||||
export function tryCatch<T>({ tryFn, catchFn }: TryCatchArgs<T>): T;
|
||||
export function tryCatch<T>(tryFn: TryFn<T>): T;
|
||||
export function tryCatch<T>(tryFn: TryFn<T>, catchFn: CatchFn<T>): T;
|
||||
export function tryCatch<T>(tryFn: TryFn<T> | TryCatchArgs<T>, catchFn?: CatchFn<T>): T {
|
||||
if (isTryCatchArgs(tryFn)) {
|
||||
catchFn = tryFn.catchFn;
|
||||
tryFn = tryFn.tryFn;
|
||||
}
|
||||
|
||||
if (catchFn === undefined) {
|
||||
catchFn = () => null as T;
|
||||
}
|
||||
|
||||
try {
|
||||
return tryFn();
|
||||
} catch (error) {
|
||||
return catchFn(error);
|
||||
}
|
||||
}
|
95
web/src/user/LibraryPage/ApplicationCards.ts
Normal file
95
web/src/user/LibraryPage/ApplicationCards.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { LayoutType } from "@goauthentik/common/ui/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import type { Application } from "@goauthentik/api";
|
||||
|
||||
import type { AppGroupEntry, AppGroupList } from "./types";
|
||||
|
||||
type Pair = [string, string];
|
||||
|
||||
// prettier-ignore
|
||||
const LAYOUTS = new Map<string, [string, string]>([
|
||||
[
|
||||
"row",
|
||||
["pf-m-12-col", "pf-m-all-6-col-on-sm pf-m-all-4-col-on-md pf-m-all-5-col-on-lg pf-m-all-2-col-on-xl"]],
|
||||
[
|
||||
"2-column",
|
||||
["pf-m-6-col", "pf-m-all-12-col-on-sm pf-m-all-12-col-on-md pf-m-all-4-col-on-lg pf-m-all-4-col-on-xl"],
|
||||
],
|
||||
[
|
||||
"3-column",
|
||||
["pf-m-4-col", "pf-m-all-12-col-on-sm pf-m-all-12-col-on-md pf-m-all-6-col-on-lg pf-m-all-6-col-on-xl"],
|
||||
],
|
||||
]);
|
||||
|
||||
@customElement("ak-library-application-cards")
|
||||
export class LibraryPageApplicationCards extends AKElement {
|
||||
static get styles() {
|
||||
return [
|
||||
PFBase,
|
||||
PFEmptyState,
|
||||
PFContent,
|
||||
PFGrid,
|
||||
css`
|
||||
.app-group-header {
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1.2em;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@property({ attribute: true })
|
||||
layout = "row" as LayoutType;
|
||||
|
||||
@property({ attribute: true })
|
||||
background: string | undefined = undefined;
|
||||
|
||||
@property({ attribute: true })
|
||||
selected = "";
|
||||
|
||||
@property({ attribute: false })
|
||||
apps: AppGroupList = [];
|
||||
|
||||
get currentLayout(): Pair {
|
||||
const layout = LAYOUTS.get(this.layout);
|
||||
if (!layout) {
|
||||
console.warn(`Unrecognized layout: ${this.layout || "-undefined-"}`);
|
||||
return LAYOUTS.get("row") as Pair;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
render() {
|
||||
const [groupClass, groupGrid] = this.currentLayout;
|
||||
|
||||
return html`<div class="pf-l-grid pf-m-gutter">
|
||||
${this.apps.map(([group, apps]: AppGroupEntry) => {
|
||||
return html`<div class="pf-l-grid__item ${groupClass}">
|
||||
<div class="pf-c-content app-group-header">
|
||||
<h2>${group}</h2>
|
||||
</div>
|
||||
<div class="pf-l-grid pf-m-gutter ${groupGrid}">
|
||||
${apps.map((app: Application) => {
|
||||
return html`<ak-library-app
|
||||
class="pf-l-grid__item"
|
||||
.application=${app}
|
||||
background=${ifDefined(this.background)}
|
||||
?selected=${app.slug === this.selected}
|
||||
></ak-library-app>`;
|
||||
})}
|
||||
</div>
|
||||
</div> `;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
}
|
@ -1,48 +1,34 @@
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import { LayoutType } from "@goauthentik/common/ui/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
|
||||
import { UserInterface } from "@goauthentik/user/UserInterface";
|
||||
|
||||
import { css, html } from "lit";
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import type { Application } from "@goauthentik/api";
|
||||
|
||||
import type { AppGroupEntry, AppGroupList } from "./types";
|
||||
|
||||
type Pair = [string, string];
|
||||
|
||||
// prettier-ignore
|
||||
const LAYOUTS = new Map<string, [string, string]>([
|
||||
[
|
||||
"row",
|
||||
["pf-m-12-col", "pf-m-all-6-col-on-sm pf-m-all-4-col-on-md pf-m-all-5-col-on-lg pf-m-all-2-col-on-xl"]],
|
||||
[
|
||||
"2-column",
|
||||
["pf-m-6-col", "pf-m-all-12-col-on-sm pf-m-all-12-col-on-md pf-m-all-4-col-on-lg pf-m-all-4-col-on-xl"],
|
||||
],
|
||||
[
|
||||
"3-column",
|
||||
["pf-m-4-col", "pf-m-all-12-col-on-sm pf-m-all-12-col-on-md pf-m-all-6-col-on-lg pf-m-all-6-col-on-xl"],
|
||||
],
|
||||
]);
|
||||
|
||||
@customElement("ak-library-application-list")
|
||||
export class LibraryPageApplicationList extends AKElement {
|
||||
static get styles() {
|
||||
return [
|
||||
PFBase,
|
||||
PFTable,
|
||||
PFButton,
|
||||
PFEmptyState,
|
||||
PFContent,
|
||||
PFGrid,
|
||||
css`
|
||||
.app-group-header {
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1.2em;
|
||||
.app-row a {
|
||||
font-weight: bold;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -60,36 +46,110 @@ export class LibraryPageApplicationList extends AKElement {
|
||||
@property({ attribute: false })
|
||||
apps: AppGroupList = [];
|
||||
|
||||
get currentLayout(): Pair {
|
||||
const layout = LAYOUTS.get(this.layout);
|
||||
if (!layout) {
|
||||
console.warn(`Unrecognized layout: ${this.layout || "-undefined-"}`);
|
||||
return LAYOUTS.get("row") as Pair;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
expanded = new Set<string>();
|
||||
|
||||
render() {
|
||||
const [groupClass, groupGrid] = this.currentLayout;
|
||||
const me = rootInterface<UserInterface>()?.me;
|
||||
const canEdit =
|
||||
rootInterface()?.uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser;
|
||||
|
||||
return html`<div class="pf-l-grid pf-m-gutter">
|
||||
${this.apps.map(([group, apps]: AppGroupEntry) => {
|
||||
return html`<div class="pf-l-grid__item ${groupClass}">
|
||||
<div class="pf-c-content app-group-header">
|
||||
<h2>${group}</h2>
|
||||
</div>
|
||||
<div class="pf-l-grid pf-m-gutter ${groupGrid}">
|
||||
${apps.map((app: Application) => {
|
||||
return html`<ak-library-app
|
||||
class="pf-l-grid__item"
|
||||
.application=${app}
|
||||
background=${ifDefined(this.background)}
|
||||
?selected=${app.slug === this.selected}
|
||||
></ak-library-app>`;
|
||||
})}
|
||||
</div>
|
||||
</div> `;
|
||||
})}
|
||||
</div>`;
|
||||
const toggleExpansion = (pk: string) => {
|
||||
if (this.expanded.has(pk)) {
|
||||
this.expanded.delete(pk);
|
||||
} else {
|
||||
this.expanded.add(pk);
|
||||
}
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
const expandedClass = (pk: string) => ({
|
||||
"pf-m-expanded": this.expanded.has(pk),
|
||||
});
|
||||
|
||||
const renderExpansionCell = (app: Application) =>
|
||||
app.metaDescription
|
||||
? html`<td class="pf-c-table__toggle" role="cell">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain ${classMap(expandedClass(app.pk))}"
|
||||
@click=${() => toggleExpansion(app.pk)}
|
||||
>
|
||||
<div class="pf-c-table__toggle-icon">
|
||||
<i class="fas fa-angle-down" aria-hidden="true"></i>
|
||||
</div>
|
||||
</button>
|
||||
</td>`
|
||||
: nothing;
|
||||
|
||||
const renderAppIcon = (app: Application) =>
|
||||
html`<a
|
||||
href="${ifDefined(app.launchUrl ?? "")}"
|
||||
target="${ifDefined(app.openInNewTab ? "_blank" : undefined)}"
|
||||
>
|
||||
<ak-app-icon size=${PFSize.Small} .app=${app}></ak-app-icon>
|
||||
</a>`;
|
||||
|
||||
const renderAppUrl = (app: Application) =>
|
||||
app.launchUrl === "goauthentik.io://providers/rac/launch"
|
||||
? html`<ak-library-rac-endpoint-launch .app=${app}>
|
||||
<a slot="trigger"> ${app.name} </a>
|
||||
</ak-library-rac-endpoint-launch>`
|
||||
: html`<a
|
||||
href="${ifDefined(app.launchUrl ?? "")}"
|
||||
target="${ifDefined(app.openInNewTab ? "_blank" : undefined)}"
|
||||
>${app.name}</a
|
||||
>`;
|
||||
|
||||
const renderAppDescription = (app: Application) =>
|
||||
app.metaDescription
|
||||
? html` <tr
|
||||
class="pf-c-table__expandable-row ${classMap(expandedClass(app.pk))}"
|
||||
role="row"
|
||||
>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td colspan="3">${app.metaDescription}</td>
|
||||
</tr>`
|
||||
: nothing;
|
||||
|
||||
const renderGroup = ([group, apps]: AppGroupEntry) => html`
|
||||
${group
|
||||
? html`<tr>
|
||||
<td colspan="5"><h2>${group}</h2></td>
|
||||
</tr>`
|
||||
: nothing}
|
||||
${apps.map(
|
||||
(app: Application) =>
|
||||
html`<tr>
|
||||
<td>${renderExpansionCell(app)}</td>
|
||||
<td>${renderAppIcon(app)}</td>
|
||||
<td class="app-row">${renderAppUrl(app)}</td>
|
||||
<td>${app.metaPublisher ?? ""}</td>
|
||||
<td>
|
||||
<a
|
||||
class="pf-c-button pf-m-control pf-m-small pf-m-block"
|
||||
href="/if/admin/#/core/applications/${app?.slug}"
|
||||
>
|
||||
<i class="fas fa-edit"></i> ${msg("Edit")}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
${this.expanded.has(app.pk) ? renderAppDescription(app) : nothing} `,
|
||||
)}
|
||||
`;
|
||||
|
||||
return html`<table class="pf-c-table pf-m-compact pf-m-grid-sm pf-m-expandable">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>${msg("Application")}</th>
|
||||
<th>${msg("Publisher")}</th>
|
||||
${canEdit ? html`<th>${msg("Edit")}</th>` : nothing}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.apps.map(renderGroup)}
|
||||
</tbody>
|
||||
</table> `;
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,30 @@ export const styles = [PFBase, PFDisplay, PFEmptyState, PFPage, PFContent].conca
|
||||
.pf-c-page__main-section {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#library-page-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#library-page-title h1 {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
#library-page-title i {
|
||||
display: inline-block;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
#library-page-title i[checked] {
|
||||
border: 3px solid var(--pf-global--BorderColor--100);
|
||||
}
|
||||
|
||||
#library-page-title a,
|
||||
#library-page-title i {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
`);
|
||||
|
||||
export default styles;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import { tryCatch } from "@goauthentik/elements/utils/tryCatch.js";
|
||||
import "@goauthentik/user/LibraryApplication";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@ -12,6 +13,7 @@ import styles from "./LibraryPageImpl.css";
|
||||
|
||||
import type { Application } from "@goauthentik/api";
|
||||
|
||||
import "./ApplicationCards";
|
||||
import "./ApplicationEmptyState";
|
||||
import "./ApplicationList";
|
||||
import "./ApplicationSearch";
|
||||
@ -20,6 +22,8 @@ import { SEARCH_ITEM_SELECTED, SEARCH_UPDATED } from "./constants";
|
||||
import { isCustomEvent, loading } from "./helpers";
|
||||
import type { AppGroupList, PageUIConfig } from "./types";
|
||||
|
||||
const VIEW_KEY = "ak-library-page-view-preference";
|
||||
|
||||
/**
|
||||
* List of Applications available
|
||||
*
|
||||
@ -53,6 +57,9 @@ export class LibraryPage extends AKElement {
|
||||
@state()
|
||||
filteredApps: Application[] = [];
|
||||
|
||||
@property()
|
||||
viewPreference?: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.searchUpdated = this.searchUpdated.bind(this);
|
||||
@ -66,6 +73,12 @@ export class LibraryPage extends AKElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.filteredApps = this.apps;
|
||||
this.viewPreference =
|
||||
this.viewPreference ??
|
||||
tryCatch(
|
||||
() => window.localStorage.getItem(VIEW_KEY) ?? undefined,
|
||||
(e) => "card",
|
||||
);
|
||||
if (this.filteredApps === undefined) {
|
||||
throw new Error(
|
||||
"Application.results should never be undefined when passed to the Library Page.",
|
||||
@ -110,6 +123,13 @@ export class LibraryPage extends AKElement {
|
||||
return groupBy(this.filteredApps.filter(appHasLaunchUrl), (app) => app.group || "");
|
||||
}
|
||||
|
||||
setView(view: string) {
|
||||
this.viewPreference = view;
|
||||
tryCatch(() => {
|
||||
window.localStorage.setItem(VIEW_KEY, view);
|
||||
});
|
||||
}
|
||||
|
||||
renderEmptyState() {
|
||||
return html`<ak-library-application-empty-list
|
||||
?isadmin=${this.isAdmin}
|
||||
@ -122,12 +142,17 @@ export class LibraryPage extends AKElement {
|
||||
const layout = this.uiConfig.layout as string;
|
||||
const background = this.uiConfig.background;
|
||||
|
||||
return html`<ak-library-application-list
|
||||
layout="${layout}"
|
||||
background="${ifDefined(background)}"
|
||||
selected="${ifDefined(selected)}"
|
||||
.apps=${apps}
|
||||
></ak-library-application-list>`;
|
||||
return this.viewPreference === "list"
|
||||
? html`<ak-library-application-list
|
||||
selected="${ifDefined(selected)}"
|
||||
.apps=${apps}
|
||||
></ak-library-application-list>`
|
||||
: html`<ak-library-application-cards
|
||||
layout="${layout}"
|
||||
background="${ifDefined(background)}"
|
||||
selected="${ifDefined(selected)}"
|
||||
.apps=${apps}
|
||||
></ak-library-application-cards>`;
|
||||
}
|
||||
|
||||
renderSearch() {
|
||||
@ -137,9 +162,15 @@ export class LibraryPage extends AKElement {
|
||||
render() {
|
||||
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
|
||||
<div class="pf-c-content header">
|
||||
<h1 role="heading" aria-level="1" id="library-page-title">
|
||||
${msg("My applications")}
|
||||
</h1>
|
||||
<div id="library-page-title">
|
||||
<h1 role="heading" aria-level="1">${msg("My applications")}</h1>
|
||||
<a id="card-view" @click=${() => this.setView("card")}
|
||||
><i ?checked=${this.viewPreference === "card"} class="fas fa-th-large"></i
|
||||
></a>
|
||||
<a id="list-view" @click=${() => this.setView("list")}
|
||||
><i ?checked=${this.viewPreference === "list"} class="fas fa-list"></i
|
||||
></a>
|
||||
</div>
|
||||
${this.uiConfig.searchEnabled ? this.renderSearch() : html``}
|
||||
</div>
|
||||
<section class="pf-c-page__main-section">
|
||||
|
Reference in New Issue
Block a user