From 8f995aab625eebf5fb1e6c70aa5620dc8bfd1ae2 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Wed, 8 May 2024 17:47:26 -0700 Subject: [PATCH] web: break application view into constituent parts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../admin/applications/ApplicationViewPage.ts | 264 ++---------------- .../ApplicationViewPageRenderers.ts | 214 ++++++++++++++ 2 files changed, 239 insertions(+), 239 deletions(-) create mode 100644 web/src/admin/applications/ApplicationViewPageRenderers.ts diff --git a/web/src/admin/applications/ApplicationViewPage.ts b/web/src/admin/applications/ApplicationViewPage.ts index 19db71df55..47e5a3dd3d 100644 --- a/web/src/admin/applications/ApplicationViewPage.ts +++ b/web/src/admin/applications/ApplicationViewPage.ts @@ -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) { new OutpostsApi(DEFAULT_CONFIG) .outpostsInstancesList({ @@ -94,231 +96,15 @@ export class ApplicationViewPage extends AKElement { } } - render(): TemplateResult { - return html` - - - ${this.renderApp()}`; - } + render() { + const renderer = this.application + ? new ApplicationViewPageRenderer( + this.application, + this.missingOutpost, + RbacPermissionsAssignedByUsersListModelEnum.CoreApplication, + ) + : new ApplicationViewPageLoadingRenderer(); - renderApp(): TemplateResult { - if (!this.application) { - return html` - `; - } - return html` - ${this.missingOutpost - ? html`
- ${msg("Warning: Application is not used by any Outpost.")} -
` - : html``} -
-
-
-
${msg("Related")}
-
-
- ${this.application.providerObj - ? html`` - : html``} - ${(this.application.backchannelProvidersObj || []).length > 0 - ? html`
-
- ${msg("Backchannel Providers")} -
-
-
- -
-
-
` - : html``} -
-
- ${msg("Policy engine mode")} -
-
-
- ${this.application.policyEngineMode?.toUpperCase()} -
-
-
-
-
- ${msg("Edit")} -
-
-
- - ${msg("Update")} - - ${msg("Update Application")} - - - - - -
-
-
-
-
- ${msg("Check access")} -
-
-
- - ${msg("Check")} - - ${msg("Check Application access")} - - - - - -
-
-
- ${this.application.launchUrl - ? html`
-
- ${msg("Launch")} -
-
- -
-
` - : html``} -
-
-
-
-
- ${msg("Logins over the last week (per 8 hours)")} -
-
- ${this.application && - html` - `} -
-
-
-
${msg("Changelog")}
-
- - -
-
-
-
-
-
-
- ${msg("These policies control which users can access this application.")} -
- - -
-
- -
`; + return renderer.render(); } } diff --git a/web/src/admin/applications/ApplicationViewPageRenderers.ts b/web/src/admin/applications/ApplicationViewPageRenderers.ts new file mode 100644 index 0000000000..39c4d3db80 --- /dev/null +++ b/web/src/admin/applications/ApplicationViewPageRenderers.ts @@ -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` `; + } +} + +export class ApplicationViewPageRenderer { + constructor( + private app: Application, + private noOutpost: boolean, + private rbacModel: RbacPermissionsAssignedByUsersListModelEnum, + ) {} + + missingOutpostMessage() { + return this.noOutpost + ? html`
+ ${msg("Warning: Application is not used by any Outpost.")} +
` + : nothing; + } + + controlCardContents(app: Application): DescriptionPair[] { + // prettier-ignore + const rows: (DescriptionPair | null)[] = [ + app.providerObj + ? [ + msg("Provider"), + html` + + ${app.providerObj?.name} (${app.providerObj?.verboseName}) + + `, + ] + : null, + + (app.backchannelProvidersObj || []).length > 0 + ? [ + msg("Backchannel Providers"), + html` + + `, + ] + : null, + + [ + msg("Policy engine mode"), + app.policyEngineMode?.toUpperCase() + ], + + [ + msg("Edit"), + html` + + ${msg("Update")} + ${msg("Update Application")} + + + + + `, + ], + + [ + msg("Check access"), + html` + + ${msg("Check")} + ${msg("Check Application access")} + + + + + `, + ], + + app.launchUrl + ? [ + msg("Launch"), + html` + + ${msg("Launch")} + + `, + ] + : null, + ]; + + return rows.filter((row) => row !== null) as DescriptionPair[]; + } + + controlCard(app: Application) { + return html` +
+
${msg("Related")}
+
+ ${renderDescriptionList(this.controlCardContents(app))} +
+
+ `; + } + + loginsChart(app: Application) { + return html`
+
${msg("Logins over the last week (per 8 hours)")}
+
+ ${app && + html` + `} +
+
`; + } + + changelog(app: Application) { + return html` +
+
${msg("Changelog")}
+
+ + +
+
+ `; + } + + overview(app: Application) { + return html` +
+ ${this.controlCard(app)} ${this.loginsChart(app)} ${this.changelog(app)} +
+ `; + } + + policiesList(app: Application) { + return html` +
+
+ ${msg("These policies control which users can access this application.")} +
+ +
+ `; + } + + render() { + return html` + + + + ${this.missingOutpostMessage()} +
+ ${this.overview(this.app)} +
+
+ ${this.policiesList(this.app)} +
+ +
`; + } +}