Files
authentik/web/src/user/LibraryApplication/index.ts
Ken Sternberg 0a1d283ac8 web: provide storybook demos and docs for existing tests (#11651)
* Added tests and refinements as tests indicate.

* Building out the test suite.

* web: test the simple things. Fix what the tests revealed.

- Move `EmptyState.test.ts` into the `./tests` folder.
- Provide unit tests for:
  - Alert
  - Divider
  - Expand
  - Label
  - LoadingOverlay
- Give all tested items an Interface and a functional variant for rendering
- Give Label an alternative syntax for declaring alert levels
- Remove the slot name in LoadingOverlay
  - Change the slot call in `./enterprise/rac/index.ts` to not need the slot name as well
- Change the attribute names `topMost`, `textOpen`, and `textClosed` to `topmost`, `text-open`, and
  `text-closed`, respectively.
  - Change locations in the code where those are used to correspond

** Why interfaces: **

Provides another check on the input/output boundaries of our elements, gives Storybook and
WebdriverIO another validation to check, and guarantees any rendering functions cannot be passed
invalid property names.

** Why functions for rendering: **

Providing functions for rendering gets us one step closer to dynamically defining our forms-in-code
at runtime without losing any type safety.

** Why rename the attributes: **

A *very* subtle bug:
[Element:setAttribute()](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute)
automatically "converts an attribute name to all lower-case when called on an HTML element in an
HTML document." The three attributes renamed are all treated *as* attributes, either classic boolean
or stringly-typed attributes, and attempting to manipulate them with `setAttribute()` will fail.

All of these attributes are presentational; none of them end up in a transaction with the back-end,
so kebab-to-camel conversions are not a concern.

Also, ["topmost" is one word](https://www.merriam-webster.com/dictionary/topmost).

** Why remove the slot name: **

Because there was only one slot.  A name is not needed.

* Fix minor spelling error.

* First pass at a custom, styled input object.

* .

* web: Demo the simple things. Fix things the Demo says need fixing.

- Move the Element's stories into a `./stories` folder
- Provide stories for (these are the same ones "provided tests for" in the [previous
  PR](https://github.com/goauthentik/authentik/pull/11633))
  - Alert
  - Divider
  - Expand
  - Label
  - LoadingOverlay
- Provide Storybook documentation for:
  - AppIcon
  - ActionButton
  - AggregateCard
  - AggregatePromiseCard
  - QuickActionsCard
  - Alert
  - Divider
  - EmptyState
  - Expand
  - Label
  - LoadingOverlay
  - ApplicationEmptyState
- Fix a bug in LoadingOverlay; naming error in nested slots caused any message attached to the
  overlay to not sow up correctly.
- Revise AppIcon to be independent of authentik; it just cares if the data has a name or an icon
  reference, it does not need to know about `Application` objects. As such, it's an *element*, not a
  *component*, and I've moved it into the right location, and updated the few places it is used to
  match.

* Prettier has opinions with which I sometimes diverge.

* Found a bug! Although pf-m-xl was defined as a legal size, there was no code to handle drawing something XL!

* Found a few typos and incorrect API descriptions.
2024-10-14 10:30:09 -07:00

146 lines
5.4 KiB
TypeScript

import { PFSize } from "@goauthentik/common/enums.js";
import { truncateWords } from "@goauthentik/common/utils";
import "@goauthentik/elements/AppIcon";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Expand";
import "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal";
import { UserInterface } from "@goauthentik/user/UserInterface";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { styleMap } from "lit/directives/style-map.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Application } from "@goauthentik/api";
@customElement("ak-library-app")
export class LibraryApplication extends AKElement {
@property({ attribute: false })
application?: Application;
@property({ type: Boolean })
selected = false;
@property()
background = "";
static get styles(): CSSResult[] {
return [
PFBase,
PFCard,
PFButton,
css`
.pf-c-card {
--pf-c-card--BoxShadow: var(--pf-global--BoxShadow--md);
}
.pf-c-card__header {
justify-content: space-between;
flex-direction: column;
}
.pf-c-card__header a {
display: flex;
flex-direction: column;
justify-content: center;
}
a:hover {
text-decoration: none;
}
.expander {
flex-grow: 1;
}
.pf-c-card__title {
text-align: center;
/* This is not ideal as it hard limits us to 2 lines of text for the title
of the application. In theory that should be fine for most cases, but ideally
we don't do this */
height: 48px;
}
`,
];
}
renderExpansion(application: Application) {
const me = rootInterface<UserInterface>()?.me;
return html`<ak-expand text-open=${msg("Less details")} text-closed=${msg("More details")}>
<div class="pf-c-content">
<small>${application.metaPublisher}</small>
</div>
${truncateWords(application.metaDescription || "", 10)}
${rootInterface()?.uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser
? html`
<a
class="pf-c-button pf-m-control pf-m-small pf-m-block"
href="/if/admin/#/core/applications/${application?.slug}"
>
<i class="fas fa-edit"></i>&nbsp;${msg("Edit")}
</a>
`
: html``}
</ak-expand>`;
}
renderLaunch(): TemplateResult {
if (!this.application) {
return html``;
}
if (this.application?.launchUrl === "goauthentik.io://providers/rac/launch") {
return html`<ak-library-rac-endpoint-launch .app=${this.application}>
<a slot="trigger"> ${this.application.name} </a>
</ak-library-rac-endpoint-launch>`;
}
return html`<a
href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
>${this.application.name}</a
>`;
}
render(): TemplateResult {
if (!this.application) {
return html`<ak-spinner></ak-spinner>`;
}
const me = rootInterface<UserInterface>()?.me;
const expandable =
(rootInterface()?.uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser) ||
this.application.metaPublisher !== "" ||
this.application.metaDescription !== "";
const classes = { "pf-m-selectable": this.selected, "pf-m-selected": this.selected };
const styles = this.background ? { background: this.background } : {};
return html` <div
class="pf-c-card pf-m-hoverable pf-m-compact ${classMap(classes)}"
style=${styleMap(styles)}
>
<div class="pf-c-card__header">
<a
href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
>
<ak-app-icon
size=${PFSize.Large}
name=${this.application.name}
icon=${ifDefined(this.application.metaIcon || undefined)}
></ak-app-icon>
</a>
</div>
<div class="pf-c-card__title">${this.renderLaunch()}</div>
<div class="expander"></div>
${expandable ? this.renderExpansion(this.application) : nothing}
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-library-app": LibraryApplication;
}
}