web/admin: Text and Textarea Fields that "hide" their contents until prompted (#15024)
* web: Add InvalidationFlow to Radius Provider dialogues
## What
- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
- Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`
## Note
Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.
* This (temporary) change is needed to prevent the unit tests from failing.
\# What
\# Why
\# How
\# Designs
\# Test Steps
\# Other Notes
* Revert "This (temporary) change is needed to prevent the unit tests from failing."
This reverts commit dddde09be5
.
* web/admin: Provide `hidden` text and textarea components
## Details
This commit provides two new elements (technically, since they're API-unaware), one for `<input
type="text">`, and one for `<textarea>`, that provide for the ability to create fields that are (or
can be) hidden. A new boolean attribute, `revealed`, shows the state of the component (the content
is therefore *not* revealed by default).
It also includes a third new element, `ak-visibility-toggle`, that creates a hide/show toggle with
all the right icons, styling, and eventing. It's straightforward, and isolating it improved the
DX of everything that uses that feature by quite a bit.
Storybook stories (with autodoc documentation) have been provided for `ak-hidden-text-input`,
`ak-hidden-textarea-input`, and `ak-visibility-toggle`.
## Maintenance Notice
As a maintenance detail, the field `ak-private-text` has been renamed `ak-secret-text` to reflect
its usage, and the places where it was used have all been changed to reflect that update.
* web/component: embed styling (for now) to handle the lightDom/shadowDom/slot conflicts in HorizontalLightComponent and HorizontalFormElement
* Comments and Types. I really shouldn't have to catch this stuff with my eyeballs.
* fix typo
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---------
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||||
@ -46,7 +46,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-private-textarea-input
|
<ak-secret-textarea-input
|
||||||
label=${msg("Certificate")}
|
label=${msg("Certificate")}
|
||||||
name="certificateData"
|
name="certificateData"
|
||||||
input-hint="code"
|
input-hint="code"
|
||||||
@ -54,8 +54,8 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
|||||||
required
|
required
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
help=${msg("PEM-encoded Certificate data.")}
|
help=${msg("PEM-encoded Certificate data.")}
|
||||||
></ak-private-textarea-input>
|
></ak-secret-textarea-input>
|
||||||
<ak-private-textarea-input
|
<ak-secret-textarea-input
|
||||||
label=${msg("Private Key")}
|
label=${msg("Private Key")}
|
||||||
name="keyData"
|
name="keyData"
|
||||||
input-hint="code"
|
input-hint="code"
|
||||||
@ -63,7 +63,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
|||||||
help=${msg(
|
help=${msg(
|
||||||
"Optional Private Key. If this is set, you can use this keypair for encryption.",
|
"Optional Private Key. If this is set, you can use this keypair for encryption.",
|
||||||
)}
|
)}
|
||||||
></ak-private-textarea-input>`;
|
></ak-secret-textarea-input>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
|
||||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||||
@ -62,13 +62,13 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
|
|||||||
value="${ifDefined(this.installID)}"
|
value="${ifDefined(this.installID)}"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-private-textarea-input
|
<ak-secret-textarea-input
|
||||||
name="key"
|
name="key"
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
label=${msg("License key")}
|
label=${msg("License key")}
|
||||||
input-hint="code"
|
input-hint="code"
|
||||||
>
|
>
|
||||||
</ak-private-textarea-input>`;
|
</ak-secret-textarea-input>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@ import {
|
|||||||
UserMatchingModeToLabel,
|
UserMatchingModeToLabel,
|
||||||
} from "@goauthentik/admin/sources/oauth/utils";
|
} from "@goauthentik/admin/sources/oauth/utils";
|
||||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-private-text-input.js";
|
import "@goauthentik/components/ak-secret-text-input.js";
|
||||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
import "@goauthentik/components/ak-text-input";
|
import "@goauthentik/components/ak-text-input";
|
||||||
import "@goauthentik/components/ak-textarea-input";
|
import "@goauthentik/components/ak-textarea-input";
|
||||||
@ -248,22 +248,22 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
|||||||
value=${ifDefined(this.instance?.syncPrincipal)}
|
value=${ifDefined(this.instance?.syncPrincipal)}
|
||||||
help=${msg("Principal used to authenticate to the KDC for syncing.")}
|
help=${msg("Principal used to authenticate to the KDC for syncing.")}
|
||||||
></ak-text-input>
|
></ak-text-input>
|
||||||
<ak-private-text-input
|
<ak-secret-text-input
|
||||||
name="syncPassword"
|
name="syncPassword"
|
||||||
label=${msg("Sync password")}
|
label=${msg("Sync password")}
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
help=${msg(
|
help=${msg(
|
||||||
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
|
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
|
||||||
)}
|
)}
|
||||||
></ak-private-text-input>
|
></ak-secret-text-input>
|
||||||
<ak-private-textarea-input
|
<ak-secret-textarea-input
|
||||||
name="syncKeytab"
|
name="syncKeytab"
|
||||||
label=${msg("Sync keytab")}
|
label=${msg("Sync keytab")}
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
help=${msg(
|
help=${msg(
|
||||||
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||||
)}
|
)}
|
||||||
></ak-private-textarea-input>
|
></ak-secret-textarea-input>
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="syncCcache"
|
name="syncCcache"
|
||||||
label=${msg("Sync credentials cache")}
|
label=${msg("Sync credentials cache")}
|
||||||
@ -285,14 +285,14 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
|||||||
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain",
|
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain",
|
||||||
)}
|
)}
|
||||||
></ak-text-input>
|
></ak-text-input>
|
||||||
<ak-private-textarea-input
|
<ak-secret-textarea-input
|
||||||
name="spnegoKeytab"
|
name="spnegoKeytab"
|
||||||
label=${msg("SPNEGO keytab")}
|
label=${msg("SPNEGO keytab")}
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
help=${msg(
|
help=${msg(
|
||||||
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||||
)}
|
)}
|
||||||
></ak-private-textarea-input>
|
></ak-secret-textarea-input>
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="spnegoCcache"
|
name="spnegoCcache"
|
||||||
label=${msg("SPNEGO credentials cache")}
|
label=${msg("SPNEGO credentials cache")}
|
||||||
|
@ -2,7 +2,7 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
|||||||
import { placeholderHelperText } from "@goauthentik/admin/helperText";
|
import { placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||||
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
|
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-private-text-input.js";
|
import "@goauthentik/components/ak-secret-text-input.js";
|
||||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
@ -260,11 +260,11 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
|
|||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-private-text-input
|
<ak-secret-text-input
|
||||||
label=${msg("Bind Password")}
|
label=${msg("Bind Password")}
|
||||||
name="bindPassword"
|
name="bindPassword"
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
></ak-private-text-input>
|
></ak-secret-text-input>
|
||||||
<ak-form-element-horizontal label=${msg("Base DN")} required name="baseDn">
|
<ak-form-element-horizontal label=${msg("Base DN")} required name="baseDn">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -8,8 +8,8 @@ import {
|
|||||||
UserMatchingModeToLabel,
|
UserMatchingModeToLabel,
|
||||||
} from "@goauthentik/admin/sources/oauth/utils";
|
} from "@goauthentik/admin/sources/oauth/utils";
|
||||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
|
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||||
@ -441,14 +441,14 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
|||||||
/>
|
/>
|
||||||
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
|
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-private-textarea-input
|
<ak-secret-textarea-input
|
||||||
label=${msg("Consumer secret")}
|
label=${msg("Consumer secret")}
|
||||||
name="consumerSecret"
|
name="consumerSecret"
|
||||||
input-hint="code"
|
input-hint="code"
|
||||||
help=${msg("Also known as Client Secret.")}
|
help=${msg("Also known as Client Secret.")}
|
||||||
required
|
required
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
></ak-private-textarea-input>
|
></ak-secret-textarea-input>
|
||||||
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-private-text-input.js";
|
import "@goauthentik/components/ak-secret-text-input.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/forms/SearchSelect";
|
import "@goauthentik/elements/forms/SearchSelect";
|
||||||
@ -95,13 +95,13 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-private-text-input
|
<ak-secret-text-input
|
||||||
name="clientSecret"
|
name="clientSecret"
|
||||||
label=${msg("Secret key")}
|
label=${msg("Secret key")}
|
||||||
input-hint="code"
|
input-hint="code"
|
||||||
required
|
required
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
></ak-private-text-input>
|
></ak-secret-text-input>
|
||||||
</div>
|
</div>
|
||||||
</ak-form-group>
|
</ak-form-group>
|
||||||
<ak-form-group>
|
<ak-form-group>
|
||||||
@ -125,12 +125,12 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-private-text-input
|
<ak-secret-text-input
|
||||||
name="adminSecretKey"
|
name="adminSecretKey"
|
||||||
label=${msg("Secret key")}
|
label=${msg("Secret key")}
|
||||||
input-hint="code"
|
input-hint="code"
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
></ak-private-text-input>
|
></ak-secret-text-input>
|
||||||
</div>
|
</div>
|
||||||
</ak-form-group>
|
</ak-form-group>
|
||||||
<ak-form-group expanded>
|
<ak-form-group expanded>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-private-text-input.js";
|
import "@goauthentik/components/ak-secret-text-input.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/forms/Radio";
|
import "@goauthentik/elements/forms/Radio";
|
||||||
@ -77,11 +77,11 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
|
|||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
<ak-private-text-input
|
<ak-secret-text-input
|
||||||
name="password"
|
name="password"
|
||||||
label=${msg("SMTP Password")}
|
label=${msg("SMTP Password")}
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
></ak-private-text-input>
|
></ak-secret-text-input>
|
||||||
|
|
||||||
<ak-form-element-horizontal name="useTls">
|
<ak-form-element-horizontal name="useTls">
|
||||||
<label class="pf-c-switch">
|
<label class="pf-c-switch">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-number-input";
|
import "@goauthentik/components/ak-number-input";
|
||||||
import "@goauthentik/components/ak-private-text-input.js";
|
import "@goauthentik/components/ak-secret-text-input.js";
|
||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
@ -70,7 +70,7 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
|
|||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
<ak-private-text-input
|
<ak-secret-text-input
|
||||||
name="privateKey"
|
name="privateKey"
|
||||||
label=${msg("Private Key")}
|
label=${msg("Private Key")}
|
||||||
input-hint="code"
|
input-hint="code"
|
||||||
@ -79,7 +79,7 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
|
|||||||
help=${msg(
|
help=${msg(
|
||||||
"Private key, acquired from https://www.google.com/recaptcha/intro/v3.html.",
|
"Private key, acquired from https://www.google.com/recaptcha/intro/v3.html.",
|
||||||
)}
|
)}
|
||||||
></ak-private-text-input>
|
></ak-secret-text-input>
|
||||||
|
|
||||||
<ak-switch-input
|
<ak-switch-input
|
||||||
name="interactive"
|
name="interactive"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-private-text-input.js";
|
import "@goauthentik/components/ak-secret-text-input.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||||
@ -73,11 +73,11 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
|
|||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-private-text-input
|
<ak-secret-text-input
|
||||||
label=${msg("SMTP Password")}
|
label=${msg("SMTP Password")}
|
||||||
name="password"
|
name="password"
|
||||||
?revealed=${this.instance === undefined}
|
?revealed=${this.instance === undefined}
|
||||||
></ak-private-text-input>
|
></ak-secret-text-input>
|
||||||
<ak-form-element-horizontal name="useTls">
|
<ak-form-element-horizontal name="useTls">
|
||||||
<label class="pf-c-switch">
|
<label class="pf-c-switch">
|
||||||
<input
|
<input
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement, type AKElementProps } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement.js";
|
import "@goauthentik/elements/forms/HorizontalFormElement.js";
|
||||||
|
|
||||||
import { TemplateResult, html, nothing } from "lit";
|
import { TemplateResult, html, nothing } from "lit";
|
||||||
@ -6,6 +6,19 @@ import { property } from "lit/decorators.js";
|
|||||||
|
|
||||||
type HelpType = TemplateResult | typeof nothing;
|
type HelpType = TemplateResult | typeof nothing;
|
||||||
|
|
||||||
|
export interface HorizontalLightComponentProps<T> extends AKElementProps {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
required?: boolean;
|
||||||
|
help?: string;
|
||||||
|
bighelp?: TemplateResult | TemplateResult[];
|
||||||
|
hidden?: boolean;
|
||||||
|
invalid?: boolean;
|
||||||
|
errorMessages?: string[];
|
||||||
|
value?: T;
|
||||||
|
inputHint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class HorizontalLightComponent<T> extends AKElement {
|
export class HorizontalLightComponent<T> extends AKElement {
|
||||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||||
@ -18,37 +31,81 @@ export class HorizontalLightComponent<T> extends AKElement {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name attribute for the form element
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
@property({ type: String, reflect: true })
|
@property({ type: String, reflect: true })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label for the input control
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
@property({ type: String, reflect: true })
|
@property({ type: String, reflect: true })
|
||||||
label = "";
|
label = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
required = false;
|
required = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Help text to display below the form element. Optional
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
@property({ type: String, reflect: true })
|
@property({ type: String, reflect: true })
|
||||||
help = "";
|
help = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended help content. Optional. Expects to be a TemplateResult
|
||||||
|
* @property
|
||||||
|
*/
|
||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
bighelp?: TemplateResult | TemplateResult[];
|
bighelp?: TemplateResult | TemplateResult[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
hidden = false;
|
hidden = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
invalid = false;
|
invalid = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
errorMessages: string[] = [];
|
errorMessages: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @attribute
|
||||||
|
* @property
|
||||||
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
value?: T;
|
value?: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input hint.
|
||||||
|
* - `code`: uses a monospace font and disables spellcheck & autocomplete
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
@property({ type: String, attribute: "input-hint" })
|
@property({ type: String, attribute: "input-hint" })
|
||||||
inputHint = "";
|
inputHint = "";
|
||||||
|
|
||||||
renderControl() {
|
protected renderControl() {
|
||||||
throw new Error("Must be implemented in a subclass");
|
throw new Error("Must be implemented in a subclass");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
159
web/src/components/ak-hidden-text-input.ts
Normal file
159
web/src/components/ak-hidden-text-input.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { bound } from "#elements/decorators/bound";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { css, html } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators.js";
|
||||||
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
HorizontalLightComponent,
|
||||||
|
HorizontalLightComponentProps,
|
||||||
|
} from "./HorizontalLightComponent";
|
||||||
|
import "./ak-visibility-toggle.js";
|
||||||
|
import type { VisibilityToggleProps } from "./ak-visibility-toggle.js";
|
||||||
|
|
||||||
|
type BaseProps = HorizontalLightComponentProps<string> &
|
||||||
|
Pick<VisibilityToggleProps, "showMessage" | "hideMessage">;
|
||||||
|
|
||||||
|
export interface AkHiddenTextInputProps extends BaseProps {
|
||||||
|
revealed: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputLike = HTMLTextAreaElement | HTMLInputElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @element ak-hidden-text-input
|
||||||
|
* @class AkHiddenTextInput
|
||||||
|
*
|
||||||
|
* A text-input field with a visibility control, so you can show/hide sensitive fields.
|
||||||
|
*
|
||||||
|
* ## CSS Parts
|
||||||
|
* @csspart container - The main container div
|
||||||
|
* @csspart input - The input element
|
||||||
|
* @csspart toggle - The visibility toggle button
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@customElement("ak-hidden-text-input")
|
||||||
|
export class AkHiddenTextInput<T extends InputLike = HTMLInputElement>
|
||||||
|
extends HorizontalLightComponent<string>
|
||||||
|
implements AkHiddenTextInputProps
|
||||||
|
{
|
||||||
|
public static get styles() {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: String, reflect: true })
|
||||||
|
public value = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public revealed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text for when the input has no set value
|
||||||
|
*
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: String })
|
||||||
|
public placeholder?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify kind of help the browser should try to provide
|
||||||
|
*
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: String })
|
||||||
|
public autocomplete?: "none" | AutoFill;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: String, attribute: "show-message" })
|
||||||
|
public showMessage = msg("Show field content");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: String, attribute: "hide-message" })
|
||||||
|
public hideMessage = msg("Hide field content");
|
||||||
|
|
||||||
|
@query("#main > input")
|
||||||
|
protected inputField!: T;
|
||||||
|
|
||||||
|
@bound
|
||||||
|
private handleToggleVisibility() {
|
||||||
|
this.revealed = !this.revealed;
|
||||||
|
|
||||||
|
// Maintain focus on input after toggle
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
if (this.inputField && document.activeElement === this) {
|
||||||
|
this.inputField.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Because of the peculiarities of how HorizontalLightComponent works, keeping its content
|
||||||
|
// in the LightDom so the inner components actually inherit styling, the normal `css` options
|
||||||
|
// aren't available. Embedding styles is bad styling, and we'll fix it in the next style
|
||||||
|
// refresh.
|
||||||
|
protected renderInputField(setValue: (ev: InputEvent) => void, code: boolean) {
|
||||||
|
return html` <input
|
||||||
|
style="flex: 1 1 auto; min-width: 0;"
|
||||||
|
part="input"
|
||||||
|
type=${this.revealed ? "text" : "password"}
|
||||||
|
@input=${setValue}
|
||||||
|
value=${ifDefined(this.value)}
|
||||||
|
placeholder=${ifDefined(this.placeholder)}
|
||||||
|
class="${classMap({
|
||||||
|
"pf-c-form-control": true,
|
||||||
|
"pf-m-monospace": code,
|
||||||
|
})}"
|
||||||
|
spellcheck=${code ? "false" : "true"}
|
||||||
|
?required=${this.required}
|
||||||
|
/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override renderControl() {
|
||||||
|
const code = this.inputHint === "code";
|
||||||
|
const setValue = (ev: InputEvent) => {
|
||||||
|
this.value = (ev.target as T).value;
|
||||||
|
};
|
||||||
|
return html` <div style="display: flex; gap: 0.25rem">
|
||||||
|
${this.renderInputField(setValue, code)}
|
||||||
|
<!-- -->
|
||||||
|
<ak-visibility-toggle
|
||||||
|
part="toggle"
|
||||||
|
style="flex: 0 0 auto; align-self: flex-start"
|
||||||
|
?open=${this.revealed}
|
||||||
|
show-message=${this.showMessage}
|
||||||
|
hide-message=${this.hideMessage}
|
||||||
|
@click=${() => (this.revealed = !this.revealed)}
|
||||||
|
></ak-visibility-toggle>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ak-hidden-text-input": AkHiddenTextInput;
|
||||||
|
}
|
||||||
|
}
|
128
web/src/components/ak-hidden-textarea-input.ts
Normal file
128
web/src/components/ak-hidden-textarea-input.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { css, html } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators.js";
|
||||||
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import { AkHiddenTextInput, type AkHiddenTextInputProps } from "./ak-hidden-text-input.js";
|
||||||
|
|
||||||
|
export interface AkHiddenTextAreaInputProps extends AkHiddenTextInputProps {
|
||||||
|
/**
|
||||||
|
* Number of visible text lines (rows)
|
||||||
|
*/
|
||||||
|
rows?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of visible character width (cols)
|
||||||
|
*/
|
||||||
|
cols?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How the textarea can be resized
|
||||||
|
*/
|
||||||
|
resize?: "none" | "both" | "horizontal" | "vertical";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether text should wrap
|
||||||
|
*/
|
||||||
|
wrap?: "soft" | "hard" | "off";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @element ak-hidden-text-input
|
||||||
|
* @class AkHiddenTextInput
|
||||||
|
*
|
||||||
|
* A text-input field with a visibility control, so you can show/hide sensitive fields.
|
||||||
|
*
|
||||||
|
* ## CSS Parts
|
||||||
|
* @csspart container - The main container div
|
||||||
|
* @csspart input - The input element
|
||||||
|
* @csspart toggle - The visibility toggle button
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@customElement("ak-hidden-textarea-input")
|
||||||
|
export class AkHiddenTextAreaInput
|
||||||
|
extends AkHiddenTextInput<HTMLTextAreaElement>
|
||||||
|
implements AkHiddenTextAreaInputProps
|
||||||
|
{
|
||||||
|
/* These are mostly just forwarded to the textarea component. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: Number })
|
||||||
|
rows?: number = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: Number })
|
||||||
|
cols?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*
|
||||||
|
* You want `resize=true` so that the resize value is visible in the component tag, activating
|
||||||
|
* the CSS associated with these values.
|
||||||
|
*/
|
||||||
|
@property({ type: String, reflect: true })
|
||||||
|
resize?: "none" | "both" | "horizontal" | "vertical" = "vertical";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: String })
|
||||||
|
wrap?: "soft" | "hard" | "off" = "soft";
|
||||||
|
|
||||||
|
@query("#main > textarea")
|
||||||
|
protected inputField!: HTMLTextAreaElement;
|
||||||
|
|
||||||
|
get displayValue() {
|
||||||
|
const value = this.value ?? "";
|
||||||
|
if (this.revealed) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.split("\n")
|
||||||
|
.reduce((acc: string[], line: string) => [...acc, "*".repeat(line.length)], [])
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Because of the peculiarities of how HorizontalLightComponent works, keeping its content
|
||||||
|
// in the LightDom so the inner components actually inherit styling, the normal `css` options
|
||||||
|
// aren't available. Embedding styles is bad styling, and we'll fix it in the next style
|
||||||
|
// refresh.
|
||||||
|
protected override renderInputField(setValue: (ev: InputEvent) => void, code: boolean) {
|
||||||
|
const wrap = this.revealed ? this.wrap : "soft";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<textarea
|
||||||
|
style="flex: 1 1 auto; min-width: 0;"
|
||||||
|
part="textarea"
|
||||||
|
@input=${setValue}
|
||||||
|
placeholder=${ifDefined(this.placeholder)}
|
||||||
|
rows=${ifDefined(this.rows)}
|
||||||
|
cols=${ifDefined(this.cols)}
|
||||||
|
wrap=${ifDefined(wrap)}
|
||||||
|
class=${classMap({
|
||||||
|
"pf-c-form-control": true,
|
||||||
|
"pf-m-monospace": code,
|
||||||
|
})}
|
||||||
|
spellcheck=${code ? "false" : "true"}
|
||||||
|
?required=${this.required}
|
||||||
|
>
|
||||||
|
${this.displayValue}</textarea
|
||||||
|
>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ak-hidden-textarea-input": AkHiddenTextAreaInput;
|
||||||
|
}
|
||||||
|
}
|
@ -8,8 +8,8 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
|||||||
|
|
||||||
import { HorizontalLightComponent } from "./HorizontalLightComponent";
|
import { HorizontalLightComponent } from "./HorizontalLightComponent";
|
||||||
|
|
||||||
@customElement("ak-private-text-input")
|
@customElement("ak-secret-text-input")
|
||||||
export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
export class AkSecretTextInput extends HorizontalLightComponent<string> {
|
||||||
@property({ type: String, reflect: true })
|
@property({ type: String, reflect: true })
|
||||||
public value = "";
|
public value = "";
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
|||||||
this.revealed = true;
|
this.revealed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#renderPrivateInput() {
|
#renderSecretInput() {
|
||||||
return html`<div class="pf-c-form__horizontal-group" @click=${() => this.#onReveal()}>
|
return html`<div class="pf-c-form__horizontal-group" @click=${() => this.#onReveal()}>
|
||||||
<input
|
<input
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
@ -60,14 +60,14 @@ export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override renderControl() {
|
public override renderControl() {
|
||||||
return this.revealed ? this.renderVisibleInput() : this.#renderPrivateInput();
|
return this.revealed ? this.renderVisibleInput() : this.#renderSecretInput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AkPrivateTextInput;
|
export default AkSecretTextInput;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ak-private-text-input": AkPrivateTextInput;
|
"ak-secret-text-input": AkSecretTextInput;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,10 +5,10 @@ import { customElement, property } from "lit/decorators.js";
|
|||||||
import { classMap } from "lit/directives/class-map.js";
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import { AkPrivateTextInput } from "./ak-private-text-input.js";
|
import { AkSecretTextInput } from "./ak-secret-text-input.js";
|
||||||
|
|
||||||
@customElement("ak-private-textarea-input")
|
@customElement("ak-secret-textarea-input")
|
||||||
export class AkPrivateTextAreaInput extends AkPrivateTextInput {
|
export class AkSecretTextAreaInput extends AkSecretTextInput {
|
||||||
protected override renderVisibleInput() {
|
protected override renderVisibleInput() {
|
||||||
const code = this.inputHint === "code";
|
const code = this.inputHint === "code";
|
||||||
const setValue = (ev: InputEvent) => {
|
const setValue = (ev: InputEvent) => {
|
||||||
@ -34,10 +34,10 @@ export class AkPrivateTextAreaInput extends AkPrivateTextInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AkPrivateTextAreaInput;
|
export default AkSecretTextAreaInput;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ak-private-textarea-input": AkPrivateTextAreaInput;
|
"ak-secret-textarea-input": AkSecretTextAreaInput;
|
||||||
}
|
}
|
||||||
}
|
}
|
89
web/src/components/ak-visibility-toggle.ts
Normal file
89
web/src/components/ak-visibility-toggle.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { AKElement } from "@goauthentik/elements/Base.js";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
|
export interface VisibilityToggleProps {
|
||||||
|
open: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
showMessage: string;
|
||||||
|
hideMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @component ak-visibility-toggle
|
||||||
|
* @class VisibilityToggle
|
||||||
|
*
|
||||||
|
* A straightforward two-state iconic button we use in a few places as way of telling users to hide
|
||||||
|
* or show something secret, such as a password or private key. Expects the client to manage its
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* @events
|
||||||
|
* - click: when the toggle is clicked.
|
||||||
|
*/
|
||||||
|
@customElement("ak-visibility-toggle")
|
||||||
|
export class VisibilityToggle extends AKElement implements VisibilityToggleProps {
|
||||||
|
static get styles() {
|
||||||
|
return [PFBase, PFButton];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
open = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
disabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: String, attribute: "show-message" })
|
||||||
|
showMessage = msg("Show field content");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: String, attribute: "hide-message" })
|
||||||
|
hideMessage = msg("Hide field content");
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const [label, icon] = this.open
|
||||||
|
? [this.hideMessage, "fa-eye"]
|
||||||
|
: [this.showMessage, "fa-eye-slash"];
|
||||||
|
|
||||||
|
const onClick = (ev: PointerEvent) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.dispatchEvent(new PointerEvent(ev.type, ev));
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`<button
|
||||||
|
aria-label=${label}
|
||||||
|
title=${label}
|
||||||
|
@click=${onClick}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
class="pf-c-button pf-m-control"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i class="fas ${icon}" aria-hidden="true"></i>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ak-visibility-toggle": VisibilityToggle;
|
||||||
|
}
|
||||||
|
}
|
93
web/src/components/stories/ak-hidden-text-input.stories.ts
Normal file
93
web/src/components/stories/ak-hidden-text-input.stories.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||||
|
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import "../ak-hidden-text-input";
|
||||||
|
import { type AkHiddenTextInput, type AkHiddenTextInputProps } from "../ak-hidden-text-input.js";
|
||||||
|
|
||||||
|
const metadata: Meta<AkHiddenTextInputProps> = {
|
||||||
|
title: "Components / <ak-hidden-text-input>",
|
||||||
|
component: "ak-hidden-text-input",
|
||||||
|
tags: ["autodocs"],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
# Hidden Text Input Component
|
||||||
|
|
||||||
|
A text-input field with a visibility control, so you can show/hide sensitive fields.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: "padded",
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
label: {
|
||||||
|
control: "text",
|
||||||
|
description: "Label text for the input field",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
control: "text",
|
||||||
|
description: "Current value of the input",
|
||||||
|
},
|
||||||
|
revealed: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the text is currently visible",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: "text",
|
||||||
|
description: "Placeholder text for the input",
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the input is required",
|
||||||
|
},
|
||||||
|
inputHint: {
|
||||||
|
control: "select",
|
||||||
|
options: ["text", "code"],
|
||||||
|
description: "Input type hint for styling and behavior",
|
||||||
|
},
|
||||||
|
showMessage: {
|
||||||
|
control: "text",
|
||||||
|
description: "Custom message for show action",
|
||||||
|
},
|
||||||
|
hideMessage: {
|
||||||
|
control: "text",
|
||||||
|
description: "Custom message for hide action",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default metadata;
|
||||||
|
|
||||||
|
type Story = StoryObj<AkHiddenTextInput>;
|
||||||
|
|
||||||
|
const Template: Story = {
|
||||||
|
args: {
|
||||||
|
label: "Hidden Text Input",
|
||||||
|
value: "",
|
||||||
|
revealed: false,
|
||||||
|
},
|
||||||
|
render: (args) => html`
|
||||||
|
<ak-hidden-text-input
|
||||||
|
label=${ifDefined(args.label)}
|
||||||
|
value=${ifDefined(args.value)}
|
||||||
|
?revealed=${args.revealed}
|
||||||
|
placeholder=${ifDefined(args.placeholder)}
|
||||||
|
?required=${args.required}
|
||||||
|
input-hint=${ifDefined(args.inputHint)}
|
||||||
|
show-message=${ifDefined(args.showMessage)}
|
||||||
|
hide-message=${ifDefined(args.hideMessage)}
|
||||||
|
></ak-hidden-text-input>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Password: Story = {
|
||||||
|
...Template,
|
||||||
|
args: {
|
||||||
|
label: "Password",
|
||||||
|
placeholder: "Enter your password",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
};
|
140
web/src/components/stories/ak-hidden-textarea-input.stories.ts
Normal file
140
web/src/components/stories/ak-hidden-textarea-input.stories.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||||
|
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import "../ak-hidden-textarea-input";
|
||||||
|
import {
|
||||||
|
type AkHiddenTextAreaInput,
|
||||||
|
type AkHiddenTextAreaInputProps,
|
||||||
|
} from "../ak-hidden-textarea-input.js";
|
||||||
|
|
||||||
|
const metadata: Meta<AkHiddenTextAreaInputProps> = {
|
||||||
|
title: "Components / <ak-hidden-textarea-input>",
|
||||||
|
component: "ak-hidden-textarea-input",
|
||||||
|
tags: ["autodocs"],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
# Hidden Textarea Input Component
|
||||||
|
|
||||||
|
A textarea input field with a visibility control, so you can show/hide sensitive fields.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: "padded",
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
label: {
|
||||||
|
control: "text",
|
||||||
|
description: "Label text for the input field",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
control: "text",
|
||||||
|
description: "Current value of the input",
|
||||||
|
},
|
||||||
|
revealed: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the text is currently visible",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: "text",
|
||||||
|
description: "Placeholder text for the input",
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the input is required",
|
||||||
|
},
|
||||||
|
inputHint: {
|
||||||
|
control: "select",
|
||||||
|
options: ["text", "code"],
|
||||||
|
description: "Input type hint for styling and behavior",
|
||||||
|
},
|
||||||
|
showMessage: {
|
||||||
|
control: "text",
|
||||||
|
description: "Custom message for show action",
|
||||||
|
},
|
||||||
|
hideMessage: {
|
||||||
|
control: "text",
|
||||||
|
description: "Custom message for hide action",
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
control: { type: "number", min: 1, max: 50 },
|
||||||
|
description: "Number of visible text lines",
|
||||||
|
},
|
||||||
|
cols: {
|
||||||
|
control: { type: "number", min: 10, max: 200 },
|
||||||
|
description: "Number of visible character width",
|
||||||
|
},
|
||||||
|
resize: {
|
||||||
|
control: "select",
|
||||||
|
options: ["none", "both", "horizontal", "vertical"],
|
||||||
|
description: "How the textarea can be resized",
|
||||||
|
},
|
||||||
|
wrap: {
|
||||||
|
control: "select",
|
||||||
|
options: ["soft", "hard", "off"],
|
||||||
|
description: "Text wrapping behavior",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default metadata;
|
||||||
|
|
||||||
|
type Story = StoryObj<AkHiddenTextAreaInput>;
|
||||||
|
|
||||||
|
const Template: Story = {
|
||||||
|
args: {
|
||||||
|
label: "Hidden Textarea Input",
|
||||||
|
value: "",
|
||||||
|
revealed: false,
|
||||||
|
rows: 4,
|
||||||
|
},
|
||||||
|
render: (args) => html`
|
||||||
|
<ak-hidden-textarea-input
|
||||||
|
label=${ifDefined(args.label)}
|
||||||
|
value=${ifDefined(args.value)}
|
||||||
|
?revealed=${args.revealed}
|
||||||
|
placeholder=${ifDefined(args.placeholder)}
|
||||||
|
rows=${ifDefined(args.rows)}
|
||||||
|
cols=${ifDefined(args.cols)}
|
||||||
|
resize=${ifDefined(args.resize)}
|
||||||
|
wrap=${ifDefined(args.wrap)}
|
||||||
|
?required=${args.required}
|
||||||
|
input-hint=${ifDefined(args.inputHint)}
|
||||||
|
show-message=${ifDefined(args.showMessage)}
|
||||||
|
hide-message=${ifDefined(args.hideMessage)}
|
||||||
|
></ak-hidden-textarea-input>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SslCertificate: Story = {
|
||||||
|
...Template,
|
||||||
|
args: {
|
||||||
|
label: "SSL Certificate",
|
||||||
|
value: `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||||
|
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||||
|
aWRnaXRzIFB0eSBMdGQwHhcNMTcwNTEwMTk0MDA2WhcNMTgwNTEwMTk0MDA2WjBF
|
||||||
|
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||||
|
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE3MDUxMDE5NDAwNloXDTE4MDUxMDE5
|
||||||
|
NDAwNlowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNV
|
||||||
|
BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||||
|
ggEPADCCAQoCggEBALdUlNS31SzxwoFShahGfjHj6GgpcVbzL1Siq0Pqnf82T6M2
|
||||||
|
EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggE
|
||||||
|
BAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqn
|
||||||
|
f82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgM
|
||||||
|
BAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeM
|
||||||
|
Hyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDu
|
||||||
|
neMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJ
|
||||||
|
kPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAE=
|
||||||
|
-----END CERTIFICATE-----`,
|
||||||
|
inputHint: "code",
|
||||||
|
rows: 15,
|
||||||
|
resize: "vertical",
|
||||||
|
showMessage: "Show certificate content",
|
||||||
|
hideMessage: "Hide certificate content",
|
||||||
|
autocomplete: "off",
|
||||||
|
},
|
||||||
|
};
|
121
web/src/components/stories/ak-visibility-toggle.stories.ts
Normal file
121
web/src/components/stories/ak-visibility-toggle.stories.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||||
|
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import "../ak-visibility-toggle";
|
||||||
|
import { type VisibilityToggle, type VisibilityToggleProps } from "../ak-visibility-toggle.js";
|
||||||
|
|
||||||
|
const metadata: Meta<VisibilityToggleProps> = {
|
||||||
|
title: "Elements/<ak-visibility-toggle>",
|
||||||
|
component: "ak-visibility-toggle",
|
||||||
|
tags: ["autodocs"],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
# Visibility Toggle Component
|
||||||
|
|
||||||
|
A straightforward two-state iconic button for toggling the visibility of sensitive content such as passwords, private keys, or other secret information.
|
||||||
|
|
||||||
|
- Use for sensitive content that users might want to temporarily reveal
|
||||||
|
- There are default hide/show messages for screen readers, but they can be overridden
|
||||||
|
- Clients always handle the state
|
||||||
|
- The \`open\` state is false by default; we assume you want sensitive content hidden at start
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: "padded",
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
open: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the toggle is in the 'show' state (true) or 'hide' state (false)",
|
||||||
|
},
|
||||||
|
showMessage: {
|
||||||
|
control: "text",
|
||||||
|
description:
|
||||||
|
'Message for screen readers when in hide state (default: "Show field content")',
|
||||||
|
},
|
||||||
|
hideMessage: {
|
||||||
|
control: "text",
|
||||||
|
description:
|
||||||
|
'Message for screen readers when in show state (default: "Hide field content")',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the button should be disabled (for demo purposes)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default metadata;
|
||||||
|
|
||||||
|
type Story = StoryObj<VisibilityToggle>;
|
||||||
|
|
||||||
|
const Template: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
showMessage: "Show field content",
|
||||||
|
hideMessage: "Hide field content",
|
||||||
|
},
|
||||||
|
render: (args) => html`
|
||||||
|
<ak-visibility-toggle
|
||||||
|
?open=${args.open}
|
||||||
|
show-message=${ifDefined(args.showMessage)}
|
||||||
|
hide-message=${ifDefined(args.hideMessage)}
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
const target = e.target as VisibilityToggle;
|
||||||
|
target.open = !target.open;
|
||||||
|
// In a real application, you would also toggle the visibility
|
||||||
|
// of the associated content here
|
||||||
|
}}
|
||||||
|
></ak-visibility-toggle>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Password field integration example
|
||||||
|
export const PasswordFieldExample: Story = {
|
||||||
|
args: {
|
||||||
|
showMessage: "Reveal password",
|
||||||
|
hideMessage: "Conceal password",
|
||||||
|
},
|
||||||
|
render: () => {
|
||||||
|
let isVisible = false;
|
||||||
|
|
||||||
|
const toggleVisibility = (e: Event) => {
|
||||||
|
isVisible = !isVisible;
|
||||||
|
const toggle = e.target as VisibilityToggle;
|
||||||
|
const passwordField = document.querySelector("#demo-password") as HTMLInputElement;
|
||||||
|
|
||||||
|
toggle.open = isVisible;
|
||||||
|
if (passwordField) {
|
||||||
|
passwordField.type = isVisible ? "text" : "password";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 1rem; max-width: 300px;">
|
||||||
|
<label for="demo-password" style="font-weight: bold;">Password:</label>
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<input
|
||||||
|
id="demo-password"
|
||||||
|
type="password"
|
||||||
|
value="supersecretpassword123"
|
||||||
|
style="flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px;"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<ak-visibility-toggle
|
||||||
|
?open=${isVisible}
|
||||||
|
show-message="Show password"
|
||||||
|
hide-message="Hide password"
|
||||||
|
@click=${toggleVisibility}
|
||||||
|
></ak-visibility-toggle>
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 0.875rem; color: #666;">
|
||||||
|
Click the eye icon to toggle password visibility
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
};
|
@ -16,8 +16,12 @@ import { property } from "lit/decorators.js";
|
|||||||
|
|
||||||
import { UiThemeEnum } from "@goauthentik/api";
|
import { UiThemeEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
|
export interface AKElementProps {
|
||||||
|
activeTheme: ResolvedUITheme;
|
||||||
|
}
|
||||||
|
|
||||||
@localized()
|
@localized()
|
||||||
export class AKElement extends LitElement {
|
export class AKElement extends LitElement implements AKElementProps {
|
||||||
//#region Static Properties
|
//#region Static Properties
|
||||||
|
|
||||||
public static styles?: Array<CSSResult | CSSModule>;
|
public static styles?: Array<CSSResult | CSSModule>;
|
||||||
|
Reference in New Issue
Block a user