Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

web/admin: better footer links #12004

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions web/src/admin/admin-settings/AdminSettingsFooterLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
import { type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers";

import { msg } from "@lit/localize";
import { css, html } from "lit";
import { customElement, property, queryAll } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";

import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";

import { FooterLink } from "@goauthentik/api";

export interface IFooterLinkInput {
footerLink: FooterLink;
}

@customElement("ak-admin-settings-footer-link")
export class FooterLinkInput extends AkControlElement<FooterLink> {
static get styles() {
return [
PFBase,
PFInputGroup,
PFFormControl,
css`
.pf-c-input-group input#linkname {
flex-grow: 1;
width: 8rem;
}
`,
];
}

@property({ type: Object, attribute: false })
footerLink: FooterLink = {
name: "",
href: "",
};

@queryAll(".ak-form-control")
controls?: HTMLInputElement[];

json() {
return Object.fromEntries(
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
) as unknown as FooterLink;
}

get isValid() {
const href = this.json()?.href ?? "";
return (
(href.substr(0, 7) === "http://" || href.substr(0, 8) === "https://") &&
URL.canParse(href)
);
}

render() {
const onChange = () => {
this.dispatchEvent(new Event("change", { composed: true, bubbles: true }));
};

return html` <div class="pf-c-input-group">
<input
type="text"
@change=${onChange}
value=${this.footerLink.name}
id="linkname"
class="pf-c-form-control ak-form-control"
name="name"
placeholder=${msg("Link Title")}
tabindex="1"
/>
<input
type="text"
@change=${onChange}
value="${ifDefined(this.footerLink.href ?? undefined)}"
class="pf-c-form-control ak-form-control"
required
placeholder=${msg("URL")}
name="href"
tabindex="1"
/>
</div>`;
}
}

export function akFooterLinkInput(properties: IFooterLinkInput) {
return html`<ak-admin-settings-footer-link
${spread(properties as Spread)}
></ak-admin-settings-footer-link>`;
}

declare global {
interface HTMLElementTagNameMap {
"ak-admin-settings-footer-link": FooterLinkInput;
}
}
34 changes: 25 additions & 9 deletions web/src/admin/admin-settings/AdminSettingsForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-array-input.js";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
Expand All @@ -13,13 +12,16 @@ import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";

import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";

import PFList from "@patternfly/patternfly/components/List/list.css";

import { AdminApi, Settings, SettingsRequest } from "@goauthentik/api";
import { AdminApi, FooterLink, Settings, SettingsRequest } from "@goauthentik/api";

import "./AdminSettingsFooterLinks.js";
import { akFooterLinkInput } from "./AdminSettingsFooterLinks.js";

@customElement("ak-admin-settings-form")
export class AdminSettingsForm extends Form<SettingsRequest> {
Expand All @@ -40,7 +42,14 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
private _settings?: Settings;

static get styles(): CSSResult[] {
return super.styles.concat(PFList);
return super.styles.concat(
PFList,
css`
ak-array-input {
width: 100%;
}
`,
);
}

getSuccessMessage(): string {
Expand Down Expand Up @@ -166,10 +175,17 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
>
</ak-text-input>
<ak-form-element-horizontal label=${msg("Footer links")} name="footerLinks">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
.value="${first(this._settings?.footerLinks, [])}"
></ak-codemirror>
<ak-array-input
.items=${this._settings?.footerLinks ?? []}
.newItem=${() => ({ name: "", href: "" })}
.row=${(f?: FooterLink) =>
akFooterLinkInput({
".footerLink": f,
"style": "width: 100%",
"name": "footer-link",
})}
>
</ak-array-input>
<p class="pf-c-form__helper-text">
${msg(
"This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import "@goauthentik/elements/messages/MessageContainer";
import { Meta, StoryObj, WebComponentsRenderer } from "@storybook/web-components";
import { DecoratorFunction } from "storybook/internal/types";

import { html } from "lit";

import { FooterLinkInput } from "../AdminSettingsFooterLinks.js";
import "../AdminSettingsFooterLinks.js";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Decorator = DecoratorFunction<WebComponentsRenderer, any>;

const metadata: Meta<FooterLinkInput> = {
title: "Components / Footer Link Input",
component: "ak-admin-settings-footer-link",
parameters: {
docs: {
description: {
component: "A stylized control for the footer links",
},
},
},
decorators: [
(story: Decorator) => {
window.setTimeout(() => {
const control = document.getElementById("footer-link");
if (!control) {
throw new Error("Test was not initialized correctly.");
}
const messages = document.getElementById("reported-value");
control.addEventListener("change", (event: Event) => {
if (!event.target) {
return;
}
const target = event.target as FooterLinkInput;
messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.isValid ? "Yes" : "No"}`;
});
}, 250);

return html`<div
style="background: #fff; padding: 2em; position: relative"
id="the-main-event"
>
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
#the-answer-block {
padding-top: 3em;
}
</style>
<div>
${
// @ts-expect-error The types for web components are not well-defined }
story()
}
</div>
<div style="margin-top: 2rem">
<p>Reported value:</p>
<pre id="reported-value"></pre>
</div>
</div>`;
},
],
};

export default metadata;

type Story = StoryObj;

export const Default: Story = {
render: () =>
html` <ak-admin-settings-footer-link
id="footer-link"
name="the-footer"
></ak-admin-settings-footer-link>`,
};
8 changes: 6 additions & 2 deletions web/src/elements/AkControlElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import { AKElement } from "./Base";
* extracting the value.
*
*/
export class AkControlElement extends AKElement {
export class AkControlElement<T = string | string[]> extends AKElement {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better type controls led to better type checking.

constructor() {
super();
this.dataset.akControl = "true";
}

json() {
json(): T {
throw new Error("Controllers using this protocol must override this method");
}

get isValid(): boolean {
return true;
}
}
Loading
Loading