* This commit abstracts access to the object `rootInterface()?.config?` into a single accessor,
`authentikConfig`, that can be mixed into any AKElement object that requires access to it.
Since access to `rootInterface()?.config?` is _universally_ used for a single (and repetitive)
boolean check, a separate accessor has been provided that converts all calls of the form:
``` javascript
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
```
into:
``` javascript
this.can(CapabilitiesEnum.CanImpersonate)
```
It does this via a Mixin, `WithCapabilitiesConfig`, which understands that these calls only make
sense in the context of a running, fully configured authentik instance, and that their purpose is to
inform authentik components of a user’s capabilities. The latter is why I don’t feel uncomfortable
turning a function call into a method; we should make it explicit that this is a relationship
between components.
The mixin has a single single field, `[WCC.capabilitiesConfig]`, where its association with the
upper-level configuration is made. If that syntax looks peculiar to you, good! I’ve used an explict
unique symbol as the field name; it is inaccessable an innumerable in the object list. The debugger
shows it only as:
Symbol(): {
cacheTimeout: 300
cacheTimeoutFlows: 300
cacheTimeoutPolicies: 300
cacheTimeoutReputation: 300
capabilities: (5) ['can_save_media', 'can_geo_ip', 'can_impersonate', 'can_debug', 'is_enterprise']
}
Since you can’t reference it by identity, you can’t write to it. Until every browser supports actual
private fields, this is the best we can do; it does guarantee that field name collisions are
impossible, which is a win.
The mixin takes a second optional boolean; setting this to true will cause any web component using
the mixin to automatically schedule a re-render if the capabilities list changes.
The mixin is also generic; despite the "...into a Lit-Context" in the title, the internals of the
Mixin can be replaced with anything so long as the signature of `.can()` is preserved.
Because this work builds off the work I did to give the Sidebar access to the configuration without
ad-hoc retrieval or prop-drilling, it wasn’t necessary to create a new context for it. That will be
necessary for the following:
TODO:
``` javascript
rootInterface()?.uiConfig;
rootInterface()?.tenant;
me();
```
* This commit abstracts access to the object `rootInterface()?.tenant?` into a single accessor,
`tenant`, that can be mixed into any AKElement object that requires access to it.
Like `WithCapabilitiesConfig` and `WithAuthentikConfig`, this one is named `WithTenantConfig`.
TODO:
``` javascript
rootInterface()?.uiConfig;
me();
```
* web: Added a README with a description of the applications' "mental model," essentially an architectural description.
* web: prettier did a thing
* web: prettier had opinions about the README
* web: Jens requested that subscription be by default, and it's the right call.
* web: Jens requested that the default subscription state for contexts be , and it's the right call.
* web: prettier having opinions after merging with dependent branch
* web: prettier still having opinions.
* This commit abstracts access to the object `rootInterface()?.config?` into a single accessor,
`authentikConfig`, that can be mixed into any AKElement object that requires access to it.
Since access to `rootInterface()?.config?` is _universally_ used for a single (and repetitive)
boolean check, a separate accessor has been provided that converts all calls of the form:
``` javascript
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
```
into:
``` javascript
this.can(CapabilitiesEnum.CanImpersonate)
```
It does this via a Mixin, `WithCapabilitiesConfig`, which understands that these calls only make
sense in the context of a running, fully configured authentik instance, and that their purpose is to
inform authentik components of a user’s capabilities. The latter is why I don’t feel uncomfortable
turning a function call into a method; we should make it explicit that this is a relationship
between components.
The mixin has a single single field, `[WCC.capabilitiesConfig]`, where its association with the
upper-level configuration is made. If that syntax looks peculiar to you, good! I’ve used an explict
unique symbol as the field name; it is inaccessable an innumerable in the object list. The debugger
shows it only as:
Symbol(): {
cacheTimeout: 300
cacheTimeoutFlows: 300
cacheTimeoutPolicies: 300
cacheTimeoutReputation: 300
capabilities: (5) ['can_save_media', 'can_geo_ip', 'can_impersonate', 'can_debug', 'is_enterprise']
}
Since you can’t reference it by identity, you can’t write to it. Until every browser supports actual
private fields, this is the best we can do; it does guarantee that field name collisions are
impossible, which is a win.
The mixin takes a second optional boolean; setting this to true will cause any web component using
the mixin to automatically schedule a re-render if the capabilities list changes.
The mixin is also generic; despite the "...into a Lit-Context" in the title, the internals of the
Mixin can be replaced with anything so long as the signature of `.can()` is preserved.
Because this work builds off the work I did to give the Sidebar access to the configuration without
ad-hoc retrieval or prop-drilling, it wasn’t necessary to create a new context for it. That will be
necessary for the following:
TODO:
``` javascript
rootInterface()?.uiConfig;
rootInterface()?.tenant;
me();
```
* web: Added a README with a description of the applications' "mental model," essentially an architectural description.
* web: prettier had opinions about the README
* web: Jens requested that subscription be by default, and it's the right call.
* This commit abstracts access to the object `rootInterface()?.config?` into a single accessor,
`authentikConfig`, that can be mixed into any AKElement object that requires access to it.
Since access to `rootInterface()?.config?` is _universally_ used for a single (and repetitive)
boolean check, a separate accessor has been provided that converts all calls of the form:
``` javascript
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
```
into:
``` javascript
this.can(CapabilitiesEnum.CanImpersonate)
```
It does this via a Mixin, `WithCapabilitiesConfig`, which understands that these calls only make
sense in the context of a running, fully configured authentik instance, and that their purpose is to
inform authentik components of a user’s capabilities. The latter is why I don’t feel uncomfortable
turning a function call into a method; we should make it explicit that this is a relationship
between components.
The mixin has a single single field, `[WCC.capabilitiesConfig]`, where its association with the
upper-level configuration is made. If that syntax looks peculiar to you, good! I’ve used an explict
unique symbol as the field name; it is inaccessable an innumerable in the object list. The debugger
shows it only as:
Symbol(): {
cacheTimeout: 300
cacheTimeoutFlows: 300
cacheTimeoutPolicies: 300
cacheTimeoutReputation: 300
capabilities: (5) ['can_save_media', 'can_geo_ip', 'can_impersonate', 'can_debug', 'is_enterprise']
}
Since you can’t reference it by identity, you can’t write to it. Until every browser supports actual
private fields, this is the best we can do; it does guarantee that field name collisions are
impossible, which is a win.
The mixin takes a second optional boolean; setting this to true will cause any web component using
the mixin to automatically schedule a re-render if the capabilities list changes.
The mixin is also generic; despite the "...into a Lit-Context" in the title, the internals of the
Mixin can be replaced with anything so long as the signature of `.can()` is preserved.
Because this work builds off the work I did to give the Sidebar access to the configuration without
ad-hoc retrieval or prop-drilling, it wasn’t necessary to create a new context for it. That will be
necessary for the following:
TODO:
``` javascript
rootInterface()?.uiConfig;
rootInterface()?.tenant;
me();
```
* web: Added a README with a description of the applications' "mental model," essentially an architectural description.
* web: prettier had opinions about the README
* web: Jens requested that subscription be by default, and it's the right call.
* web: adjust RAC to point to the (now independent) Interface.
- Also, removed redundant check.
This commit adds "Polish" and "Korean" to the list of languages recognized by the
web-UI, and updates the XLIFF files to include a few new strings from the RAC
project.
By adding 'grow' but not 'shrink' to the header section, the page was allowed to allocate
as much width as was available when the window opened, but not allowed to resize the width
if it was pushed closed by zoom, page resize, or summon sidebar.
This commit adds 'shrink' to the capabilities of the header.
* web: break circular dependency between AKElement & Interface.
This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.
I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.
* web: fix broken typescript references
This built... and then it didn't? Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.
* Refactor the Table component for legiibility.
This commit does not change the functionality of the Table, nor does it require any changes to
existing uses of the Table.
It will probably be easier to review this by looking at the `View Code` in the upper-right-hand
corner of GitHub's reviewer; that or side-by-side, if your monitor is wide-enough.
The existing Table component is used 49 times (at last count) in authentik, and those uses are
wide-ranging and complex, but they all come down to a couple of entries:
- Displaying a row of summary information
- Permitting the display of more complex ("expanded") information
- Displaying a collection of rows
- Displaying a collection of rows grouped by some header
- Pagination of many rows
- Permitting an action on the visible rows
- *Not* blocking events that may happen on a cell or expansion
- Providing a toolbar
- Providing a display of "selected items" when using the table as a multi-select with many pages of
items (chips display)
- Providing sort functionality on columns
- Providing the ability to filter the table from the back-end
This commit changes none of that.
What this commit does is re-arrange the innards of Table.ts into smaller units:
- The RowGroup's "checkbox" and "expansion" segments are pulled out into their own functions, which
makes the RowGroup's actual functionality much easier to see and understand. The same is true of
the rowGroup's selection and expansion handlers.
- Almost all in-line decisions and event handlers have been extracted and named, to make it easier
to see and understand what's happening inside what is otherwise a jumble of HTML.
- The TablePagination code was duplicated-- and one of the duplicates was wrong! So I've
deduplicated it and fixed the bug.
- In many cases, the conditional code grew organically, resulting in some pretty hard-to-understand
conditions.
- A really good example is the `itemSelectHandler`; there are two possible events that result in a
change, and the consequences of that change may be that *all* checkboxes are unchecked. In all
cases where there's an add/remove option, I've opted to remove the specific object always (even
if it's not present!), and then add it if it's actually an add. Logically coherent as long as
the accessors are not also mutators.
It was not possible to redefine the `columns()` function to take anything other than a TableColumn
object; I wanted to be able to replace all of the `new TableColumn("Foo")` with just `"Foo"`,
building the TableColumn dynamically at construction time. Unfortunately, some of our most complex
tables dynamically re-arrange the columns (RBAC, for example, draws an empty table, fetches the
content, then redraws with the columns based on what was retrieved), and detecting that change and
rebuilding those columns proved more difficult than anticipated. I may contemplate an alternative
column specification if I find myself building a lot of tables.
Likewise, it was not possible to replace all of our uses of the empty `html` declaration with the
Lit-preferred `nothing` sigil; hard-coded `TemplateResult` entries scattered throughout the code
caused massive type inconsistencies, since a type of `TemplateResult | nothing` is unique thanks to
`nothing`'s underlying Symbol. It is for this issue that Typescript itself recommends you "prefer
allowing Typescript infer the return type." I may revisit this issue later.
I've added a `prequick` command to `package.json`; this one runs *only* the Typescript type checker,
lit-analyse, and `eslint:precommit`, the last of which lints only the files touched since the last
commit. This is fast, intended to support quick checks of code quality not normally displayed in the
IDE.
* web: refactor table
After talking to Jens, I've put back the positional variable and eslint escape; it's better
to document existing practices than try to force something.
I also misunderstood the role of `inner` in one bit of code, and have restored its functionality.
Looking through the code, though, I can see a case where it will fail; it's expecting `inner` to
be either undefined or a TemplateResult; if there's no error message, the error message defaults
to a blank TemplateResult, which is _not_ undefined, and will result in a blank table.
This will only happen under very weird network failures, but...
* web: break circular dependency between AKElement & Interface.
This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.
I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.
* web: fix broken typescript references
This built... and then it didn't? Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.
* web: extract the form processing from the form submission process
Our forms have a lot of customized value handling, and the function `serializeForm` takes
our input structures and creates a JSON object ready for submission across the wire for
the various models provided by the API.
That function was embedded in the `ak-form` object, but it has no actual dependencies on
the state of that object; aside from identifying the input elements, which is done at the
very start of processing, this large block of code stands alone. Separating out the
"processing the form" from "identifying the form" allows us to customize our form handling
and preserve form information on the client for transactional purposes such as our wizard.
w
* web: multi-select, but there's a styling issue.
* web: provide a closed control for multi-select
This commit creates a new control, using the ak-form-element-horizontal as a *CLOSED*
object, for our multi-select. This control right now is limited to what we expect to
be using in the wizard, but that doesn't mean it can't be smarter in the future.
* web: hung up by a silly spelling error
* web: update the form-handling method
With the `serializeForm` method extracted, it's much easier to examine and parse
every *form* with every keystroke, preserving them against the changes that
happen as the customer navigates the Wizard. With that in place, it became
straightforward to retrofit the "handle changes to the application, to the provider, and to the providerType"
into the three pages of the wizard, and to provide *all* of the form elements in a base class
such that no specialized handling needs to happen to any of the child pages.
Fixed an ugly typo in the oauth2 provider, as well.
* web: wizard should work with multi-select and should reflect default values
(Note: This commit is predicated on both the "Extract serializeForm function from Form.ts" and
"Provide a controlled multi-select input control" PRs.)
The initial attempt at the wizard was woefully naive in its implementation, missing some critical
details along the way. This revision starts off with one stronger assumption: trust that Jens knows
what he's doing, and knew what he was building when he wrote the initial `Form` handler.
The problem with the `Form` handler, and the reason I avoided it, was simply that it does too many
things, especially in its ModelForm variant: it receives a model from the back-end, renders a
(hand-written) form for that model, allows the user to interact with that model, and facilitates
saving it to the back-end again, complete with on-page notifications of success or failure.
The Wizard could not use all of that. It needs to gather the information for *two* models (an
Application and a Provider, plus the ProviderType) and has a new and specialized end-point for a
transaction that allows the committing or roll back of both models to happen simultaneously,
predicated on success or failure respectively.
With "Extract `serializeForm` completed, it was possible to repurpose the forms that already
existed, stripping them down to just their input components, and eventing the entire thing in a
single event loop of "events flow up, data flows down." In this case, the *entire form* is
serialized on a per-event basis and pushed up the to the orchestration layer, which saves them off.
Writing a parent `BasePanel` class that has accessors for `formValues` and `valid` means that the
state of every page is accessible with a simple query. This simplified the `BaseProviderPanel` class
to just specialize the `dispatchUpdate` method to send the wizard update with the new provider
information filled out.
Because the *form* is being treated as the source of truth about the state of a `Partial<Application>`
or `Partial<*Provider>` object, the defaults are now being captured as expected.
Likewise, this simplified the `providerCache` layer which preserves customer input in the event that
the customer starts filling out the wrong provider to a simple conditional clause in the
orchestrator. The Wizard has much fewer smarts because it doesn't (and probably never did) need
them.
Along with the above changes, the following has also been done:
For SAML and SCIM, the providerMappings now works. They weren't being managed as `state` objects,
so they weren't receiving updates when the update event retrieved the information from the back-end.
In order to make clear what's happening, I have extracted the loops from the original definition and
built them as named objects: `propertyMappings`, `pmUserValues`, `pmGroupValues` and so on, which I
then pass into the new multi-select component.
I fixed a really embarrassing typo in Oauth2's "advanced settings" block.
I have extracted the CoreGroup search-select into a custom component.
I deleted the `merge` function. That was a faulty experiment with non-deterministic outcomes, and I
was never happy with it. I'm glad its gone.
I've added a title header to each of the providers, so the user can be sure that they're looking
at the right provider type when they start filling out the form.
I've created a new token, `data-ak-control`, with which we can mark all objects that we can treat as
Authentik value-producing components, the form value of which is available through a `json()`
method. I've added this bit of intelligence to the `serializeForm` function, short-circuiting the
complex processing and putting the "this is the shape of the value we expect from this input" *onto
the input itself*. Which is where it belongs.
* web: add error handling to wizard.
* web: improve error handling in light components
Rather than reproduce the error handling across all of the LightComponents,
I've made a parent class that takes the common fields to distribute between
the ak-form-element-horizontal and the input object itself. This made it
much easier to properly display errors in freeform input fields in the
wizard, as well as working with the routine error handling in Form.ts
* Added the radio control to the list of LightComponents.
* Fix bug where event was recorded twice.
* Fixed merge bug (?) that somehow deleted the Authorization Select block in OAuth2.
* web: prettier had opinions
* web: added error handling and display
* web: bump @lit-labs/context from 0.4.1 to 0.5.1 in /web
Bumps [@lit-labs/context](https://github.com/lit/lit/tree/HEAD/packages/labs/context) from 0.4.1 to 0.5.1.
- [Release notes](https://github.com/lit/lit/releases)
- [Changelog](https://github.com/lit/lit/blob/main/packages/labs/context/CHANGELOG.md)
- [Commits](https://github.com/lit/lit/commits/@lit-labs/context@0.5.1/packages/labs/context)
---
updated-dependencies:
- dependency-name: "@lit-labs/context"
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
* web: updated wizard to run with latest package.json configuration
Apparently, there were stale dependencies in package-lock.json that were conflicting
with the requests in our package.json. By running `npm update`, I was able to resolve
the conflict.
I have also removed the default names from the context names collection; they weren't doing
any good, and they permit frictionless renaming of dependencies, which is never a good
idea.
* web: schlepping on the errors messages
During testing, I realized I was unhappy with the error messages. They're not very helpful.
By adding links to navigate back to the place where the error occurred, and providing better
context for what the error could have been, I hope to help the use correct their errors.
* make package the same as main
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
* web: break circular dependency between AKElement & Interface.
This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.
I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.
* web: fix broken typescript references
This built... and then it didn't? Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.
* A quality of life thing: `<ak-status-label good>`
There's an idiom throughout the UI:
``` HTML
<ak-label color=${item.enabled ? PFColor.Green : PFColor.Red}>
${item.enabled ? msg("Yes") : msg("No")}
</ak-label>
```
There are two problems with this.
- Repeating the conditional multiple times is error-prone
- The color scheme doesn't communicate much.
There are uses for ak-label that aren't like this, but I'm focusing on this particular use case,
which occurs about 20 times throughout the UI.
Since it's so common, let's isolate the most common case: `<ak-status-label good />` gives you the
"good" status, and `<ak-status-label/>` gives you the "bad" status, which is the default (no
arguments to the function).
There wasn't much clarity in the system for when to use orange vs red vs grey, but looking through
the use cases, it became clear that Red meant fail/inaccessible, Orange meant "Warning, but not
blocking," and Grey just means "info: this thing is off".
So let's define that with meaning: there are three types, error, warning, and info. Which
corresponds to debugging levels, but whatever, nerds grok that stuff.
So that example at the top becomes
```<ak-status-label ?good=${item.enabled}></ak-status-label>```
... and we can now more clearly understand what that conveys.
There is some heavy tension in this case: this is an easier and quicker-to-write solution to
informing the user of a binary status in an iconic way, but the developer has to remember that it
exists.
Story provided, and changes to the existing uses of the existing idiom provided.
* Added the 'compact label' story to storybook.
* web: break circular dependency between AKElement & Interface.
This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.
I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.
* web: fix broken typescript references
This built... and then it didn't? Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.
* web: rollback dependabot's upgrade of context
The most frustrating part of this is that I RAN THIS, dammit, with the updated
context and the current Wizard, and it finished the End-to-End tests without
complaint.
* Due for amendment
* Revert "Due for amendment"
This reverts commit 829ad5d3f2.
* web: refactor sidebar capabilities for categorical subsections
The project "Change Admin UI lists to have sublists per type" requires some initial changes to the
UI to facilitate this request. The AdminSidebar is the principle target of this project, and it is
embedded in the AdminInterface. To facilitate editing the AdminSidebar as an independent entity,
AdminInterface has been moved into its own folder and the AdminSidebar extracted as a standalone Web
Component. This removes, oh, about half the code from AdminInterface. A little cleanup with
`classMap` was also committed.
The rollup config was adjusted to find the new AdminInterface location.
The Sidebar uses the global `config: Config` object to check for Enterprise capabilities. Rather
than plumb all the way down through the Interface => AdminInterface -> AdminSidebar, I chose to make
provide an alternative way of reaching the `config` object, as a *context*. Other configuration
objects (Me, UiConfig, Tenant) interfaces will be contextualized as demand warrants.
Demand will warrant. Just not yet. <sup>1</sup>
The Sidebar has been refactored only slightly; the renderers are entirely the same as they were
prior to extraction. What has been changed is the source of information: when we retrieve the
current version we story *only* the information, and use type information to ensure that the version
we store is the version we care about. The same is true of `impersonation`; we care only about the
name of the person being impersonated being present, so we don't store anything else.
Fetches have been moved from `firstUpdated` to the constructor. No reason to have the sidebar
render twice if the network returns before the render is scheduled.
Because the path used to identify the user being impersonated has changed, the `str()` references in
the XLIFF files had to be adjusted. **This change is to a variable only and does not require
translation.**
---
<sup>1</sup> The code is littered with checks to `me()?`, `uiConfig?`, `config?`, etc. In the
*context* of being logged in as an administrator those should never be in doubt. I intend to make
our interfaces not have any doubt.
* Function to help generate sizing solutions across Javascript and CSS.
* web: refactor sidebar capabilities for categorical subsections
Move open/close logic into the ak-admin-sidebar itself.
This commit removes the responsibility for opening/closing the sidebar from the interface parent
code and places it inside the sidebar entirely. Since the Django invocation passes none of the
properties ak-interface-admin is capable of receiving, this seems like a safe operation.
The sidebar now assumes the responsibility for hooking up the window event listeners for open/close
and resize.
On connection to the DOM, and on resize, the sidebar checks to see if the viewport width meets the
criteria for a behavioral change (slide-overlay vs slide-push), and on slide-push automatically
opens the sidebar on the assumption that there's plenty of room. In order to support more dynamic
styling going forward, I've substituted the 1280px with 80rem, which is the same, but allows for
some better styling if someone with older eyes needs to "zoom in" on the whole thing with a larger
font size.
The hide/show code involves "reaching up" to touch the host's classList. There's a comment
indicating that this is a slightly fragile thing to do, but in a well-known way.
* web: break circular dependency between AKElement & Interface.
This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.
I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.
* web: fix broken typescript references
This built... and then it didn't? Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.
* web: extract the form processing from the form submission process
Our forms have a lot of customized value handling, and the function `serializeForm` takes
our input structures and creates a JSON object ready for submission across the wire for
the various models provided by the API.
That function was embedded in the `ak-form` object, but it has no actual dependencies on
the state of that object; aside from identifying the input elements, which is done at the
very start of processing, this large block of code stands alone. Separating out the
"processing the form" from "identifying the form" allows us to customize our form handling
and preserve form information on the client for transactional purposes such as our wizard.
w
* web: break circular dependency between AKElement & Interface.
This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.
I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.
* web: fix broken typescript references
This built... and then it didn't? Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.
We would like to use the clipboard for more than just the token copy button. This commit
enables that by separating the "Write to Clipboard" and "Write to Notifications" routines
into separate functions, putting "writeToClipboard" into the utilities collection, and
clarifying what happens when a custom presses the TokenCopy button.
* Updates to the Context and Tasks libraries from lit.
* web: fix for bad merge
* Still trying to solve that f*&!ing merge bug.
* fix build
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* Updates to the Context and Tasks libraries from lit.
* web: fix for bad merge
* Still trying to solve that f*&!ing merge bug.
* fix build
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---------
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
* web: the return of pseudolocalization
The move to lit-locale lost the ability to automagically pseudolocalize the UI, a useful
utility for checking that additions to the UI have been properly cataloged as
translation targets. This short script (barely 40 lines) digs deep into the lit-localize
toolkit and produces a pretranslated translation bundle in the target format folder.
* Linted, prettied, and commented.
* web/elements: rename renderInlineForm to renderForm set submit handler to empty function
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* fix all kinds of forms not using the form inheritance correctly
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---------
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* web: laying the groundwork for future expansion
This commit is a hodge-podge of updates and changes to the web. Functional changes:
- Makefile: Fixed a bug in the `help` section that prevented the WIDTH from being accurately
calculated if `help` was included rather than in-lined.
- ESLint: Modified the "unused vars" rule so that variables starting with an underline are not
considered by the rule. This allows for elided variables in event handlers. It's not a perfect
solution-- a better one would be to use Typescript's function-specialization typing, but there are
too many places where we elide or ignore some variables in a function's usage that switching over
to specialization would be a huge lift.
- locale: It turns out, lit-locale does its own context management. We don't need to have a context
at all in this space, and that's one less listener we need to attach t othe DOM.
- ModalButton: A small thing, but using `nothing` instead of "html``" allows lit better control over
rendering and reduces the number of actual renders of the page.
- FormGroup: Provided a means to modify the aria-label, rather than stick with the just the word
"Details." Specializing this field will both help users of screen readers in the future, and will
allow test suites to find specific form groups now.
- RadioButton: provide a more consistent interface to the RadioButton. First, we dispatch the
events to the outside world, and we set the value locally so that the current `Form.ts` continues
to behave as expected. We also prevent the "button lost value" event from propagating; this
presents a unified select-like interface to users of the RadioButtonGroup. The current value
semantics are preserved; other clients of the RadioButton do not see a change in behavior.
- EventEmitter: If the custom event detail is *not* an object, do not use the object-like semantics
for forwarding it; just send it as-is.
- Comments: In the course of laying the groundwork for the application wizard, I throw a LOT of
comments into the code, describing APIs, interfaces, class and function signatures, to better
document the behavior inside and as signposts for future work.
* web: permit arrays to be sent in custom events without interpolation.
* actually use assignValue or rather serializeFieldRecursive
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* web: package up horizontal elements into their own components.
This commit introduces a number of "components." Jens has this idiom:
```
<ak-form-element-horizontal label=${msg("Name")} name="name" ?required=${true}>
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
```
It's a very web-oriented idiom in that it's built out of two building blocks, the "element-horizontal" descriptor,
and the input object itself. This idiom is repeated a lot throughout the code. As an alternative, let's wrap
everything into an inheritable interface:
```
<ak-text-input
name="name"
label=${msg("Name")}
value="${ifDefined(this.instance?.name)}
required
>
</ak-text-input>
```
This preserves all the information of the above, makes it much clearer what kind of interaction we're having
(sometimes the `type=` information in an input is lost or easily missed), and while it does require you know
that there are provided components rather than the pair of layout-behavior as in the original it also gives
the developer more precision over the look and feel of the components.
*Right now* these components are placed into the LightDOM, as they are in the existing source code, because
the Form handler has a need to be able to "peer into" the "element-horizontal" component to find the values
of the input objects. In a future revision I hope to place the burden of type/value processing onto the
input objects themselves such that the form handler will need only look for the `.value` of the associated
input control.
Other fixes:
- update the FlowSearch() such that it actually emits an input event when its value changes.
- Disable the storybook shortcuts; on Chrome, at least, they get confused with simple inputs
- Fix an issue with precommit to not scan any Python with ESLint! :-)
* web: provide storybook stories for the components
This commit provides storybook stories for the ak-horizontal-element wrappers. A few
bugs were found along the way, including one rather nasty one from Radio where we
were still getting the "set/unset" pair in the wrong order, so I had to knuckle down
and fix the event handler properly.
* web: test oauth2 provider "guinea pig" for new components
I used the Oauth2 provider page as my experiment in seeing if the
horizontal-element wrappers could be used instead of the raw wrappers
themselves, and I wanted to make sure a test existed that asserts
that filling out THAT form in the ProvidersList and ProvidersForm
didn't break anything.
This commit updates the WDIO tests to do just that; the test is
simple, but it does exercise the `name` field of the Provider,
something not needed in the Wizard because it's set automatically
based on the Application name, and it even asserts that the new
Provider exists in the list of available Providers when it's done.
* web: making sure ESlint and Prettier are happy
* "fix" lint
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---------
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
* web: laying the groundwork for future expansion
This commit is a hodge-podge of updates and changes to the web. Functional changes:
- Makefile: Fixed a bug in the `help` section that prevented the WIDTH from being accurately
calculated if `help` was included rather than in-lined.
- ESLint: Modified the "unused vars" rule so that variables starting with an underline are not
considered by the rule. This allows for elided variables in event handlers. It's not a perfect
solution-- a better one would be to use Typescript's function-specialization typing, but there are
too many places where we elide or ignore some variables in a function's usage that switching over
to specialization would be a huge lift.
- locale: It turns out, lit-locale does its own context management. We don't need to have a context
at all in this space, and that's one less listener we need to attach t othe DOM.
- ModalButton: A small thing, but using `nothing` instead of "html``" allows lit better control over
rendering and reduces the number of actual renders of the page.
- FormGroup: Provided a means to modify the aria-label, rather than stick with the just the word
"Details." Specializing this field will both help users of screen readers in the future, and will
allow test suites to find specific form groups now.
- RadioButton: provide a more consistent interface to the RadioButton. First, we dispatch the
events to the outside world, and we set the value locally so that the current `Form.ts` continues
to behave as expected. We also prevent the "button lost value" event from propagating; this
presents a unified select-like interface to users of the RadioButtonGroup. The current value
semantics are preserved; other clients of the RadioButton do not see a change in behavior.
- EventEmitter: If the custom event detail is *not* an object, do not use the object-like semantics
for forwarding it; just send it as-is.
- Comments: In the course of laying the groundwork for the application wizard, I throw a LOT of
comments into the code, describing APIs, interfaces, class and function signatures, to better
document the behavior inside and as signposts for future work.
* web: permit arrays to be sent in custom events without interpolation.
* actually use assignValue or rather serializeFieldRecursive
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---------
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
web/improve testability
This is a trio of small hacks that allow the E2E tests to find several components
on the page while the test is running:
- Add a `data-managed-for` field to SearchSelect's positioned elements. If a search
has a `name` field, it will be reflected here, allowing tests to find specific
instances of the dropdown elements.
- Add a forwarder to the search select wrappers we use for our SearchSelect.
- Added aria details to the UserLibrary header to make it easy to identify.
* Web: Detangling some circular dependencies in Admin and User
Admin, User, and Flow should not dependend upon each other, at least
not in a circular way. If Admin and User depend on Flow, that's
fine, but Flow should not correspondingly depend upon elements of
either; if they have something in common, let's put them in
`@goauthentik/common` or find some other smart place to store them.
This commit refactors the intentToLabel and actionToLabel functions
into `@goauthentik/common/labels` and converts them to static tables
for maintenance purposes.
* web: "Consistency is the hobgoblin of small minds" - Ralph Waldo Emerson
* web: I found these confusing to look at, so I added comments.
* web: remove admin-to-user component reference(s)
There was only one: AppIcon. This has been moved to `components`.
Touching the LibraryApplications page triggered a cyclomatic
complexity check. Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.
* web: remove admin from elements
This commit removes the two references from `elements` to `admin`: the list of UserEvents and a
reference to the FlowSearch type, used by the Forms manager to decide how to extract a value.
For FlowSearch, a different convention for detecting the type was implemented (instances of the
object have a unique fieldname for the value holder). UserEvents and ObjectChangelog have been
moved to `components` as they're clearly dependent upon the API.
This defers work on removing Admin from Components, as that is (again) references going the
wrong way, but that can happen later.
* web: remove admin-to-user component reference(s) (#6856)
There was only one: AppIcon. This has been moved to `components`.
Touching the LibraryApplications page triggered a cyclomatic
complexity check. Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.
* This was supposed to be merged.
* web: remove `./element`⇢`./user` references
The offender here is UserDevicesList, which despite being in `elements` is only
used by the admin/user/UserViewPage. The problem is that UserDevicesList,
despite being in `admin`, inherits from `user`, so moving it would have created
a new admin⇢user reference, and the whole point of this exercise is to get rid
of references that point "up" from the foundational pieces to the views, or
that refer to components in sibling applications.
After examining UserDevicesList, I realized that *every feature* of MFADevicesList
had been overridden: the rows, the columns, the toolbar, and the endpoint all had
custom overrides. Nothing was left of MFADevicesList after that. Even the
property that the web component used had been completely changed. The only thing
they had in common was that they both inherited from `Table<Device>`.
Refactoring UserDevicesList so that it inherited directly from `Table<Device>` and
then moving it into `./admin/users` was the obvious and correct step.
Both used the same label table, so that went into the `common/labels` folder.
Along the way, I cleaned up a few minor details. Just little things, like the repeated invocation
of:
```
new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorAdminMETHODDestroy({ id: device.pk });
```
This is repeated five times, once for each Method. By creating these:
```
const api = new AuthenticatorsApi(DEFAULT_CONFIG);
const id = { id: device.pk };
```
The method invocation could be just `api.authenticatorsMETHODDestroy(id)`, which is easier on the
eyes. See the MFADevicesPage for the full example.
Similarly,
```
return [
new TableColumn(msg("Name"), ""),
new TableColumn(msg("Type"), ""),
new TableColumn("")
];
```
is more straightforward as:
```
const headers = [msg("Name"), msg("Type"), ""];
return headers.map((th) => new TableColumn(th, ""));
```
We've labeled what we're working with, and web developers ought to know that `th` is the HTML code
for `table header`.
I've had to alter what files are scanned in pre-commit mode; it doesn't handle renamed files very well,
and at the moment a file that is renamed is not scanned, as its "new" name is not straightforwardly
displayed, not even by `git porcelain`.
* web: make the table of column headers look like a table
* web: detangle `common` from `elements`.
And just like that, `common` no longer has a reference to `elements`. I don't mind this little bit of
code duplication if it removes a cycle. What it does point out is that there are bits of `common` that
are predicated on the presence of the browser, and that there are bits of `elements` that, if they rely
on `common`, can't be disentangled from the application as a whole. Which seems to me that we have two
different things going on in common: things about an application, and things about elements that are
independent of the application.
I'll think about those later.
```
$ rg 'import.*@goauthentik' ./common/ | perl -ne 'm{"(@goauthentik[^"]*)"} && print "$1\n"' | sort | cut -d '/' -f1-2 | uniq | sort
@goauthentik/api
@goauthentik/common
$
```
* web: odd bug; merge-related? Gonna investigate.
* web: build failure thanks to local cache; fixed
* web: detangle `components` from `admin`.
This was the last inappropriate reference: something from `./components` referencing something in
`./admin`, in this case the `ak-event-info` component. Used by both Users and Admin, moving it
into `./components` was the obvious correct step.
`ak-event-info` is a lookup table relating specific events in the event log to rich, textual
representations; in the special case of model changes and email info, even more rich content is
available in a dl/dt format. I've tableized the model changes and email info renderer, and I've
extracted every event's textual representation into its own method, converting the `switch/case`
rendering statement into a `switch/case` dispatch switch. This has the virtue of isolating each
unique case and making the dispatch switch short and coherent.
The conversion was done mechanistically; I gave the refactorer (Tide, in this case) instructions to
duplicate the switch block and then convert every case into a method with a name patterned on the
`case`. Going back to the original switch block, it was easy to duplicate the pattern matching and
convert it into a dispatch switch.
And with this, there are zero cycles in the references between the different "packageable" sections
of the UI. The only thing left to do is figure out how to redistribute `./elements` and `./components`
in a way that makes sense for each.
* Changed function name from 'emailMessageBody' to 'githubIssueMessageBody' to better reflect its usage.
* web: added comments about length and purpose of githubIssueMessageBody.
* Update web/src/common/labels.ts
Co-authored-by: Jens L. <jens@goauthentik.io>
Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
* Unwanted change.
* web/add tooltip buttons to user details page
This commit wraps the command buttons on the UserDetailsPage with tooltips providing greater copy
explaining what each button does. It also ensures that every button is a minimum of 11ems in width
(The longest phrase, 'Reset Password', results in a width of 10.75ems; this makes them all
consistent.)
The technique for giving the `ak-action-button` objects a mininum width uses the CSS `::part()`
syntax, which is new. CanIUse shows that it's at 95.3% of global usage; our weak points remain Opera
Mini and UC Browser for Android.
Oh, and IE. But the various Powers That Be™ agree we're no longer tracking or caring about IE.
* I added some text, so it's my responibility to add the language files.
* fix text
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* rework
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* web: enforce a max-width on the container for the buttons so that they don't look funky on ultrawide monitors.
* wbe: re-ran and confirmed prettier.
---------
Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
* Web: Detangling some circular dependencies in Admin and User
Admin, User, and Flow should not dependend upon each other, at least
not in a circular way. If Admin and User depend on Flow, that's
fine, but Flow should not correspondingly depend upon elements of
either; if they have something in common, let's put them in
`@goauthentik/common` or find some other smart place to store them.
This commit refactors the intentToLabel and actionToLabel functions
into `@goauthentik/common/labels` and converts them to static tables
for maintenance purposes.
* web: "Consistency is the hobgoblin of small minds" - Ralph Waldo Emerson
* web: I found these confusing to look at, so I added comments.
* web: remove admin-to-user component reference(s)
There was only one: AppIcon. This has been moved to `components`.
Touching the LibraryApplications page triggered a cyclomatic
complexity check. Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.
* web: remove admin from elements
This commit removes the two references from `elements` to `admin`: the list of UserEvents and a
reference to the FlowSearch type, used by the Forms manager to decide how to extract a value.
For FlowSearch, a different convention for detecting the type was implemented (instances of the
object have a unique fieldname for the value holder). UserEvents and ObjectChangelog have been
moved to `components` as they're clearly dependent upon the API.
This defers work on removing Admin from Components, as that is (again) references going the
wrong way, but that can happen later.
* web: remove admin-to-user component reference(s) (#6856)
There was only one: AppIcon. This has been moved to `components`.
Touching the LibraryApplications page triggered a cyclomatic
complexity check. Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.
* This was supposed to be merged.
* web: remove `./element`⇢`./user` references
The offender here is UserDevicesList, which despite being in `elements` is only
used by the admin/user/UserViewPage. The problem is that UserDevicesList,
despite being in `admin`, inherits from `user`, so moving it would have created
a new admin⇢user reference, and the whole point of this exercise is to get rid
of references that point "up" from the foundational pieces to the views, or
that refer to components in sibling applications.
After examining UserDevicesList, I realized that *every feature* of MFADevicesList
had been overridden: the rows, the columns, the toolbar, and the endpoint all had
custom overrides. Nothing was left of MFADevicesList after that. Even the
property that the web component used had been completely changed. The only thing
they had in common was that they both inherited from `Table<Device>`.
Refactoring UserDevicesList so that it inherited directly from `Table<Device>` and
then moving it into `./admin/users` was the obvious and correct step.
Both used the same label table, so that went into the `common/labels` folder.
Along the way, I cleaned up a few minor details. Just little things, like the repeated invocation
of:
```
new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorAdminMETHODDestroy({ id: device.pk });
```
This is repeated five times, once for each Method. By creating these:
```
const api = new AuthenticatorsApi(DEFAULT_CONFIG);
const id = { id: device.pk };
```
The method invocation could be just `api.authenticatorsMETHODDestroy(id)`, which is easier on the
eyes. See the MFADevicesPage for the full example.
Similarly,
```
return [
new TableColumn(msg("Name"), ""),
new TableColumn(msg("Type"), ""),
new TableColumn("")
];
```
is more straightforward as:
```
const headers = [msg("Name"), msg("Type"), ""];
return headers.map((th) => new TableColumn(th, ""));
```
We've labeled what we're working with, and web developers ought to know that `th` is the HTML code
for `table header`.
I've had to alter what files are scanned in pre-commit mode; it doesn't handle renamed files very well,
and at the moment a file that is renamed is not scanned, as its "new" name is not straightforwardly
displayed, not even by `git porcelain`.
* web: make the table of column headers look like a table
* web: build failure thanks to local cache; fixed
* Update web/src/common/labels.ts
Co-authored-by: Jens L. <jens@goauthentik.io>
Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
---------
Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
* Web: Detangling some circular dependencies in Admin and User
Admin, User, and Flow should not dependend upon each other, at least
not in a circular way. If Admin and User depend on Flow, that's
fine, but Flow should not correspondingly depend upon elements of
either; if they have something in common, let's put them in
`@goauthentik/common` or find some other smart place to store them.
This commit refactors the intentToLabel and actionToLabel functions
into `@goauthentik/common/labels` and converts them to static tables
for maintenance purposes.
* web: "Consistency is the hobgoblin of small minds" - Ralph Waldo Emerson
* web: I found these confusing to look at, so I added comments.
* web: remove admin-to-user component reference(s)
There was only one: AppIcon. This has been moved to `components`.
Touching the LibraryApplications page triggered a cyclomatic
complexity check. Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.
* web: remove admin from elements
This commit removes the two references from `elements` to `admin`: the list of UserEvents and a
reference to the FlowSearch type, used by the Forms manager to decide how to extract a value.
For FlowSearch, a different convention for detecting the type was implemented (instances of the
object have a unique fieldname for the value holder). UserEvents and ObjectChangelog have been
moved to `components` as they're clearly dependent upon the API.
This defers work on removing Admin from Components, as that is (again) references going the
wrong way, but that can happen later.
* web: remove admin-to-user component reference(s) (#6856)
There was only one: AppIcon. This has been moved to `components`.
Touching the LibraryApplications page triggered a cyclomatic
complexity check. Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.
* This was supposed to be merged.
* Web: Detangling some circular dependencies in Admin and User
Admin, User, and Flow should not dependend upon each other, at least
not in a circular way. If Admin and User depend on Flow, that's
fine, but Flow should not correspondingly depend upon elements of
either; if they have something in common, let's put them in
`@goauthentik/common` or find some other smart place to store them.
This commit refactors the intentToLabel and actionToLabel functions
into `@goauthentik/common/labels` and converts them to static tables
for maintenance purposes.
* web: "Consistency is the hobgoblin of small minds" - Ralph Waldo Emerson
* web: I found these confusing to look at, so I added comments.
* web: remove admin-to-user component reference(s) (#6856)
There was only one: AppIcon. This has been moved to `components`.
Touching the LibraryApplications page triggered a cyclomatic
complexity check. Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.
* web: Replace ad-hoc toggle control with ak-toggle-group
This commit replaces various ad-hoc implementations of the Patternfly Toggle Group HTML with a web
component that encapsulates all of the needed behavior and exposes a single API with a single event
handler, return the value of the option clicked.
The results are: Lots of visual clutter is eliminated. A single link of:
```
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.mode = ProxyMode.Proxy;
}}>
<span class="pf-c-toggle-group__text">${msg("Proxy")}</span>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
```
Now looks like:
```
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
```
This also means that the three pages that used the Patternfly Toggle Group could eliminate all of
their Patternfly PFToggleGroup needs, as well as the `justify-content: center` extension, which also
eliminated the `css` import.
The savings aren't as spectacular as I'd hoped: removed 178 lines, but added 123; total savings 55
lines of code. I still count this a win: we need never write another toggle component again, and
any bugs, extensions or features we may want to add can be centralized or forked without risking the
whole edifice.
* web: minor code formatting issue.
* web: adding a storybook for the ak-toggle-group component
* Bugs found by CI/CD.
* web: Replace ad-hoc search for CryptoCertificateKeyPairs with crypto-certificate-search (#6475)
* web: Replace ad-hoc search for CryptoCertificateKeyPairs with ak-crypto-certeficate-search
This commit replaces various ad-hoc implementations of `search-select` for CryptoCertificateKeyPairs
with a web component that encapsulates all of the needed behavior and exposes a single API.
The results are: Lots of visual clutter is eliminated. A single search of:
```HTML
<ak-search-select
.fetchObjects=${async (query?: string): Promise<CertificateKeyPair[]> => {
const args: CryptoCertificatekeypairsListRequest = {
ordering: "name",
hasKey: true,
includeDetails: false,
};
if (query !== undefined) {
args.search = query;
}
const certificates = await new CryptoApi(
DEFAULT_CONFIG,
).cryptoCertificatekeypairsList(args);
return certificates.results;
}}
.renderElement=${(item: CertificateKeyPair): string => {
return item.name;
}}
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
return item?.pk;
}}
.selected=${(item: CertificateKeyPair): boolean => {
return this.instance?.tlsVerification === item.pk;
}}
?blankable=${true}
>
</ak-search-select>
```
Now looks like:
```HTML
<ak-crypto-certificate-search certificate=${this.instance?.tlsVerification}>
</ak-crypto-certificate-search>
```
There are three searches that do not require there to be a valid key with the certificate; these are
supported with the boolean property `nokey`; likewise, there is one search (in SAMLProviderForm)
that states that if there is no current certificate in the SAMLProvider and only one certificate can
be found in the Authentik database, use that one; this is supported with the boolean property
`singleton`.
These changes replace 382 lines of object-oriented invocations with 36 lines of declarative
configuration, and 98 lines for the class. Overall, the code for "find a crypto certificate" has
been reduced by 46%.
Suggestions for a better word than `singleton` are welcome!
* web: display tests for CryptoCertificateKeypair search
This adds a Storybook for the CryptoCertificateKeypair search, including
a mock fetch of the data. In the course of running the tests, we discovered
that including the SearchSelect _class_ won't include the customElement declaration
unless you include the whole file! Other bugs found: including the CSS from
Storybook is different from that of LitElement native, so much so that the
adapter needed to be included. FlowSearch had a similar bug. The problem
only manifests when building via Webpack (which Storybook uses) and not
Rollup, but we should support both in distribution.
remove default example stories that were broken
currently only the dark theme works due to the way storybook includes CSS files in the iframe
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* web: weightloss program, part 1: FlowSearch
This commit extracts the multiple uses of SearchSelect for Flow lookups in the `providers`
collection and replaces them with a slightly more legible format, from:
```HTML
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
const args: FlowsInstancesListRequest = {
ordering: "slug",
designation: FlowsInstancesListDesignationEnum.Authentication,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
return flows.results;
}}
.renderElement=${(flow: Flow): string => {
return RenderFlowOption(flow);
}}
.renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`;
}}
.value=${(flow: Flow | undefined): string | undefined => {
return flow?.pk;
}}
.selected=${(flow: Flow): boolean => {
return flow.pk === this.instance?.authenticationFlow;
}}
>
</ak-search-select>
```
... to:
```HTML
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
required
></ak-flow-search>
```
All of those middle methods, like `renderElement`, `renderDescription`, etc, are *completely the
same* for *all* of the searches, and there are something like 25 of them; this commit only covers
the 8 in `providers`, but the next commit should carry all of them.
The topmost example has been extracted into its own Web Component, `ak-flow-search`, that takes only
two arguments: the type of `FlowInstanceListDesignation` and the current instance of the flow.
The static methods for `renderElement`, `renderDescription` and `value` (which are all the same in
all 25 instances of `FlowInstancesListRequest`) have been made into standalone functions.
`fetchObjects` has been made into a method that takes the parameter from the `designation` property,
and `selected` has been turned into a method that takes the comparator instance from the
`currentFlow` property. That's it. That's the whole of it.
`SearchSelect` now emits an event whenever the user changes the field, and `ak-flow-search`
intercepts that event to mirror the value locally.
`Form` has been adapted to recognize the `ak-flow-search` element and extract the current value.
There are a number of legibility issues remaining, even with this fix. The Authentik Form manager
is dependent upon a component named `ak-form-element-horizontal`, which is a container for a single
displayed element in a form:
```HTML
<ak-form-element-horizontal
label=${msg("Authorization flow")}
?required=${true}
name="authorizationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
```
Imagine, instead, if we could write:
```HTML
<ak-form-element-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
required
name="authorizationFlow">
<label slot="label">${msg("Authorization flow")}</label>
<span slot="help">${msg("Flow used when authorizing this provider.")}</span>
<ak-form-element-flow-search>
```
Starting with a superclass that understands the need for `label` and `help` slots, it would
automatically configure the input object that would be used. We've already specified multiple
identical copies of this thing in multiple different places; centralizing their definition and then
re-using them would be classic code re-use.
Even better, since the Authorization flow is used 10 times in the whole of our code base, and the
Authentication flow 8 times, and they are *all identical*, it would be fitting if we just created
wrappers:
```HTML
<ak-form-element-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}>
<ak-form-element-flow-search>
```
That's really all that's needed. There are *hundreds* (about 470 total) cases where nine or more
lines of repetitious HTML could be replaced with a one-liner like the above.
A "narrow waist" design is one that allows for a system to communicate between two different
components through a small but consistent collection of calls. The Form manager needs to be narrowed
hard. The `ak-form-element-horizontal` is a wrapper around an input object, and it has this at its
core for extracting that information. This forwards the name component to the containing input
object so that when the input object generates an event, we can identify the field it's associated
with.
```Javascript
this.querySelectorAll("*").forEach((input) => {
switch (input.tagName.toLowerCase()) {
case "input":
case "textarea":
case "select":
case "ak-codemirror":
case "ak-chip-group":
case "ak-search-select":
case "ak-radio":
input.setAttribute("name", this.name);
break;
default:
return;
}
```
A *temporary* variant of this is in the `ak-flow-search` component, to support this API without
having to modify `ak-form-element-horizontal`.
And then `ak-form` itself has this:
```Javascript
if (
inputElement.tagName.toLowerCase() === "select" &&
"multiple" in inputElement.attributes
) {
const selectElement = inputElement as unknown as HTMLSelectElement;
json[element.name] = Array.from(selectElement.selectedOptions).map((v) => v.value);
} else if (
inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "date"
) {
json[element.name] = inputElement.valueAsDate;
} else if (
inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "datetime-local"
) {
json[element.name] = new Date(inputElement.valueAsNumber);
}
// ... another 20 lines removed
```
This ought to read:
```Javascript
const json = elements.filter((element => element instanceof AkFormComponent)
.reduce((acc, element) => ({ ...acc, [element.name]: element.value] });
```
Where, instead of hand-writing all the different input objects for date and datetime and checkbox
into our forms, and then having to craft custom value extractors for each and every one of them,
just write *one* version of each with all the wrappers and bells and whistles already attached, and
have each one of them have a `value` getter descriptor that returns the value expected by our form
handler.
A back-of-the-envelope estimation is that there's about four *thousand* lines that could disappear
if we did this right.
More importantly, it would be possible to create new `AkFormComponent`s without having to register
them or define them for `ak-form`; as long as they conformed to the AkFormComponent's expectations
for "what is a source of values for a Form", `ak-form` would understand how to handle it.
Ultimately, what I want is to be able to do this:
``` HTML
<ak-input-form
itemtype="ak-search"
itemid="ak-authentication"
itemprop=${this.instance}></ak-inputform>
```
And it will (1) go out and find the right kind of search to put there, (2) conduct the right kind of
fetch to fill that search, (3) pre-configure it with the user's current choice in that locale.
I don't think this is possible-- for one thing, it would be very expensive in terms of development,
and it may break the "narrow waist" ideal by require that the `ak-input-form` object know all the
different kinds of searches that are available. The old Midgardian dream was that the object would
have *just* the identity triple (A table, a row of that table, a field of that row), and the
Javascript would go out and, using the identity, *find* the right object for CRUD (Creating,
Retrieving, Updating, and Deleting) it.
But that inspiration, as unreachable as it is, is where I'm headed. Where our objects are both
*smart* and *standalone*. Where they're polite citizens in an ordered universe, capable of
independence sufficient to be tested and validated and trusted, but working in concert to achieve
our aims.
* web: unravel the search-select for flows completely.
This commit removes *all* instances of the search-select
for flows, classifying them into four different categories:
- a search with no default
- a search with a default
- a search with a default and a fallback to a static default if non specified
- a search with a default and a fallback to the tenant's preferred default if this is a new instance
and no flow specified.
It's not humanly possible to test all the instances where this has been committed, but the linters
are very happy with the results, and I'm going to eyeball every one of them in the github
presentation before I move this out of draft.
* web: several were declared 'required' that were not.
* web: I can't believe this was rejected because of a misspelling in a code comment. Well done\!
* web: another codespell fix for a comment.
* web: adding 'codespell' to the pre-commit command. Fixed spelling error in eventEmitter.
* web: basic cleanup of buttons
This commit adds Storybook features to the Authentik four-stage button.
The four-stage button is used to:
- trigger an action
- show that the action is running
- show when the action has succeeded, then reset
- show when the action has failed, then reset
It is used mostly for fetching data from the server. The variants are:
- ak-spinner-button: The basic form takes a single property argument, `callAction` a function that
returns a Promise (an asynchronous function).
- ak-action-button: Takes an API request function (which are all asynchronous) and adapts it to the
`callAction`. The only difference in behavior with the Spinner button is that on failure the error
message will be displayed by a notification.
- ak-token-copy-button: A specialized button that, on success, pushes the content of the retrieved
object into the clipboard.
Cleanup consisted of:
- removing a lot of the in-line code from the HTML, decluttering it and making more explicit what
the behaviors of each button type are on success and on failure.
- Replacing the ad-hoc Promise management with Lit's own `Task` handler. The `Task` handler knows
how to notify a Lit-Element of its own internal state change, making it ideal for objects like
this button that need to change their appearance as a Promise'd task progresses from idle →
running → (success or failure).
- Providing JSDoc strings for all of the properties, slots, attributes, elements, and events.
- Adding 'pointer-events: none' during the running phases of the action, to prevent the user from
clicking the button multiple times and launching multiple queries.
- Emitting an event for every stage of the operation:
- `ak-button-click` when the button is clicked.
- `ak-button-success` when the action completes. The payload is included in `Event.detail.result`
- `ak-button-failure` when the action fails. The error message is included in `Event.detail.error`
- `ak-button-reset` when the button completes a notification and goes back to idle
**Storybook**
Since the API requests for both `ak-spinner-button` and `ak-action-button` require only that a
promise be returned, Storybooking them was straightforward. `ak-token-copy-button` is a
special-purpose derivative with an internal functionality that can't be easily mocked (yet), so
there's no Storybook for it.
All of the stories provide the required asynchronous function, in this cose one that waits three
seconds before emitting either a `response` or `reject` Promise.
`ak-action-button`'s Story has event handler code so that pressing on the button will result in a
message being written to a display block under the button.
I've added a new pair of class mixins, `CustomEmitterElement` and `CustomListenerElement`. These
each add an additional method to the classes they're mixed into; one provides a very easy way to
emit a custom event and one provides a way to receive the custom event while sweeping all of the
custom event type handling under the rug.
`emitCustomEvent` replaces this:
``` JavaScript
this.dispatchEvent(
new CustomEvent('ak-button-click', {
composed: true,
bubbles: true,
detail: {
target: this,
result: "Some result, huh?"
},
})
);
```
... with this:
``` JavaScript
this.dispatchCustomEvent('ak-button-click', { result: "Some result, huh?" });
```
The `CustomListenerElement` handler just ensures that the handler being passed to it takes a
CustomEvent, and then makes sure that any actual event passed to the handler has been type-guarded
to ensure it is a custom event.
**Observations**
*Composition vs Inheritance, Part 1*
The four-state button has three implementations. All three inherit from `BaseTaskButton`:
- `spinner`
- provides a default `callAction()`
- `action`
- provides a different name for `callAction`
- overrides `onError` to display a Notification.
- `token-copy`
- provides a custom `callAction`
- overrides `onSuccess` to copy the results to the keyboard
- overrides `onError` to display a Notification, with special handling for asynchronous
processing.
The *results* of all of these could be handled higher up as event handlers, and the button could be
just a thing that displays the states. As it is, the BaseStateToken has only one reason to change
(the Promise changes its state), so I'm satisfied that this is a suitable evolution of the product,
and that it does what it says it does.
*Developer Ergonomics*
The one thing that stands out to me time and again is just how *confusing* all of the Patternfly
stuff tends to be; not because it's not logical, but because it overwhelms the human 7±2 ability to
remember details like this without any imperative to memorize all of them. I would like to get them
under control by marshalling them under a semantic CSS regime, but I'm blocked by some basic
disconnects in the current development environment. We can't shake out the CSS as much as we'd like
because there's no ESPrima equivalent for Typescript, and the smallest bundle purgeCSS is capable of
making for just *one* button is about 55KB. That's a bit too much. It's a great system for getting
off the ground, but long-term it needs more love than we (can) give it.
* Prettier has opinions.
* Removed extraneous debugging code.
* Added comments to the BaseTaskButton parent class.
* web: fixed two build errors (typing) in the stories.
* web: prettier's got opinions
* web: refactor the buttons
This commit adds URL mocking to Storybook, which in turn allows us to
commit a Story for ak-token-copy-button.
I have confirmed that the button's algorithm for writing to the
clipboard works on Safari, Chrome, and Firefox. I don't know
what's up with IE.
* ONE BYTE in .storybook/main blocked integration.
With the repair of lit-analyze, it's time to fix the rule set
to at least let us pass for the moment.
* Still looking for the list of exceptions in lit-analyze that will let us pass once more.
* web: repair error in EnterpriseLicenseForm
This commit continues to find the right configuration for
lit-analyze. During the course of this repair, I discovered
a bug in the EnterpriseLicenseForm; the original usage could
result in the _string_ `undefined` being passed back as a
value. To handle the case where the value truly is undefined,
the `ifDefined()` directive must be used in the HTML template.
I have also instituted a case-by-case stylistic decision to allow
the HTML, and only the HTML, to be longer that 100 characters
when doing so reduces the visual "noise" of a function.
* web: begin refactoring the application for future development
This commit:
- Deletes a bit of code.
- Extracts *all* of the Locale logic into a single folder, turns management of the Locale files over
to Lit itself, and restricts our responsibility to setting the locale on startup and when the user
changes the locale. We do this by converting a lot of internal calls into events; a request to
change a locale isn't a function call, it's an event emitted asking `REQUEST_LOCALE_CHANGE`. We've
even eliminated the `DETECT_LOCALE_CHANGE` event, which redrew elements with text in them, since
Lit's own `@localized()` decorator does that for us automagically.
- We wrap our interfaces in an `ak-locale-context` that handles the startup and listens for the
`REQUEST_LOCALE_CHANGE` event.
- ... and that's pretty much it. Adding `@localized()` as a default behavior to `AKElement` means
no more custom localization is needed *anywhere*.
* web: improve the localization experience
This commit fixes the Storybook story for the localization context component,
and fixes the localization initialization pass so that it is only called once
per interface environment initialization. Since all our interfaces share the
same environment (the Django server), this preserves functionality across
all interfaces.
---------
Co-authored-by: Jens Langhammer <jens@goauthentik.io>