* web: fix dual-select with dynamic selection
For dynamic selection, the property name is `.selector` to message that it's a function the
API layer uses to select the elements.
A few bits of lint picked.
* web: added comment to clarify what the fallback selector does
* web: fix e2e tests to work with latest WebdriverIO and authentik 2024.8
- Adjust the ApplicationWizard "select provider type" typeslist to have the right OUIA tags when
running
- Add OUIA tags to TypeCreateWizard
- Provide default values for `.jwksSources` when needed.
- Upgrade E2E WebUI tests to use WebdriverIO 9.
- Upgrade the linters to include `package.json` and `package-lock.json`.
- Adjust a *lot* of the WebdriverIO selectors!
- Provide a driver for the TypeCreate card-based radio interface.
- Split `Bad Logins` into two separate files.
Aside from the obvious, "because testing needs this" or "because there were warnings on the console
when this was running," the real issue is that WebdriverIO 9 has changed the syntax and semantics of
its ShadowDOM-piercing `$` mechanism.
For Oauth2 and Proxy, the field `.jwksSources` may be undefined, but `undefined` is not a legal
value for ak-dual-select's `selected` field. Provide a default or use `ifDefined()`. I chose to
provide a default of `[]`.
In the previous iteration, `$(">>>ak-search-select input")` would be sufficient for WebdriverIO to
find an input inside a component. Now, it needs to be written as: `$("ak-search-select").$("input")`.
And in rare cases, when you have a floating component that is separated from its invocation (such as
Notification or SearchSelect), even that doesn't work well and you have to fall back to some
old-school hacking (see `./tests/wdio/test/pageobjects/page.ts` for an example) to find some child
elements.
Also, the monadic nature of `$` seems to have faded a bit. `$` used to wrap all child invocations in
promises, making the entire expression a single valid promise; it seems that it is now necessary to
unwrap the promises yourself under some circumstances, resulting in a lot of `await (await (await
... )))` blocks in the tests.
We've slightly changed the semantics of our login mechanism, and now the default behavior is to not
reveal when a username is invalid, but to treat the entire login as a single failure mechanism, so
as not to expose any details about the username database.
The problem arises that now, we (or Chrome) cache the username between roundtrips, and WebdriverIO's
second pass was becoming confused by its presence. By putting the Bad Logins into two separate
files, I get two separate browser instances with cleared caches, so each test can be run in the
pristine environment it needs to validate the behavior I'm expecting.
* web: added comment to explain the hack
* Add comment to TypeCreateWizardPage to explain the component name hack.
* web: fix some lint found by CI/CD
* web: fix esbuild issue with style sheets
Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).
Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.
In standard mode, the following warning appears on the console when running a Flow:
```
Autofocus processing was blocked because a document already has a focused element.
```
In compatibility mode, the following **error** appears on the console when running a Flow:
```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
at initDomMutationObservers (crawler-inject.js:1106:18)
at crawler-inject.js:1114:24
at Array.forEach (<anonymous>)
at initDomMutationObservers (crawler-inject.js:1114:10)
at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```
Despite this error, nothing seems to be broken and flows work as anticipated.
* web: provide a test framework
As is typical of a system where a new build engine is involved, this thing is sadly fragile. Use the
wrong import style in wdio.conf.js and it breaks; there are several notes in tsconfig.test.conf and
wdio.conf.ts to tell eslint or tsc not to complain, it's just a different build with different
criteria, the native criteria don't apply.
On the other hand, writing tests is easy and predictable. We can test behaviors at the unit and
component scale in a straightforward manner, and validate our expectations that things work the way
we believe they should.
* Rolling back a reversion.
* web: update storybook, storybook a few things, fix a few things
After examining how people like Adobe and Salesforce do things, I have updated the storybook
configuration to provide run-time configuration of light/dark mode (although right now nothing
happens), inject the correct styling into the page, and update the preview handling so that we can
see the components better. We'll see how this pans out.
I have provided stories for the AggregateCard, AggregatePromiseCard, and a new QuickActionsCard. I
also fixed a bug in AggregatePromiseCard where it would fail to report a fetch error. It will only
report that "the operation falied," but it will give the full error into the console.
**As an experiment**, I have changed the interpreter for `lint:precommit` and `build:watch` to use
[Bun](https://bun.sh/) instead of NodeJS. We have observed significant speed-ups and much better
memory management with Bun for these two operations. Those are both developer-facing operations, the
behavior of the system undur current CI/CD should not change.
And finally, I've switched the QuickActionsCard view in Admin-Overview to use the new component.
Looks the same. Reads *way* easier. :-)
* Slight revision in exception logic.
* Added a ton of documentation; made the failure message configurable.
* A few documentation changes.
* Adjusting paths to work with tests.
* web: Provide tests for the aggregate cards, fix a few minor things
This commit provides tests alongside the stories for the aggregate cards. The tests are fairly
basic, but they're good enough for starting *and* they provide a pretty good example of how to test
when a promise with a delay is involved.
Two minor fixes in this code:
- The subtext was given a small amount of whitespace above, to remove the crowding that happened.
It looks much better with a half-rem of space.
- In the rare case that we have a card header with no icon, the ' ' symbol that separates the
icon from the header is now not rendered. In the previous form, it would push the header to the
left, making it "hang in space" one rem to the right of the visual line formed by the rightmost
content border. The padding between the header, body, and footer is odd; body is 1 rem, the
header and footer 2rems. This looks good for the graphs, but for the text, not so much.
* Prettier had opinions.
* Merge and catching up with the evolution of our test framework.
* clamp width to 100% width
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* add case for unlicensed and set to infinity when users of a type exists that dont have licenses
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* rework license status into separate component...
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* enable coverage
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* remove annoying disable-search-engine-choice-screen
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* refactor percentage calculation
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* fix a bug found by tests, yay
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* add tests for enterprise status card
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* upgrade vite-tsconfig-paths
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* ...?
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---------
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* web: fix esbuild issue with style sheets
Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).
Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.
In standard mode, the following warning appears on the console when running a Flow:
```
Autofocus processing was blocked because a document already has a focused element.
```
In compatibility mode, the following **error** appears on the console when running a Flow:
```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
at initDomMutationObservers (crawler-inject.js:1106:18)
at crawler-inject.js:1114:24
at Array.forEach (<anonymous>)
at initDomMutationObservers (crawler-inject.js:1114:10)
at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```
Despite this error, nothing seems to be broken and flows work as anticipated.
* web: replace multi-select with dual-select for all propertyMapping invocations
All of the uses of <select> to show propertyMappings have been replaced with an invocation to a
variant of dual select that allows for dynamic production of the "selected" list. Instead of giving
a "selected" list of elements, a "selector" function is passed that can, given the elements listed
by the provider, generated the "selected" list dynamically.
This feature is required for propertyMappings because many of the propertyMappings have an alternative
"default selected" feature whereby an object with no property mappings is automatically granted some
by the `.managed` field of the property mapping. The `DualSelectPair` type is now tragically
mis-named, as it it's now a 4-tuple, the fourth being whatever object or field is necessary to
figure out what the default value might be. For example, the Oauth2PropertyMappingsSelector looks
like this:
```
export function makeOAuth2PropertyMappingsSelector(instanceMappings: string[] | undefined) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, scope]: DualSelectPair<ScopeMapping>) =>
scope?.managed?.startsWith("goauthentik.io/providers/oauth2/scope-") &&
scope?.managed !== "goauthentik.io/providers/oauth2/scope-offline_access";
}
```
If there are instanceMappings, we create a Set of them and just look up the pk for "is this
selected" as we generate the component.
If there is not, we look at the `scope` object itself (Oauth2PropertyMappings were called "scopes"
in the original source) and perform a token analysis.
It works well, is reasonably fast, and reasonably memory-friendly.
In the case of RAC, OAuth2, and ProxyProviders, I've also provided external definitions of the
MappingProvider and MappingSelector, so that they can be shared between the Provider and the
ApplicationWizard.
The algorithm for finding the "alternative (default) selections" was *different* between the two
instances of both Oauth and Proxy. I'm not marking this as "ready" until Jens (@BeryJu) and I can go
over why that might have been so, and decide if using a common implementation for both is the
correct thing to do.
Also, a lot of this is (still) cut-and-paste; the dual-select invocation, and the definitions of
Providers and Selectors have a bit of boilerplate that it just didn't make sense to try and abstract
away; the code is DAMP (Descriptive and Meaningful Phrases), and I can live with it. Unfortunately,
that also points to the possibility of something being off; the wrong default token, or the wrong
phrase to describe the "Available" and "Selected" columns. So this is not (yet) ready for a full
pull review.
On the other hand, if this passes muster and we're happy with it, there are 11 more places to put
DualSelect, four of which are pure cut-and-paste lookups of the PaginatedOauthSourceList, plus a
miscellany of Prompts, Sources, Stages, Roles, EventTransports and Policies.
Despite the churn, the difference between the two implementations is 438 lines removed, 231 lines
added, 121 lines new. 86 LOC deleted. Could be better. :-)
* web: make the ...Selector semantics uniform across the definition set.
* web: fix proxy property mapping default criteria
* web: restoring dropped message to user.
* Completed one. Stashing momentarily.
* Ensuring the neccessary components are imported.
* I hate trying to coax MacOS into accepting case changes.
* Still trying to rename that thing.
* OAuth2 Sources multiple implementation completed.
* web: replace remaining multi-selects with dual-selects
This commit replaces the remaining multi-selects with their dual-select equivalents.
* web: fix problem with 'selector' overselecting
The 'selector' feature was overselecting, preventing items from
being removed from the "selected" list if they were part of the
host object. This has the shortcoming that `default` items *must*
be in the first page of options from the server, or they probably
won't be registered. Fortunately, that's currently the case.
* fix a
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* fix b
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* migrate new providers
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* remove old incorrect help message
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* fix incorrect copy paste
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* fix status label for gorups
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: fix Flash of Unstructured Content while SearchSelect is loading from the backend
Provide an alternative, readonly, disabled, unindexed input object with the text "Loading...", to be
replaced with the _real_ input element after the content is loaded.
This provides the correct appearance and spacing so the content doesn't jiggle about between the
start of loading and the SearchSelect element being finalized. It was visually distracting and
unappealing.
* web: comment on state management in API layer, move file to point to correct component under test.
* web: test for flash of unstructured content
- Add a unit test to ensure the "Loading..." element is displayed correctly before data arrives
- Demo how to mock a `fetchObjects()` call in testing. Very cool.
- Make distinguishing rule sets for code, tests, and scripts in nightmare mode
- In SearchSelect, Move the `styles()` declaration to the top of the class for consistency.
- To test for the FLOUC issue in SearchSelect.
This is both an exercise in mocking @beryju's `fetchObjects()` protocol, and shows how we can unit
test generic components that render API objects.
* web: much better focus discipline
Fix the way focus is handled in SearchSelect so that the drop-down isn't grabbing the focus away
from the Input when the user wants to type in their selection.
Because it was broken otherwise!
There's still a bug where it's possible to type in a complete value
*Label*, then leave the component's focus (input and menu) completely,
in which case the Label remains, looking innocent and correct, but
it is *not* reflective of the value as understood by the SearchSelect
API controller.
Gonna try to fix that next. But I'm saving this as a useful checkpoint.
* .
* root: insert daphne app in correct order
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* web: implement ak-list-select
Creates a new element, ak-list-select, which is a scrollable list that reports when an element is clicked or
selected by the keyboard.
I was hideously over-engineering ak-search-select-menu, and I decided to try something simpler. This is
that something. The events we care about are just "change" and "lost focus", and both of those can be
attached by the parent regardless of portaling.
* web: ak-list-select is complete
An extraction of the "menu" and "list" features from SearchSelect
and DualSelect, this is a very simplified version of a visible list
that emulates the Radio/Select behavior (i.e only one from the
collection may be "valued" at the time). It has no visible indicators
of selection (aside from some highlighting), as it's meant to be
used to present the list rather than be indicative of any state of
the list.
I was seriously over-engineering the menu. It turns out, it's just
not that difficult after all. The only things we care about, really,
are "did the user change the selection," "did the user click out
of the list," and "did the user press the escape key." Those are
pre-existing events (click w/value, blur, and keydown w/keycode,
respectively), so there was no need for me to introduce new custom
events to handler them.
* web: downgrade sonarjs again, because dependabot
Dammit, really need to tell that machine to leave our versions alone.
* web: search select
After a lot of testing and experimenting, it's finally starting to look stable.
What a pain in the neck this has all been.
* web: hold
* web: search select with focus and progressive search
- New component: ak-list-select, which allows you to select from a list of elements, with keyboard
control.
- New component: ak-portal, which manages elements by moving "slotted" content into a distant
component, usually one attached to the body, and positions it relative to an existing element.
- ak-search-select-view has been revamped to handle focus, change, input, and blur using
the browser native event handlers, rather than inventing my own.
- ak-search-select has been turned into a simple driver that manages the view.
- ak-search-select has a new declarative syntax for the most common use case.
I seriously over-engineered this thing, leaning too heavily on outdated knowledge or assumptions
about how the browser works. The native event handlers attached at the component's borders works
more than fine, and by attaching the event handlers to the portaled component before sending it
off to the slots, the correct handlers get the message. This revision leverages the browser
a *lot* more, and gets much more effective interaction with much less code.
`<ak-list-select>` is a new component that replaces the ad-hoc menu object of the old SearchSelect.
It is a standalone component that just shows a list, allows someone to navigate that list with the
keyboard or the mouse. By default, it is limited to half the height of the viewport.
The list does not have an indicator of "selected" at this time. That's just a side effect of it
being developed as an adjunct to search-select. Its design does not preclude extension.
It has a *lot* of CSS components that can be customized. The properties and events are documented,
but there is only one event: `change`. Consistent with HTML, the value is not sent with the `change`
event; clients are expected to extract it with `change:event.target.value`.
Like all HTML components, it is completely stringly defined; the value is either a string or
undefined.
`<ak-portal>` is a somewhat specialized "portal" component that places an `ak-list-select` in an
object on top of the existing DOM content. It can generalized to do this with any component, though,
and can be extended. It has no events or CSS, since it's "just" managing the portaling relationship.
`<ak-search-select-view>` is the heart of the system. It takes a collection options and behaves
like an autocomplete component for them. The only unique event it sends out is `change`, and like
`ak-list-select`, it expects the client to retrieve the value.
Like all HTML components, it is completely stringly defined; the value is either a string or
undefined.
This is the SearchSelect component we've all known to come and love, but with a better pop-up and
cleaner keyboard interaction. It emits only one event, `ak-change`, which *does* carry the value
with it.
The Storybooks have been updated to show the current version of Search Select, with a (simulated)
API layer as well as more blunt stringly-typed tests for the View layer. A handful of tests have
been provided to cover a number of edge cases that I discovered during testing. These run fine
with the `npx` command, and I would love to see them integrated into CI/CD.
The search select fields `renderElement`, `renderDescription`, and `value` properties of
`ak-search-select` have been modified to take a string. For example, the search for the
list of user looks like this:
```
<ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = { ordering: "username" };
if (query !== undefined) {
args.search = query;
}
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args);
return users.results;
}}
.renderElement=${(user: User): string => {
return user.username;
}}
.renderDescription=${(user: User): TemplateResult => {
return html`${user.name}`;
}}
.value=${(user: User | undefined): string | undefined => {
return user?.username;
}}
></ak-search-select>
```
The most common syntax for the these three fields is "just return the string contents of a field by
name," in the case of the description wrapped in a TemplateResult with no DOM components. By
automating that initialization in the `connectedCallback` of the `ak-search-select` component,
this object would look like:
<ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = { ordering: "username" };
if (query !== undefined) {
args.search = query;
}
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args);
return users.results;
}}
.renderElement=${"username"}
.renderDescription=${"name"}
.value=${"username"}
></ak-search-select>
```
Due to a limitation in the way properties (such as functions) are interpreted, the syntax
`renderElement="username"` is invalid; it has to be a property expression. Sorry; best I could do.
The old syntax works just fine. This is a "detect and extend at runtime" enhancement.
* Added comments to the Component Driver Harness.
* Added more safety and comments.
* web: remove string-based access to API; replace with a consolidated "adapter" layer.
Clean out the string-based API layer in SearchSelect. Break SearchSelect into a
"Base" that does all the work, and then wrap it in two different front-ends:
one that conforms to the old WCAPI, and one with a slightly new WCAPI:
```
<ak-search-select-ez
.config=${{
fetchObjects: async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
includeUsers: false,
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
args,
);
return groups.results;
},
renderElement: (group: Group): string => group.name,
value: (group: Group | undefined): string | undefined => group?.pk,
selected: (group: Group): boolean => group.pk === this.instance?.group
}}
blankable
>
</ak-search-select-ez>
```
* Prettier had opinions. In one case, an important opinion.
* Rename test and fix lint error.
* 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: bug / licenseStatus is not defined on initial render
- Test if the licenseStatus is available before rendering the banner
- The banner is rendered correctly when the status becomes available.
The loading sequence is such that if the user reloads the page, the
first attempt to render the license banner fails because the
licenseStatus field is not yet populated; the result is an ugly
`licenseStatus is undefined` on the console.
Because the licenseStatus is a live context, when it is updated
any objects that subscribe to it are scheduled for a re-render.
This is why the system appears to behave correctly now.
While this is invisible to the user, it's still undesirable behavior.
Returning `nothing` requires that we remove the type declarations
as return values from the renderers. Typescript's inferers do
just fine.
* fix some other small things
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: update to ESLint 9
ESLint 9 has been out for awhile now, and all of the plug-ins that we use have caught up, so it is
time to bite the bullet and upgrade. This commit:
- upgrades to ESLint 9, and upgrades all associated plugins
- Replaces the `.eslintrc` and `.eslintignore` files with the new, "flat" configuration file,
"eslint.config.mjs".
- Places the previous "precommit" and "nightmare" rules in `./scripts/eslint.precommit.mjs` and
`./scripts/eslint.nightmare.mjs`, respectively
- Replaces the scripted wrappers for eslint (`eslint`, `eslint-precommit`) with a single executable
that takes the arguments `--precommit`, which applies a stricter set of rules, and `--nightmare`,
which applies an even more terrifyingly strict set of rules.
- Provides the scripted wrapper `./scripts/eslint.mjs` so that eslint can be run from `bun`, if one
so chooses.
- Fixes *all* of the lint `eslint.config.mjs` now finds, including removing all of the `eslint`
styling rules and overrides because Eslint now proudly leaves that entirely up to Prettier.
To shut Dependabot up about ESLint.
* Added explanation for no-console removal.
* web: did not need the old and unmaintained nightmare mode; it can be configured directly.
* add GeoIP policy
* handle empty lists of ASNs and countries
* handle missing GeoIP database or missing IP from the database
The exceptions raised here are `PolicyException`s to let admins bypass
an execution failure.
* fix translations
whoops
* remove `GeoIPPolicyMode`
Use the policy binding's `negate` option instead
* fix `DataProvision` typing
`ak-dual-select-provider` can handle unpaginated data
* use `django-countries` instead of a static list of countries for ISO-3166
* simplify `GeoIPPolicyForm`
* pass `GeoIPPolicy` on empty policy
* add backend tests to `GeoIPPolicy`
* revise translations
* move `iso-3166/` to `policies/geoip_iso3166/`
* add client-side caching to ISO3166 API call
* fix `GeoIPPolicy` creation
The automatically generated APIs can't seem to handle `CountryField`,
so I'll have to do this by hand too.
* add docs for GeoIP Policy
* docs: stylize
add review suggestions from @tanberry
* refactor `GeoIPPolicy` API
It is now as declarative as I could make it.
* clean up `api.py` and `views.py`
Replace all occurences of the theme placeholder
This allows the placeholder to occur multiple times in the theme url.
Signed-off-by: Chasethechicken <neuringe1234@gmail.com>