web: display tests for CryptoCertificateKeypair search

This adds a Storybook for the CryptoCertificateKeypair search, including
a mock fetch of the data.  In the course of running the tests, we discovered
that including the SearchSelect _class_ won't include the customElement declaration
unless you include the whole file!  Other bugs found: including the CSS from
Storybook is different from that of LitElement native, so much so that the
adapter needed to be included.  FlowSearch had a similar bug.  The problem
only manifests when building via Webpack (which Storybook uses) and not
Rollup, but we should support both in distribution.
This commit is contained in:
Ken Sternberg 2023-08-04 14:06:09 -07:00
parent 911f8e49cc
commit 5d52038afc
5 changed files with 126 additions and 3 deletions

View File

@ -1,6 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/forms/SearchSelect";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { html } from "lit"; import { html } from "lit";
@ -29,7 +30,7 @@ const renderValue = (item: CertificateKeyPair | undefined): string | undefined =
*/ */
@customElement("ak-crypto-certificate-search") @customElement("ak-crypto-certificate-search")
export class CryptoCertificateSearch extends CustomListenerElement(AKElement) { export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement) {
@property({ type: String, reflect: true }) @property({ type: String, reflect: true })
certificate?: string; certificate?: string;
@ -126,4 +127,4 @@ export class CryptoCertificateSearch extends CustomListenerElement(AKElement) {
} }
} }
export default CryptoCertificateSearch; export default AkCryptoCertificateSearch;

View File

@ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/forms/SearchSelect";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { html } from "lit"; import { html } from "lit";

View File

@ -0,0 +1,91 @@
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/messages/MessageContainer";
import { Meta } from "@storybook/web-components";
import { TemplateResult, html } from "lit";
import "../ak-crypto-certificate-search";
import AkCryptoCertificateSearch from "../ak-crypto-certificate-search";
import { dummyCryptoCertsSearch } from "./samples";
const metadata: Meta<AkCryptoCertificateSearch> = {
title: "Components / Searches / CryptoCertificateKeyPair",
component: "ak-crypto-certificate-search",
parameters: {
docs: {
description: {
component: "A search function for cryptographic certificates in Authentik",
},
},
mockData: [
{
url: "/api/v3/crypto/certificatekeypairs/?has_key=true&include_details=false&ordering=name",
method: "GET",
status: 200,
response: dummyCryptoCertsSearch,
},
],
},
argTypes: {
// Typescript is unaware that arguments for components are treated as properties, and
// properties are typically renamed to lower case, even if the variable is not.
// @ts-expect-error
nokey: {
control: "boolean",
description:
"When true, certificates without valid keys will be included in the search",
},
singleton: {
control: "boolean",
description:
"Supports the SAML Source search: when true, if there is no certificate in the current form and there is one and only one certificate in the Authentik database, use that certificate by default.",
},
},
};
export default metadata;
const LIGHT = "pf-t-light";
function injectTheme() {
setTimeout(() => {
if (!document.body.classList.contains(LIGHT)) {
document.body.classList.add(LIGHT);
}
});
}
const container = (testItem: TemplateResult) => {
injectTheme();
return html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<pre id="message-pad" style="margin-top: 1em"></pre>
</div>`;
};
export const CryptoCertificateSearch = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const showMessage = (ev: CustomEvent<any>) => {
const detail = ev.detail;
delete detail["target"];
document.getElementById("message-pad")!.innerText = `Event: ${JSON.stringify(
detail,
null,
2,
)}`;
};
return container(
html` <ak-form-element-horizontal name="test-crypto-certificate-search">
<ak-crypto-certificate-search @ak-change=${showMessage}></ak-crypto-certificate-search
></ak-form-element-horizontal>`,
);
};

View File

@ -0,0 +1,29 @@
export const dummyCryptoCertsSearch = {
pagination: {
next: 0,
previous: 0,
count: 1,
current: 1,
total_pages: 1,
start_index: 1,
end_index: 1,
},
results: [
{
pk: "63efd1b8-6c39-4f65-8157-9a406cb37447",
name: "authentik Self-signed Certificate",
fingerprint_sha256: null,
fingerprint_sha1: null,
cert_expiry: null,
cert_subject: null,
private_key_available: true,
private_key_type: null,
certificate_download_url:
"/api/v3/crypto/certificatekeypairs/63efd1b8-6c39-4f65-8157-9a406cb37447/view_certificate/?download",
private_key_download_url:
"/api/v3/crypto/certificatekeypairs/63efd1b8-6c39-4f65-8157-9a406cb37447/view_private_key/?download",
managed: null,
},
],
};

View File

@ -1,5 +1,6 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { ascii_letters, digits, groupBy, randomString } from "@goauthentik/common/utils"; import { ascii_letters, digits, groupBy, randomString } from "@goauthentik/common/utils";
import { adaptCSS } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { PreventFormSubmit } from "@goauthentik/elements/forms/Form"; import { PreventFormSubmit } from "@goauthentik/elements/forms/Form";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
@ -75,7 +76,7 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
constructor() { constructor() {
super(); super();
if (!document.adoptedStyleSheets.includes(PFDropdown)) { if (!document.adoptedStyleSheets.includes(PFDropdown)) {
document.adoptedStyleSheets = [...document.adoptedStyleSheets, PFDropdown]; document.adoptedStyleSheets = adaptCSS([...document.adoptedStyleSheets, PFDropdown]);
} }
this.dropdownContainer = document.createElement("div"); this.dropdownContainer = document.createElement("div");
this.observer = new IntersectionObserver(() => { this.observer = new IntersectionObserver(() => {