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:
Teffen Ellis
2026-03-25 07:07:29 +01:00
committed by GitHub
parent 5ff8400815
commit b88d082947
240 changed files with 6717 additions and 2531 deletions

View File

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

View File

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

View File

@@ -3,50 +3,39 @@ 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(),
]);
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 !== "") {
} else if (version.buildHash) {
build = html`<a
rel="noopener noreferrer"
href="https://github.com/goauthentik/authentik/commit/${version.buildHash}"
@@ -54,6 +43,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
>${version.buildHash}</a
>`;
}
return [
[msg("Version"), version.versionCurrent],
[msg("UI Version"), import.meta.env.AK_VERSION],
@@ -68,32 +58,88 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
];
}
#contentRef = createRef<HTMLDivElement>();
@customElement("ak-about-modal")
export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
public static override formatARIALabel = () => msg("About authentik");
#backdropListener = (event: PointerEvent) => {
// We only want to close the modal when the backdrop is clicked, not when it's children are clicked.
if (this.#contentRef.value?.contains(event.target as Node)) {
return;
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);
}
this.close();
};
`,
];
protected override renderModal() {
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;
});
}
public connectedCallback(): void {
super.connectedCallback();
this.refresh();
}
//#region Renderers
protected override renderCloseButton() {
return null;
}
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)}
return html`<div
${ref(this.scrollContainerRef)}
class="pf-c-about-modal-box"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
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")}
>
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-about-modal-box__brand">
${ThemedImage({
src: this.brandingFavicon,
@@ -103,11 +149,6 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
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>
@@ -115,25 +156,22 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
<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]) => {
${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>`,
)}
</dl>`
: html`<ak-empty-state loading></ak-empty-state>`}
</div>
</div>
<p class="pf-c-about-modal-box__strapline"></p>
</div>
</div>
</div>
</div>`;
}
//#endregion
}
declare global {

View File

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

View File

@@ -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);
@@ -172,12 +271,46 @@ export class AdminInterface extends WithCapabilitiesConfig(
<i aria-hidden="true" class="fas fa-bars"></i>
</button>
<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>
<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(createAdminSidebarEntries())}
>${renderSidebarItems(this.entries)}
${this.can(CapabilitiesEnum.IsEnterprise)
? renderSidebarItems(createAdminSidebarEnterpriseEntries())
: nothing}
@@ -201,7 +334,6 @@ export class AdminInterface extends WithCapabilitiesConfig(
</div>
</div>
${renderNotificationDrawerPanel(this.drawer)}
<ak-about-modal></ak-about-modal>
</div>
</div>
@@ -213,7 +345,8 @@ export class AdminInterface extends WithCapabilitiesConfig(
tabindex="0"
></div>
</div>
</div>`;
</div>
${this.commandPalette}`;
}
//#endregion

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
}

View File

@@ -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}"`)}
${ApplicationForm.asEditModalInvoker(item.slug)}
>
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
${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)}>
protected override renderObjectCreate(): TemplateResult {
return html`<ak-dropdown class="pf-c-dropdown">
<button
slot="trigger"
class="pf-c-button pf-m-primary"
data-ouia-component-id="start-application-wizard"
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"
>
${msg("Create with Provider")}
<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>
</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>`;
<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 {
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();
}}

View File

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

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

View File

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

View File

@@ -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,12 +91,14 @@ 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">
<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>
</ak-provider-select-table>
<div class="pf-c-form-control">
<ak-chip-group>${map(this.providers, renderOneChip)}</ak-chip-group>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,43 +91,43 @@ 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>;
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,
() =>
[
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],
] as DescriptionPair[],
)
.with(
ProxyMode.ForwardSingle,
() => [[msg("External Host"), provider.externalHost]] as DescriptionPair[],
)
.with(
ProxyMode.ForwardDomain,
() =>
[
];
})
.with(ProxyMode.ForwardSingle, () => {
return [[msg("External Host"), provider.externalHost]];
})
.with(ProxyMode.ForwardDomain, () => {
return [
[msg("Authentication URL"), provider.externalHost],
[msg("Cookie domain"), provider.cookieDomain],
] as DescriptionPair[],
)
];
})
.otherwise(() => {
throw new Error(
`Unrecognized proxy mode: ${provider.mode?.toString() ?? "-- undefined __"}`,
);
}),
});
const label = proxyModeToLabel[provider.mode ?? ProxyMode.Proxy];
return renderSummary("Proxy", provider.name, [
[msg("Mode"), label()],
...proxyHostMappings,
[
msg("Basic-Auth"),
html`<ak-status-label
@@ -135,31 +136,31 @@ function renderProxyOverview(rawProvider: OneOfProvider) {
></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>][]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">
<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>
</ak-forms-modal>
</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>`;
}
}

View File

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

View File

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

View File

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

View File

@@ -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
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")}
@click=${this.openUserSelectionModal}
>
<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>
</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">
<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>
</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">
${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>
</ak-forms-modal>
`
: nothing}
</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 {
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">
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}
<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}
</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`<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"
>
<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")}
? html`<li role="presentation">
<button
type="button"
role="menuitem"
class="pf-c-dropdown__menu-item"
@click=${this.openNewUserToTargetGroupModal}
>
<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}
${msg("New Group User...")}
</button>
</li>`
: null}
${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>
? 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">
<button
type="button"
role="menuitem"
class="pf-c-dropdown__menu-item"
@click=${this.openNewServiceUserToTargetGroupModal}
>
${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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">
return html`<button
slot="trigger"
class="pf-c-button pf-m-secondary"
${UserForm.asEditModalInvoker(item.userObj?.pk)}
>
${msg("Edit User")}
</button>
</ak-forms-modal>`;
</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"),
})}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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