mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
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:
@@ -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",
|
||||
|
||||
@@ -36,6 +36,7 @@ export default defineConfig({
|
||||
testIdAttribute: "data-test-id",
|
||||
baseURL,
|
||||
trace: "on-first-retry",
|
||||
colorScheme: "dark",
|
||||
launchOptions: {
|
||||
logger: {
|
||||
isEnabled() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.",
|
||||
)}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)],
|
||||
]);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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")}
|
||||
<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,
|
||||
|
||||
@@ -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>`,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)")}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> `;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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()];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?`,
|
||||
)}
|
||||
|
||||
@@ -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),
|
||||
})}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>`,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
32
web/src/admin/outposts/ak-service-connection-wizard.ts
Normal file
32
web/src/admin/outposts/ak-service-connection-wizard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
90
web/src/admin/policies/ak-policy-wizard.ts
Normal file
90
web/src/admin/policies/ak-policy-wizard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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")}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user