mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
web/a11y: Modals, Command Palette (Merge branch) (#17812)
* Use project relative paths. * Fix tests. * Fix types. * Clean up admin imports. * Move admin import. * Remove or replace references to admin. * Typo fix. * Flesh out ak-modal, about modal. * Flesh out lazy modal. * Fix portal elements not using dialog scope. * Fix url parameters, wizards. * Fix invokers, lazy load. * Fix theming. * Add placeholders, help. * Flesh out command palette. Flesh out styles, command invokers. Continue clean up. Allow slotted content. Flesh out. * Flesh out edit invoker. Prep groups. * Fix odd labeling, legacy situations. * Prepare deprecation of table modal. Clean up serialization. * Tidy types. * Port provider select modal. * Port member select form. * Flesh out role modal. Fix loading state. * Port user group form. * Fix spellcheck. * Fix dialog detection. * Revise types. * Port rac launch modal. * Remove deprecated table modal. * Consistent form action placement. * Consistent casing. * Consistent alignment. * Use more appropriate description. * Flesh out icon. Fix alignment, colors. * Flesh out user search. * Consistent save button. * Clean up labels. * Reduce warning noise. * Clean up label. * Use attribute e2e expects. * Use directive. Fix lifecycle * Fix frequent un-memoized entries. * Fix up closedBy detection. * Tidy alignment. * Fix types, composition. * Fix labels, tests. * Fix up impersonation, labels. * Flesh out. Fix refresh after submit. * Flesh out basic modal test. * Fix ARIA. * Flesh out roles test. * Revise selectors. * Clean up selectors. * Fix impersonation labels, form references. * Fix messages appearing under modals. * Ensure reason is parsed. * Flesh out impersonation test. * Flesh out impersonate test. * Flesh out application tests. Clean up toolbar header, ARIA. * Flesh out wizard test. * Refine weight, order. * Fix up initial values, selectors. * Fix tests. * Fix selector.
This commit is contained in:
@@ -27,6 +27,7 @@ export class FormFixture extends PageFixture {
|
||||
.filter({
|
||||
hasNot: context.getByRole("presentation"),
|
||||
})
|
||||
.and(context.locator(":not(button)"))
|
||||
.or(
|
||||
context.getByRole("textbox", {
|
||||
name: fieldName,
|
||||
@@ -191,17 +192,17 @@ export class FormFixture extends PageFixture {
|
||||
// Find the search select input control and activate it.
|
||||
await control.click();
|
||||
|
||||
if (typeof pattern === "string") {
|
||||
this.fill(control, pattern, parent);
|
||||
}
|
||||
|
||||
const button = this.page
|
||||
// ---
|
||||
.locator(`div[data-managed-for*="${fieldName}"] button`, {
|
||||
hasText: pattern,
|
||||
});
|
||||
|
||||
if (!button) {
|
||||
throw new Error(
|
||||
`Unable to find an ak-search-select entry matching ${fieldLabel}:${pattern.toString()}`,
|
||||
);
|
||||
}
|
||||
await expect(button, `Search select entry (${pattern}) should be visible`).toBeVisible();
|
||||
|
||||
await button.click();
|
||||
await this.page.keyboard.press("Tab");
|
||||
|
||||
@@ -26,7 +26,9 @@ export class PointerFixture extends PageFixture {
|
||||
context: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
if (typeof optionsOrRole === "string") {
|
||||
return context.getByRole(optionsOrRole, { name }).first().click();
|
||||
const target = context.getByRole(optionsOrRole, { name }).first();
|
||||
|
||||
return target.click();
|
||||
}
|
||||
|
||||
const options = {
|
||||
|
||||
@@ -3,137 +3,175 @@ import "#elements/EmptyState";
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { globalAK } from "#common/global";
|
||||
|
||||
import { ModalButton } from "#elements/buttons/ModalButton";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import { AKModal } from "#elements/modals/ak-modal";
|
||||
import { asInvoker } from "#elements/modals/utils";
|
||||
import { ThemedImage } from "#elements/utils/images";
|
||||
|
||||
import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
import { ref } from "lit-html/directives/ref.js";
|
||||
import { styleMap } from "lit-html/directives/style-map.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css";
|
||||
|
||||
@customElement("ak-about-modal")
|
||||
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
|
||||
static styles = [
|
||||
...ModalButton.styles,
|
||||
PFAbout,
|
||||
css`
|
||||
.pf-c-about-modal-box {
|
||||
--pf-c-about-modal-box--BackgroundColor: var(--pf-global--palette--black-900);
|
||||
}
|
||||
const DEFAULT_BRAND_IMAGE = "/static/dist/assets/images/flow_background.jpg";
|
||||
|
||||
.pf-c-about-modal-box__hero {
|
||||
background-image: url("/static/dist/assets/images/flow_background.jpg");
|
||||
}
|
||||
.pf-c-about-modal-box__brand {
|
||||
--pf-c-about-modal-box__brand-image--Height: 6.25rem;
|
||||
}
|
||||
.pf-c-about-modal-box__brand i {
|
||||
font-size: var(--pf-c-about-modal-box__brand-image--Height);
|
||||
type AboutEntry = [label: string, content: string | TemplateResult];
|
||||
|
||||
async function fetchAboutDetails(): Promise<AboutEntry[]> {
|
||||
const api = new AdminApi(DEFAULT_CONFIG);
|
||||
|
||||
const [status, version] = await Promise.all([
|
||||
api.adminSystemRetrieve(),
|
||||
api.adminVersionRetrieve(),
|
||||
]);
|
||||
|
||||
let build: string | TemplateResult = msg("Release");
|
||||
|
||||
if (globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
||||
build = msg("Development");
|
||||
} else if (version.buildHash) {
|
||||
build = html`<a
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/goauthentik/authentik/commit/${version.buildHash}"
|
||||
target="_blank"
|
||||
>${version.buildHash}</a
|
||||
>`;
|
||||
}
|
||||
|
||||
return [
|
||||
[msg("Version"), version.versionCurrent],
|
||||
[msg("UI Version"), import.meta.env.AK_VERSION],
|
||||
[msg("Build"), build],
|
||||
[msg("Python version"), status.runtime.pythonVersion],
|
||||
[msg("Platform"), status.runtime.platform],
|
||||
[msg("Kernel"), status.runtime.uname],
|
||||
[
|
||||
msg("OpenSSL"),
|
||||
`${status.runtime.opensslVersion} ${status.runtime.opensslFipsEnabled ? "FIPS" : ""}`,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@customElement("ak-about-modal")
|
||||
export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
|
||||
public static override formatARIALabel = () => msg("About authentik");
|
||||
|
||||
public static hostStyles = [
|
||||
css`
|
||||
dialog.ak-c-modal:has(ak-about-modal) {
|
||||
--ak-c-modal--BackgroundColor: var(--pf-global--palette--black-900);
|
||||
--ak-c-modal--BorderColor: var(--pf-global--palette--black-600);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async getAboutEntries(): Promise<[string, string | TemplateResult][]> {
|
||||
const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
|
||||
const version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
|
||||
let build: string | TemplateResult = msg("Release");
|
||||
if (globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
||||
build = msg("Development");
|
||||
} else if (version.buildHash !== "") {
|
||||
build = html`<a
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/goauthentik/authentik/commit/${version.buildHash}"
|
||||
target="_blank"
|
||||
>${version.buildHash}</a
|
||||
>`;
|
||||
}
|
||||
return [
|
||||
[msg("Version"), version.versionCurrent],
|
||||
[msg("UI Version"), import.meta.env.AK_VERSION],
|
||||
[msg("Build"), build],
|
||||
[msg("Python version"), status.runtime.pythonVersion],
|
||||
[msg("Platform"), status.runtime.platform],
|
||||
[msg("Kernel"), status.runtime.uname],
|
||||
[
|
||||
msg("OpenSSL"),
|
||||
`${status.runtime.opensslVersion} ${status.runtime.opensslFipsEnabled ? "FIPS" : ""}`,
|
||||
],
|
||||
];
|
||||
public static styles = [
|
||||
...AKModal.styles,
|
||||
PFAbout,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pf-c-about-modal-box {
|
||||
--pf-c-about-modal-box--BackgroundColor: var(--ak-c-modal--BackgroundColor);
|
||||
width: unset;
|
||||
height: 100%;
|
||||
max-height: unset;
|
||||
max-width: unset;
|
||||
z-index: unset;
|
||||
position: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public static ariaLabel = msg("About authentik");
|
||||
|
||||
public static open = asInvoker(AboutModal);
|
||||
|
||||
@state()
|
||||
protected entries: AboutEntry[] | null = null;
|
||||
|
||||
public refresh() {
|
||||
return fetchAboutDetails().then((entries) => {
|
||||
this.entries = entries;
|
||||
});
|
||||
}
|
||||
|
||||
#contentRef = createRef<HTMLDivElement>();
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
#backdropListener = (event: PointerEvent) => {
|
||||
// We only want to close the modal when the backdrop is clicked, not when it's children are clicked.
|
||||
//#region Renderers
|
||||
|
||||
if (this.#contentRef.value?.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
protected override renderCloseButton() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override renderModal() {
|
||||
protected override render() {
|
||||
let product = this.brandingTitle;
|
||||
|
||||
if (this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed) {
|
||||
product += ` ${msg("Enterprise")}`;
|
||||
}
|
||||
return html`<div class="pf-c-backdrop" @click=${this.#backdropListener}>
|
||||
<div class="pf-l-bullseye">
|
||||
<div
|
||||
${ref(this.#contentRef)}
|
||||
class="pf-c-about-modal-box"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
|
||||
return html`<div
|
||||
${ref(this.scrollContainerRef)}
|
||||
class="pf-c-about-modal-box"
|
||||
style=${styleMap({
|
||||
"--pf-c-about-modal-box__hero--sm--BackgroundImage": `url(${DEFAULT_BRAND_IMAGE})`,
|
||||
})}
|
||||
>
|
||||
<div class="pf-c-about-modal-box__close">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
@click=${this.closeListener}
|
||||
aria-label=${msg("Close dialog")}
|
||||
>
|
||||
<div class="pf-c-about-modal-box__brand">
|
||||
${ThemedImage({
|
||||
src: this.brandingFavicon,
|
||||
alt: msg("authentik Logo"),
|
||||
className: "pf-c-about-modal-box__brand-image",
|
||||
theme: this.activeTheme,
|
||||
themedUrls: this.brandingFaviconThemedUrls,
|
||||
})}
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__close">
|
||||
<button class="pf-c-button pf-m-plain" type="button" @click=${this.close}>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-4xl" id="modal-title">${product}</h1>
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__hero"></div>
|
||||
<div class="pf-c-about-modal-box__content">
|
||||
<div class="pf-c-about-modal-box__body">
|
||||
<div class="pf-c-content">
|
||||
${until(
|
||||
this.getAboutEntries().then((entries) => {
|
||||
return html`<dl>
|
||||
${entries.map(([label, value]) => {
|
||||
return html`<dt>${label}</dt>
|
||||
<dd>${value}</dd>`;
|
||||
})}
|
||||
</dl>`;
|
||||
}),
|
||||
html`<ak-empty-state loading></ak-empty-state>`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p class="pf-c-about-modal-box__strapline"></p>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__brand">
|
||||
${ThemedImage({
|
||||
src: this.brandingFavicon,
|
||||
alt: msg("authentik Logo"),
|
||||
className: "pf-c-about-modal-box__brand-image",
|
||||
theme: this.activeTheme,
|
||||
themedUrls: this.brandingFaviconThemedUrls,
|
||||
})}
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-4xl" id="modal-title">${product}</h1>
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__hero"></div>
|
||||
<div class="pf-c-about-modal-box__content">
|
||||
<div class="pf-c-about-modal-box__body">
|
||||
<div class="pf-c-content">
|
||||
${this.entries
|
||||
? html`<dl>
|
||||
${this.entries.map(([label, value]) => {
|
||||
return html`<dt>${label}</dt>
|
||||
<dd>${value}</dd>`;
|
||||
})}
|
||||
</dl>`
|
||||
: html`<ak-empty-state loading></ak-empty-state>`}
|
||||
</div>
|
||||
</div>
|
||||
<p class="pf-c-about-modal-box__strapline"></p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -34,3 +34,9 @@ ak-sidebar-item:active ak-sidebar-item::part(list-item) {
|
||||
.pf-c-drawer__panel {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.command-palette-trigger {
|
||||
cursor: pointer;
|
||||
|
||||
padding-inline-end: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import "#admin/AdminInterface/AboutModal";
|
||||
import "#elements/banner/EnterpriseStatusBanner";
|
||||
import "#elements/banner/VersionBanner";
|
||||
import "#elements/messages/MessageContainer";
|
||||
import "#elements/router/RouterOutlet";
|
||||
import "#elements/sidebar/Sidebar";
|
||||
import "#elements/sidebar/SidebarItem";
|
||||
import "#elements/commands/ak-command-palette-user-modal";
|
||||
|
||||
import {
|
||||
createAdminSidebarEnterpriseEntries,
|
||||
createAdminSidebarEntries,
|
||||
renderSidebarItems,
|
||||
SidebarEntry,
|
||||
} from "./AdminSidebar.js";
|
||||
|
||||
import { isAPIResultReady } from "#common/api/responses";
|
||||
@@ -18,10 +19,16 @@ import { isGuest } from "#common/users";
|
||||
import { WebsocketClient } from "#common/ws/WebSocketClient";
|
||||
|
||||
import { AuthenticatedInterface } from "#elements/AuthenticatedInterface";
|
||||
import {
|
||||
CommandPrefix,
|
||||
PaletteCommandDefinitionInit,
|
||||
PaletteCommandNamespace,
|
||||
} from "#elements/commands/shared";
|
||||
import { listen } from "#elements/decorators/listen";
|
||||
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { WithNotifications } from "#elements/mixins/notifications";
|
||||
import { canAccessAdmin, WithSession } from "#elements/mixins/session";
|
||||
import { renderDialog } from "#elements/modals/utils";
|
||||
import { AKDrawerChangeEvent } from "#elements/notifications/events";
|
||||
import {
|
||||
DrawerState,
|
||||
@@ -29,18 +36,19 @@ import {
|
||||
readDrawerParams,
|
||||
renderNotificationDrawerPanel,
|
||||
} from "#elements/notifications/utils";
|
||||
import { navigate } from "#elements/router/RouterOutlet";
|
||||
|
||||
import type { AboutModal } from "#admin/AdminInterface/AboutModal";
|
||||
import Styles from "#admin/AdminInterface/index.entrypoint.css";
|
||||
import { ROUTES } from "#admin/Routes";
|
||||
|
||||
import { CapabilitiesEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { LOCALE_STATUS_EVENT, LocaleStatusEventDetail, msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, eventOptions, property, query, state } from "lit/decorators.js";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
||||
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
|
||||
@@ -56,18 +64,25 @@ export class AdminInterface extends WithCapabilitiesConfig(
|
||||
) {
|
||||
//#region Styles
|
||||
|
||||
public static readonly styles: CSSResult[] = [PFPage, PFButton, PFDrawer, PFNav, Styles];
|
||||
public static readonly styles: CSSResult[] = [
|
||||
PFPage,
|
||||
PFButton,
|
||||
PFDrawer,
|
||||
PFNav,
|
||||
PFBanner,
|
||||
Styles,
|
||||
];
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Properties
|
||||
|
||||
@query("ak-about-modal")
|
||||
public aboutModal?: AboutModal;
|
||||
//#region Public Properties
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "sidebar" })
|
||||
public sidebarOpen = false;
|
||||
|
||||
@property({ type: Array })
|
||||
public entries: readonly SidebarEntry[] = createAdminSidebarEntries();
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Public Methods
|
||||
@@ -76,9 +91,14 @@ export class AdminInterface extends WithCapabilitiesConfig(
|
||||
this.sidebarOpen = !this.sidebarOpen;
|
||||
};
|
||||
|
||||
public synchronizeSidebarEntries = () => {
|
||||
this.logger.debug("Synchronizing sidebar entries with current locale");
|
||||
this.entries = createAdminSidebarEntries();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
//#region Event Listeners
|
||||
|
||||
#sidebarMatcher: MediaQueryList;
|
||||
#sidebarMediaQueryListener = (event: MediaQueryListEvent) => {
|
||||
@@ -99,6 +119,17 @@ export class AdminInterface extends WithCapabilitiesConfig(
|
||||
persistDrawerParams(event.drawer);
|
||||
};
|
||||
|
||||
@listen(LOCALE_STATUS_EVENT)
|
||||
localeStatusListener = (event: CustomEvent<LocaleStatusEventDetail>) => {
|
||||
if (event.detail.status === "ready") {
|
||||
this.synchronizeSidebarEntries();
|
||||
}
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
constructor() {
|
||||
configureSentry();
|
||||
|
||||
@@ -110,6 +141,66 @@ export class AdminInterface extends WithCapabilitiesConfig(
|
||||
this.sidebarOpen = this.#sidebarMatcher.matches;
|
||||
}
|
||||
|
||||
#refreshCommandsFrameID = -1;
|
||||
|
||||
#refreshCommands = () => {
|
||||
const commands: PaletteCommandDefinitionInit[] = [
|
||||
{
|
||||
label: msg("Create a new application..."),
|
||||
action: () => navigate("/core/applications", { createWizard: true }),
|
||||
group: msg("Applications"),
|
||||
},
|
||||
{
|
||||
namespace: PaletteCommandNamespace.Navigation,
|
||||
label: msg("Check the logs"),
|
||||
action: () => navigate("/events/log"),
|
||||
group: msg("Events"),
|
||||
},
|
||||
{
|
||||
namespace: PaletteCommandNamespace.Navigation,
|
||||
label: msg("Manage users"),
|
||||
action: () => navigate("/identity/users"),
|
||||
group: msg("Users"),
|
||||
},
|
||||
...this.entries.flatMap(([, label, , children]) => [
|
||||
...(children ?? []).map(
|
||||
([path, childLabel]): PaletteCommandDefinitionInit => ({
|
||||
namespace: PaletteCommandNamespace.Navigation,
|
||||
label: childLabel,
|
||||
group: label,
|
||||
action: () => {
|
||||
navigate(path!);
|
||||
},
|
||||
}),
|
||||
),
|
||||
]),
|
||||
{
|
||||
namespace: PaletteCommandNamespace.Search,
|
||||
label: msg("Username or email address..."),
|
||||
prefix: CommandPrefix.SearchFor(),
|
||||
group: msg("Users"),
|
||||
keywords: [msg("search"), msg("find")],
|
||||
action: async (data, event) => {
|
||||
event?.stopPropagation();
|
||||
|
||||
const userPalette = this.ownerDocument.createElement(
|
||||
"ak-command-palette-user-modal",
|
||||
);
|
||||
|
||||
renderDialog(userPalette, {
|
||||
parentElement: this,
|
||||
});
|
||||
|
||||
userPalette.show();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.commandPalette.modal.setCommands(
|
||||
commands.map((command) => ({ namespace: PaletteCommandNamespace.Action, ...command })),
|
||||
);
|
||||
};
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -121,11 +212,19 @@ export class AdminInterface extends WithCapabilitiesConfig(
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
cancelAnimationFrame(this.#refreshCommandsFrameID);
|
||||
|
||||
this.#sidebarMatcher.removeEventListener("change", this.#sidebarMediaQueryListener);
|
||||
|
||||
WebsocketClient.close();
|
||||
}
|
||||
|
||||
public firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
|
||||
this.#refreshCommandsFrameID = requestAnimationFrame(this.#refreshCommands);
|
||||
}
|
||||
|
||||
public override updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
@@ -158,62 +257,96 @@ export class AdminInterface extends WithCapabilitiesConfig(
|
||||
};
|
||||
|
||||
return html`<div class="pf-c-page">
|
||||
<ak-page-navbar>
|
||||
<button
|
||||
slot="toggle"
|
||||
aria-controls="global-nav"
|
||||
class="pf-c-button pf-m-plain"
|
||||
@click=${this.toggleSidebar}
|
||||
aria-label=${this.sidebarOpen
|
||||
? msg("Collapse navigation")
|
||||
: msg("Expand navigation")}
|
||||
aria-expanded=${this.sidebarOpen ? "true" : "false"}
|
||||
>
|
||||
<i aria-hidden="true" class="fas fa-bars"></i>
|
||||
</button>
|
||||
<ak-page-navbar>
|
||||
<button
|
||||
slot="toggle"
|
||||
aria-controls="global-nav"
|
||||
class="pf-c-button pf-m-plain"
|
||||
@click=${this.toggleSidebar}
|
||||
aria-label=${this.sidebarOpen
|
||||
? msg("Collapse navigation")
|
||||
: msg("Expand navigation")}
|
||||
aria-expanded=${this.sidebarOpen ? "true" : "false"}
|
||||
>
|
||||
<i aria-hidden="true" class="fas fa-bars"></i>
|
||||
</button>
|
||||
|
||||
<ak-version-banner></ak-version-banner>
|
||||
<ak-enterprise-status interface="admin"></ak-enterprise-status>
|
||||
</ak-page-navbar>
|
||||
|
||||
<ak-sidebar ?hidden=${!this.sidebarOpen} class="${classMap(sidebarClasses)}"
|
||||
>${renderSidebarItems(createAdminSidebarEntries())}
|
||||
${this.can(CapabilitiesEnum.IsEnterprise)
|
||||
? renderSidebarItems(createAdminSidebarEnterpriseEntries())
|
||||
: nothing}
|
||||
</ak-sidebar>
|
||||
|
||||
<div class="pf-c-page__drawer">
|
||||
<div class="pf-c-drawer ${classMap(drawerClasses)}">
|
||||
<div class="pf-c-drawer__main">
|
||||
<div class="pf-c-drawer__content">
|
||||
<div class="pf-c-drawer__body">
|
||||
<ak-router-outlet
|
||||
role="presentation"
|
||||
class="pf-c-page__main"
|
||||
tabindex="-1"
|
||||
id="main-content"
|
||||
default-url="/administration/overview"
|
||||
.routes=${ROUTES}
|
||||
@ak-route-change=${this.routeChangeListener}
|
||||
>
|
||||
</ak-router-outlet>
|
||||
<button
|
||||
slot="nav-buttons"
|
||||
@click=${this.commandPalette.showListener}
|
||||
class="pf-c-button pf-m-plain command-palette-trigger"
|
||||
aria-label=${msg("Open Command Palette", {
|
||||
id: "command-palette-trigger-label",
|
||||
desc: "Label for the button that opens the command palette",
|
||||
})}
|
||||
>
|
||||
<pf-tooltip position="top-end">
|
||||
<div slot="content" class="ak-tooltip__content--inline">
|
||||
${msg("Open Command Palette", {
|
||||
id: "command-palette-trigger-tooltip",
|
||||
desc: "Tooltip for the button that opens the command palette",
|
||||
})}
|
||||
<div class="ak-c-kbd"><kbd>Ctrl</kbd> + <kbd>K</kbd></div>
|
||||
</div>
|
||||
</div>
|
||||
${renderNotificationDrawerPanel(this.drawer)}
|
||||
<ak-about-modal></ak-about-modal>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pf-c-page__sidebar-backdrop"
|
||||
aria-label=${this.sidebarOpen ? msg("Close sidebar") : msg("Open sidebar")}
|
||||
@click=${this.toggleSidebar}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
></div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
class="ak-c-vector-icon"
|
||||
role="img"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<path
|
||||
d="M26 4.01H6a2 2 0 0 0-2 2v20a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-20a2 2 0 0 0-2-2m0 2v4H6v-4Zm-20 20v-14h20v14Z"
|
||||
/>
|
||||
<path
|
||||
d="m10.76 16.18 2.82 2.83-2.82 2.83 1.41 1.41 4.24-4.24-4.24-4.24z"
|
||||
/>
|
||||
</svg>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
<ak-version-banner></ak-version-banner>
|
||||
<ak-enterprise-status interface="admin"></ak-enterprise-status>
|
||||
</ak-page-navbar>
|
||||
|
||||
<ak-sidebar ?hidden=${!this.sidebarOpen} class="${classMap(sidebarClasses)}"
|
||||
>${renderSidebarItems(this.entries)}
|
||||
${this.can(CapabilitiesEnum.IsEnterprise)
|
||||
? renderSidebarItems(createAdminSidebarEnterpriseEntries())
|
||||
: nothing}
|
||||
</ak-sidebar>
|
||||
|
||||
<div class="pf-c-page__drawer">
|
||||
<div class="pf-c-drawer ${classMap(drawerClasses)}">
|
||||
<div class="pf-c-drawer__main">
|
||||
<div class="pf-c-drawer__content">
|
||||
<div class="pf-c-drawer__body">
|
||||
<ak-router-outlet
|
||||
role="presentation"
|
||||
class="pf-c-page__main"
|
||||
tabindex="-1"
|
||||
id="main-content"
|
||||
default-url="/administration/overview"
|
||||
.routes=${ROUTES}
|
||||
@ak-route-change=${this.routeChangeListener}
|
||||
>
|
||||
</ak-router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
${renderNotificationDrawerPanel(this.drawer)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pf-c-page__sidebar-backdrop"
|
||||
aria-label=${this.sidebarOpen ? msg("Close sidebar") : msg("Open sidebar")}
|
||||
@click=${this.toggleSidebar}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
${this.commandPalette}`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import "#admin/admin-overview/AdminOverviewPage";
|
||||
|
||||
import { globalAK } from "#common/global";
|
||||
|
||||
import { ID_REGEX, Route, SLUG_REGEX, UUID_REGEX } from "#elements/router/Route";
|
||||
|
||||
import { html } from "lit";
|
||||
@@ -198,14 +196,3 @@ export const ROUTES: Route[] = [
|
||||
return html`<ak-enterprise-license-list></ak-enterprise-license-list>`;
|
||||
}),
|
||||
];
|
||||
|
||||
/**
|
||||
* Application route helpers.
|
||||
*
|
||||
* @TODO: This API isn't quite right yet. Revisit after the hash router is replaced.
|
||||
*/
|
||||
export const ApplicationRoute = {
|
||||
EditURL(slug: string, base = globalAK().api.base) {
|
||||
return `${base}if/admin/#/core/applications/${slug}`;
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -4,6 +4,7 @@ import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/events/LogViewer";
|
||||
import "#elements/tasks/ScheduleList";
|
||||
import "#elements/tasks/TaskList";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AkControlElement } from "#elements/AkControlElement";
|
||||
import { AKControlElement } from "#elements/ControlElement";
|
||||
import { type Spread } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
@@ -22,7 +22,7 @@ const hasLegalScheme = (url: string) =>
|
||||
LEGAL_SCHEMES.some((scheme) => url.substr(0, scheme.length).toLowerCase() === scheme);
|
||||
|
||||
@customElement("ak-admin-settings-footer-link")
|
||||
export class FooterLinkInput extends AkControlElement<FooterLink> {
|
||||
export class FooterLinkInput extends AKControlElement<FooterLink> {
|
||||
static styles = [
|
||||
PFInputGroup,
|
||||
PFFormControl,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "#admin/admin-settings/AdminSettingsForm";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/EmptyState";
|
||||
import "#elements/Tabs";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import "#admin/admin-settings/AdminSettingsFooterLinks";
|
||||
import "#elements/messages/MessageContainer";
|
||||
import "../ak-array-input.js";
|
||||
import "#elements/ak-array-input";
|
||||
|
||||
import { IArrayInput } from "../ak-array-input.js";
|
||||
import { IArrayInput } from "#elements/ak-array-input";
|
||||
|
||||
import { FooterLinkInput } from "#admin/admin-settings/AdminSettingsFooterLinks";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "#admin/applications/ProviderSelectModal";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
import "#components/ak-file-search-input";
|
||||
import "#components/ak-radio-input";
|
||||
import "#components/ak-slug-input";
|
||||
@@ -11,9 +11,9 @@ import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/forms/Radio";
|
||||
import "#elements/forms/SearchSelect/ak-search-select";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
import "./components/ak-backchannel-input.js";
|
||||
import "./components/ak-provider-search-input.js";
|
||||
import "#admin/applications/ProviderSelectForm";
|
||||
import "#admin/applications/components/ak-backchannel-input";
|
||||
import "#admin/applications/components/ak-provider-search-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
@@ -40,6 +40,9 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Application, string>) {
|
||||
#api = new CoreApi(DEFAULT_CONFIG);
|
||||
|
||||
public override entitySingular = msg("Application");
|
||||
public override entityPlural = msg("Applications");
|
||||
|
||||
protected override async loadInstance(pk: string): Promise<Application> {
|
||||
const app = await this.#api.coreApplicationsRetrieve({
|
||||
slug: pk,
|
||||
@@ -104,6 +107,8 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
};
|
||||
};
|
||||
|
||||
//#region Rendering
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
const alertMsg = msg(
|
||||
"Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.",
|
||||
@@ -130,6 +135,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
label=${msg("Slug")}
|
||||
required
|
||||
help=${msg("Internal application name used in URLs.")}
|
||||
placeholder=${msg("e.g. my-application")}
|
||||
input-hint="code"
|
||||
></ak-slug-input>
|
||||
<ak-text-input
|
||||
@@ -207,16 +213,24 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
label=${msg("Publisher")}
|
||||
name="metaPublisher"
|
||||
value="${ifDefined(this.instance?.metaPublisher)}"
|
||||
placeholder=${msg("Type an optional publisher name...")}
|
||||
help=${msg("The publisher is shown in the application library.")}
|
||||
></ak-text-input>
|
||||
<ak-textarea-input
|
||||
label=${msg("Description")}
|
||||
name="metaDescription"
|
||||
placeholder=${msg("Type an optional description...")}
|
||||
value=${ifDefined(this.instance?.metaDescription)}
|
||||
help=${msg(
|
||||
"The description is shown in the application library and may provide additional information about the application to end users.",
|
||||
)}
|
||||
></ak-textarea-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
15
web/src/admin/applications/ApplicationListPage.css
Normal file
15
web/src/admin/applications/ApplicationListPage.css
Normal file
@@ -0,0 +1,15 @@
|
||||
/* Fix alignment issues with images in tables */
|
||||
.pf-c-table tbody > tr > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr td:first-child {
|
||||
width: auto;
|
||||
min-width: 0px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pf-c-sidebar.pf-m-gutter > .pf-c-sidebar__main > * + * {
|
||||
margin-left: calc(var(--pf-c-sidebar__main--child--MarginLeft) / 2);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
import "#elements/forms/ConfirmationForm";
|
||||
import "#admin/applications/ApplicationForm";
|
||||
import "#elements/AppIcon";
|
||||
import "#elements/ak-mdx/ak-mdx";
|
||||
import "#elements/buttons/SpinnerButton/ak-spinner-button";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
import "./ApplicationWizardHint.js";
|
||||
import "#elements/modals/ak-modal";
|
||||
import "#admin/applications/ApplicationForm";
|
||||
import "#admin/applications/ApplicationWizardHint";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
@@ -17,34 +18,31 @@ import { TablePage } from "#elements/table/TablePage";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
import { ApplicationForm } from "#admin/applications/ApplicationForm";
|
||||
import Styles from "#admin/applications/ApplicationListPage.css";
|
||||
import { AkApplicationWizard } from "#admin/applications/wizard/ak-application-wizard";
|
||||
|
||||
import { Application, CoreApi, PoliciesApi } from "@goauthentik/api";
|
||||
|
||||
import MDApplication from "~docs/add-secure-apps/applications/index.md";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { css, CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
|
||||
export const applicationListStyle = css`
|
||||
/* Fix alignment issues with images in tables */
|
||||
.pf-c-table tbody > tr > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
tr td:first-child {
|
||||
width: auto;
|
||||
min-width: 0px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.pf-c-sidebar.pf-m-gutter > .pf-c-sidebar__main > * + * {
|
||||
margin-left: calc(var(--pf-c-sidebar__main--child--MarginLeft) / 2);
|
||||
}
|
||||
`;
|
||||
export const applicationListStyle = css``;
|
||||
|
||||
@customElement("ak-application-list")
|
||||
export class ApplicationListPage extends WithBrandConfig(TablePage<Application>) {
|
||||
public static styles: CSSResult[] = [
|
||||
// ---
|
||||
...TablePage.styles,
|
||||
PFCard,
|
||||
Styles,
|
||||
];
|
||||
|
||||
protected override searchEnabled = true;
|
||||
public pageTitle = msg("Applications");
|
||||
public get pageDescription() {
|
||||
@@ -54,11 +52,11 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
}
|
||||
public pageIcon = "pf-icon pf-icon-applications";
|
||||
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
public override checkbox = true;
|
||||
public override clearOnRefresh = true;
|
||||
|
||||
@property()
|
||||
order = "name";
|
||||
public order = "name";
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<Application>> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({
|
||||
@@ -67,7 +65,15 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
});
|
||||
}
|
||||
|
||||
static styles: CSSResult[] = [...TablePage.styles, PFCard, applicationListStyle];
|
||||
public override firstUpdated(changed: PropertyValues<this>): void {
|
||||
super.firstUpdated(changed);
|
||||
|
||||
if (getURLParam("createWizard", false)) {
|
||||
AkApplicationWizard.showModal();
|
||||
} else if (getURLParam("createForm", false)) {
|
||||
ApplicationForm.showModal();
|
||||
}
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
["", undefined, msg("Application Icon")],
|
||||
@@ -91,7 +97,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
</aside>`;
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
protected override renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
object-label=${msg("Application(s)")}
|
||||
@@ -113,7 +119,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: Application): SlottedTemplateResult[] {
|
||||
protected row(item: Application): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<ak-app-icon
|
||||
aria-label=${msg(str`Application icon for "${item.name}"`)}
|
||||
@@ -133,21 +139,15 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
: html`-`,
|
||||
html`${item.providerObj?.verboseName || msg("-")}`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="header">${msg("Update Application")}</span>
|
||||
<ak-application-form slot="form" .instancePk=${item.slug}>
|
||||
</ak-application-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-plain"
|
||||
aria-label=${msg(str`Edit "${item.name}"`)}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
aria-label=${msg(str`Edit "${item.name}"`)}
|
||||
${ApplicationForm.asEditModalInvoker(item.slug)}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
${item.launchUrl
|
||||
? html`<a
|
||||
href=${item.launchUrl}
|
||||
@@ -164,30 +164,62 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
];
|
||||
}
|
||||
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html` <ak-application-wizard .open=${getURLParam("createWizard", false)}>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-primary"
|
||||
data-ouia-component-id="start-application-wizard"
|
||||
>
|
||||
${msg("Create with Provider")}
|
||||
</button>
|
||||
</ak-application-wizard>
|
||||
<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
||||
<span slot="submit">${msg("Create")}</span>
|
||||
<span slot="header">${msg("Create Application")}</span>
|
||||
<ak-application-form slot="form"> </ak-application-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||
</ak-forms-modal>`;
|
||||
protected override renderObjectCreate(): TemplateResult {
|
||||
return html`<ak-dropdown class="pf-c-dropdown">
|
||||
<button
|
||||
class="pf-c-button pf-m-primary pf-c-dropdown__toggle"
|
||||
type="button"
|
||||
id="new-application-toggle"
|
||||
aria-haspopup="menu"
|
||||
aria-controls="new-application-menu"
|
||||
tabindex="0"
|
||||
>
|
||||
<span class="pf-c-dropdown__toggle-text">${msg("New Application")}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<menu
|
||||
class="pf-c-dropdown__menu"
|
||||
hidden
|
||||
id="new-application-menu"
|
||||
aria-labelledby="new-application-toggle"
|
||||
tabindex="-1"
|
||||
>
|
||||
<li role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="pf-c-dropdown__menu-item"
|
||||
${AkApplicationWizard.asModalInvoker()}
|
||||
aria-description=${msg(
|
||||
"Opens the new application wizard, which will guide you through creating a new application with an existing provider.",
|
||||
)}
|
||||
>
|
||||
${msg("With New Provider...")}
|
||||
</button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="pf-c-dropdown__menu-item"
|
||||
${ApplicationForm.asModalInvoker()}
|
||||
aria-description=${msg(
|
||||
"Opens the new application form, which will guide you through creating a new application with an existing provider.",
|
||||
)}
|
||||
>
|
||||
${msg("With Existing Provider...")}
|
||||
</button>
|
||||
</li>
|
||||
</menu>
|
||||
</ak-dropdown>`;
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html` ${super.renderToolbar()}
|
||||
protected override renderToolbar(): TemplateResult {
|
||||
return html`${super.renderToolbar()}
|
||||
<ak-forms-confirm
|
||||
successMessage=${msg("Successfully cleared application cache")}
|
||||
errorMessage=${msg("Failed to delete application cache")}
|
||||
action=${msg("Clear cache")}
|
||||
action=${msg("Clear Cache")}
|
||||
.onConfirm=${() => {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesAllCacheClearCreate();
|
||||
}}
|
||||
|
||||
@@ -5,7 +5,7 @@ import "#admin/applications/entitlements/ApplicationEntitlementPage";
|
||||
import "#admin/policies/BoundPoliciesList";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#admin/lifecycle/ObjectLifecyclePage";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/AppIcon";
|
||||
import "#elements/EmptyState";
|
||||
import "#elements/Tabs";
|
||||
@@ -238,7 +238,9 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) {
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit"
|
||||
>${msg("Save Changes")}</span
|
||||
>
|
||||
<span slot="header">
|
||||
${msg("Update Application")}
|
||||
</span>
|
||||
|
||||
57
web/src/admin/applications/ProviderSelectForm.ts
Normal file
57
web/src/admin/applications/ProviderSelectForm.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { Provider, ProvidersApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-provider-select-form")
|
||||
export class ProviderSelectForm extends Table<Provider> {
|
||||
public override checkbox = true;
|
||||
public override checkboxChip = true;
|
||||
|
||||
protected override searchEnabled = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public backchannel = false;
|
||||
|
||||
public override order = "name";
|
||||
|
||||
protected async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersAllList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
backchannel: this.backchannel,
|
||||
});
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
// ---
|
||||
[msg("Name"), "username"],
|
||||
[msg("Type")],
|
||||
];
|
||||
|
||||
protected row(item: Provider): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<div>
|
||||
<div>${item.name}</div>
|
||||
</div>`,
|
||||
html`${item.verboseName}`,
|
||||
];
|
||||
}
|
||||
|
||||
protected renderSelectedChip(item: Provider): SlottedTemplateResult {
|
||||
return item.name;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-provider-select-form": ProviderSelectForm;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
|
||||
import { TableModal } from "#elements/table/TableModal";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { Provider, ProvidersApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-provider-select-table")
|
||||
export class ProviderSelectModal extends TableModal<Provider> {
|
||||
public override checkbox = true;
|
||||
public override checkboxChip = true;
|
||||
|
||||
protected override searchEnabled = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public backchannel = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public confirm!: (selectedItems: Provider[]) => Promise<unknown>;
|
||||
|
||||
public override order = "name";
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersAllList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
backchannel: this.backchannel,
|
||||
});
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
// ---
|
||||
[msg("Name"), "username"],
|
||||
[msg("Type")],
|
||||
];
|
||||
|
||||
row(item: Provider): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<div>
|
||||
<div>${item.name}</div>
|
||||
</div>`,
|
||||
html`${item.verboseName}`,
|
||||
];
|
||||
}
|
||||
|
||||
renderSelectedChip(item: Provider): TemplateResult {
|
||||
return html`${item.name}`;
|
||||
}
|
||||
|
||||
renderModalInner(): TemplateResult {
|
||||
return html`<section class="pf-c-modal-box__header pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">
|
||||
${msg("Select providers to add to application")}
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-modal-box__body pf-m-light">${this.renderTable()}</section>
|
||||
<footer class="pf-c-modal-box__footer">
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
await this.confirm(this.selectedElements);
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-primary"
|
||||
>
|
||||
${msg("Add")} </ak-spinner-button
|
||||
>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary"
|
||||
>
|
||||
${msg("Cancel")}
|
||||
</ak-spinner-button>
|
||||
</footer>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-provider-select-table": ProviderSelectModal;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
import "#admin/applications/ProviderSelectModal";
|
||||
import "#admin/applications/ProviderSelectForm";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/chips/Chip";
|
||||
import "#elements/chips/ChipGroup";
|
||||
import "#elements/forms/Form";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { AKFormSubmitEvent } from "#elements/forms/Form";
|
||||
import { renderModal } from "#elements/modals/utils";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { Provider } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@@ -56,6 +61,24 @@ export class AkBackchannelProvidersInput extends AKElement {
|
||||
@property({ type: String })
|
||||
public help = "";
|
||||
|
||||
protected openSelectBackchannelProvidersModal = () => {
|
||||
return renderModal(html`
|
||||
<ak-form
|
||||
headline=${this.label}
|
||||
action-label=${msg("Confirm")}
|
||||
@submit=${(event: AKFormSubmitEvent<Provider[]>) => {
|
||||
const providers = event.target.toJSON();
|
||||
|
||||
this.confirm(providers);
|
||||
}}
|
||||
>
|
||||
${this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
|
||||
<ak-provider-select-form backchannel></ak-provider-select-form>
|
||||
</ak-form>
|
||||
`);
|
||||
};
|
||||
|
||||
render() {
|
||||
const renderOneChip = (provider: Provider) =>
|
||||
html`<ak-chip
|
||||
@@ -68,14 +91,16 @@ export class AkBackchannelProvidersInput extends AKElement {
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${this.label} name=${this.name}>
|
||||
<div class="pf-c-input-group">
|
||||
<ak-provider-select-table backchannel .confirm=${this.confirm}>
|
||||
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
|
||||
${this.tooltip ? this.tooltip : nothing}
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ak-provider-select-table>
|
||||
<button
|
||||
class="pf-c-button pf-m-control"
|
||||
type="button"
|
||||
@click=${this.openSelectBackchannelProvidersModal}
|
||||
>
|
||||
${this.tooltip ? this.tooltip : nothing}
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group> ${map(this.providers, renderOneChip)} </ak-chip-group>
|
||||
<ak-chip-group>${map(this.providers, renderOneChip)}</ak-chip-group>
|
||||
</div>
|
||||
</div>
|
||||
${this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { groupBy } from "#common/utils";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
@@ -12,6 +13,7 @@ import { IDGenerator } from "#packages/core/id";
|
||||
|
||||
import { Provider, ProvidersAllListRequest, ProvidersApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize/init/install";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@@ -91,7 +93,7 @@ export class AkProviderInput extends AKElement {
|
||||
render() {
|
||||
const readOnlyValue = this.readOnly && typeof this.value === "number";
|
||||
|
||||
return html` <ak-form-element-horizontal name=${this.name}>
|
||||
return html`<ak-form-element-horizontal name=${this.name}>
|
||||
${AKLabel(
|
||||
{
|
||||
slot: "label",
|
||||
@@ -105,6 +107,7 @@ export class AkProviderInput extends AKElement {
|
||||
? html`<input type="hidden" name=${this.name} value=${this.value ?? ""} />`
|
||||
: nothing}
|
||||
<ak-search-select
|
||||
label=${ifPresent(this.label)}
|
||||
.fieldID=${this.fieldID}
|
||||
.selected=${this.#selected}
|
||||
.fetchObjects=${this.#fetch}
|
||||
@@ -114,6 +117,7 @@ export class AkProviderInput extends AKElement {
|
||||
?blankable=${readOnlyValue ? false : !!this.blankable}
|
||||
?readonly=${this.readOnly}
|
||||
name=${ifDefined(readOnlyValue ? undefined : this.name)}
|
||||
placeholder=${msg("Search for a provider...")}
|
||||
>
|
||||
</ak-search-select>
|
||||
${this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
|
||||
@@ -8,12 +8,11 @@ import "#elements/forms/ModalForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
import { PolicyBindingCheckTarget } from "#common/policies/utils";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { PolicyBindingCheckTarget } from "#admin/policies/utils";
|
||||
|
||||
import {
|
||||
ApplicationEntitlement,
|
||||
CoreApi,
|
||||
@@ -77,7 +76,7 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
|
||||
return [
|
||||
html`${item.name}`,
|
||||
html`<ak-forms-modal size=${PFSize.Medium}>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Entitlement")}</span>
|
||||
<ak-application-entitlement-form
|
||||
slot="form"
|
||||
|
||||
@@ -10,7 +10,7 @@ import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
|
||||
import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";
|
||||
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
||||
|
||||
export const styles = [
|
||||
export const ApplicationWizardStyles = [
|
||||
PFCard,
|
||||
PFButton,
|
||||
PFForm,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
ApplicationTransactionValidationError,
|
||||
type ApplicationWizardState,
|
||||
type ApplicationWizardStateUpdate,
|
||||
} from "./types.js";
|
||||
|
||||
import { serializeForm } from "#elements/forms/Form";
|
||||
import { reportValidityDeep } from "#elements/forms/FormGroup";
|
||||
import { serializeForm } from "#elements/forms/serialization";
|
||||
|
||||
import {
|
||||
NavigationEventInit,
|
||||
@@ -14,23 +8,32 @@ import {
|
||||
} from "#components/ak-wizard/events";
|
||||
import { WizardStep } from "#components/ak-wizard/WizardStep";
|
||||
|
||||
import { styles } from "#admin/applications/wizard/ApplicationWizardFormStepStyles.styles";
|
||||
import { ApplicationWizardStyles } from "#admin/applications/wizard/ApplicationWizardFormStepStyles.styles";
|
||||
import {
|
||||
type ApplicationWizardState,
|
||||
type ApplicationWizardStateUpdate,
|
||||
} from "#admin/applications/wizard/steps/providers/shared";
|
||||
|
||||
import { ApplicationRequest, ValidationError } from "@goauthentik/api";
|
||||
import { ApplicationRequest } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
export class ApplicationWizardStep<T = Partial<ApplicationRequest>> extends WizardStep {
|
||||
static styles = [...WizardStep.styles, ...styles];
|
||||
/**
|
||||
* Base class for application wizard steps. Provides common functionality such as form handling and wizard state management.
|
||||
*
|
||||
* @prop wizard - The current state of the application wizard, shared across all steps.
|
||||
*/
|
||||
export abstract class ApplicationWizardStep<T = Partial<ApplicationRequest>> extends WizardStep {
|
||||
static styles = [...WizardStep.styles, ...ApplicationWizardStyles];
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
wizard!: ApplicationWizardState;
|
||||
public wizard!: ApplicationWizardState;
|
||||
|
||||
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
|
||||
// these fields and provide them to all the child classes.
|
||||
protected wizardTitle = msg("New application");
|
||||
protected wizardDescription = msg("Create a new application and configure a provider for it.");
|
||||
protected override wizardTitle = msg("New application");
|
||||
protected override wizardDescription = msg(
|
||||
"Create a new application and configure a provider for it.",
|
||||
);
|
||||
public canCancel = true;
|
||||
|
||||
// This should be overridden in the children for more precise targeting.
|
||||
@@ -73,25 +76,6 @@ export class ApplicationWizardStep<T = Partial<ApplicationRequest>> extends Wiza
|
||||
]);
|
||||
}
|
||||
|
||||
protected removeErrors(
|
||||
keyToDelete: keyof ApplicationTransactionValidationError,
|
||||
): ValidationError | undefined {
|
||||
if (!this.wizard.errors) {
|
||||
return undefined;
|
||||
}
|
||||
const empty = {};
|
||||
const errors = Object.entries(this.wizard.errors).reduce(
|
||||
(acc, [key, value]) =>
|
||||
key === keyToDelete ||
|
||||
value === undefined ||
|
||||
(Array.isArray(this.wizard?.errors?.[key]) && this.wizard.errors[key].length === 0)
|
||||
? acc
|
||||
: { ...acc, [key]: value },
|
||||
empty,
|
||||
);
|
||||
return errors;
|
||||
}
|
||||
|
||||
// This pattern became visible during development, and the order is important: wizard updating
|
||||
// and validation must complete before navigation is attempted.
|
||||
public handleUpdate(
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import "#components/ak-wizard/ak-wizard-steps";
|
||||
import "./steps/ak-application-wizard-application-step.js";
|
||||
import "./steps/ak-application-wizard-bindings-step.js";
|
||||
import "./steps/ak-application-wizard-edit-binding-step.js";
|
||||
import "./steps/ak-application-wizard-provider-choice-step.js";
|
||||
import "./steps/ak-application-wizard-provider-step.js";
|
||||
import "./steps/ak-application-wizard-submit-step.js";
|
||||
|
||||
import { applicationWizardProvidersContext } from "./ContextIdentity.js";
|
||||
import { type ApplicationWizardState, type ApplicationWizardStateUpdate } from "./types.js";
|
||||
import "#admin/applications/wizard/steps/ak-application-wizard-application-step";
|
||||
import "#admin/applications/wizard/steps/ak-application-wizard-bindings-step";
|
||||
import "#admin/applications/wizard/steps/ak-application-wizard-edit-binding-step";
|
||||
import "#admin/applications/wizard/steps/ak-application-wizard-provider-choice-step";
|
||||
import "#admin/applications/wizard/steps/ak-application-wizard-provider-step";
|
||||
import "#admin/applications/wizard/steps/ak-application-wizard-submit-step";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { assertEveryPresent } from "#common/utils";
|
||||
@@ -16,6 +13,12 @@ import { AKElement } from "#elements/Base";
|
||||
|
||||
import { WizardUpdateEvent } from "#components/ak-wizard/events";
|
||||
|
||||
import { applicationWizardProvidersContext } from "#admin/applications/wizard/ContextIdentity";
|
||||
import {
|
||||
type ApplicationWizardState,
|
||||
type ApplicationWizardStateUpdate,
|
||||
} from "#admin/applications/wizard/steps/providers/shared";
|
||||
|
||||
import type { TypeCreate } from "@goauthentik/api";
|
||||
import { ProviderModelEnum, ProvidersApi, ProxyMode } from "@goauthentik/api";
|
||||
|
||||
@@ -50,10 +53,14 @@ export const providerTypePriority: ProviderModelNameEnum[] = [
|
||||
|
||||
@customElement("ak-application-wizard-main")
|
||||
export class AkApplicationWizardMain extends AKElement {
|
||||
@state()
|
||||
wizard: ApplicationWizardState = freshWizardState();
|
||||
protected createRenderRoot(): HTMLElement | DocumentFragment {
|
||||
return this;
|
||||
}
|
||||
|
||||
wizardProviderProvider = new ContextProvider(this, {
|
||||
@state()
|
||||
protected wizard: ApplicationWizardState = freshWizardState();
|
||||
|
||||
protected wizardProviderProvider = new ContextProvider(this, {
|
||||
context: applicationWizardProvidersContext,
|
||||
initialValue: [],
|
||||
});
|
||||
@@ -63,8 +70,9 @@ export class AkApplicationWizardMain extends AKElement {
|
||||
this.addEventListener(WizardUpdateEvent.eventName, this.handleUpdate);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
public override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
|
||||
const providerNameToProviderMap = new Map(
|
||||
providerTypes.map((providerType) => [providerType.modelName, providerType]),
|
||||
@@ -85,7 +93,8 @@ export class AkApplicationWizardMain extends AKElement {
|
||||
handleUpdate(ev: WizardUpdateEvent<ApplicationWizardStateUpdate>) {
|
||||
ev.stopPropagation();
|
||||
const update = ev.content;
|
||||
if (update !== undefined) {
|
||||
|
||||
if (typeof update !== "undefined") {
|
||||
this.wizard = {
|
||||
...this.wizard,
|
||||
...update,
|
||||
|
||||
@@ -1,28 +1,41 @@
|
||||
import "./ak-application-wizard-main.js";
|
||||
import "#admin/applications/wizard/ak-application-wizard-main";
|
||||
|
||||
import { ModalButton } from "#elements/buttons/ModalButton";
|
||||
import { bound } from "#elements/decorators/bound";
|
||||
import { AKModal } from "#elements/modals/ak-modal";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { WizardCloseEvent } from "#components/ak-wizard/events";
|
||||
|
||||
import { html } from "lit";
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-application-wizard")
|
||||
export class AkApplicationWizard extends ModalButton {
|
||||
export class AkApplicationWizard extends AKModal {
|
||||
public static override formatARIALabel?(): string {
|
||||
return msg("New Application Wizard");
|
||||
}
|
||||
|
||||
public static override styles: CSSResult[] = [
|
||||
...super.styles,
|
||||
css`
|
||||
[part="main"] {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(WizardCloseEvent.eventName, this.onCloseEvent);
|
||||
|
||||
this.addEventListener(WizardCloseEvent.eventName, this.closeListener);
|
||||
}
|
||||
|
||||
@bound
|
||||
onCloseEvent(ev: WizardCloseEvent) {
|
||||
ev.stopPropagation();
|
||||
this.open = false;
|
||||
protected renderCloseButton(): SlottedTemplateResult {
|
||||
return null;
|
||||
}
|
||||
|
||||
renderModalInner() {
|
||||
return html` <ak-application-wizard-main> </ak-application-wizard-main>`;
|
||||
render() {
|
||||
return html`<ak-application-wizard-main part="main"></ak-application-wizard-main>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import "#components/ak-status-label";
|
||||
|
||||
import { OneOfProvider } from "../types.js";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { type DescriptionPair, renderDescriptionList } from "#components/DescriptionList";
|
||||
|
||||
import { OneOfProvider } from "#admin/applications/wizard/steps/providers/shared";
|
||||
|
||||
import {
|
||||
ClientTypeEnum,
|
||||
LDAPProvider,
|
||||
@@ -30,39 +32,38 @@ const renderSummary = (type: string, name: string, fields: DescriptionPair[]) =>
|
||||
threecolumn: true,
|
||||
});
|
||||
|
||||
function renderSAMLOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as SAMLProvider;
|
||||
export type ProviderOverview<T extends OneOfProvider | unknown = unknown> = (
|
||||
provider: T,
|
||||
) => SlottedTemplateResult;
|
||||
|
||||
const renderSAMLOverview: ProviderOverview<SAMLProvider> = (provider) => {
|
||||
return renderSummary("SAML", provider.name, [
|
||||
[msg("ACS URL"), provider.acsUrl],
|
||||
[msg("Audience"), provider.audience || "-"],
|
||||
[msg("Issuer"), provider.issuer],
|
||||
]);
|
||||
}
|
||||
|
||||
function renderSAMLImportOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as ProvidersSamlImportMetadataCreateRequest;
|
||||
};
|
||||
|
||||
const renderSAMLImportOverview: ProviderOverview<ProvidersSamlImportMetadataCreateRequest> = (
|
||||
provider,
|
||||
) => {
|
||||
return renderSummary("SAML", provider.name, [
|
||||
[msg("Authorization flow"), provider.authorizationFlow ?? "-"],
|
||||
[msg("Invalidation flow"), provider.invalidationFlow ?? "-"],
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
function renderSCIMOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as SCIMProvider;
|
||||
const renderSCIMOverview: ProviderOverview<SCIMProvider> = (provider) => {
|
||||
return renderSummary("SCIM", provider.name, [[msg("URL"), provider.url]]);
|
||||
}
|
||||
};
|
||||
|
||||
function renderRadiusOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as RadiusProvider;
|
||||
const renderRadiusOverview: ProviderOverview<RadiusProvider> = (provider) => {
|
||||
return renderSummary("Radius", provider.name, [
|
||||
[msg("Client Networks"), provider.clientNetworks],
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
function renderRACOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as RACProvider;
|
||||
const renderRACOverview: ProviderOverview<RACProvider> = (provider) => {
|
||||
return renderSummary("RAC", provider.name, [
|
||||
[msg("Connection expiry"), provider.connectionExpiry ?? "-"],
|
||||
[
|
||||
@@ -72,7 +73,7 @@ function renderRACOverview(rawProvider: OneOfProvider) {
|
||||
: msg("None"),
|
||||
],
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
function formatRedirectUris(uris: RedirectURI[] = []) {
|
||||
return uris.length > 0
|
||||
@@ -90,76 +91,76 @@ function formatRedirectUris(uris: RedirectURI[] = []) {
|
||||
: "-";
|
||||
}
|
||||
|
||||
const proxyModeToLabel = new Map([
|
||||
[ProxyMode.Proxy, msg("Proxy")],
|
||||
[ProxyMode.ForwardSingle, msg("Forward auth (single application)")],
|
||||
[ProxyMode.ForwardDomain, msg("Forward auth (domain-level)")],
|
||||
[ProxyMode.UnknownDefaultOpenApi, msg("Unknown proxy mode")],
|
||||
]);
|
||||
const proxyModeToLabel = {
|
||||
[ProxyMode.Proxy]: () => msg("Proxy"),
|
||||
[ProxyMode.ForwardSingle]: () => msg("Forward auth (single application)"),
|
||||
[ProxyMode.ForwardDomain]: () => msg("Forward auth (domain-level)"),
|
||||
[ProxyMode.UnknownDefaultOpenApi]: () => msg("Unknown proxy mode"),
|
||||
} as const satisfies Record<ProxyMode, () => string>;
|
||||
|
||||
const renderProxyOverview: ProviderOverview<ProxyProvider> = (provider) => {
|
||||
const proxyHostMappings: DescriptionPair[] = match<ProxyMode | undefined, DescriptionPair[]>(
|
||||
provider.mode,
|
||||
)
|
||||
.with(ProxyMode.Proxy, () => {
|
||||
return [
|
||||
[msg("Internal Host"), provider.internalHost],
|
||||
[msg("External Host"), provider.externalHost],
|
||||
];
|
||||
})
|
||||
.with(ProxyMode.ForwardSingle, () => {
|
||||
return [[msg("External Host"), provider.externalHost]];
|
||||
})
|
||||
.with(ProxyMode.ForwardDomain, () => {
|
||||
return [
|
||||
[msg("Authentication URL"), provider.externalHost],
|
||||
[msg("Cookie domain"), provider.cookieDomain],
|
||||
];
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new Error(
|
||||
`Unrecognized proxy mode: ${provider.mode?.toString() ?? "-- undefined __"}`,
|
||||
);
|
||||
});
|
||||
|
||||
const label = proxyModeToLabel[provider.mode ?? ProxyMode.Proxy];
|
||||
|
||||
function renderProxyOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as ProxyProvider;
|
||||
return renderSummary("Proxy", provider.name, [
|
||||
[msg("Mode"), proxyModeToLabel.get(provider.mode ?? ProxyMode.Proxy)],
|
||||
...match(provider.mode)
|
||||
.with(
|
||||
ProxyMode.Proxy,
|
||||
() =>
|
||||
[
|
||||
[msg("Internal Host"), provider.internalHost],
|
||||
[msg("External Host"), provider.externalHost],
|
||||
] as DescriptionPair[],
|
||||
)
|
||||
.with(
|
||||
ProxyMode.ForwardSingle,
|
||||
() => [[msg("External Host"), provider.externalHost]] as DescriptionPair[],
|
||||
)
|
||||
.with(
|
||||
ProxyMode.ForwardDomain,
|
||||
() =>
|
||||
[
|
||||
[msg("Authentication URL"), provider.externalHost],
|
||||
[msg("Cookie domain"), provider.cookieDomain],
|
||||
] as DescriptionPair[],
|
||||
)
|
||||
.otherwise(() => {
|
||||
throw new Error(
|
||||
`Unrecognized proxy mode: ${provider.mode?.toString() ?? "-- undefined __"}`,
|
||||
);
|
||||
}),
|
||||
[msg("Mode"), label()],
|
||||
...proxyHostMappings,
|
||||
[
|
||||
msg("Basic-Auth"),
|
||||
html` <ak-status-label
|
||||
html`<ak-status-label
|
||||
type="info"
|
||||
?good=${provider.basicAuthEnabled}
|
||||
></ak-status-label>`,
|
||||
],
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const clientTypeToLabel = new Map<ClientTypeEnum, string>([
|
||||
[ClientTypeEnum.Confidential, msg("Confidential")],
|
||||
[ClientTypeEnum.Public, msg("Public")],
|
||||
[ClientTypeEnum.UnknownDefaultOpenApi, msg("Unknown type")],
|
||||
]);
|
||||
const clientTypeToLabel = {
|
||||
[ClientTypeEnum.Confidential]: () => msg("Confidential"),
|
||||
[ClientTypeEnum.Public]: () => msg("Public"),
|
||||
[ClientTypeEnum.UnknownDefaultOpenApi]: () => msg("Unknown type"),
|
||||
} as const satisfies Record<ClientTypeEnum, () => string>;
|
||||
|
||||
const renderOAuth2Overview: ProviderOverview<OAuth2Provider> = (provider) => {
|
||||
const label = provider.clientType ? clientTypeToLabel[provider.clientType]() : "";
|
||||
|
||||
function renderOAuth2Overview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as OAuth2Provider;
|
||||
return renderSummary("OAuth2", provider.name, [
|
||||
[msg("Client type"), provider.clientType ? clientTypeToLabel.get(provider.clientType) : ""],
|
||||
[msg("Client type"), label],
|
||||
[msg("Client ID"), provider.clientId],
|
||||
[msg("Redirect URIs"), formatRedirectUris(provider.redirectUris)],
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
function renderLDAPOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as LDAPProvider;
|
||||
const renderLDAPOverview: ProviderOverview<LDAPProvider> = (provider) => {
|
||||
return renderSummary("Proxy", provider.name, [[msg("Base DN"), provider.baseDn]]);
|
||||
}
|
||||
};
|
||||
|
||||
const providerName = (p: ProviderModelEnum): string => p.toString().split(".")[1];
|
||||
|
||||
export const providerRenderers = new Map([
|
||||
export const providerRenderers = new Map<string, ProviderOverview<OneOfProvider>>([
|
||||
[providerName(ProviderModelEnum.AuthentikProvidersSamlSamlprovider), renderSAMLOverview],
|
||||
["samlproviderimportmodel", renderSAMLImportOverview],
|
||||
[providerName(ProviderModelEnum.AuthentikProvidersScimScimprovider), renderSCIMOverview],
|
||||
@@ -168,4 +169,4 @@ export const providerRenderers = new Map([
|
||||
[providerName(ProviderModelEnum.AuthentikProvidersProxyProxyprovider), renderProxyOverview],
|
||||
[providerName(ProviderModelEnum.AuthentikProvidersOauth2Oauth2provider), renderOAuth2Overview],
|
||||
[providerName(ProviderModelEnum.AuthentikProvidersLdapLdapprovider), renderLDAPOverview],
|
||||
]);
|
||||
] satisfies [string, ProviderOverview<never>][] as [string, ProviderOverview<OneOfProvider>][]);
|
||||
|
||||
@@ -8,13 +8,17 @@ import "#components/ak-textarea-input";
|
||||
import "#elements/forms/FormGroup";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
|
||||
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types.js";
|
||||
import { omitKeys, trimMany } from "#common/objects";
|
||||
|
||||
import { isSlug } from "#elements/router/utils";
|
||||
|
||||
import { type NavigableButton, type WizardButton } from "#components/ak-wizard/types";
|
||||
import { type NavigableButton, type WizardButton } from "#components/ak-wizard/shared";
|
||||
|
||||
import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep";
|
||||
import {
|
||||
ApplicationWizardStateUpdate,
|
||||
WizardValidationRecord,
|
||||
} from "#admin/applications/wizard/steps/providers/shared";
|
||||
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
|
||||
|
||||
import { AdminFileListUsageEnum, type ApplicationRequest } from "@goauthentik/api";
|
||||
@@ -26,18 +30,14 @@ import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
function trimMany<T extends object, K extends keyof T>(target: T, keys: K[]): Pick<T, K> {
|
||||
const output = {} as Record<K, unknown>;
|
||||
|
||||
for (const key of keys) {
|
||||
const value = target[key];
|
||||
|
||||
output[key] = typeof value === "string" ? value.trim() : value;
|
||||
}
|
||||
|
||||
return output as Pick<T, K>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first step of the application wizard, responsible for collecting
|
||||
* basic application information such as name, slug, group, and UI settings.
|
||||
*
|
||||
* This step performs validation on the form inputs and updates the wizard state accordingly when the "Next" button is clicked.
|
||||
*
|
||||
* @prop wizard - The current state of the application wizard, shared across all steps.
|
||||
*/
|
||||
@customElement("ak-application-wizard-application-step")
|
||||
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
label = msg("Application");
|
||||
@@ -62,13 +62,17 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
}
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }];
|
||||
return [
|
||||
// ---
|
||||
{ kind: "cancel" },
|
||||
{ kind: "next", destination: "provider-choice" },
|
||||
];
|
||||
}
|
||||
|
||||
get valid() {
|
||||
this.errors = new Map();
|
||||
|
||||
const values = trimMany(this.formValues, ["metaLaunchUrl", "name", "slug"]);
|
||||
const values = trimMany(this.formValues, "metaLaunchUrl", "name", "slug");
|
||||
|
||||
if (!values.name) {
|
||||
this.errors.set("name", msg("An application name is required"));
|
||||
@@ -85,7 +89,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
return this.errors.size === 0;
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
public override handleButton(button: NavigableButton) {
|
||||
if (button.kind !== "next") {
|
||||
return super.handleButton(button);
|
||||
}
|
||||
@@ -102,7 +106,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
|
||||
const payload: ApplicationWizardStateUpdate = {
|
||||
app,
|
||||
errors: this.removeErrors("app"),
|
||||
errors: omitKeys(this.wizard.errors, "app"),
|
||||
};
|
||||
|
||||
if (!this.wizard.provider?.name?.trim() && app.name) {
|
||||
@@ -116,7 +120,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(app: Partial<ApplicationRequest>, errors: ValidationRecord) {
|
||||
protected renderForm(app: Partial<ApplicationRequest>, errors: WizardValidationRecord = {}) {
|
||||
return html` <ak-wizard-title>${msg("Configure the Application")}</ak-wizard-title>
|
||||
<form id="applicationform" class="pf-c-form pf-m-horizontal" slot="form">
|
||||
<ak-text-input
|
||||
@@ -139,6 +143,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
.errorMessages=${this.errorMessages("slug")}
|
||||
help=${msg("Internal application name used in URLs.")}
|
||||
input-hint="code"
|
||||
placeholder=${msg("e.g. my-application")}
|
||||
></ak-slug-input>
|
||||
<ak-text-input
|
||||
name="group"
|
||||
@@ -198,12 +203,16 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
name="metaPublisher"
|
||||
value="${ifDefined(app.metaPublisher)}"
|
||||
.errorMessages=${errors.metaPublisher}
|
||||
help=${msg("The publisher is shown in the application library.")}
|
||||
></ak-text-input>
|
||||
<ak-textarea-input
|
||||
label=${msg("Description")}
|
||||
name="metaDescription"
|
||||
value=${ifDefined(app.metaDescription)}
|
||||
.errorMessages=${errors.metaDescription}
|
||||
help=${msg(
|
||||
"The description is shown in the application library and may provide additional information about the application to end users.",
|
||||
)}
|
||||
></ak-textarea-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
@@ -214,10 +223,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
if (!(this.wizard.app && this.wizard.errors)) {
|
||||
throw new Error("Application Step received uninitialized wizard context.");
|
||||
}
|
||||
return this.renderForm(
|
||||
this.wizard.app as ApplicationRequest,
|
||||
this.wizard.errors?.app ?? {},
|
||||
);
|
||||
return this.renderForm(this.wizard.app, this.wizard.errors?.app);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,14 @@ import "#components/ak-text-input";
|
||||
import "#elements/ak-table/ak-select-table";
|
||||
import "#elements/forms/FormGroup";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "./bindings/ak-application-wizard-bindings-toolbar.js";
|
||||
|
||||
import { makeEditButton } from "./bindings/ak-application-wizard-bindings-edit-button.js";
|
||||
import "#admin/applications/wizard/steps/bindings/ak-application-wizard-bindings-toolbar";
|
||||
|
||||
import { SelectTable } from "#elements/ak-table/ak-select-table";
|
||||
|
||||
import { type WizardButton } from "#components/ak-wizard/types";
|
||||
import { type WizardButton } from "#components/ak-wizard/shared";
|
||||
|
||||
import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep";
|
||||
import { makeEditButton } from "#admin/applications/wizard/steps/bindings/ak-application-wizard-bindings-edit-button";
|
||||
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
@@ -34,15 +33,18 @@ const COLUMNS = [
|
||||
[msg("Actions"), null, msg("Row Actions")],
|
||||
];
|
||||
|
||||
/**
|
||||
* @prop wizard - The current state of the application wizard, shared across all steps.
|
||||
*/
|
||||
@customElement("ak-application-wizard-bindings-step")
|
||||
export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
|
||||
label = msg("Configure Bindings");
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [
|
||||
{ kind: "next", destination: "submit" },
|
||||
{ kind: "back", destination: "provider" },
|
||||
{ kind: "cancel" },
|
||||
{ kind: "back", destination: "provider" },
|
||||
{ kind: "next", destination: "submit" },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,19 +10,19 @@ import "#elements/forms/SearchSelect/ak-search-select-ez";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import {
|
||||
createPassFailOptions,
|
||||
PolicyBindingCheckTarget,
|
||||
PolicyObjectKeys,
|
||||
} from "#common/policies/utils";
|
||||
import { groupBy } from "#common/utils";
|
||||
|
||||
import { ISearchSelectConfig } from "#elements/forms/SearchSelect/ak-search-select-ez";
|
||||
import { type SearchSelectBase } from "#elements/forms/SearchSelect/SearchSelect";
|
||||
|
||||
import { type NavigableButton, type WizardButton } from "#components/ak-wizard/types";
|
||||
import { type NavigableButton, type WizardButton } from "#components/ak-wizard/shared";
|
||||
|
||||
import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep";
|
||||
import {
|
||||
createPassFailOptions,
|
||||
PolicyBindingCheckTarget,
|
||||
PolicyObjectKeys,
|
||||
} from "#admin/policies/utils";
|
||||
|
||||
import { CoreApi, Group, PoliciesApi, Policy, PolicyBinding, User } from "@goauthentik/api";
|
||||
|
||||
@@ -32,8 +32,11 @@ import { customElement, query, state } from "lit/decorators.js";
|
||||
|
||||
const withQuery = <T>(search: string | undefined, args: T) => (search ? { ...args, search } : args);
|
||||
|
||||
/**
|
||||
* @prop wizard - The current state of the application wizard, shared across all steps.
|
||||
*/
|
||||
@customElement("ak-application-wizard-edit-binding-step")
|
||||
export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
|
||||
export class ApplicationWizardEditBindingStep extends ApplicationWizardStep<PolicyBinding> {
|
||||
label = msg("Edit Binding");
|
||||
|
||||
hide = true;
|
||||
@@ -54,13 +57,13 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [
|
||||
{ kind: "cancel" },
|
||||
{ kind: "next", label: msg("Save Binding"), destination: "bindings" },
|
||||
{ kind: "back", destination: "bindings" },
|
||||
{ kind: "cancel" },
|
||||
];
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
public override handleButton(button: NavigableButton) {
|
||||
if (button.kind === "next") {
|
||||
if (!this.form?.checkValidity()) {
|
||||
return;
|
||||
@@ -69,7 +72,7 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
|
||||
const policyObject = this.searchSelect.selectedObject;
|
||||
const policyKey = PolicyObjectKeys[this.policyGroupUser];
|
||||
const newBinding: PolicyBinding = {
|
||||
...(this.formValues as unknown as PolicyBinding),
|
||||
...this.formValues,
|
||||
[policyKey]: policyObject,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { bound } from "#elements/decorators/bound";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage";
|
||||
|
||||
import type { NavigableButton, WizardButton } from "#components/ak-wizard/types";
|
||||
import type { NavigableButton, WizardButton } from "#components/ak-wizard/shared";
|
||||
|
||||
import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep";
|
||||
|
||||
@@ -21,6 +21,10 @@ import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
/**
|
||||
*
|
||||
* @prop wizard - The current state of the application wizard, shared across all steps.
|
||||
*/
|
||||
@customElement("ak-application-wizard-provider-choice-step")
|
||||
export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) {
|
||||
label = msg("Choose a Provider");
|
||||
@@ -33,13 +37,13 @@ export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(Appl
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [
|
||||
{ kind: "next", destination: "provider" },
|
||||
{ kind: "back", destination: "application" },
|
||||
{ kind: "cancel" },
|
||||
{ kind: "back", destination: "application" },
|
||||
{ kind: "next", destination: "provider" },
|
||||
];
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
public override handleButton(button: NavigableButton) {
|
||||
this.failureMessage = "";
|
||||
if (button.kind === "next") {
|
||||
if (!this.wizard.providerModel) {
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
import "./providers/ak-application-wizard-provider-for-ldap.js";
|
||||
import "./providers/ak-application-wizard-provider-for-oauth.js";
|
||||
import "./providers/ak-application-wizard-provider-for-proxy.js";
|
||||
import "./providers/ak-application-wizard-provider-for-rac.js";
|
||||
import "./providers/ak-application-wizard-provider-for-radius.js";
|
||||
import "./providers/ak-application-wizard-provider-for-saml.js";
|
||||
import "./providers/ak-application-wizard-provider-for-saml-metadata.js";
|
||||
import "./providers/ak-application-wizard-provider-for-scim.js";
|
||||
import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-ldap";
|
||||
import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth";
|
||||
import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-proxy";
|
||||
import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-rac";
|
||||
import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-radius";
|
||||
import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml";
|
||||
import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml-metadata";
|
||||
import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-scim";
|
||||
|
||||
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
||||
import { OneOfProvider } from "../types.js";
|
||||
import { ApplicationWizardProviderForm } from "./providers/ApplicationWizardProviderForm.js";
|
||||
import { omitKeys } from "#common/objects";
|
||||
|
||||
import { type NavigableButton, type WizardButton } from "#components/ak-wizard/types";
|
||||
import { StrictUnsafe } from "#elements/utils/unsafe";
|
||||
|
||||
import { type NavigableButton, type WizardButton } from "#components/ak-wizard/shared";
|
||||
|
||||
import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep";
|
||||
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
|
||||
import { OneOfProvider } from "#admin/applications/wizard/steps/providers/shared";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { nothing, PropertyValues } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { html, unsafeStatic } from "lit/static-html.js";
|
||||
|
||||
const providerToTag = new Map([
|
||||
["ldapprovider", "ak-application-wizard-provider-for-ldap"],
|
||||
["oauth2provider", "ak-application-wizard-provider-for-oauth"],
|
||||
["proxyprovider", "ak-application-wizard-provider-for-proxy"],
|
||||
["racprovider", "ak-application-wizard-provider-for-rac"],
|
||||
["radiusprovider", "ak-application-wizard-provider-for-radius"],
|
||||
["samlprovider", "ak-application-wizard-provider-for-saml"],
|
||||
["samlproviderimportmodel", "ak-application-wizard-provider-for-saml-metadata"],
|
||||
["scimprovider", "ak-application-wizard-provider-for-scim"],
|
||||
]);
|
||||
const providerToTag = {
|
||||
ldapprovider: "ak-application-wizard-provider-for-ldap",
|
||||
oauth2provider: "ak-application-wizard-provider-for-oauth",
|
||||
proxyprovider: "ak-application-wizard-provider-for-proxy",
|
||||
racprovider: "ak-application-wizard-provider-for-rac",
|
||||
radiusprovider: "ak-application-wizard-provider-for-radius",
|
||||
samlprovider: "ak-application-wizard-provider-for-saml",
|
||||
samlproviderimportmodel: "ak-application-wizard-provider-for-saml-metadata",
|
||||
scimprovider: "ak-application-wizard-provider-for-scim",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
type ProviderModel = keyof typeof providerToTag;
|
||||
|
||||
/**
|
||||
* @prop wizard - The current state of the application wizard, shared across all steps.
|
||||
*/
|
||||
@customElement("ak-application-wizard-provider-step")
|
||||
export class ApplicationWizardProviderStep extends ApplicationWizardStep {
|
||||
@state()
|
||||
label = msg("Configure Provider");
|
||||
public override label = msg("Configure Provider");
|
||||
|
||||
@query("#providerform")
|
||||
protected element!: ApplicationWizardProviderForm<OneOfProvider>;
|
||||
@@ -62,7 +70,7 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep {
|
||||
return this.element.formValues;
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
public override handleButton(button: NavigableButton) {
|
||||
if (button.kind === "next") {
|
||||
if (!this.valid) {
|
||||
this.handleEnabling({
|
||||
@@ -75,7 +83,7 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep {
|
||||
...this.formValues,
|
||||
mode: this.wizard.proxyMode,
|
||||
},
|
||||
errors: this.removeErrors("provider"),
|
||||
errors: omitKeys(this.wizard.errors, "provider"),
|
||||
};
|
||||
this.handleUpdate(payload, button.destination, {
|
||||
enable: ["bindings", "submit"],
|
||||
@@ -87,9 +95,9 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep {
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [
|
||||
{ kind: "next", destination: "bindings" },
|
||||
{ kind: "back", destination: "provider-choice" },
|
||||
{ kind: "cancel" },
|
||||
{ kind: "back", destination: "provider-choice" },
|
||||
{ kind: "next", destination: "bindings" },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -101,19 +109,21 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep {
|
||||
// This is, I'm afraid, some rather esoteric bit of Lit-ing, and it makes ESLint
|
||||
// sad. It does allow us to get away with specifying very little about the
|
||||
// provider here.
|
||||
const tag = providerToTag.get(this.wizard.providerModel);
|
||||
return tag
|
||||
? // eslint-disable-next-line lit/binding-positions,lit/no-invalid-html
|
||||
html`<${unsafeStatic(tag)}
|
||||
id="providerform"
|
||||
.wizard=${this.wizard}
|
||||
.errors=${this.wizard.errors?.provider ?? {}}
|
||||
const tag = providerToTag[this.wizard.providerModel as ProviderModel];
|
||||
|
||||
></${
|
||||
/* eslint-disable-next-line lit/binding-positions,lit/no-invalid-html */
|
||||
unsafeStatic(tag)
|
||||
}>`
|
||||
: nothing;
|
||||
if (!tag) {
|
||||
this.logger.warn(
|
||||
`No provider form found for provider model ${this.wizard.providerModel}`,
|
||||
);
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return StrictUnsafe<ApplicationWizardProviderForm<OneOfProvider>>(tag, {
|
||||
wizard: this.wizard,
|
||||
id: "providerform",
|
||||
errors: this.wizard.errors?.provider ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
updated(changed: PropertyValues<this>) {
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import "#admin/applications/wizard/ak-wizard-title";
|
||||
|
||||
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
||||
import { isApplicationTransactionValidationError, OneOfProvider } from "../types.js";
|
||||
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import { parseAPIResponseError } from "#common/errors/network";
|
||||
|
||||
import { showAPIErrorMessage } from "#elements/messages/MessageContainer";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { CustomEmitterElement } from "#elements/utils/eventEmitter";
|
||||
|
||||
import { WizardNavigationEvent } from "#components/ak-wizard/events";
|
||||
import { type WizardButton } from "#components/ak-wizard/types";
|
||||
import { type WizardButton } from "#components/ak-wizard/shared";
|
||||
|
||||
import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep";
|
||||
import {
|
||||
isApplicationTransactionValidationError,
|
||||
OneOfProvider,
|
||||
} from "#admin/applications/wizard/steps/providers/shared";
|
||||
import { providerRenderers } from "#admin/applications/wizard/steps/SubmitStepOverviewRenderers";
|
||||
|
||||
import {
|
||||
type ApplicationRequest,
|
||||
@@ -26,7 +30,6 @@ import {
|
||||
type ProvidersSamlImportMetadataCreateRequest,
|
||||
ProxyMode,
|
||||
type ProxyProviderRequest,
|
||||
type SAMLProvider,
|
||||
type TransactionApplicationRequest,
|
||||
type TransactionApplicationResponse,
|
||||
type TransactionPolicyBindingRequest,
|
||||
@@ -35,11 +38,10 @@ import {
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html, nothing, TemplateResult } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
// import { map } from "lit/directives/map.js";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
|
||||
import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css";
|
||||
@@ -51,19 +53,22 @@ type SubmitStates = (typeof _submitStates)[number];
|
||||
|
||||
type StrictProviderModelEnum = Exclude<ProviderModelEnum, "11184809">;
|
||||
|
||||
const providerMap: Map<string, string> = Object.values(ProviderModelEnum)
|
||||
.filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value))
|
||||
.reduce((acc: Map<string, string>, value) => {
|
||||
acc.set(value.split(".")[1], value);
|
||||
const providerMap: Map<string, StrictProviderModelEnum> = Object.values(ProviderModelEnum)
|
||||
.filter((value): value is StrictProviderModelEnum => {
|
||||
return /^authentik_providers_/.test(value) && /provider$/.test(value);
|
||||
})
|
||||
.reduce((acc: Map<string, StrictProviderModelEnum>, value) => {
|
||||
const key = value.split(".")[1];
|
||||
acc.set(key, value);
|
||||
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
||||
type MaybeTemplateResult = TemplateResult | typeof nothing;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isNotEmpty = (arr: any): arr is NonEmptyArray<any> => Array.isArray(arr) && arr.length > 0;
|
||||
function isNotEmpty<T>(arr: T[] | undefined): arr is NonEmptyArray<T> {
|
||||
return Array.isArray(arr) && arr.length > 0;
|
||||
}
|
||||
|
||||
const cleanApplication = (app: Partial<ApplicationRequest>): ApplicationRequest => ({
|
||||
name: "",
|
||||
@@ -99,10 +104,10 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
`,
|
||||
];
|
||||
|
||||
label = msg("Review and Submit Application");
|
||||
public override label = msg("Review and Submit Application");
|
||||
|
||||
@state()
|
||||
state: SubmitStates = "reviewing";
|
||||
protected state: SubmitStates = "reviewing";
|
||||
|
||||
async sendSAMLMetadataImport() {
|
||||
const providerData = this.wizard.provider as ProvidersSamlImportMetadataCreateRequest;
|
||||
@@ -112,12 +117,12 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
|
||||
try {
|
||||
// Step 1: Import SAML metadata to create the provider
|
||||
const createdProvider = (await providersApi.providersSamlImportMetadataCreate({
|
||||
const createdProvider = await providersApi.providersSamlImportMetadataCreate({
|
||||
file: providerData.file,
|
||||
name: providerData.name,
|
||||
authorizationFlow: providerData.authorizationFlow || "",
|
||||
invalidationFlow: providerData.invalidationFlow || "",
|
||||
})) as unknown as SAMLProvider;
|
||||
});
|
||||
|
||||
// Step 2: Create the application linked to the provider
|
||||
const appData = cleanApplication(this.wizard.app);
|
||||
@@ -158,12 +163,12 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
const app = this.wizard.app;
|
||||
const provider = this.wizard.provider as ModelRequest;
|
||||
|
||||
if (app === undefined) {
|
||||
throw new Error("Reached the submit state with the app undefined");
|
||||
if (!app) {
|
||||
throw new Error("Reached the submit state without the application initialized");
|
||||
}
|
||||
|
||||
if (provider === undefined) {
|
||||
throw new Error("Reached the submit state with the provider undefined");
|
||||
if (!provider) {
|
||||
throw new Error("Reached the submit state without the provider initialized");
|
||||
}
|
||||
|
||||
this.state = "running";
|
||||
@@ -176,14 +181,21 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
// Stringly-based API. Not the best, but it works. Just be aware that it is
|
||||
// stringly-based.
|
||||
|
||||
const providerModel = providerMap.get(this.wizard.providerModel) as StrictProviderModelEnum;
|
||||
const providerModel = providerMap.get(this.wizard.providerModel);
|
||||
|
||||
if (!providerModel) {
|
||||
throw new TypeError("Unrecognized provider model: " + this.wizard.providerModel);
|
||||
}
|
||||
|
||||
provider.providerModel = providerModel;
|
||||
|
||||
// Special case for the Proxy provider.
|
||||
if (this.wizard.providerModel === "proxyprovider") {
|
||||
(provider as ProxyProviderRequest).mode = this.wizard.proxyMode;
|
||||
if ((provider as ProxyProviderRequest).mode !== ProxyMode.ForwardDomain) {
|
||||
(provider as ProxyProviderRequest).cookieDomain = "";
|
||||
const proxyProviderRequest = provider as ProxyProviderRequest;
|
||||
proxyProviderRequest.mode = this.wizard.proxyMode;
|
||||
|
||||
if (proxyProviderRequest.mode !== ProxyMode.ForwardDomain) {
|
||||
proxyProviderRequest.cookieDomain = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +248,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
});
|
||||
}
|
||||
|
||||
override handleButton(button: WizardButton) {
|
||||
public override handleButton(button: WizardButton) {
|
||||
match([button.kind, this.state])
|
||||
.with([P.union("back", "cancel"), P._], () => {
|
||||
super.handleButton(button);
|
||||
@@ -259,9 +271,9 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
const forReview: WizardButton[] = [
|
||||
{ kind: "next", label: msg("Submit"), destination: "here" },
|
||||
{ kind: "back", destination: "bindings" },
|
||||
{ kind: "cancel" },
|
||||
{ kind: "back", destination: "bindings" },
|
||||
{ kind: "next", label: msg("Create Application"), destination: "here" },
|
||||
];
|
||||
|
||||
const forSubmit: WizardButton[] = [{ kind: "close" }];
|
||||
@@ -277,7 +289,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
state: string,
|
||||
label: string,
|
||||
icons: string[],
|
||||
extraInfo: MaybeTemplateResult = nothing,
|
||||
extraInfo: SlottedTemplateResult = nothing,
|
||||
) {
|
||||
const icon = classMap(icons.reduce((acc, icon) => ({ ...acc, [icon]: true }), {}));
|
||||
|
||||
@@ -385,14 +397,14 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
</dt>
|
||||
</div>
|
||||
${metaLaunchUrl
|
||||
? html` <div class="pf-c-description-list__group">
|
||||
? html`<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">${msg("Launch URL")}</dt>
|
||||
<dt class="pf-c-description-list__description">${metaLaunchUrl}</dt>
|
||||
</div>`
|
||||
: nothing}
|
||||
</dl>
|
||||
${renderer
|
||||
? html` <h2 class="pf-c-title pf-m-xl pf-u-pt-xl">${msg("Provider")}</h2>
|
||||
? html`<h2 class="pf-c-title pf-m-xl pf-u-pt-xl">${msg("Provider")}</h2>
|
||||
${renderer(provider)}`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export class ApplicationWizardBindingsToolbar extends AKElement {
|
||||
static styles = [PFButton, PFToolbar];
|
||||
|
||||
@property({ type: Boolean, attribute: "can-delete", reflect: true })
|
||||
canDelete = false;
|
||||
public canDelete = false;
|
||||
|
||||
notify(eventName: string) {
|
||||
this.dispatchEvent(new Event(eventName, { bubbles: true, composed: true }));
|
||||
|
||||
@@ -5,27 +5,35 @@ import "#components/ak-text-input";
|
||||
import "#elements/forms/FormGroup";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
|
||||
import { styles as ApplicationWizardStyles } from "../../ApplicationWizardFormStepStyles.styles.js";
|
||||
import { type ApplicationWizardState, type OneOfProvider } from "../../types.js";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { serializeForm } from "#elements/forms/Form";
|
||||
import { serializeForm } from "#elements/forms/serialization";
|
||||
|
||||
import { ApplicationWizardStyles } from "#admin/applications/wizard/ApplicationWizardFormStepStyles.styles";
|
||||
import {
|
||||
ApplicationTransactionValidationError,
|
||||
type ApplicationWizardState,
|
||||
ApplicationWizardStateError,
|
||||
type OneOfProvider,
|
||||
} from "#admin/applications/wizard/steps/providers/shared";
|
||||
|
||||
import { snakeCase } from "change-case";
|
||||
|
||||
import { CSSResult } from "lit";
|
||||
import { property, query } from "lit/decorators.js";
|
||||
|
||||
export abstract class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKElement {
|
||||
export abstract class ApplicationWizardProviderForm<
|
||||
P extends OneOfProvider,
|
||||
E extends ApplicationWizardStateError = ApplicationTransactionValidationError,
|
||||
> extends AKElement {
|
||||
static styles: CSSResult[] = [...ApplicationWizardStyles];
|
||||
|
||||
label = "";
|
||||
public abstract label: string;
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
wizard!: ApplicationWizardState;
|
||||
public wizard!: ApplicationWizardState<P, E>;
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
errors: Record<string | number | symbol, string> = {};
|
||||
public errors: E = {} as E;
|
||||
|
||||
@query("form#providerform")
|
||||
public form!: HTMLFormElement | null;
|
||||
@@ -42,23 +50,20 @@ export abstract class ApplicationWizardProviderForm<T extends OneOfProvider> ext
|
||||
}
|
||||
|
||||
get valid() {
|
||||
this.errors = {};
|
||||
this.errors = {} as E;
|
||||
|
||||
return !!this.form?.checkValidity();
|
||||
}
|
||||
|
||||
errorMessages(name: string) {
|
||||
return name in this.errors
|
||||
? [this.errors[name]]
|
||||
: (this.wizard.errors?.provider?.[name] ??
|
||||
this.wizard.errors?.provider?.[snakeCase(name)] ??
|
||||
[]);
|
||||
}
|
||||
errorMessages<T extends Extract<keyof E, string>>(name: T): Array<E[T]> {
|
||||
if (name in this.errors) {
|
||||
return [this.errors[name]];
|
||||
}
|
||||
|
||||
isValid(name: keyof T) {
|
||||
return !(
|
||||
(this.wizard.errors?.provider?.[name as string] ?? []).length > 0 ||
|
||||
this.errors?.[name] !== undefined
|
||||
return (
|
||||
this.wizard.errors?.provider?.[name] ??
|
||||
this.wizard.errors?.provider?.[snakeCase(name) as keyof E] ??
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import "#admin/applications/wizard/ak-wizard-title";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
|
||||
import { ValidationRecord } from "#admin/applications/wizard/types";
|
||||
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
|
||||
import {
|
||||
ApplicationTransactionValidationError,
|
||||
WizardValidationRecord,
|
||||
} from "#admin/applications/wizard/steps/providers/shared";
|
||||
import { renderForm } from "#admin/providers/ldap/LDAPProviderFormForm";
|
||||
|
||||
import type { LDAPProvider } from "@goauthentik/api";
|
||||
@@ -15,11 +17,11 @@ import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-application-wizard-provider-for-ldap")
|
||||
export class ApplicationWizardLdapProviderForm extends WithBrandConfig(
|
||||
ApplicationWizardProviderForm<LDAPProvider>,
|
||||
ApplicationWizardProviderForm<LDAPProvider, ApplicationTransactionValidationError>,
|
||||
) {
|
||||
label = msg("Configure LDAP Provider");
|
||||
|
||||
renderForm(provider: LDAPProvider, errors: ValidationRecord) {
|
||||
renderForm(provider: LDAPProvider, errors: WizardValidationRecord) {
|
||||
return html`
|
||||
<ak-wizard-title>${this.label}</ak-wizard-title>
|
||||
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
|
||||
@@ -32,10 +34,7 @@ export class ApplicationWizardLdapProviderForm extends WithBrandConfig(
|
||||
if (!(this.wizard.provider && this.wizard.errors)) {
|
||||
throw new Error("LDAP Provider Step received uninitialized wizard context.");
|
||||
}
|
||||
return this.renderForm(
|
||||
this.wizard.provider as LDAPProvider,
|
||||
this.wizard.errors.provider ?? {},
|
||||
);
|
||||
return this.renderForm(this.wizard.provider, this.wizard.errors.provider ?? {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import "#admin/applications/wizard/ak-wizard-title";
|
||||
|
||||
import { ApplicationTransactionValidationError } from "../../types.js";
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
|
||||
import { ApplicationTransactionValidationError } from "#admin/applications/wizard/steps/providers/shared";
|
||||
import { renderForm } from "#admin/providers/oauth2/OAuth2ProviderFormForm";
|
||||
|
||||
import {
|
||||
type OAuth2Provider,
|
||||
OAuth2ProviderRequest,
|
||||
type PaginatedOAuthSourceList,
|
||||
SourcesApi,
|
||||
} from "@goauthentik/api";
|
||||
import { type OAuth2Provider, type PaginatedOAuthSourceList, SourcesApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-application-wizard-provider-for-oauth")
|
||||
export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProviderForm<OAuth2ProviderRequest> {
|
||||
export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProviderForm<OAuth2Provider> {
|
||||
label = msg("Configure OAuth2 Provider");
|
||||
|
||||
@state()
|
||||
@@ -67,7 +61,7 @@ export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProvid
|
||||
if (!(this.wizard.provider && this.wizard.errors)) {
|
||||
throw new Error("Oauth2 Provider Step received uninitialized wizard context.");
|
||||
}
|
||||
return this.renderForm(this.wizard.provider as OAuth2Provider, this.wizard.errors);
|
||||
return this.renderForm(this.wizard.provider, this.wizard.errors);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import "#admin/applications/wizard/ak-wizard-title";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
import { WizardUpdateEvent } from "#components/ak-wizard/events";
|
||||
|
||||
import { ValidationRecord } from "#admin/applications/wizard/types";
|
||||
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
|
||||
import { WizardValidationRecord } from "#admin/applications/wizard/steps/providers/shared";
|
||||
import {
|
||||
ProxyModeValue,
|
||||
renderForm,
|
||||
@@ -25,7 +24,7 @@ export class ApplicationWizardProxyProviderForm extends ApplicationWizardProvide
|
||||
@state()
|
||||
showHttpBasic = true;
|
||||
|
||||
renderForm(provider: ProxyProvider, errors: ValidationRecord) {
|
||||
protected renderForm(provider: ProxyProvider, errors: WizardValidationRecord = {}) {
|
||||
const onSetMode: SetMode = (ev: CustomEvent<ProxyModeValue>) => {
|
||||
this.dispatchEvent(
|
||||
new WizardUpdateEvent({ ...this.wizard, proxyMode: ev.detail.value }),
|
||||
@@ -59,10 +58,7 @@ export class ApplicationWizardProxyProviderForm extends ApplicationWizardProvide
|
||||
if (!(this.wizard.provider && this.wizard.errors)) {
|
||||
throw new Error("Proxy Provider Step received uninitialized wizard context.");
|
||||
}
|
||||
return this.renderForm(
|
||||
this.wizard.provider as ProxyProvider,
|
||||
this.wizard.errors?.provider ?? {},
|
||||
);
|
||||
return this.renderForm(this.wizard.provider, this.wizard.errors?.provider);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ import "#components/ak-text-input";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/ak-dual-select/ak-dual-select-dynamic-selected-provider";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
|
||||
import {
|
||||
propertyMappingsProvider,
|
||||
propertyMappingsSelector,
|
||||
@@ -84,7 +83,7 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF
|
||||
if (!(this.wizard.provider && this.wizard.errors)) {
|
||||
throw new Error("RAC Provider Step received uninitialized wizard context.");
|
||||
}
|
||||
return this.renderForm(this.wizard.provider as RACProvider);
|
||||
return this.renderForm(this.wizard.provider);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import "#admin/applications/wizard/ak-wizard-title";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
|
||||
import { ValidationRecord } from "#admin/applications/wizard/types";
|
||||
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
|
||||
import { WizardValidationRecord } from "#admin/applications/wizard/steps/providers/shared";
|
||||
import { renderForm } from "#admin/providers/radius/RadiusProviderFormForm";
|
||||
|
||||
import { RadiusProvider } from "@goauthentik/api";
|
||||
@@ -19,7 +18,7 @@ export class ApplicationWizardRadiusProviderForm extends WithBrandConfig(
|
||||
) {
|
||||
label = msg("Configure Radius Provider");
|
||||
|
||||
renderForm(provider: RadiusProvider, errors: ValidationRecord) {
|
||||
renderForm(provider: RadiusProvider, errors: WizardValidationRecord = {}) {
|
||||
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
|
||||
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
|
||||
${renderForm({ provider, errors, brand: this.brand })}
|
||||
@@ -30,10 +29,7 @@ export class ApplicationWizardRadiusProviderForm extends WithBrandConfig(
|
||||
if (!(this.wizard.provider && this.wizard.errors)) {
|
||||
throw new Error("RAC Provider Step received uninitialized wizard context.");
|
||||
}
|
||||
return this.renderForm(
|
||||
this.wizard.provider as RadiusProvider,
|
||||
this.wizard.errors?.provider ?? {},
|
||||
);
|
||||
return this.renderForm(this.wizard.provider, this.wizard.errors?.provider);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import "#admin/applications/wizard/ak-wizard-title";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
import { createFileMap } from "#elements/utils/inputs";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
|
||||
import { renderForm } from "#admin/providers/saml/SAMLProviderImportFormForm";
|
||||
|
||||
import type { ProvidersSamlImportMetadataCreateRequest } from "@goauthentik/api";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import "#admin/applications/wizard/ak-wizard-title";
|
||||
import "#elements/forms/FormGroup";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
|
||||
import { type AkCryptoCertificateSearch } from "#admin/common/ak-crypto-certificate-search";
|
||||
import { renderForm } from "#admin/providers/saml/SAMLProviderFormForm";
|
||||
|
||||
@@ -84,7 +83,7 @@ export class ApplicationWizardProviderSamlForm extends ApplicationWizardProvider
|
||||
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
|
||||
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
|
||||
${renderForm({
|
||||
provider: this.wizard.provider as SAMLProvider,
|
||||
provider: this.wizard.provider,
|
||||
errors: this.wizard.errors?.provider,
|
||||
setHasSigningKp,
|
||||
hasSigningKp: this.hasSigningKp,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import "#admin/applications/wizard/ak-wizard-title";
|
||||
import "#elements/forms/FormGroup";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
|
||||
import { renderForm } from "#admin/providers/scim/SCIMProviderFormForm";
|
||||
|
||||
import { PaginatedSCIMMappingList, type SCIMProvider } from "@goauthentik/api";
|
||||
@@ -23,7 +22,7 @@ export class ApplicationWizardSCIMProvider extends ApplicationWizardProviderForm
|
||||
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
|
||||
${renderForm({
|
||||
update: this.requestUpdate.bind(this),
|
||||
provider: this.wizard.provider as SCIMProvider,
|
||||
provider: this.wizard.provider,
|
||||
errors: this.wizard.errors.provider,
|
||||
})}
|
||||
</form>`;
|
||||
|
||||
@@ -13,27 +13,34 @@ import {
|
||||
type ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
export type OneOfProvider =
|
||||
| Partial<SCIMProviderRequest>
|
||||
| Partial<SAMLProviderRequest>
|
||||
| Partial<ProvidersSamlImportMetadataCreateRequest>
|
||||
| Partial<RACProviderRequest>
|
||||
| Partial<RadiusProviderRequest>
|
||||
| Partial<ProxyProviderRequest>
|
||||
| Partial<OAuth2ProviderRequest>
|
||||
| Partial<LDAPProviderRequest>;
|
||||
export type OneOfProvider = Partial<
|
||||
| SCIMProviderRequest
|
||||
| SAMLProviderRequest
|
||||
| ProvidersSamlImportMetadataCreateRequest
|
||||
| RACProviderRequest
|
||||
| RadiusProviderRequest
|
||||
| ProxyProviderRequest
|
||||
| OAuth2ProviderRequest
|
||||
| LDAPProviderRequest
|
||||
>;
|
||||
|
||||
export type ValidationRecord = { [key: string]: string[] };
|
||||
export type WizardValidationRecord<K extends PropertyKey = string> = {
|
||||
[key in K]: string[] | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* An error that occurs during the creation or modification of an application.
|
||||
*
|
||||
* @todo (Elf) Extend this type to include all possible errors that can occur during the creation or modification of an application.
|
||||
*/
|
||||
export interface ApplicationTransactionValidationError extends ValidationError {
|
||||
app?: ValidationRecord;
|
||||
provider?: ValidationRecord;
|
||||
bindings?: ValidationRecord;
|
||||
export interface ApplicationTransactionValidationError extends Pick<
|
||||
ValidationError,
|
||||
"code" | "nonFieldErrors"
|
||||
> {
|
||||
app?: WizardValidationRecord;
|
||||
name?: WizardValidationRecord;
|
||||
provider?: WizardValidationRecord;
|
||||
bindings?: WizardValidationRecord;
|
||||
detail?: unknown;
|
||||
}
|
||||
|
||||
@@ -50,20 +57,25 @@ export function isApplicationTransactionValidationError(
|
||||
return false;
|
||||
}
|
||||
|
||||
export type ApplicationWizardStateError = ValidationError | ApplicationTransactionValidationError;
|
||||
|
||||
// We use the PolicyBinding instead of the PolicyBindingRequest here, because that gives us a slot
|
||||
// in which to preserve the retrieved policy, group, or user object from the SearchSelect used to
|
||||
// find it, which in turn allows us to create a user-friendly display of bindings on the "List of
|
||||
// configured bindings" page in the wizard. The PolicyBinding is converted into a
|
||||
// PolicyBindingRequest during the submission phase.
|
||||
|
||||
export interface ApplicationWizardState {
|
||||
export interface ApplicationWizardState<
|
||||
P extends OneOfProvider = OneOfProvider,
|
||||
E = ApplicationTransactionValidationError,
|
||||
> {
|
||||
app: Partial<ApplicationRequest>;
|
||||
providerModel: string;
|
||||
provider: OneOfProvider;
|
||||
provider: P;
|
||||
proxyMode: ProxyMode;
|
||||
bindings: PolicyBinding[];
|
||||
currentBinding: number;
|
||||
errors: ValidationError | ApplicationTransactionValidationError;
|
||||
errors: E;
|
||||
}
|
||||
|
||||
export interface ApplicationWizardStateUpdate {
|
||||
@@ -156,7 +156,7 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
|
||||
html`<ak-status-label ?good=${item.enabled}></ak-status-label>`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Blueprint")}</span>
|
||||
<ak-blueprint-form slot="form" .instancePk=${item.pk}> </ak-blueprint-form>
|
||||
<button
|
||||
|
||||
@@ -79,7 +79,7 @@ export class BrandListPage extends TablePage<Brand> {
|
||||
html`<ak-status-label ?good=${item._default}></ak-status-label>`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Brand")}</span>
|
||||
<ak-brand-form slot="form" .instancePk=${item.brandUuid}> </ak-brand-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
|
||||
@@ -115,7 +115,7 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
|
||||
html`<ak-label color=${color}> ${item.certExpiry?.toLocaleString()} </ak-label>`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Certificate-Key Pair")}</span>
|
||||
<ak-crypto-certificate-form slot="form" .instancePk=${item.pk}>
|
||||
</ak-crypto-certificate-form>
|
||||
|
||||
@@ -42,7 +42,7 @@ export class DeviceAccessGroupsListPage extends TablePage<DeviceAccessGroup> {
|
||||
html`${item.name}`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Group")}</span>
|
||||
<ak-endpoints-device-access-groups-form slot="form" .instancePk=${item.pbmUuid}>
|
||||
</ak-endpoints-device-access-groups-form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "#admin/common/ak-license-notice";
|
||||
import "#elements/LicenseNotice";
|
||||
import "#admin/endpoints/connectors/agent/AgentConnectorForm";
|
||||
import "#admin/endpoints/connectors/fleet/FleetConnectorForm";
|
||||
import "#admin/endpoints/connectors/gdtc/GoogleChromeConnectorForm";
|
||||
|
||||
@@ -51,7 +51,7 @@ export class ConnectorsListPage extends TablePage<Connector> {
|
||||
${StrictUnsafe<CustomFormElementTagName>(item.component, {
|
||||
slot: "form",
|
||||
instancePk: item.connectorUuid,
|
||||
actionLabel: msg("Update"),
|
||||
submitLabel: msg("Save Changes"),
|
||||
headline: msg(str`Update ${item.verboseName}`, {
|
||||
id: "form.headline.update",
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "#elements/Tabs";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#admin/endpoints/connectors/agent/EnrollmentTokenListPage";
|
||||
import "#admin/endpoints/connectors/agent/AgentConnectorSetup";
|
||||
|
||||
@@ -86,11 +86,16 @@ export class ConfigModal extends ModalButton {
|
||||
></ak-codemirror>
|
||||
</ak-expand>
|
||||
</div>
|
||||
<footer class="pf-c-modal-box__footer pf-m-align-left">
|
||||
<ak-action-button class="pf-m-primary" .apiRequest=${this.#downloadConnectorConfig}>
|
||||
${msg("Download")}
|
||||
</ak-action-button>
|
||||
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
@click=${() => {
|
||||
this.open = false;
|
||||
}}
|
||||
>
|
||||
${msg("Close")}
|
||||
</button>
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => {
|
||||
@@ -107,16 +112,10 @@ export class ConfigModal extends ModalButton {
|
||||
>
|
||||
${msg("Copy")}
|
||||
</ak-action-button>
|
||||
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
@click=${() => {
|
||||
this.open = false;
|
||||
}}
|
||||
>
|
||||
${msg("Close")}
|
||||
</button>
|
||||
</footer>`;
|
||||
<ak-action-button class="pf-m-primary" .apiRequest=${this.#downloadConnectorConfig}>
|
||||
${msg("Download")}
|
||||
</ak-action-button>
|
||||
</fieldset>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export class EnrollmentTokenListPage extends Table<EnrollmentToken> {
|
||||
Timestamp(item.expires && item.expiring ? item.expires : null),
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Enrollment Token")}</span>
|
||||
<ak-endpoints-agent-enrollment-token-form
|
||||
slot="form"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#elements/Tabs";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#elements/tasks/ScheduleList";
|
||||
import "#elements/tasks/TaskList";
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#elements/Tabs";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#elements/tasks/ScheduleList";
|
||||
import "#elements/tasks/TaskList";
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import "#elements/forms/ModalForm";
|
||||
import "#admin/endpoints/devices/DeviceUserBindingForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PolicyBindingCheckTarget, PolicyBindingCheckTargetToLabel } from "#common/policies/utils";
|
||||
|
||||
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { BoundPoliciesList } from "#admin/policies/BoundPoliciesList";
|
||||
import { PolicyBindingCheckTarget, PolicyBindingCheckTargetToLabel } from "#admin/policies/utils";
|
||||
|
||||
import { DeviceUserBinding, EndpointsApi } from "@goauthentik/api";
|
||||
|
||||
|
||||
@@ -65,7 +65,8 @@ export class DeviceAddHowTo extends ModalButton {
|
||||
})}
|
||||
</ak-tabs>`}
|
||||
</div>
|
||||
<footer class="pf-c-modal-box__footer pf-m-align-left">
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
@click=${() => {
|
||||
@@ -74,7 +75,7 @@ export class DeviceAddHowTo extends ModalButton {
|
||||
>
|
||||
${msg("Close")}
|
||||
</button>
|
||||
</footer>`;
|
||||
</fieldset>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ export class DeviceListPage extends TablePage<EndpointDevice> {
|
||||
html`${item.accessGroupObj?.name || "-"}`,
|
||||
item.facts.created ? Timestamp(item.facts.created) : html`-`,
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Device")}</span>
|
||||
<ak-endpoints-device-form slot="form" .instancePk=${item.deviceUuid}>
|
||||
</ak-endpoints-device-form>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import "#components/ak-switch-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PolicyBindingCheckTarget } from "#common/policies/utils";
|
||||
|
||||
import { PolicyBindingForm } from "#admin/policies/PolicyBindingForm";
|
||||
import { PolicyBindingCheckTarget } from "#admin/policies/utils";
|
||||
|
||||
import { DeviceUserBinding, EndpointsApi, PolicyBinding } from "@goauthentik/api";
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export class DeviceViewPage extends AKElement {
|
||||
[
|
||||
msg("Actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Device")}</span>
|
||||
<ak-endpoints-device-form
|
||||
slot="form"
|
||||
|
||||
@@ -221,7 +221,7 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
|
||||
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update License")}</span>
|
||||
<ak-enterprise-license-form slot="form" .instancePk=${item.licenseUuid}>
|
||||
</ak-enterprise-license-form>
|
||||
|
||||
@@ -89,7 +89,7 @@ export class RuleListPage extends TablePage<NotificationRule> {
|
||||
: msg("-")}`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Notification Rule")}</span>
|
||||
<ak-event-rule-form slot="form" .instancePk=${item.pk}> </ak-event-rule-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
|
||||
@@ -80,7 +80,7 @@ export class TransportListPage extends TablePage<NotificationTransport> {
|
||||
html`${item.modeVerbose}`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Notification Transport")}</span>
|
||||
<ak-event-transport-form slot="form" .instancePk=${item.pk}>
|
||||
</ak-event-transport-form>
|
||||
|
||||
@@ -90,7 +90,7 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||
${StrictUnsafe<CustomFormElementTagName>(item.stageObj?.component, {
|
||||
slot: "form",
|
||||
instancePk: item.stageObj?.pk,
|
||||
actionLabel: msg("Update"),
|
||||
submitLabel: msg("Save Changes"),
|
||||
headline: msg(str`Update ${item.stageObj?.verboseName}`, {
|
||||
id: "form.headline.update",
|
||||
}),
|
||||
@@ -100,7 +100,7 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Stage binding")}</span>
|
||||
<ak-stage-binding-form slot="form" .instancePk=${item.pk}>
|
||||
</ak-stage-binding-form>
|
||||
|
||||
@@ -25,6 +25,8 @@ import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
|
||||
@customElement("ak-flow-list")
|
||||
export class FlowListPage extends TablePage<Flow> {
|
||||
static styles = [...super.styles, PFBanner];
|
||||
|
||||
protected override searchEnabled = true;
|
||||
public pageTitle = msg("Flows");
|
||||
public pageDescription = msg(
|
||||
@@ -38,8 +40,6 @@ export class FlowListPage extends TablePage<Flow> {
|
||||
@property()
|
||||
order = "slug";
|
||||
|
||||
static styles = [...super.styles, PFBanner];
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<Flow>> {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(await this.defaultEndpointConfig());
|
||||
}
|
||||
@@ -86,12 +86,12 @@ export class FlowListPage extends TablePage<Flow> {
|
||||
<code>${item.slug}</code>
|
||||
</a>
|
||||
<small>${item.title}</small>`,
|
||||
html`${item.name}`,
|
||||
html`${Array.from(item.stages || []).length}`,
|
||||
html`${Array.from(item.policies || []).length}`,
|
||||
item.name,
|
||||
Array.from(item.stages || []).length,
|
||||
Array.from(item.policies || []).length,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Flow")}</span>
|
||||
<ak-flow-form slot="form" .instancePk=${item.slug}> </ak-flow-form>
|
||||
<button
|
||||
@@ -134,10 +134,10 @@ export class FlowListPage extends TablePage<Flow> {
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html`
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Create")}</span>
|
||||
<span slot="header">${msg("Create Flow")}</span>
|
||||
<span slot="submit">${msg("Create Flow")}</span>
|
||||
<span slot="header">${msg("New Flow")}</span>
|
||||
<ak-flow-form slot="form"> </ak-flow-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("New Flow")}</button>
|
||||
</ak-forms-modal>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Import")}</span>
|
||||
@@ -161,7 +161,7 @@ export class FlowListPage extends TablePage<Flow> {
|
||||
<ak-forms-confirm
|
||||
successMessage=${msg("Successfully cleared flow cache")}
|
||||
errorMessage=${msg("Failed to delete flow cache")}
|
||||
action=${msg("Clear cache")}
|
||||
action=${msg("Clear Cache")}
|
||||
.onConfirm=${() => {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesCacheClearCreate();
|
||||
}}
|
||||
|
||||
@@ -3,7 +3,7 @@ import "#admin/flows/FlowDiagram";
|
||||
import "#admin/flows/FlowForm";
|
||||
import "#admin/policies/BoundPoliciesList";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/SpinnerButton/ak-spinner-button";
|
||||
|
||||
@@ -127,7 +127,9 @@ export class FlowViewPage extends AKElement {
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="submit"
|
||||
>${msg("Save Changes")}</span
|
||||
>
|
||||
<span slot="header">
|
||||
${msg("Update Flow")}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "#admin/groups/MemberSelectModal";
|
||||
import "#admin/groups/MemberSelectForm";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/ak-dual-select/ak-dual-select-provider";
|
||||
import "#elements/chips/Chip";
|
||||
@@ -43,6 +43,9 @@ export class GroupForm extends ModelForm<Group, string> {
|
||||
`,
|
||||
];
|
||||
|
||||
public entitySingular = msg("Group");
|
||||
public entityPlural = msg("Groups");
|
||||
|
||||
#fetchGroups = (page: number, search?: string): Promise<DataProvision> => {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreGroupsList({
|
||||
|
||||
@@ -11,6 +11,8 @@ import { PaginatedResponse, TableColumn } from "#elements/table/Table";
|
||||
import { TablePage } from "#elements/table/TablePage";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { GroupForm } from "#admin/groups/GroupForm";
|
||||
|
||||
import { CoreApi, Group } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@@ -19,9 +21,11 @@ import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-group-list")
|
||||
export class GroupListPage extends TablePage<Group> {
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
protected override searchEnabled = true;
|
||||
|
||||
public override checkbox = true;
|
||||
public override clearOnRefresh = true;
|
||||
|
||||
public searchPlaceholder = msg("Search for a group by name…");
|
||||
public searchLabel = msg("Group Search");
|
||||
public pageTitle = msg("Groups");
|
||||
@@ -32,9 +36,9 @@ export class GroupListPage extends TablePage<Group> {
|
||||
public supportsQL = true;
|
||||
|
||||
@property()
|
||||
order = "name";
|
||||
public order = "name";
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<Group>> {
|
||||
protected async apiEndpoint(): Promise<PaginatedResponse<Group>> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
includeUsers: false,
|
||||
@@ -48,7 +52,7 @@ export class GroupListPage extends TablePage<Group> {
|
||||
[msg("Actions"), null, msg("Row Actions")],
|
||||
];
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
protected renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
object-label=${msg("Group(s)")}
|
||||
@@ -70,7 +74,7 @@ export class GroupListPage extends TablePage<Group> {
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: Group): SlottedTemplateResult[] {
|
||||
protected row(item: Group): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<a
|
||||
href="#/identity/groups/${item.pk}"
|
||||
@@ -80,29 +84,23 @@ export class GroupListPage extends TablePage<Group> {
|
||||
html`${Array.from(item.users || []).length}`,
|
||||
html`<ak-status-label type="neutral" ?good=${item.isSuperuser}></ak-status-label>`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="header">${msg("Update Group")}</span>
|
||||
<ak-group-form slot="form" .instancePk=${item.pk}> </ak-group-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
aria-label=${msg(str`Edit "${item.name}"`)}
|
||||
${GroupForm.asEditModalInvoker(item.pk)}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</div>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html`
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Create Group")}</span>
|
||||
<span slot="header">${msg("New Group")}</span>
|
||||
<ak-group-form slot="form"> </ak-group-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("New Group")}</button>
|
||||
</ak-forms-modal>
|
||||
`;
|
||||
protected renderObjectCreate(): TemplateResult {
|
||||
return html`<button class="pf-c-button pf-m-primary" ${GroupForm.asModalInvoker()}>
|
||||
${msg("New Group")}
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import "#admin/roles/RelatedRoleList";
|
||||
import "#components/ak-object-attributes-card";
|
||||
import "#admin/lifecycle/ObjectLifecyclePage";
|
||||
import "#components/ak-status-label";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/ActionButton/index";
|
||||
@@ -181,7 +181,7 @@ export class GroupViewPage extends WithLicenseSummary(AKElement) {
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Group")}</span>
|
||||
<ak-group-form slot="form" .instancePk=${this.group.pk}>
|
||||
</ak-group-form>
|
||||
|
||||
@@ -3,8 +3,7 @@ import "#elements/buttons/SpinnerButton/index";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { PaginatedResponse, TableColumn, Timestamp } from "#elements/table/Table";
|
||||
import { TableModal } from "#elements/table/TableModal";
|
||||
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { CoreApi, CoreUsersListRequest, User } from "@goauthentik/api";
|
||||
@@ -12,19 +11,16 @@ import { CoreApi, CoreUsersListRequest, User } from "@goauthentik/api";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { css, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
// Leaving room in the future for a multi-state control if someone somehow needs to filter inactive
|
||||
// users as well.
|
||||
type UserListFilter = "active" | "all";
|
||||
type UserListRequestFilter = Partial<Pick<CoreUsersListRequest, "isActive">>;
|
||||
|
||||
@customElement("ak-group-member-select-table")
|
||||
export class MemberSelectTable extends TableModal<User> {
|
||||
public override searchPlaceholder = msg("Search for users by username or display name...");
|
||||
public override searchLabel = msg("Search Users");
|
||||
public override label = msg("Select Users");
|
||||
@customElement("ak-group-member-select-form")
|
||||
export class MemberSelectForm extends Table<User> {
|
||||
static styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
@@ -37,16 +33,17 @@ export class MemberSelectTable extends TableModal<User> {
|
||||
}
|
||||
`,
|
||||
];
|
||||
public supportsQL = true;
|
||||
|
||||
checkbox = true;
|
||||
checkboxChip = true;
|
||||
public override searchPlaceholder = msg("Search for users by username or display name...");
|
||||
public override searchLabel = msg("Search Users");
|
||||
public override label = msg("Select Users");
|
||||
public overridesupportsQL = true;
|
||||
|
||||
public override checkbox = true;
|
||||
public override checkboxChip = true;
|
||||
|
||||
protected override searchEnabled = true;
|
||||
|
||||
@property()
|
||||
confirm!: (selectedItems: User[]) => Promise<unknown>;
|
||||
|
||||
userListFilter: UserListFilter = "active";
|
||||
|
||||
order = "username";
|
||||
@@ -115,41 +112,13 @@ export class MemberSelectTable extends TableModal<User> {
|
||||
];
|
||||
}
|
||||
|
||||
renderSelectedChip(item: User): TemplateResult {
|
||||
return html`${item.username}`;
|
||||
}
|
||||
|
||||
renderModalInner(): TemplateResult {
|
||||
return html`<div class="pf-c-modal-box__header pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1 id="modal-title" class="pf-c-title pf-m-2xl">${msg("Select users")}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body pf-m-light">${this.renderTable()}</div>
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<ak-spinner-button
|
||||
.callAction=${() => {
|
||||
return this.confirm(this.selectedElements).then(() => {
|
||||
this.open = false;
|
||||
});
|
||||
}}
|
||||
class="pf-m-primary"
|
||||
>${msg("Confirm")}</ak-spinner-button
|
||||
>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary"
|
||||
>${msg("Cancel")}</ak-spinner-button
|
||||
>
|
||||
</fieldset>`;
|
||||
renderSelectedChip(item: User): SlottedTemplateResult {
|
||||
return item.username;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-group-member-select-table": MemberSelectTable;
|
||||
"ak-group-member-select-form": MemberSelectForm;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import "#admin/groups/GroupForm";
|
||||
import "#admin/users/GroupSelectModal";
|
||||
import "#admin/users/UserGroupSelectForm";
|
||||
import "#components/ak-status-label";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
@@ -9,7 +9,8 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { AKFormSubmitEvent, Form } from "#elements/forms/Form";
|
||||
import { renderModal } from "#elements/modals/utils";
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
@@ -23,16 +24,16 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@customElement("ak-group-related-add")
|
||||
export class RelatedGroupAdd extends Form<{ groups: string[] }> {
|
||||
@property({ attribute: false })
|
||||
user?: User;
|
||||
public user?: User;
|
||||
|
||||
@state()
|
||||
groupsToAdd: Group[] = [];
|
||||
public groupsToAdd: Group[] = [];
|
||||
|
||||
getSuccessMessage(): string {
|
||||
public override getSuccessMessage(): string {
|
||||
return msg("Successfully added user to group(s).");
|
||||
}
|
||||
|
||||
async send(data: { groups: string[] }): Promise<unknown> {
|
||||
protected async send(data: { groups: string[] }): Promise<unknown> {
|
||||
await Promise.all(
|
||||
data.groups.map((group) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsAddUserCreate({
|
||||
@@ -43,25 +44,35 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
protected openUserGroupSelectModal = () => {
|
||||
return renderModal(html`
|
||||
<ak-form
|
||||
headline=${msg("Select Groups")}
|
||||
action-label=${msg("Confirm")}
|
||||
@submit=${(event: AKFormSubmitEvent<Group[]>) => {
|
||||
this.groupsToAdd = event.target.toJSON();
|
||||
}}
|
||||
><ak-user-group-select-form></ak-user-group-select-form>
|
||||
</ak-form>
|
||||
`);
|
||||
};
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${msg("Groups to add")} name="groups">
|
||||
<div class="pf-c-input-group">
|
||||
<ak-user-group-select-table
|
||||
.confirm=${(items: Group[]) => {
|
||||
this.groupsToAdd = items;
|
||||
this.requestUpdate();
|
||||
return Promise.resolve();
|
||||
}}
|
||||
<button
|
||||
class="pf-c-button pf-m-control"
|
||||
type="button"
|
||||
@click=${this.openUserGroupSelectModal}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
|
||||
<pf-tooltip position="top" content=${msg("Add group")}>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-user-group-select-table>
|
||||
<pf-tooltip position="top" content=${msg("Add group")}>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group>
|
||||
${this.groupsToAdd.map((group) => {
|
||||
@@ -141,7 +152,7 @@ export class RelatedGroupList extends Table<Group> {
|
||||
html`<a href="#/identity/groups/${item.pk}">${item.name}</a>`,
|
||||
html`<ak-status-label type="neutral" ?good=${item.isSuperuser}></ak-status-label>`,
|
||||
html` <ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Group")}</span>
|
||||
<ak-group-form slot="form" .instancePk=${item.pk}> </ak-group-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "#admin/groups/MemberSelectModal";
|
||||
import "#admin/groups/MemberSelectForm";
|
||||
import "#admin/users/ServiceAccountForm";
|
||||
import "#admin/users/UserActiveForm";
|
||||
import "#admin/users/UserForm";
|
||||
@@ -14,11 +14,11 @@ import "#elements/forms/ModalForm";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { AKFormSubmitEvent, Form } from "#elements/forms/Form";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { renderModal } from "#elements/modals/utils";
|
||||
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
|
||||
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
@@ -26,9 +26,19 @@ import { UserOption } from "#elements/user/utils";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { UserForm } from "#admin/users/UserForm";
|
||||
import { UserImpersonateForm } from "#admin/users/UserImpersonateForm";
|
||||
import { renderRecoveryButtons } from "#admin/users/UserListPage";
|
||||
|
||||
import { CoreApi, CoreUsersListTypeEnum, Group, RbacApi, Role, User } from "@goauthentik/api";
|
||||
import {
|
||||
CapabilitiesEnum,
|
||||
CoreApi,
|
||||
CoreUsersListTypeEnum,
|
||||
Group,
|
||||
RbacApi,
|
||||
Role,
|
||||
User,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
@@ -36,11 +46,13 @@ import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
@customElement("ak-user-related-add")
|
||||
export class RelatedUserAdd extends Form<{ users: number[] }> {
|
||||
@customElement("ak-add-related-user-form")
|
||||
export class AddRelatedUserForm extends Form<{ users: number[] }> {
|
||||
public override headline = msg("Assign Additional Users");
|
||||
public override submitLabel = msg("Assign");
|
||||
|
||||
@property({ attribute: false })
|
||||
public targetGroup: Group | null = null;
|
||||
|
||||
@@ -50,7 +62,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
|
||||
@state()
|
||||
usersToAdd: User[] = [];
|
||||
|
||||
getSuccessMessage(): string {
|
||||
public override getSuccessMessage(): string {
|
||||
return msg("Successfully added user(s).");
|
||||
}
|
||||
|
||||
@@ -79,6 +91,21 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
|
||||
return data;
|
||||
}
|
||||
|
||||
protected openUserSelectionModal = () => {
|
||||
return renderModal(html`
|
||||
<ak-form
|
||||
headline=${msg("Select users")}
|
||||
action-label=${msg("Confirm")}
|
||||
@submit=${(event: AKFormSubmitEvent<User[]>) => {
|
||||
this.usersToAdd = event.target.toJSON();
|
||||
}}
|
||||
><ak-group-member-select-form></ak-group-member-select-form>
|
||||
</ak-form>
|
||||
`);
|
||||
};
|
||||
|
||||
//#region Rendering
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
// TODO: The `form-control-sibling` container is a workaround to get the
|
||||
// table to allow the table to appear as an inline-block element next to the input group.
|
||||
@@ -95,26 +122,18 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
|
||||
)}
|
||||
<div class="pf-c-input-group">
|
||||
<div class="form-control-sibling">
|
||||
<ak-group-member-select-table
|
||||
.confirm=${(items: User[]) => {
|
||||
this.usersToAdd = items;
|
||||
this.requestUpdate();
|
||||
return Promise.resolve();
|
||||
}}
|
||||
<button
|
||||
class="pf-c-button pf-m-control"
|
||||
type="button"
|
||||
id="assign-users-button"
|
||||
aria-haspopup="dialog"
|
||||
aria-label=${msg("Open user selection dialog")}
|
||||
@click=${this.openUserSelectionModal}
|
||||
>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-control"
|
||||
type="button"
|
||||
id="assign-users-button"
|
||||
aria-haspopup="dialog"
|
||||
aria-label=${msg("Open user selection dialog")}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Add users")}>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-group-member-select-table>
|
||||
<pf-tooltip position="top" content=${msg("Add users")}>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group>
|
||||
@@ -140,31 +159,38 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
|
||||
|
||||
@customElement("ak-user-related-list")
|
||||
export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Table<User>)) {
|
||||
public static styles: CSSResult[] = [...Table.styles, PFDescriptionList, PFAlert];
|
||||
|
||||
public override searchPlaceholder = msg("Search for users by username or display name...");
|
||||
public override searchLabel = msg("User Search");
|
||||
public override label = msg("Users");
|
||||
|
||||
expandable = true;
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
public override expandable = true;
|
||||
public override checkbox = true;
|
||||
public override clearOnRefresh = true;
|
||||
|
||||
protected override searchEnabled = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
targetGroup?: Group;
|
||||
public targetGroup: Group | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
targetRole?: Role;
|
||||
public targetRole: Role | null = null;
|
||||
|
||||
@property()
|
||||
order = "last_login";
|
||||
public override order = "last_login";
|
||||
|
||||
@property({ type: Boolean })
|
||||
hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
|
||||
public hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
|
||||
|
||||
static styles: CSSResult[] = [...Table.styles, PFDescriptionList, PFAlert, PFBanner];
|
||||
protected canImpersonate = false;
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.canImpersonate = this.can(CapabilitiesEnum.CanImpersonate);
|
||||
}
|
||||
|
||||
protected async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
||||
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
...(this.targetGroup && { groupsByPk: [this.targetGroup.pk] }),
|
||||
@@ -190,9 +216,10 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
[msg("Actions"), null, msg("Row Actions")],
|
||||
];
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
protected override renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
const targetLabel = this.targetGroup?.name || this.targetRole?.name;
|
||||
|
||||
return html`<ak-forms-delete-bulk
|
||||
object-label=${msg("User(s)")}
|
||||
action-label=${msg("Remove User(s)")}
|
||||
@@ -233,9 +260,9 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: User): SlottedTemplateResult[] {
|
||||
const canImpersonate =
|
||||
this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.currentUser?.pk;
|
||||
protected override row(item: User): SlottedTemplateResult[] {
|
||||
const showImpersonate = this.canImpersonate && item.pk !== this.currentUser?.pk;
|
||||
|
||||
return [
|
||||
html`<a href="#/identity/users/${item.pk}">
|
||||
<div>${item.username}</div>
|
||||
@@ -245,41 +272,29 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
Timestamp(item.lastLogin),
|
||||
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="header">${msg("Update User")}</span>
|
||||
<ak-user-form slot="form" .instancePk=${item.pk}> </ak-user-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${canImpersonate
|
||||
? html`
|
||||
<ak-forms-modal size=${PFSize.Medium} id="impersonate-request">
|
||||
<span slot="submit">${msg("Impersonate")}</span>
|
||||
<span slot="header">${msg("Impersonate")} ${item.username}</span>
|
||||
<ak-user-impersonate-form
|
||||
slot="form"
|
||||
.instancePk=${item.pk}
|
||||
></ak-user-impersonate-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-tertiary">
|
||||
<pf-tooltip
|
||||
position="top"
|
||||
content=${msg("Temporarily assume the identity of this user")}
|
||||
>
|
||||
<span>${msg("Impersonate")}</span>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
`
|
||||
: nothing}
|
||||
<button class="pf-c-button pf-m-plain" ${UserForm.asEditModalInvoker(item.pk)}>
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
${showImpersonate
|
||||
? html`<button
|
||||
class="pf-c-button pf-m-tertiary"
|
||||
${UserImpersonateForm.asEditModalInvoker(item.pk)}
|
||||
>
|
||||
<pf-tooltip
|
||||
position="top"
|
||||
content=${msg("Temporarily assume the identity of this user")}
|
||||
>
|
||||
<span>${msg("Impersonate")}</span>
|
||||
</pf-tooltip>
|
||||
</button>`
|
||||
: null}
|
||||
</div>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: User): TemplateResult {
|
||||
protected override renderExpanded(item: User): TemplateResult {
|
||||
return html`<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
@@ -335,39 +350,94 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
</dl>`;
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
protected openAddUserToTargetGroupModal = () => {
|
||||
const banner = this.targetGroup?.isSuperuser
|
||||
? html`<div class="pf-c-banner pf-m-warning" slot="before-body">
|
||||
${msg(
|
||||
"Warning: This group is configured with superuser access. Added users will have superuser access.",
|
||||
)}
|
||||
</div>`
|
||||
: nothing;
|
||||
|
||||
return renderModal(
|
||||
html`${banner}<ak-add-related-user-form .targetGroup=${this.targetGroup}>
|
||||
</ak-add-related-user-form>`,
|
||||
);
|
||||
};
|
||||
|
||||
protected openAddUserToTargetRoleModal = () => {
|
||||
return renderModal(
|
||||
html`<ak-add-related-user-form .targetRole=${this.targetRole}>
|
||||
</ak-add-related-user-form>`,
|
||||
);
|
||||
};
|
||||
|
||||
protected openNewUserToTargetGroupModal = () => {
|
||||
const banner = this.targetGroup
|
||||
? html`<div class="pf-c-banner pf-m-info" slot="before-body">
|
||||
${msg(str`This user will be added to the group "${this.targetGroup.name}".`)}
|
||||
</div>`
|
||||
: nothing;
|
||||
|
||||
return renderModal(
|
||||
html`${banner}<ak-user-form
|
||||
.targetGroup=${this.targetGroup}
|
||||
headline=${msg("New Group User")}
|
||||
></ak-user-form>`,
|
||||
);
|
||||
};
|
||||
|
||||
protected openNewUserToTargetRoleModal = () => {
|
||||
const banner = this.targetRole
|
||||
? html`<div class="pf-c-banner pf-m-info" slot="before-body">
|
||||
${msg(str`This user will be added to the role "${this.targetRole.name}".`)}
|
||||
</div>`
|
||||
: nothing;
|
||||
|
||||
return renderModal(
|
||||
html`${banner}<ak-user-form
|
||||
.targetRole=${this.targetRole}
|
||||
headline=${msg("New Role User")}
|
||||
></ak-user-form>`,
|
||||
);
|
||||
};
|
||||
|
||||
protected openNewServiceUserToTargetGroupModal = () => {
|
||||
const banner = this.targetGroup
|
||||
? html`<div class="pf-c-banner pf-m-info" slot="before-body">
|
||||
${msg(str`This user will be added to the group "${this.targetGroup.name}".`)}
|
||||
</div>`
|
||||
: nothing;
|
||||
|
||||
return renderModal(
|
||||
html`${banner}<ak-user-service-account-form
|
||||
.targetGroup=${this.targetGroup}
|
||||
></ak-user-service-account-form>`,
|
||||
{
|
||||
closedBy: "none",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
protected override renderToolbar(): TemplateResult {
|
||||
return html`
|
||||
${this.targetGroup
|
||||
? html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Assign")}</span>
|
||||
<span slot="header">${msg("Assign Additional Users")}</span>
|
||||
${this.targetGroup.isSuperuser
|
||||
? html`
|
||||
<div class="pf-c-banner pf-m-warning" slot="above-form">
|
||||
${msg(
|
||||
"Warning: This group is configured with superuser access. Added users will have superuser access.",
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ak-user-related-add .targetGroup=${this.targetGroup} slot="form">
|
||||
</ak-user-related-add>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Add existing user")}
|
||||
</button>
|
||||
</ak-forms-modal>`
|
||||
: nothing}
|
||||
? html`<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
@click=${this.openAddUserToTargetGroupModal}
|
||||
>
|
||||
${msg("Add Existing User")}
|
||||
</button>`
|
||||
: null}
|
||||
${this.targetRole
|
||||
? html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Assign")}</span>
|
||||
<span slot="header">${msg("Assign Additional Users")}</span>
|
||||
<ak-user-related-add .targetRole=${this.targetRole} slot="form">
|
||||
</ak-user-related-add>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Add existing user")}
|
||||
</button>
|
||||
</ak-forms-modal>`
|
||||
: nothing}
|
||||
? html`<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
@click=${this.openAddUserToTargetRoleModal}
|
||||
>
|
||||
${msg("Add Existing User")}
|
||||
</button>`
|
||||
: null}
|
||||
|
||||
<ak-dropdown class="pf-c-dropdown">
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary pf-c-dropdown__toggle"
|
||||
@@ -377,7 +447,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
aria-controls="add-user-menu"
|
||||
tabindex="0"
|
||||
>
|
||||
<span class="pf-c-dropdown__toggle-text">${msg("Add new user")}</span>
|
||||
<span class="pf-c-dropdown__toggle-text">${msg("Add New User")}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<menu
|
||||
@@ -387,74 +457,40 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
aria-labelledby="add-user-toggle"
|
||||
tabindex="-1"
|
||||
>
|
||||
${this.targetGroup
|
||||
? html`<li role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="pf-c-dropdown__menu-item"
|
||||
@click=${this.openNewUserToTargetGroupModal}
|
||||
>
|
||||
${msg("New Group User...")}
|
||||
</button>
|
||||
</li>`
|
||||
: null}
|
||||
${this.targetRole
|
||||
? html`<li role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="pf-c-dropdown__menu-item"
|
||||
@click=${this.openNewUserToTargetRoleModal}
|
||||
>
|
||||
${msg("New Role User...")}
|
||||
</button>
|
||||
</li>`
|
||||
: null}
|
||||
|
||||
<li role="presentation">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Create User")}</span>
|
||||
<span slot="header">${msg("New User")}</span>
|
||||
${this.targetGroup
|
||||
? html`
|
||||
<div class="pf-c-banner pf-m-info" slot="above-form">
|
||||
${msg(
|
||||
str`This user will be added to the group "${this.targetGroup.name}".`,
|
||||
)}
|
||||
</div>
|
||||
<ak-user-form .targetGroup=${this.targetGroup} slot="form">
|
||||
</ak-user-form>
|
||||
`
|
||||
: nothing}
|
||||
${this.targetRole
|
||||
? html`
|
||||
<div class="pf-c-banner pf-m-info" slot="above-form">
|
||||
${msg(
|
||||
str`This user will be added to the role "${this.targetRole.name}".`,
|
||||
)}
|
||||
</div>
|
||||
<ak-user-form .targetRole=${this.targetRole} slot="form">
|
||||
</ak-user-form>
|
||||
`
|
||||
: nothing}
|
||||
<a role="menuitem" slot="trigger" class="pf-c-dropdown__menu-item">
|
||||
${msg("New user...")}
|
||||
</a>
|
||||
</ak-forms-modal>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<ak-forms-modal
|
||||
.closeAfterSuccessfulSubmit=${false}
|
||||
.cancelText=${msg("Close")}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="pf-c-dropdown__menu-item"
|
||||
@click=${this.openNewServiceUserToTargetGroupModal}
|
||||
>
|
||||
<span slot="submit">${msg("Create Service Account")}</span>
|
||||
<span slot="header">${msg("New Service Account")}</span>
|
||||
${this.targetGroup
|
||||
? html`
|
||||
<div class="pf-c-banner pf-m-info" slot="above-form">
|
||||
${msg(
|
||||
str`This user will be added to the group "${this.targetGroup.name}".`,
|
||||
)}
|
||||
</div>
|
||||
<ak-user-service-account-form
|
||||
.targetGroup=${this.targetGroup}
|
||||
slot="form"
|
||||
></ak-user-service-account-form>
|
||||
`
|
||||
: nothing}
|
||||
${this.targetRole
|
||||
? html`
|
||||
<div class="pf-c-banner pf-m-info" slot="above-form">
|
||||
${msg(
|
||||
str`This user will be added to the role "${this.targetRole.name}".`,
|
||||
)}
|
||||
</div>
|
||||
<ak-user-service-account-form
|
||||
.targetRole=${this.targetRole}
|
||||
slot="form"
|
||||
></ak-user-service-account-form>
|
||||
`
|
||||
: nothing}
|
||||
<a role="menuitem" slot="trigger" class="pf-c-dropdown__menu-item">
|
||||
${msg("New service account...")}
|
||||
</a>
|
||||
</ak-forms-modal>
|
||||
${msg("New Service Account...")}
|
||||
</button>
|
||||
</li>
|
||||
</menu>
|
||||
</ak-dropdown>
|
||||
@@ -462,7 +498,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
`;
|
||||
}
|
||||
|
||||
renderToolbarAfter(): TemplateResult {
|
||||
protected override renderToolbarAfter(): TemplateResult {
|
||||
return html`<div class="pf-c-toolbar__group pf-m-filter-group">
|
||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
||||
<div class="pf-c-input-group">
|
||||
@@ -497,6 +533,6 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-user-related-list": RelatedUserList;
|
||||
"ak-user-related-add": RelatedUserAdd;
|
||||
"ak-add-related-user-form": AddRelatedUserForm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ function formatContentTypePlaceholder(contentType: ContentTypeEnum): string {
|
||||
}
|
||||
|
||||
@customElement("ak-lifecycle-rule-form")
|
||||
export class LifecycleRuleForm extends ModelForm<LifecycleRule, string> {
|
||||
export class LifecycleRuleForm extends ModelForm<LifecycleRule, string, LifecycleRule | null> {
|
||||
#targetSelectRef = createRef<SearchSelect<TargetObject>>();
|
||||
#reviewerGroupsSelectRef = createRef<SearchSelect<Group>>();
|
||||
#reviewerUsersSelectRef = createRef<SearchSelect<Group>>();
|
||||
@@ -146,8 +146,8 @@ export class LifecycleRuleForm extends ModelForm<LifecycleRule, string> {
|
||||
});
|
||||
}
|
||||
|
||||
protected override serialize(): LifecycleRule | null {
|
||||
const result = super.serialize();
|
||||
public override toJSON(): LifecycleRule | null {
|
||||
const result = super.toJSON();
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
|
||||
@@ -87,7 +87,7 @@ export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
|
||||
html`${item.gracePeriod}`,
|
||||
html` <div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Lifecycle Rule")}</span>
|
||||
<ak-lifecycle-rule-form
|
||||
slot="form"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-object-review-form")
|
||||
export class ObjectReviewForm extends ModelForm<Review, string> {
|
||||
export class ObjectReviewForm extends ModelForm<Review, string, Review | null> {
|
||||
@property({ attribute: false })
|
||||
public iteration: LifecycleIteration | null = null;
|
||||
|
||||
@@ -26,8 +26,8 @@ export class ObjectReviewForm extends ModelForm<Review, string> {
|
||||
});
|
||||
}
|
||||
|
||||
protected override serialize(): Review | null {
|
||||
const review = super.serialize();
|
||||
public override toJSON(): Review | null {
|
||||
const review = super.toJSON();
|
||||
|
||||
if (!review || !this.iteration) return null;
|
||||
|
||||
|
||||
@@ -89,7 +89,8 @@ export class OutpostDeploymentModal extends ModalButton {
|
||||
: nothing}
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-modal-box__footer pf-m-align-left">
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
@click=${() => {
|
||||
@@ -98,7 +99,7 @@ export class OutpostDeploymentModal extends ModalButton {
|
||||
>
|
||||
${msg("Close")}
|
||||
</button>
|
||||
</footer>`;
|
||||
</fieldset>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ export class OutpostListPage extends TablePage<Outpost> {
|
||||
></ak-outpost-health-simple>`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Outpost")}</span>
|
||||
<ak-outpost-form
|
||||
slot="form"
|
||||
|
||||
@@ -87,7 +87,7 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
|
||||
${StrictUnsafe<CustomFormElementTagName>(item.component, {
|
||||
slot: "form",
|
||||
instancePk: item.pk,
|
||||
actionLabel: msg("Update"),
|
||||
submitLabel: msg("Save Changes"),
|
||||
headline: msg(str`Update ${item.verboseName}`, {
|
||||
id: "form.headline.update",
|
||||
}),
|
||||
|
||||
@@ -10,6 +10,7 @@ import "#elements/forms/ModalForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
import { PolicyBindingCheckTarget, PolicyBindingCheckTargetToLabel } from "#common/policies/utils";
|
||||
|
||||
import { CustomFormElementTagName } from "#elements/forms/unsafe";
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
@@ -18,7 +19,7 @@ import { StrictUnsafe } from "#elements/utils/unsafe";
|
||||
|
||||
import { PolicyBindingForm, PolicyBindingNotice } from "#admin/policies/PolicyBindingForm";
|
||||
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
|
||||
import { PolicyBindingCheckTarget, PolicyBindingCheckTargetToLabel } from "#admin/policies/utils";
|
||||
import { UserForm } from "#admin/users/UserForm";
|
||||
|
||||
import {
|
||||
PoliciesApi,
|
||||
@@ -113,7 +114,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
|
||||
${StrictUnsafe<CustomFormElementTagName>(item.policyObj?.component, {
|
||||
slot: "form",
|
||||
instancePk: item.policyObj?.pk,
|
||||
actionLabel: msg("Update"),
|
||||
submitLabel: msg("Save Changes"),
|
||||
headline: msg(str`Update ${item.policyObj?.name}`, {
|
||||
id: "form.headline.update",
|
||||
}),
|
||||
@@ -125,7 +126,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
|
||||
</ak-forms-modal>`;
|
||||
} else if (item.group) {
|
||||
return html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Group")}</span>
|
||||
<ak-group-form slot="form" .instancePk=${item.groupObj?.pk}> </ak-group-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
@@ -133,14 +134,13 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
|
||||
</button>
|
||||
</ak-forms-modal>`;
|
||||
} else if (item.user) {
|
||||
return html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="header">${msg("Update User")}</span>
|
||||
<ak-user-form slot="form" .instancePk=${item.userObj?.pk}> </ak-user-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Edit User")}
|
||||
</button>
|
||||
</ak-forms-modal>`;
|
||||
return html`<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
${UserForm.asEditModalInvoker(item.userObj?.pk)}
|
||||
>
|
||||
${msg("Edit User")}
|
||||
</button>`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
@@ -184,7 +184,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
|
||||
html`${item.timeout}`,
|
||||
html` ${this.getObjectEditButton(item)}
|
||||
<ak-forms-modal size=${PFSize.Medium}>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Binding")}</span>
|
||||
${StrictUnsafe<PolicyBindingForm>(this.bindingEditForm, {
|
||||
slot: "form",
|
||||
@@ -193,7 +193,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
|
||||
typeNotices: this.typeNotices,
|
||||
targetPk: this.target || "",
|
||||
|
||||
actionLabel: msg("Update"),
|
||||
submitLabel: msg("Save Changes"),
|
||||
headline: msg("Update Binding"),
|
||||
})}
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
@@ -227,7 +227,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
|
||||
typeNotices: this.typeNotices,
|
||||
targetPk: this.target || "",
|
||||
|
||||
actionLabel: msg("Create"),
|
||||
submitLabel: msg("Create"),
|
||||
headline: msg("Create Binding"),
|
||||
})}
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
@@ -254,7 +254,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
|
||||
typeNotices: this.typeNotices,
|
||||
targetPk: this.target || "",
|
||||
|
||||
actionLabel: msg("Create"),
|
||||
submitLabel: msg("Create"),
|
||||
headline: msg("Create Binding"),
|
||||
})}
|
||||
|
||||
|
||||
@@ -5,15 +5,14 @@ import "#elements/forms/Radio";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { groupBy } from "#common/utils";
|
||||
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import {
|
||||
createPassFailOptions,
|
||||
PolicyBindingCheckTarget,
|
||||
PolicyBindingCheckTargetToLabel,
|
||||
} from "#admin/policies/utils";
|
||||
} from "#common/policies/utils";
|
||||
import { groupBy } from "#common/utils";
|
||||
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
|
||||
@@ -69,7 +69,7 @@ export class PolicyListPage extends TablePage<Policy> {
|
||||
${StrictUnsafe<CustomFormElementTagName>(item.component, {
|
||||
slot: "form",
|
||||
instancePk: item.pk,
|
||||
actionLabel: msg("Update"),
|
||||
submitLabel: msg("Save Changes"),
|
||||
headline: msg(str`Update ${item.verboseName}`, {
|
||||
id: "form.headline.update",
|
||||
}),
|
||||
@@ -127,7 +127,7 @@ export class PolicyListPage extends TablePage<Policy> {
|
||||
<ak-forms-confirm
|
||||
successMessage=${msg("Successfully cleared policy cache")}
|
||||
errorMessage=${msg("Failed to delete policy cache")}
|
||||
action=${msg("Clear cache")}
|
||||
action=${msg("Clear Cache")}
|
||||
.onConfirm=${() => {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesAllCacheClearCreate();
|
||||
}}
|
||||
|
||||
@@ -94,7 +94,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
|
||||
${StrictUnsafe<CustomFormElementTagName>(item.component, {
|
||||
slot: "form",
|
||||
instancePk: item.pk,
|
||||
actionLabel: msg("Update"),
|
||||
submitLabel: msg("Save Changes"),
|
||||
headline: msg(str`Update ${item.verboseName}`, {
|
||||
id: "form.headline.update",
|
||||
}),
|
||||
|
||||
@@ -118,7 +118,7 @@ export class ProviderListPage extends TablePage<Provider> {
|
||||
${StrictUnsafe<CustomFormElementTagName>(item.component, {
|
||||
slot: "form",
|
||||
instancePk: item.pk,
|
||||
actionLabel: msg("Update"),
|
||||
submitLabel: msg("Save Changes"),
|
||||
headline: msg(str`Update ${item.verboseName}`, {
|
||||
id: "form.headline.update",
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "#admin/common/ak-license-notice";
|
||||
import "#elements/LicenseNotice";
|
||||
import "#admin/providers/ldap/LDAPProviderForm";
|
||||
import "#admin/providers/oauth2/OAuth2ProviderForm";
|
||||
import "#admin/providers/proxy/ProxyProviderForm";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ import "#admin/providers/google_workspace/GoogleWorkspaceProviderForm";
|
||||
import "#admin/providers/google_workspace/GoogleWorkspaceProviderGroupList";
|
||||
import "#admin/providers/google_workspace/GoogleWorkspaceProviderUserList";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#components/ak-status-label";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/ActionButton/index";
|
||||
import "#elements/buttons/ModalButton";
|
||||
@@ -205,7 +206,7 @@ export class GoogleWorkspaceProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Google Workspace Provider")}</span>
|
||||
<ak-provider-google-workspace-form
|
||||
slot="form"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "#admin/providers/RelatedApplicationButton";
|
||||
import "#admin/providers/ldap/LDAPProviderForm";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/ModalButton";
|
||||
@@ -181,7 +181,7 @@ export class LDAPProviderViewPage extends WithSession(AKElement) {
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update LDAP Provider")}</span>
|
||||
<ak-provider-ldap-form slot="form" .instancePk=${this.provider.pk}>
|
||||
</ak-provider-ldap-form>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import "#admin/providers/microsoft_entra/MicrosoftEntraProviderForm";
|
||||
import "#admin/providers/microsoft_entra/MicrosoftEntraProviderGroupList";
|
||||
import "#admin/providers/microsoft_entra/MicrosoftEntraProviderUserList";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/ActionButton/index";
|
||||
import "#elements/buttons/ModalButton";
|
||||
@@ -205,7 +206,7 @@ export class MicrosoftEntraProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Microsoft Entra Provider")}</span>
|
||||
<ak-provider-microsoft-entra-form
|
||||
slot="form"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "#admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||
|
||||
import { AkControlElement } from "#elements/AkControlElement";
|
||||
import { AKControlElement } from "#elements/ControlElement";
|
||||
import { LitPropertyRecord } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
@@ -22,7 +22,7 @@ export type RedirectURIProperties = LitPropertyRecord<{
|
||||
};
|
||||
|
||||
@customElement("ak-provider-oauth2-redirect-uri")
|
||||
export class OAuth2ProviderRedirectURI extends AkControlElement<RedirectURI> {
|
||||
export class OAuth2ProviderRedirectURI extends AKControlElement<RedirectURI> {
|
||||
static styles = [
|
||||
PFInputGroup,
|
||||
PFFormControl,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import "#admin/providers/RelatedApplicationButton";
|
||||
import "#admin/providers/oauth2/OAuth2ProviderForm";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/EmptyState";
|
||||
import "#elements/Tabs";
|
||||
@@ -298,7 +299,7 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update OAuth2 Provider")}</span>
|
||||
<ak-provider-oauth2-form
|
||||
slot="form"
|
||||
|
||||
@@ -2,7 +2,7 @@ import "#admin/providers/RelatedApplicationButton";
|
||||
import "#admin/providers/proxy/ProxyProviderForm";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#components/ak-status-label";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/ak-mdx/index";
|
||||
@@ -380,7 +380,7 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Proxy Provider")}</span>
|
||||
<ak-provider-proxy-form
|
||||
slot="form"
|
||||
|
||||
@@ -88,7 +88,7 @@ export class EndpointListPage extends Table<Endpoint> {
|
||||
html`${item.host}`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Endpoint")}</span>
|
||||
<ak-rac-endpoint-form slot="form" .instancePk=${item.pk}>
|
||||
</ak-rac-endpoint-form>
|
||||
|
||||
@@ -5,7 +5,7 @@ import "#admin/providers/rac/EndpointList";
|
||||
import "#admin/providers/rac/RACProviderForm";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#components/ak-status-label";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/ModalButton";
|
||||
@@ -188,7 +188,7 @@ export class RACProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update RAC Provider")}</span>
|
||||
<ak-provider-rac-form slot="form" .instancePk=${this.provider.pk || 0}>
|
||||
</ak-provider-rac-form>
|
||||
|
||||
@@ -8,7 +8,7 @@ import "#components/ak-text-input";
|
||||
import "#elements/forms/FormGroup";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
import "#admin/common/ak-license-notice";
|
||||
import "#elements/LicenseNotice";
|
||||
|
||||
import { propertyMappingsProvider, propertyMappingsSelector } from "./RadiusProviderFormHelpers.js";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "#admin/providers/RelatedApplicationButton";
|
||||
import "#admin/providers/radius/RadiusProviderForm";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/ModalButton";
|
||||
@@ -137,7 +137,7 @@ export class RadiusProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">
|
||||
${msg("Update Radius Provider")}
|
||||
</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "#admin/providers/RelatedApplicationButton";
|
||||
import "#admin/providers/saml/SAMLProviderForm";
|
||||
import "#admin/rbac/ObjectPermissionsPage";
|
||||
import "#components/events/ObjectChangelog";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/EmptyState";
|
||||
import "#elements/Tabs";
|
||||
@@ -361,7 +361,7 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update SAML Provider")}</span>
|
||||
<ak-provider-saml-form slot="form" .instancePk=${this.provider.pk || 0}>
|
||||
</ak-provider-saml-form>
|
||||
|
||||
@@ -7,7 +7,7 @@ import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/Radio";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
import "#elements/CodeMirror";
|
||||
import "#admin/common/ak-license-notice";
|
||||
import "#elements/LicenseNotice";
|
||||
import "#components/ak-number-input";
|
||||
import "#elements/utils/TimeDeltaHelp";
|
||||
import "#components/ak-text-input";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user