web: Replace ad-hoc toggle control with ak-toggle-group (#6470)
* web: Replace ad-hoc toggle control with ak-toggle-group This commit replaces various ad-hoc implementations of the Patternfly Toggle Group HTML with a web component that encapsulates all of the needed behavior and exposes a single API with a single event handler, return the value of the option clicked. The results are: Lots of visual clutter is eliminated. A single link of: ``` <div class="pf-c-toggle-group__item"> <button class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy ? "pf-m-selected" : ""}" type="button" @click=${() => { this.mode = ProxyMode.Proxy; }}> <span class="pf-c-toggle-group__text">${msg("Proxy")}</span> </button> </div> <div class="pf-c-divider pf-m-vertical" role="separator"></div> ``` Now looks like: ``` <option value=${ProxyMode.Proxy}>${msg("Proxy")}</option> ``` This also means that the three pages that used the Patternfly Toggle Group could eliminate all of their Patternfly PFToggleGroup needs, as well as the `justify-content: center` extension, which also eliminated the `css` import. The savings aren't as spectacular as I'd hoped: removed 178 lines, but added 123; total savings 55 lines of code. I still count this a win: we need never write another toggle component again, and any bugs, extensions or features we may want to add can be centralized or forked without risking the whole edifice. * web: minor code formatting issue. * web: adding a storybook for the ak-toggle-group component * Bugs found by CI/CD. * web: Replace ad-hoc search for CryptoCertificateKeyPairs with crypto-certificate-search (#6475) * web: Replace ad-hoc search for CryptoCertificateKeyPairs with ak-crypto-certeficate-search This commit replaces various ad-hoc implementations of `search-select` for CryptoCertificateKeyPairs with a web component that encapsulates all of the needed behavior and exposes a single API. The results are: Lots of visual clutter is eliminated. A single search of: ```HTML <ak-search-select .fetchObjects=${async (query?: string): Promise<CertificateKeyPair[]> => { const args: CryptoCertificatekeypairsListRequest = { ordering: "name", hasKey: true, includeDetails: false, }; if (query !== undefined) { args.search = query; } const certificates = await new CryptoApi( DEFAULT_CONFIG, ).cryptoCertificatekeypairsList(args); return certificates.results; }} .renderElement=${(item: CertificateKeyPair): string => { return item.name; }} .value=${(item: CertificateKeyPair | undefined): string | undefined => { return item?.pk; }} .selected=${(item: CertificateKeyPair): boolean => { return this.instance?.tlsVerification === item.pk; }} ?blankable=${true} > </ak-search-select> ``` Now looks like: ```HTML <ak-crypto-certificate-search certificate=${this.instance?.tlsVerification}> </ak-crypto-certificate-search> ``` There are three searches that do not require there to be a valid key with the certificate; these are supported with the boolean property `nokey`; likewise, there is one search (in SAMLProviderForm) that states that if there is no current certificate in the SAMLProvider and only one certificate can be found in the Authentik database, use that one; this is supported with the boolean property `singleton`. These changes replace 382 lines of object-oriented invocations with 36 lines of declarative configuration, and 98 lines for the class. Overall, the code for "find a crypto certificate" has been reduced by 46%. Suggestions for a better word than `singleton` are welcome! * 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:
parent
30cb38ac6d
commit
f5394da9f7
|
@ -9,12 +9,11 @@ import "@goauthentik/elements/forms/SearchSelect";
|
|||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
|
||||
|
||||
import { BlueprintFile, BlueprintInstance, ManagedApi } from "@goauthentik/api";
|
||||
|
||||
|
@ -51,15 +50,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
|
|||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(
|
||||
PFToggleGroup,
|
||||
PFContent,
|
||||
css`
|
||||
.pf-c-toggle-group {
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
);
|
||||
return [...super.styles, PFContent];
|
||||
}
|
||||
|
||||
async send(data: BlueprintInstance): Promise<BlueprintInstance> {
|
||||
|
@ -105,52 +96,16 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
|
|||
</ak-form-element-horizontal>
|
||||
<div class="pf-c-card pf-m-selectable pf-m-selected">
|
||||
<div class="pf-c-card__body">
|
||||
<div class="pf-c-toggle-group">
|
||||
<div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="pf-c-toggle-group__button ${this.source ===
|
||||
blueprintSource.file
|
||||
? "pf-m-selected"
|
||||
: ""}"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.source = blueprintSource.file;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text">${msg("Local path")}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
|
||||
<div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="pf-c-toggle-group__button ${this.source ===
|
||||
blueprintSource.oci
|
||||
? "pf-m-selected"
|
||||
: ""}"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.source = blueprintSource.oci;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text">${msg("OCI Registry")}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
|
||||
<div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="pf-c-toggle-group__button ${this.source ===
|
||||
blueprintSource.internal
|
||||
? "pf-m-selected"
|
||||
: ""}"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.source = blueprintSource.internal;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text">${msg("Internal")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ak-toggle-group
|
||||
value=${this.source}
|
||||
@ak-toggle=${(ev: CustomEvent<{ value: blueprintSource }>) => {
|
||||
this.source = ev.detail.value;
|
||||
}}
|
||||
>
|
||||
<option value=${blueprintSource.file}>${msg("Local path")}</option>
|
||||
<option value=${blueprintSource.oci}>${msg("OCI Registry")}</option>
|
||||
<option value=${blueprintSource.internal}>${msg("Internal")}</option>
|
||||
</ak-toggle-group>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
${this.source === blueprintSource.file
|
||||
|
|
130
web/src/admin/common/ak-crypto-certificate-search.ts
Normal file
130
web/src/admin/common/ak-crypto-certificate-search.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { property, query } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
const renderElement = (item: CertificateKeyPair): string => item.name;
|
||||
|
||||
const renderValue = (item: CertificateKeyPair | undefined): string | undefined => item?.pk;
|
||||
|
||||
/**
|
||||
* Cryptographic Certificate Search
|
||||
*
|
||||
* @element ak-crypto-certificate-search
|
||||
*
|
||||
* A wrapper around SearchSelect for the many searches of cryptographic key-pairs used throughout our
|
||||
* code base. This is another one of those "If it's not error-free, at least it's localized to one
|
||||
* place" issues.
|
||||
*
|
||||
*/
|
||||
|
||||
@customElement("ak-crypto-certificate-search")
|
||||
export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement) {
|
||||
@property({ type: String, reflect: true })
|
||||
certificate?: string;
|
||||
|
||||
@query("ak-search-select")
|
||||
search!: SearchSelect<CertificateKeyPair>;
|
||||
|
||||
@property({ type: String })
|
||||
name: string | null | undefined;
|
||||
|
||||
/**
|
||||
* Set to `true` if you want to find pairs that don't have a valid key. Of our 14 searches, 11
|
||||
* require the key, 3 do not (as of 2023-08-01).
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "nokey" })
|
||||
noKey = false;
|
||||
|
||||
/**
|
||||
* Set this to true if, should there be only one certificate available, you want the system to
|
||||
* use it by default.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "singleton" })
|
||||
singleton = false;
|
||||
|
||||
selectedKeypair?: CertificateKeyPair;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
this.fetchObjects = this.fetchObjects.bind(this);
|
||||
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
|
||||
this.addCustomListener("ak-change", this.handleSearchUpdate);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.selectedKeypair ? renderValue(this.selectedKeypair) : undefined;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const horizontalContainer = this.closest("ak-form-element-horizontal[name]");
|
||||
if (!horizontalContainer) {
|
||||
throw new Error("This search can only be used in a named ak-form-element-horizontal");
|
||||
}
|
||||
const name = horizontalContainer.getAttribute("name");
|
||||
const myName = this.getAttribute("name");
|
||||
if (name !== null && name !== myName) {
|
||||
this.setAttribute("name", name);
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchUpdate(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this.selectedKeypair = ev.detail.value;
|
||||
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
async fetchObjects(query?: string): Promise<CertificateKeyPair[]> {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: !this.noKey,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList(
|
||||
args,
|
||||
);
|
||||
return certificates.results;
|
||||
}
|
||||
|
||||
selected(item: CertificateKeyPair, items: CertificateKeyPair[]) {
|
||||
return (
|
||||
(this.singleton && !this.certificate && items.length === 1) ||
|
||||
(!!this.certificate && this.certificate === item.pk)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ak-search-select
|
||||
.fetchObjects=${this.fetchObjects}
|
||||
.renderElement=${renderElement}
|
||||
.value=${renderValue}
|
||||
.selected=${this.selected}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkCryptoCertificateSearch;
|
|
@ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
|||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { html } from "lit";
|
||||
|
|
|
@ -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>`,
|
||||
);
|
||||
};
|
29
web/src/admin/common/stories/samples.ts
Normal file
29
web/src/admin/common/stories/samples.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -1,3 +1,4 @@
|
|||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
@ -9,13 +10,7 @@ import { TemplateResult, html } from "lit";
|
|||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
DockerServiceConnection,
|
||||
OutpostsApi,
|
||||
} from "@goauthentik/api";
|
||||
import { DockerServiceConnection, OutpostsApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-service-connection-docker-form")
|
||||
export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnection, string> {
|
||||
|
@ -93,33 +88,9 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
|
|||
label=${msg("TLS Verification Certificate")}
|
||||
name="tlsVerification"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return this.instance?.tlsVerification === item.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.tlsVerification}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"CA which the endpoint's Certificate is verified against. Can be left empty for no validation.",
|
||||
|
@ -130,33 +101,9 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
|
|||
label=${msg("TLS Authentication Certificate/SSH Keypair")}
|
||||
name="tlsAuthentication"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return this.instance?.tlsAuthentication === item.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.tlsAuthentication}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Certificate/Key used for authentication. Can be left empty for no authentication.",
|
||||
|
|
|
@ -5,12 +5,11 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
|||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { CSSResult } from "lit";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
|
@ -70,15 +69,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
|
|||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(
|
||||
PFToggleGroup,
|
||||
PFContent,
|
||||
css`
|
||||
.pf-c-toggle-group {
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
);
|
||||
return [...super.styles, PFContent];
|
||||
}
|
||||
|
||||
send(data: PolicyBinding): Promise<unknown> {
|
||||
|
@ -112,55 +103,22 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
|
|||
}
|
||||
|
||||
renderModeSelector(): TemplateResult {
|
||||
return html` <div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="pf-c-toggle-group__button ${this.policyGroupUser === target.policy
|
||||
? "pf-m-selected"
|
||||
: ""}"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.policyGroupUser = target.policy;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text">${msg("Policy")}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
|
||||
<div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="pf-c-toggle-group__button ${this.policyGroupUser === target.group
|
||||
? "pf-m-selected"
|
||||
: ""}"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.policyGroupUser = target.group;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text">${msg("Group")}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
|
||||
<div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="pf-c-toggle-group__button ${this.policyGroupUser === target.user
|
||||
? "pf-m-selected"
|
||||
: ""}"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.policyGroupUser = target.user;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text">${msg("User")}</span>
|
||||
</button>
|
||||
</div>`;
|
||||
return html` <ak-toggle-group
|
||||
value=${this.policyGroupUser}
|
||||
@ak-toggle=${(ev: CustomEvent<{ value: target }>) => {
|
||||
this.policyGroupUser = ev.detail.value;
|
||||
}}
|
||||
>
|
||||
<option value=${target.policy}>${msg("Policy")}</option>
|
||||
<option value=${target.group}>${msg("Group")}</option>
|
||||
<option value=${target.user}>${msg("User")}</option>
|
||||
</ak-toggle-group>`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<div class="pf-c-card pf-m-selectable pf-m-selected">
|
||||
<div class="pf-c-card__body">
|
||||
<div class="pf-c-toggle-group">${this.renderModeSelector()}</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">${this.renderModeSelector()}</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy")}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
|
@ -14,11 +15,8 @@ import { customElement } from "lit/decorators.js";
|
|||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
Group,
|
||||
LDAPAPIAccessMode,
|
||||
|
@ -208,35 +206,9 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
|||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return item.pk === this.instance?.certificate;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.certificate}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
|
@ -14,10 +15,7 @@ import { customElement, state } from "lit/decorators.js";
|
|||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
ClientTypeEnum,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
IssuerModeEnum,
|
||||
OAuth2Provider,
|
||||
|
@ -206,42 +204,10 @@ ${this.instance?.redirectUris}</textarea
|
|||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(
|
||||
item: CertificateKeyPair,
|
||||
items: CertificateKeyPair[],
|
||||
): boolean => {
|
||||
let selected = this.instance?.signingKey === item.pk;
|
||||
if (!this.instance && items.length === 1) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.signingKey}
|
||||
singleton
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-toggle-group";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
|
@ -8,20 +10,16 @@ import "@goauthentik/elements/forms/SearchSelect";
|
|||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { CSSResult } from "lit";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
|
||||
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PaginatedOAuthSourceList,
|
||||
PaginatedScopeMappingList,
|
||||
|
@ -35,17 +33,7 @@ import {
|
|||
@customElement("ak-provider-proxy-form")
|
||||
export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(
|
||||
PFToggleGroup,
|
||||
PFContent,
|
||||
PFList,
|
||||
PFSpacing,
|
||||
css`
|
||||
.pf-c-toggle-group {
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
);
|
||||
return [...super.styles, PFContent, PFList, PFSpacing];
|
||||
}
|
||||
|
||||
async loadInstance(pk: number): Promise<ProxyProvider> {
|
||||
|
@ -137,51 +125,18 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
|||
}
|
||||
|
||||
renderModeSelector(): TemplateResult {
|
||||
return html` <div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy
|
||||
? "pf-m-selected"
|
||||
: ""}"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.mode = ProxyMode.Proxy;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text">${msg("Proxy")}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
|
||||
<div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardSingle
|
||||
? "pf-m-selected"
|
||||
: ""}"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.mode = ProxyMode.ForwardSingle;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text"
|
||||
>${msg("Forward auth (single application)")}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
|
||||
<div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardDomain
|
||||
? "pf-m-selected"
|
||||
: ""}"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.mode = ProxyMode.ForwardDomain;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text"
|
||||
>${msg("Forward auth (domain level)")}</span
|
||||
>
|
||||
</button>
|
||||
</div>`;
|
||||
const setMode = (ev: CustomEvent<{ value: ProxyMode }>) => {
|
||||
this.mode = ev.detail.value;
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<ak-toggle-group value=${this.mode} @ak-toggle=${setMode}>
|
||||
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
|
||||
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
|
||||
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>
|
||||
</ak-toggle-group>
|
||||
`;
|
||||
}
|
||||
|
||||
renderSettings(): TemplateResult {
|
||||
|
@ -362,9 +317,7 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
|||
</ak-form-element-horizontal>
|
||||
|
||||
<div class="pf-c-card pf-m-selectable pf-m-selected">
|
||||
<div class="pf-c-card__body">
|
||||
<div class="pf-c-toggle-group">${this.renderModeSelector()}</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">${this.renderModeSelector()}</div>
|
||||
<div class="pf-c-card__footer">${this.renderSettings()}</div>
|
||||
</div>
|
||||
<ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity">
|
||||
|
@ -383,35 +336,9 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
|||
<span slot="header">${msg("Advanced protocol settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return item.pk === this.instance?.certificate;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.certificate}
|
||||
></ak-crypto-certificate-search>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Additional scopes")}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
|
@ -13,9 +14,6 @@ import { customElement } from "lit/decorators.js";
|
|||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
DigestAlgorithmEnum,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PaginatedSAMLPropertyMappingList,
|
||||
|
@ -177,35 +175,9 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
|
|||
label=${msg("Signing Certificate")}
|
||||
name="signingKp"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return item.pk === this.instance?.signingKp;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.signingKp}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Certificate used to sign outgoing Responses going to the Service Provider.",
|
||||
|
@ -216,41 +188,16 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
|
|||
label=${msg("Verification Certificate")}
|
||||
name="verificationKp"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return item.pk === this.instance?.verificationKp;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.verificationKp}
|
||||
nokey
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
?required=${true}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import { placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
|
@ -12,11 +13,8 @@ import { customElement } from "lit/decorators.js";
|
|||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
Group,
|
||||
LDAPSource,
|
||||
LDAPSourceRequest,
|
||||
|
@ -208,34 +206,10 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
|
|||
label=${msg("TLS Verification Certificate")}
|
||||
name="peerCertificate"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return item.pk === this.instance?.peerCertificate;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.peerCertificate}
|
||||
nokey
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate.",
|
||||
|
@ -246,35 +220,9 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
|
|||
label=${msg("TLS Client authentication certificate")}
|
||||
name="clientCertificate"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return item.pk === this.instance?.clientCertificate;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.clientCertificate}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Client certificate keypair to authenticate against the LDAP Server's Certificate.",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-source-flow-search";
|
||||
import { iconHelperText, placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
|
||||
|
@ -18,9 +19,6 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
|||
import {
|
||||
BindingTypeEnum,
|
||||
CapabilitiesEnum,
|
||||
CertificateKeyPair,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
DigestAlgorithmEnum,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
NameIdPolicyEnum,
|
||||
|
@ -274,35 +272,9 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
|||
</ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Signing keypair")} name="signingKp">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return item.pk === this.instance?.signingKp;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.signingKp}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Keypair which is used to sign outgoing requests. Leave empty to disable signing.",
|
||||
|
@ -313,34 +285,10 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
|||
label=${msg("Verification Certificate")}
|
||||
name="verificationKp"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return item.pk === this.instance?.verificationKp;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.verificationKp}
|
||||
nokey
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
|
@ -13,14 +14,7 @@ import { msg } from "@lit/localize";
|
|||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
CoreApi,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
Tenant,
|
||||
} from "@goauthentik/api";
|
||||
import { CoreApi, FlowsInstancesListDesignationEnum, Tenant } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-tenant-form")
|
||||
export class TenantForm extends ModelForm<Tenant, string> {
|
||||
|
@ -236,35 +230,9 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
label=${msg("Web Certificate")}
|
||||
name="webCertificate"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const certificates = await new CryptoApi(
|
||||
DEFAULT_CONFIG,
|
||||
).cryptoCertificatekeypairsList(args);
|
||||
return certificates.results;
|
||||
}}
|
||||
.renderElement=${(item: CertificateKeyPair): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: CertificateKeyPair): boolean => {
|
||||
return item.pk === this.instance?.webCertificate;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${this.instance?.webCertificate}
|
||||
></ak-crypto-certificate-search>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Event retention")}
|
||||
|
|
90
web/src/components/ak-toggle-group.ts
Normal file
90
web/src/components/ak-toggle-group.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
|
||||
|
||||
type Pair = [string, string];
|
||||
|
||||
/**
|
||||
* Toggle Group
|
||||
*
|
||||
* An implementation of the Patternfly Toggle Group as a LitElement
|
||||
*
|
||||
* @element ak-toggle-group
|
||||
*
|
||||
* @fires ak-toggle - Fired when someone clicks on a toggle option. Carries the value of the option.
|
||||
*/
|
||||
|
||||
// MYNIS:
|
||||
// A 'name' property so that the event carries *which* toggle group emitted the event.
|
||||
|
||||
@customElement("ak-toggle-group")
|
||||
export class AkToggleGroup extends CustomEmitterElement(AKElement) {
|
||||
static get styles() {
|
||||
return [
|
||||
PFToggleGroup,
|
||||
css`
|
||||
.pf-c-toggle-group {
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* The value (causes highlighting, value is returned)
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
value = "";
|
||||
|
||||
get rawOptions(): HTMLOptionElement[] {
|
||||
return Array.from(this.querySelectorAll("option") ?? []);
|
||||
}
|
||||
|
||||
get options(): Pair[] {
|
||||
return Array.from(this.rawOptions).map(
|
||||
(option: HTMLOptionElement): Pair => [
|
||||
option.getAttribute("value") ?? "",
|
||||
option.textContent ?? "",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const last = this.options.length - 1;
|
||||
const mkClass = (v: string) => ({
|
||||
"pf-c-toggle-group__button": true,
|
||||
"pf-m-selected": this.value === v,
|
||||
});
|
||||
|
||||
const mkClick = (v: string) => () => {
|
||||
this.dispatchCustomEvent("ak-toggle", { value: v });
|
||||
};
|
||||
|
||||
return html` <div class="pf-c-toggle-group">
|
||||
${this.options.map(
|
||||
([key, label], idx) =>
|
||||
html`<div class="pf-c-toggle-group__item">
|
||||
<button
|
||||
class="${classMap(mkClass(key))}"
|
||||
type="button"
|
||||
@click=${mkClick(key)}
|
||||
>
|
||||
<span class="pf-c-toggle-group__text">${label}</span>
|
||||
</button>
|
||||
</div>
|
||||
${idx < last
|
||||
? html`<div class="pf-c-divider pf-m-vertical" role="separator"></div>`
|
||||
: nothing} `,
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkToggleGroup;
|
67
web/src/components/stories/ak-toggle-group.stories.ts
Normal file
67
web/src/components/stories/ak-toggle-group.stories.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import "../ak-toggle-group";
|
||||
import AkToggleGroup from "../ak-toggle-group";
|
||||
|
||||
const metadata: Meta<AkToggleGroup> = {
|
||||
title: "Components / Toggle Group",
|
||||
component: "ak-toggle-group",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A stylized toggle control",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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>
|
||||
${testItem}
|
||||
<ul id="toggle-message-pad" style="margin-top: 1em"></ul>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
const testOptions = [
|
||||
["funky", "Option One: The Funky One"],
|
||||
["invalid", "Option Two: The Invalid One"],
|
||||
["weird", "Option Three: The Weird One"],
|
||||
];
|
||||
|
||||
export const ToggleGroup = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById(
|
||||
"toggle-message-pad",
|
||||
)!.innerText = `Value selected: ${ev.detail.value}`;
|
||||
};
|
||||
|
||||
return container(
|
||||
html`<ak-toggle-group @ak-toggle=${displayChange}>
|
||||
${testOptions.map(([key, label]) => html`<option value="${key}">${label}</option>`)}
|
||||
</ak-toggle-group>`,
|
||||
);
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { ascii_letters, digits, groupBy, randomString } from "@goauthentik/common/utils";
|
||||
import { adaptCSS } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { PreventFormSubmit } from "@goauthentik/elements/forms/Form";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
@ -75,7 +76,7 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
|||
constructor() {
|
||||
super();
|
||||
if (!document.adoptedStyleSheets.includes(PFDropdown)) {
|
||||
document.adoptedStyleSheets = [...document.adoptedStyleSheets, PFDropdown];
|
||||
document.adoptedStyleSheets = adaptCSS([...document.adoptedStyleSheets, PFDropdown]);
|
||||
}
|
||||
this.dropdownContainer = document.createElement("div");
|
||||
this.observer = new IntersectionObserver(() => {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"@goauthentik/app/*": ["src/*"],
|
||||
"@goauthentik/admin/*": ["src/admin/*"],
|
||||
"@goauthentik/common/*": ["src/common/*"],
|
||||
"@goauthentik/components/*": ["src/components/*"],
|
||||
"@goauthentik/docs/*": ["../website/docs/*"],
|
||||
"@goauthentik/elements/*": ["src/elements/*"],
|
||||
"@goauthentik/flow/*": ["src/flow/*"],
|
||||
|
|
Reference in a new issue