web: break application view into constituent parts
As part of the project to make the verticals more controllable and responsive, this commit breaks the ApplicationView into two different parts: The API layer and the rendering layer. The rendering layer is officially dumb beyond words; it knows nothing at all about Applications, RBAC, or Outposts; it just draws what it's told to draw. It has parts inside that have their own reactivity, but that reactivity means nothing to the renderer. The Renderer itself is broken into two: The LoadingRenderer works when there is no application, and the regular Renderer is build when there is. Typescript's check makes it impossible to attempt to use the standard renderer when there is no application, so all of the `this.application?` checks just... go away. A _huge_ section of the View is the control card, which offers the user the power to visit the provider, provide access to the backchannel providers, edit the application, run an access check against a given user, and launch the application. All of these features were heavily obscured by a blizzard of dg/dl/dt/dd html objects that made it hard to see what was in there. Each "description" pair has been broken out into a tuple of Term and Description, with filters to remove the ones that aren't applicable whenever an application doesn't have, for example, a launch url, or backchannel providers, and a utility function I wrote _ages_ ago renders the description list syntax for me without my having to do it all by hand. The nice thing about this work is that it now allows me to *see* where in the ApplicationView code to focus my efforts on providing activation hooks for the "create a new policy," "assign a new permission to a user," or "edit an application" commands that should be accessible by the palette, or even from the sidebar. The other nice thing is that it reveals just *where* in our code to focus our efforts on revamping our styling, and making it better for ourselves and our users. There's a reason I call this my *legibility project*. It didn't even take that long... about 2½ hours, and I'm only going to get faster at it as the needs of the different components become clear.
This commit is contained in:
@ -3,7 +3,6 @@ import "@goauthentik/admin/applications/ApplicationCheckAccessForm";
|
||||
import "@goauthentik/admin/applications/ApplicationForm";
|
||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import "@goauthentik/components/ak-app-icon";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
@ -13,10 +12,8 @@ import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
@ -35,18 +32,14 @@ import {
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
ApplicationViewPageLoadingRenderer,
|
||||
ApplicationViewPageRenderer,
|
||||
} from "./ApplicationViewPageRenderers.js";
|
||||
|
||||
@customElement("ak-application-view")
|
||||
export class ApplicationViewPage extends AKElement {
|
||||
@property({ type: String })
|
||||
applicationSlug?: string;
|
||||
|
||||
@state()
|
||||
application?: Application;
|
||||
|
||||
@state()
|
||||
missingOutpost = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
static get styles() {
|
||||
return [
|
||||
PFBase,
|
||||
PFList,
|
||||
@ -60,6 +53,15 @@ export class ApplicationViewPage extends AKElement {
|
||||
];
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
applicationSlug?: string;
|
||||
|
||||
@state()
|
||||
application?: Application;
|
||||
|
||||
@state()
|
||||
missingOutpost = false;
|
||||
|
||||
fetchIsMissingOutpost(providersByPk: Array<number>) {
|
||||
new OutpostsApi(DEFAULT_CONFIG)
|
||||
.outpostsInstancesList({
|
||||
@ -94,231 +96,15 @@ export class ApplicationViewPage extends AKElement {
|
||||
}
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<ak-page-header
|
||||
header=${this.application?.name || msg("Loading")}
|
||||
description=${ifDefined(this.application?.metaPublisher)}
|
||||
.iconImage=${true}
|
||||
>
|
||||
<ak-app-icon
|
||||
size=${PFSize.Medium}
|
||||
slot="icon"
|
||||
.app=${this.application}
|
||||
></ak-app-icon>
|
||||
</ak-page-header>
|
||||
${this.renderApp()}`;
|
||||
}
|
||||
render() {
|
||||
const renderer = this.application
|
||||
? new ApplicationViewPageRenderer(
|
||||
this.application,
|
||||
this.missingOutpost,
|
||||
RbacPermissionsAssignedByUsersListModelEnum.CoreApplication,
|
||||
)
|
||||
: new ApplicationViewPageLoadingRenderer();
|
||||
|
||||
renderApp(): TemplateResult {
|
||||
if (!this.application) {
|
||||
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
||||
</ak-empty-state>`;
|
||||
}
|
||||
return html`<ak-tabs>
|
||||
${this.missingOutpost
|
||||
? html`<div slot="header" class="pf-c-banner pf-m-warning">
|
||||
${msg("Warning: Application is not used by any Outpost.")}
|
||||
</div>`
|
||||
: html``}
|
||||
<section
|
||||
slot="page-overview"
|
||||
data-tab-title="${msg("Overview")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-2-col-on-xl pf-m-2-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card__title">${msg("Related")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list">
|
||||
${this.application.providerObj
|
||||
? html`<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Provider")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<a
|
||||
href="#/core/providers/${this.application
|
||||
.providerObj?.pk}"
|
||||
>
|
||||
${this.application.providerObj?.name}
|
||||
(${this.application.providerObj?.verboseName})
|
||||
</a>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`
|
||||
: html``}
|
||||
${(this.application.backchannelProvidersObj || []).length > 0
|
||||
? html`<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Backchannel Providers")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ul class="pf-c-list">
|
||||
${this.application.backchannelProvidersObj.map(
|
||||
(provider) => {
|
||||
return html`
|
||||
<li>
|
||||
<a
|
||||
href="#/core/providers/${provider.pk}"
|
||||
>
|
||||
${provider.name}
|
||||
(${provider.verboseName})
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
},
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`
|
||||
: html``}
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Policy engine mode")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.application.policyEngineMode?.toUpperCase()}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Edit")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header">
|
||||
${msg("Update Application")}
|
||||
</span>
|
||||
<ak-application-form
|
||||
slot="form"
|
||||
.instancePk=${this.application.slug}
|
||||
>
|
||||
</ak-application-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Check access")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
|
||||
<span slot="submit"> ${msg("Check")} </span>
|
||||
<span slot="header">
|
||||
${msg("Check Application access")}
|
||||
</span>
|
||||
<ak-application-check-access-form
|
||||
slot="form"
|
||||
.application=${this.application}
|
||||
>
|
||||
</ak-application-check-access-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
>
|
||||
${msg("Test")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
${this.application.launchUrl
|
||||
? html`<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Launch")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<a
|
||||
target="_blank"
|
||||
href=${this.application.launchUrl}
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
>
|
||||
${msg("Launch")}
|
||||
</a>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`
|
||||
: html``}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-10-col-on-xl pf-m-10-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card__title">
|
||||
${msg("Logins over the last week (per 8 hours)")}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
${this.application &&
|
||||
html` <ak-charts-application-authorize
|
||||
applicationSlug=${this.application.slug}
|
||||
>
|
||||
</ak-charts-application-authorize>`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__title">${msg("Changelog")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-object-changelog
|
||||
targetModelPk=${this.application.pk || ""}
|
||||
targetModelApp="authentik_core"
|
||||
targetModelName="application"
|
||||
>
|
||||
</ak-object-changelog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
slot="page-policy-bindings"
|
||||
data-tab-title="${msg("Policy / Group / User Bindings")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__title">
|
||||
${msg("These policies control which users can access this application.")}
|
||||
</div>
|
||||
<ak-bound-policies-list .target=${this.application.pk}>
|
||||
</ak-bound-policies-list>
|
||||
</div>
|
||||
</section>
|
||||
<ak-rbac-object-permission-page
|
||||
slot="page-permissions"
|
||||
data-tab-title="${msg("Permissions")}"
|
||||
model=${RbacPermissionsAssignedByUsersListModelEnum.CoreApplication}
|
||||
objectPk=${this.application.pk}
|
||||
></ak-rbac-object-permission-page>
|
||||
</ak-tabs>`;
|
||||
return renderer.render();
|
||||
}
|
||||
}
|
||||
|
214
web/src/admin/applications/ApplicationViewPageRenderers.ts
Normal file
214
web/src/admin/applications/ApplicationViewPageRenderers.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import { DescriptionPair, renderDescriptionList } from "@goauthentik/components/DescriptionList.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import type { Application, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api";
|
||||
|
||||
export class ApplicationViewPageLoadingRenderer {
|
||||
constructor() {}
|
||||
|
||||
render() {
|
||||
return html`<ak-page-header header=${msg("Loading")}
|
||||
><ak-empty-state ?loading="${true}" header=${msg("Loading")}> </ak-empty-state
|
||||
></ak-page-header>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ApplicationViewPageRenderer {
|
||||
constructor(
|
||||
private app: Application,
|
||||
private noOutpost: boolean,
|
||||
private rbacModel: RbacPermissionsAssignedByUsersListModelEnum,
|
||||
) {}
|
||||
|
||||
missingOutpostMessage() {
|
||||
return this.noOutpost
|
||||
? html`<div slot="header" class="pf-c-banner pf-m-warning">
|
||||
${msg("Warning: Application is not used by any Outpost.")}
|
||||
</div>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
controlCardContents(app: Application): DescriptionPair[] {
|
||||
// prettier-ignore
|
||||
const rows: (DescriptionPair | null)[] = [
|
||||
app.providerObj
|
||||
? [
|
||||
msg("Provider"),
|
||||
html`
|
||||
<a href="#/core/providers/${app.providerObj?.pk}">
|
||||
${app.providerObj?.name} (${app.providerObj?.verboseName})
|
||||
</a>
|
||||
`,
|
||||
]
|
||||
: null,
|
||||
|
||||
(app.backchannelProvidersObj || []).length > 0
|
||||
? [
|
||||
msg("Backchannel Providers"),
|
||||
html`
|
||||
<ul class="pf-c-list">
|
||||
${app.backchannelProvidersObj.map((provider) => {
|
||||
return html`
|
||||
<li>
|
||||
<a href="#/core/providers/${provider.pk}">
|
||||
${provider.name} (${provider.verboseName})
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
})}
|
||||
</ul>
|
||||
`,
|
||||
]
|
||||
: null,
|
||||
|
||||
[
|
||||
msg("Policy engine mode"),
|
||||
app.policyEngineMode?.toUpperCase()
|
||||
],
|
||||
|
||||
[
|
||||
msg("Edit"),
|
||||
html`
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("Update Application")} </span>
|
||||
<ak-application-form slot="form" .instancePk=${app.slug}>
|
||||
</ak-application-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
`,
|
||||
],
|
||||
|
||||
[
|
||||
msg("Check access"),
|
||||
html`
|
||||
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
|
||||
<span slot="submit"> ${msg("Check")} </span>
|
||||
<span slot="header"> ${msg("Check Application access")} </span>
|
||||
<ak-application-check-access-form slot="form" .application=${app}>
|
||||
</ak-application-check-access-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Test")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
`,
|
||||
],
|
||||
|
||||
app.launchUrl
|
||||
? [
|
||||
msg("Launch"),
|
||||
html`
|
||||
<a
|
||||
target="_blank"
|
||||
href=${app.launchUrl}
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
>
|
||||
${msg("Launch")}
|
||||
</a>
|
||||
`,
|
||||
]
|
||||
: null,
|
||||
];
|
||||
|
||||
return rows.filter((row) => row !== null) as DescriptionPair[];
|
||||
}
|
||||
|
||||
controlCard(app: Application) {
|
||||
return html`
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-2-col-on-xl pf-m-2-col-on-2xl">
|
||||
<div class="pf-c-card__title">${msg("Related")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${renderDescriptionList(this.controlCardContents(app))}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
loginsChart(app: Application) {
|
||||
return html`<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-10-col-on-xl pf-m-10-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card__title">${msg("Logins over the last week (per 8 hours)")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${app &&
|
||||
html` <ak-charts-application-authorize applicationSlug=${app.slug}>
|
||||
</ak-charts-application-authorize>`}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
changelog(app: Application) {
|
||||
return html`
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__title">${msg("Changelog")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-object-changelog
|
||||
targetModelPk=${app.pk || ""}
|
||||
targetModelApp="authentik_core"
|
||||
targetModelName="application"
|
||||
>
|
||||
</ak-object-changelog>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
overview(app: Application) {
|
||||
return html`
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
${this.controlCard(app)} ${this.loginsChart(app)} ${this.changelog(app)}
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
policiesList(app: Application) {
|
||||
return html`
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__title">
|
||||
${msg("These policies control which users can access this application.")}
|
||||
</div>
|
||||
<ak-bound-policies-list .target=${app.pk}> </ak-bound-policies-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <ak-page-header
|
||||
header=${this.app.name}
|
||||
description=${ifDefined(this.app.metaPublisher)}
|
||||
.iconImage=${true}
|
||||
>
|
||||
<ak-app-icon size=${PFSize.Medium} slot="icon" .app=${this.app}></ak-app-icon>
|
||||
</ak-page-header>
|
||||
<ak-tabs>
|
||||
${this.missingOutpostMessage()}
|
||||
<section
|
||||
slot="page-overview"
|
||||
data-tab-title="${msg("Overview")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
${this.overview(this.app)}
|
||||
</section>
|
||||
<section
|
||||
slot="page-policy-bindings"
|
||||
data-tab-title="${msg("Policy / Group / User Bindings")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
${this.policiesList(this.app)}
|
||||
</section>
|
||||
<ak-rbac-object-permission-page
|
||||
slot="page-permissions"
|
||||
data-tab-title="${msg("Permissions")}"
|
||||
model=${this.rbacModel}
|
||||
objectPk=${this.app.pk}
|
||||
></ak-rbac-object-permission-page>
|
||||
</ak-tabs>`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user