web: add visualizing and testing for the FieldRenderers
This commit is contained in:
parent
0d94373f10
commit
c48eee0ebf
108
web/src/flow/stages/prompt/FieldRenderers.stories.ts
Normal file
108
web/src/flow/stages/prompt/FieldRenderers.stories.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import "@patternfly/patternfly/components/Button/button.css";
|
||||
import "@patternfly/patternfly/components/Check/check.css";
|
||||
import "@patternfly/patternfly/components/Form/form.css";
|
||||
import "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import "@patternfly/patternfly/components/Login/login.css";
|
||||
import "@patternfly/patternfly/components/Title/title.css";
|
||||
import "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { PromptTypeEnum } from "@goauthentik/api";
|
||||
import type { StagePrompt } from "@goauthentik/api";
|
||||
|
||||
import promptRenderers from "./FieldRenderers";
|
||||
import { renderContinue, renderPromptHelpText, renderPromptInner } from "./helpers";
|
||||
|
||||
// Storybook stories are meant to show not just that the objects work, but to document good
|
||||
// practices around using them. Because of their uniform signature, the renderers can easily
|
||||
// be encapsulated into containers that show them at their most functional, even without
|
||||
// building Shadow DOMs with which to do it. This is 100% Light DOM work, and they still
|
||||
// work well.
|
||||
|
||||
const baseRenderer = (prompt: TemplateResult) =>
|
||||
html`<div style="background: #fff; padding: 4em; max-width: 24em;">
|
||||
<style>
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
button,
|
||||
.pf-c-form__helper-text:not(.pf-m-error),
|
||||
input + label.pf-c-check__label {
|
||||
color: #000;
|
||||
}
|
||||
input[readonly],
|
||||
textarea[readonly] {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
${prompt}
|
||||
</div>`;
|
||||
|
||||
function renderer(kind: PromptTypeEnum, prompt: Partial<StagePrompt>) {
|
||||
const renderer = promptRenderers.get(kind);
|
||||
if (!renderer) {
|
||||
throw new Error(`A renderer of type ${kind} does not exist.`);
|
||||
}
|
||||
return baseRenderer(html`${renderer(prompt as StagePrompt)}`);
|
||||
}
|
||||
|
||||
const textPrompt = {
|
||||
fieldKey: "test_text_field",
|
||||
placeholder: "This is the placeholder",
|
||||
required: false,
|
||||
initialValue: "initial value",
|
||||
};
|
||||
|
||||
export const Text = () => renderer(PromptTypeEnum.Text, textPrompt);
|
||||
export const TextArea = () => renderer(PromptTypeEnum.TextArea, textPrompt);
|
||||
export const TextReadOnly = () => renderer(PromptTypeEnum.TextReadOnly, textPrompt);
|
||||
export const TextAreaReadOnly = () => renderer(PromptTypeEnum.TextAreaReadOnly, textPrompt);
|
||||
export const Username = () => renderer(PromptTypeEnum.Username, textPrompt);
|
||||
export const Password = () => renderer(PromptTypeEnum.Password, textPrompt);
|
||||
|
||||
const emailPrompt = { ...textPrompt, initialValue: "example@example.fun" };
|
||||
export const Email = () => renderer(PromptTypeEnum.Email, emailPrompt);
|
||||
|
||||
const numberPrompt = { ...textPrompt, initialValue: "10" };
|
||||
export const Number = () => renderer(PromptTypeEnum.Number, numberPrompt);
|
||||
|
||||
const datePrompt = { ...textPrompt, initialValue: "2018-06-12T19:30" };
|
||||
export const Date = () => renderer(PromptTypeEnum.Date, datePrompt);
|
||||
export const DateTime = () => renderer(PromptTypeEnum.DateTime, datePrompt);
|
||||
|
||||
const separatorPrompt = { placeholder: "😊" };
|
||||
export const Separator = () => renderer(PromptTypeEnum.Separator, separatorPrompt);
|
||||
|
||||
const staticPrompt = { initialValue: "😊" };
|
||||
export const Static = () => renderer(PromptTypeEnum.Static, staticPrompt);
|
||||
|
||||
const choicePrompt = {
|
||||
fieldKey: "test_text_field",
|
||||
placeholder: "This is the placeholder",
|
||||
required: false,
|
||||
initialValue: "first",
|
||||
choices: ["first", "second", "third"],
|
||||
};
|
||||
|
||||
export const Dropdown = () => renderer(PromptTypeEnum.Dropdown, choicePrompt);
|
||||
export const RadioButtonGroup = () => renderer(PromptTypeEnum.RadioButtonGroup, choicePrompt);
|
||||
|
||||
const checkPrompt = { ...textPrompt, label: "Favorite Subtext?", subText: "(Xena & Gabrielle)" };
|
||||
export const Checkbox = () => renderer(PromptTypeEnum.Checkbox, checkPrompt);
|
||||
|
||||
const localePrompt = { ...textPrompt, initialValue: "en" };
|
||||
export const Locale = () => renderer(PromptTypeEnum.AkLocale, localePrompt);
|
||||
|
||||
export const PromptFailure = () =>
|
||||
baseRenderer(renderPromptInner({ type: null } as unknown as StagePrompt));
|
||||
|
||||
export const HelpText = () =>
|
||||
baseRenderer(renderPromptHelpText({ subText: "There is no subtext here." } as StagePrompt));
|
||||
|
||||
export const Continue = () => baseRenderer(renderContinue());
|
||||
|
||||
export default {
|
||||
title: "Flow Components/Field Renderers",
|
||||
};
|
|
@ -207,6 +207,24 @@ export function renderRadioButtonGroup(prompt: StagePrompt) {
|
|||
})}`;
|
||||
}
|
||||
|
||||
export function renderCheckbox(prompt: StagePrompt) {
|
||||
return html`<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
id="${prompt.fieldKey}"
|
||||
name="${prompt.fieldKey}"
|
||||
?checked=${prompt.initialValue !== ""}
|
||||
?required=${prompt.required}
|
||||
/>
|
||||
<label class="pf-c-check__label" for="${prompt.fieldKey}">${prompt.label}</label>
|
||||
${prompt.required
|
||||
? html`<p class="pf-c-form__helper-text">${msg("Required.")}</p>`
|
||||
: html``}
|
||||
<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export function renderAkLocale(prompt: StagePrompt) {
|
||||
// TODO: External reference.
|
||||
const inDebug = rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanDebug);
|
||||
|
@ -247,6 +265,7 @@ export const promptRenderers = new Map<PromptTypeEnum, Renderer>([
|
|||
[PromptTypeEnum.Static, renderStatic],
|
||||
[PromptTypeEnum.Dropdown, renderDropdown],
|
||||
[PromptTypeEnum.RadioButtonGroup, renderRadioButtonGroup],
|
||||
[PromptTypeEnum.Checkbox, renderCheckbox],
|
||||
[PromptTypeEnum.AkLocale, renderAkLocale],
|
||||
]);
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
|
|||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
|
@ -24,7 +23,13 @@ import {
|
|||
StagePrompt,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import promptRenderers from "./FieldRenderers";
|
||||
import { renderCheckbox } from "./FieldRenderers";
|
||||
import {
|
||||
renderContinue,
|
||||
renderPromptHelpText,
|
||||
renderPromptInner,
|
||||
shouldRenderInWrapper,
|
||||
} from "./helpers";
|
||||
|
||||
@customElement("ak-stage-prompt")
|
||||
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
|
||||
|
@ -48,70 +53,35 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
|||
];
|
||||
}
|
||||
|
||||
renderPromptInner(prompt: StagePrompt): TemplateResult {
|
||||
const renderer = promptRenderers.get(prompt.type);
|
||||
if (!renderer) {
|
||||
return html`<p>invalid type '${prompt.type}'</p>`;
|
||||
}
|
||||
return renderer(prompt);
|
||||
}
|
||||
/* TODO: Legacy: None of these refer to the `this` field. Static fields are a code smell. */
|
||||
|
||||
renderPromptHelpText(prompt: StagePrompt): TemplateResult {
|
||||
if (prompt.subText === "") {
|
||||
return html``;
|
||||
}
|
||||
return html`<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>`;
|
||||
renderPromptInner(prompt: StagePrompt) {
|
||||
return renderPromptInner(prompt);
|
||||
}
|
||||
|
||||
shouldRenderInWrapper(prompt: StagePrompt): boolean {
|
||||
// Special types that aren't rendered in a wrapper
|
||||
const specialTypes = [
|
||||
PromptTypeEnum.Static,
|
||||
PromptTypeEnum.Hidden,
|
||||
PromptTypeEnum.Separator,
|
||||
];
|
||||
const special = specialTypes.find((s) => s === prompt.type);
|
||||
return !special;
|
||||
renderPromptHelpText(prompt: StagePrompt) {
|
||||
return renderPromptHelpText(prompt);
|
||||
}
|
||||
shouldRenderInWrapper(prompt: StagePrompt) {
|
||||
return shouldRenderInWrapper(prompt);
|
||||
}
|
||||
|
||||
renderField(prompt: StagePrompt): TemplateResult {
|
||||
// Checkbox is rendered differently
|
||||
// Checkbox has a slightly different layout, so it must be intercepted early.
|
||||
if (prompt.type === PromptTypeEnum.Checkbox) {
|
||||
return html`<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
id="${prompt.fieldKey}"
|
||||
name="${prompt.fieldKey}"
|
||||
?checked=${prompt.initialValue !== ""}
|
||||
?required=${prompt.required}
|
||||
/>
|
||||
<label class="pf-c-check__label" for="${prompt.fieldKey}">${prompt.label}</label>
|
||||
${prompt.required
|
||||
? html`<p class="pf-c-form__helper-text">${msg("Required.")}</p>`
|
||||
: html``}
|
||||
<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>
|
||||
</div>`;
|
||||
return renderCheckbox(prompt);
|
||||
}
|
||||
if (this.shouldRenderInWrapper(prompt)) {
|
||||
|
||||
if (shouldRenderInWrapper(prompt)) {
|
||||
return html`<ak-form-element
|
||||
label="${prompt.label}"
|
||||
?required="${prompt.required}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
|
||||
>
|
||||
${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}
|
||||
${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}
|
||||
</ak-form-element>`;
|
||||
}
|
||||
return html` ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}`;
|
||||
}
|
||||
|
||||
renderContinue(): TemplateResult {
|
||||
return html` <div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Continue")}
|
||||
</button>
|
||||
</div>`;
|
||||
return html` ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
|
@ -119,6 +89,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
|||
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
||||
</ak-empty-state>`;
|
||||
}
|
||||
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
|
@ -137,7 +108,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
|||
this.challenge?.responseErrors?.non_field_errors || [],
|
||||
)
|
||||
: html``}
|
||||
${this.renderContinue()}
|
||||
${renderContinue()}
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
|
|
37
web/src/flow/stages/prompt/helpers.ts
Normal file
37
web/src/flow/stages/prompt/helpers.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
import { PromptTypeEnum, StagePrompt } from "@goauthentik/api";
|
||||
|
||||
import promptRenderers from "./FieldRenderers";
|
||||
|
||||
export function renderPromptInner(prompt: StagePrompt) {
|
||||
const renderer = promptRenderers.get(prompt.type);
|
||||
if (!renderer) {
|
||||
return html`<p>invalid type '${JSON.stringify(prompt.type, null, 2)}'</p>`;
|
||||
}
|
||||
return renderer(prompt);
|
||||
}
|
||||
|
||||
export function renderPromptHelpText(prompt: StagePrompt) {
|
||||
if (prompt.subText === "") {
|
||||
return html``;
|
||||
}
|
||||
return html`<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>`;
|
||||
}
|
||||
|
||||
export function shouldRenderInWrapper(prompt: StagePrompt) {
|
||||
// Special types that aren't rendered in a wrapper
|
||||
const specialTypes = [PromptTypeEnum.Static, PromptTypeEnum.Hidden, PromptTypeEnum.Separator];
|
||||
const special = specialTypes.find((s) => s === prompt.type);
|
||||
return !special;
|
||||
}
|
||||
|
||||
export function renderContinue() {
|
||||
return html` <div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Continue")}
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
Reference in a new issue