web: User Wizard, Modal Revisions Merge Branch (#21336)

* web/elements: rename hasSlotted to findSlotted and refactor host styles

Rename the slot-inspection helper on `AKElement` from `hasSlotted` to
`findSlotted` and return the first matching element rather than a
boolean, so callers can both check for presence and reach the node.
Update every call site in the tree (default callers pass no argument
instead of `null`).

Along the way, tidy `AKElement`'s host-style plumbing: expose
`hostStyles` as a getter/setter backed by a `CSSStyleSheet` cache and
move the adoption logic into `attachHostStyles` / `detachHostStyles`
class methods, so subclasses can share the lifecycle. Drop the now
unused `@localized` decorator import.

Also add a `findAssignedSlot` helper in `elements/utils/slots.ts` for
light-DOM → slot lookups, and give `EmptyState` an explicit
`display: block` so empty-state placement doesn't collapse when
wrapped.

* web/chips: tighten chip group rendering and add placeholder class

Make `ChipGroup` generic over its chip value type, expose a
`placeholder` property that renders an inline placeholder when the
default slot is empty, and intercept clicks that land on child chips
so outer handlers can tell "clicked the group" apart from "clicked a
chip". Give the host an explicit `display: block` so the group
participates in layout correctly.

Move the removal tooltip on `Chip` to the right so it doesn't clip at
the top of the row.

In `base/common.css`, add the `ak-m-placeholder` class used by the
new chip-group placeholder and extend `.ak-fade-in` with an opt-in
`ak-m-delayed` modifier that animates height alongside the fade via
`interpolate-size`, so loading cards can slide in without jank.

* web/elements: add scrollbar helpers and polish table styles

Introduce `elements/utils/scrollbars.ts` with `measureScrollbarWidth`
and `applyScrollbarClass`, and call it from `Interface` so the root
document picks up `ak-m-visible-scrollbars` / `ak-m-overlay-scrollbars`
depending on the platform. Add an `ak-m-thin-scrollbar` selector to
the thin-scrollbar rule in `base/scrollbars.css` so ad-hoc containers
can opt in.

Refresh `Table.css`: expose `search-form`, `search-input`,
`pagination-bottom`, and `table` parts; introduce
`--ak-c-table--expandable-overlay--Color` theming for expandable rows
(including a nested-table background pass); add an
`ak-c-table__actions` helper so per-row action buttons wrap
consistently; and teach the host to honor `display-box="contents"` so
tables embedded in `display: contents` parents still participate in
layout checks.

Drop the unused `elements/utils/isVisible.ts`; the only live
`isVisible` helpers live beside their callers under SearchSelect.

* web/buttons: support split-button Dropdown layout

Teach `ak-dropdown` to recognize a PatternFly split-button toggle —
look for `.pf-c-dropdown__toggle.pf-m-split-button .pf-c-dropdown__toggle-button:last-child`
first and fall back to the single-button selector — so a primary
action and a menu trigger can coexist in one dropdown. Drop the
workaround that skipped wiring menu-item click handlers: now that
dropdowns live inside native dialogs, letting a menu-item click
bubble no longer closes the parent modal. Switch the private fields
to `protected` so subclasses can reach them, and anchor the
AKRefreshEvent and outside-click listeners at `window` explicitly
(matching the new `@listen` default).

In `@listen`, flip the default target from `window` to `this`. A
component's own element is the more intuitive default for a decorator
attached to an instance method, and call sites that want the window
now opt in explicitly.

Extend `Dropdown/dropdown.css` with `--pf-c-dropdown__toggle--*`
padding variables so split-button variants get consistent spacing.

* web/forms: improve form ARIA scaffolding and tighten group styles

Add a sticky `ak-c-form__header` row to `Form.css` with a
`form-actions` part so form headers can host an inline title and
action cluster without each form reinventing the layout.

In `Form/form.css`, add a `.ak-m-content-center` variant for forms
that center their body inside a fixed-size container, and introduce a
PatternFly-compatible grid-based Radio label so the input and its
description align cleanly and the whole row is clickable.

Tighten the `FormGroup` summary spacing (use `spacer--sm` inline and
`spacer-xs` block) and hoist the high-contrast overrides onto the
open group so the details marker stays aligned.

Make `AKControlElement` abstract (requiring a `name`), rename
`isValid` → `valid`, declare it as implementing the new
`FormField<T>` interface, and mark it deprecated in favor of
`FormAssociatedElement`. Make `FormField` generic over the JSON
value type, extend `HTMLElement`, and drop the `Jsonifiable` runtime
import in favor of a type-only import. `HorizontalFormElement` now
searches for either legacy control elements or the new `FormField`
shape when picking its focus target.

* web/elements: migrate modal plumbing to the native <dialog> element

Replace the bespoke modal stack with an `<ak-modal>` built on the
browser's native `<dialog>`, and collect every piece of the new
infrastructure under `#elements/dialogs`:

 * `ak-modal.ts` / `ak-modal.css` — the element + its PatternFly
   compatible styles.
 * `dialog.css` — the global `ak-c-dialog` token and backdrop rules,
   imported via the new `components/Modal/modal.css` entry point
   (replacing the old `base/modal.css` import in `base.css` and
   `interface.global.css`).
 * `shared.ts` — the `TransclusionChildElement` /
   `TransclusionChildSymbol` contract plus the parent-side helpers
   (`isTransclusionParentElement`, `slottedElementUpdatedAt`), so
   forms and tables hosted inside a modal can signal re-render hints
   to the dialog wrapper.
 * `directives.ts` / `invokers.ts` / `utils.ts` — the
   `modalInvoker`, `renderModal`, and `DialogInit` helpers that
   declarative call sites use to open a modal from a button without
   imperatively mounting the element.
 * `components/` — the ready-made invoker buttons
   (`ModalInvokerButton`, `IconEditButton`, `IconEditButtonByTagName`,
   `IconPermissionButton`) and the `components.ts` barrel.
 * `components/Modal/modal.css` — the short host wrapper that pulls
   `dialog.css` into the bundled base stylesheet chain.

Rewire the existing modal consumers to use the new contract:

 * `Form` now implements `TransclusionChildElement`, exposes
   `verboseName`/`verboseNamePlural`/`createLabel`/`submitVerb`
   statics, tracks visibility via `intersectionObserver`, and
   forwards `asModalInvoker` / `showModal` through the new
   `modalInvoker` / `renderModal` helpers. `ModalForm` and
   `ModelForm` follow the same shape. `ModalButton` drops its own
   `pf-c-modal-box` padding fix (the dialog handles it).
 * `Table` implements `TransclusionChildElement`, dispatches refresh
   via `AKRefreshEvent`, and exposes `display-box="contents"` so
   tables embedded in dialogs participate in layout checks.
   `TablePage` / `TableSearch` widen types and surface `search-form`
   / `search-input` parts for dialog-scoped styling.
 * `ak-about-modal`, `ObjectPermissionModal`,
   `RACLaunchEndpointModal`, the command palette, and the admin/user
   interface roots all move off `#elements/modals` and onto
   `#elements/dialogs`.
 * `AdminSettingsForm` / `AdminSettingsPage` render their header /
   actions through the new `ak-c-form__header` + `form-actions`
   slots introduced in the prior Form CSS commit, and swap the
   outermost `<section>` for `<main>` for better landmark semantics.
 * `elements/utils/render-roots.ts` and
   `elements/utils/unsafe.ts` gain dialog-aware helpers (notably a
   directive-based replacement for the old `unsafe` builder).
 * `base/globals.css` disables overscroll while any dialog is open
   via `html[data-dialog-count]`; `package.json` adds the
   `#elements/dialogs` barrel alias.

Delete the old `elements/modals/` directory (`ak-modal.ts`,
`shared.ts`, `styles.css`, `utils.ts`) and `styles/authentik/base/modal.css`
now that nothing imports them.

* web/wizards: refactor wizards to dialog-based flow

Rebuild the shared Wizard primitives on top of the new <dialog> contract:
split CreateWizard/utils out of Wizard, rename admin *Wizard.ts entry
points to ak-*-wizard.ts (Policy, Provider, Source, Stage,
PropertyMapping, ServiceConnection), and port the Application wizard
steps to the new WizardStep base. Adds the user wizard and recovery
invoker plus the refreshed Wizard component styles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* web/admin: migrate forms and list pages to dialog-based modals

Port every admin form, list page, and RBAC surface to the new
TransclusionChildElement / asModalInvoker contract introduced with the
native <dialog> migration. Replace the old ModalButton-driven helpers
with the new modalInvoker/renderModal flow, add the shared
IconCopyButton/IconTokenCopyButton/IconEnrollmentTokenCopyButton
components (with .ak-c-button--icon__progress styling), and refresh
messages, notifications, flow inspector, and user portal consumers to
match. Includes small common/element utility updates picked up along
the way.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* web/test: update browser e2e tests for dialog-based flow

Adjust application, group, session, and user browser tests to the new
wizard and modal selectors introduced by the <dialog> migration and
relax a handful of timeouts that were tight against the old
ModalButton animation sequence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix visibility detection.

* Fix layout, behavior.

* Fix type.

* Flesh out test revisions.

* Fix type.

* Format.

* Use plural path.

* Fix strict selector in Safari.

* Remove unused.

* Spellcheck.

* Partial type fix.

* Fix translation.

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Fletcher Heisler
2026-04-11 00:00:49 -07:00
committed by GitHub
parent 1858125d3d
commit 03e67aea34
279 changed files with 8163 additions and 5554 deletions

View File

@@ -57,6 +57,7 @@
"#styles/*.css": "./src/styles/*.css",
"#styles/*": "./src/styles/*.js",
"#common/*": "./src/common/*.js",
"#elements/dialogs": "./src/elements/dialogs/index.js",
"#elements/*.css": "./src/elements/*.css",
"#elements/*": "./src/elements/*.js",
"#components/*.css": "./src/components/*.css",

View File

@@ -36,6 +36,7 @@ export default defineConfig({
testIdAttribute: "data-test-id",
baseURL,
trace: "on-first-retry",
colorScheme: "dark",
launchOptions: {
logger: {
isEnabled() {

View File

@@ -43,13 +43,16 @@ export class FooterLinkInput extends AKControlElement<FooterLink> {
@queryAll(".ak-form-control")
controls?: HTMLInputElement[];
json() {
@property({ type: String })
public name: string | null = null;
toJSON(): FooterLink {
return Object.fromEntries(
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
) as unknown as FooterLink;
}
get isValid() {
get valid() {
const href = this.json()?.href ?? "";
return hasLegalScheme(href) && URL.canParse(href);
}

View File

@@ -14,11 +14,12 @@ import { akFooterLinkInput, IFooterLinkInput } from "./AdminSettingsFooterLinks.
import { DEFAULT_CONFIG } from "#common/api/config";
import { Form } from "#elements/forms/Form";
import { SlottedTemplateResult } from "#elements/types";
import { AdminApi, FooterLink, Settings, SettingsRequest } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, CSSResult, html, TemplateResult } from "lit";
import { css, CSSResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -56,7 +57,20 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
return result;
}
protected override renderForm(): TemplateResult {
public override submitLabel = msg("Save changes");
public override renderHeader() {
return html`<div class="ak-c-form__header">
<h2 class="pf-c-title pf-m-2xl sr-only">${msg("Edit Settings")}</h2>
<div part="form-actions">${this.renderSubmitButton()}</div>
</div>`;
}
public override renderActions(): SlottedTemplateResult {
return null;
}
protected override renderForm(): SlottedTemplateResult {
const { settings } = this;
return html`

View File

@@ -66,19 +66,18 @@ export class AdminSettingsPage extends AKElement {
if (!this.settings) return nothing;
return html`
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
<main class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-admin-settings-form
id="form"
.settings=${this.settings}
action-label=${msg("Update settings")}
@ak-form-submitted=${{ handleEvent: this.#refresh, passive: true }}
>
</ak-admin-settings-form>
</div>
</div>
</section>
</main>
`;
}

View File

@@ -34,7 +34,7 @@ const metadata: Meta<FooterLinkInput> = {
return;
}
const target = event.target as FooterLinkInput;
messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.isValid ? "Yes" : "No"}`;
messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.valid ? "Yes" : "No"}`;
});
}, 250);

View File

@@ -40,7 +40,7 @@ const metadata: Meta<IArrayInput<unknown>> = {
return;
}
const target = event.target as FooterLinkInput;
messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.isValid ? "Yes" : "No"}`;
messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.valid ? "Yes" : "No"}`;
});
}, 250);

View File

@@ -1,72 +1,51 @@
import "#elements/EmptyState";
import "#elements/ak-progress-bar";
import { DEFAULT_CONFIG } from "#common/api/config";
import { globalAK } from "#common/global";
import { asInvoker } from "#elements/dialogs";
import { AKModal } from "#elements/dialogs/ak-modal";
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 { SlottedTemplateResult } from "#elements/types";
import { ThemedImage } from "#elements/utils/images";
import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthentik/api";
import {
AdminApi,
CapabilitiesEnum,
LicenseSummaryStatusEnum,
SystemInfo,
Version,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, html, TemplateResult } from "lit";
import { css, html } from "lit";
import { ref } from "lit-html/directives/ref.js";
import { styleMap } from "lit-html/directives/style-map.js";
import { until } from "lit-html/directives/until.js";
import { customElement, state } from "lit/decorators.js";
import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css";
const DEFAULT_BRAND_IMAGE = "/static/dist/assets/images/flow_background.jpg";
type AboutEntry = [label: string, content: string | TemplateResult];
type AboutEntry = [label: string, content?: SlottedTemplateResult];
async function fetchAboutDetails(): Promise<AboutEntry[]> {
const api = new AdminApi(DEFAULT_CONFIG);
const [status, version] = await Promise.all([
api.adminSystemRetrieve(),
api.adminVersionRetrieve(),
]);
let build: string | TemplateResult = msg("Release");
if (globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug)) {
build = msg("Development");
} else if (version.buildHash) {
build = html`<a
rel="noopener noreferrer"
href="https://github.com/goauthentik/authentik/commit/${version.buildHash}"
target="_blank"
>${version.buildHash}</a
>`;
}
return [
[msg("Version"), version.versionCurrent],
[msg("UI Version"), import.meta.env.AK_VERSION],
[msg("Build"), build],
[msg("Python version"), status.runtime.pythonVersion],
[msg("Platform"), status.runtime.platform],
[msg("Kernel"), status.runtime.uname],
[
msg("OpenSSL"),
`${status.runtime.opensslVersion} ${status.runtime.opensslFipsEnabled ? "FIPS" : ""}`,
],
];
function renderEntry([label, content = null]: AboutEntry): SlottedTemplateResult {
return html`<dt>${label}</dt>
<dd>${content === null ? msg("Loading...") : content}</dd>`;
}
@customElement("ak-about-modal")
export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
public static override formatARIALabel = () => msg("About authentik");
public override formatARIALabel = () => msg("About authentik");
public static hostStyles = [
...AKModal.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);
.ak-c-dialog:has(ak-about-modal) {
--ak-c-dialog--BackgroundColor: var(--pf-global--palette--black-900);
--ak-c-dialog--BorderColor: var(--pf-global--palette--black-600);
}
`,
];
@@ -80,7 +59,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
}
.pf-c-about-modal-box {
--pf-c-about-modal-box--BackgroundColor: var(--ak-c-modal--BackgroundColor);
--pf-c-about-modal-box--BackgroundColor: var(--ak-c-dialog--BackgroundColor);
width: unset;
height: 100%;
max-height: unset;
@@ -89,6 +68,17 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
position: unset;
box-shadow: unset;
}
[part="brand"] {
position: relative;
}
[part="loading-bar"] {
position: absolute;
z-index: 1;
inset-block-start: 0;
inset-inline: 0;
}
`,
];
@@ -96,26 +86,100 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
public static open = asInvoker(AboutModal);
@state()
protected entries: AboutEntry[] | null = null;
#api = new AdminApi(DEFAULT_CONFIG);
public refresh() {
return fetchAboutDetails().then((entries) => {
this.entries = entries;
protected canDebug = globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug);
@state()
protected version: Version | null = null;
@state()
protected systemInfo: SystemInfo | null = null;
@state()
protected refreshPromise: Promise<[Version, SystemInfo]> | null = null;
public refresh = (): void => {
const versionPromise = this.#api.adminVersionRetrieve();
const systemInfoPromise = this.#api.adminSystemRetrieve();
this.refreshPromise = Promise.all([versionPromise, systemInfoPromise]).then((result) => {
this.version = result[0];
this.systemInfo = result[1];
return result;
});
}
};
public connectedCallback(): void {
super.connectedCallback();
this.refresh();
}
protected renderVersionInfo = () => {
const { version } = this;
let build: SlottedTemplateResult = null;
if (this.canDebug) {
build = msg("Development");
} else if (version?.buildHash) {
build = html`<a
rel="noopener noreferrer"
href="https://github.com/goauthentik/authentik/commit/${version.buildHash}"
target="_blank"
>${version.buildHash}</a
>`;
} else if (version) {
build = msg("Release");
}
const entries: AboutEntry[] = [
[msg("Server Version"), version?.versionCurrent],
[msg("Build"), build],
];
return entries.map(renderEntry);
};
protected renderSystemInfo = () => {
const { runtime } = this.systemInfo || {};
const sslLabel = runtime
? `${runtime.opensslVersion} ${runtime.opensslFipsEnabled ? "FIPS" : ""}`
: null;
const entries: AboutEntry[] = [
[msg("Python version"), runtime?.pythonVersion],
[msg("Platform"), runtime?.platform],
[msg("OpenSSL"), sslLabel],
[
msg("Kernel"),
runtime?.uname ?? html`<div style="min-height: 3em;">${msg("Loading...")}</div>`,
],
];
return entries.map(renderEntry);
};
//#region Renderers
protected override renderCloseButton() {
return null;
}
protected renderLoadingBar(): SlottedTemplateResult {
return until(
this.refreshPromise?.then(() => null),
html`<ak-progress-bar
part="loading-bar"
indeterminate
?inert=${!!this.systemInfo && !!this.version}
label=${msg("Loading")}
></ak-progress-bar>`,
);
}
protected override render() {
let product = this.brandingTitle;
@@ -129,6 +193,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
style=${styleMap({
"--pf-c-about-modal-box__hero--sm--BackgroundImage": `url(${DEFAULT_BRAND_IMAGE})`,
})}
part="box"
>
<div class="pf-c-about-modal-box__close">
<button
@@ -140,7 +205,8 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-about-modal-box__brand">
<div class="pf-c-about-modal-box__brand" part="brand">
${this.renderLoadingBar()}
${ThemedImage({
src: this.brandingFavicon,
alt: msg("authentik Logo"),
@@ -149,21 +215,25 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
themedUrls: this.brandingFaviconThemedUrls,
})}
</div>
<div class="pf-c-about-modal-box__header">
<h1 class="pf-c-title pf-m-4xl" id="modal-title">${product}</h1>
<div class="pf-c-about-modal-box__header" part="header">
<h1 class="pf-c-title pf-m-4xl" id="modal-title" part="title">${product}</h1>
</div>
<div class="pf-c-about-modal-box__hero"></div>
<div class="pf-c-about-modal-box__content">
<div class="pf-c-about-modal-box__body">
<div class="pf-c-content">
${this.entries
? html`<dl>
${this.entries.map(([label, value]) => {
return html`<dt>${label}</dt>
<dd>${value}</dd>`;
})}
</dl>`
: html`<ak-empty-state loading></ak-empty-state>`}
<dl>
<dt>${msg("UI Version")}</dt>
<dd>${import.meta.env.AK_VERSION}</dd>
${until(
this.refreshPromise?.then(this.renderVersionInfo),
this.renderVersionInfo(),
)}
${until(
this.refreshPromise?.then(this.renderSystemInfo),
this.renderSystemInfo(),
)}
</dl>
</div>
</div>
<p class="pf-c-about-modal-box__strapline"></p>

View File

@@ -4,6 +4,7 @@ import "#elements/sidebar/Sidebar";
import "#elements/sidebar/SidebarItem";
import "#elements/router/RouterOutlet";
import "#elements/commands/ak-command-palette";
import "#elements/commands/ak-command-palette-user-modal";
import {
createAdminSidebarEnterpriseEntries,
@@ -24,10 +25,10 @@ import {
PaletteCommandNamespace,
} from "#elements/commands/shared";
import { listen } from "#elements/decorators/listen";
import { renderDialog } from "#elements/dialogs";
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,
@@ -36,6 +37,7 @@ import {
renderNotificationDrawerPanel,
} from "#elements/notifications/utils";
import { navigate } from "#elements/router/RouterOutlet";
import { SlottedTemplateResult } from "#elements/types";
import Styles from "#admin/ak-interface-admin.css";
import { ROUTES } from "#admin/Routes";
@@ -46,6 +48,7 @@ import { LOCALE_STATUS_EVENT, LocaleStatusEventDetail, msg } from "@lit/localize
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
import { customElement, eventOptions, property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { guard } from "lit/directives/guard.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@@ -266,40 +269,7 @@ 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>
${this.renderCommandPaletteButton()}
<ak-version-banner></ak-version-banner>
<ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar>
@@ -344,6 +314,47 @@ export class AdminInterface extends WithCapabilitiesConfig(
${this.commandPalette}`;
}
protected renderCommandPaletteButton(): SlottedTemplateResult {
return guard([this.commandPalette.showListener], () => {
const macOS = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const primaryModifierKey = macOS ? "⌘" : "Ctrl";
return html`<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>${primaryModifierKey}</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>`;
});
}
//#endregion
}

View File

@@ -27,7 +27,7 @@ import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
import { Application, CoreApi, Provider, UsageEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, nothing, TemplateResult } from "lit";
import { html, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -40,8 +40,8 @@ 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");
public static override verboseName = msg("Application");
public static override verboseNamePlural = msg("Applications");
protected override async loadInstance(pk: string): Promise<Application> {
const app = await this.#api.coreApplicationsRetrieve({
@@ -54,7 +54,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
}
@property({ attribute: false })
public provider?: number;
public provider: number | null = null;
@state()
protected backchannelProviders: Provider[] = [];
@@ -115,10 +115,12 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
);
const providerFromInstance = this.instance?.provider;
const providerValue = providerFromInstance ?? this.provider;
const providerPrefilled = !this.instance && this.provider !== undefined;
const providerPrefilled = !this.instance && this.provider !== null;
return html`
${this.instance ? nothing : html`<ak-alert level="pf-m-info">${alertMsg}</ak-alert>`}
${this.instance || this.provider
? null
: html`<ak-alert level="pf-m-info">${alertMsg}</ak-alert>`}
<ak-text-input
name="name"
autocomplete="off"

View File

@@ -5,12 +5,13 @@ import "#elements/ak-mdx/ak-mdx";
import "#elements/buttons/SpinnerButton/ak-spinner-button";
import "#elements/forms/DeleteBulkForm";
import "#elements/forms/ModalForm";
import "#elements/modals/ak-modal";
import "#elements/dialogs/ak-modal";
import "#admin/applications/ApplicationForm";
import "#admin/applications/ApplicationWizardHint";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton } from "#elements/dialogs";
import { WithBrandConfig } from "#elements/mixins/branding";
import { getURLParam } from "#elements/router/RouteMatch";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
@@ -20,7 +21,7 @@ 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 { AKApplicationWizard } from "#admin/applications/wizard/ak-application-wizard";
import { Application, CoreApi, PoliciesApi } from "@goauthentik/api";
@@ -45,6 +46,9 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
protected override searchEnabled = true;
public pageTitle = msg("Applications");
public searchLabel = msg("Applications search");
public searchPlaceholder = msg("Search for application by name, group or provider...");
public get pageDescription() {
return msg(
str`External applications that use ${this.brandingTitle} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
@@ -69,7 +73,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
super.firstUpdated(changed);
if (getURLParam("createWizard", false)) {
AkApplicationWizard.showModal();
AKApplicationWizard.showModal();
} else if (getURLParam("createForm", false)) {
ApplicationForm.showModal();
}
@@ -138,16 +142,8 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
</a>`
: html`-`,
html`${item.providerObj?.verboseName || msg("-")}`,
html`<div>
<button
class="pf-c-button pf-m-plain"
aria-label=${msg(str`Edit "${item.name}"`)}
${ApplicationForm.asEditModalInvoker(item.slug)}
>
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
html`<div class="ak-c-table__actions">
${IconEditButton(ApplicationForm, item.slug)}
${item.launchUrl
? html`<a
href=${item.launchUrl}
@@ -166,17 +162,28 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
protected override renderObjectCreate(): TemplateResult {
return html`<ak-dropdown class="pf-c-dropdown">
<button
class="pf-c-button pf-m-primary pf-c-dropdown__toggle"
type="button"
id="new-application-toggle"
aria-haspopup="menu"
aria-controls="new-application-menu"
tabindex="0"
>
<span class="pf-c-dropdown__toggle-text">${msg("New Application")}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<div class="pf-c-dropdown__toggle pf-m-primary pf-m-split-button pf-m-action">
<button
class="pf-c-dropdown__toggle-button"
type="button"
${AKApplicationWizard.asModalInvoker()}
>
${msg("New Application")}
</button>
<button
class="pf-c-dropdown__toggle-button"
type="button"
id="new-application-toggle"
aria-haspopup="menu"
aria-controls="new-application-menu"
tabindex="0"
aria-label=${msg("New Application options")}
>
<i class="fas fa-caret-down" aria-hidden="true"></i>
</button>
</div>
<menu
class="pf-c-dropdown__menu"
hidden
@@ -189,7 +196,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
type="button"
role="menuitem"
class="pf-c-dropdown__menu-item"
${AkApplicationWizard.asModalInvoker()}
${AKApplicationWizard.asModalInvoker()}
aria-description=${msg(
"Opens the new application wizard, which will guide you through creating a new application with an existing provider.",
)}

View File

@@ -5,8 +5,8 @@ import "#elements/chips/ChipGroup";
import "#elements/forms/Form";
import { AKElement } from "#elements/Base";
import { renderModal } from "#elements/dialogs";
import { AKFormSubmitEvent } from "#elements/forms/Form";
import { renderModal } from "#elements/modals/utils";
import { SlottedTemplateResult } from "#elements/types";
import { Provider } from "@goauthentik/api";
@@ -65,7 +65,7 @@ export class AkBackchannelProvidersInput extends AKElement {
return renderModal(html`
<ak-form
headline=${this.label}
action-label=${msg("Confirm")}
submit-label=${msg("Confirm")}
@submit=${(event: AKFormSubmitEvent<Provider[]>) => {
const providers = event.target.toJSON();

View File

@@ -105,7 +105,7 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
</div>`;
}
renderEmpty(): TemplateResult {
protected override renderEmpty(): SlottedTemplateResult {
return super.renderEmpty(
html`<ak-empty-state icon="pf-icon-module"
><span>${msg("No app entitlements created.")}</span>

View File

@@ -10,8 +10,8 @@ import { WizardStep } from "#components/ak-wizard/WizardStep";
import { ApplicationWizardStyles } from "#admin/applications/wizard/ApplicationWizardFormStepStyles.styles";
import {
type ApplicationWizardState,
type ApplicationWizardStateUpdate,
type ApplicationWizardContext,
type ApplicationWizardContextUpdate,
} from "#admin/applications/wizard/steps/providers/shared";
import { ApplicationRequest } from "@goauthentik/api";
@@ -19,6 +19,12 @@ import { ApplicationRequest } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { property } from "lit/decorators.js";
export interface ApplicationDispatchInit {
update?: ApplicationWizardContextUpdate | null;
destination?: string | null;
details?: NavigationEventInit | null;
}
/**
* Base class for application wizard steps. Provides common functionality such as form handling and wizard state management.
*
@@ -28,7 +34,7 @@ export abstract class ApplicationWizardStep<T = Partial<ApplicationRequest>> ext
static styles = [...WizardStep.styles, ...ApplicationWizardStyles];
@property({ type: Object, attribute: false })
public wizard!: ApplicationWizardState;
public wizard!: ApplicationWizardContext;
protected override wizardTitle = msg("New application");
protected override wizardDescription = msg(
@@ -78,19 +84,15 @@ export abstract class ApplicationWizardStep<T = Partial<ApplicationRequest>> ext
// This pattern became visible during development, and the order is important: wizard updating
// and validation must complete before navigation is attempted.
public handleUpdate(
update?: ApplicationWizardStateUpdate,
destination?: string,
enable?: NavigationEventInit,
) {
public dispatchEvents({ update, destination, details }: ApplicationDispatchInit): void {
// Inform ApplicationWizard of content state
if (update) {
this.dispatchEvent(new WizardUpdateEvent(update));
}
// Inform WizardStepManager of steps state
if (destination || enable) {
this.dispatchEvent(new WizardNavigationEvent(destination, enable));
if (destination || details) {
this.dispatchEvent(new WizardNavigationEvent(details, destination));
}
}
}

View File

@@ -1,140 +0,0 @@
import "#components/ak-wizard/ak-wizard-steps";
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";
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";
import { ContextProvider } from "@lit/context";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
const freshWizardState = (): ApplicationWizardState => ({
providerModel: "",
currentBinding: -1,
app: {},
provider: {},
proxyMode: ProxyMode.Proxy,
bindings: [],
errors: {},
});
type ExtractProviderName<T extends string> = T extends `${string}.${infer Name}` ? Name : never;
type ProviderModelNameEnum = ExtractProviderName<ProviderModelEnum> | "samlproviderimportmodel";
export const providerTypePriority: ProviderModelNameEnum[] = [
"oauth2provider",
"samlprovider",
"samlproviderimportmodel",
"racprovider",
"proxyprovider",
"radiusprovider",
"ldapprovider",
"scimprovider",
"wsfederationprovider",
];
@customElement("ak-application-wizard-main")
export class AkApplicationWizardMain extends AKElement {
protected createRenderRoot(): HTMLElement | DocumentFragment {
return this;
}
@state()
protected wizard: ApplicationWizardState = freshWizardState();
protected wizardProviderProvider = new ContextProvider(this, {
context: applicationWizardProvidersContext,
initialValue: [],
});
constructor() {
super();
this.addEventListener(WizardUpdateEvent.eventName, this.handleUpdate);
}
public override connectedCallback() {
super.connectedCallback();
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
const providerNameToProviderMap = new Map(
providerTypes.map((providerType) => [providerType.modelName, providerType]),
);
const providersInOrder = providerTypePriority.map((name) =>
providerNameToProviderMap.get(name),
);
assertEveryPresent<TypeCreate>(
providersInOrder,
"Provider priority list includes name for which no provider model was returned.",
);
this.wizardProviderProvider.setValue(providersInOrder);
});
}
// This is the actual top of the Wizard; so this is where we accept the update information and
// incorporate it into the wizard.
handleUpdate(ev: WizardUpdateEvent<ApplicationWizardStateUpdate>) {
ev.stopPropagation();
const update = ev.content;
if (typeof update !== "undefined") {
this.wizard = {
...this.wizard,
...update,
};
}
}
render() {
return html`<ak-wizard-steps>
<ak-application-wizard-application-step
slot="application"
.wizard=${this.wizard}
></ak-application-wizard-application-step>
<ak-application-wizard-provider-choice-step
slot="provider-choice"
.wizard=${this.wizard}
></ak-application-wizard-provider-choice-step>
<ak-application-wizard-provider-step
slot="provider"
.wizard=${this.wizard}
></ak-application-wizard-provider-step>
<ak-application-wizard-bindings-step
slot="bindings"
.wizard=${this.wizard}
></ak-application-wizard-bindings-step>
<ak-application-wizard-edit-binding-step
slot="edit-binding"
.wizard=${this.wizard}
></ak-application-wizard-edit-binding-step>
<ak-application-wizard-submit-step
slot="submit"
.wizard=${this.wizard}
></ak-application-wizard-submit-step>
</ak-wizard-steps>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-main": AkApplicationWizardMain;
}
}

View File

@@ -1,46 +1,147 @@
import "#admin/applications/wizard/ak-application-wizard-main";
import "#components/ak-wizard/ak-wizard-steps";
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 { AKModal } from "#elements/modals/ak-modal";
import { SlottedTemplateResult } from "#elements/types";
import { DEFAULT_CONFIG } from "#common/api/config";
import { assertEveryPresent } from "#common/utils";
import { WizardCloseEvent } from "#components/ak-wizard/events";
import { listen } from "#elements/decorators/listen";
import { CreateWizard } from "#elements/wizard/CreateWizard";
import { WizardUpdateEvent } from "#components/ak-wizard/events";
import { applicationWizardProvidersContext } from "#admin/applications/wizard/ContextIdentity";
import {
type ApplicationWizardContext,
type ApplicationWizardContextUpdate,
} from "#admin/applications/wizard/steps/providers/shared";
import type { TypeCreate } from "@goauthentik/api";
import { ProviderModelEnum, ProvidersApi, ProxyMode } from "@goauthentik/api";
import { ContextProvider } from "@lit/context";
import { msg } from "@lit/localize";
import { css, CSSResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
const createWizardContextValue = (): ApplicationWizardContext => ({
providerModel: "",
currentBinding: -1,
app: {},
provider: {},
proxyMode: ProxyMode.Proxy,
bindings: [],
errors: {},
});
type ExtractProviderName<T extends string> = T extends `${string}.${infer Name}` ? Name : never;
type ProviderModelNameEnum = ExtractProviderName<ProviderModelEnum> | "samlproviderimportmodel";
export const providerTypePriority: ProviderModelNameEnum[] = [
"oauth2provider",
"samlprovider",
"samlproviderimportmodel",
"racprovider",
"proxyprovider",
"radiusprovider",
"ldapprovider",
"scimprovider",
"wsfederationprovider",
];
@customElement("ak-application-wizard")
export class AkApplicationWizard extends AKModal {
public static override formatARIALabel?(): string {
return msg("New Application Wizard");
export class AKApplicationWizard extends CreateWizard {
#api = new ProvidersApi(DEFAULT_CONFIG);
public static override verboseName = msg("Application");
public static override verboseNamePlural = msg("Applications");
protected createRenderRoot(): HTMLElement | DocumentFragment {
return this;
}
public static override styles: CSSResult[] = [
...super.styles,
css`
[part="main"] {
display: block;
}
`,
];
@state()
protected context: ApplicationWizardContext = createWizardContextValue();
constructor() {
super();
protected wizardProviderProvider = new ContextProvider(this, {
context: applicationWizardProvidersContext,
initialValue: [],
});
this.addEventListener(WizardCloseEvent.eventName, this.closeListener);
public override refresh = (): Promise<void> => {
return this.#api.providersAllTypesList().then((providerTypes) => {
const providerNameToProviderMap = new Map(
providerTypes.map((providerType) => [providerType.modelName, providerType]),
);
const providersInOrder = providerTypePriority.map((name) =>
providerNameToProviderMap.get(name),
);
assertEveryPresent<TypeCreate>(
providersInOrder,
"Provider priority list includes name for which no provider model was returned.",
);
this.wizardProviderProvider.setValue(providersInOrder);
});
};
// This is the actual top of the Wizard; so this is where we accept the update information and
// incorporate it into the wizard.
/**
* Handles updates to the wizard context, which are emitted by the individual steps when their data changes.
*/
@listen(WizardUpdateEvent)
handleUpdate(ev: WizardUpdateEvent<ApplicationWizardContextUpdate>) {
ev.stopPropagation();
const update = ev.content;
if (update) {
this.context = {
...this.context,
...update,
};
}
}
protected renderCloseButton(): SlottedTemplateResult {
return null;
}
render() {
return html`<ak-application-wizard-main part="main"></ak-application-wizard-main>`;
protected override render() {
return html`<ak-wizard-steps>
<ak-application-wizard-application-step
slot="application"
.wizard=${this.context}
></ak-application-wizard-application-step>
<ak-application-wizard-provider-choice-step
slot="provider-choice"
.wizard=${this.context}
></ak-application-wizard-provider-choice-step>
<ak-application-wizard-provider-step
slot="provider"
.wizard=${this.context}
></ak-application-wizard-provider-step>
<ak-application-wizard-bindings-step
slot="bindings"
.wizard=${this.context}
></ak-application-wizard-bindings-step>
<ak-application-wizard-edit-binding-step
slot="edit-binding"
.wizard=${this.context}
></ak-application-wizard-edit-binding-step>
<ak-application-wizard-submit-step
slot="submit"
.wizard=${this.context}
></ak-application-wizard-submit-step>
</ak-wizard-steps>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard": AkApplicationWizard;
"ak-application-wizard": AKApplicationWizard;
}
}

View File

@@ -1,34 +0,0 @@
import { AKElement } from "#elements/Base";
import { css, html } from "lit";
import { customElement } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
@customElement("ak-wizard-title")
export class AkWizardTitle extends AKElement {
static styles = [
PFContent,
PFTitle,
css`
.ak-bottom-spacing {
padding-bottom: var(--pf-global--spacer--lg);
}
`,
];
render() {
return html`<div class="ak-bottom-spacing pf-c-content">
<h3><slot></slot></h3>
</div>`;
}
}
export default AkWizardTitle;
declare global {
interface HTMLElementTagNameMap {
"ak-wizard-title": AkWizardTitle;
}
}

View File

@@ -50,8 +50,8 @@ const renderSAMLImportOverview: ProviderOverview<ProvidersSamlImportMetadataCrea
provider,
) => {
return renderSummary("SAML", provider.name, [
[msg("Authorization flow"), provider.authorizationFlow ?? "-"],
[msg("Invalidation flow"), provider.invalidationFlow ?? "-"],
[msg("Authorization Flow"), provider.authorizationFlow ?? "-"],
[msg("Invalidation Flow"), provider.invalidationFlow ?? "-"],
]);
};
@@ -153,7 +153,7 @@ const renderOAuth2Overview: ProviderOverview<OAuth2Provider> = (provider) => {
const label = provider.clientType ? clientTypeToLabel[provider.clientType]() : "";
return renderSummary("OAuth2", provider.name, [
[msg("Client type"), label],
[msg("Client Type"), label],
[msg("Client ID"), provider.clientId],
[msg("Redirect URIs"), formatRedirectUris(provider.redirectUris)],
]);

View File

@@ -1,4 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import "#components/ak-file-search-input";
import "#components/ak-radio-input";
import "#components/ak-slug-input";
@@ -16,7 +15,7 @@ import { type NavigableButton, type WizardButton } from "#components/ak-wizard/s
import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep";
import {
ApplicationWizardStateUpdate,
ApplicationWizardContextUpdate,
WizardValidationRecord,
} from "#admin/applications/wizard/steps/providers/shared";
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
@@ -61,13 +60,11 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
: (this.wizard.errors?.app?.[name] ?? this.wizard.errors?.app?.[snakeCase(name)] ?? []);
}
get buttons(): WizardButton[] {
return [
// ---
{ kind: "cancel" },
{ kind: "next", destination: "provider-choice" },
];
}
protected buttons: WizardButton[] = [
// ---
{ kind: "cancel" },
{ kind: "next", destination: "provider-choice" },
];
get valid() {
this.errors = new Map();
@@ -95,7 +92,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
}
if (!this.valid) {
this.handleEnabling({
this.dispatchNavigationEvent({
disabled: ["provider-choice", "provider", "bindings", "submit"],
});
@@ -104,24 +101,26 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
const app = { ...this.formValues };
const payload: ApplicationWizardStateUpdate = {
const update: ApplicationWizardContextUpdate = {
app,
errors: omitKeys(this.wizard.errors, "app"),
};
if (!this.wizard.provider?.name?.trim() && app.name) {
payload.provider = {
update.provider = {
name: `Provider for ${app.name}`,
};
}
this.handleUpdate(payload, button.destination, {
enable: "provider-choice",
return this.dispatchEvents({
update,
destination: button.destination,
details: { enable: "provider-choice" },
});
}
protected renderForm(app: Partial<ApplicationRequest>, errors: WizardValidationRecord = {}) {
return html` <ak-wizard-title>${msg("Configure the Application")}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${msg("Configure the Application")}</h3>
<form id="applicationform" class="pf-c-form pf-m-horizontal" slot="form">
<ak-text-input
name="name"

View File

@@ -1,5 +1,4 @@
import "#elements/EmptyState";
import "#admin/applications/wizard/ak-wizard-title";
import "#components/ak-radio-input";
import "#components/ak-slug-input";
import "#components/ak-status-label";
@@ -40,13 +39,11 @@ const COLUMNS = [
export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
label = msg("Configure Bindings");
get buttons(): WizardButton[] {
return [
{ kind: "cancel" },
{ kind: "back", destination: "provider" },
{ kind: "next", destination: "submit" },
];
}
protected buttons: WizardButton[] = [
{ kind: "cancel" },
{ kind: "back", destination: "provider" },
{ kind: "next", destination: "submit" },
];
@query("ak-select-table")
selectTable!: SelectTable;
@@ -64,6 +61,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
get bindingsAsColumns() {
return this.wizard.bindings.map((binding, index) => {
const { order, enabled, timeout } = binding;
const isSet = P.union(P.string.minLength(1), P.number);
const policy = match(binding)
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
@@ -89,26 +87,32 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
// TODO Fix those dispatches so that we handle them here, in this component, and *choose* how to
// forward them.
onBindingEvent(binding?: number) {
this.handleUpdate({ currentBinding: binding ?? -1 }, "edit-binding", {
enable: "edit-binding",
this.dispatchEvents({
update: { currentBinding: binding ?? -1 },
destination: "edit-binding",
details: { enable: "edit-binding" },
});
}
onDeleteBindings() {
protected onDeleteBindings() {
const toDelete = this.selectTable
.json()
.map((i) => (typeof i === "string" ? parseInt(i, 10) : i));
const bindings = this.wizard.bindings.filter((binding, index) => !toDelete.includes(index));
this.handleUpdate({ bindings }, "bindings");
return this.dispatchEvents({
update: { bindings },
destination: "bindings",
});
}
renderEmptyCollection() {
return html`<ak-wizard-title
>${msg("Configure Policy/User/Group Bindings")}</ak-wizard-title
>
<h6 class="pf-c-title pf-m-md">
protected renderEmptyCollection() {
return html`<h3 class="pf-c-wizard__main-title">
${msg("Configure Policy/User/Group Bindings")}
</h3>
<h4 class="pf-c-title pf-m-md">
${msg("These policies control which users can access this application.")}
</h6>
</h4>
<div class="pf-c-card">
<ak-application-wizard-bindings-toolbar
@clickNew=${() => this.onBindingEvent()}
@@ -136,11 +140,11 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
</div>`;
}
renderCollection() {
return html` <ak-wizard-title>${msg("Configure Policy Bindings")}</ak-wizard-title>
<h6 class="pf-c-title pf-m-md">
protected renderCollection() {
return html`<h3 class="pf-c-wizard__main-title">${msg("Configure Policy Bindings")}</h3>
<h4 class="pf-c-title pf-m-md">
${msg("These policies control which users can access this application.")}
</h6>
</h4>
<ak-application-wizard-bindings-toolbar
@clickNew=${() => this.onBindingEvent()}
@clickDelete=${() => this.onDeleteBindings()}
@@ -155,7 +159,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
></ak-select-table>`;
}
renderMain() {
protected renderMain() {
if ((this.wizard.bindings ?? []).length === 0) {
return this.renderEmptyCollection();
}

View File

@@ -1,5 +1,4 @@
import "#components/ak-number-input";
import "#admin/applications/wizard/ak-wizard-title";
import "#components/ak-radio-input";
import "#components/ak-switch-input";
import "#components/ak-text-input";
@@ -27,7 +26,7 @@ import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWiz
import { CoreApi, Group, PoliciesApi, Policy, PolicyBinding, User } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { msg, str } from "@lit/localize";
import { html, nothing } from "lit";
import { customElement, query, state } from "lit/decorators.js";
@@ -50,17 +49,15 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep<Poli
@state()
policyGroupUser: PolicyBindingCheckTarget = PolicyBindingCheckTarget.Policy;
instanceId = -1;
protected instanceId = -1;
instance?: PolicyBinding;
protected instance: PolicyBinding | null = null;
get buttons(): WizardButton[] {
return [
{ kind: "cancel" },
{ kind: "next", label: msg("Save Binding"), destination: "bindings" },
{ kind: "back", destination: "bindings" },
];
}
protected buttons: WizardButton[] = [
{ kind: "cancel" },
{ kind: "back", destination: "bindings" },
{ kind: "next", label: msg("Save Binding"), destination: "bindings" },
];
public override handleButton(button: NavigableButton) {
if (button.kind === "next") {
@@ -84,12 +81,14 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep<Poli
}
this.instanceId = -1;
this.handleUpdate({ bindings }, "bindings");
return;
return this.dispatchEvents({
update: { bindings },
destination: "bindings",
});
}
super.handleButton(button);
return super.handleButton(button);
}
// The search select configurations for the three different types of fetches that we care about,
@@ -153,7 +152,7 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep<Poli
}
}
renderSearch(title: string, policyKind: PolicyBindingCheckTarget) {
protected renderSearch(title: string, policyKind: PolicyBindingCheckTarget) {
if (policyKind !== this.policyGroupUser) {
return nothing;
}
@@ -163,12 +162,15 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep<Poli
.config=${this.searchSelectConfigs(policyKind)}
class="policy-search-select"
blankable
placeholder=${msg(str`Select a ${title}...`)}
></ak-search-select-ez>
</ak-form-element-horizontal>`;
}
renderForm(instance?: PolicyBinding) {
return html`<ak-wizard-title>${msg("Create a Policy/User/Group Binding")}</ak-wizard-title>
protected renderForm(instance?: PolicyBinding | null) {
return html`<h3 class="pf-c-wizard__main-title">
${msg("Create a Policy/User/Group Binding")}
</h3>
<form id="bindingform" class="pf-c-form pf-m-horizontal" slot="form">
<div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body">
@@ -222,16 +224,18 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep<Poli
</form>`;
}
renderMain() {
protected renderMain() {
if (!(this.wizard.bindings && this.wizard.errors)) {
throw new Error("Application Step received uninitialized wizard context.");
}
const currentBinding = this.wizard.currentBinding ?? -1;
if (this.instanceId !== currentBinding) {
this.instanceId = currentBinding;
this.instance =
this.instanceId === -1 ? undefined : this.wizard.bindings[this.instanceId];
this.instance = this.instanceId === -1 ? null : this.wizard.bindings[this.instanceId];
}
return this.renderForm(this.instance);
}
}

View File

@@ -1,4 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import "#elements/EmptyState";
import "#elements/forms/FormGroup";
import "#elements/forms/HorizontalFormElement";
@@ -6,8 +5,8 @@ import "#elements/wizard/TypeCreateWizardPage";
import { applicationWizardProvidersContext } from "../ContextIdentity.js";
import { bound } from "#elements/decorators/bound";
import { WithLicenseSummary } from "#elements/mixins/license";
import { SlottedTemplateResult } from "#elements/types";
import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage";
import type { NavigableButton, WizardButton } from "#components/ak-wizard/shared";
@@ -19,6 +18,7 @@ import type { TypeCreate } from "@goauthentik/api";
import { consume } from "@lit/context";
import { msg } from "@lit/localize";
import { html } from "lit";
import { guard } from "lit-html/directives/guard.js";
import { customElement, state } from "lit/decorators.js";
/**
@@ -30,65 +30,69 @@ export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(Appl
label = msg("Choose a Provider");
@state()
failureMessage = "";
protected failureMessage = "";
@consume({ context: applicationWizardProvidersContext, subscribe: true })
public providerModelsList!: TypeCreate[];
get buttons(): WizardButton[] {
return [
{ kind: "cancel" },
{ kind: "back", destination: "application" },
{ kind: "next", destination: "provider" },
];
}
protected buttons: WizardButton[] = [
{ kind: "cancel" },
{ kind: "back", destination: "application" },
{ kind: "next", destination: "provider" },
];
public override handleButton(button: NavigableButton) {
this.failureMessage = "";
if (button.kind === "next") {
if (!this.wizard.providerModel) {
this.failureMessage = msg("Please choose a provider type before proceeding.");
this.handleEnabling({ disabled: ["provider", "bindings", "submit"] });
this.dispatchNavigationEvent({ disabled: ["provider", "bindings", "submit"] });
return;
}
this.handleUpdate(undefined, button.destination, { enable: "provider" });
return;
return this.dispatchEvents({
destination: button.destination,
details: { enable: "provider" },
});
}
super.handleButton(button);
return super.handleButton(button);
}
@bound
onSelect(ev: CustomEvent<TypeCreate>) {
ev.stopPropagation();
const detail: TypeCreate = ev.detail;
this.handleUpdate({ providerModel: detail.modelName });
}
protected typeSelectListener = (event: CustomEvent<TypeCreate>) => {
return this.dispatchEvents({
update: {
...this.wizard,
providerModel: event.detail.modelName,
},
details: { enable: "provider" },
});
};
renderMain() {
const selectedTypes = this.providerModelsList.filter(
(t) => t.modelName === this.wizard.providerModel,
);
protected renderMain(): SlottedTemplateResult {
const { providerModelsList } = this;
return this.providerModelsList.length > 0
? html` <ak-wizard-title>${msg("Choose a Provider Type")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal">
<ak-wizard-page-type-create
.types=${this.providerModelsList}
layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
@select=${(ev: CustomEvent<TypeCreate>) => {
this.handleUpdate(
{
...this.wizard,
providerModel: ev.detail.modelName,
},
undefined,
{ enable: "provider" },
);
}}
></ak-wizard-page-type-create>
</form>`
: html`<ak-empty-state default-label></ak-empty-state>`;
return guard([providerModelsList], () => {
if (!providerModelsList.length) {
return html`<ak-empty-state default-label></ak-empty-state>`;
}
const selectedTypes = providerModelsList.filter(
(t) => t.modelName === this.wizard.providerModel,
);
return html`<h3 class="pf-c-wizard__main-title">${msg("Choose a Provider Type")}</h3>
<form class="pf-c-form pf-m-horizontal">
<ak-wizard-page-type-create
.types=${providerModelsList}
layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : null}
@ak-type-create-select=${this.typeSelectListener}
></ak-wizard-page-type-create>
</form>`;
});
}
}

View File

@@ -75,11 +75,12 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep {
public override handleButton(button: NavigableButton) {
if (button.kind === "next") {
if (!this.valid) {
this.handleEnabling({
this.dispatchNavigationEvent({
disabled: ["bindings", "submit"],
});
return;
}
const payload = {
provider: {
...this.formValues,
@@ -87,21 +88,22 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep {
},
errors: omitKeys(this.wizard.errors, "provider"),
};
this.handleUpdate(payload, button.destination, {
enable: ["bindings", "submit"],
return this.dispatchEvents({
update: payload,
destination: button.destination,
details: { enable: ["bindings", "submit"] },
});
return;
}
super.handleButton(button);
return super.handleButton(button);
}
get buttons(): WizardButton[] {
return [
{ kind: "cancel" },
{ kind: "back", destination: "provider-choice" },
{ kind: "next", destination: "bindings" },
];
}
protected buttons: WizardButton[] = [
{ kind: "cancel" },
{ kind: "back", destination: "provider-choice" },
{ kind: "next", destination: "bindings" },
];
renderMain() {
if (!this.wizard.providerModel) {

View File

@@ -1,5 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EVENT_REFRESH } from "#common/constants";
import { parseAPIResponseError } from "#common/errors/network";
@@ -154,7 +152,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
return;
}
this.handleUpdate({ errors: parsedError });
this.dispatchEvents({ update: { errors: parsedError } });
this.state = "reviewing";
}
}
@@ -243,7 +241,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
}
}
this.handleUpdate({ errors: parsedError });
this.dispatchEvents({ update: { errors: parsedError } });
this.state = "reviewing";
});
}
@@ -269,19 +267,22 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
});
}
get buttons(): WizardButton[] {
const forReview: WizardButton[] = [
{ kind: "cancel" },
{ kind: "back", destination: "bindings" },
{ kind: "next", label: msg("Create Application"), destination: "here" },
];
const forSubmit: WizardButton[] = [{ kind: "close" }];
protected get buttons(): WizardButton[] {
return match(this.state)
.with("submitted", () => forSubmit)
.with("submitted", () => {
return [
{ kind: "close" },
{ kind: "finish", destination: "close" },
] satisfies WizardButton[];
})
.with("reviewing", () => {
return [
{ kind: "cancel" },
{ kind: "back", destination: "bindings" },
{ kind: "next", label: msg("Create Application"), destination: "here" },
] satisfies WizardButton[];
})
.with("running", () => [])
.with("reviewing", () => forReview)
.exhaustive();
}
@@ -377,36 +378,53 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
const metaLaunchUrl = app.metaLaunchUrl?.trim();
return html`
<div class="ak-wizard-main-content">
<ak-wizard-title>${msg("Review the Application and Provider")}</ak-wizard-title>
<h2 class="pf-c-title pf-m-xl">${msg("Application")}</h2>
<dl class="pf-c-description-list">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Name")}</dt>
<dt class="pf-c-description-list__description">${app.name}</dt>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Group")}</dt>
<dt class="pf-c-description-list__description">${app.group || msg("-")}</dt>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Policy engine mode")}</dt>
<dt class="pf-c-description-list__description">
${app.policyEngineMode?.toUpperCase()}
</dt>
</div>
${metaLaunchUrl
? html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Launch URL")}</dt>
<dt class="pf-c-description-list__description">${metaLaunchUrl}</dt>
</div>`
: nothing}
</dl>
${renderer
? html`<h2 class="pf-c-title pf-m-xl pf-u-pt-xl">${msg("Provider")}</h2>
${renderer(provider)}`
: nothing}
return html`<h2 class="pf-c-wizard__main-title">
${msg("Review the Application and Provider")}
</h2>
<fieldset>
<legend>${msg("Application Details")}</legend>
<dl class="pf-c-description-list">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Application Name")}</dt>
<dt class="pf-c-description-list__description">${app.name}</dt>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Group")}</dt>
<dt class="pf-c-description-list__description">
${app.group || msg("-")}
</dt>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
${msg("Policy engine mode")}
</dt>
<dt class="pf-c-description-list__description">
${app.policyEngineMode?.toUpperCase()}
</dt>
</div>
${
metaLaunchUrl
? html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
${msg("Launch URL")}
</dt>
<dt class="pf-c-description-list__description">
${metaLaunchUrl}
</dt>
</div>`
: nothing
}
</dl>
</fieldset>
${
renderer
? html`<fieldset>
<legend>${msg("Provider Details")}</legend>
${renderer(provider)}
</fieldset>`
: null
}
</div>
`;
}

View File

@@ -11,7 +11,7 @@ import { serializeForm } from "#elements/forms/serialization";
import { ApplicationWizardStyles } from "#admin/applications/wizard/ApplicationWizardFormStepStyles.styles";
import {
ApplicationTransactionValidationError,
type ApplicationWizardState,
type ApplicationWizardContext,
ApplicationWizardStateError,
type OneOfProvider,
} from "#admin/applications/wizard/steps/providers/shared";
@@ -30,7 +30,7 @@ export abstract class ApplicationWizardProviderForm<
public abstract label: string;
@property({ type: Object, attribute: false })
public wizard!: ApplicationWizardState<P, E>;
public wizard!: ApplicationWizardContext<P, E>;
@property({ type: Object, attribute: false })
public errors: E = {} as E;

View File

@@ -1,5 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import { WithBrandConfig } from "#elements/mixins/branding";
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
@@ -22,12 +20,10 @@ export class ApplicationWizardLdapProviderForm extends WithBrandConfig(
label = msg("Configure LDAP Provider");
renderForm(provider: LDAPProvider, errors: WizardValidationRecord) {
return html`
<ak-wizard-title>${this.label}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${this.label}</h3>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm({ provider, errors, brand: this.brand })}
</form>
`;
</form>`;
}
render() {

View File

@@ -1,5 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import { DEFAULT_CONFIG } from "#common/api/config";
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
@@ -44,7 +42,7 @@ export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProvid
const showLogoutMethodCallback = (show: boolean) => {
this.showLogoutMethod = show;
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${this.label}</h3>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm({
provider,

View File

@@ -1,5 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import { WizardUpdateEvent } from "#components/ak-wizard/events";
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
@@ -39,7 +37,7 @@ export class ApplicationWizardProxyProviderForm extends ApplicationWizardProvide
this.showHttpBasic = el.checked;
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${this.label}</h3>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm({
provider,

View File

@@ -1,4 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import "#admin/common/ak-crypto-certificate-search";
import "#admin/common/ak-flow-search/ak-flow-search";
import "#components/ak-text-input";
@@ -23,8 +22,7 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF
label = msg("Configure Remote Access Provider");
renderForm(provider: RACProvider) {
return html`
<ak-wizard-title>${this.label}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${this.label}</h3>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
<ak-text-input
name="name"
@@ -36,7 +34,7 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
label=${msg("Authorization Flow")}
required
>
<ak-flow-search
@@ -75,8 +73,7 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>
`;
</form>`;
}
render() {

View File

@@ -1,5 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import { WithBrandConfig } from "#elements/mixins/branding";
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
@@ -19,7 +17,7 @@ export class ApplicationWizardRadiusProviderForm extends WithBrandConfig(
label = msg("Configure Radius Provider");
renderForm(provider: RadiusProvider, errors: WizardValidationRecord = {}) {
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${this.label}</h3>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm({ provider, errors, brand: this.brand })}
</form>`;

View File

@@ -1,5 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import { createFileMap } from "#elements/utils/inputs";
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
@@ -30,12 +28,10 @@ export class ApplicationWizardProviderSamlMetadataForm extends ApplicationWizard
}
renderForm() {
return html`
<ak-wizard-title>${this.label}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${this.label}</h3>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm(this.wizard.provider)}
</form>
`;
</form>`;
}
render() {

View File

@@ -1,4 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import "#elements/forms/FormGroup";
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
@@ -80,7 +79,7 @@ export class ApplicationWizardProviderSamlForm extends ApplicationWizardProvider
this.logoutMethod = target.value;
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${this.label}</h3>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm({
provider: this.wizard.provider,

View File

@@ -1,4 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import "#elements/forms/FormGroup";
import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm";
@@ -18,7 +17,7 @@ export class ApplicationWizardSCIMProvider extends ApplicationWizardProviderForm
propertyMappings?: PaginatedSCIMMappingList;
render() {
return html`<ak-wizard-title>${this.label}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${this.label}</h3>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm({
update: this.requestUpdate.bind(this),

View File

@@ -1,4 +1,3 @@
import "#admin/applications/wizard/ak-wizard-title";
import "#elements/forms/FormGroup";
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
@@ -30,7 +29,7 @@ export class ApplicationWizardProviderWSFedForm extends ApplicationWizardProvide
this.signingKeyType = target.selectedKeypair?.keyType ?? KeyTypeEnum.Rsa;
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
return html`<h3 class="pf-c-wizard__main-title">${this.label}</h3>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm({
provider: this.wizard.provider as WSFederationProvider,

View File

@@ -67,7 +67,7 @@ export type ApplicationWizardStateError = ValidationError | ApplicationTransacti
// configured bindings" page in the wizard. The PolicyBinding is converted into a
// PolicyBindingRequest during the submission phase.
export interface ApplicationWizardState<
export interface ApplicationWizardContext<
P extends OneOfProvider = OneOfProvider,
E = ApplicationTransactionValidationError,
> {
@@ -80,7 +80,7 @@ export interface ApplicationWizardState<
errors: E;
}
export interface ApplicationWizardStateUpdate {
export interface ApplicationWizardContextUpdate {
app?: Partial<ApplicationRequest>;
providerModel?: string;
provider?: OneOfProvider;

View File

@@ -31,6 +31,9 @@ export enum BlueprintSource {
@customElement("ak-blueprint-form")
export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
public static override verboseName = msg("Blueprint");
public static override verboseNamePlural = msg("Blueprints");
@state()
protected source: BlueprintSource = BlueprintSource.File;

View File

@@ -4,6 +4,7 @@ import "#elements/forms/HorizontalFormElement";
import "#components/ak-toggle-group";
import { DEFAULT_CONFIG } from "#common/api/config";
import { PFSize } from "#common/enums";
import { Form } from "#elements/forms/Form";
import { PreventFormSubmit } from "#elements/forms/helpers";
@@ -34,6 +35,14 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList
export class BlueprintImportForm extends Form<ManagedBlueprintsImportCreateRequest> {
static styles: CSSResult[] = [...super.styles, PFDescriptionList, PFBanner];
public static override verboseName = msg("Flow Blueprint");
public static override verboseNamePlural = msg("Flow Blueprints");
public static override createLabel = msg("Import");
public static override submitVerb = msg("Import");
public static override submittingVerb = msg("Importing");
public override size = PFSize.Medium;
@state()
protected result: BlueprintImportResult | null = null;
@@ -98,6 +107,11 @@ export class BlueprintImportForm extends Form<ManagedBlueprintsImportCreateReque
</ak-toggle-group>
${this.source === BlueprintSource.Upload
? html`
${this.findSlotted("banner-warning")
? html`<div class="pf-c-banner pf-m-warning" slot="above-form">
<slot name="banner-warning"></slot>
</div>`
: null}
<ak-form-element-horizontal name="blueprint">
${AKLabel(
{
@@ -123,24 +137,20 @@ export class BlueprintImportForm extends Form<ManagedBlueprintsImportCreateReque
".yaml files, which can be found in the Example Flows documentation",
)}
</p>
${this.hasSlotted("read-more-link")
${this.findSlotted("read-more-link")
? html`<p class="pf-c-form__helper-text">
${msg("Read more about")}&nbsp;
<slot name="read-more-link"></slot>
</p>`
: nothing}
: null}
</div>
</ak-form-element-horizontal>
${this.hasSlotted("banner-warning")
? html`<div class="pf-c-banner pf-m-warning" slot="above-form">
<slot name="banner-warning"></slot>
</div>`
: nothing}
`
: nothing}
: null}
${this.source === BlueprintSource.File
? html`<ak-form-element-horizontal label=${msg("Path")} name="path">
<ak-search-select
placeholder=${msg("Select a blueprint...")}
.fetchObjects=${async (query?: string): Promise<BlueprintFile[]> => {
const items = await new ManagedApi(
DEFAULT_CONFIG,

View File

@@ -14,10 +14,14 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { EVENT_REFRESH } from "#common/constants";
import { docLink } from "#common/global";
import { IconEditButton, modalInvoker, ModalInvokerButton } from "#elements/dialogs";
import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton";
import { PaginatedResponse, TableColumn, Timestamp } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { BlueprintForm } from "#admin/blueprints/BlueprintForm";
import {
BlueprintInstance,
BlueprintInstanceStatusEnum,
@@ -26,8 +30,9 @@ import {
} from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { CSSResult, html, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { CSSResult, html, nothing } from "lit";
import { guard } from "lit-html/directives/guard.js";
import { customElement } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
@@ -58,27 +63,28 @@ export function formatBlueprintDescription(item: BlueprintInstance): string | nu
@customElement("ak-blueprint-list")
export class BlueprintListPage extends TablePage<BlueprintInstance> {
static styles: CSSResult[] = [...super.styles, PFDescriptionList];
protected override searchEnabled = true;
public pageTitle = msg("Blueprints");
public pageDescription = msg("Automate and template configuration within authentik.");
public pageIcon = "pf-icon pf-icon-blueprint";
expandable = true;
checkbox = true;
clearOnRefresh = true;
public override expandable = true;
public override checkbox = true;
public override clearOnRefresh = true;
public override searchPlaceholder = msg("Search for a blueprint by name or path...");
@property()
order = "name";
public override order = "name";
static styles: CSSResult[] = [...super.styles, PFDescriptionList];
async apiEndpoint(): Promise<PaginatedResponse<BlueprintInstance>> {
protected override async apiEndpoint(): Promise<PaginatedResponse<BlueprintInstance>> {
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsList(
await this.defaultEndpointConfig(),
);
}
protected columns: TableColumn[] = [
protected override columns: TableColumn[] = [
[msg("Name"), "name"],
[msg("Status"), "status"],
[msg("Last applied"), "last_applied"],
@@ -86,7 +92,7 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Blueprint(s)")}
@@ -111,7 +117,7 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
</ak-forms-delete-bulk>`;
}
renderExpanded(item: BlueprintInstance): TemplateResult {
protected override renderExpanded(item: BlueprintInstance): SlottedTemplateResult {
const [appLabel, modelName] = ModelEnum.AuthentikBlueprintsBlueprintinstance.split(".");
return html`<dl class="pf-c-description-list pf-m-horizontal">
@@ -144,7 +150,7 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
</dl>`;
}
row(item: BlueprintInstance): SlottedTemplateResult[] {
protected override row(item: BlueprintInstance): SlottedTemplateResult[] {
const description = formatBlueprintDescription(item);
return [
@@ -152,30 +158,16 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
${description
? html`<small><ak-mdx .content=${description}></ak-mdx></small>`
: nothing}`,
html`${BlueprintStatus(item)}`,
BlueprintStatus(item),
Timestamp(item.lastApplied),
html`<ak-status-label ?good=${item.enabled}></ak-status-label>`,
html`<div>
<ak-forms-modal>
<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
slot="trigger"
class="pf-c-button pf-m-plain"
aria-label=${msg(str`Edit "${item.name}" blueprint`)}
>
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
<ak-rbac-object-permission-modal
label=${item.name}
model=${ModelEnum.AuthentikBlueprintsBlueprintinstance}
objectPk=${item.pk}
>
</ak-rbac-object-permission-modal>
html`<div class="ak-c-table__actions">
${IconEditButton(BlueprintForm, item.pk, item.name)}
${IconPermissionButton(item.name, {
model: ModelEnum.AuthentikBlueprintsBlueprintinstance,
objectPk: item.pk,
})}
<ak-action-button
class="pf-m-plain"
label=${msg(str`Apply "${item.name}" blueprint`)}
@@ -202,36 +194,36 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">${msg("Create")}</span>
<span slot="header">${msg("Create Blueprint Instance")}</span>
<ak-blueprint-form slot="form"> </ak-blueprint-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
<ak-forms-modal>
<span slot="submit">${msg("Import")}</span>
<span slot="header">${msg("Import Blueprint")}</span>
<ak-blueprint-import-form slot="form">
<a
target="_blank"
rel="noopener noreferrer"
href=${docLink("/customize/blueprints/working_with_blueprints/")}
slot="read-more-link"
>${msg("Flow Examples")}</a
>
<span slot="banner-warning">
${msg(
"Warning: Blueprint files may contain objects such as users, policies and expression.",
)}<br />${msg(
"You should only import files from trusted sources and review blueprints before importing them.",
)}
</span>
</ak-blueprint-import-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">${msg("Import")}</button>
</ak-forms-modal>
`;
protected override renderObjectCreate(): SlottedTemplateResult {
return guard([], () => {
return [
ModalInvokerButton(BlueprintForm),
html`<button
class="pf-c-button pf-m-primary"
type="button"
${modalInvoker(() => {
return html`<ak-blueprint-import-form>
<a
target="_blank"
rel="noopener noreferrer"
href=${docLink("/customize/blueprints/working_with_blueprints/")}
slot="read-more-link"
>${msg("Flow Examples")}</a
>
<span slot="banner-warning">
${msg(
"Warning: Blueprint files may contain objects such as users, policies and expression.",
)}<br />${msg(
"You should only import files from trusted sources and review blueprints before importing them.",
)}
</span>
</ak-blueprint-import-form>`;
})}
>
${msg("Import")}
</button>`,
];
});
}
}

View File

@@ -36,6 +36,9 @@ import { customElement } from "lit/decorators.js";
@customElement("ak-brand-form")
export class BrandForm extends ModelForm<Brand, string> {
public static override verboseName = msg("Brand");
public static override verboseNamePlural = msg("Brands");
loadInstance(pk: string): Promise<Brand> {
return new CoreApi(DEFAULT_CONFIG).coreBrandsRetrieve({
brandUuid: pk,
@@ -192,7 +195,7 @@ export class BrandForm extends ModelForm<Brand, string> {
<ak-form-group label="${msg("Default flows")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
label=${msg("Authentication Flow")}
name="flowAuthentication"
>
<ak-flow-search
@@ -207,7 +210,7 @@ export class BrandForm extends ModelForm<Brand, string> {
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
label=${msg("Invalidation Flow")}
name="flowInvalidation"
>
<ak-flow-search

View File

@@ -8,14 +8,17 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { BrandForm } from "#admin/brands/BrandForm";
import { Brand, CoreApi, ModelEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("ak-brand-list")
@@ -47,8 +50,9 @@ export class BrandListPage extends TablePage<Brand> {
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Brand(s)")}
.objects=${this.selectedElements}
@@ -72,22 +76,13 @@ export class BrandListPage extends TablePage<Brand> {
</ak-forms-delete-bulk>`;
}
row(item: Brand): SlottedTemplateResult[] {
protected override row(item: Brand): SlottedTemplateResult[] {
return [
html`${item.domain}`,
html`${item.brandingTitle}`,
item.domain,
item.brandingTitle || msg("-"),
html`<ak-status-label ?good=${item._default}></ak-status-label>`,
html`<div>
<ak-forms-modal>
<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">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
html`<div class="ak-c-table__actions">
${IconEditButton(BrandForm, item.brandUuid, item.brandingTitle)}
<ak-rbac-object-permission-modal
model=${ModelEnum.AuthentikBrandsBrand}
@@ -98,15 +93,8 @@ export class BrandListPage extends TablePage<Brand> {
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">${msg("Create Brand")}</span>
<span slot="header">${msg("New Brand")}</span>
<ak-brand-form slot="form"> </ak-brand-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("New Brand")}</button>
</ak-forms-modal>
`;
protected override renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(BrandForm);
}
}

View File

@@ -1,3 +1,5 @@
import "#components/ak-text-input";
import "#components/ak-number-input";
import "#elements/forms/Radio";
import "#elements/forms/HorizontalFormElement";
@@ -17,7 +19,12 @@ import { html, TemplateResult } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("ak-crypto-certificate-generate-form")
export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
export class CryptoCertificateGenerateForm extends Form<CertificateGenerationRequest> {
public static override verboseName = msg("Certificate-Key Pair");
public static override verboseNamePlural = msg("Certificate-Key Pairs");
public static override createLabel = msg("Generate");
public static override submitVerb = msg("Generate");
getSuccessMessage(): string {
return msg("Successfully generated certificate-key pair.");
}
@@ -29,22 +36,31 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
}
protected override renderForm(): TemplateResult {
return html`<ak-form-element-horizontal
return html`<ak-text-input
label=${msg("Common Name")}
name="commonName"
required
>
<input type="text" class="pf-c-form-control" required />
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Subject-alt name")} name="subjectAltName">
<input class="pf-c-form-control" type="text" />
<p class="pf-c-form__helper-text">
${msg("Optional, comma-separated SubjectAlt Names.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Validity days")} name="validityDays" required>
<input class="pf-c-form-control" type="number" value="365" />
</ak-form-element-horizontal>
placeholder=${msg("Type a name for this certificate...")}
autofocus
autocomplete="off"
spellcheck="false"
></ak-text-input>
<ak-text-input
label=${msg("Subject-alt name")}
name="subjectAltName"
autocomplete="off"
input-hint="code"
help=${msg("Optional, comma-separated SubjectAlt Names.")}
placeholder=${msg("e.g. mydomain.com, *.mydomain.com, mydomain.local")}
></ak-text-input>
<ak-number-input
label=${msg("Validity days")}
name="validityDays"
required
value="365"
></ak-number-input>
<ak-form-element-horizontal label=${msg("Private key Algorithm")} required name="alg">
<ak-radio
.options=${[
@@ -77,6 +93,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
declare global {
interface HTMLElementTagNameMap {
"ak-crypto-certificate-generate-form": CertificateKeyPairForm;
"ak-crypto-certificate-generate-form": CryptoCertificateGenerateForm;
}
}

View File

@@ -1,4 +1,5 @@
import "#components/ak-secret-textarea-input";
import "#components/ak-text-input";
import "#elements/CodeMirror";
import "#elements/forms/HorizontalFormElement";
@@ -14,7 +15,13 @@ import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@customElement("ak-crypto-certificate-form")
export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string> {
export class CryptoCertificateForm extends ModelForm<CertificateKeyPair, string> {
public static override verboseName = msg("Certificate-Key Pair");
public static override verboseNamePlural = msg("Certificate-Key Pairs");
public static override createLabel = msg("Import Existing");
public static override submitVerb = msg("Import");
public static override submittingVerb = msg("Importing");
loadInstance(pk: string): Promise<CertificateKeyPair> {
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsRetrieve({
kpUuid: pk,
@@ -40,14 +47,16 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
}
protected override renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} name="name" required>
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
return html`<ak-text-input
label=${msg("Certificate Name")}
name="name"
required
value="${ifDefined(this.instance?.name)}"
placeholder=${msg("Type a name for this certificate-key pair...")}
autofocus
autocomplete="off"
spellcheck="false"
></ak-text-input>
<ak-secret-textarea-input
label=${msg("Certificate")}
name="certificateData"
@@ -59,6 +68,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
></ak-secret-textarea-input>
<ak-secret-textarea-input
label=${msg("Private Key")}
placeholder="-----BEGIN PRIVATE KEY-----"
name="keyData"
input-hint="code"
?revealed=${!this.instance}
@@ -71,6 +81,6 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
declare global {
interface HTMLElementTagNameMap {
"ak-crypto-certificate-form": CertificateKeyPairForm;
"ak-crypto-certificate-form": CryptoCertificateForm;
}
}

View File

@@ -9,36 +9,41 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { ModalInvokerButton } from "#elements/dialogs";
import { PFColor } from "#elements/Label";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { CryptoCertificateGenerateForm } from "#admin/crypto/CertificateGenerateForm";
import { CryptoCertificateForm } from "#admin/crypto/CertificateKeyPairForm";
import { CertificateKeyPair, CryptoApi, ModelEnum } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { CSSResult, html, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { CSSResult, html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
@customElement("ak-crypto-certificate-list")
export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
expandable = true;
checkbox = true;
clearOnRefresh = true;
static styles: CSSResult[] = [...super.styles, PFDescriptionList];
public override expandable = true;
public override checkbox = true;
public override clearOnRefresh = true;
public override searchPlaceholder = msg("Search for a certificate or key name...");
protected override searchEnabled = true;
public pageTitle = msg("Certificate-Key Pairs");
public pageDescription = msg(
"Import certificates of external providers or create certificates to sign requests with.",
);
public pageIcon = "pf-icon pf-icon-key";
@property()
order = "name";
static styles: CSSResult[] = [...super.styles, PFDescriptionList];
public override order = "name";
async apiEndpoint(): Promise<PaginatedResponse<CertificateKeyPair>> {
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({
@@ -53,7 +58,7 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
const count = this.selectedElements.length;
return html`<ak-forms-delete-bulk
@@ -82,7 +87,7 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
</ak-forms-delete-bulk>`;
}
row(item: CertificateKeyPair): SlottedTemplateResult[] {
protected override row(item: CertificateKeyPair): SlottedTemplateResult[] {
let managedSubText = msg("Managed by authentik");
if (item.managed && item.managed.startsWith("goauthentik.io/crypto/discovered")) {
managedSubText = msg("Managed by authentik (Discovered)");
@@ -130,7 +135,7 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
];
}
renderExpanded(item: CertificateKeyPair): TemplateResult {
protected override renderExpanded(item: CertificateKeyPair): SlottedTemplateResult {
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">
@@ -188,24 +193,13 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
</dl>`;
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">${msg("Import")}</span>
<span slot="header">${msg("Import Existing Certificate-Key Pair")}</span>
<ak-crypto-certificate-form slot="form"> </ak-crypto-certificate-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Import")}</button>
</ak-forms-modal>
<ak-forms-modal>
<span slot="submit">${msg("Generate")}</span>
<span slot="header">${msg("Generate New Certificate-Key Pair")}</span>
<ak-crypto-certificate-generate-form slot="form">
</ak-crypto-certificate-generate-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Generate")}
</button>
</ak-forms-modal>
`;
protected override renderObjectCreate(): SlottedTemplateResult {
return [
ModalInvokerButton(CryptoCertificateForm),
ModalInvokerButton(CryptoCertificateGenerateForm, null, {
kind: "secondary",
}),
];
}
}

View File

@@ -2,6 +2,7 @@ import "#components/ak-text-input";
import "#elements/forms/HorizontalFormElement";
import { DEFAULT_CONFIG } from "#common/api/config";
import { PFSize } from "#common/enums";
import { ModelForm } from "#elements/forms/ModelForm";
import { WithBrandConfig } from "#elements/mixins/branding";
@@ -20,35 +21,42 @@ import { ifDefined } from "lit/directives/if-defined.js";
*/
@customElement("ak-endpoints-device-access-groups-form")
export class DeviceAccessGroupForm extends WithBrandConfig(ModelForm<DeviceAccessGroup, string>) {
loadInstance(pk: string): Promise<DeviceAccessGroup> {
public static override verboseName = msg("Device Access Group");
public static override verboseNamePlural = msg("Device Access Groups");
public override size = PFSize.Small;
protected override loadInstance(pk: string): Promise<DeviceAccessGroup> {
return new EndpointsApi(DEFAULT_CONFIG).endpointsDeviceAccessGroupsRetrieve({
pbmUuid: pk,
});
}
getSuccessMessage(): string {
public override getSuccessMessage(): string {
return this.instance
? msg("Successfully updated group.")
: msg("Successfully created group.");
}
async send(data: DeviceAccessGroup): Promise<DeviceAccessGroup> {
protected override async send(data: DeviceAccessGroup): Promise<DeviceAccessGroup> {
if (this.instance) {
return new EndpointsApi(DEFAULT_CONFIG).endpointsDeviceAccessGroupsPartialUpdate({
pbmUuid: this.instance.pbmUuid,
patchedDeviceAccessGroupRequest: data,
});
}
return new EndpointsApi(DEFAULT_CONFIG).endpointsDeviceAccessGroupsCreate({
deviceAccessGroupRequest: data as unknown as DeviceAccessGroupRequest,
});
}
renderForm() {
protected override renderForm() {
return html`<ak-text-input
name="name"
placeholder=${msg("Group name...")}
label=${msg("Group name")}
autocomplete="off"
placeholder=${msg("Type a group name...")}
label=${msg("Group Name")}
value=${ifDefined(this.instance?.name)}
required
></ak-text-input>`;

View File

@@ -6,10 +6,13 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { DeviceAccessGroupForm } from "#admin/endpoints/DeviceAccessGroupForm";
import { DeviceAccessGroup, EndpointsApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -18,59 +21,44 @@ import { customElement } from "lit/decorators.js";
@customElement("ak-endpoints-device-access-groups-list")
export class DeviceAccessGroupsListPage extends TablePage<DeviceAccessGroup> {
public pageIcon = "pf-icon pf-icon-server-group ";
public pageTitle = msg("Device access groups");
public pageDescription = msg("Create groups of devices to manage access.");
protected searchEnabled: boolean = true;
protected columns: TableColumn[] = [
[msg("Name"), "name"],
[msg("Actions"), null, msg("Row Actions")],
];
checkbox = true;
expandable = true;
public override pageIcon = "pf-icon pf-icon-server-group ";
public override pageTitle = msg("Device access groups");
public override pageDescription = msg("Create groups of devices to manage access.");
public override searchPlaceholder = msg("Search device groups by name...");
async apiEndpoint(): Promise<PaginatedResponse<DeviceAccessGroup>> {
public override checkbox = true;
public override expandable = true;
protected override async apiEndpoint(): Promise<PaginatedResponse<DeviceAccessGroup>> {
return new EndpointsApi(DEFAULT_CONFIG).endpointsDeviceAccessGroupsList(
await this.defaultEndpointConfig(),
);
}
row(item: DeviceAccessGroup): SlottedTemplateResult[] {
protected override row(item: DeviceAccessGroup): SlottedTemplateResult[] {
return [
html`${item.name}`,
html`<div>
<ak-forms-modal>
<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>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
// ---
item.name,
html`<div class="ak-c-table__actions">
${IconEditButton(DeviceAccessGroupForm, item.pbmUuid)}
</div>`,
];
}
renderExpanded(item: DeviceAccessGroup) {
protected override renderExpanded(item: DeviceAccessGroup) {
return html`<div class="pf-c-content">
<ak-bound-policies-list .target=${item.pbmUuid}></ak-bound-policies-list>
</div>`;
}
renderObjectCreate() {
return html`<ak-forms-modal>
<span slot="submit">${msg("Create")}</span>
<span slot="header">${msg("Create Device Group")}</span>
<ak-endpoints-device-access-groups-form
slot="form"
></ak-endpoints-device-access-groups-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>`;
protected override renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(DeviceAccessGroupForm);
}
renderToolbarSelected() {

View File

@@ -10,6 +10,7 @@ import {
EndpointsDeviceAccessGroupsListRequest,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property, query } from "lit/decorators.js";
@@ -85,6 +86,7 @@ export class EndpointsDeviceAccessGroupSearch extends CustomListenerElement(AKEl
render() {
return html`
<ak-search-select
placeholder=${msg("Select a device access group...")}
.fetchObjects=${fetchObjects}
.renderElement=${renderElement}
.value=${renderValue}

View File

@@ -8,92 +8,34 @@ import "#elements/wizard/Wizard";
import { DEFAULT_CONFIG } from "#common/api/config";
import { AKElement } from "#elements/Base";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage";
import { Wizard } from "#elements/wizard/Wizard";
import { SlottedTemplateResult } from "#elements/types";
import { CreateWizard } from "#elements/wizard/CreateWizard";
import { EndpointsApi, TypeCreate } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, html, TemplateResult } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@customElement("ak-endpoint-connector-wizard")
export class EndpointConnectorWizard extends AKElement {
static styles: CSSResult[] = [PFButton];
export class AKEndpointConnectorWizard extends CreateWizard {
#api = new EndpointsApi(DEFAULT_CONFIG);
@property()
createText = msg("Create");
public static override verboseName = msg("Endpoint Connector");
public static override verboseNamePlural = msg("Endpoint Connectors");
@property({ attribute: false })
connectorTypes: TypeCreate[] = [];
protected apiEndpoint = (requestInit?: RequestInit): Promise<TypeCreate[]> => {
return this.#api.endpointsConnectorsTypesList(requestInit);
};
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new EndpointsApi(DEFAULT_CONFIG).endpointsConnectorsTypesList().then((types) => {
this.connectorTypes = types;
});
}
render(): TemplateResult {
return html`
<ak-wizard
.steps=${["initial"]}
header=${msg("New connector")}
description=${msg("Create a new connector.")}
>
<ak-wizard-page-type-create
slot="initial"
.types=${this.connectorTypes}
layout=${TypeCreateWizardPageLayouts.grid}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
const idx = this.wizard.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.wizard.steps = this.wizard.steps.filter(
(step) => !step.startsWith("type-"),
);
this.wizard.steps.splice(
idx,
0,
`type-${ev.detail.component}-${ev.detail.modelName}`,
);
this.wizard.isValid = true;
}}
>
<div slot="above-form">
<p>
${msg(
"Connectors are required to create devices. Depending on connector type, agents either directly talk to them or they talk to and external API to create devices.",
)}
</p>
</div>
</ak-wizard-page-type-create>
${this.connectorTypes.map((type) => {
return html`
<ak-wizard-page-form
slot=${`type-${type.component}-${type.modelName}`}
label=${msg(str`Create ${type.name}`)}
>
${StrictUnsafe(type.component)}
</ak-wizard-page-form>
`;
})}
<button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button>
</ak-wizard>
`;
protected override renderInitialPageContent(): SlottedTemplateResult {
return msg(
"Connectors are required to create devices. Depending on connector type, agents either directly talk to them or they talk to and external API to create devices.",
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-endpoint-connector-wizard": EndpointConnectorWizard;
"ak-endpoint-connector-wizard": AKEndpointConnectorWizard;
}
}

View File

@@ -7,70 +7,58 @@ import "#elements/forms/ModalForm";
import { DEFAULT_CONFIG } from "#common/api/config";
import { CustomFormElementTagName } from "#elements/forms/unsafe";
import { IconEditButtonByTagName, ModalInvokerButton } from "#elements/dialogs";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { AKEndpointConnectorWizard } from "#admin/endpoints/connectors/ConnectorWizard";
import { Connector, EndpointsApi } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("ak-endpoints-connectors-list")
export class ConnectorsListPage extends TablePage<Connector> {
public pageIcon = "pf-icon pf-icon-data-source";
public pageTitle = msg("Connectors");
public pageDescription = msg(
public override searchPlaceholder = msg("Search connectors by name or type...");
public override pageIcon = "pf-icon pf-icon-data-source";
public override pageTitle = msg("Connectors");
public override pageDescription = msg(
"Configure how devices connect with authentik and ingest external device data.",
);
protected searchEnabled: boolean = true;
protected columns: TableColumn[] = [
protected override searchEnabled: boolean = true;
protected override columns: TableColumn[] = [
[msg("Name"), "name"],
[msg("Type")],
[msg("Actions"), null, msg("Row Actions")],
];
checkbox = true;
public override checkbox = true;
async apiEndpoint(): Promise<PaginatedResponse<Connector>> {
protected override async apiEndpoint(): Promise<PaginatedResponse<Connector>> {
return new EndpointsApi(DEFAULT_CONFIG).endpointsConnectorsList(
await this.defaultEndpointConfig(),
);
}
row(item: Connector): SlottedTemplateResult[] {
protected override row(item: Connector): SlottedTemplateResult[] {
return [
html`<a href="#/endpoints/connectors/${item.connectorUuid}">${item.name}</a>`,
html`${item.verboseName}`,
html`<div>
<ak-forms-modal>
${StrictUnsafe<CustomFormElementTagName>(item.component, {
slot: "form",
instancePk: item.connectorUuid,
submitLabel: msg("Save Changes"),
headline: msg(str`Update ${item.verboseName}`, {
id: "form.headline.update",
}),
})}
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
item.verboseName,
html`<div class="ak-c-table__actions">
${IconEditButtonByTagName(item.component, item.connectorUuid, item.verboseName)}
</div>`,
];
}
renderObjectCreate() {
return html`<ak-endpoint-connector-wizard></ak-endpoint-connector-wizard> `;
protected override renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(AKEndpointConnectorWizard);
}
renderToolbarSelected() {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Connector(s)")}

View File

@@ -89,11 +89,11 @@ export class AgentConnectorForm extends WithBrandConfig(ModelForm<AgentConnector
<ak-form-group label="${msg("Authentication settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authorization flow")}
label=${msg("Authorization Flow")}
name="authorizationFlow"
>
<ak-flow-search
label=${msg("Authorization flow")}
label=${msg("Authorization Flow")}
flowType=${FlowDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
></ak-flow-search>

View File

@@ -29,13 +29,18 @@ const EXPIRATION_DURATION = 30 * 60 * 1000; // 30 minutes
*/
@customElement("ak-endpoints-agent-enrollment-token-form")
export class EnrollmentTokenForm extends WithBrandConfig(ModelForm<EnrollmentToken, string>) {
#api = new EndpointsApi(DEFAULT_CONFIG);
public static override verboseName = msg("Enrollment Token");
public static override verboseNamePlural = msg("Enrollment Tokens");
protected expirationMinimumDate = new Date();
@state()
protected expiresAt: Date | null = new Date(Date.now() + EXPIRATION_DURATION);
@property({ type: String, attribute: "connector-id" })
public connectorID?: string;
public connectorID: string | null = null;
public override reset(): void {
super.reset();
@@ -57,25 +62,25 @@ export class EnrollmentTokenForm extends WithBrandConfig(ModelForm<EnrollmentTok
return token;
}
getSuccessMessage(): string {
public override getSuccessMessage(): string {
return this.instance
? msg("Successfully updated token.")
: msg("Successfully created token.");
}
async send(data: EnrollmentToken): Promise<EnrollmentToken> {
protected override async send(data: EnrollmentToken): Promise<EnrollmentToken> {
if (!this.instance) {
data.connector = this.connectorID || "";
} else {
data.connector = this.instance.connector;
}
if (this.instance) {
return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensPartialUpdate({
return this.#api.endpointsAgentsEnrollmentTokensPartialUpdate({
tokenUuid: this.instance.tokenUuid,
patchedEnrollmentTokenRequest: data,
});
}
return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensCreate({
return this.#api.endpointsAgentsEnrollmentTokensCreate({
enrollmentTokenRequest: data as unknown as EnrollmentTokenRequest,
});
}
@@ -102,7 +107,7 @@ export class EnrollmentTokenForm extends WithBrandConfig(ModelForm<EnrollmentTok
//#region Rendering
renderForm() {
protected override renderForm() {
return html`<ak-text-input
name="name"
placeholder=${msg("Type a name for the token...")}
@@ -148,7 +153,7 @@ export class EnrollmentTokenForm extends WithBrandConfig(ModelForm<EnrollmentTok
?disabled=${!this.expiresAt}
class="pf-c-form-control"
/>
</ak-form-element-horizontal> `;
</ak-form-element-horizontal>`;
}
//#endregion

View File

@@ -1,6 +1,5 @@
import "#admin/rbac/ObjectPermissionModal";
import "#admin/endpoints/connectors/agent/EnrollmentTokenForm";
import "#admin/endpoints/connectors/agent/ak-enrollment-token-copy-button";
import "#elements/buttons/SpinnerButton/index";
import "#elements/forms/DeleteBulkForm";
import "#elements/forms/ModalForm";
@@ -9,9 +8,14 @@ import "#components/ak-status-label";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEnrollmentTokenCopyButton } from "#elements/buttons/IconEnrollmentTokenCopyButton";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton";
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import { EnrollmentTokenForm } from "#admin/endpoints/connectors/agent/EnrollmentTokenForm";
import { AgentConnector, EndpointsApi, EnrollmentToken, ModelEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -20,19 +24,23 @@ import { customElement, property } from "lit/decorators.js";
@customElement("ak-endpoints-agent-enrollment-token-list")
export class EnrollmentTokenListPage extends Table<EnrollmentToken> {
checkbox = true;
clearOnRefresh = true;
#api = new EndpointsApi(DEFAULT_CONFIG);
protected override searchEnabled = true;
protected emptyStateMessage = msg("No enrollment tokens found for this connector.");
@property()
order = "name";
public override checkbox = true;
public override clearOnRefresh = true;
public override searchPlaceholder = msg("Search for an enrollment token...");
public override order = "name";
@property({ attribute: false })
connector?: AgentConnector;
public connector: AgentConnector | null = null;
async apiEndpoint(): Promise<PaginatedResponse<EnrollmentToken>> {
return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensList({
protected override async apiEndpoint(): Promise<PaginatedResponse<EnrollmentToken>> {
return this.#api.endpointsAgentsEnrollmentTokensList({
...(await this.defaultEndpointConfig()),
connector: this.connector?.connectorUuid,
});
@@ -46,8 +54,9 @@ export class EnrollmentTokenListPage extends Table<EnrollmentToken> {
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Enrollment Token(s)")}
.objects=${this.selectedElements}
@@ -58,12 +67,12 @@ export class EnrollmentTokenListPage extends Table<EnrollmentToken> {
];
}}
.usedBy=${(item: EnrollmentToken) => {
return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensUsedByList({
return this.#api.endpointsAgentsEnrollmentTokensUsedByList({
tokenUuid: item.tokenUuid,
});
}}
.delete=${(item: EnrollmentToken) => {
return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensDestroy({
return this.#api.endpointsAgentsEnrollmentTokensDestroy({
tokenUuid: item.tokenUuid,
});
}}
@@ -74,54 +83,27 @@ export class EnrollmentTokenListPage extends Table<EnrollmentToken> {
</ak-forms-delete-bulk>`;
}
row(item: EnrollmentToken): SlottedTemplateResult[] {
protected override row(item: EnrollmentToken): SlottedTemplateResult[] {
return [
html`${item.name}`,
html`${item.deviceGroupObj?.name || "-"}`,
item.name,
item.deviceGroupObj?.name || msg("-"),
html`<ak-status-label type="warning" ?good=${item.expiring}></ak-status-label>`,
Timestamp(item.expires && item.expiring ? item.expires : null),
html`<div>
<ak-forms-modal>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header">${msg("Update Enrollment Token")}</span>
<ak-endpoints-agent-enrollment-token-form
slot="form"
.instancePk=${item.tokenUuid}
>
</ak-endpoints-agent-enrollment-token-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
<ak-rbac-object-permission-modal
model=${ModelEnum.AuthentikEndpointsConnectorsAgentEnrollmenttoken}
objectPk=${item.tokenUuid}
>
</ak-rbac-object-permission-modal>
<ak-enrollment-token-copy-button .identifier=${item.tokenUuid}>
<pf-tooltip position="top" content=${msg("Copy token")}>
<i class="fas fa-copy" aria-hidden="true"></i>
</pf-tooltip>
</ak-enrollment-token-copy-button>
html`<div class="ak-c-table__actions">
${IconEditButton(EnrollmentTokenForm, item.tokenUuid, item.name)}
${IconPermissionButton(item.name, {
model: ModelEnum.AuthentikEndpointsConnectorsAgentEnrollmenttoken,
objectPk: item.tokenUuid,
})}
${IconEnrollmentTokenCopyButton(item.tokenUuid)}
</div>`,
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">${msg("Create")}</span>
<span slot="header">${msg("Create Enrollment Token")}</span>
<ak-endpoints-agent-enrollment-token-form
slot="form"
.connectorID=${this.connector?.connectorUuid}
>
</ak-endpoints-agent-enrollment-token-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
`;
protected override renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(EnrollmentTokenForm, {
connectorID: this.connector?.connectorUuid,
});
}
}

View File

@@ -1,37 +0,0 @@
import { DEFAULT_CONFIG } from "#common/api/config";
import { writeToClipboard } from "#common/clipboard";
import TokenCopyButton from "#elements/buttons/TokenCopyButton/ak-token-copy-button";
import { EndpointsApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { customElement } from "lit/decorators.js";
@customElement("ak-enrollment-token-copy-button")
export class EnrollmentTokenCopyButton extends TokenCopyButton {
public override entityLabel = msg("Enrollment Token");
public override callAction(): Promise<null> {
if (!this.identifier) {
throw new TypeError("No `identifier` set for `EnrollmentTokenCopyButton`");
}
// Safari permission hack.
const text = new ClipboardItem({
"text/plain": new EndpointsApi(DEFAULT_CONFIG)
.endpointsAgentsEnrollmentTokensViewKeyRetrieve({
tokenUuid: this.identifier,
})
.then((tokenView) => new Blob([tokenView.key], { type: "text/plain" })),
});
return writeToClipboard(text, this.entityLabel).then(() => null);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-enrollment-token-copy-button": EnrollmentTokenCopyButton;
}
}

View File

@@ -42,7 +42,8 @@ export class FleetConnectorForm extends ModelForm<FleetConnector, string> {
renderForm() {
return html`<ak-text-input
name="name"
placeholder=${msg("Connector name...")}
autofocus
placeholder=${msg("Type a connector name...")}
label=${msg("Connector name")}
value=${this.instance?.name ?? ""}
required
@@ -57,6 +58,7 @@ export class FleetConnectorForm extends ModelForm<FleetConnector, string> {
<ak-text-input
name="url"
label=${msg("Fleet Server URL")}
inputmode="url"
value=${this.instance?.url ?? ""}
required
input-hint="code"
@@ -64,6 +66,7 @@ export class FleetConnectorForm extends ModelForm<FleetConnector, string> {
</ak-text-input>
<ak-secret-text-input
label=${msg("Fleet API Token")}
placeholder=${msg("Provide your Fleet API token...")}
name="token"
?revealed=${!this.instance}
></ak-secret-text-input>

View File

@@ -43,7 +43,8 @@ export class GoogleChromeConnectorForm extends ModelForm<GoogleChromeConnector,
renderForm() {
return html`<ak-text-input
name="name"
placeholder=${msg("Connector name...")}
autofocus
placeholder=${msg("Type a connector name...")}
label=${msg("Connector name")}
value=${this.instance?.name ?? ""}
required

View File

@@ -21,13 +21,7 @@ import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
@customElement("ak-endpoints-device-list")
export class DeviceListPage extends TablePage<EndpointDevice> {
public pageTitle = msg("Devices");
public pageDescription = "";
public pageIcon = "fa fa-laptop";
checkbox = true;
static styles: CSSResult[] = [
public static styles: CSSResult[] = [
...super.styles,
PFGrid,
PFBanner,
@@ -37,6 +31,13 @@ export class DeviceListPage extends TablePage<EndpointDevice> {
}
`,
];
public override pageTitle = msg("Devices");
public override pageDescription = "";
public override pageIcon = "fa fa-laptop";
public override checkbox = true;
public override searchPlaceholder = msg("Search devices by name, OS, or group...");
protected searchEnabled: boolean = true;
protected columns: TableColumn[] = [
@@ -59,7 +60,7 @@ export class DeviceListPage extends TablePage<EndpointDevice> {
);
}
protected renderEmpty(inner?: TemplateResult): TemplateResult {
protected renderEmpty(inner?: TemplateResult): SlottedTemplateResult {
return super.renderEmpty(html`
${inner
? inner

View File

@@ -1,4 +1,5 @@
import "#components/ak-secret-textarea-input";
import "#components/ak-text-input";
import "#elements/CodeMirror";
import "#elements/forms/HorizontalFormElement";
@@ -6,16 +7,24 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { EVENT_REFRESH_ENTERPRISE } from "#common/constants";
import { ModelForm } from "#elements/forms/ModelForm";
import { SlottedTemplateResult } from "#elements/types";
import { ifPresent } from "#elements/utils/attributes";
import { EnterpriseApi, License } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
@customElement("ak-enterprise-license-form")
export class EnterpriseLicenseForm extends ModelForm<License, string> {
public static override verboseName = msg("Enterprise License");
public static override verboseNamePlural = msg("Enterprise Licenses");
public static override createLabel = msg("Install");
public static override submitVerb = msg("Install");
#api = new EnterpriseApi(DEFAULT_CONFIG);
@state()
protected installID: string | null = null;
@@ -26,7 +35,7 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
}
loadInstance(pk: string): Promise<License> {
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseRetrieve({
return this.#api.enterpriseLicenseRetrieve({
licenseUuid: pk,
});
}
@@ -38,19 +47,17 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
}
async load(): Promise<void> {
this.installID = (
await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseInstallIdRetrieve()
).installId;
this.installID = (await this.#api.enterpriseLicenseInstallIdRetrieve()).installId;
}
async send(data: License): Promise<License> {
return (
this.instance
? new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicensePartialUpdate({
? this.#api.enterpriseLicensePartialUpdate({
licenseUuid: this.instance.licenseUuid || "",
patchedLicenseRequest: data,
})
: new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseCreate({
: this.#api.enterpriseLicenseCreate({
licenseRequest: data,
})
).then((data) => {
@@ -59,19 +66,21 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
});
}
protected override renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Install ID")}>
<input
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
readonly
type="text"
value="${ifPresent(this.installID)}"
/>
</ak-form-element-horizontal>
protected override renderForm(): SlottedTemplateResult {
return html`<ak-text-input
label=${msg("Install ID")}
autocomplete="off"
spellcheck="false"
readonly
type="text"
name="installID"
input-hint="code"
value="${ifPresent(this.installID)}"
>
</ak-text-input>
<ak-secret-textarea-input
name="key"
?required=${!this.instance}
?revealed=${!this.instance}
placeholder=${msg("Paste your license key...")}
label=${msg("License key")}

View File

@@ -11,11 +11,14 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { docLink } from "#common/global";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { PFColor } from "#elements/Label";
import { PaginatedResponse, TableColumn, Timestamp } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { EnterpriseLicenseForm } from "#admin/enterprise/EnterpriseLicenseForm";
import {
EnterpriseApi,
License,
@@ -26,8 +29,8 @@ import {
} from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { css, CSSResult, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@@ -37,27 +40,7 @@ import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
@customElement("ak-enterprise-license-list")
export class EnterpriseLicenseListPage extends TablePage<License> {
checkbox = true;
clearOnRefresh = true;
protected override searchEnabled = true;
public pageTitle = msg("Licenses");
public pageDescription = msg("Manage enterprise licenses");
public pageIcon = "pf-icon pf-icon-key";
@property()
order = "name";
@state()
forecast?: LicenseForecast;
@state()
summary?: LicenseSummary;
@state()
installID?: string;
static styles: CSSResult[] = [
public static styles: CSSResult[] = [
...super.styles,
PFGrid,
PFBanner,
@@ -74,6 +57,25 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
`,
];
public override checkbox = true;
public override clearOnRefresh = true;
protected override searchEnabled = true;
public override pageTitle = msg("Licenses");
public override pageDescription = msg("Manage enterprise licenses");
public override pageIcon = "pf-icon pf-icon-key";
public override searchPlaceholder = msg("Search for a license by name...");
public override order = "name";
@state()
protected forecast?: LicenseForecast;
@state()
protected summary?: LicenseSummary;
@state()
protected installID?: string;
async apiEndpoint(): Promise<PaginatedResponse<License>> {
this.forecast = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseForecastRetrieve();
this.summary = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve({
@@ -96,7 +98,7 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
// TODO: Make this more generic, maybe automatically get the plural name
// of the object to use in the renderEmpty
renderEmpty(inner?: TemplateResult): TemplateResult {
protected override renderEmpty(inner?: SlottedTemplateResult): SlottedTemplateResult {
return super.renderEmpty(html`
${inner
? inner
@@ -110,7 +112,7 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
`);
}
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("License(s)")}
@@ -138,7 +140,7 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
</ak-forms-delete-bulk>`;
}
renderSectionBefore(): TemplateResult {
protected override renderSectionBefore(): SlottedTemplateResult {
const {
externalUsers = 0,
internalUsers = 0,
@@ -219,18 +221,9 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
html`<div>${msg(str`Internal: ${item.internalUsers}`)}</div>
<div>${msg(str`External: ${item.externalUsers}`)}</div>`,
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
html`<div>
<ak-forms-modal>
<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>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
html`<div class="ak-c-table__actions">
${IconEditButton(EnterpriseLicenseForm, item.licenseUuid, item.name)}
<ak-rbac-object-permission-modal
model=${ModelEnum.AuthentikEnterpriseLicense}
objectPk=${item.licenseUuid}
@@ -240,7 +233,7 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
];
}
renderGetLicenseCard() {
protected renderGetLicenseCard() {
const renderSpinner = () =>
html` <div class="pf-c-card__body">
<ak-spinner></ak-spinner>
@@ -277,15 +270,8 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
</div> `;
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">${msg("Install")}</span>
<span slot="header">${msg("Install License")}</span>
<ak-enterprise-license-form slot="form"> </ak-enterprise-license-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Install")}</button>
</ak-forms-modal>
`;
protected override renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(EnterpriseLicenseForm);
}
}

View File

@@ -120,7 +120,7 @@ export class DataExportListPage extends TablePage<DataExport> {
</dl>`;
}
protected renderEmpty(_inner?: TemplateResult): TemplateResult {
protected renderEmpty(_inner?: TemplateResult): SlottedTemplateResult {
return super.renderEmpty(
html`<ak-empty-state icon=${this.pageIcon}
><span

View File

@@ -16,7 +16,7 @@ import { EventGeo, renderEventUser } from "#admin/events/utils";
import { Event, EventsApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, PropertyValues, TemplateResult } from "lit";
import { html, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("ak-object-changelog")
@@ -82,11 +82,11 @@ export class ObjectChangelog extends Table<Event> {
];
}
renderExpanded(item: Event): TemplateResult {
renderExpanded(item: Event): SlottedTemplateResult {
return html`<ak-event-info .event=${item as EventWithContext}></ak-event-info>`;
}
renderEmpty(): TemplateResult {
renderEmpty(): SlottedTemplateResult {
return super.renderEmpty(
html`<ak-empty-state
><span>${msg("No Events found.")}</span>

View File

@@ -1,4 +1,5 @@
import "#components/ak-switch-input";
import "#components/ak-text-input";
import "#elements/ak-dual-select/ak-dual-select-dynamic-selected-provider";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/Radio";
@@ -29,6 +30,9 @@ import { ifDefined } from "lit/directives/if-defined.js";
@customElement("ak-event-rule-form")
export class RuleForm extends ModelForm<NotificationRule, string> {
public static verboseName = msg("Notification Rule");
public static verboseNamePlural = msg("Notification Rules");
eventTransports?: PaginatedNotificationTransportList;
loadInstance(pk: string): Promise<NotificationRule> {
@@ -62,23 +66,24 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
}
protected override renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
return html` <ak-text-input
required
autocomplete="off"
name="name"
label=${msg("Rule Name")}
placeholder=${msg("Type a name for this rule...")}
value="${ifDefined(this.instance?.name)}"
></ak-text-input>
<ak-form-element-horizontal label=${msg("Group")} name="destinationGroup">
<ak-search-select
placeholder=${msg("Select a group...")}
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
includeUsers: false,
};
if (query !== undefined) {
if (typeof query !== "undefined") {
args.search = query;
}

View File

@@ -11,10 +11,13 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { severityToLabel } from "#common/labels";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { RuleForm } from "#admin/events/RuleForm";
import { EventsApi, ModelEnum, NotificationRule } from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -23,9 +26,12 @@ import { customElement, property } from "lit/decorators.js";
@customElement("ak-event-rule-list")
export class RuleListPage extends TablePage<NotificationRule> {
expandable = true;
checkbox = true;
clearOnRefresh = true;
public override expandable = true;
public override checkbox = true;
public override clearOnRefresh = true;
public override searchPlaceholder = msg(
"Search for a notification rule by name, severity or group...",
);
protected override searchEnabled = true;
public pageTitle = msg("Notification Rules");
@@ -35,9 +41,9 @@ export class RuleListPage extends TablePage<NotificationRule> {
public pageIcon = "pf-icon pf-icon-attention-bell";
@property()
order = "name";
public order = "name";
async apiEndpoint(): Promise<PaginatedResponse<NotificationRule>> {
protected override async apiEndpoint(): Promise<PaginatedResponse<NotificationRule>> {
return new EventsApi(DEFAULT_CONFIG).eventsRulesList(await this.defaultEndpointConfig());
}
@@ -49,7 +55,7 @@ export class RuleListPage extends TablePage<NotificationRule> {
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Notification rule(s)")}
@@ -71,7 +77,7 @@ export class RuleListPage extends TablePage<NotificationRule> {
</ak-forms-delete-bulk>`;
}
row(item: NotificationRule): SlottedTemplateResult[] {
protected override row(item: NotificationRule): SlottedTemplateResult[] {
const enabled = !!item.destinationGroupObj || item.destinationEventUser;
return [
html`<ak-status-label type="warning" ?good=${enabled}></ak-status-label>`,
@@ -82,17 +88,8 @@ export class RuleListPage extends TablePage<NotificationRule> {
>${item.destinationGroupObj.name}</a
>`
: msg("-")}`,
html`<div>
<ak-forms-modal>
<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">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
html`<div class="ak-c-table__actions">
${IconEditButton(RuleForm, item.pk, item.name)}
<ak-rbac-object-permission-modal
model=${ModelEnum.AuthentikEventsNotificationrule}
@@ -103,19 +100,13 @@ export class RuleListPage extends TablePage<NotificationRule> {
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">${msg("Create")}</span>
<span slot="header">${msg("Create Notification Rule")}</span>
<ak-event-rule-form slot="form"> </ak-event-rule-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
`;
protected override renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(RuleForm);
}
renderExpanded(item: NotificationRule): TemplateResult {
protected override renderExpanded(item: NotificationRule): TemplateResult {
const [appLabel, modelName] = ModelEnum.AuthentikEventsNotificationrule.split(".");
return html`<p>
${msg(
`These bindings control upon which events this rule triggers.

View File

@@ -5,13 +5,14 @@ import { EventWithContext } from "#common/events";
import { actionToLabel } from "#common/labels";
import { PaginatedResponse, RowType, Table, TableColumn, Timestamp } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import { EventGeo, renderEventUser } from "#admin/events/utils";
import { Event, EventsApi, EventsEventsListRequest } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit-html";
import { html } from "lit-html";
import { property } from "lit/decorators.js";
export abstract class SimpleEventTable extends Table<Event> {
@@ -57,11 +58,11 @@ export abstract class SimpleEventTable extends Table<Event> {
];
}
renderExpanded(item: Event): TemplateResult {
protected override renderExpanded(item: Event): SlottedTemplateResult {
return html`<ak-event-info .event=${item as EventWithContext}></ak-event-info>`;
}
renderEmpty(): TemplateResult {
protected override renderEmpty(): SlottedTemplateResult {
return super.renderEmpty(
html`<ak-empty-state
><span>${msg("No Events found.")}</span>

View File

@@ -1,5 +1,6 @@
import "#components/ak-hidden-text-input";
import "#components/ak-switch-input";
import "#components/ak-text-input";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/Radio";
import "#elements/forms/SearchSelect/index";
@@ -27,6 +28,9 @@ import { ifDefined } from "lit/directives/if-defined.js";
@customElement("ak-event-transport-form")
export class TransportForm extends ModelForm<NotificationTransport, string> {
public static override verboseName = msg("Notification Transport");
public static override verboseNamePlural = msg("Notification Transports");
loadInstance(pk: string): Promise<NotificationTransport> {
return new EventsApi(DEFAULT_CONFIG)
.eventsTransportsRetrieve({
@@ -88,15 +92,16 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
}
protected override renderForm(): TemplateResult {
return html`
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
return html`<ak-text-input
label=${msg("Transport Name")}
placeholder=${msg("Type a name for this transport...")}
autofocus
spellcheck="false"
autocomplete="off"
required
name="name"
value="${ifDefined(this.instance?.name)}"
></ak-text-input>
<ak-switch-input
name="sendOnce"
label=${msg("Send once")}
@@ -248,8 +253,7 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
</option>`;
})}
</select>
</ak-form-element-horizontal>
`;
</ak-form-element-horizontal> `;
}
}

View File

@@ -9,15 +9,18 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { TransportForm } from "#admin/events/TransportForm";
import { EventsApi, ModelEnum, NotificationTransport } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("ak-event-transport-list")
export class TransportListPage extends TablePage<NotificationTransport> {
@@ -28,26 +31,28 @@ export class TransportListPage extends TablePage<NotificationTransport> {
);
public pageIcon = "pf-icon pf-icon-export";
checkbox = true;
clearOnRefresh = true;
expandable = true;
public override checkbox = true;
public override clearOnRefresh = true;
public override expandable = true;
public override searchPlaceholder = msg(
"Search for a notification transport by name or mode...",
);
@property()
order = "name";
public override order = "name";
async apiEndpoint(): Promise<PaginatedResponse<NotificationTransport>> {
protected override async apiEndpoint(): Promise<PaginatedResponse<NotificationTransport>> {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsList(
await this.defaultEndpointConfig(),
);
}
protected columns: TableColumn[] = [
protected override columns: TableColumn[] = [
[msg("Name"), "name"],
[msg("Mode"), "mode"],
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Notification transport(s)")}
@@ -69,22 +74,12 @@ export class TransportListPage extends TablePage<NotificationTransport> {
</ak-forms-delete-bulk>`;
}
row(item: NotificationTransport): SlottedTemplateResult[] {
protected override row(item: NotificationTransport): SlottedTemplateResult[] {
return [
html`${item.name}`,
html`${item.modeVerbose}`,
html`<div>
<ak-forms-modal>
<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>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
item.name,
item.modeVerbose,
html`<div class="ak-c-table__actions">
${IconEditButton(TransportForm, item.pk, item.name)}
<ak-rbac-object-permission-modal
model=${ModelEnum.AuthentikEventsNotificationtransport}
@@ -107,7 +102,7 @@ export class TransportListPage extends TablePage<NotificationTransport> {
];
}
renderExpanded(item: NotificationTransport): TemplateResult {
protected override renderExpanded(item: NotificationTransport): SlottedTemplateResult {
const [appLabel, modelName] = ModelEnum.AuthentikEventsNotificationtransport.split(".");
return html`<dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group">
@@ -127,15 +122,8 @@ export class TransportListPage extends TablePage<NotificationTransport> {
</dl>`;
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">${msg("Create")}</span>
<span slot="header">${msg("Create Notification Transport")}</span>
<ak-event-transport-form slot="form"> </ak-event-transport-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
`;
protected override renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(TransportForm);
}
}

View File

@@ -37,6 +37,7 @@ export class FileListPage extends WithCapabilitiesConfig(TablePage<FileItem>) {
public override pageTitle = msg("Files");
public override pageDescription = msg("Manage uploaded files.");
public override pageIcon = "pf-icon pf-icon-folder-open";
public override searchPlaceholder = msg("Search for a file by name...");
@property({ type: String, useDefault: true })
public order: FileListOrderKey = "name";

View File

@@ -1,24 +1,27 @@
import "#admin/flows/StageBindingForm";
import "#admin/policies/BoundPoliciesList";
import "#admin/rbac/ObjectPermissionModal";
import "#admin/stages/StageWizard";
import "#elements/Tabs";
import "#elements/forms/DeleteBulkForm";
import "#elements/forms/ModalForm";
import { DEFAULT_CONFIG } from "#common/api/config";
import { modalInvoker } from "#elements/dialogs";
import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton";
import { CustomFormElementTagName } from "#elements/forms/unsafe";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { StageBindingForm } from "#admin/flows/StageBindingForm";
import { AKStageWizard } from "#admin/stages/ak-stage-wizard";
import { FlowsApi, FlowStageBinding, ModelEnum } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@customElement("ak-bound-stages-list")
export class BoundStagesList extends Table<FlowStageBinding> {
@@ -80,39 +83,56 @@ export class BoundStagesList extends Table<FlowStageBinding> {
row(item: FlowStageBinding): SlottedTemplateResult[] {
return [
html`<pre>${item.order}</pre>`,
html`${item.stageObj?.name}`,
html`${item.stageObj?.verboseName}`,
html` <ak-forms-modal>
${StrictUnsafe<CustomFormElementTagName>(item.stageObj?.component, {
slot: "form",
instancePk: item.stageObj?.pk,
submitLabel: msg("Save Changes"),
headline: msg(str`Update ${item.stageObj?.verboseName}`, {
id: "form.headline.update",
item.stageObj?.name,
item.stageObj?.verboseName,
html`<div class="ak-c-table__actions">
<button
type="button"
class="pf-c-button pf-m-secondary"
${modalInvoker(() =>
StrictUnsafe<CustomFormElementTagName>(item.stageObj?.component, {
instancePk: item.stageObj?.pk,
}),
})}
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Edit Stage")}
</button>
</ak-forms-modal>
<ak-forms-modal>
<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>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Edit Binding")}
</button>
</ak-forms-modal>
<ak-rbac-object-permission-modal
model=${ModelEnum.AuthentikFlowsFlowstagebinding}
objectPk=${item.pk}
)}
>
</ak-rbac-object-permission-modal>`,
${msg("Edit Stage")}
</button>
<button
type="button"
class="pf-c-button pf-m-secondary"
${modalInvoker(StageBindingForm, { instancePk: item.pk })}
>
${msg("Edit Binding")}
</button>
${IconPermissionButton(item.stageObj?.name || "", {
model: ModelEnum.AuthentikFlowsFlowstagebinding,
objectPk: item.pk,
})}
</div>`,
];
}
renderExpanded(item: FlowStageBinding): TemplateResult {
protected renderActions(): SlottedTemplateResult {
return html`<button
class="pf-c-button pf-m-primary"
${modalInvoker(AKStageWizard, {
showBindingPage: true,
bindingTarget: this.target,
})}
>
${msg("New Stage")}
</button>
<button
slot="trigger"
class="pf-c-button pf-m-primary"
${modalInvoker(StageBindingForm, { targetPk: this.target })}
>
${msg("Bind Existing Stage")}
</button>`;
}
protected override renderExpanded(item: FlowStageBinding): TemplateResult {
return html`<div class="pf-c-content">
<p>${msg("These bindings control if this stage will be applied to the flow.")}</p>
<ak-bound-policies-list
@@ -123,49 +143,18 @@ export class BoundStagesList extends Table<FlowStageBinding> {
</div>`;
}
renderEmpty(): TemplateResult {
protected override renderEmpty(): SlottedTemplateResult {
return super.renderEmpty(
html`<ak-empty-state icon="pf-icon-module">
<span>${msg("No Stages bound")}</span>
<div slot="body">${msg("No stages are currently bound to this flow.")}</div>
<div slot="primary">
<ak-stage-wizard
createText=${msg("Create and bind Stage")}
showBindingPage
bindingTarget=${ifDefined(this.target)}
></ak-stage-wizard>
<ak-forms-modal>
<span slot="submit">${msg("Create")}</span>
<span slot="header">${msg("Create Stage binding")}</span>
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
</ak-stage-binding-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Bind existing Stage")}
</button>
</ak-forms-modal>
</div>
<div slot="primary">${this.renderActions()}</div>
</ak-empty-state>`,
);
}
renderToolbar(): TemplateResult {
return html`
<ak-stage-wizard
createText=${msg("Create and bind Stage")}
showBindingPage
bindingTarget=${ifDefined(this.target)}
></ak-stage-wizard>
<ak-forms-modal>
<span slot="submit">${msg("Create")}</span>
<span slot="header">${msg("Create Stage binding")}</span>
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
</ak-stage-binding-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Bind existing Stage")}
</button>
</ak-forms-modal>
${super.renderToolbar()}
`;
protected override renderToolbar(): SlottedTemplateResult {
return [this.renderActions(), super.renderToolbar()];
}
}

View File

@@ -1,5 +1,6 @@
import "#components/ak-file-search-input";
import "#components/ak-slug-input";
import "#components/ak-text-input";
import "#components/ak-switch-input";
import "#elements/forms/FormGroup";
import "#elements/forms/HorizontalFormElement";
@@ -10,6 +11,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { ModelForm } from "#elements/forms/ModelForm";
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import { AKLabel } from "#components/ak-label";
import { DesignationToLabel, LayoutToLabel } from "#admin/flows/utils";
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
@@ -35,62 +38,80 @@ import { ifDefined } from "lit/directives/if-defined.js";
*/
@customElement("ak-flow-form")
export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
async loadInstance(pk: string): Promise<Flow> {
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesRetrieve({
public static override verboseName = msg("Flow");
public static override verboseNamePlural = msg("Flows");
#api = new FlowsApi(DEFAULT_CONFIG);
protected override async loadInstance(pk: string): Promise<Flow> {
return this.#api.flowsInstancesRetrieve({
slug: pk,
});
}
getSuccessMessage(): string {
public override getSuccessMessage(): string {
return this.instance
? msg("Successfully updated flow.")
: msg("Successfully created flow.");
}
async send(data: Flow): Promise<void | Flow> {
protected override async send(data: Flow): Promise<void | Flow> {
if (this.instance) {
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesUpdate({
return this.#api.flowsInstancesUpdate({
slug: this.instance.slug,
flowRequest: data,
});
}
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesCreate({
return this.#api.flowsInstancesCreate({
flowRequest: data,
});
}
protected override renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Title")} required name="title">
<input
type="text"
value="${ifDefined(this.instance?.title)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">${msg("Shown as the Title in Flow pages.")}</p>
</ak-form-element-horizontal>
return html`<ak-text-input
label=${msg("Flow Name")}
placeholder=${msg("Type a name for this flow...")}
autofocus
autocomplete="off"
required
name="name"
value="${ifDefined(this.instance?.name)}"
></ak-text-input>
<ak-text-input
label=${msg("Title")}
placeholder=${msg("Type a title for this flow...")}
help=${msg("Shown as the Title in Flow pages.")}
autocomplete="off"
required
name="title"
value="${ifDefined(this.instance?.title)}"
></ak-text-input>
<ak-slug-input
name="slug"
value=${ifDefined(this.instance?.slug)}
placeholder=${msg("e.g. my-flow")}
label=${msg("Slug")}
required
help=${msg("Visible in the URL.")}
input-hint="code"
></ak-slug-input>
<ak-form-element-horizontal label=${msg("Designation")} required name="designation">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.designation === undefined}>
---------
<ak-form-element-horizontal required name="designation">
${AKLabel(
{
slot: "label",
className: "pf-c-form__group-label",
htmlFor: "designation",
required: true,
},
msg("Designation"),
)}
<select id="designation" class="pf-c-form-control" required>
<option value="" ?selected=${!this.instance?.designation}>
${msg("Select a designation...")}
</option>
<option
value=${FlowDesignationEnum.Authentication}
@@ -144,12 +165,18 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Authentication")}
required
name="authentication"
>
<select class="pf-c-form-control">
<ak-form-element-horizontal required name="authentication">
${AKLabel(
{
slot: "label",
className: "pf-c-form__group-label",
htmlFor: "authentication",
required: true,
},
msg("Authentication"),
)}
<select id="authentication" class="pf-c-form-control" required>
<option
value=${AuthenticationEnum.None}
?selected=${this.instance?.authentication === AuthenticationEnum.None}
@@ -261,8 +288,17 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
</ak-form-group>
<ak-form-group label="${msg("Appearance settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Layout")} required name="layout">
<select class="pf-c-form-control">
<ak-form-element-horizontal required name="layout">
${AKLabel(
{
slot: "label",
className: "pf-c-form__group-label",
htmlFor: "layout",
required: true,
},
msg("Layout"),
)}
<select id="layout" class="pf-c-form-control" required>
<option
value=${FlowLayoutEnum.Stacked}
?selected=${this.instance?.layout === FlowLayoutEnum.Stacked}

View File

@@ -10,17 +10,19 @@ import { AndNext, DEFAULT_CONFIG } from "#common/api/config";
import { docLink } from "#common/global";
import { groupBy } from "#common/utils";
import { IconEditButton, modalInvoker, ModalInvokerButton } from "#elements/dialogs";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { FlowForm } from "#admin/flows/FlowForm";
import { DesignationToLabel } from "#admin/flows/utils";
import { Flow, FlowsApi } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
@@ -29,17 +31,18 @@ export class FlowListPage extends TablePage<Flow> {
static styles = [...super.styles, PFBanner];
protected override searchEnabled = true;
public pageTitle = msg("Flows");
public pageDescription = msg(
public override searchPlaceholder = msg("Search for a flow by name or identifier...");
public override pageTitle = msg("Flows");
public override pageDescription = msg(
"Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them.",
);
public pageIcon = "pf-icon pf-icon-process-automation";
public override pageIcon = "pf-icon pf-icon-process-automation";
checkbox = true;
clearOnRefresh = true;
public override checkbox = true;
public override clearOnRefresh = true;
@property()
order = "slug";
public override order = "slug";
async apiEndpoint(): Promise<PaginatedResponse<Flow>> {
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(await this.defaultEndpointConfig());
@@ -90,21 +93,8 @@ export class FlowListPage extends TablePage<Flow> {
item.name,
Array.from(item.stages || []).length,
Array.from(item.policies || []).length,
html`<div>
<ak-forms-modal>
<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
slot="trigger"
class="pf-c-button pf-m-plain"
aria-label=${msg(str`Edit "${item.name}"`)}
>
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
html`<div class="ak-c-table__actions">
${IconEditButton(FlowForm, item.slug, item.name)}
<button
aria-label=${msg(str`Execute "${item.name}"`)}
class="pf-c-button pf-m-plain"
@@ -132,39 +122,37 @@ export class FlowListPage extends TablePage<Flow> {
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<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("New Flow")}</button>
</ak-forms-modal>
<ak-forms-modal>
<span slot="submit">${msg("Import")}</span>
<span slot="header">${msg("Import Flow")}</span>
<ak-blueprint-import-form slot="form">
<a
target="_blank"
rel="noopener noreferrer"
href=${docLink("/add-secure-apps/flows-stages/flow/examples/flows/")}
slot="read-more-link"
>${msg("Flow Examples")}</a
>
<span slot="banner-warning">
${msg(
"Warning: Flow imports are blueprint files, which may contain objects other than flows (such as users, policies, etc).",
)}<br />${msg(
"You should only import files from trusted sources and review blueprints before importing them.",
)}
</span>
</ak-blueprint-import-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Import")}</button>
</ak-forms-modal>
`;
protected renderObjectCreate(): SlottedTemplateResult {
return [
ModalInvokerButton(FlowForm),
html`<button
class="pf-c-button pf-m-primary"
type="button"
${modalInvoker(() => {
return html`<ak-blueprint-import-form>
<a
target="_blank"
rel="noopener noreferrer"
href=${docLink("/add-secure-apps/flows-stages/flow/examples/flows/")}
slot="read-more-link"
>${msg("Flow Examples")}</a
>
<span slot="banner-warning">
${msg(
"Warning: Flow imports are blueprint files, which may contain objects other than flows (such as users, policies, etc).",
)}<br />${msg(
"You should only import files from trusted sources and review blueprints before importing them.",
)}
</span>
</ak-blueprint-import-form>`;
})}
>
${msg("Import")}
</button>`,
];
}
renderToolbar(): TemplateResult {
protected renderToolbar(): SlottedTemplateResult {
return html`
${super.renderToolbar()}
<ak-forms-confirm

View File

@@ -24,7 +24,7 @@ import {
} from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, nothing, TemplateResult } from "lit";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
function createInvalidResponseOptions(): RadioOption<InvalidResponseActionEnum>[] {
@@ -52,6 +52,9 @@ function createInvalidResponseOptions(): RadioOption<InvalidResponseActionEnum>[
@customElement("ak-stage-binding-form")
export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
public static override verboseName = msg("Stage Binding");
public static override verboseNamePlural = msg("Stage Bindings");
async load() {
this.defaultOrder = await this.getOrder();
}
@@ -124,10 +127,11 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
</ak-form-element-horizontal>`;
}
protected override renderForm(): TemplateResult {
return html` ${this.renderTarget()}
protected override renderForm(): SlottedTemplateResult {
return html`${this.renderTarget()}
<ak-form-element-horizontal label=${msg("Stage")} required name="stage">
<ak-search-select
placeholder=${msg("Select a stage...")}
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
const args: StagesAllListRequest = {
ordering: "name",

View File

@@ -7,6 +7,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
@@ -83,24 +84,14 @@ 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>
<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>
html`<div class="ak-c-table__actions">
${IconEditButton(GroupForm, item.pk, item.name)}
</div>`,
];
}
protected renderObjectCreate(): TemplateResult {
return html`<button class="pf-c-button pf-m-primary" ${GroupForm.asModalInvoker()}>
${msg("New Group")}
</button>`;
protected renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(GroupForm);
}
}

View File

@@ -9,8 +9,8 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { renderModal } from "#elements/dialogs";
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";
@@ -52,7 +52,7 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
return renderModal(html`
<ak-form
headline=${msg("Select Groups")}
action-label=${msg("Confirm")}
submit-label=${msg("Confirm")}
@submit=${(event: AKFormSubmitEvent<Group[]>) => {
this.groupsToAdd = event.target.toJSON();
}}
@@ -125,7 +125,7 @@ export class RelatedGroupList extends Table<Group> {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Group(s)")}
action-label=${msg("Remove from Group(s)")}
submit-label=${msg("Remove from Group(s)")}
action-subtext=${msg(
str`Are you sure you want to remove user ${this.targetUser?.username} from the following groups?`,
)}

View File

@@ -15,10 +15,10 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton, renderModal } from "#elements/dialogs";
import { AKFormSubmitEvent, Form } from "#elements/forms/Form";
import { WithBrandConfig } from "#elements/mixins/branding";
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,9 @@ import { UserOption } from "#elements/user/utils";
import { AKLabel } from "#components/ak-label";
import { RecoveryButtons } from "#admin/users/recovery";
import { UserForm } from "#admin/users/UserForm";
import { UserImpersonateForm } from "#admin/users/UserImpersonateForm";
import { renderRecoveryButtons } from "#admin/users/UserListPage";
import {
CapabilitiesEnum,
@@ -91,11 +91,15 @@ export class AddRelatedUserForm extends Form<{ users: number[] }> {
return data;
}
protected openUserSelectionModal = () => {
protected openUserSelectionModal = (event?: Event) => {
if (event?.defaultPrevented) {
return;
}
return renderModal(html`
<ak-form
headline=${msg("Select users")}
action-label=${msg("Confirm")}
submit-label=${msg("Confirm")}
@submit=${(event: AKFormSubmitEvent<User[]>) => {
this.usersToAdd = event.target.toJSON();
}}
@@ -111,7 +115,7 @@ export class AddRelatedUserForm extends Form<{ users: number[] }> {
// table to allow the table to appear as an inline-block element next to the input group.
// This should be fixed by moving the `@container` query off `:host`.
return html` <ak-form-element-horizontal name="users">
return html`<ak-form-element-horizontal name="users">
${AKLabel(
{
slot: "label",
@@ -130,14 +134,16 @@ export class AddRelatedUserForm extends Form<{ users: number[] }> {
aria-label=${msg("Open user selection dialog")}
@click=${this.openUserSelectionModal}
>
<pf-tooltip position="top" content=${msg("Add users")}>
<pf-tooltip position="right" content=${msg("Add users")}>
<i class="fas fa-plus" aria-hidden="true"></i>
</pf-tooltip>
</button>
</div>
<div class="pf-c-form-control">
<ak-chip-group>
${this.usersToAdd.map((user) => {
<ak-chip-group
@click=${this.openUserSelectionModal}
placeholder=${msg("Select one or more users to assign...")}
>${this.usersToAdd.map((user) => {
return html`<ak-chip
removable
value=${ifDefined(user.pk)}
@@ -149,8 +155,8 @@ export class AddRelatedUserForm extends Form<{ users: number[] }> {
>
${UserOption(user)}
</ak-chip>`;
})}
</ak-chip-group>
})}</ak-chip-group
>
</div>
</div>
</ak-form-element-horizontal>`;
@@ -222,7 +228,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
return html`<ak-forms-delete-bulk
object-label=${msg("User(s)")}
action-label=${msg("Remove User(s)")}
submit-label=${msg("Remove User(s)")}
action=${msg("removed")}
action-subtext=${targetLabel
? msg(str`Are you sure you want to remove the selected users from ${targetLabel}?`)
@@ -271,16 +277,12 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
html`<ak-status-label ?good=${item.isActive}></ak-status-label>`,
Timestamp(item.lastLogin),
html`<div>
<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>
html`<div class="ak-c-table__actions">
${IconEditButton(UserForm, item.pk)}
${showImpersonate
? html`<button
class="pf-c-button pf-m-tertiary"
${UserImpersonateForm.asEditModalInvoker(item.pk)}
${UserImpersonateForm.asInstanceInvoker(item.pk)}
>
<pf-tooltip
position="top"
@@ -340,7 +342,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${renderRecoveryButtons({
${RecoveryButtons({
user: item,
brandHasRecoveryFlow: Boolean(this.brand.flowRecovery),
})}

View File

@@ -43,8 +43,8 @@ export class GroupForm extends ModelForm<Group, string> {
`,
];
public entitySingular = msg("Group");
public entityPlural = msg("Groups");
public static verboseName = msg("Group");
public static verboseNamePlural = msg("Groups");
#fetchGroups = (page: number, search?: string): Promise<DataProvision> => {
return new CoreApi(DEFAULT_CONFIG)

View File

@@ -84,6 +84,9 @@ function formatContentTypePlaceholder(contentType: ContentTypeEnum): string {
@customElement("ak-lifecycle-rule-form")
export class LifecycleRuleForm extends ModelForm<LifecycleRule, string, LifecycleRule | null> {
public static override verboseName = msg("Lifecycle Rule");
public static override verboseNamePlural = msg("Lifecycle Rules");
#targetSelectRef = createRef<SearchSelect<TargetObject>>();
#reviewerGroupsSelectRef = createRef<SearchSelect<Group>>();
#reviewerUsersSelectRef = createRef<SearchSelect<Group>>();

View File

@@ -11,14 +11,17 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { LifecycleRuleForm } from "#admin/lifecycle/LifecycleRuleForm";
import { LifecycleApi, LifecycleRule, ModelEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("ak-lifecycle-rule-list")
@@ -26,10 +29,10 @@ export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
public override expandable = true;
public override checkbox = true;
public override clearOnRefresh = true;
public pageTitle = msg("Object Lifecycle Rules");
public pageDescription = msg("Schedule periodic reviews for objects in authentik.");
public pageIcon = "pf-icon pf-icon-history";
public override searchPlaceholder = msg("Search for a lifecycle rule by name or target...");
public override pageTitle = msg("Object Lifecycle Rules");
public override pageDescription = msg("Schedule periodic reviews for objects in authentik.");
public override pageIcon = "pf-icon pf-icon-history";
public override order = "name";
@@ -41,11 +44,11 @@ export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
);
}
protected renderSectionBefore(): TemplateResult {
protected override renderSectionBefore(): SlottedTemplateResult {
return html`<ak-lifecycle-preview-banner></ak-lifecycle-preview-banner>`;
}
protected columns: TableColumn[] = [
protected override columns: TableColumn[] = [
[msg("Name"), "name"],
[msg("Target"), "content_type__model"],
[msg("Interval"), "interval"],
@@ -53,7 +56,7 @@ export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html` <ak-forms-delete-bulk
object-label=${msg("Lifecycle rule(s)")}
@@ -74,26 +77,14 @@ export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
</ak-forms-delete-bulk>`;
}
row(item: LifecycleRule): SlottedTemplateResult[] {
protected override row(item: LifecycleRule): SlottedTemplateResult[] {
return [
html`${item.name}`,
html`${item.targetVerbose}`,
html`${item.interval}`,
html`${item.gracePeriod}`,
html` <div>
<ak-forms-modal>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header">${msg("Update Lifecycle Rule")}</span>
<ak-lifecycle-rule-form
slot="form"
.instancePk=${item.id}
></ak-lifecycle-rule-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
item.name,
item.targetVerbose,
item.interval || msg("-"),
item.gracePeriod || msg("-"),
html`<div class="ak-c-table__actions">
${IconEditButton(LifecycleRuleForm, item.id, item.name)}
<ak-rbac-object-permission-modal
model=${ModelEnum.AuthentikLifecycleLifecyclerule}
@@ -104,7 +95,7 @@ export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
];
}
renderExpanded(item: LifecycleRule): TemplateResult {
protected override renderExpanded(item: LifecycleRule): SlottedTemplateResult {
const [appLabel, modelName] = ModelEnum.AuthentikLifecycleLifecyclerule.split(".");
return html`<dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group">
@@ -114,6 +105,7 @@ export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-task-list
search-placeholder=${msg("Search tasks...")}
.relObjAppLabel=${appLabel}
.relObjModel=${modelName}
.relObjId="${item.id}"
@@ -123,16 +115,8 @@ export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
</div>
</dl>`;
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">${msg("Create")}</span>
<span slot="header">${msg("Create Object Lifecycle Rule")}</span>
<ak-lifecycle-rule-form slot="form"></ak-lifecycle-rule-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
`;
protected override renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(LifecycleRuleForm);
}
}

View File

@@ -8,10 +8,12 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { createPaginatedResponse } from "#common/api/responses";
import { isResponseErrorLike } from "#common/errors/network";
import { ModalInvokerButton } from "#elements/dialogs";
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import { ifPreviousValue } from "#elements/utils/properties";
import { ObjectReviewForm } from "#admin/lifecycle/ObjectReviewForm";
import { LifecycleIterationStatus } from "#admin/lifecycle/utils";
import {
@@ -257,7 +259,7 @@ export class ObjectLifecyclePage extends Table<Review> {
];
}
protected override renderEmpty(): TemplateResult {
protected override renderEmpty(): SlottedTemplateResult {
return super.renderEmpty(
html`<ak-empty-state icon="pf-icon-task"
><span>${this.emptyStateMessage}</span></ak-empty-state
@@ -267,18 +269,12 @@ export class ObjectLifecyclePage extends Table<Review> {
protected renderObjectCreate(): SlottedTemplateResult {
if (!this.iteration?.userCanReview) {
return nothing;
return null;
}
return html`<ak-forms-modal>
<span slot="submit">${msg("Confirm Review")}</span>
<span slot="header">${msg("Confirm this object has been reviewed")}</span>
<ak-object-review-form slot="form" .iteration=${this.iteration}>
</ak-object-review-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Confirm Review")}
</button>
</ak-forms-modal>`;
return ModalInvokerButton(ObjectReviewForm, {
iteration: this.iteration,
});
}
protected override render(): SlottedTemplateResult {

View File

@@ -13,6 +13,11 @@ import { customElement, property } from "lit/decorators.js";
@customElement("ak-object-review-form")
export class ObjectReviewForm extends ModelForm<Review, string, Review | null> {
public static override verboseName = msg("Review");
public static override verboseNamePlural = msg("Reviews");
public static override submitVerb = msg("Confirm");
public static override createLabel = msg("Confirm");
@property({ attribute: false })
public iteration: LifecycleIteration | null = null;

View File

@@ -95,19 +95,19 @@ function providerProvider(type: OutpostTypeEnum): DataProvider {
@customElement("ak-outpost-form")
export class OutpostForm extends ModelForm<Outpost, string> {
public entitySingular = msg("Outpost");
public entityPlural = msg("Outposts");
public static verboseName = msg("Outpost");
public static verboseNamePlural = msg("Outposts");
@property()
type: OutpostTypeEnum = OutpostTypeEnum.Proxy;
@property({ type: String })
public type: OutpostTypeEnum = OutpostTypeEnum.Proxy;
@property({ type: Boolean })
embedded = false;
public embedded = false;
@state()
providers: DataProvider = providerProvider(this.type);
protected providers: DataProvider = providerProvider(this.type);
defaultConfig?: OutpostDefaultConfig;
protected defaultConfig?: OutpostDefaultConfig;
public override reset(): void {
super.reset();

View File

@@ -7,6 +7,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton } from "#elements/dialogs";
import { PFColor } from "#elements/Label";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
@@ -26,7 +27,9 @@ import { ifDefined } from "lit/directives/if-defined.js";
export class OutpostListPage extends TablePage<Outpost> {
protected override searchEnabled = true;
public override searchPlaceholder = msg("Search outposts...");
public override searchPlaceholder = msg(
"Search outposts by name, type or assigned integration...",
);
public override pageTitle = msg("Outposts");
public override pageDescription = msg(
"Outposts are deployments of authentik components to support different environments and protocols, like reverse proxies.",
@@ -70,19 +73,6 @@ export class OutpostListPage extends TablePage<Outpost> {
@property({ type: String })
public order = "name";
protected openEditModal = (event: Event) => {
const button = event.currentTarget as HTMLButtonElement;
const instancePk = button.dataset.instancePk!;
const managed = button.dataset.managed === "true";
const form = new OutpostForm();
form.instancePk = instancePk;
form.embedded = managed;
return form.showModal();
};
protected renderItemProviders(item: Outpost) {
if (item.providers.length < 1) {
return html`-`;
@@ -116,17 +106,11 @@ export class OutpostListPage extends TablePage<Outpost> {
html`<ak-outpost-health-simple
outpostId=${ifDefined(item.pk)}
></ak-outpost-health-simple>`,
html`<button
class="pf-c-button pf-m-plain"
aria-label=${msg(str`Edit ${item.name}`)}
data-instance-pk=${item.pk}
data-managed=${item.managed === embeddedOutpostManaged}
@click=${this.openEditModal}
>
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>`,
html`<div class="ak-c-table__actions">
${IconEditButton(OutpostForm, item.pk, item.name, {
embedded: item.managed === embeddedOutpostManaged,
})}
</div>`,
];
}

View File

@@ -12,6 +12,7 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { docLink } from "#common/global";
import { AKElement } from "#elements/Base";
import { IconTokenCopyButton } from "#elements/buttons/IconTokenCopyButton";
import { SlottedTemplateResult } from "#elements/types";
import { setPageDetails } from "#components/ak-page-navbar";
@@ -27,7 +28,6 @@ import { CSSResult, PropertyValues } from "lit";
import { html } from "lit-html";
import { guard } from "lit-html/directives/guard.js";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@@ -242,14 +242,7 @@ export class OutpostViewPage extends AKElement {
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">AUTHENTIK_TOKEN</span>
</label>
<div>
<ak-token-copy-button
class="pf-m-primary"
identifier="${ifDefined(this.outpost?.tokenIdentifier)}"
>
${msg("Click to copy token")}
</ak-token-copy-button>
</div>
<div>${IconTokenCopyButton(this.outpost?.tokenIdentifier)}</div>
</div>
<h3>
${msg(

View File

@@ -1,6 +1,6 @@
import "#admin/outposts/ServiceConnectionDockerForm";
import "#admin/outposts/ServiceConnectionKubernetesForm";
import "#admin/outposts/ServiceConnectionWizard";
import "#admin/outposts/ak-service-connection-wizard";
import "#admin/rbac/ObjectPermissionModal";
import "#components/ak-status-label";
import "#elements/buttons/SpinnerButton/index";
@@ -12,16 +12,23 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { CustomFormElementTagName } from "#elements/forms/unsafe";
import { IconEditButtonByTagName } from "#elements/dialogs";
import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton";
import { PFColor } from "#elements/Label";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { OutpostsApi, ServiceConnection, ServiceConnectionState } from "@goauthentik/api";
import { AKServiceConnectionWizard } from "#admin/outposts/ak-service-connection-wizard";
import { msg, str } from "@lit/localize";
import {
ModelEnum,
OutpostsApi,
ServiceConnection,
ServiceConnectionState,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -36,9 +43,12 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
public pageIcon = "pf-icon pf-icon-integration";
protected override searchEnabled = true;
checkbox = true;
expandable = true;
clearOnRefresh = true;
public override checkbox = true;
public override expandable = true;
public override clearOnRefresh = true;
public override searchPlaceholder = msg(
"Search for an outpost integration by name, type or assigned integration...",
);
async apiEndpoint(): Promise<PaginatedResponse<ServiceConnection>> {
const connections = await new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllList(
@@ -75,31 +85,19 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
row(item: ServiceConnection): SlottedTemplateResult[] {
const itemState = this.state[item.pk];
return [
html`${item.name}`,
html`${item.verboseName}`,
item.name,
item.verboseName,
html`<ak-status-label type="info" ?good=${item.local}></ak-status-label>`,
html`${itemState?.healthy
? html`<ak-label color=${PFColor.Green}>${ifDefined(itemState.version)}</ak-label>`
: html`<ak-label color=${PFColor.Red}>${msg("Unhealthy")}</ak-label>`}`,
html`
<ak-forms-modal>
${StrictUnsafe<CustomFormElementTagName>(item.component, {
slot: "form",
instancePk: item.pk,
submitLabel: msg("Save Changes"),
headline: msg(str`Update ${item.verboseName}`, {
id: "form.headline.update",
}),
})}
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
<ak-rbac-object-permission-modal model=${item.metaModelName} objectPk=${item.pk}>
</ak-rbac-object-permission-modal>
`,
html`<div class="ak-c-table__actions">
${IconEditButtonByTagName(item.component, item.pk, item.verboseName)}
${IconPermissionButton(item.name, {
model: item.metaModelName as ModelEnum,
objectPk: item.pk,
})}
</div>`,
];
}
@@ -161,8 +159,15 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
</ak-forms-delete-bulk>`;
}
renderObjectCreate(): TemplateResult {
return html`<ak-service-connection-wizard></ak-service-connection-wizard> `;
protected override renderObjectCreate(): SlottedTemplateResult {
return html`<button
class="pf-c-button pf-m-primary"
type="button"
aria-description="${msg("Open the wizard to create a new service connection.")}"
${AKServiceConnectionWizard.asModalInvoker()}
>
${msg("New Outpost Integration")}
</button>`;
}
}

View File

@@ -1,81 +0,0 @@
import "#admin/outposts/ServiceConnectionDockerForm";
import "#admin/outposts/ServiceConnectionKubernetesForm";
import "#elements/wizard/FormWizardPage";
import "#elements/wizard/TypeCreateWizardPage";
import "#elements/wizard/Wizard";
import { DEFAULT_CONFIG } from "#common/api/config";
import { AKElement } from "#elements/Base";
import { StrictUnsafe } from "#elements/utils/unsafe";
import type { Wizard } from "#elements/wizard/Wizard";
import { OutpostsApi, TypeCreate } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, html, TemplateResult } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@customElement("ak-service-connection-wizard")
export class ServiceConnectionWizard extends AKElement {
static styles: CSSResult[] = [PFButton];
@property()
createText = msg("Create");
@property({ attribute: false })
connectionTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllTypesList().then((types) => {
this.connectionTypes = types;
});
}
render(): TemplateResult {
return html`
<ak-wizard
.steps=${["initial"]}
header=${msg("New outpost integration")}
description=${msg("Create a new outpost integration.")}
>
<ak-wizard-page-type-create
slot="initial"
.types=${this.connectionTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = [
"initial",
`type-${ev.detail.component}-${ev.detail.modelName}`,
];
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.connectionTypes.map((type) => {
return html`
<ak-wizard-page-form
slot=${`type-${type.component}-${type.modelName}`}
label=${msg(str`Create ${type.name}`)}
>
${StrictUnsafe(type.component)}
</ak-wizard-page-form>
`;
})}
<button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button>
</ak-wizard>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-service-connection-wizard": ServiceConnectionWizard;
}
}

View File

@@ -0,0 +1,32 @@
import "#admin/outposts/ServiceConnectionDockerForm";
import "#admin/outposts/ServiceConnectionKubernetesForm";
import "#elements/wizard/FormWizardPage";
import "#elements/wizard/TypeCreateWizardPage";
import "#elements/wizard/Wizard";
import { DEFAULT_CONFIG } from "#common/api/config";
import { CreateWizard } from "#elements/wizard/CreateWizard";
import { OutpostsApi, TypeCreate } from "@goauthentik/api";
import { msg } from "@lit/localize/init/install";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
@customElement("ak-service-connection-wizard")
export class AKServiceConnectionWizard extends CreateWizard {
public static override verboseName = msg("Outpost Integration");
public static override verboseNamePlural = msg("Outpost Integrations");
#api = new OutpostsApi(DEFAULT_CONFIG);
protected apiEndpoint = (): Promise<TypeCreate[]> => {
return this.#api.outpostsServiceConnectionsAllTypesList();
};
}
declare global {
interface HTMLElementTagNameMap {
"ak-service-connection-wizard": AKServiceConnectionWizard;
}
}

View File

@@ -3,6 +3,9 @@ import { ModelForm } from "#elements/forms/ModelForm";
import { msg } from "@lit/localize";
export abstract class BasePolicyForm<T extends object> extends ModelForm<T, string> {
public static override verboseName = msg("Policy");
public static override verboseNamePlural = msg("Policies");
getSuccessMessage(): string {
return this.instance
? msg("Successfully updated policy.")

View File

@@ -1,6 +1,6 @@
import "#admin/groups/ak-group-form";
import "#admin/policies/PolicyBindingForm";
import "#admin/policies/PolicyWizard";
import "#admin/policies/ak-policy-wizard";
import "#admin/rbac/ObjectPermissionModal";
import "#admin/users/UserForm";
import "#components/ak-status-label";
@@ -9,14 +9,16 @@ import "#elements/forms/DeleteBulkForm";
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 { asInstanceInvokerByTagName, modalInvoker } from "#elements/dialogs";
import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { GroupForm } from "#admin/groups/ak-group-form";
import { PolicyWizard } from "#admin/policies/ak-policy-wizard";
import { PolicyBindingForm, PolicyBindingNotice } from "#admin/policies/PolicyBindingForm";
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
import { UserForm } from "#admin/users/UserForm";
@@ -24,36 +26,12 @@ import { UserForm } from "#admin/users/UserForm";
import { ModelEnum, PoliciesApi, PolicyBinding } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
import { css, CSSResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@customElement("ak-bound-policies-list")
export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends Table<T> {
@property()
target?: string;
@property()
policyEngineMode: string = "";
@property({ type: Array })
allowedTypes: PolicyBindingCheckTarget[] = [
PolicyBindingCheckTarget.Policy,
PolicyBindingCheckTarget.Group,
PolicyBindingCheckTarget.User,
];
@property({ type: Array })
typeNotices: PolicyBindingNotice[] = [];
checkbox = true;
clearOnRefresh = true;
order = "order";
protected bindingEditForm = "ak-policy-binding-form";
static styles: CSSResult[] = [
public static styles: CSSResult[] = [
...super.styles,
css`
/* Align policy engine description to left padding of the card title */
@@ -63,11 +41,34 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
`,
];
@property({ type: String })
public target: string | null = null;
@property({ type: String })
public policyEngineMode: string = "";
@property({ type: Array })
public allowedTypes: PolicyBindingCheckTarget[] = [
PolicyBindingCheckTarget.Policy,
PolicyBindingCheckTarget.Group,
PolicyBindingCheckTarget.User,
];
@property({ type: Array })
public typeNotices: PolicyBindingNotice[] = [];
public override checkbox = true;
public override clearOnRefresh = true;
public override order = "order";
protected bindingEditForm = "ak-policy-binding-form";
get allowedTypesLabel(): string {
return this.allowedTypes.map((ct) => PolicyBindingCheckTargetToLabel(ct)).join(" / ");
}
async apiEndpoint(): Promise<PaginatedResponse<T>> {
protected override async apiEndpoint(): Promise<PaginatedResponse<T>> {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({
...(await this.defaultEndpointConfig()),
target: this.target || "",
@@ -78,7 +79,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
return item.order?.toString() ?? null;
}
protected columns: TableColumn[] = [
protected override columns: TableColumn[] = [
[msg("Order"), "order"],
[this.allowedTypesLabel],
[msg("Enabled"), "enabled"],
@@ -86,7 +87,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
[msg("Actions"), null, msg("Row Actions")],
];
getPolicyUserGroupRowLabel(item: PolicyBinding): string {
protected getPolicyUserGroupRowLabel(item: PolicyBinding): string {
if (item.policy) {
return msg(str`Policy ${item.policyObj?.name}`);
} else if (item.group) {
@@ -97,7 +98,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
return msg("-");
}
getPolicyUserGroupRow(item: PolicyBinding): TemplateResult {
protected getPolicyUserGroupRow(item: PolicyBinding): SlottedTemplateResult {
const label = this.getPolicyUserGroupRowLabel(item);
if (item.user) {
return html` <a href=${`#/identity/users/${item.user}`}> ${label} </a> `;
@@ -108,159 +109,158 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
return html`${label}`;
}
getObjectEditButton(item: PolicyBinding): SlottedTemplateResult {
protected getObjectEditButton(item: PolicyBinding): SlottedTemplateResult {
if (item.policyObj) {
return html`<ak-forms-modal>
${StrictUnsafe<CustomFormElementTagName>(item.policyObj.component, {
slot: "form",
instancePk: item.policyObj.pk,
submitLabel: msg("Save Changes"),
headline: msg(str`Update ${item.policyObj.name}`, {
id: "form.headline.update",
}),
})}
return html`<button
type="button"
class="pf-c-button pf-m-secondary"
${asInstanceInvokerByTagName(item.policyObj?.component, item.policyObj?.pk)}
>
${msg("Edit Policy")}
</button>`;
}
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Edit Policy")}
</button>
</ak-forms-modal>`;
} else if (item.groupObj) {
return html`<ak-forms-modal>
<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">
${msg("Edit Group")}
</button>
</ak-forms-modal>`;
} else if (item.userObj) {
if (item.groupObj) {
return html`<button
class="pf-c-button pf-m-secondary"
${UserForm.asEditModalInvoker(item.userObj.pk)}
${GroupForm.asInstanceInvoker(item.groupObj?.pk)}
>
${msg("Edit Group")}
</button>`;
}
if (item.userObj) {
return html`<button
class="pf-c-button pf-m-secondary"
${UserForm.asInstanceInvoker(item.userObj?.pk)}
>
${msg("Edit User")}
</button>`;
}
return nothing;
return null;
}
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Policy binding(s)")}
.objects=${this.selectedElements}
.metadata=${(item: PolicyBinding) => {
return [
{ key: msg("Order"), value: item.order.toString() },
{
key: this.allowedTypesLabel,
value: this.getPolicyUserGroupRowLabel(item),
},
];
}}
.usedBy=${(item: PolicyBinding) => {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsUsedByList({
policyBindingUuid: item.pk,
});
}}
.delete=${(item: PolicyBinding) => {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsDestroy({
policyBindingUuid: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
return html`<ak-spinner-button .callAction=${this.refreshListener} class="pf-m-secondary">
${msg("Refresh")}</ak-spinner-button
><ak-forms-delete-bulk
object-label=${msg("Policy binding(s)")}
.objects=${this.selectedElements}
.metadata=${(item: PolicyBinding) => {
return [
{ key: msg("Order"), value: item.order.toString() },
{
key: this.allowedTypesLabel,
value: this.getPolicyUserGroupRowLabel(item),
},
];
}}
.usedBy=${(item: PolicyBinding) => {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsUsedByList({
policyBindingUuid: item.pk,
});
}}
.delete=${(item: PolicyBinding) => {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsDestroy({
policyBindingUuid: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
row(item: PolicyBinding): SlottedTemplateResult[] {
protected renderNewPolicyButton(): SlottedTemplateResult {
return html`<button
class="pf-c-button pf-m-primary"
type="button"
aria-description="${msg("Open the wizard to create a new policy.")}"
${modalInvoker(PolicyWizard, {
showBindingPage: true,
bindingTarget: this.target,
})}
>
${msg("Create and bind Policy")}
</button>`;
}
protected override row(item: PolicyBinding): SlottedTemplateResult[] {
return [
html`<pre>${item.order}</pre>`,
html`${this.getPolicyUserGroupRow(item)}`,
html`<ak-status-label type="warning" ?good=${item.enabled}></ak-status-label>`,
html`${item.timeout}`,
html` ${this.getObjectEditButton(item)}
<ak-forms-modal size=${PFSize.Medium}>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header">${msg("Update Binding")}</span>
${StrictUnsafe<PolicyBindingForm>(this.bindingEditForm, {
slot: "form",
instancePk: item.pk,
allowedTypes: this.allowedTypes,
typeNotices: this.typeNotices,
targetPk: this.target || "",
submitLabel: msg("Save Changes"),
headline: msg("Update Binding"),
html`<div class="ak-c-table__actions">
${this.getObjectEditButton(item)}
<button
type="button"
class="pf-c-button pf-m-secondary"
${modalInvoker(() => {
return StrictUnsafe<PolicyBindingForm>(this.bindingEditForm, {
instancePk: item.pk,
allowedTypes: this.allowedTypes,
typeNotices: this.typeNotices,
targetPk: this.target || "",
});
})}
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Edit Binding")}
</button>
</ak-forms-modal>
<ak-rbac-object-permission-modal
model=${ModelEnum.AuthentikPoliciesPolicybinding}
objectPk=${item.pk}
>
</ak-rbac-object-permission-modal>`,
${msg("Edit Binding")}
</button>
${IconPermissionButton(this.getPolicyUserGroupRowLabel(item), {
model: ModelEnum.AuthentikPoliciesPolicybinding,
objectPk: item.pk,
})}
</div>`,
];
}
renderEmpty(): TemplateResult {
protected override renderEmpty(): SlottedTemplateResult {
return super.renderEmpty(
html`<ak-empty-state icon="pf-icon-module"
><span>${msg("No Policies bound.")}</span>
<div slot="body">${msg("No policies are currently bound to this object.")}</div>
<fieldset class="pf-c-form__group pf-m-action" slot="primary">
<legend class="sr-only">${msg("Policy actions")}</legend>
<ak-policy-wizard
createText=${msg("Create and bind Policy")}
showBindingPage
bindingTarget=${ifDefined(this.target)}
></ak-policy-wizard>
<ak-forms-modal size=${PFSize.Medium}>
${StrictUnsafe<PolicyBindingForm>(this.bindingEditForm, {
slot: "form",
allowedTypes: this.allowedTypes,
typeNotices: this.typeNotices,
targetPk: this.target || "",
submitLabel: msg("Create"),
headline: msg("Create Binding"),
${this.renderNewPolicyButton()}
<button
type="button"
class="pf-c-button pf-m-secondary"
${modalInvoker(() => {
return StrictUnsafe<PolicyBindingForm>(this.bindingEditForm, {
allowedTypes: this.allowedTypes,
typeNotices: this.typeNotices,
targetPk: this.target || "",
});
})}
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Bind existing policy/group/user")}
</button>
</ak-forms-modal>
>
${msg("Bind existing policy/group/user")}
</button>
</fieldset>
</ak-empty-state>`,
);
}
renderToolbar(): TemplateResult {
renderToolbar(): SlottedTemplateResult {
return html`${this.allowedTypes.includes(PolicyBindingCheckTarget.Policy)
? html`<ak-policy-wizard
createText=${msg("Create and bind Policy")}
showBindingPage
bindingTarget=${ifDefined(this.target)}
></ak-policy-wizard>`
: nothing}
<ak-forms-modal size=${PFSize.Medium}>
${StrictUnsafe<PolicyBindingForm>(this.bindingEditForm, {
slot: "form",
allowedTypes: this.allowedTypes,
typeNotices: this.typeNotices,
targetPk: this.target || "",
submitLabel: msg("Create"),
headline: msg("Create Binding"),
? this.renderNewPolicyButton()
: null}
<button
type="button"
class="pf-c-button pf-m-secondary"
${modalInvoker(() => {
return StrictUnsafe<PolicyBindingForm>(this.bindingEditForm, {
allowedTypes: this.allowedTypes,
typeNotices: this.typeNotices,
targetPk: this.target || "",
});
})}
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg(str`Bind existing ${this.allowedTypesLabel}`)}
</button>
</ak-forms-modal> `;
>
${msg(str`Bind existing ${this.allowedTypesLabel}`)}
</button>`;
}
renderPolicyEngineMode() {

View File

@@ -39,7 +39,9 @@ export class PolicyBindingForm<T extends PolicyBinding = PolicyBinding> extends
T,
string
> {
static styles: CSSResult[] = [...super.styles, PFContent];
public static styles: CSSResult[] = [...super.styles, PFContent];
public static verboseName = msg("Policy Binding");
public static verboseNamePlural = msg("Policy Bindings");
async loadInstance(pk: string): Promise<T> {
const binding = await new PoliciesApi(DEFAULT_CONFIG).policiesBindingsRetrieve({

View File

@@ -1,5 +1,5 @@
import "#admin/policies/PolicyTestForm";
import "#admin/policies/PolicyWizard";
import "#admin/policies/ak-policy-wizard";
import "#admin/policies/dummy/DummyPolicyForm";
import "#admin/policies/event_matcher/EventMatcherPolicyForm";
import "#admin/policies/expiry/ExpiryPolicyForm";
@@ -15,33 +15,37 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { CustomFormElementTagName } from "#elements/forms/unsafe";
import { IconEditButtonByTagName, modalInvoker } from "#elements/dialogs";
import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton";
import { PFColor } from "#elements/Label";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { PoliciesApi, Policy } from "@goauthentik/api";
import { PolicyWizard } from "#admin/policies/ak-policy-wizard";
import { PolicyTestForm } from "#admin/policies/PolicyTestForm";
import { ModelEnum, PoliciesApi, Policy } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("ak-policy-list")
export class PolicyListPage extends TablePage<Policy> {
protected override searchEnabled = true;
public pageTitle = msg("Policies");
public pageDescription = msg(
public override pageTitle = msg("Policies");
public override pageDescription = msg(
"Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Stages.",
);
public pageIcon = "pf-icon pf-icon-infrastructure";
public override pageIcon = "pf-icon pf-icon-infrastructure";
checkbox = true;
clearOnRefresh = true;
public override searchPlaceholder = msg("Search for a policy by name or type...");
@property()
order = "name";
public override checkbox = true;
public override clearOnRefresh = true;
public override order = "name";
async apiEndpoint(): Promise<PaginatedResponse<Policy>> {
return new PoliciesApi(DEFAULT_CONFIG).policiesAllList(await this.defaultEndpointConfig());
@@ -54,7 +58,7 @@ export class PolicyListPage extends TablePage<Policy> {
[msg("Actions")],
];
row(item: Policy): SlottedTemplateResult[] {
protected override row(item: Policy): SlottedTemplateResult[] {
return [
html`<div>${item.name}</div>
${(item.boundTo || 0) > 0
@@ -65,38 +69,32 @@ export class PolicyListPage extends TablePage<Policy> {
${msg("Warning: Policy is not assigned.")}
</ak-label>`}`,
html`${item.verboseName}`,
html`<ak-forms-modal>
${StrictUnsafe<CustomFormElementTagName>(item.component, {
slot: "form",
instancePk: item.pk,
submitLabel: msg("Save Changes"),
headline: msg(str`Update ${item.verboseName}`, {
id: "form.headline.update",
}),
})}
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
html`<div class="ak-c-table__actions">
${IconEditButtonByTagName(item.component, item.pk)}
${IconPermissionButton(item.name, {
model: item.metaModelName as ModelEnum,
objectPk: item.pk,
})}
<ak-rbac-object-permission-modal model=${item.metaModelName} objectPk=${item.pk}>
</ak-rbac-object-permission-modal>
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Test")}</span>
<span slot="header">${msg("Test Policy")}</span>
<ak-policy-test-form slot="form" .policy=${item}> </ak-policy-test-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Test")}>
<i class="fas fa-vial" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>`,
<button
class="pf-c-button pf-m-plain"
${modalInvoker(
PolicyTestForm,
{ policy: item },
{
closedBy: "closerequest",
},
)}
>
<pf-tooltip position="top" content=${msg("Test")}>
<i class="fas fa-vial" aria-hidden="true"></i>
</pf-tooltip>
</button>
</div>`,
];
}
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Policy / Policies")}
@@ -118,12 +116,21 @@ export class PolicyListPage extends TablePage<Policy> {
</ak-forms-delete-bulk>`;
}
renderObjectCreate(): TemplateResult {
return html`<ak-policy-wizard> </ak-policy-wizard>`;
protected override renderObjectCreate(): SlottedTemplateResult {
return html`
<button
class="pf-c-button pf-m-primary"
type="button"
aria-description="${msg("Open the wizard to create a new policy.")}"
${PolicyWizard.asModalInvoker()}
>
${msg("New Policy")}
</button>
`;
}
renderToolbar(): TemplateResult {
return html` ${super.renderToolbar()}
protected override renderToolbar(): SlottedTemplateResult {
return html`${super.renderToolbar()}
<ak-forms-confirm
successMessage=${msg("Successfully cleared policy cache")}
errorMessage=${msg("Failed to delete policy cache")}

View File

@@ -5,8 +5,12 @@ import "#elements/forms/HorizontalFormElement";
import "#elements/forms/SearchSelect/index";
import { DEFAULT_CONFIG } from "#common/api/config";
import { PFSize } from "#common/enums";
import { Form } from "#elements/forms/Form";
import { SlottedTemplateResult } from "#elements/types";
import { AKLabel } from "#components/ak-label";
import {
CoreApi,
@@ -21,40 +25,20 @@ import {
import YAML from "yaml";
import { msg } from "@lit/localize";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
import { css, CSSResult, html, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
@customElement("ak-policy-test-form")
export class PolicyTestForm extends Form<PolicyTestRequest> {
@property({ attribute: false })
public policy?: Policy;
public static verboseName = msg("Policy");
public static verboseNamePlural = msg("Policies");
public static createLabel = msg("Test");
@state()
protected result: PolicyTestResult | null = null;
public override cancelable = true;
@property({ attribute: false })
public request?: PolicyTestRequest;
public override reset(): void {
super.reset();
this.result = null;
}
getSuccessMessage(): string {
return msg("Successfully sent test-request.");
}
async send(data: PolicyTestRequest): Promise<PolicyTestResult> {
this.request = data;
const result = await new PoliciesApi(DEFAULT_CONFIG).policiesAllTestCreate({
policyUuid: this.policy?.pk || "",
policyTestRequest: data,
});
return (this.result = result);
}
public override size = PFSize.XLarge;
static styles: CSSResult[] = [
...super.styles,
@@ -66,9 +50,52 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
`,
];
renderResult(): TemplateResult {
return html`
<ak-form-element-horizontal label=${msg("Passing")}>
#api = new PoliciesApi(DEFAULT_CONFIG);
protected override formatSubmitLabel(submitLabel?: string | null): string {
return submitLabel || msg("Run Test");
}
@property({ attribute: false })
public policy: Policy | null = null;
@state()
protected result: PolicyTestResult | null = null;
@property({ attribute: false })
public request: PolicyTestRequest | null = null;
public get verboseName(): string | null {
return this.policy?.verboseName || null;
}
public get verboseNamePlural(): string | null {
return this.policy?.verboseNamePlural || null;
}
public override reset(): void {
super.reset();
this.result = null;
}
public override getSuccessMessage(): string {
return msg("Successfully sent test-request.");
}
protected override async send(data: PolicyTestRequest): Promise<PolicyTestResult> {
this.request = data;
this.result = await this.#api.policiesAllTestCreate({
policyUuid: this.policy?.pk || "",
policyTestRequest: data,
});
return this.result;
}
protected renderResult(): SlottedTemplateResult {
return html`<ak-form-element-horizontal label=${msg("Passing")}>
<div class="pf-c-form__group-label">
<div class="c-form__horizontal-group">
<span class="pf-c-form__label-text">
@@ -103,13 +130,13 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
</dl>
</div>
</div>
</ak-form-element-horizontal>
`;
</ak-form-element-horizontal>`;
}
protected override renderForm(): TemplateResult {
protected override renderForm(): SlottedTemplateResult {
return html`<ak-form-element-horizontal label=${msg("User")} required name="user">
<ak-search-select
placeholder=${msg("Select a user...")}
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
ordering: "username",
@@ -135,14 +162,28 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
>
</ak-search-select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Context")} name="context">
<ak-codemirror mode="yaml" value=${YAML.stringify(this.request?.context ?? {})}>
<ak-form-element-horizontal name="context">
${AKLabel(
{
slot: "label",
className: "pf-c-form__group-label",
htmlFor: "context",
},
msg("Context"),
)}
<ak-codemirror
id="context"
mode="yaml"
value=${YAML.stringify(this.request?.context ?? {})}
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Set custom attributes using YAML or JSON.")}
</p>
</ak-form-element-horizontal>
${this.result ? this.renderResult() : nothing}`;
${this.result ? this.renderResult() : null}`;
}
}

View File

@@ -1,126 +0,0 @@
import "#admin/policies/dummy/DummyPolicyForm";
import "#admin/policies/event_matcher/EventMatcherPolicyForm";
import "#admin/policies/expiry/ExpiryPolicyForm";
import "#admin/policies/expression/ExpressionPolicyForm";
import "#admin/policies/geoip/GeoIPPolicyForm";
import "#admin/policies/password/PasswordPolicyForm";
import "#admin/policies/reputation/ReputationPolicyForm";
import "#admin/policies/unique_password/UniquePasswordPolicyForm";
import "#elements/wizard/FormWizardPage";
import "#elements/wizard/TypeCreateWizardPage";
import "#elements/wizard/Wizard";
import { DEFAULT_CONFIG } from "#common/api/config";
import { AKElement } from "#elements/Base";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { FormWizardPage } from "#elements/wizard/FormWizardPage";
import type { Wizard } from "#elements/wizard/Wizard";
import { PolicyBindingForm } from "#admin/policies/PolicyBindingForm";
import { PoliciesApi, Policy, PolicyBinding, TypeCreate } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, html, nothing, TemplateResult } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@customElement("ak-policy-wizard")
export class PolicyWizard extends AKElement {
static styles: CSSResult[] = [PFButton];
@property()
createText = msg("Create");
@property({ type: Boolean })
showBindingPage = false;
@property()
bindingTarget?: string;
@property({ attribute: false })
policyTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new PoliciesApi(DEFAULT_CONFIG).policiesAllTypesList().then((types) => {
this.policyTypes = types;
});
}
selectListener = ({ detail }: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
const { component, modelName } = detail;
const idx = this.wizard.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.wizard.steps = this.wizard.steps.filter((step) => !step.startsWith("type-"));
this.wizard.steps.splice(idx, 0, `type-${component}-${modelName}`);
this.wizard.isValid = true;
};
render(): TemplateResult {
return html`
<ak-wizard
.steps=${this.showBindingPage ? ["initial", "create-binding"] : ["initial"]}
header=${msg("New policy")}
description=${msg("Create a new policy.")}
>
<ak-wizard-page-type-create
slot="initial"
.types=${this.policyTypes}
@select=${this.selectListener}
>
</ak-wizard-page-type-create>
${this.policyTypes.map((type) => {
return html`
<ak-wizard-page-form
slot=${`type-${type.component}-${type.modelName}`}
label=${msg(str`Create ${type.name}`)}
>
${StrictUnsafe(type.component)}
</ak-wizard-page-form>
`;
})}
${this.showBindingPage
? html`<ak-wizard-page-form
slot="create-binding"
label=${msg("Create Binding")}
.activePageCallback=${async (context: FormWizardPage) => {
const createSlot = context.host.steps[1];
const bindingForm =
context.querySelector<PolicyBindingForm>(
"ak-policy-binding-form",
);
if (!bindingForm) return;
bindingForm.instance = {
policy: (context.host.state[createSlot] as Policy).pk,
} as PolicyBinding;
}}
>
<ak-policy-binding-form
.targetPk=${this.bindingTarget}
></ak-policy-binding-form>
</ak-wizard-page-form>`
: nothing}
<button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button>
</ak-wizard>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-policy-wizard": PolicyWizard;
}
}

View File

@@ -0,0 +1,90 @@
import "#admin/policies/dummy/DummyPolicyForm";
import "#admin/policies/event_matcher/EventMatcherPolicyForm";
import "#admin/policies/expiry/ExpiryPolicyForm";
import "#admin/policies/expression/ExpressionPolicyForm";
import "#admin/policies/geoip/GeoIPPolicyForm";
import "#admin/policies/password/PasswordPolicyForm";
import "#admin/policies/reputation/ReputationPolicyForm";
import "#admin/policies/unique_password/UniquePasswordPolicyForm";
import "#elements/wizard/FormWizardPage";
import "#elements/wizard/TypeCreateWizardPage";
import "#elements/wizard/Wizard";
import { DEFAULT_CONFIG } from "#common/api/config";
import { SlottedTemplateResult } from "#elements/types";
import { CreateWizard } from "#elements/wizard/CreateWizard";
import { FormWizardPage } from "#elements/wizard/FormWizardPage";
import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage";
import { PolicyBindingForm } from "#admin/policies/PolicyBindingForm";
import { PoliciesApi, Policy, PolicyBinding, TypeCreate } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html, PropertyValues } from "lit";
import { property } from "lit/decorators.js";
@customElement("ak-policy-wizard")
export class PolicyWizard extends CreateWizard {
#api = new PoliciesApi(DEFAULT_CONFIG);
@property({ type: Boolean })
public showBindingPage = false;
@property()
public bindingTarget: string | null = null;
public override initialSteps = this.showBindingPage
? ["initial", "create-binding"]
: ["initial"];
public static override verboseName = msg("Policy");
public static override verboseNamePlural = msg("Policies");
public override layout = TypeCreateWizardPageLayouts.list;
protected apiEndpoint = async (requestInit?: RequestInit): Promise<TypeCreate[]> => {
return this.#api.policiesAllTypesList(requestInit);
};
protected updated(changedProperties: PropertyValues<this>): void {
super.updated(changedProperties);
if (changedProperties.has("showBindingPage")) {
this.initialSteps = this.showBindingPage ? ["initial", "create-binding"] : ["initial"];
}
}
protected createBindingActivate = async (page: FormWizardPage) => {
const createSlot = page.host.steps[1];
const bindingForm = page.querySelector<PolicyBindingForm>("ak-policy-binding-form");
if (!bindingForm) return;
bindingForm.instance = {
policy: (page.host.state[createSlot] as Policy).pk,
} as PolicyBinding;
};
protected renderForms(): SlottedTemplateResult {
const bindingPage = this.showBindingPage
? html`<ak-wizard-page-form
slot="create-binding"
headline=${msg("Create Binding")}
.activePageCallback=${this.createBindingActivate}
>
<ak-policy-binding-form .targetPk=${this.bindingTarget}></ak-policy-binding-form>
</ak-wizard-page-form>`
: null;
return [super.renderForms(), bindingPage];
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-policy-wizard": PolicyWizard;
}
}

View File

@@ -1,3 +1,4 @@
import "#components/ak-text-input";
import "#components/ak-switch-input";
import "#elements/forms/FormGroup";
import "#elements/forms/HorizontalFormElement";
@@ -34,19 +35,21 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
}
protected override renderForm(): TemplateResult {
return html` <span>
return html`<span>
${msg(
"A policy used for testing. Always returns the same result as specified below after waiting a random duration.",
)}
</span>
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name || "")}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-text-input
label=${msg("Policy Name")}
required
name="name"
value="${ifDefined(this.instance?.name || "")}"
placeholder=${msg("Type a policy name...")}
autocomplete="off"
autofocus
>
</ak-text-input>
<ak-switch-input
name="executionLogging"
label=${msg("Execution logging")}

View File

@@ -15,25 +15,24 @@ import { ModelEnum, PoliciesApi, Reputation } from "@goauthentik/api";
import getUnicodeFlagIcon from "country-flag-icons/unicode";
import { msg } from "@lit/localize";
import { html, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("ak-policy-reputation-list")
export class ReputationListPage extends TablePage<Reputation> {
protected override searchEnabled = true;
public pageTitle = msg("Reputation scores");
public pageDescription = msg(
public override pageTitle = msg("Reputation scores");
public override pageDescription = msg(
"Reputation for IP and user identifiers. Scores are decreased for each failed login and increased for each successful login.",
);
public pageIcon = "fa fa-ban";
public override pageIcon = "fa fa-ban";
public override order = "identifier";
public override checkbox = true;
public override clearOnRefresh = true;
public override searchPlaceholder = msg("Search for a reputation by identifier or IP...");
@property()
order = "identifier";
checkbox = true;
clearOnRefresh = true;
async apiEndpoint(): Promise<PaginatedResponse<Reputation>> {
protected override async apiEndpoint(): Promise<PaginatedResponse<Reputation>> {
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresList({
...(await this.defaultEndpointConfig()),
});
@@ -51,7 +50,7 @@ export class ReputationListPage extends TablePage<Reputation> {
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Reputation")}
@@ -73,9 +72,9 @@ export class ReputationListPage extends TablePage<Reputation> {
</ak-forms-delete-bulk>`;
}
row(item: Reputation): SlottedTemplateResult[] {
protected override row(item: Reputation): SlottedTemplateResult[] {
return [
html`${item.identifier}`,
item.identifier,
html`${item.ipGeoData?.country
? html` ${getUnicodeFlagIcon(item.ipGeoData.country)} `
: nothing}

View File

@@ -1,4 +1,5 @@
import "#elements/CodeMirror/ak-codemirror";
import "#components/ak-text-input";
import { docLink } from "#common/global";
@@ -20,6 +21,9 @@ export abstract class BasePropertyMappingForm<T extends PropertyMapping> extends
> {
protected docLink: string | URL = "/add-secure-apps/providers/property-mappings/expression";
public static override verboseName = msg("Property Mapping");
public static override verboseNamePlural = msg("Property Mappings");
getSuccessMessage(): string {
return this.instance
? msg("Successfully updated mapping.")
@@ -31,14 +35,15 @@ export abstract class BasePropertyMappingForm<T extends PropertyMapping> extends
}
protected override renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
return html`<ak-text-input
label=${msg("Mapping Name")}
placeholder=${msg("Type a name for this mapping...")}
autocomplete="off"
required
name="name"
value="${ifDefined(this.instance?.name)}"
>
</ak-text-input>
${this.renderExtraFields()}
<ak-form-element-horizontal label=${msg("Expression")} required name="expression">
<ak-codemirror mode="python" value="${ifDefined(this.instance?.expression)}">

View File

@@ -14,7 +14,7 @@ import "#admin/property-mappings/PropertyMappingSourceSAMLForm";
import "#admin/property-mappings/PropertyMappingSourceSCIMForm";
import "#admin/property-mappings/PropertyMappingSourceTelegramForm";
import "#admin/property-mappings/PropertyMappingTestForm";
import "#admin/property-mappings/PropertyMappingWizard";
import "#admin/property-mappings/ak-property-mapping-wizard";
import "#admin/rbac/ObjectPermissionModal";
import "#elements/forms/DeleteBulkForm";
import "#elements/forms/ModalForm";
@@ -22,49 +22,54 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { CustomFormElementTagName } from "#elements/forms/unsafe";
import { IconEditButtonByTagName, modalInvoker } from "#elements/dialogs";
import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { PropertyMapping, PropertymappingsApi } from "@goauthentik/api";
import { AKPropertyMappingWizard } from "#admin/property-mappings/ak-property-mapping-wizard";
import { PropertyMappingTestForm } from "#admin/property-mappings/PropertyMappingTestForm";
import { msg, str } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ModelEnum, PropertyMapping, PropertymappingsApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
@customElement("ak-property-mapping-list")
export class PropertyMappingListPage extends TablePage<PropertyMapping> {
protected override searchEnabled = true;
public pageTitle = msg("Property Mappings");
public pageDescription = msg("Control how authentik exposes and interprets information.");
public pageIcon = "pf-icon pf-icon-blueprint";
public override pageTitle = msg("Property Mappings");
public override pageDescription = msg(
"Control how authentik exposes and interprets information.",
);
public override pageIcon = "pf-icon pf-icon-blueprint";
public override searchPlaceholder = msg("Search for a property mapping by name or type...");
checkbox = true;
clearOnRefresh = true;
public override checkbox = true;
public override clearOnRefresh = true;
@property()
order = "name";
public override order = "name";
@state()
hideManaged = getURLParam<boolean>("hideManaged", true);
protected hideManaged = getURLParam<boolean>("hideManaged", true);
async apiEndpoint(): Promise<PaginatedResponse<PropertyMapping>> {
protected override async apiEndpoint(): Promise<PaginatedResponse<PropertyMapping>> {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllList({
...(await this.defaultEndpointConfig()),
managedIsnull: this.hideManaged ? true : undefined,
});
}
protected columns: TableColumn[] = [
protected override columns: TableColumn[] = [
[msg("Name"), "name"],
[msg("Type"), "type"],
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
protected override renderToolbarSelected(): SlottedTemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Property Mapping(s)")}
@@ -86,46 +91,45 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
</ak-forms-delete-bulk>`;
}
row(item: PropertyMapping): SlottedTemplateResult[] {
protected override row(item: PropertyMapping): SlottedTemplateResult[] {
return [
html`${item.name}`,
html`${item.verboseName}`,
html` <ak-forms-modal>
${StrictUnsafe<CustomFormElementTagName>(item.component, {
slot: "form",
instancePk: item.pk,
submitLabel: msg("Save Changes"),
headline: msg(str`Update ${item.verboseName}`, {
id: "form.headline.update",
}),
})}
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
<ak-rbac-object-permission-modal model=${item.metaModelName} objectPk=${item.pk}>
</ak-rbac-object-permission-modal>
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Test")}</span>
<span slot="header">${msg("Test Property Mapping")}</span>
<ak-property-mapping-test-form slot="form" .mapping=${item}>
</ak-property-mapping-test-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Test")}>
<i class="fas fa-vial" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal>`,
html`<div class="ak-c-table__actions">
${IconEditButtonByTagName(item.component, item.pk)}
${IconPermissionButton(item.name, {
model: item.metaModelName as ModelEnum,
objectPk: item.pk,
})}
<button
class="pf-c-button pf-m-plain"
${modalInvoker(
PropertyMappingTestForm,
{ mapping: item },
{
closedBy: "closerequest",
},
)}
>
<pf-tooltip position="top" content=${msg("Test")}>
<i class="fas fa-vial" aria-hidden="true"></i>
</pf-tooltip>
</button>
</div>`,
];
}
renderObjectCreate(): TemplateResult {
return html`<ak-property-mapping-wizard></ak-property-mapping-wizard> `;
protected override renderObjectCreate(): SlottedTemplateResult {
return html`<button
class="pf-c-button pf-m-primary"
${modalInvoker(AKPropertyMappingWizard)}
>
${msg("New Property Mapping")}
</button>`;
}
renderToolbarAfter(): TemplateResult {
protected override renderToolbarAfter(): SlottedTemplateResult {
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">

View File

@@ -3,8 +3,12 @@ import "#elements/forms/HorizontalFormElement";
import "#elements/forms/SearchSelect/index";
import { DEFAULT_CONFIG } from "#common/api/config";
import { PFSize } from "#common/enums";
import { Form } from "#elements/forms/Form";
import { SlottedTemplateResult } from "#elements/types";
import { AKLabel } from "#components/ak-label";
import {
CoreApi,
@@ -22,45 +26,80 @@ import {
import YAML from "yaml";
import { msg } from "@lit/localize";
import { html, nothing, TemplateResult } from "lit";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@customElement("ak-property-mapping-test-form")
export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
@property({ attribute: false })
mapping?: PropertyMapping;
export class PropertyMappingTestForm extends Form<PropertyMappingTestRequest> {
public static verboseName = msg("Property Mapping");
public static verboseNamePlural = msg("Property Mappings");
public static createLabel = msg("Test");
public override cancelable = true;
public override size = PFSize.XLarge;
#api = new PropertymappingsApi(DEFAULT_CONFIG);
protected override formatSubmitLabel(submitLabel?: string | null): string {
return submitLabel || msg("Run Test");
}
@property({ attribute: false })
result?: PropertyMappingTestResult;
public mapping: PropertyMapping | null = null;
@property({ attribute: false })
request?: PropertyMappingTestRequest;
public result: PropertyMappingTestResult | null = null;
getSuccessMessage(): string {
@property({ attribute: false })
public request: PropertyMappingTestRequest | null = null;
public override getSuccessMessage(): string {
return msg("Successfully sent test-request.");
}
async send(data: PropertyMappingTestRequest): Promise<PropertyMappingTestResult> {
protected override async send(
data: PropertyMappingTestRequest,
): Promise<PropertyMappingTestResult> {
this.request = data;
const result = await new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllTestCreate({
this.result = await this.#api.propertymappingsAllTestCreate({
pmUuid: this.mapping?.pk || "",
propertyMappingTestRequest: data,
formatResult: true,
});
return (this.result = result);
return this.result;
}
renderResult(): TemplateResult {
return html`<ak-form-element-horizontal label=${msg("Result")}>
public get verboseName(): string | null {
return this.mapping?.verboseName || null;
}
public get verboseNamePlural(): string | null {
return this.mapping?.verboseNamePlural || null;
}
protected renderResult(): SlottedTemplateResult {
return html`<ak-form-element-horizontal>
${this.result?.successful
? html`<ak-codemirror
mode="javascript"
readonly
value="${ifDefined(this.result?.result)}"
>
</ak-codemirror>`
: html` <div class="pf-c-form__group-label">
? html`${AKLabel(
{
slot: "label",
className: "pf-c-form__group-label",
htmlFor: "result",
},
msg("Result"),
)}
<ak-codemirror
id="result"
mode="javascript"
readonly
value="${ifDefined(this.result?.result)}"
>
</ak-codemirror>`
: html`<div class="pf-c-form__group-label">
<div class="c-form__horizontal-group">
<span class="pf-c-form__label-text">
<pre>${this.result?.result}</pre>
@@ -70,15 +109,26 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
</ak-form-element-horizontal>`;
}
renderExampleButtons() {
return this.mapping?.metaModelName ===
ModelEnum.AuthentikSourcesLdapLdapsourcepropertymapping
? html`<p>${msg("Example context data")}</p>
${this.renderExampleLDAP()}`
: nothing;
protected renderExampleButtons(): SlottedTemplateResult {
if (
this.mapping?.metaModelName !== ModelEnum.AuthentikSourcesLdapLdapsourcepropertymapping
) {
return null;
}
return html`<div class="pf-c-form__group">
${AKLabel(
{
slot: "label",
className: "pf-c-form__group-label",
},
msg("Example Context Data"),
)}
<p class="pf-c-form__helper-text">${this.renderExampleLDAP()}</p>
</div>`;
}
renderExampleLDAP(): TemplateResult {
protected renderExampleLDAP(): SlottedTemplateResult {
return html`
<button
type="button"
@@ -127,9 +177,10 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
`;
}
protected override renderForm(): TemplateResult {
protected override renderForm(): SlottedTemplateResult {
return html`<ak-form-element-horizontal label=${msg("User")} name="user">
<ak-search-select
placeholder=${msg("Select a user...")}
blankable
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
@@ -144,7 +195,7 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
.renderElement=${(user: User): string => {
return user.username;
}}
.renderDescription=${(user: User): TemplateResult => {
.renderDescription=${(user: User): SlottedTemplateResult => {
return html`${user.name}`;
}}
.value=${(user: User | undefined): number | undefined => {
@@ -158,6 +209,7 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Group")} name="group">
<ak-search-select
placeholder=${msg("Select a group...")}
blankable
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
@@ -181,17 +233,30 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
>
</ak-search-select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Context")} name="context">
<ak-codemirror mode="yaml" value=${YAML.stringify(this.request?.context ?? {})}>
${this.renderExampleButtons()}
<ak-form-element-horizontal name="context">
${AKLabel(
{
slot: "label",
className: "pf-c-form__group-label",
htmlFor: "context",
},
msg("Context"),
)}
<ak-codemirror
id="context"
mode="yaml"
value=${YAML.stringify(this.request?.context ?? {})}
>
</ak-codemirror>
<p class="pf-c-form__helper-text">${this.renderExampleButtons()}</p>
</ak-form-element-horizontal>
${this.result ? this.renderResult() : nothing}`;
${this.result ? this.renderResult() : null}`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-property-mapping-test-form": PolicyTestForm;
"ak-property-mapping-test-form": PropertyMappingTestForm;
}
}

View File

@@ -1,92 +0,0 @@
import "#admin/property-mappings/PropertyMappingNotification";
import "#admin/property-mappings/PropertyMappingProviderGoogleWorkspaceForm";
import "#admin/property-mappings/PropertyMappingProviderMicrosoftEntraForm";
import "#admin/property-mappings/PropertyMappingProviderRACForm";
import "#admin/property-mappings/PropertyMappingProviderRadiusForm";
import "#admin/property-mappings/PropertyMappingProviderSAMLForm";
import "#admin/property-mappings/PropertyMappingProviderSCIMForm";
import "#admin/property-mappings/PropertyMappingProviderScopeForm";
import "#admin/property-mappings/PropertyMappingSourceKerberosForm";
import "#admin/property-mappings/PropertyMappingSourceLDAPForm";
import "#admin/property-mappings/PropertyMappingSourceOAuthForm";
import "#admin/property-mappings/PropertyMappingSourcePlexForm";
import "#admin/property-mappings/PropertyMappingSourceSAMLForm";
import "#admin/property-mappings/PropertyMappingSourceSCIMForm";
import "#admin/property-mappings/PropertyMappingSourceTelegramForm";
import "#admin/property-mappings/PropertyMappingTestForm";
import "#elements/wizard/FormWizardPage";
import "#elements/wizard/TypeCreateWizardPage";
import "#elements/wizard/Wizard";
import { DEFAULT_CONFIG } from "#common/api/config";
import { AKElement } from "#elements/Base";
import { StrictUnsafe } from "#elements/utils/unsafe";
import type { Wizard } from "#elements/wizard/Wizard";
import { PropertymappingsApi, TypeCreate } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html, TemplateResult } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@customElement("ak-property-mapping-wizard")
export class PropertyMappingWizard extends AKElement {
static styles = [PFButton];
@property({ attribute: false })
mappingTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
async firstUpdated(): Promise<void> {
this.mappingTypes = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsAllTypesList();
}
render(): TemplateResult {
return html`
<ak-wizard
.steps=${["initial"]}
header=${msg("New property mapping")}
description=${msg("Create a new property mapping.")}
>
<ak-wizard-page-type-create
slot="initial"
.types=${this.mappingTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = [
"initial",
`type-${ev.detail.component}-${ev.detail.modelName}`,
];
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.mappingTypes.map((type) => {
return html`
<ak-wizard-page-form
slot=${`type-${type.component}-${type.modelName}`}
label=${msg(str`Create ${type.name}`)}
>
${StrictUnsafe(type.component)}
</ak-wizard-page-form>
`;
})}
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-wizard>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-property-mapping-wizard": PropertyMappingWizard;
}
}

View File

@@ -0,0 +1,46 @@
import "#admin/property-mappings/PropertyMappingNotification";
import "#admin/property-mappings/PropertyMappingProviderGoogleWorkspaceForm";
import "#admin/property-mappings/PropertyMappingProviderMicrosoftEntraForm";
import "#admin/property-mappings/PropertyMappingProviderRACForm";
import "#admin/property-mappings/PropertyMappingProviderRadiusForm";
import "#admin/property-mappings/PropertyMappingProviderSAMLForm";
import "#admin/property-mappings/PropertyMappingProviderSCIMForm";
import "#admin/property-mappings/PropertyMappingProviderScopeForm";
import "#admin/property-mappings/PropertyMappingSourceKerberosForm";
import "#admin/property-mappings/PropertyMappingSourceLDAPForm";
import "#admin/property-mappings/PropertyMappingSourceOAuthForm";
import "#admin/property-mappings/PropertyMappingSourcePlexForm";
import "#admin/property-mappings/PropertyMappingSourceSAMLForm";
import "#admin/property-mappings/PropertyMappingSourceSCIMForm";
import "#admin/property-mappings/PropertyMappingSourceTelegramForm";
import "#admin/property-mappings/PropertyMappingTestForm";
import "#elements/wizard/FormWizardPage";
import "#elements/wizard/TypeCreateWizardPage";
import "#elements/wizard/Wizard";
import { DEFAULT_CONFIG } from "#common/api/config";
import { CreateWizard } from "#elements/wizard/CreateWizard";
import { PropertymappingsApi, TypeCreate } from "@goauthentik/api";
import { msg } from "@lit/localize/init/install";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
@customElement("ak-property-mapping-wizard")
export class AKPropertyMappingWizard extends CreateWizard {
#api = new PropertymappingsApi(DEFAULT_CONFIG);
protected override apiEndpoint(requestInit?: RequestInit): Promise<TypeCreate[]> {
return this.#api.propertymappingsAllTypesList(requestInit);
}
public static override verboseName = msg("Property Mapping");
public static override verboseNamePlural = msg("Property Mappings");
}
declare global {
interface HTMLElementTagNameMap {
"ak-property-mapping-wizard": AKPropertyMappingWizard;
}
}

View File

@@ -11,6 +11,9 @@ import { msg } from "@lit/localize";
* @prop {number} instancePk - The primary key of the instance to load.
*/
export abstract class BaseProviderForm<T extends object> extends ModelForm<T, number> {
public static override verboseName = msg("Provider");
public static override verboseNamePlural = msg("Providers");
public override getSuccessMessage(): string {
return this.instance
? msg("Successfully updated provider.")

View File

@@ -1,5 +1,5 @@
import "#admin/applications/ApplicationWizardHint";
import "#admin/providers/ProviderWizard";
import "#admin/providers/ak-provider-wizard";
import "#admin/providers/google_workspace/GoogleWorkspaceProviderForm";
import "#admin/providers/ldap/LDAPProviderForm";
import "#admin/providers/microsoft_entra/MicrosoftEntraProviderForm";
@@ -13,20 +13,20 @@ import "#admin/providers/ssf/SSFProviderFormPage";
import "#admin/providers/wsfed/WSFederationProviderForm";
import "#elements/buttons/SpinnerButton/index";
import "#elements/forms/DeleteBulkForm";
import "#elements/forms/ModalForm";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { CustomFormElementTagName } from "#elements/forms/unsafe";
import { IconEditButtonByTagName } from "#elements/dialogs";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { AKProviderWizard } from "#admin/providers/ak-provider-wizard";
import { Provider, ProvidersApi } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
@@ -49,7 +49,7 @@ export class ProviderListPage extends TablePage<Provider> {
public order = "name";
public searchLabel = msg("Provider Search");
public searchPlaceholder = msg("Search for providers…");
public searchPlaceholder = msg("Search for provider by name, type or assigned application...");
override async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
return new ProvidersApi(DEFAULT_CONFIG).providersAllList(
@@ -112,33 +112,24 @@ export class ProviderListPage extends TablePage<Provider> {
return [
html`<a href="#/core/providers/${item.pk}">${item.name}</a>`,
this.#rowApp(item),
html`${item.verboseName}`,
html`<div>
<ak-forms-modal>
${StrictUnsafe<CustomFormElementTagName>(item.component, {
slot: "form",
instancePk: item.pk,
submitLabel: msg("Save Changes"),
headline: msg(str`Update ${item.verboseName}`, {
id: "form.headline.update",
}),
})}
<button
aria-label=${msg(str`Edit "${item.name}" provider`)}
slot="trigger"
class="pf-c-button pf-m-plain"
>
<pf-tooltip position="top" content=${msg("Edit")}>
<i aria-hidden="true" class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
item.verboseName,
html`<div class="ak-c-table__actions">
${IconEditButtonByTagName(item.component, item.pk)}
</div>`,
];
}
override renderObjectCreate(): TemplateResult {
return html`<ak-provider-wizard> </ak-provider-wizard> `;
protected override renderObjectCreate(): SlottedTemplateResult {
return html`
<button
class="pf-c-button pf-m-primary"
type="button"
aria-description="${msg("Open the wizard to create a new provider.")}"
${AKProviderWizard.asModalInvoker()}
>
${msg("New Provider")}
</button>
`;
}
}

View File

@@ -1,98 +0,0 @@
import "#elements/LicenseNotice";
import "#admin/providers/ldap/LDAPProviderForm";
import "#admin/providers/oauth2/OAuth2ProviderForm";
import "#admin/providers/proxy/ProxyProviderForm";
import "#admin/providers/saml/SAMLProviderForm";
import "#admin/providers/saml/SAMLProviderImportForm";
import "#elements/wizard/FormWizardPage";
import "#elements/wizard/TypeCreateWizardPage";
import "#elements/wizard/Wizard";
import { DEFAULT_CONFIG } from "#common/api/config";
import { AKElement } from "#elements/Base";
import { StrictUnsafe } from "#elements/utils/unsafe";
import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage";
import type { Wizard } from "#elements/wizard/Wizard";
import { ProvidersApi, TypeCreate } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, html, TemplateResult } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@customElement("ak-provider-wizard")
export class ProviderWizard extends AKElement {
static styles: CSSResult[] = [PFButton];
@property()
createText = msg("Create");
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
@property({ attribute: false })
public finalHandler?: () => Promise<void>;
@query("ak-wizard")
wizard?: Wizard;
connectedCallback() {
super.connectedCallback();
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
this.providerTypes = providerTypes;
});
}
render(): TemplateResult {
return html`
<ak-wizard
.steps=${["initial"]}
header=${msg("New provider")}
description=${msg("Create a new provider.")}
.finalHandler=${this.finalHandler}
>
<ak-wizard-page-type-create
slot="initial"
layout=${TypeCreateWizardPageLayouts.grid}
.types=${this.providerTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = ["initial", `type-${ev.detail.component}`];
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.providerTypes.map((type) => {
return html`
<ak-wizard-page-form
slot=${`type-${type.component}`}
label=${msg(str`Create ${type.name}`)}
>
${StrictUnsafe(type.component)}
</ak-wizard-page-form>
`;
})}
<button
aria-label=${msg("New Provider")}
aria-description="${msg("Open the wizard to create a new provider.")}"
type="button"
part="button trigger"
slot="trigger"
class="pf-c-button pf-m-primary"
>
${msg("Create")}
</button>
</ak-wizard>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-provider-wizard": ProviderWizard;
}
}

Some files were not shown because too many files have changed in this diff Show More