Repository: goauthentik/authentik
File: web/src/components/ak-object-attributes-card.ts (TypeScript)

Code before:
@customElement("ak-object-attributes-card")
export class ObjectAttributesCard extends AKElement {
    static styles: CSSResult[] = [PFCard, PFDescriptionList];

    @property({ attribute: false })
    objectAttributes: Record<string, unknown> = {};

    @property({ type: Boolean, attribute: "exclude-notes" })
    excludeNotes = true;

    /**
     * Filters the attributes to only include custom (non-system) attributes.
     * Excludes keys starting with "goauthentik.io/" and optionally the "notes" key.
     */
    private get customAttributes(): Array<[string, unknown]> {
        return Object.entries(this.objectAttributes || {}).filter(([key]) => {
            if (key.startsWith("goauthentik.io/")) return false;
            if (this.excludeNotes && key === "notes") return false;
            return true;
        });
    }

    /**
     * Formats a value for display based on its type.
     */
    private formatValue(value: unknown): TemplateResult | string {
        if (value === null || value === undefined) {
            return "-";
        }

        if (typeof value === "boolean") {
            return html`<ak-status-label ?good=${value}></ak-status-label>`;
        }

        if (typeof value === "string" || typeof value === "number") {
            return String(value);
        }

        if (Array.isArray(value)) {
            // Simple arrays of primitives: render as comma-separated list
            if (value.every((item) => typeof item === "string" || typeof item === "number")) {
                return value.join(", ");
            }
            // Complex arrays: render as JSON
            return html`<code>
                <pre style="margin: 0; white-space: pre-wrap;">
${JSON.stringify(value, null, 2)}</pre
                >
            </code>`;
        }


Review comment (bug):
This method never calls `this`, so it's a function, not a method. Every time you create this object, you create a copy of this function despite the fact that it never changes, which wastes memory and time. (Not a lot, I know). To avoid that, move it out of the class and make it a standalone function elsewhere in the file, then call it without `this`.  (Also, dereferencing `this` for something that never uses it wastes CPU.)

It's also one long conditional with some repetition.  We have an excellent conditional matching library.  Consider (warning, this is a rough draft off the top of my head before I've finished my first cup of coffee; it may be incomplete or buggy):

```
import { match, P, isMatching } from "ts-pattern";

function formatValue(value: unknown): TemplateResult | string {
    const jsonify = (s: Record<string | number | symbol, unknown> | Array<unknown>) =>
        html`<code>
            <pre style="margin: 0; white-space: pre-wrap;">${JSON.stringify(value, null, 2)}</pre>
        </code>`;

    const Stringable = P.union(P.string, P.number);
    const isStringable = (s: unknown) => isMatching(Stringable, s);
    const isStringableArray = (a: Array<unknown>) => a.every(isStringable);

    match(value)
        .with(P.nullish, () => "-")
        .with(P.boolean, (v) => html`<ak-status-label ?good=${v}></ak-status-label>`)
        .with(Stringable, (v) => String(v))
        .with(P.array, (v) => (isStringableArray(v) ? v.join(", ") : jsonify(v)))
        .with(P.object, (v) => jsonify(v))
        .otherwise((v) => String(v));
}
```



Code after:
    const isStringable = (s: unknown) => isMatching(Stringable, s);
    const isStringableArray = (a: unknown[]) => a.every(isStringable);

    return match(value)
        .with(P.nullish, () => "-")
        .with(P.boolean, (v) => html`<ak-status-label ?good=${v}></ak-status-label>`)
        .with(Stringable, (v) => String(v))
        .with(P.array(), (v) => (isStringableArray(v) ? v.join(", ") : jsonify(v)))
        .with(P.record(P.string, P.any), (v) => jsonify(v))
        .otherwise((v) => String(v));
}

/**
 * A reusable card component to display custom attributes for objects like User, Group, or Device.
 *
 * This component filters out system attributes (keys starting with `goauthentik.io/`)
 * and optionally excludes the `notes` attribute (since it's typically displayed separately).
 *
 * Value types are rendered appropriately:
 * - string/number: Plain text
 * - boolean: ak-status-label component
 * - simple arrays: Comma-separated list
 * - objects/complex arrays: Formatted JSON in code block
 */
@customElement("ak-object-attributes-card")
export class ObjectAttributesCard extends AKElement {
    static styles: CSSResult[] = [PFCard, PFDescriptionList];

    @property({ attribute: false })
    objectAttributes: Record<string, unknown> = {};

    @property({ type: Boolean, attribute: "exclude-notes" })
    excludeNotes = true;

    /**
     * Filters the attributes to only include custom (non-system) attributes.
     * Excludes keys starting with "goauthentik.io/" and optionally the "notes" key.
     */
    private get customAttributes(): Array<[string, unknown]> {
        return Object.entries(this.objectAttributes || {}).filter(([key]) => {
            if (key.startsWith("goauthentik.io/")) return false;
            if (this.excludeNotes && key === "notes") return false;
            return true;
        });
    }

    render() {
        const attrs = this.customAttributes;

        return html`
            <div class="pf-c-card__title">${msg("Custom Attributes")}</div>