web: Update WebDriver types. Fix issues surrounding async tests.

WIP;

WIP 2

web: Flesh out fixtures, test IDs.

web: Flesh out provider tests.

web: Flesh out LDAP test.

web: Fix typo.

web: Allow base URL to be updated.

web: Clean up.

web: Tidy types.

web: Update ARIA attributes for better test targeting.

web: Clean up message labeling.

web: Clean up ARIA labels.

web: Flesh out table ARIA labels.

web: Flesh out series.

web: Fix linter.

web: Clean up test reporting, timing issues. Add RADIUS test.
This commit is contained in:
Teffen Ellis
2025-05-30 23:31:38 +02:00
parent 2fdf345271
commit e4c8c79ed4
234 changed files with 7333 additions and 10350 deletions

View File

@ -141,6 +141,7 @@ skip = [
"**/web/src/locales", "**/web/src/locales",
"**/web/xliff", "**/web/xliff",
"**/web/out", "**/web/out",
"**/web/playwright-report",
"./web/storybook-static", "./web/storybook-static",
"./web/custom-elements.json", "./web/custom-elements.json",
"./website/build", "./website/build",

2
web/.gitignore vendored
View File

@ -25,6 +25,8 @@ lib-cov
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage coverage
playwright-report
test-results
*.lcov *.lcov
# nyc test coverage # nyc test coverage

View File

@ -27,11 +27,6 @@ const inlineCSSPlugin = {
}, },
}; };
/**
* @satisfies {InlineConfig}
*/
// const viteFinal = ;
/** /**
* @satisfies {StorybookConfig} * @satisfies {StorybookConfig}
*/ */

89
web/e2e/elements/proxy.ts Normal file
View File

@ -0,0 +1,89 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { LocatorContext } from "#e2e/selectors/types";
import { ConsoleLogger } from "#logger/node";
import { Locator, Page, expect } from "@playwright/test";
import { kebabCase } from "change-case";
export type LocatorMatchers = ReturnType<typeof expect<Locator>>;
export interface LocatorProxy extends Pick<Locator, keyof Locator> {
$: Locator;
expect: LocatorMatchers;
}
// Type helpers to extract the shape of the proxy
export type DeepLocatorProxy<T> =
Disposable & T extends Record<string, any>
? T extends HTMLElement
? LocatorProxy
: {
[K in keyof T]: DeepLocatorProxy<T[K]>;
}
: LocatorProxy;
export function createLocatorProxy<T extends Record<string, any>>(
ctx: LocatorContext,
initialPathPrefix: string[] = [],
dataAttribute: string = "test-id",
): DeepLocatorProxy<T> {
dataAttribute = kebabCase(dataAttribute);
function createProxy(path: string[] = initialPathPrefix): any {
const proxyCache = new Map<string, LocatorProxy>();
return new Proxy({} as any, {
get(_, property: string) {
// Build the current path
const currentPath = [...path, property];
// Convert the path to kebab-case and join with hyphens
const selectorValue = currentPath.map((segment) => kebabCase(segment)).join("-");
const selector = `[data-${dataAttribute}="${selectorValue}"]`;
// Create a locator for the current selector
const locator = ctx.locator(selector);
if (proxyCache.has(selector)) {
ConsoleLogger.debug(`Using cached locator for ${selector}`);
return proxyCache.get(selector)!;
}
// Return a new proxy that also behaves like a Locator
// This allows us to either continue chaining or use Locator methods
const nextProxy = new Proxy(locator, {
get(target, prop) {
if (typeof prop === "string") {
// The user is likely trying to access a property on the page.
if (prop === "$") {
return target as any;
}
if (prop === "expect") {
return expect(target);
}
}
// If the property exists on the Locator, use it
if (prop in target) {
const value = (target as any)[prop];
// Bind methods to the locator instance
if (typeof value === "function") {
return value.bind(target);
}
return value;
}
// Otherwise, continue building the path
return createProxy(currentPath)[prop];
},
});
proxyCache.set(selector, nextProxy as LocatorProxy);
return nextProxy;
},
});
}
return createProxy() as DeepLocatorProxy<T>;
}

View File

@ -0,0 +1,174 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import type { LocatorContext } from "#e2e/selectors/types";
import { Page, expect } from "@playwright/test";
export class FormFixture extends PageFixture {
static fixtureName = "Form";
//#region Selector Methods
//#endregion
//#region Field Methods
/**
* Set the value of a text input.
*
* @param fieldName The name of the form element.
* @param value the value to set.
*/
public fill = async (
fieldName: string,
value: string,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent
.getByRole("textbox", {
name: fieldName,
})
.or(
parent.getByRole("spinbutton", {
name: fieldName,
}),
)
.first();
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
await control.fill(value);
};
/**
* Set the value of a radio or checkbox input.
*
* @param fieldName The name of the form element.
* @param value the value to set.
*/
public setInputCheck = async (
fieldName: string,
value: boolean = true,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent.locator("ak-switch-input", {
hasText: fieldName,
});
await control.scrollIntoViewIfNeeded();
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
const currentChecked = await control
.getAttribute("checked")
.then((value) => value !== null);
if (currentChecked === value) {
return;
}
await control.click();
};
/**
* Set the value of a radio or checkbox input.
*
* @param fieldName The name of the form element.
* @param pattern the value to set.
*/
public setRadio = async (
groupName: string,
fieldName: string,
parent: LocatorContext = this.page,
): Promise<void> => {
const group = parent.getByRole("group", { name: groupName });
await expect(group, `Field "${groupName}" should be visible`).toBeVisible();
const control = parent.getByRole("radio", { name: fieldName });
await control.setChecked(true, {
force: true,
});
};
/**
* Set the value of a search select input.
*
* @param fieldLabel The name of the search select element.
* @param pattern The text to match against the search select entry.
*/
public selectSearchValue = async (
fieldLabel: string,
pattern: string | RegExp,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent.getByRole("textbox", { name: fieldLabel });
await expect(
control,
`Search select control (${fieldLabel}) should be visible`,
).toBeVisible();
const fieldName = await control.getAttribute("name");
if (!fieldName) {
throw new Error(`Unable to find name attribute on search select (${fieldLabel})`);
}
// Find the search select input control and activate it.
await control.click();
const button = this.page
// ---
.locator(`div[data-managed-for*="${fieldName}"] button`, {
hasText: pattern,
});
if (!button) {
throw new Error(
`Unable to find an ak-search-select entry matching ${fieldLabel}:${pattern.toString()}`,
);
}
await button.click();
await this.page.keyboard.press("Tab");
await control.blur();
};
public setFormGroup = async (
pattern: string | RegExp,
value: boolean = true,
parent: LocatorContext = this.page,
) => {
const control = parent
.locator("ak-form-group", {
hasText: pattern,
})
.first();
const currentOpen = await control.getAttribute("open").then((value) => value !== null);
if (currentOpen === value) {
this.logger.debug(`Form group ${pattern} is already ${value ? "open" : "closed"}`);
return;
}
this.logger.debug(`Toggling form group ${pattern} to ${value ? "open" : "closed"}`);
await control.click();
if (value) {
await expect(control).toHaveAttribute("open");
} else {
await expect(control).not.toHaveAttribute("open");
}
};
//#endregion
//#region Lifecycle
constructor(page: Page, testName: string) {
super({ page, testName });
}
//#endregion
}

View File

@ -0,0 +1,29 @@
import { ConsoleLogger, FixtureLogger } from "#logger/node";
import { Page } from "@playwright/test";
export interface PageFixtureOptions {
page: Page;
testName: string;
}
export abstract class PageFixture {
/**
* The name of the fixture.
*
* Used for logging.
*/
static fixtureName: string;
protected readonly logger: FixtureLogger;
protected readonly page: Page;
protected readonly testName: string;
constructor({ page, testName }: PageFixtureOptions) {
this.page = page;
this.testName = testName;
const Constructor = this.constructor as typeof PageFixture;
this.logger = ConsoleLogger.fixture(Constructor.fixtureName, this.testName);
}
}

View File

@ -0,0 +1,41 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import type { LocatorContext } from "#e2e/selectors/types";
import { Page } from "@playwright/test";
export type GetByRoleParameters = Parameters<Page["getByRole"]>;
export type ARIARole = GetByRoleParameters[0];
export type ARIAOptions = GetByRoleParameters[1];
export type ClickByName = (name: string) => Promise<void>;
export type ClickByRole = (
role: ARIARole,
options?: ARIAOptions,
context?: LocatorContext,
) => Promise<void>;
export class PointerFixture extends PageFixture {
public static fixtureName = "Pointer";
public click = (
name: string,
optionsOrRole?: ARIAOptions | ARIARole,
context: LocatorContext = this.page,
): Promise<void> => {
if (typeof optionsOrRole === "string") {
return context.getByRole(optionsOrRole, { name }).click();
}
const options = {
...optionsOrRole,
name,
};
return (
context
// ---
.getByRole("button", options)
.or(context.getByRole("link", options))
.click()
);
};
}

View File

@ -0,0 +1,118 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import { Page, expect } from "@playwright/test";
export const GOOD_USERNAME = "test-admin@goauthentik.io";
export const GOOD_PASSWORD = "test-runner";
export const BAD_USERNAME = "bad-username@bad-login.io";
export const BAD_PASSWORD = "-this-is-a-bad-password-";
export interface LoginInit {
username?: string;
password?: string;
to?: URL | string;
}
export class SessionFixture extends PageFixture {
static fixtureName = "Session";
public static readonly pathname = "/if/flow/default-authentication-flow/";
//#region Selectors
public $identificationStage = this.page.locator("ak-stage-identification");
/**
* The username field on the login page.
*/
public $usernameField = this.$identificationStage.locator('input[name="uidField"]');
/**
* The button to continue with the login process,
* typically to the password flow stage.
*/
public $submitUsernameStageButton = this.$identificationStage.locator('button[type="submit"]');
public $passwordStage = this.page.locator("ak-stage-password");
public $passwordField = this.$passwordStage.locator('input[name="password"]');
/**
* The button to submit the the login flow,
* typically redirecting to the authenticated interface.
*/
public $submitPasswordStageButton = this.$passwordStage.locator('button[type="submit"]');
/**
* A possible authentication failure message.
*/
public $authFailureMessage = this.page.locator(".pf-m-error");
//#endregion
constructor(page: Page, testName: string) {
super({ page, testName });
}
//#region Specific interactions
public async submitUsernameStage(username: string) {
this.logger.info("Submitting username stage", username);
await this.$usernameField.fill(username);
await expect(this.$submitUsernameStageButton).toBeEnabled();
await this.$submitUsernameStageButton.click();
}
public async submitPasswordStage(password: string) {
this.logger.info("Submitting password stage");
await this.$passwordField.fill(password);
await expect(this.$submitPasswordStageButton).toBeEnabled();
await this.$submitPasswordStageButton.click();
}
public checkAuthenticated = async (): Promise<boolean> => {
// TODO: Check if the user is authenticated via API
return true;
};
/**
* Log into the application.
*/
public async login({
username = GOOD_USERNAME,
password = GOOD_PASSWORD,
to = SessionFixture.pathname,
}: LoginInit = {}) {
this.logger.info("Logging in...");
const initialURL = new URL(this.page.url());
if (initialURL.pathname === SessionFixture.pathname) {
this.logger.info("Skipping navigation because we're already in a authentication flow");
} else {
await this.page.goto(to.toString());
}
await this.submitUsernameStage(username);
await this.$passwordField.waitFor({ state: "visible" });
await this.submitPasswordStage(password);
const expectedPathname = typeof to === "string" ? to : to.pathname;
await this.page.waitForURL(`**${expectedPathname}`);
}
//#endregion
//#region Navigation
public async toLoginPage() {
await this.page.goto(SessionFixture.pathname);
}
}

55
web/e2e/index.ts Normal file
View File

@ -0,0 +1,55 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { DeepLocatorProxy, createLocatorProxy } from "#e2e/elements/proxy";
import { FormFixture } from "#e2e/fixtures/FormFixture";
import { PointerFixture } from "#e2e/fixtures/PointerFixture";
import { SessionFixture } from "#e2e/fixtures/SessionFixture";
import { createOUIDNameEngine } from "#e2e/selectors/ouid";
import { type Page, test as base } from "@playwright/test";
export { expect } from "@playwright/test";
type TestIDLocatorProxy = DeepLocatorProxy<TestIDSelectorMap>;
interface E2EFixturesTestScope {
/**
* A proxy to retrieve elements by test ID.
*
* ```ts
* const $button = $.button;
* ```
*/
$: TestIDLocatorProxy;
session: SessionFixture;
pointer: PointerFixture;
form: FormFixture;
}
interface E2EWorkerScope {
selectorRegistration: void;
}
export const test = base.extend<E2EFixturesTestScope, E2EWorkerScope>({
selectorRegistration: [
async ({ playwright }, use) => {
await playwright.selectors.register("ouid", createOUIDNameEngine);
await use();
},
{ auto: true, scope: "worker" },
],
$: async ({ page }, use) => {
await use(createLocatorProxy<TestIDSelectorMap>(page));
},
session: async ({ page }, use, { title }) => {
await use(new SessionFixture(page, title));
},
form: async ({ page }, use, { title }) => {
await use(new FormFixture(page, title));
},
pointer: async ({ page }, use, { title }) => {
await use(new PointerFixture({ page, testName: title }));
},
});

44
web/e2e/selectors/ouid.ts Normal file
View File

@ -0,0 +1,44 @@
/* eslint-disable no-console */
type SelectorRoot = Document | ShadowRoot;
export function createOUIDNameEngine() {
const attributeName = "data-ouid-component-name";
console.log("Creating OUID selector engine!!");
return {
// Returns all elements matching given selector in the root's subtree.
queryAll(scope: SelectorRoot, componentName: string) {
const result: Element[] = [];
const match = (element: Element) => {
const name = element.getAttribute(attributeName);
if (name === componentName) {
result.push(element);
}
};
const query = (root: Element | ShadowRoot | Document) => {
const shadows: ShadowRoot[] = [];
if ((root as Element).shadowRoot) {
shadows.push((root as Element).shadowRoot!);
}
for (const element of root.querySelectorAll("*")) {
match(element);
if (element.shadowRoot) {
shadows.push(element.shadowRoot);
}
}
shadows.forEach(query);
};
query(scope);
return result;
},
};
}

View File

@ -0,0 +1,13 @@
import type { Locator } from "@playwright/test";
export type LocatorContext = Pick<
Locator,
| "locator"
| "getByRole"
| "getByTestId"
| "getByText"
| "getByLabel"
| "getByAltText"
| "getByTitle"
| "getByPlaceholder"
>;

View File

@ -0,0 +1,59 @@
import { IDGenerator } from "@goauthentik/core/id";
import {
Config as NameConfig,
adjectives,
colors,
uniqueNamesGenerator,
} from "unique-names-generator";
/**
* Given a dictionary of words, slice the dictionary to only include words that start with the given letter.
*/
export function alliterate(dictionary: string[], letter: string): string[] {
let firstIndex = 0;
for (let i = 0; i < dictionary.length; i++) {
if (dictionary[i][0] === letter) {
firstIndex = i;
break;
}
}
let lastIndex = firstIndex;
for (let i = firstIndex; i < dictionary.length; i++) {
if (dictionary[i][0] !== letter) {
lastIndex = i;
break;
}
}
return dictionary.slice(firstIndex, lastIndex);
}
export function createRandomName({
seed = IDGenerator.randomID(),
...config
}: Partial<NameConfig> = {}) {
const randomLetterIndex =
typeof seed === "number"
? seed
: Array.from(seed).reduce((acc, char) => acc + char.charCodeAt(0), 0);
const letter = adjectives[randomLetterIndex % adjectives.length][0];
const availableAdjectives = alliterate(adjectives, letter);
const availableColors = alliterate(colors, letter);
const name = uniqueNamesGenerator({
dictionaries: [availableAdjectives, availableAdjectives, availableColors],
style: "capital",
separator: " ",
length: 3,
seed,
...config,
});
return name;
}

101
web/logger/node.js Normal file
View File

@ -0,0 +1,101 @@
/**
* Application logger.
*
* @import { LoggerOptions, Logger, Level, ChildLoggerOptions } from "pino"
* @import { PrettyOptions } from "pino-pretty"
*/
import { pino } from "pino";
//#region Constants
/**
* Default options for creating a Pino logger.
*
* @category Logger
* @satisfies {LoggerOptions<never, false>}
*/
export const DEFAULT_PINO_LOGGER_OPTIONS = {
enabled: true,
level: "info",
transport: {
target: "./transport.js",
options: /** @satisfies {PrettyOptions} */ ({
colorize: true,
}),
},
};
//#endregion
//#region Functions
/**
* Read the log level from the environment.
* @return {Level}
*/
export function readLogLevel() {
return process.env.AK_LOG_LEVEL || DEFAULT_PINO_LOGGER_OPTIONS.level;
}
/**
* @typedef {Logger} FixtureLogger
*/
/**
* @this {Logger}
* @param {string} fixtureName
* @param {string} [testName]
* @param {ChildLoggerOptions} [options]
* @returns {FixtureLogger}
*/
function createFixtureLogger(fixtureName, testName, options) {
return this.child(
{ name: fixtureName },
{
msgPrefix: `[${testName}] `,
...options,
},
);
}
/**
* @typedef {object} CustomLoggerMethods
* @property {typeof createFixtureLogger} fixture
*/
/**
* @typedef {Logger & CustomLoggerMethods} ConsoleLogger
*/
/**
* A singleton logger instance for Node.js.
*
* ```js
* import { ConsoleLogger } from "#logger/node";
*
* ConsoleLogger.info("Hello, world!");
* ```
*
* @runtime node
* @type {ConsoleLogger}
*/
export const ConsoleLogger = Object.assign(
pino({
...DEFAULT_PINO_LOGGER_OPTIONS,
level: readLogLevel(),
}),
{ fixture: createFixtureLogger },
);
/**
* @typedef {ReturnType<ConsoleLogger['child']>} ChildConsoleLogger
*/
//#region Aliases
export const info = ConsoleLogger.info.bind(ConsoleLogger);
export const debug = ConsoleLogger.debug.bind(ConsoleLogger);
export const warn = ConsoleLogger.warn.bind(ConsoleLogger);
export const error = ConsoleLogger.error.bind(ConsoleLogger);
//#endregion

21
web/logger/transport.js Normal file
View File

@ -0,0 +1,21 @@
/**
* @file Pretty transport for Pino
*
* @import { PrettyOptions } from "pino-pretty"
*/
import PinoPretty from "pino-pretty";
/**
* @param {PrettyOptions} options
*/
function prettyTransporter(options) {
const pretty = PinoPretty({
...options,
ignore: "pid,hostname",
translateTime: "SYS:HH:MM:ss",
});
return pretty;
}
export default prettyTransporter;

8555
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,8 +24,8 @@
"pseudolocalize": "node ./scripts/pseudolocalize.mjs", "pseudolocalize": "node ./scripts/pseudolocalize.mjs",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"storybook:build": "wireit", "storybook:build": "wireit",
"test": "wireit", "test": "vitest",
"test:e2e": "wireit", "test:e2e": "playwright test",
"test:e2e:watch": "wireit", "test:e2e:watch": "wireit",
"test:watch": "wireit", "test:watch": "wireit",
"tsc": "wireit", "tsc": "wireit",
@ -69,6 +69,9 @@
"#flow/*": "./src/flow/*.js", "#flow/*": "./src/flow/*.js",
"#locales/*": "./src/locales/*.js", "#locales/*": "./src/locales/*.js",
"#stories/*": "./src/stories/*.js", "#stories/*": "./src/stories/*.js",
"#tests/*": "./tests/*.js",
"#e2e": "./e2e/index.ts",
"#e2e/*": "./e2e/*.ts",
"#*/browser": { "#*/browser": {
"types": "./out/*/browser.d.ts", "types": "./out/*/browser.d.ts",
"import": "./*/browser.js" "import": "./*/browser.js"
@ -93,7 +96,7 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11", "@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@goauthentik/api": "^2025.6.2-1750112513", "@goauthentik/api": "^2025.6.1-1749515784",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",
@ -102,9 +105,10 @@
"@open-wc/lit-helpers": "^0.7.0", "@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^4.1.0", "@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^9.29.0", "@sentry/browser": "^9.28.0",
"@spotlightjs/spotlight": "^3.0.0", "@spotlightjs/spotlight": "^2.13.3",
"@webcomponents/webcomponentsjs": "^2.8.0", "@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"change-case": "^5.4.4", "change-case": "^5.4.4",
"chart.js": "^4.4.9", "chart.js": "^4.4.9",
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
@ -120,7 +124,9 @@
"hastscript": "^9.0.1", "hastscript": "^9.0.1",
"lit": "^3.2.0", "lit": "^3.2.0",
"md-front-matter": "^1.0.4", "md-front-matter": "^1.0.4",
"mermaid": "^11.6.0", "mermaid": "^11.4.1",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"rapidoc": "^9.3.8", "rapidoc": "^9.3.8",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@ -136,7 +142,6 @@
"trusted-types": "^2.0.0", "trusted-types": "^2.0.0",
"ts-pattern": "^5.7.1", "ts-pattern": "^5.7.1",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"webauthn-polyfills": "^0.1.7",
"webcomponent-qr-code": "^1.2.0", "webcomponent-qr-code": "^1.2.0",
"yaml": "^2.8.0" "yaml": "^2.8.0"
}, },
@ -149,6 +154,7 @@
"@goauthentik/tsconfig": "^1.0.4", "@goauthentik/tsconfig": "^1.0.4",
"@hcaptcha/types": "^1.0.4", "@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0", "@lit/localize-tools": "^0.8.0",
"@playwright/test": "^1.52.0",
"@storybook/addon-essentials": "^8.6.14", "@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-links": "^8.6.14", "@storybook/addon-links": "^8.6.14",
"@storybook/blocks": "^8.6.12", "@storybook/blocks": "^8.6.12",
@ -158,6 +164,7 @@
"@storybook/test": "^8.6.14", "@storybook/test": "^8.6.14",
"@storybook/web-components": "^8.6.14", "@storybook/web-components": "^8.6.14",
"@storybook/web-components-vite": "^8.6.14", "@storybook/web-components-vite": "^8.6.14",
"@testing-library/dom": "^10.4.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chart.js": "^2.9.41", "@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15", "@types/codemirror": "^5.60.15",
@ -170,16 +177,14 @@
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.5",
"@typescript-eslint/eslint-plugin": "^8.8.0", "@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0", "@typescript-eslint/parser": "^8.8.0",
"@wdio/browser-runner": "9.15", "@vitest/browser": "^3.2.0",
"@wdio/cli": "9.15", "@wdio/cli": "^9.15.0",
"@wdio/spec-reporter": "^9.15.0", "@wdio/spec-reporter": "^9.15.0",
"@web/test-runner": "^0.20.2", "esbuild": "^0.25.4",
"chromedriver": "^136.0.3",
"esbuild": "^0.25.5",
"esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.0", "esbuild-plugins-node-modules-polyfill": "^1.7.0",
"eslint": "^9.29.0", "eslint": "^9.28.0",
"eslint-plugin-lit": "^2.1.1", "eslint-plugin-lit": "^2.1.1",
"eslint-plugin-wc": "^3.0.1", "eslint-plugin-wc": "^3.0.1",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
@ -187,16 +192,22 @@
"knip": "^5.58.0", "knip": "^5.58.0",
"lit-analyzer": "^2.0.3", "lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"p-iteration": "^1.1.8",
"playwright": "^1.52.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"pseudolocale": "^2.1.0", "pseudolocale": "^2.1.0",
"rollup-plugin-postcss-lit": "^2.2.0", "rollup-plugin-postcss-lit": "^2.2.0",
"storybook": "^8.6.14", "storybook": "^8.6.14",
"storybook-addon-mock": "^5.0.0", "storybook-addon-mock": "^5.0.0",
"turnstile-types": "^1.2.3", "turnstile-types": "^1.2.3",
"type-fest": "^4.41.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.34.1", "typescript-eslint": "^8.34.0",
"vite-plugin-lit-css": "^2.0.0", "unique-names-generator": "^4.7.1",
"vite": "^6.3.5",
"vite-plugin-lit-css": "^2.1.0",
"vite-tsconfig-paths": "^5.0.1", "vite-tsconfig-paths": "^5.0.1",
"vitest": "^3.2.0",
"wireit": "^0.14.12" "wireit": "^0.14.12"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -278,7 +289,7 @@
"command": "lit-analyzer src" "command": "lit-analyzer src"
}, },
"lint:types:tests": { "lint:types:tests": {
"command": "tsc --noEmit -p ./tests" "command": "tsc --noEmit -p tsconfig.test.json"
}, },
"lint:types": { "lint:types": {
"command": "tsc -p .", "command": "tsc -p .",
@ -314,33 +325,33 @@
} }
}, },
"test": { "test": {
"command": "wdio ./wdio.conf.ts --logLevel=warn", "command": "wdio ./wdio.conf.js --logLevel=warn",
"env": { "env": {
"CI": "true", "CI": "true",
"TS_NODE_PROJECT": "tsconfig.test.json" "TS_NODE_PROJECT": "tsconfig.test.json"
} }
}, },
"test:e2e": { "test:e2e": {
"command": "wdio run ./tests/wdio.conf.ts", "command": "wdio run ./tests/wdio.conf.js",
"dependencies": [ "dependencies": [
"build" "build"
], ],
"env": { "env": {
"CI": "true", "CI": "true",
"TS_NODE_PROJECT": "./tests/tsconfig.test.json" "TS_NODE_PROJECT": "tsconfig.test.json"
} }
}, },
"test:e2e:watch": { "test:e2e:watch": {
"command": "wdio run ./tests/wdio.conf.ts", "command": "wdio run ./tests/wdio.conf.js",
"dependencies": [ "dependencies": [
"build" "build"
], ],
"env": { "env": {
"TS_NODE_PROJECT": "./tests/tsconfig.test.json" "TS_NODE_PROJECT": "tsconfig.test.json"
} }
}, },
"test:watch": { "test:watch": {
"command": "wdio run ./wdio.conf.ts", "command": "wdio run ./wdio.conf.js",
"dependencies": [ "dependencies": [
"build" "build"
], ],

View File

@ -0,0 +1,50 @@
/**
* @file Unique ID utilities.
*/
/**
* A global ID generator.
*
* @singleton
* @runtime common
*
* @category IDs
*/
export class IDGenerator {
static #sequenceIndex = 0;
static #elementIndex = 0;
/**
* Create a new ID for an HTML element.
*
* This ID will be unique for the lifetime of the page and will not be
* exposed on the `window` object.
*
* @param {string | number} [name] An optional name to use for the element.
*/
static elementID(name) {
name = name || ++this.#elementIndex;
return "«ak-" + name + "»";
}
/**
* Create a new ID.
*/
static next() {
this.#sequenceIndex += 1;
return this.#sequenceIndex;
}
/**
* Generate a random ID in hexadecimal format.
*
* @param {number} [characterLength]
*/
static randomID(characterLength = 6) {
const bytes = crypto.getRandomValues(new Uint8Array(characterLength / 2));
return Array.from(bytes, (a) => a.toString(16)).join("");
}
}

View File

@ -0,0 +1,27 @@
/**
* @file Helpers for running tests.
*/
/**
* A function that returns a promise.
* @template {never[]} [A=never[]]
* @typedef {(...args: A) => Promise<unknown>} Thenable
*/
/**
* A tuple of a function and its arguments.
* @template {Thenable} [T=Thenable]
* @typedef {[T, Parameters<T>]} SerializedThenable
*/
/**
* Executes a sequence of promise-returning functions in series
* @template {Thenable[]} T
* @param {{ [K in keyof T]: [T[K], ...Parameters<T[K]>] }} sequence
* @returns {Promise<void>}
*/
export async function series(...sequence) {
for (const [thenable, ...args] of sequence) {
await thenable(...args);
}
}

View File

@ -14,7 +14,7 @@ declare module "module" {
* const relativeDirname = dirname(fileURLToPath(import.meta.url)); * const relativeDirname = dirname(fileURLToPath(import.meta.url));
* ``` * ```
*/ */
// eslint-disable-next-line no-var
var __dirname: string; var __dirname: string;
} }
} }

92
web/playwright.config.js Normal file
View File

@ -0,0 +1,92 @@
/**
* @file Playwright configuration.
*
* @see https://playwright.dev/docs/test-configuration
*
* @import { LogFn, Logger } from "pino"
*/
import { ConsoleLogger } from "#logger/node";
import { defineConfig, devices } from "@playwright/test";
const CI = !!process.env.CI;
/**
* @type {Map<string, Logger>}
*/
const LoggerCache = new Map();
const baseURL = process.env.AK_TEST_RUNNER_PAGE_URL ?? "http://localhost:9000";
export default defineConfig({
testDir: "./test/browser",
fullyParallel: true,
forbidOnly: CI,
retries: CI ? 2 : 0,
workers: CI ? 1 : undefined,
reporter: CI
? "github"
: [
// ---
["list", { printSteps: true }],
["html", { open: "never" }],
],
use: {
testIdAttribute: "data-test-id",
baseURL,
trace: "on-first-retry",
launchOptions: {
logger: {
isEnabled() {
return true;
},
log: (name, severity, message, args) => {
let logger = LoggerCache.get(name);
if (!logger) {
logger = ConsoleLogger.child({
name: `Playwright ${name.toUpperCase()}`,
});
LoggerCache.set(name, logger);
}
/**
* @type {LogFn}
*/
let log;
switch (severity) {
case "verbose":
log = logger.debug;
break;
case "warning":
log = logger.warn;
break;
case "error":
log = logger.error;
break;
default:
log = logger.info;
break;
}
if (name === "api") {
log = logger.debug;
}
log.call(logger, message.toString(), args);
},
},
},
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
],
});

View File

@ -6,8 +6,9 @@ import "@goauthentik/elements/EmptyState";
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton"; import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { until } from "lit/directives/until.js"; import { until } from "lit/directives/until.js";
import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css"; import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css";
@ -16,16 +17,15 @@ import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthent
@customElement("ak-about-modal") @customElement("ak-about-modal")
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) { export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
static get styles() { static styles: CSSResult[] = [
return ModalButton.styles.concat( ...ModalButton.styles,
PFAbout, PFAbout,
css` css`
.pf-c-about-modal-box__hero { .pf-c-about-modal-box__hero {
background-image: url("/static/dist/assets/images/flow_background.jpg"); background-image: url("/static/dist/assets/images/flow_background.jpg");
} }
`, `,
); ];
}
async getAboutEntries(): Promise<[string, string | TemplateResult][]> { async getAboutEntries(): Promise<[string, string | TemplateResult][]> {
const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve(); const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
@ -55,21 +55,32 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
]; ];
} }
renderModal() { #contentRef = createRef<HTMLDivElement>();
#backdropListener = (event: PointerEvent) => {
// We only want to close the modal when the backdrop is clicked, not when it's children are clicked.
if (this.#contentRef.value?.contains(event.target as Node)) {
return;
}
this.close();
};
protected override renderModal() {
let product = this.brandingTitle; let product = this.brandingTitle;
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) { if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`; product += ` ${msg("Enterprise")}`;
} }
return html`<div return html`<div class="pf-c-backdrop" @click=${this.#backdropListener}>
class="pf-c-backdrop"
@click=${(e: PointerEvent) => {
e.stopPropagation();
this.closeModal();
}}
>
<div class="pf-l-bullseye"> <div class="pf-l-bullseye">
<div class="pf-c-about-modal-box" role="dialog" aria-modal="true"> <div
${ref(this.#contentRef)}
class="pf-c-about-modal-box"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div class="pf-c-about-modal-box__brand"> <div class="pf-c-about-modal-box__brand">
<img <img
class="pf-c-about-modal-box__brand-image" class="pf-c-about-modal-box__brand-image"
@ -78,18 +89,12 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
/> />
</div> </div>
<div class="pf-c-about-modal-box__close"> <div class="pf-c-about-modal-box__close">
<button <button class="pf-c-button pf-m-plain" type="button" @click=${this.close}>
class="pf-c-button pf-m-plain"
type="button"
@click=${() => {
this.open = false;
}}
>
<i class="fas fa-times" aria-hidden="true"></i> <i class="fas fa-times" aria-hidden="true"></i>
</button> </button>
</div> </div>
<div class="pf-c-about-modal-box__header"> <div class="pf-c-about-modal-box__header">
<h1 class="pf-c-title pf-m-4xl">${product}</h1> <h1 class="pf-c-title pf-m-4xl" id="modal-title">${product}</h1>
</div> </div>
<div class="pf-c-about-modal-box__hero"></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__content">

View File

@ -1,16 +1,44 @@
import { SidebarItemProperties } from "#elements/sidebar/SidebarItem";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { spread } from "@open-wc/lit-helpers"; import { spread } from "@open-wc/lit-helpers";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit"; import { TemplateResult, html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { repeat } from "lit/directives/repeat.js"; import { repeat } from "lit/directives/repeat.js";
/**
* Given a record-like object, prefixes each key with a dot, allowing it to be spread into a
* template literal.
*
* ```ts
* interface MyElementProperties {
* foo: string;
* bar: number;
* }
*
* const properties {} as LitPropertyRecord<MyElementProperties>
*
* console.log(properties) // { '.foo': string; '.bar': number }
* ```
*/
export type LitPropertyRecord<T extends object> = {
[K in keyof T as K extends string ? LitPropertyKey<K> : never]: T[K];
};
/**
* A type that represents a property key that can be used in a LitPropertyRecord.
*
* @see {@linkcode LitPropertyRecord}
*/
export type LitPropertyKey<K> = K extends string ? `.${K}` | `?${K}` | K : K;
// The second attribute type is of string[] to help with the 'activeWhen' control, which was // The second attribute type is of string[] to help with the 'activeWhen' control, which was
// commonplace and singular enough to merit its own handler. // commonplace and singular enough to merit its own handler.
type SidebarEntry = [ type SidebarEntry = [
path: string | null, path: string | null,
label: string, label: string,
attributes?: Record<string, any> | string[] | null, // eslint-disable-line attributes?: LitPropertyRecord<SidebarItemProperties> | string[] | null,
children?: SidebarEntry[], children?: SidebarEntry[],
]; ];
@ -31,8 +59,7 @@ export function renderSidebarItem([
properties.path = path; properties.path = path;
} }
return html`<ak-sidebar-item ${spread(properties)}> return html`<ak-sidebar-item label=${ifDefined(label)} ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
${children ? renderSidebarItems(children) : nothing} ${children ? renderSidebarItems(children) : nothing}
</ak-sidebar-item>`; </ak-sidebar-item>`;
} }

View File

@ -7,6 +7,7 @@ import { me } from "#common/users";
import { WebsocketClient } from "#common/ws"; import { WebsocketClient } from "#common/ws";
import { SidebarToggleEventDetail } from "#components/ak-page-header"; import { SidebarToggleEventDetail } from "#components/ak-page-header";
import { AuthenticatedInterface } from "#elements/AuthenticatedInterface"; import { AuthenticatedInterface } from "#elements/AuthenticatedInterface";
import "#elements/a11y/ak-skip-to-content";
import "#elements/ak-locale-context/ak-locale-context"; import "#elements/ak-locale-context/ak-locale-context";
import "#elements/banner/EnterpriseStatusBanner"; import "#elements/banner/EnterpriseStatusBanner";
import "#elements/banner/EnterpriseStatusBanner"; import "#elements/banner/EnterpriseStatusBanner";
@ -22,6 +23,7 @@ import "#elements/router/RouterOutlet";
import "#elements/sidebar/Sidebar"; import "#elements/sidebar/Sidebar";
import "#elements/sidebar/SidebarItem"; import "#elements/sidebar/SidebarItem";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, eventOptions, property, query } from "lit/decorators.js"; import { customElement, eventOptions, property, query } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
@ -162,16 +164,18 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
this.user = await me(); me().then((session) => {
this.user = session;
const canAccessAdmin = const canAccessAdmin =
this.user.user.isSuperuser || this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema // TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface"); this.user.user.systemPermissions.includes("access_admin_interface");
if (!canAccessAdmin && this.user.user.pk > 0) { if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user/"); window.location.assign("/if/user/");
} }
});
} }
render(): TemplateResult { render(): TemplateResult {
@ -190,13 +194,14 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
}; };
return html` <ak-locale-context> return html` <ak-locale-context>
<ak-skip-to-content></ak-skip-to-content>
<div class="pf-c-page"> <div class="pf-c-page">
<ak-page-navbar ?open=${this.sidebarOpen} @sidebar-toggle=${this.sidebarListener}> <ak-page-navbar ?open=${this.sidebarOpen} @sidebar-toggle=${this.sidebarListener}>
<ak-version-banner></ak-version-banner> <ak-version-banner></ak-version-banner>
<ak-enterprise-status interface="admin"></ak-enterprise-status> <ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar> </ak-page-navbar>
<ak-sidebar class="${classMap(sidebarClasses)}"> <ak-sidebar ?hidden=${!this.sidebarOpen} class="${classMap(sidebarClasses)}">
${renderSidebarItems(AdminSidebarEntries)} ${renderSidebarItems(AdminSidebarEntries)}
${this.can(CapabilitiesEnum.IsEnterprise) ${this.can(CapabilitiesEnum.IsEnterprise)
? renderSidebarItems(AdminSidebarEnterpriseEntries) ? renderSidebarItems(AdminSidebarEnterpriseEntries)
@ -208,9 +213,10 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
<div class="pf-c-drawer__main"> <div class="pf-c-drawer__main">
<div class="pf-c-drawer__content"> <div class="pf-c-drawer__content">
<div class="pf-c-drawer__body"> <div class="pf-c-drawer__body">
<main class="pf-c-page__main"> <div class="pf-c-page__main">
<ak-router-outlet <ak-router-outlet
role="main" role="main"
aria-label="${msg("Main content")}"
class="pf-c-page__main" class="pf-c-page__main"
tabindex="-1" tabindex="-1"
id="main-content" id="main-content"
@ -218,7 +224,7 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
.routes=${ROUTES} .routes=${ROUTES}
> >
</ak-router-outlet> </ak-router-outlet>
</main> </div>
</div> </div>
</div> </div>
<ak-notification-drawer <ak-notification-drawer

View File

@ -1,3 +1,4 @@
import { SlottedTemplateResult } from "#elements/types";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { PFSize } from "@goauthentik/common/enums.js"; import { PFSize } from "@goauthentik/common/enums.js";
import { import {
@ -13,7 +14,7 @@ import { state } from "lit/decorators.js";
export interface AdminStatus { export interface AdminStatus {
icon: string; icon: string;
message?: TemplateResult; message?: SlottedTemplateResult;
} }
/** /**
@ -98,8 +99,8 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
* *
* @returns TemplateResult displaying the value * @returns TemplateResult displaying the value
*/ */
protected renderValue(): TemplateResult { protected renderValue(): SlottedTemplateResult {
return html`${this.value}`; return this.value ? html`${this.value}` : nothing;
} }
/** /**
@ -108,7 +109,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
* @param status - AdminStatus object containing icon and message * @param status - AdminStatus object containing icon and message
* @returns TemplateResult for status display * @returns TemplateResult for status display
*/ */
private renderStatus(status: AdminStatus): TemplateResult { private renderStatus(status: AdminStatus): SlottedTemplateResult {
return html` return html`
<p><i class="${status.icon}"></i>&nbsp;${this.renderValue()}</p> <p><i class="${status.icon}"></i>&nbsp;${this.renderValue()}</p>
${status.message ? html`<p class="subtext">${status.message}</p>` : nothing} ${status.message ? html`<p class="subtext">${status.message}</p>` : nothing}
@ -121,9 +122,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
* @param error - Error message to display * @param error - Error message to display
* @returns TemplateResult for error display * @returns TemplateResult for error display
*/ */
private renderError(error: string): TemplateResult { private renderError(error: string): SlottedTemplateResult {
return html` return html`
<p><i class="fa fa-times"></i>&nbsp;${msg("Failed to fetch")}</p> <p><i aria-hidden="true" class="fa fa-times"></i>&nbsp;${msg("Failed to fetch")}</p>
<p class="subtext">${error}</p> <p class="subtext">${error}</p>
`; `;
} }
@ -133,7 +134,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
* *
* @returns TemplateResult for loading spinner * @returns TemplateResult for loading spinner
*/ */
private renderLoading(): TemplateResult { private renderLoading(): SlottedTemplateResult {
return html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`; return html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`;
} }
@ -142,7 +143,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
* *
* @returns TemplateResult for current component state * @returns TemplateResult for current component state
*/ */
renderInner(): TemplateResult { renderInner(): SlottedTemplateResult {
return html` return html`
<p class="center-value"> <p class="center-value">
${ ${

View File

@ -1,3 +1,4 @@
import { SlottedTemplateResult } from "#elements/types";
import { import {
AdminStatus, AdminStatus,
AdminStatusCard, AdminStatusCard,
@ -5,7 +6,7 @@ import {
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { AdminApi, OutpostsApi, SystemInfo } from "@goauthentik/api"; import { AdminApi, OutpostsApi, SystemInfo } from "@goauthentik/api";
@ -84,12 +85,12 @@ export class SystemStatusCard extends AdminStatusCard<SystemInfo> {
}); });
} }
renderHeader(): TemplateResult { renderHeader(): SlottedTemplateResult {
return html`${msg("System status")}`; return msg("System status");
} }
renderValue(): TemplateResult { renderValue(): SlottedTemplateResult {
return html`${this.statusSummary}`; return this.statusSummary ? html`${this.statusSummary}` : nothing;
} }
} }

View File

@ -1,4 +1,7 @@
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js"; import {
AkControlElement,
formatFormElementAsJSON,
} from "@goauthentik/elements/AkControlElement.js";
import { type Spread } from "@goauthentik/elements/types"; import { type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers"; import { spread } from "@open-wc/lit-helpers";
@ -23,33 +26,32 @@ const hasLegalScheme = (url: string) =>
@customElement("ak-admin-settings-footer-link") @customElement("ak-admin-settings-footer-link")
export class FooterLinkInput extends AkControlElement<FooterLink> { export class FooterLinkInput extends AkControlElement<FooterLink> {
static get styles() { static styles = [
return [ PFBase,
PFBase, PFInputGroup,
PFInputGroup, PFFormControl,
PFFormControl, css`
css` .pf-c-input-group input#linkname {
.pf-c-input-group input#linkname { flex-grow: 1;
flex-grow: 1; width: 8rem;
width: 8rem; }
} `,
`, ];
];
}
@property({ type: Object, attribute: false }) @property({ type: Object, attribute: false })
footerLink: FooterLink = { public footerLink: FooterLink = {
name: "", name: "",
href: "", href: "",
}; };
@queryAll(".ak-form-control") @property({ type: String })
controls?: HTMLInputElement[]; public name?: string | null;
json() { @queryAll(".ak-form-control")
return Object.fromEntries( protected controls?: HTMLInputElement[];
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
) as unknown as FooterLink; public override json() {
return formatFormElementAsJSON<FooterLink>(this.controls);
} }
get isValid() { get isValid() {

View File

@ -1,42 +1,49 @@
import { render } from "@goauthentik/elements/tests/utils.js"; import { render } from "@goauthentik/elements/tests/utils.js";
import { $, expect } from "@wdio/globals"; import { $, browser, expect } from "@wdio/globals";
import { html } from "lit"; import { html } from "lit";
import "../AdminSettingsFooterLinks.js"; import "../AdminSettingsFooterLinks.js";
describe("ak-admin-settings-footer-link", () => { describe("ak-admin-settings-footer-link", () => {
afterEach(async () => { afterEach(() =>
await browser.execute(async () => { browser.execute(() => {
await document.body.querySelector("ak-admin-settings-footer-link")?.remove(); document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit if ("_$litPart$" in document.body) {
await delete document.body._$litPart$; delete document.body._$litPart$;
} }
}); }),
}); );
it("should render an empty control", async () => { it("should render an empty control", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`); render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link"); const link = $("ak-admin-settings-footer-link");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" }); await expect(link.getProperty("isValid")).resolves.toStrictEqual(false);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "",
href: "",
});
}); });
it("should not be valid if just a name is filled in", async () => { it("should not be valid if just a name is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`); render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link"); const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo"); await link.$('input[name="name"]').setValue("foo");
await expect(await link.getProperty("isValid")).toStrictEqual(false); await expect(link.getProperty("isValid")).resolves.toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" }); await expect(link.getProperty("toJson")).resolves.toEqual({
name: "foo",
href: "",
});
}); });
it("should be valid if just a URL is filled in", async () => { it("should be valid if just a URL is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`); render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link"); const link = $("ak-admin-settings-footer-link");
await link.$('input[name="href"]').setValue("https://foo.com"); await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true); await expect(link.getProperty("isValid")).resolves.toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({ await expect(link.getProperty("toJson")).resolves.toEqual({
name: "", name: "",
href: "https://foo.com", href: "https://foo.com",
}); });
@ -44,11 +51,13 @@ describe("ak-admin-settings-footer-link", () => {
it("should be valid if both are filled in", async () => { it("should be valid if both are filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`); render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link"); const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo"); await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("https://foo.com"); await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({ await expect(link.getProperty("isValid")).resolves.toStrictEqual(true);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "foo", name: "foo",
href: "https://foo.com", href: "https://foo.com",
}); });
@ -56,13 +65,13 @@ describe("ak-admin-settings-footer-link", () => {
it("should not be valid if the URL is not valid", async () => { it("should not be valid if the URL is not valid", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`); render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link"); const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo"); await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("never://foo.com"); await link.$('input[name="href"]').setValue("never://foo.com");
await expect(await link.getProperty("toJson")).toEqual({ await expect(link.getProperty("toJson")).resolves.toEqual({
name: "foo", name: "foo",
href: "never://foo.com", href: "never://foo.com",
}); });
await expect(await link.getProperty("isValid")).toStrictEqual(false); await expect(link.getProperty("isValid")).resolves.toStrictEqual(false);
}); });
}); });

View File

@ -177,9 +177,8 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
.options=${policyEngineModes} .options=${policyEngineModes}
.value=${this.instance?.policyEngineMode} .value=${this.instance?.policyEngineMode}
></ak-radio-input> ></ak-radio-input>
<ak-form-group> <ak-form-group label="${msg("UI settings")}">
<span slot="header"> ${msg("UI settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="metaLaunchUrl" name="metaLaunchUrl"
label=${msg("Launch URL")} label=${msg("Launch URL")}

View File

@ -85,7 +85,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
]; ];
} }
renderSidebarAfter(): TemplateResult { protected renderSidebarAfter(): TemplateResult {
return html`<div class="pf-c-sidebar__panel pf-m-width-25"> return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__body"> <div class="pf-c-card__body">

View File

@ -5,7 +5,8 @@ import {
WizardNavigationEvent, WizardNavigationEvent,
WizardUpdateEvent, WizardUpdateEvent,
} from "@goauthentik/components/ak-wizard/events"; } from "@goauthentik/components/ak-wizard/events";
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form"; import type { AkControlElement } from "@goauthentik/elements/forms/Form";
import { serializeForm } from "@goauthentik/elements/forms/Form";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement"; import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -29,22 +30,21 @@ export class ApplicationWizardStep extends WizardStep {
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override // As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
// these fields and provide them to all the child classes. // these fields and provide them to all the child classes.
wizardTitle = msg("New application"); protected wizardTitle = msg("New application");
wizardDescription = msg("Create a new application and configure a provider for it."); protected wizardDescription = msg("Create a new application and configure a provider for it.");
canCancel = true; public cancelable = true;
// This should be overridden in the children for more precise targeting. // This should be overridden in the children for more precise targeting.
@query("form") @query("form")
form!: HTMLFormElement; protected form!: HTMLFormElement;
get formValues(): KeyUnknown | undefined { get formValues(): Record<string, unknown> {
const elements = [ const elements = [
...Array.from( ...this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"), ...this.form.querySelectorAll<AkControlElement>("[data-ak-control]"),
),
...Array.from(this.form.querySelectorAll<HTMLElement>("[data-ak-control=true]")),
]; ];
return serializeForm(elements as unknown as NodeListOf<HorizontalFormElement>);
return serializeForm(elements);
} }
protected removeErrors( protected removeErrors(

View File

@ -22,7 +22,7 @@ export class AkWizardTitle extends AKElement {
render() { render() {
return html`<div class="ak-bottom-spacing pf-c-content"> return html`<div class="ak-bottom-spacing pf-c-content">
<h3><slot></slot></h3> <h3 data-test-id="wizard-heading"><slot></slot></h3>
</div>`; </div>`;
} }
} }
@ -33,4 +33,12 @@ declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ak-wizard-title": AkWizardTitle; "ak-wizard-title": AkWizardTitle;
} }
interface WizardTestIDMap {
heading: HTMLHeadingElement;
}
interface TestIDSelectorMap {
wizard: WizardTestIDMap;
}
} }

View File

@ -1,16 +1,15 @@
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js"; import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js"; import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { policyEngineModes } from "@goauthentik/admin/policies/PolicyEngineModes"; import { policyEngineModes } from "@goauthentik/admin/policies/PolicyEngineModes";
import { camelToSnake } from "@goauthentik/common/utils.js";
import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-slug-input"; import "@goauthentik/components/ak-slug-input";
import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types"; import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { isSlug } from "@goauthentik/elements/router/utils.js"; import { isSlug } from "@goauthentik/elements/router/utils.js";
import { snakeCase } from "change-case";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { html } from "lit"; import { html } from "lit";
@ -23,11 +22,10 @@ import { ApplicationWizardStateUpdate, ValidationRecord } from "../types";
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v); const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
const trimMany = (o: KeyUnknown, vs: string[]) => const trimMany = (o: Record<string, unknown>, vs: string[]) =>
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])])); Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
// eslint-disable-next-line @typescript-eslint/no-explicit-any const isStr = (v: unknown): v is string => typeof v === "string";
const isStr = (v: any): v is string => typeof v === "string";
@customElement("ak-application-wizard-application-step") @customElement("ak-application-wizard-application-step")
export class ApplicationWizardApplicationStep extends ApplicationWizardStep { export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
@ -48,9 +46,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
errorMessages(name: string) { errorMessages(name: string) {
return this.errors.has(name) return this.errors.has(name)
? [this.errors.get(name)] ? [this.errors.get(name)]
: (this.wizard.errors?.app?.[name] ?? : (this.wizard.errors?.app?.[name] ?? this.wizard.errors?.app?.[snakeCase(name)] ?? []);
this.wizard.errors?.app?.[camelToSnake(name)] ??
[]);
} }
get buttons(): WizardButton[] { get buttons(): WizardButton[] {
@ -146,9 +142,8 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
.value=${app.policyEngineMode} .value=${app.policyEngineMode}
.errorMessages=${errors.policyEngineMode ?? []} .errorMessages=${errors.policyEngineMode ?? []}
></ak-radio-input> ></ak-radio-input>
<ak-form-group aria-label=${msg("UI Settings")}> <ak-form-group label=${msg("UI Settings")}>
<span slot="header"> ${msg("UI Settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="metaLaunchUrl" name="metaLaunchUrl"
label=${msg("Launch URL")} label=${msg("Launch URL")}

View File

@ -1,13 +1,14 @@
import { camelToSnake } from "@goauthentik/common/utils.js";
import "@goauthentik/components/ak-number-input"; import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
import { AKElement } from "@goauthentik/elements/Base.js"; import { AKElement } from "@goauthentik/elements/Base.js";
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form"; import type { AkControlElement } from "@goauthentik/elements/forms/Form";
import { serializeForm } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement"; import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { snakeCase } from "change-case";
import { property, query } from "lit/decorators.js"; import { property, query } from "lit/decorators.js";
@ -30,14 +31,13 @@ export class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKEl
@query("form#providerform") @query("form#providerform")
form!: HTMLFormElement; form!: HTMLFormElement;
get formValues(): KeyUnknown | undefined { get formValues(): Record<string, unknown> {
const elements = [ const elements = [
...Array.from( ...this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"), ...this.form.querySelectorAll<AkControlElement>("[data-ak-control]"),
),
...Array.from(this.form.querySelectorAll<HTMLElement>("[data-ak-control=true]")),
]; ];
return serializeForm(elements as unknown as NodeListOf<HorizontalFormElement>);
return serializeForm(elements);
} }
get valid() { get valid() {
@ -49,7 +49,7 @@ export class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKEl
return name in this.errors return name in this.errors
? [this.errors[name]] ? [this.errors[name]]
: (this.wizard.errors?.provider?.[name] ?? : (this.wizard.errors?.provider?.[name] ??
this.wizard.errors?.provider?.[camelToSnake(name)] ?? this.wizard.errors?.provider?.[snakeCase(name)] ??
[]); []);
} }

View File

@ -60,9 +60,8 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF
input-hint="code" input-hint="code"
></ak-text-input> ></ak-text-input>
<ak-form-group expanded> <ak-form-group open label=" ${msg("Protocol settings")} ">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Property mappings")} label=${msg("Property mappings")}
name="propertyMappings" name="propertyMappings"

View File

@ -176,9 +176,8 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
</div> </div>
</div> </div>
<ak-form-group> <ak-form-group label="${msg("Additional settings")}">
<span slot="header">${msg("Additional settings")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Context")} name="context"> <ak-form-element-horizontal label=${msg("Context")} name="context">
<ak-codemirror <ak-codemirror
mode=${CodeMirrorMode.YAML} mode=${CodeMirrorMode.YAML}

View File

@ -87,9 +87,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group> <ak-form-group label="${msg("Branding settings")} ">
<span slot="header"> ${msg("Branding settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Title")} required name="brandingTitle"> <ak-form-element-horizontal label=${msg("Title")} required name="brandingTitle">
<input <input
type="text" type="text"
@ -170,9 +169,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("External user settings")} ">
<span slot="header"> ${msg("External user settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Default application")} label=${msg("Default application")}
name="defaultApplication" name="defaultApplication"
@ -215,9 +213,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Default flows")} ">
<span slot="header"> ${msg("Default flows")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Authentication flow")} label=${msg("Authentication flow")}
name="flowAuthentication" name="flowAuthentication"
@ -295,9 +292,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Other global settings")} ">
<span slot="header"> ${msg("Other global settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Web Certificate")} label=${msg("Web Certificate")}
name="webCertificate" name="webCertificate"

View File

@ -44,19 +44,18 @@ export class CoreGroupSearch extends CustomListenerElement(AKElement) {
* @attr * @attr
*/ */
@property({ type: String, reflect: true }) @property({ type: String, reflect: true })
group?: string; public group?: string;
@query("ak-search-select") @query("ak-search-select")
search!: SearchSelect<Group>; public search!: SearchSelect<Group>;
@property({ type: String }) @property({ type: String })
name: string | null | undefined; public name?: string | null;
selectedGroup?: Group; selectedGroup?: Group;
constructor() { constructor() {
super(); super();
this.selected = this.selected.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this); this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
} }
@ -83,9 +82,9 @@ export class CoreGroupSearch extends CustomListenerElement(AKElement) {
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true })); this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
} }
selected(group: Group) { selected = (group: Group) => {
return this.group === group.pk; return this.group === group.pk;
} };
render() { render() {
return html` return html`

View File

@ -32,13 +32,19 @@ const renderValue = (item: CertificateKeyPair | undefined): string | undefined =
@customElement("ak-crypto-certificate-search") @customElement("ak-crypto-certificate-search")
export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement) { export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement) {
@property({ type: String, reflect: true }) @property({ type: String, reflect: true })
certificate?: string; public certificate?: string;
@query("ak-search-select") @query("ak-search-select")
search!: SearchSelect<CertificateKeyPair>; public search!: SearchSelect<CertificateKeyPair>;
@property({ type: String }) @property({ type: String })
name: string | null | undefined; public name?: string | null;
@property({ type: String })
public label?: string | undefined;
@property({ type: String })
public placeholder?: string | undefined;
/** /**
* Set to `true` to allow certificates without private key to show up. When set to `false`, * Set to `true` to allow certificates without private key to show up. When set to `false`,
@ -46,7 +52,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
* @attr * @attr
*/ */
@property({ type: Boolean, attribute: "nokey" }) @property({ type: Boolean, attribute: "nokey" })
noKey = false; public noKey = false;
/** /**
* Set this to true if, should there be only one certificate available, you want the system to * Set this to true if, should there be only one certificate available, you want the system to
@ -55,16 +61,12 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
* @attr * @attr
*/ */
@property({ type: Boolean, attribute: "singleton" }) @property({ type: Boolean, attribute: "singleton" })
singleton = false; public singleton = false;
selectedKeypair?: CertificateKeyPair; /**
* @todo Document this.
constructor() { */
super(); public selectedKeypair?: CertificateKeyPair;
this.selected = this.selected.bind(this);
this.fetchObjects = this.fetchObjects.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
}
get value() { get value() {
return this.selectedKeypair ? renderValue(this.selectedKeypair) : null; return this.selectedKeypair ? renderValue(this.selectedKeypair) : null;
@ -83,13 +85,13 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
} }
} }
handleSearchUpdate(ev: CustomEvent) { handleSearchUpdate = (ev: CustomEvent) => {
ev.stopPropagation(); ev.stopPropagation();
this.selectedKeypair = ev.detail.value; this.selectedKeypair = ev.detail.value;
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true })); this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
} };
async fetchObjects(query?: string): Promise<CertificateKeyPair[]> { fetchObjects = async (query?: string): Promise<CertificateKeyPair[]> => {
const args: CryptoCertificatekeypairsListRequest = { const args: CryptoCertificatekeypairsListRequest = {
ordering: "name", ordering: "name",
hasKey: !this.noKey, hasKey: !this.noKey,
@ -102,19 +104,21 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
args, args,
); );
return certificates.results; return certificates.results;
} };
selected(item: CertificateKeyPair, items: CertificateKeyPair[]) { selected = (item: CertificateKeyPair, items: CertificateKeyPair[]) => {
return ( return (
(this.singleton && !this.certificate && items.length === 1) || (this.singleton && !this.certificate && items.length === 1) ||
(!!this.certificate && this.certificate === item.pk) (!!this.certificate && this.certificate === item.pk)
); );
} };
render() { render() {
return html` return html`
<ak-search-select <ak-search-select
name=${ifDefined(this.name ?? undefined)} name=${ifDefined(this.name ?? undefined)}
label=${ifDefined(this.label ?? undefined)}
placeholder=${ifDefined(this.placeholder ?? undefined)}
.fetchObjects=${this.fetchObjects} .fetchObjects=${this.fetchObjects}
.renderElement=${renderElement} .renderElement=${renderElement}
.value=${renderValue} .value=${renderValue}

View File

@ -5,6 +5,7 @@ import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize";
import { html } from "lit"; import { html } from "lit";
import { property, query } from "lit/decorators.js"; import { property, query } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
@ -34,13 +35,15 @@ export function getFlowValue(flow: Flow | undefined): string | undefined {
*/ */
export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement) { export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement) {
//#region Properties
/** /**
* The type of flow we're looking for. * The type of flow we're looking for.
* *
* @attr * @attr
*/ */
@property({ type: String }) @property({ type: String })
flowType?: FlowsInstancesListDesignationEnum; public flowType?: FlowsInstancesListDesignationEnum;
/** /**
* The id of the current flow, if any. For stages where the flow is already defined. * The id of the current flow, if any. For stages where the flow is already defined.
@ -48,7 +51,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
* @attr * @attr
*/ */
@property({ type: String }) @property({ type: String })
currentFlow?: string | undefined; public currentFlow?: string | undefined;
/** /**
* If true, it is not valid to leave the flow blank. * If true, it is not valid to leave the flow blank.
@ -56,10 +59,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
* @attr * @attr
*/ */
@property({ type: Boolean }) @property({ type: Boolean })
required?: boolean = false; public required?: boolean = false;
@query("ak-search-select")
search!: SearchSelect<T>;
/** /**
* When specified and the object instance does not have a flow selected, auto-select the flow with the given slug. * When specified and the object instance does not have a flow selected, auto-select the flow with the given slug.
@ -70,9 +70,29 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
defaultFlowSlug?: string; defaultFlowSlug?: string;
@property({ type: String }) @property({ type: String })
name: string | null | undefined; public name?: string | null;
selectedFlow?: T; /**
* The label of the input, for forms.
*
* @attr
*/
@property({ type: String })
public label?: string;
/**
* The textual placeholder for the search's <input> object, if currently empty. Used as the
* native <input> object's `placeholder` field.
*
* @attr
*/
@property({ type: String })
public placeholder: string = msg("Select a flow...");
@query("ak-search-select")
protected search!: SearchSelect<T>;
protected selectedFlow?: T;
get value() { get value() {
return this.selectedFlow ? getFlowValue(this.selectedFlow) : null; return this.selectedFlow ? getFlowValue(this.selectedFlow) : null;
@ -80,18 +100,16 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
constructor() { constructor() {
super(); super();
this.fetchObjects = this.fetchObjects.bind(this);
this.selected = this.selected.bind(this); this.selected = this.selected.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
} }
handleSearchUpdate(ev: CustomEvent) { handleSearchUpdate = (ev: CustomEvent) => {
ev.stopPropagation(); ev.stopPropagation();
this.selectedFlow = ev.detail.value; this.selectedFlow = ev.detail.value;
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true })); this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
} };
async fetchObjects(query?: string): Promise<Flow[]> { fetchObjects = async (query?: string): Promise<Flow[]> => {
const args: FlowsInstancesListRequest = { const args: FlowsInstancesListRequest = {
ordering: "slug", ordering: "slug",
designation: this.flowType, designation: this.flowType,
@ -99,7 +117,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
}; };
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args); const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
return flows.results; return flows.results;
} };
/* This is the most commonly overridden method of this class. About half of the Flow Searches /* This is the most commonly overridden method of this class. About half of the Flow Searches
* use this method, but several have more complex needs, such as relating to the brand, or just * use this method, but several have more complex needs, such as relating to the brand, or just
@ -134,6 +152,8 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
.renderElement=${renderElement} .renderElement=${renderElement}
.renderDescription=${renderDescription} .renderDescription=${renderDescription}
.value=${getFlowValue} .value=${getFlowValue}
placeholder=${ifDefined(this.placeholder ?? undefined)}
label=${ifDefined(this.label ?? undefined)}
name=${ifDefined(this.name ?? undefined)} name=${ifDefined(this.name ?? undefined)}
@ak-change=${this.handleSearchUpdate} @ak-change=${this.handleSearchUpdate}
?blankable=${!this.required} ?blankable=${!this.required}

View File

@ -21,14 +21,9 @@ export class AkBrandedFlowSearch<T extends Flow> extends FlowSearch<T> {
@property({ attribute: false, type: String }) @property({ attribute: false, type: String })
brandFlow?: string; brandFlow?: string;
constructor() { public selected = (flow: Flow): boolean => {
super();
this.selected = this.selected.bind(this);
}
selected(flow: Flow): boolean {
return super.selected(flow) || flow.pk === this.brandFlow; return super.selected(flow) || flow.pk === this.brandFlow;
} };
} }
declare global { declare global {

View File

@ -32,19 +32,14 @@ export class AkSourceFlowSearch<T extends Flow> extends FlowSearch<T> {
@property({ type: String }) @property({ type: String })
instanceId: string | undefined; instanceId: string | undefined;
constructor() {
super();
this.selected = this.selected.bind(this);
}
// If there's no instance or no currentFlowId for it and the flow resembles the fallback, // If there's no instance or no currentFlowId for it and the flow resembles the fallback,
// otherwise defer to the parent class. // otherwise defer to the parent class.
selected(flow: Flow): boolean { selected = (flow: Flow): boolean => {
return ( return (
(!this.instanceId && !this.currentFlow && flow.slug === this.fallback) || (!this.instanceId && !this.currentFlow && flow.slug === this.fallback) ||
super.selected(flow) super.selected(flow)
); );
} };
} }
declare global { declare global {

View File

@ -8,25 +8,35 @@ import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
@customElement("ak-license-notice") @customElement("ak-license-notice")
export class AkLicenceNotice extends WithLicenseSummary(AKElement) { export class AKLicenceNotice extends WithLicenseSummary(AKElement) {
static styles = [$PFBase]; static styles = [$PFBase];
@property() @property()
notice = msg("Enterprise only"); public label = msg("Enterprise only");
@property()
public description = msg("Learn more about the enterprise license.");
render() { render() {
return this.hasEnterpriseLicense if (this.hasEnterpriseLicense) {
? nothing return nothing;
: html` }
<ak-alert class="pf-c-radio__description" inline plain>
<a href="#/enterprise/licenses">${this.notice}</a> return html`
</ak-alert> <ak-alert class="pf-c-radio__description" inline plain>
`; <a
aria-label="${this.label}"
aria-description="${this.description}"
href="#/enterprise/licenses"
>${this.label}</a
>
</ak-alert>
`;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ak-license-notice": AkLicenceNotice; "ak-license-notice": AKLicenceNotice;
} }
} }

View File

@ -12,8 +12,8 @@ describe("ak-enterprise-status-card", () => {
it("should not error when no data is loaded", async () => { it("should not error when no data is loaded", async () => {
render(html`<ak-enterprise-status-card></ak-enterprise-status-card>`); render(html`<ak-enterprise-status-card></ak-enterprise-status-card>`);
const status = await $("ak-enterprise-status-card"); const status = $("ak-enterprise-status-card");
await expect(status).toHaveText(msg("Loading")); await expect(status).resolves.toHaveText(msg("Loading"));
}); });
it("should render empty when unlicensed", async () => { it("should render empty when unlicensed", async () => {
@ -35,22 +35,22 @@ describe("ak-enterprise-status-card", () => {
</ak-enterprise-status-card>`, </ak-enterprise-status-card>`,
); );
const status = await $("ak-enterprise-status-card").$( const status = $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text", ">>>.pf-c-description-list__description > .pf-c-description-list__text",
); );
await expect(status).toExist(); await expect(status).resolves.toExist();
await expect(status).toHaveText(msg("Unlicensed")); await expect(status).resolves.toHaveText(msg("Unlicensed"));
const internalUserProgress = await $("ak-enterprise-status-card").$( const internalUserProgress = $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar", ">>>#internalUsers > .pf-c-progress__bar",
); );
await expect(internalUserProgress).toExist(); await expect(internalUserProgress).resolves.toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "0"); await expect(internalUserProgress).resolves.toHaveAttr("aria-valuenow", "0");
const externalUserProgress = await $("ak-enterprise-status-card").$( const externalUserProgress = $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar", ">>>#externalUsers > .pf-c-progress__bar",
); );
await expect(externalUserProgress).toExist(); await expect(externalUserProgress).resolves.toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "0"); await expect(externalUserProgress).resolves.toHaveAttr("aria-valuenow", "0");
}); });
it("should show warnings when full", async () => { it("should show warnings when full", async () => {
@ -72,34 +72,35 @@ describe("ak-enterprise-status-card", () => {
</ak-enterprise-status-card>`, </ak-enterprise-status-card>`,
); );
const status = await $("ak-enterprise-status-card").$( const status = $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text", ">>>.pf-c-description-list__description > .pf-c-description-list__text",
); );
await expect(status).toExist(); await expect(status).resolves.toExist();
await expect(status).toHaveText(msg("Valid")); await expect(status).resolves.toHaveText(msg("Valid"));
const internalUserProgress = await $("ak-enterprise-status-card").$( const internalUserProgress = $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar", ">>>#internalUsers > .pf-c-progress__bar",
); );
await expect(internalUserProgress).toExist(); await expect(internalUserProgress).resolves.toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "100"); await expect(internalUserProgress).resolves.toHaveAttr("aria-valuenow", "100");
await expect( await expect($("ak-enterprise-status-card").$(">>>#internalUsers")).toHaveElementClass(
await $("ak-enterprise-status-card").$(">>>#internalUsers"), "pf-m-warning",
).toHaveElementClass("pf-m-warning"); );
const externalUserProgress = await $("ak-enterprise-status-card").$( const externalUserProgress = $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar", ">>>#externalUsers > .pf-c-progress__bar",
); );
await expect(externalUserProgress).toExist(); await expect(externalUserProgress).resolves.toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "100"); await expect(externalUserProgress).resolves.toHaveAttr("aria-valuenow", "100");
await expect( await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"), $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning"); ).resolves.toHaveElementClass("pf-m-warning");
await expect( await expect(
await $("ak-enterprise-status-card").$(">>>#externalUsers"), $("ak-enterprise-status-card").$(">>>#externalUsers"),
).toHaveElementClass("pf-m-warning"); ).resolves.toHaveElementClass("pf-m-warning");
}); });
it("should show infinity when not licensed for a user type", async () => { it("should show infinity when not licensed for a user type", async () => {
@ -121,33 +122,33 @@ describe("ak-enterprise-status-card", () => {
</ak-enterprise-status-card>`, </ak-enterprise-status-card>`,
); );
const status = await $("ak-enterprise-status-card").$( const status = $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text", ">>>.pf-c-description-list__description > .pf-c-description-list__text",
); );
await expect(status).toExist(); await expect(status).resolves.toExist();
await expect(status).toHaveText(msg("Valid")); await expect(status).resolves.toHaveText(msg("Valid"));
const internalUserProgress = await $("ak-enterprise-status-card").$( const internalUserProgress = $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar", ">>>#internalUsers > .pf-c-progress__bar",
); );
await expect(internalUserProgress).toExist(); await expect(internalUserProgress).resolves.toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "100"); await expect(internalUserProgress).resolves.toHaveAttr("aria-valuenow", "100");
await expect( await expect($("ak-enterprise-status-card").$(">>>#internalUsers")).toHaveElementClass(
await $("ak-enterprise-status-card").$(">>>#internalUsers"), "pf-m-warning",
).toHaveElementClass("pf-m-warning"); );
const externalUserProgress = await $("ak-enterprise-status-card").$( const externalUserProgress = $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar", ">>>#externalUsers > .pf-c-progress__bar",
); );
await expect(externalUserProgress).toExist(); await expect(externalUserProgress).resolves.toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "∞"); await expect(externalUserProgress).resolves.toHaveAttr("aria-valuenow", "∞");
await expect( await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"), $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning"); ).resolves.toHaveElementClass("pf-m-warning");
await expect( await expect(
await $("ak-enterprise-status-card").$(">>>#externalUsers"), $("ak-enterprise-status-card").$(">>>#externalUsers"),
).toHaveElementClass("pf-m-danger"); ).resolves.toHaveElementClass("pf-m-danger");
}); });
}); });

View File

@ -211,9 +211,8 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
${msg("Required authentication level for this flow.")} ${msg("Required authentication level for this flow.")}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group> <ak-form-group label="${msg("Behavior settings")}">
<span slot="header"> ${msg("Behavior settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="compatibilityMode"> <ak-form-element-horizontal name="compatibilityMode">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input
@ -286,9 +285,8 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Appearance settings")}">
<span slot="header"> ${msg("Appearance settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Layout")} required name="layout"> <ak-form-element-horizontal label=${msg("Layout")} required name="layout">
<select class="pf-c-form-control"> <select class="pf-c-form-control">
<option <option

View File

@ -231,9 +231,8 @@ export class OutpostForm extends ModelForm<Outpost, string> {
selected-label="${msg("Selected Applications")}" selected-label="${msg("Selected Applications")}"
></ak-dual-select-provider> ></ak-dual-select-provider>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group aria-label=${msg("Advanced settings")}> <ak-form-group label=${msg("Advanced settings")}>
<span slot="header"> ${msg("Advanced settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Configuration")} name="config"> <ak-form-element-horizontal label=${msg("Configuration")} name="config">
<ak-codemirror <ak-codemirror
mode=${CodeMirrorMode.YAML} mode=${CodeMirrorMode.YAML}

View File

@ -64,9 +64,8 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Policy-specific settings")}">
<span slot="header"> ${msg("Policy-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="result"> <ak-form-element-horizontal name="result">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input

View File

@ -76,9 +76,8 @@ export class EventMatcherPolicyForm extends BasePolicyForm<EventMatcherPolicy> {
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Policy-specific settings")}">
<span slot="header"> ${msg("Policy-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Action")} name="action"> <ak-form-element-horizontal label=${msg("Action")} name="action">
<ak-search-select <ak-search-select
.fetchObjects=${async (query?: string): Promise<TypeCreate[]> => { .fetchObjects=${async (query?: string): Promise<TypeCreate[]> => {

View File

@ -64,9 +64,8 @@ export class PasswordExpiryPolicyForm extends BasePolicyForm<PasswordExpiryPolic
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Policy-specific settings")}">
<span slot="header"> ${msg("Policy-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Maximum age (in days)")} label=${msg("Maximum age (in days)")}
required required

View File

@ -67,9 +67,8 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Policy-specific settings")}">
<span slot="header"> ${msg("Policy-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Expression")} label=${msg("Expression")}
required required

View File

@ -78,9 +78,8 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group> <ak-form-group label="${msg("Distance settings")}">
<span slot="header"> ${msg("Distance settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="checkHistoryDistance"> <ak-form-element-horizontal name="checkHistoryDistance">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input
@ -185,9 +184,8 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Static rule settings")}">
<span slot="header">${msg("Static rule settings")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("ASNs")} name="asns"> <ak-form-element-horizontal label=${msg("ASNs")} name="asns">
<input <input
type="text" type="text"

View File

@ -44,9 +44,8 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
} }
renderStaticRules(): TemplateResult { renderStaticRules(): TemplateResult {
return html` <ak-form-group> return html` <ak-form-group label="${msg("Static rules")}">
<span slot="header"> ${msg("Static rules")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Minimum length")} label=${msg("Minimum length")}
required required
@ -142,9 +141,8 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
renderHIBP(): TemplateResult { renderHIBP(): TemplateResult {
return html` return html`
<ak-form-group expanded> <ak-form-group open label="${msg("HaveIBeenPwned settings")}">
<span slot="header"> ${msg("HaveIBeenPwned settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Allowed count")} label=${msg("Allowed count")}
required required
@ -167,9 +165,8 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
renderZxcvbn(): TemplateResult { renderZxcvbn(): TemplateResult {
return html` return html`
<ak-form-group expanded> <ak-form-group open label="${msg("zxcvbn settings")}">
<span slot="header"> ${msg("zxcvbn settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Score threshold")} label=${msg("Score threshold")}
required required

View File

@ -74,9 +74,8 @@ doesn't pass when either or both of the selected options are equal or above the
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Policy-specific settings")}">
<span slot="header"> ${msg("Policy-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="checkIp"> <ak-form-element-horizontal name="checkIp">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input

View File

@ -62,9 +62,8 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("General settings")}">
<span slot="header"> ${msg("General settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Username")} label=${msg("Username")}
name="staticSettings.username" name="staticSettings.username"
@ -89,9 +88,8 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("RDP settings")}">
<span slot="header"> ${msg("RDP settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Ignore server certificate")} label=${msg("Ignore server certificate")}
name="staticSettings.ignore-cert" name="staticSettings.ignore-cert"
@ -134,9 +132,8 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Advanced settings")}">
<span slot="header"> ${msg("Advanced settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Expression")} label=${msg("Expression")}
required required

View File

@ -3,7 +3,7 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
export abstract class BaseProviderForm<T> extends ModelForm<T, number> { export abstract class BaseProviderForm<T> extends ModelForm<T, number> {
getSuccessMessage(): string { public override getSuccessMessage(): string {
return this.instance return this.instance
? msg("Successfully updated provider.") ? msg("Successfully updated provider.")
: msg("Successfully created provider."); : msg("Successfully created provider.");

View File

@ -28,32 +28,38 @@ import { Provider, ProvidersApi } from "@goauthentik/api";
@customElement("ak-provider-list") @customElement("ak-provider-list")
export class ProviderListPage extends TablePage<Provider> { export class ProviderListPage extends TablePage<Provider> {
searchEnabled(): boolean { override searchEnabled(): boolean {
return true; return true;
} }
pageTitle(): string {
override pageTitle(): string {
return msg("Providers"); return msg("Providers");
} }
pageDescription(): string {
override pageDescription(): string {
return msg("Provide support for protocols like SAML and OAuth to assigned applications."); return msg("Provide support for protocols like SAML and OAuth to assigned applications.");
} }
pageIcon(): string {
override pageIcon(): string {
return "pf-icon pf-icon-integration"; return "pf-icon pf-icon-integration";
} }
checkbox = true; override checkbox = true;
clearOnRefresh = true; override clearOnRefresh = true;
@property() @property()
order = "name"; public order = "name";
async apiEndpoint(): Promise<PaginatedResponse<Provider>> { public searchLabel = msg("Provider name");
public searchPlaceholder = msg("Search for providers…");
override async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
return new ProvidersApi(DEFAULT_CONFIG).providersAllList( return new ProvidersApi(DEFAULT_CONFIG).providersAllList(
await this.defaultEndpointConfig(), await this.defaultEndpointConfig(),
); );
} }
columns(): TableColumn[] { override columns(): TableColumn[] {
return [ return [
new TableColumn(msg("Name"), "name"), new TableColumn(msg("Name"), "name"),
new TableColumn(msg("Application")), new TableColumn(msg("Application")),
@ -62,8 +68,9 @@ export class ProviderListPage extends TablePage<Provider> {
]; ];
} }
renderToolbarSelected(): TemplateResult { override renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1; const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk return html`<ak-forms-delete-bulk
objectLabel=${msg("Provider(s)")} objectLabel=${msg("Provider(s)")}
.objects=${this.selectedElements} .objects=${this.selectedElements}
@ -84,7 +91,7 @@ export class ProviderListPage extends TablePage<Provider> {
</ak-forms-delete-bulk>`; </ak-forms-delete-bulk>`;
} }
rowApp(item: Provider): TemplateResult { #rowApp(item: Provider): TemplateResult {
if (item.assignedApplicationName) { if (item.assignedApplicationName) {
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i> return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
${msg("Assigned to application ")} ${msg("Assigned to application ")}
@ -92,6 +99,7 @@ export class ProviderListPage extends TablePage<Provider> {
>${item.assignedApplicationName}</a >${item.assignedApplicationName}</a
>`; >`;
} }
if (item.assignedBackchannelApplicationName) { if (item.assignedBackchannelApplicationName) {
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i> return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
${msg("Assigned to application (backchannel) ")} ${msg("Assigned to application (backchannel) ")}
@ -99,15 +107,15 @@ export class ProviderListPage extends TablePage<Provider> {
>${item.assignedBackchannelApplicationName}</a >${item.assignedBackchannelApplicationName}</a
>`; >`;
} }
return html`<i class="pf-icon pf-icon-warning-triangle pf-m-warning"></i> ${msg(
"Warning: Provider not assigned to any application.", return html`<i aria-hidden="true" class="pf-icon pf-icon-warning-triangle pf-m-warning"></i>
)}`; ${msg("Warning: Provider not assigned to any application.")}`;
} }
row(item: Provider): TemplateResult[] { override row(item: Provider): TemplateResult[] {
return [ return [
html`<a href="#/core/providers/${item.pk}"> ${item.name} </a>`, html`<a href="#/core/providers/${item.pk}"> ${item.name} </a>`,
this.rowApp(item), this.#rowApp(item),
html`${item.verboseName}`, html`${item.verboseName}`,
html`<ak-forms-modal> html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span> <span slot="submit"> ${msg("Update")} </span>
@ -120,16 +128,20 @@ export class ProviderListPage extends TablePage<Provider> {
type=${item.component} type=${item.component}
> >
</ak-proxy-form> </ak-proxy-form>
<button slot="trigger" class="pf-c-button pf-m-plain"> <button
aria-label=${msg("Edit provider")}
slot="trigger"
class="pf-c-button pf-m-plain"
>
<pf-tooltip position="top" content=${msg("Edit")}> <pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i> <i aria-hidden="true" class="fas fa-edit"></i>
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal>`, </ak-forms-modal>`,
]; ];
} }
renderObjectCreate(): TemplateResult { override renderObjectCreate(): TemplateResult {
return html`<ak-provider-wizard> </ak-provider-wizard> `; return html`<ak-provider-wizard> </ak-provider-wizard> `;
} }
} }

View File

@ -25,23 +25,16 @@ import { ProvidersApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-provider-wizard") @customElement("ak-provider-wizard")
export class ProviderWizard extends AKElement { export class ProviderWizard extends AKElement {
static get styles(): CSSResult[] { static styles: CSSResult[] = [PFBase, PFButton];
return [PFBase, PFButton];
}
@property()
createText = msg("Create");
@property({ attribute: false }) @property({ attribute: false })
providerTypes: TypeCreate[] = []; public providerTypes: TypeCreate[] = [];
@property({ attribute: false }) @property({ attribute: false })
finalHandler: () => Promise<void> = () => { public finalHandler?: () => Promise<void>;
return Promise.resolve();
};
@query("ak-wizard") @query("ak-wizard")
wizard?: Wizard; private wizard?: Wizard;
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
@ -56,9 +49,7 @@ export class ProviderWizard extends AKElement {
.steps=${["initial"]} .steps=${["initial"]}
header=${msg("New provider")} header=${msg("New provider")}
description=${msg("Create a new provider.")} description=${msg("Create a new provider.")}
.finalHandler=${() => { .finalHandler=${this.finalHandler}
return this.finalHandler();
}}
> >
<ak-wizard-page-type-create <ak-wizard-page-type-create
name="selectProviderType" name="selectProviderType"
@ -82,7 +73,15 @@ export class ProviderWizard extends AKElement {
</ak-wizard-page-form> </ak-wizard-page-form>
`; `;
})} })}
<button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button> <button
aria-label=${msg("New Provider")}
aria-description="${msg("Open the wizard to create a new provider.")}"
type="button"
slot="trigger"
class="pf-c-button pf-m-primary"
>
${msg("Create")}
</button>
</ak-wizard> </ak-wizard>
`; `;
} }

View File

@ -56,9 +56,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Credentials")} label=${msg("Credentials")}
required required
@ -181,9 +180,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("User filtering")}">
<span slot="header">${msg("User filtering")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount"> <ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input
@ -234,9 +232,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("Attribute mapping")}">
<span slot="header"> ${msg("Attribute mapping")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
name="propertyMappings" name="propertyMappings"

View File

@ -47,7 +47,9 @@ export function renderForm(
) { ) {
return html` return html`
<ak-text-input <ak-text-input
autocomplete="on"
name="name" name="name"
placeholder=${msg("Provider name")}
value=${ifDefined(provider?.name)} value=${ifDefined(provider?.name)}
label=${msg("Name")} label=${msg("Name")}
.errorMessages=${errors?.name ?? []} .errorMessages=${errors?.name ?? []}
@ -80,10 +82,8 @@ export function renderForm(
> >
</ak-switch-input> </ak-switch-input>
<ak-form-group expanded> <ak-form-group open label="${msg("Flow settings")}">
<span slot="header"> ${msg("Flow settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Bind flow")} label=${msg("Bind flow")}
required required
@ -91,6 +91,7 @@ export function renderForm(
.errorMessages=${errors?.authorizationFlow ?? []} .errorMessages=${errors?.authorizationFlow ?? []}
> >
<ak-branded-flow-search <ak-branded-flow-search
label=${msg("Bind flow")}
flowType=${FlowsInstancesListDesignationEnum.Authentication} flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow} .currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication} .brandFlow=${brand?.flowAuthentication}
@ -119,9 +120,8 @@ export function renderForm(
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="baseDn" name="baseDn"
label=${msg("Base DN")} label=${msg("Base DN")}
@ -141,6 +141,8 @@ export function renderForm(
.errorMessages=${errors?.certificate ?? []} .errorMessages=${errors?.certificate ?? []}
> >
<ak-crypto-certificate-search <ak-crypto-certificate-search
label=${msg("Certificate")}
placeholder=${msg("Select a certificate...")}
certificate=${ifDefined(provider?.certificate ?? nothing)} certificate=${ifDefined(provider?.certificate ?? nothing)}
name="certificate" name="certificate"
> >

View File

@ -55,9 +55,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Client ID")} required name="clientId"> <ak-form-element-horizontal label=${msg("Client ID")} required name="clientId">
<input <input
type="text" type="text"
@ -157,9 +156,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("User filtering")}">
<span slot="header">${msg("User filtering")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount"> <ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input
@ -210,9 +208,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("Attribute mapping")}">
<span slot="header"> ${msg("Attribute mapping")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
name="propertyMappings" name="propertyMappings"

View File

@ -2,7 +2,7 @@ import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types"; import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import { css } from "lit"; import { CSSResult, css } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ClientTypeEnum, OAuth2Provider, ProvidersApi } from "@goauthentik/api"; import { ClientTypeEnum, OAuth2Provider, ProvidersApi } from "@goauthentik/api";
@ -21,16 +21,14 @@ export async function oauth2ProvidersProvider(page = 1, search = "") {
return { return {
pagination: oauthProviders.pagination, pagination: oauthProviders.pagination,
options: oauthProviders.results.map((provider) => providerToSelect(provider)), options: oauthProviders.results.map(providerToSelect),
}; };
} }
export function oauth2ProviderSelector(instanceProviders: number[] | undefined) { export function oauth2ProviderSelector(instanceProviders: number[] | undefined) {
if (!instanceProviders) { if (!instanceProviders) {
return async (mappings: DualSelectPair<OAuth2Provider>[]) => return async (mappings: DualSelectPair<OAuth2Provider>[]) =>
mappings.filter( mappings.filter(([, , , source]: DualSelectPair<OAuth2Provider>) => !source);
([_0, _1, _2, source]: DualSelectPair<OAuth2Provider>) => source !== undefined,
);
} }
return async () => { return async () => {
@ -57,41 +55,46 @@ export function oauth2ProviderSelector(instanceProviders: number[] | undefined)
@customElement("ak-provider-oauth2-form") @customElement("ak-provider-oauth2-form")
export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> { export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
@state() static styles: CSSResult[] = [
showClientSecret = true; ...super.styles,
css`
static get styles() {
return super.styles.concat(css`
ak-array-input { ak-array-input {
width: 100%; width: 100%;
} }
`); `,
} ];
async loadInstance(pk: number): Promise<OAuth2Provider> { @state()
protected showClientSecret = true;
override async loadInstance(pk: number): Promise<OAuth2Provider> {
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({ const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({
id: pk, id: pk,
}); });
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential; this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
return provider; return provider;
} }
async send(data: OAuth2Provider): Promise<OAuth2Provider> { override async send(data: OAuth2Provider): Promise<OAuth2Provider> {
if (this.instance) { if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({ return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({
id: this.instance.pk, id: this.instance.pk,
oAuth2ProviderRequest: data, oAuth2ProviderRequest: data,
}); });
} }
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({ return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({
oAuth2ProviderRequest: data, oAuth2ProviderRequest: data,
}); });
} }
renderForm() { override renderForm() {
const showClientSecretCallback = (show: boolean) => { const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show; this.showClientSecret = show;
}; };
return renderForm(this.instance ?? {}, [], this.showClientSecret, showClientSecretCallback); return renderForm(this.instance ?? {}, [], this.showClientSecret, showClientSecretCallback);
} }
} }

View File

@ -125,7 +125,9 @@ export function renderForm(
showClientSecretCallback: ShowClientSecret = defaultShowClientSecret, showClientSecretCallback: ShowClientSecret = defaultShowClientSecret,
) { ) {
return html` <ak-text-input return html` <ak-text-input
autocomplete="on"
name="name" name="name"
placeholder=${msg("Provider name")}
label=${msg("Name")} label=${msg("Name")}
value=${ifDefined(provider?.name)} value=${ifDefined(provider?.name)}
required required
@ -137,6 +139,8 @@ export function renderForm(
required required
> >
<ak-flow-search <ak-flow-search
label=${msg("Authorization flow")}
placeholder=${msg("Select an authorization flow...")}
flowType=${FlowsInstancesListDesignationEnum.Authorization} flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow} .currentFlow=${provider?.authorizationFlow}
required required
@ -145,9 +149,8 @@ export function renderForm(
${msg("Flow used when authorizing this provider.")} ${msg("Flow used when authorizing this provider.")}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-radio-input <ak-radio-input
name="clientType" name="clientType"
label=${msg("Client type")} label=${msg("Client type")}
@ -196,6 +199,8 @@ export function renderForm(
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey"> <ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements --> <!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search <ak-crypto-certificate-search
label=${msg("Signing Key")}
placeholder=${msg("Select a signing key...")}
certificate=${ifDefined(provider?.signingKey ?? undefined)} certificate=${ifDefined(provider?.signingKey ?? undefined)}
singleton singleton
></ak-crypto-certificate-search> ></ak-crypto-certificate-search>
@ -204,6 +209,8 @@ export function renderForm(
<ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey"> <ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey">
<!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements --> <!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements -->
<ak-crypto-certificate-search <ak-crypto-certificate-search
label=${msg("Encryption Key")}
placeholder=${msg("Select an encryption key...")}
certificate=${ifDefined(provider?.encryptionKey ?? undefined)} certificate=${ifDefined(provider?.encryptionKey ?? undefined)}
></ak-crypto-certificate-search> ></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to encrypt the tokens.")}</p> <p class="pf-c-form__helper-text">${msg("Key used to encrypt the tokens.")}</p>
@ -211,14 +218,15 @@ export function renderForm(
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label=${msg("Advanced flow settings")}>
<span slot="header"> ${msg("Advanced flow settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
name="authenticationFlow" name="authenticationFlow"
label=${msg("Authentication flow")} label=${msg("Authentication flow")}
> >
<ak-flow-search <ak-flow-search
label=${msg("Authentication flow")}
placeHolder=${msg("Select an authentication flow...")}
flowType=${FlowsInstancesListDesignationEnum.Authentication} flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow} .currentFlow=${provider?.authenticationFlow}
></ak-flow-search> ></ak-flow-search>
@ -234,6 +242,8 @@ export function renderForm(
required required
> >
<ak-flow-search <ak-flow-search
label=${msg("Invalidation flow")}
placeHolder=${msg("Select an invalidation flow...")}
flowType=${FlowsInstancesListDesignationEnum.Invalidation} flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow} .currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow" defaultFlowSlug="default-provider-invalidation-flow"
@ -246,9 +256,8 @@ export function renderForm(
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Advanced protocol settings")}">
<span slot="header"> ${msg("Advanced protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="accessCodeValidity" name="accessCodeValidity"
label=${msg("Access code validity")} label=${msg("Access code validity")}
@ -331,9 +340,8 @@ export function renderForm(
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Machine-to-Machine authentication settings")}">
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Federated OIDC Sources")} label=${msg("Federated OIDC Sources")}
name="jwtFederationSources" name="jwtFederationSources"

View File

@ -1,5 +1,8 @@
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI"; import "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js"; import {
AkControlElement,
formatFormElementAsJSON,
} from "@goauthentik/elements/AkControlElement.js";
import { type Spread } from "@goauthentik/elements/types"; import { type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers"; import { spread } from "@open-wc/lit-helpers";
@ -43,9 +46,7 @@ export class OAuth2ProviderRedirectURI extends AkControlElement<RedirectURI> {
controls?: HTMLInputElement[]; controls?: HTMLInputElement[];
json() { json() {
return Object.fromEntries( return formatFormElementAsJSON<RedirectURI>(this.controls);
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
) as unknown as RedirectURI;
} }
get isValid() { get isValid() {

View File

@ -4,6 +4,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import renderDescriptionList from "@goauthentik/components/DescriptionList"; import renderDescriptionList from "@goauthentik/components/DescriptionList";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
import { IDGenerator } from "@goauthentik/core/id";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
@ -265,12 +266,16 @@ export class OAuth2ProviderViewPage extends AKElement {
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<form class="pf-c-form"> <form class="pf-c-form">
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<label class="pf-c-form__label"> <label
class="pf-c-form__label"
for="${IDGenerator.elementID("providerInfo")}"
>
<span class="pf-c-form__label-text" <span class="pf-c-form__label-text"
>${msg("OpenID Configuration URL")}</span >${msg("OpenID Configuration URL")}</span
> >
</label> </label>
<input <input
id="${IDGenerator.elementID("providerInfo")}"
class="pf-c-form-control" class="pf-c-form-control"
readonly readonly
type="text" type="text"
@ -278,12 +283,16 @@ export class OAuth2ProviderViewPage extends AKElement {
/> />
</div> </div>
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<label class="pf-c-form__label"> <label
class="pf-c-form__label"
for="${IDGenerator.elementID("issuer")}"
>
<span class="pf-c-form__label-text" <span class="pf-c-form__label-text"
>${msg("OpenID Configuration Issuer")}</span >${msg("OpenID Configuration Issuer")}</span
> >
</label> </label>
<input <input
id="${IDGenerator.elementID("issuer")}"
class="pf-c-form-control" class="pf-c-form-control"
readonly readonly
type="text" type="text"
@ -292,12 +301,16 @@ export class OAuth2ProviderViewPage extends AKElement {
</div> </div>
<hr class="pf-c-divider" /> <hr class="pf-c-divider" />
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<label class="pf-c-form__label"> <label
class="pf-c-form__label"
for="${IDGenerator.elementID("authorize")}"
>
<span class="pf-c-form__label-text" <span class="pf-c-form__label-text"
>${msg("Authorize URL")}</span >${msg("Authorize URL")}</span
> >
</label> </label>
<input <input
id="${IDGenerator.elementID("authorize")}"
class="pf-c-form-control" class="pf-c-form-control"
readonly readonly
type="text" type="text"
@ -305,10 +318,14 @@ export class OAuth2ProviderViewPage extends AKElement {
/> />
</div> </div>
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<label class="pf-c-form__label"> <label
class="pf-c-form__label"
for="${IDGenerator.elementID("token")}"
>
<span class="pf-c-form__label-text">${msg("Token URL")}</span> <span class="pf-c-form__label-text">${msg("Token URL")}</span>
</label> </label>
<input <input
id="${IDGenerator.elementID("token")}"
class="pf-c-form-control" class="pf-c-form-control"
readonly readonly
type="text" type="text"
@ -316,12 +333,16 @@ export class OAuth2ProviderViewPage extends AKElement {
/> />
</div> </div>
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<label class="pf-c-form__label"> <label
class="pf-c-form__label"
for="${IDGenerator.elementID("userInfo")}"
>
<span class="pf-c-form__label-text" <span class="pf-c-form__label-text"
>${msg("Userinfo URL")}</span >${msg("Userinfo URL")}</span
> >
</label> </label>
<input <input
id="${IDGenerator.elementID("userInfo")}"
class="pf-c-form-control" class="pf-c-form-control"
readonly readonly
type="text" type="text"
@ -329,10 +350,14 @@ export class OAuth2ProviderViewPage extends AKElement {
/> />
</div> </div>
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<label class="pf-c-form__label"> <label
class="pf-c-form__label"
for="${IDGenerator.elementID("logout")}"
>
<span class="pf-c-form__label-text">${msg("Logout URL")}</span> <span class="pf-c-form__label-text">${msg("Logout URL")}</span>
</label> </label>
<input <input
id="${IDGenerator.elementID("logout")}"
class="pf-c-form-control" class="pf-c-form-control"
readonly readonly
type="text" type="text"
@ -340,10 +365,14 @@ export class OAuth2ProviderViewPage extends AKElement {
/> />
</div> </div>
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<label class="pf-c-form__label"> <label
class="pf-c-form__label"
for="${IDGenerator.elementID("jwks")}"
>
<span class="pf-c-form__label-text">${msg("JWKS URL")}</span> <span class="pf-c-form__label-text">${msg("JWKS URL")}</span>
</label> </label>
<input <input
id="${IDGenerator.elementID("jwks")}"
class="pf-c-form-control" class="pf-c-form-control"
readonly readonly
type="text" type="text"
@ -389,9 +418,12 @@ export class OAuth2ProviderViewPage extends AKElement {
${renderDescriptionList( ${renderDescriptionList(
[ [
[ [
msg("Preview for user"), html`<label for="${IDGenerator.elementID("preview-user")}"
>${msg("Preview for user")}</label
>`,
html` html`
<ak-search-select <ak-search-select
id="${IDGenerator.elementID("preview-user")}"
.fetchObjects=${async (query?: string): Promise<User[]> => { .fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = { const args: CoreUsersListRequest = {
ordering: "username", ordering: "username",

View File

@ -228,9 +228,8 @@ export function renderForm(
input-hint="code" input-hint="code"
></ak-text-input> ></ak-text-input>
<ak-form-group> <ak-form-group label="${msg("Advanced protocol settings")}">
<span slot="header">${msg("Advanced protocol settings")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate"> <ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search <ak-crypto-certificate-search
.certificate=${provider?.certificate} .certificate=${provider?.certificate}
@ -273,9 +272,8 @@ ${provider?.skipPathRegex}</textarea
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Authentication settings")}">
<span slot="header">${msg("Authentication settings")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-switch-input <ak-switch-input
name="interceptHeaderAuth" name="interceptHeaderAuth"
label=${msg("Intercept header authentication")} label=${msg("Intercept header authentication")}
@ -333,9 +331,8 @@ ${provider?.skipPathRegex}</textarea
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Advanced flow settings")}">
<span slot="header"> ${msg("Advanced flow settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Authentication flow")} label=${msg("Authentication flow")}
name="authenticationFlow" name="authenticationFlow"

View File

@ -115,9 +115,8 @@ export class EndpointForm extends ModelForm<Endpoint, string> {
selected-label="${msg("Selected User Property Mappings")}" selected-label="${msg("Selected User Property Mappings")}"
></ak-dual-select-dynamic-selected> ></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group> <ak-form-group label="${msg("Advanced settings")}">
<span slot="header"> ${msg("Advanced settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Settings")} name="settings"> <ak-form-element-horizontal label=${msg("Settings")} name="settings">
<ak-codemirror <ak-codemirror
mode="yaml" mode="yaml"

View File

@ -115,9 +115,8 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> {
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Property mappings")} label=${msg("Property mappings")}
name="propertyMappings" name="propertyMappings"

View File

@ -44,6 +44,7 @@ export function renderForm(
<ak-text-input <ak-text-input
name="name" name="name"
label=${msg("Name")} label=${msg("Name")}
placeholder=${msg("Provider name")}
value=${ifDefined(provider?.name)} value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []} .errorMessages=${errors?.name ?? []}
required required
@ -57,6 +58,8 @@ export function renderForm(
.errorMessages=${errors?.authorizationFlow ?? []} .errorMessages=${errors?.authorizationFlow ?? []}
> >
<ak-branded-flow-search <ak-branded-flow-search
label=${msg("Authentication flow")}
placeholder=${msg("Select an authentication flow...")}
flowType=${FlowsInstancesListDesignationEnum.Authentication} flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow} .currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication} .brandFlow=${brand?.flowAuthentication}
@ -73,17 +76,14 @@ export function renderForm(
> >
</ak-switch-input> </ak-switch-input>
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form"> <ak-hidden-text-input>
<ak-hidden-text-input name="sharedSecret" label=${msg("Shared secret")}
name="sharedSecret"
label=${msg("Shared secret")}
.errorMessages=${errors?.sharedSecret ?? []} .errorMessages=${errors?.sharedSecret ?? []}
value=${provider?.sharedSecret ?? randomString(128, ascii_letters + digits)} value=${provider?.sharedSecret ?? randomString(128, ascii_letters + digits)}
required required input-hint="code" ></ak-hidden-text-input
input-hint="code" >
></ak-hidden-text-input>
<ak-text-input <ak-text-input
name="clientNetworks" name="clientNetworks"
label=${msg("Client Networks")} label=${msg("Client Networks")}
@ -106,15 +106,16 @@ export function renderForm(
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Advanced flow settings")}">
<span slot="header"> ${msg("Advanced flow settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Invalidation flow")} label=${msg("Invalidation flow")}
name="invalidationFlow" name="invalidationFlow"
required required
> >
<ak-flow-search <ak-flow-search
label=${msg("Invalidation flow")}
placeholder=${msg("Select an invalidation flow...")}
flowType=${FlowsInstancesListDesignationEnum.Invalidation} flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow} .currentFlow=${provider?.invalidationFlow}
.errorMessages=${errors?.invalidationFlow ?? []} .errorMessages=${errors?.invalidationFlow ?? []}

View File

@ -84,9 +84,8 @@ export function renderForm(
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="acsUrl" name="acsUrl"
label=${msg("ACS URL")} label=${msg("ACS URL")}
@ -122,9 +121,8 @@ export function renderForm(
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Advanced flow settings")}">
<span slot="header"> ${msg("Advanced flow settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Authentication flow")} label=${msg("Authentication flow")}
name="authenticationFlow" name="authenticationFlow"
@ -157,9 +155,8 @@ export function renderForm(
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Advanced protocol settings")}">
<span slot="header"> ${msg("Advanced protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Signing Certificate")} name="signingKp"> <ak-form-element-horizontal label=${msg("Signing Certificate")} name="signingKp">
<ak-crypto-certificate-search <ak-crypto-certificate-search
.certificate=${provider?.signingKp} .certificate=${provider?.signingKp}

View File

@ -31,9 +31,8 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
required required
help=${msg("Method's display Name.")} help=${msg("Method's display Name.")}
></ak-text-input> ></ak-text-input>
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="url" name="url"
label=${msg("URL")} label=${msg("URL")}
@ -113,9 +112,8 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("User filtering")}">
<span slot="header">${msg("User filtering")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-switch-input <ak-switch-input
name="excludeUsersServiceAccount" name="excludeUsersServiceAccount"
label=${msg("Exclude service accounts")} label=${msg("Exclude service accounts")}
@ -155,9 +153,8 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("Attribute mapping")}">
<span slot="header"> ${msg("Attribute mapping")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
name="propertyMappings" name="propertyMappings"

View File

@ -57,9 +57,8 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
value=${ifDefined(provider?.name)} value=${ifDefined(provider?.name)}
required required
></ak-text-input> ></ak-text-input>
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Signing Key")} label=${msg("Signing Key")}
name="signingKey" name="signingKey"
@ -93,9 +92,8 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Authentication settings")}">
<span slot="header">${msg("Authentication settings")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("OIDC Providers")} label=${msg("OIDC Providers")}
name="oidcAuthProviders" name="oidcAuthProviders"

View File

@ -67,7 +67,7 @@ export class InitialPermissionsListPage extends TablePage<InitialPermissions> {
</ak-forms-delete-bulk>`; </ak-forms-delete-bulk>`;
} }
render(): TemplateResult { render() {
return html`<ak-page-header return html`<ak-page-header
icon=${this.pageIcon()} icon=${this.pageIcon()}
header=${this.pageTitle()} header=${this.pageTitle()}

View File

@ -65,7 +65,7 @@ export class RoleListPage extends TablePage<Role> {
</ak-forms-delete-bulk>`; </ak-forms-delete-bulk>`;
} }
render(): TemplateResult { render() {
return html`<ak-page-header return html`<ak-page-header
icon=${this.pageIcon()} icon=${this.pageIcon()}
header=${this.pageTitle()} header=${this.pageTitle()}

View File

@ -121,9 +121,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
"Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled.", "Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled.",
)} )}
></ak-switch-input> ></ak-switch-input>
<ak-form-group expanded> <ak-form-group open label="${msg("Realm settings")}">
<span slot="header"> ${msg("Realm settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="realm" name="realm"
label=${msg("Realm")} label=${msg("Realm")}
@ -213,9 +212,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Sync connection settings")}">
<span slot="header"> ${msg("Sync connection settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("KAdmin type")} label=${msg("KAdmin type")}
required required
@ -276,9 +274,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
></ak-text-input> ></ak-text-input>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("SPNEGO settings")}">
<span slot="header"> ${msg("SPNEGO settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="spnegoServerName" name="spnegoServerName"
label=${msg("SPNEGO server name")} label=${msg("SPNEGO server name")}
@ -305,9 +302,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
></ak-text-input> ></ak-text-input>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Kerberos Attribute mapping")}">
<span slot="header"> ${msg("Kerberos Attribute mapping")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
name="userPropertyMappings" name="userPropertyMappings"
@ -344,9 +340,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Flow settings")}">
<span slot="header"> ${msg("Flow settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Authentication flow")} label=${msg("Authentication flow")}
name="authenticationFlow" name="authenticationFlow"
@ -377,9 +372,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Additional settings")}">
<span slot="header"> ${msg("Additional settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="userPathTemplate" name="userPathTemplate"
label=${msg("User path")} label=${msg("User path")}

View File

@ -171,9 +171,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Connection settings")}">
<span slot="header"> ${msg("Connection settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Server URI")} label=${msg("Server URI")}
required required
@ -277,9 +276,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("LDAP Attribute mapping")}">
<span slot="header"> ${msg("LDAP Attribute mapping")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
name="userPropertyMappings" name="userPropertyMappings"
@ -314,9 +312,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Additional settings")}">
<span slot="header"> ${msg("Additional settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Parent Group")} name="syncParentGroup"> <ak-form-element-horizontal label=${msg("Parent Group")} name="syncParentGroup">
<ak-search-select <ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => { .fetchObjects=${async (query?: string): Promise<Group[]> => {

View File

@ -126,9 +126,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
if (!this.providerType?.urlsCustomizable) { if (!this.providerType?.urlsCustomizable) {
return html``; return html``;
} }
return html` <ak-form-group expanded> return html` <ak-form-group open label="${msg("URL settings")}">
<span slot="header"> ${msg("URL settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Authorization URL")} label=${msg("Authorization URL")}
name="authorizationUrl" name="authorizationUrl"
@ -421,9 +420,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<p class="pf-c-form__helper-text">${iconHelperText}</p> <p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`} </ak-form-element-horizontal>`}
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Consumer key")} label=${msg("Consumer key")}
required required
@ -464,9 +462,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
</div> </div>
</ak-form-group> </ak-form-group>
${this.renderUrlOptions()} ${this.renderUrlOptions()}
<ak-form-group expanded> <ak-form-group open label="${msg("OAuth Attribute mapping")}">
<span slot="header"> ${msg("OAuth Attribute mapping")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
name="userPropertyMappings" name="userPropertyMappings"
@ -501,9 +498,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Flow settings")}">
<span slot="header"> ${msg("Flow settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Authentication flow")} label=${msg("Authentication flow")}
name="authenticationFlow" name="authenticationFlow"

View File

@ -334,9 +334,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
/> />
<p class="pf-c-form__helper-text">${iconHelperText}</p> <p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`} </ak-form-element-horizontal>`}
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Client ID")} required name="clientId"> <ak-form-element-horizontal label=${msg("Client ID")} required name="clientId">
<input <input
type="text" type="text"
@ -348,9 +347,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
${this.renderSettings()} ${this.renderSettings()}
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Flow settings")}">
<span slot="header"> ${msg("Flow settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Authentication flow")} label=${msg("Authentication flow")}
name="authenticationFlow" name="authenticationFlow"
@ -381,9 +379,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("Plex Attribute mapping")}">
<span slot="header"> ${msg("Plex Attribute mapping")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
name="userPropertyMappings" name="userPropertyMappings"

View File

@ -233,9 +233,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
<p class="pf-c-form__helper-text">${iconHelperText}</p> <p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`} </ak-form-element-horizontal>`}
<ak-form-group expanded> <ak-form-group open label="${msg("Protocol settings")}">
<span slot="header"> ${msg("Protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("SSO URL")} required name="ssoUrl"> <ak-form-element-horizontal label=${msg("SSO URL")} required name="ssoUrl">
<input <input
type="text" type="text"
@ -321,9 +320,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Advanced protocol settings")}">
<span slot="header"> ${msg("Advanced protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="allowIdpInitiated"> <ak-form-element-horizontal name="allowIdpInitiated">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input
@ -493,9 +491,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("SAML Attribute mapping")}">
<span slot="header"> ${msg("SAML Attribute mapping")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
name="userPropertyMappings" name="userPropertyMappings"
@ -530,9 +527,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Flow settings")}">
<span slot="header"> ${msg("Flow settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Pre-authentication flow")} label=${msg("Pre-authentication flow")}
required required

View File

@ -68,9 +68,8 @@ export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
<label class="pf-c-check__label"> ${msg("Enabled")} </label> <label class="pf-c-check__label"> ${msg("Enabled")} </label>
</div> </div>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("SCIM Attribute mapping")}">
<span slot="header"> ${msg("SCIM Attribute mapping")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
name="userPropertyMappings" name="userPropertyMappings"
@ -105,9 +104,8 @@ export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Advanced protocol settings")}">
<span slot="header"> ${msg("Advanced protocol settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate"> <ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate">
<input <input
type="text" type="text"

View File

@ -80,9 +80,8 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Duo Auth API")}">
<span slot="header"> ${msg("Duo Auth API")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Integration key")} label=${msg("Integration key")}
required required
@ -104,15 +103,13 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
></ak-secret-text-input> ></ak-secret-text-input>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group
<span slot="header">${msg("Duo Admin API (optional)")}</span> label=${msg("Duo Admin API (optional)")}
<span slot="description"> description="${msg(
${msg( `When using a Duo MFA, Access or Beyond plan, an Admin API application can be created. This will allow authentik to import devices automatically.`,
`When using a Duo MFA, Access or Beyond plan, an Admin API application can be created. )}"
This will allow authentik to import devices automatically.`, >
)} <div class="pf-c-form">
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Integration key")} label=${msg("Integration key")}
name="adminIntegrationKey" name="adminIntegrationKey"
@ -133,9 +130,8 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
></ak-secret-text-input> ></ak-secret-text-input>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Configuration flow")} label=${msg("Configuration flow")}
name="configureFlow" name="configureFlow"

View File

@ -50,9 +50,8 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
if (!this.showConnectionSettings) { if (!this.showConnectionSettings) {
return html``; return html``;
} }
return html`<ak-form-group expanded> return html`<ak-form-group open label="${msg("Connection settings")}">
<span slot="header"> ${msg("Connection settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("SMTP Host")} required name="host"> <ak-form-element-horizontal label=${msg("SMTP Host")} required name="host">
<input <input
type="text" type="text"
@ -191,9 +190,8 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${this.renderConnectionSettings()} ${this.renderConnectionSettings()}
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Subject")} required name="subject"> <ak-form-element-horizontal label=${msg("Subject")} required name="subject">
<input <input
type="text" type="text"

View File

@ -55,9 +55,8 @@ export class AuthenticatorEndpointGDTCStageForm extends BaseStageForm<Authentica
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Google Verified Access API")}">
<span slot="header"> ${msg("Google Verified Access API")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Credentials")} label=${msg("Credentials")}
required required

View File

@ -222,9 +222,8 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Provider")} required name="provider"> <ak-form-element-horizontal label=${msg("Provider")} required name="provider">
<select <select
class="pf-c-form-control" class="pf-c-form-control"

View File

@ -67,9 +67,8 @@ export class AuthenticatorStaticStageForm extends BaseStageForm<AuthenticatorSta
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Token count")} label=${msg("Token count")}
required required

View File

@ -69,9 +69,8 @@ export class AuthenticatorTOTPStageForm extends BaseStageForm<AuthenticatorTOTPS
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Digits")} required name="digits"> <ak-form-element-horizontal label=${msg("Digits")} required name="digits">
<select name="users" class="pf-c-form-control"> <select name="users" class="pf-c-form-control">
<option <option

View File

@ -95,9 +95,8 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Device classes")} label=${msg("Device classes")}
required required
@ -208,9 +207,8 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
: html``} : html``}
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group expanded> <ak-form-group open label="${msg("WebAuthn-specific settings")}">
<span slot="header"> ${msg("WebAuthn-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("WebAuthn User verification")} label=${msg("WebAuthn User verification")}
required required

View File

@ -77,9 +77,8 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User verification")} label=${msg("User verification")}
required required

View File

@ -47,9 +47,8 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Public Key")} label=${msg("Public Key")}
required required
@ -126,9 +125,8 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Advanced settings")}">
<span slot="header"> ${msg("Advanced settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("JS URL")} required name="jsUrl"> <ak-form-element-horizontal label=${msg("JS URL")} required name="jsUrl">
<input <input
type="url" type="url"

View File

@ -53,9 +53,8 @@ export class ConsentStageForm extends BaseStageForm<ConsentStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Mode")} required name="mode"> <ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<select <select
class="pf-c-form-control" class="pf-c-form-control"

View File

@ -44,9 +44,8 @@ export class DenyStageForm extends BaseStageForm<DenyStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Deny message")} name="denyMessage"> <ak-form-element-horizontal label=${msg("Deny message")} name="denyMessage">
<input <input
type="text" type="text"

View File

@ -47,9 +47,8 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
if (!this.showConnectionSettings) { if (!this.showConnectionSettings) {
return html``; return html``;
} }
return html`<ak-form-group> return html`<ak-form-group label="${msg("Connection settings")}">
<span slot="header"> ${msg("Connection settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("SMTP Host")} required name="host"> <ak-form-element-horizontal label=${msg("SMTP Host")} required name="host">
<input <input
type="text" type="text"
@ -146,9 +145,8 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="activateUserOnSuccess"> <ak-form-element-horizontal name="activateUserOnSuccess">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input

View File

@ -84,9 +84,8 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("User fields")} name="userFields"> <ak-form-element-horizontal label=${msg("User fields")} name="userFields">
<ak-checkbox-group <ak-checkbox-group
class="user-field-select" class="user-field-select"
@ -193,9 +192,8 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
></ak-switch-input> ></ak-switch-input>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Source settings")}">
<span slot="header"> ${msg("Source settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Sources")} required name="sources"> <ak-form-element-horizontal label=${msg("Sources")} required name="sources">
<ak-dual-select-dynamic-selected <ak-dual-select-dynamic-selected
.provider=${sourcesProvider} .provider=${sourcesProvider}
@ -231,9 +229,8 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group> <ak-form-group label="${msg("Flow settings")}">
<span slot="header">${msg("Flow settings")}</span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Passwordless flow")} label=${msg("Passwordless flow")}
name="passwordlessFlow" name="passwordlessFlow"

View File

@ -172,7 +172,7 @@ export class InvitationListPage extends TablePage<Invitation> {
`; `;
} }
render(): TemplateResult { render() {
return html`<ak-page-header return html`<ak-page-header
icon=${this.pageIcon()} icon=${this.pageIcon()}
header=${this.pageTitle()} header=${this.pageTitle()}

View File

@ -41,9 +41,8 @@ export class InvitationStageForm extends BaseStageForm<InvitationStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="continueFlowWithoutInvitation"> <ak-form-element-horizontal name="continueFlowWithoutInvitation">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input

View File

@ -53,9 +53,8 @@ export class MTLSStageForm extends BaseStageForm<MutualTLSStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Mode")} required name="mode"> <ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<ak-radio <ak-radio
.options=${[ .options=${[

View File

@ -82,9 +82,8 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Backends")} required name="backends"> <ak-form-element-horizontal label=${msg("Backends")} required name="backends">
<ak-checkbox-group <ak-checkbox-group
class="user-field-select" class="user-field-select"

View File

@ -55,9 +55,8 @@ export class PromptStageForm extends BaseStageForm<PromptStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Fields")} required name="fields"> <ak-form-element-horizontal label=${msg("Fields")} required name="fields">
<ak-dual-select-dynamic-selected <ak-dual-select-dynamic-selected
.provider=${promptFieldsProvider} .provider=${promptFieldsProvider}

View File

@ -56,9 +56,8 @@ export class RedirectStageForm extends BaseStageForm<RedirectStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Mode")} required name="mode"> <ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<select <select
class="pf-c-form-control" class="pf-c-form-control"

View File

@ -41,9 +41,8 @@ export class UserLoginStageForm extends BaseStageForm<UserLoginStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Session duration")} label=${msg("Session duration")}
required required

View File

@ -55,9 +55,8 @@ export class UserWriteStageForm extends BaseStageForm<UserWriteStage> {
required required
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group expanded> <ak-form-group open label="${msg("Stage-specific settings")}">
<span slot="header"> ${msg("Stage-specific settings")} </span> <div class="pf-c-form">
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="userCreationMode"> <ak-form-element-horizontal name="userCreationMode">
<ak-radio <ak-radio
.options=${[ .options=${[

View File

@ -399,7 +399,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
`; `;
} }
renderSidebarBefore(): TemplateResult { protected renderSidebarBefore(): TemplateResult {
return html`<div class="pf-c-sidebar__panel pf-m-width-25"> return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__title">${msg("User folders")}</div> <div class="pf-c-card__title">${msg("User folders")}</div>

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