Compare commits

...

21 Commits

Author SHA1 Message Date
Ken Sternberg
f0256a0535 As alway, prettier has opinions 2024-07-16 14:04:23 -07:00
Ken Sternberg
142a985914 Tightened the language. 2024-07-16 13:56:53 -07:00
Ken Sternberg
a8531d498a Tests are updated and working. Had to revise the 'search-select' binding to work with the new search-select. 2024-07-16 13:49:17 -07:00
Ken Sternberg
f8cb4e880b web: roll back update to sonar
Bloody dependabot.  We're not compatible with ESLint 9 yet, darnit, and yet
dependabot keeps pushing upgrades on us.
2024-07-16 09:23:28 -07:00
Ken Sternberg
3ced637db3 web: grammar fix and lint update
1. Merged the two SonarJS lints into one
2. Fixed a grammatical error in RedirectStage
2024-07-16 09:19:57 -07:00
Marc 'risson' Schmitt
409934196c web: fix lint (#10524) 2024-07-16 15:42:07 +00:00
dependabot[bot]
62c882cb0e web: bump @typescript-eslint/parser from 7.16.0 to 7.16.1 in /tests/wdio (#10515)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 7.16.0 to 7.16.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.16.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-16 12:29:09 +02:00
dependabot[bot]
033b55ba51 web: bump @babel/core from 7.24.8 to 7.24.9 in /web in the babel group across 1 directory (#10513)
web: bump @babel/core in /web in the babel group across 1 directory

Bumps the babel group with 1 update in the /web directory: [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core).


Updates `@babel/core` from 7.24.8 to 7.24.9
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.9/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: babel
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-16 12:12:13 +02:00
dependabot[bot]
e5e14d3b5a web: bump eslint-plugin-sonarjs from 0.25.1 to 1.0.3 in /web (#10518)
Bumps [eslint-plugin-sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) from 0.25.1 to 1.0.3.
- [Release notes](https://github.com/SonarSource/eslint-plugin-sonarjs/releases)
- [Commits](https://github.com/SonarSource/eslint-plugin-sonarjs/compare/0.25.1...1.0.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-sonarjs
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-16 12:11:50 +02:00
dependabot[bot]
9475c1b0cf web: bump prettier from 3.3.2 to 3.3.3 in /web (#10517)
Bumps [prettier](https://github.com/prettier/prettier) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.3.2...3.3.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-16 12:11:37 +02:00
dependabot[bot]
6875efcfdd web: bump @typescript-eslint/eslint-plugin from 7.16.0 to 7.16.1 in /tests/wdio (#10516)
web: bump @typescript-eslint/eslint-plugin in /tests/wdio

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 7.16.0 to 7.16.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.16.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-16 12:11:02 +02:00
dependabot[bot]
5cf4172a6f web: bump the storybook group across 1 directory with 7 updates (#10514)
Bumps the storybook group with 5 updates in the /web directory:

| Package | From | To |
| --- | --- | --- |
| [@storybook/addon-essentials](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/essentials) | `8.2.2` | `8.2.4` |
| [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links) | `8.2.2` | `8.2.4` |
| [@storybook/manager-api](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/manager-api) | `8.2.2` | `8.2.4` |
| [@storybook/web-components](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/web-components) | `8.2.2` | `8.2.4` |
| [@storybook/web-components-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/web-components-vite) | `8.2.2` | `8.2.4` |



Updates `@storybook/addon-essentials` from 8.2.2 to 8.2.4
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.2.4/code/addons/essentials)

Updates `@storybook/addon-links` from 8.2.2 to 8.2.4
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.2.4/code/addons/links)

Updates `@storybook/blocks` from 8.2.2 to 8.2.4
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.2.4/code/lib/blocks)

Updates `@storybook/manager-api` from 8.2.2 to 8.2.4
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.2.4/code/lib/manager-api)

Updates `@storybook/web-components` from 8.2.2 to 8.2.4
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.2.4/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 8.2.2 to 8.2.4
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.2.4/code/frameworks/web-components-vite)

Updates `storybook` from 8.2.2 to 8.2.4
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.2.4/code/lib/cli)

---
updated-dependencies:
- dependency-name: "@storybook/addon-essentials"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/blocks"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/manager-api"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-16 12:10:53 +02:00
dependabot[bot]
dbfa5f2fd1 core: bump goauthentik.io/api/v3 from 3.2024061.2 to 3.2024061.3 (#10512)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024061.2 to 3.2024061.3.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024061.2...v3.2024061.3)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-16 11:51:55 +02:00
authentik-automation[bot]
e44341d5e0 web: bump API Client version (#10511)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-07-16 11:51:48 +02:00
dependabot[bot]
f77ee77f7e core: bump sentry-sdk from 2.9.0 to 2.10.0 (#10519)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.9.0 to 2.10.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.9.0...2.10.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-16 11:51:21 +02:00
Marc 'risson' Schmitt
d4b39b30cb website: fix lint (#10520) 2024-07-16 11:49:07 +02:00
Ken Sternberg
b0507d2063 web: provide 'show password' button (#10337)
* 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 `show password` on login page

Provide a `show password` icon, text, and button for the password field both in the
IdentificationStage and the PasswordStage. Essentially the same code for both, although the id of
the password field is unique to each.

Requested by Cloudflare.  Seems to be a common thing anyway.

Should it be an administrative option that this facility is available?  From where should I derive
that information?  I suspect the answer is "a site attribute," but I'd like to get confirmation.

* web: comment doesn't need to be exposed. It's sufficient where it is .

* web: fix button rendering issues

During testing, the buttons did not change as expected.  We are using pure DOM
state to control the look of the button, and avoiding using `.requestUpdate()`
to avoid losing customer input, so depending upon Lit to re-render just the
button was an error.

This commit goes old-school and updates the button's label and icon using
standard DOM features, although we do lean into Lit-html`s `render()`
function to create the DOM component for the icon.

* web: provide `show password` on login page

Provide a `show password` icon, text, and button for the password field both in the
IdentificationStage and the PasswordStage. Essentially the same code for both, although the id of
the password field is unique to each.

Provide a configuration detail server-side to allow administrator to enable or disable the 'show
password' feature.  Off by default.

Requested by Cloudflare.  Seems to be a common thing anyway.  Making it configurable wasn't in
Cloudfare's request, but it seemed logical to add.

* ensure the tests pass; quibbling over the wording of the admin field continues.

* Removed some manually identified fluff.

* web: break out `show password`-enabled input field into its own component

Provides a `show password` field, but as a LightDOM-oriented web component. This form of
input[type="password"] is for flows only, as it has a number of specializations for understanding a
flow's validating round-trip, possible error messages within the challenge, and is left within the
LightDOM both to support compatibility issues and to avoid using `elementInterals`, which is a DOM
feature not supported by some older browsers.

Avoids having to maintain two different instances of the same logic, both for permitting 'show
password', and for handling it.

* web: update PasswordStageForm according to lit-analyzer

With lit-analyzer in the mix and functional, we're seeing new complaints about
inconsistent typing in lit objects, and this was one of them.

* Another lit-analyze error found.
2024-07-15 18:14:46 -07:00
Ken Sternberg
d0a459076b web: enhance search select with portal, overflow, and keyboard controls (#9517)
* 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: enhance search select

Patternfly doesn't even *have* a setting for "selected but not hovered," so I had to invent one. I
borrowed a trick from MUI and used the light blue "info" color, making it darker on
"selected+hovered."

This commit starts the revision process for search select. The goal is to have it broken down into
four major components: The inline-DOM component, which just shows the current value (or placeholder,
if none) and creates the portal for the floating component, then have a higher-level component for
the SearchSelect behavior, and a sidecar to manage the keyboard interactivity.

This is the portaled component: the actual list.

* web: enhance search select. Break menu and Input items into separate handlers.

* web: search select: added keyboard controller.

* web: search select - the isolation continues

This commit brings us to the position of having an independently rendered menu that listens for
click events on its contents, records the value internally *and* sends it upstream as an event.

This commit also includes a KeyboardController reactor that listens for keyboard events on a list of
objects, moving the focus up and down and sending a both a "selected" event when the user presses
Enter or Space, and a "close" event when the user presses Escape.

A lot of this is just infrastructure.  None of these *do* very much; they're just tools for making
SearchSelect better.

AkSearchSelectView is next: it's responsible for rendering the input and menu items, and then for
forwarding the `value` component up to whoever cares.

`ak-search-select` will ultimately be responsible for fetching the data and mapping the string
tokens from AkSearchSelectView back into the objects that Authentik cares about.

* web: search select - a functioning search select

So search select is now separated into the following components:

- SearchSelectView: Takes the renderables and the selected() Value and draws the Value in a
  box, then forwards the Options to a portaled drop-down component.
- SearchSelectMenuPosition: A web component that renders the Menu into the <BODY> element and
  positions it with respect to an anchor provided by SearchSelectView.
- SearchSelectMenu: Renders the Menu and listens for events indicating an Item has been selected.
  Sends events through a reference to the View.
- SearchKeyboardController: A specialized listener that keeps an independent list of indices and
  tabstops, and listens for keyboard events to move the index forward or backward, as well as for
  Event or Space for "select" and Escape for "close". Doesn't actually _do_ these things; they're
  just semantics implied by the event names, it just sends an event up to the host, which can do
  what it wants with them.

What's not done:

- SearchSelect: The interface with the API.  Maps to and from API values to renderable Options.

One thing of note: of the 35 uses of SearchSelect in our product, 28 of them have `renderElement`
annotations of a single field. Six of them use the same annotation (renderFlow), and only one (in
EventMatcherPolicyForm) is at all complex.  The 28 are:

- 7: group.name;
- 1: item.label;
- 5: item.name;
- 1: policy.name;
- 1: role.name;
- 1: source.name;
- 3: stage.name;
- 9: user.username;

I propose to modify `.renderElement` to take a string of `keyof T`, where T is the type passed to the
SearchSelect; it will simply look that up in the object passed in and use that as the Label.

`.renderDescription` is more or less similar, except it has _no_ special cases:

- 6: html`${flow.name}`;
- 1: html`${source.verboseName}`;
- 9: html`${user.name}`;
- 2: html`${flow.slug}`;

Given that, it makes sense to modify this as well to take a field key as a look up and render it,
making all that function calling somewhat moot.

Selected has a similar issue; passing it a value that is _not_ a function would be a signal to find
this specific element in the corresponding 'pk'.  Or we could pass a tuple of [keyof T] and value,
so we didn't have to hard-code 'pk' into the thing.

- 1             return item.pk === this.instance?.createUsersGroup;
- 1             return item.pk === this.instance?.filterGroup;
- 2             return item.pk === this.instance?.group;
- 1             return item.pk === this.instance?.parent;
- 1             return item.pk === this.instance?.searchGroup;
- 1             return item.pk === this.instance?.syncParentGroup;
- 1             return item.pk === this.instance?.policy;
- 1             return item.pk === this.instance?.source;
- 1             return item.pk === this.instance?.passwordStage;
- 1             return item.pk === this.instance?.stage;
- 1             return item.pk === this.instance?.user;
- 2             return item.pk === this.previewUser?.pk;
- 5             return item.pk === this.instance?.configureFlow;
- 1             return item.pk === this.instance?.mapping;
- 1             return item.pk === this.instance?.nameIdMapping;
- 1             return item.pk === this.instance?.user;
- 1             return item.pk === this.instance?.webhookMapping;
- 1             return item.component === this.instance?.action;
- 1             return item.path === this.instance?.path;
- 1             return item.name === this.instance?.model;
- 1             return item.name === this.instance?.app;
- 1             return user.pk.toString() === this.request?.toString();
- 2             return this.request?.user.toString() === user.pk.toString();

And of course, `.value` kinda sorta has the same thing going on:

- 6: flow?.pk;
- 3: group ? group.pk : undefined;
- 4: group?.pk;
- 1: item?.component;
- 2: item?.name;
- 1: item?.path;
- 4: item?.pk;
- 1: policy?.pk;
- 1: role?.pk;
- 1: source?.pk;
- 3: stage?.pk;
- 8: user?.pk;
- 1: user?.username;

All in all, the _protocol_ for SearchSelect could be streamlined. A _lot_. And still retain the
existing power.

* Old take; not keeping.

* Didn't need this either.

* web: search select - a functioning search select with API interface

So many edge cases!

Because the propagation here is sometimes KeyboardEvent -> MenuEvent -> SearchSelectEvent, I had to
rename some of the events to prevent them from creating infinite loops of event handling.  This
resulted in having to define separate events for Input, Close, and Select.

I struggled like heck to get the `<input>` object to show the value after updating. Ultimately, I
had to special case the `updated()` method to make sure it was showing the currently chosen display
value.  Looking through Stack Overflow, there's a lot of contention about the meaning of the `value`
field on HTMLInputElements.

The API layer distinguishes between a "search" event, which triggers the query to run, and the
"select" event, which triggers the component to pick an object as _the_ `.value`.

The API layer handles the conversion to GroupedItems and makes sure that the View receives either
FlatSelect or GroupedSelect options collections (see ./types, but in practice users should never
care too much about this.)

* web: completed the search select update

* web: search-select reveals a weakness in our plans

While testing SearchSelect, I realized that the protocol for our "custom input elements" was
neither specified nor documented.  I have attempted to fix that, and am finding edge cases
and buggy implementations that required addressing.

I've described the protocol by creating a class that implements it: AkControlElement.  It
extends the constructor to always provide the "this is an data-ak-control element," and
provides a `json()` method that throws an exception in the base class, so it must always
be overriden and never called as super().

I've also fixed ak-dual-select so it carries its name properly into the Forms parser.

* web: search select (and friends)

This commit finalizes the search select quest! Headline: Search Select is now keyboard-friendly
*and* CSS friendly; the styling needed for position is small enough to fit in a `styleMap`, and the
styling for the menu itself can be safely locked into a web component.

Primarily, I was forgetting to map the value to its displayValue whenever the value was changed from
an external source. It should have been an easy catch, but I missed it the first dozen times
through.

* Not using this yet.  ESLint-9 experiment that was loosely left here for some reason.

* Added lots of comments.

* Added new comments, fixed error message.

* Removing a console.log

* Fixed an incorrect comment.

* Added comments about workaround.

* web: focus fixes.

Fixes several issues with the drop-down, including primarily how "loss of focus"
does not result in the pop-up being banished. Also, the type definition for the
attribute `hidden` is inconsistent between Typescript, the attribute, and the
related property; I've chosen to route around that problem by using a custom
attribute and setting `hidden` in the template, where `lit-analyze` has a workable
definition and allows it to pass. Finally, on `open` the focus is passed to the
current value, if any.
2024-07-15 17:54:06 -07:00
Tana M Berry
e20eaac56e Create readme.md (#10507)
Create for for docs migrations scripts (and others in future).

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-07-15 16:37:43 -05:00
Ken Sternberg
085ab3c2dd web: all aboard the anti-if bus, according to tooling (#10220)
* 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: all-aboard the anti-if bus, according to tooling

This commit revises a number of bugs `eslint` has been complaining about for awhile now. This is the
lesser of two PRs that will address this issue, and in this case the two biggest problems were
inappropriate conditionals (using a `switch` for a single comparison), unnecessarily named returns,
empty returns. This brings our use of conditions in-line with the coding standards we _say_ we want
in eslintrc!

* web: better names and logic for comparing the dates of Xliff vs generated files

* Missed one.

* Fixed a redirect issue that was creating an empty file in the ./web folder
2024-07-15 13:36:32 -07:00
Ken Sternberg
c0063c1749 web: fix bad name target that's breaking build (#10506)
* 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.

* root: fix migrations missing using db_alias

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* more

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* web: have no idea how this snuck through but I should have caught it.

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-07-15 18:15:46 +00:00
69 changed files with 2420 additions and 952 deletions

View File

@@ -45,6 +45,7 @@ class TestFlowInspector(APITestCase):
self.assertJSONEqual(
res.content,
{
"allow_show_password": False,
"component": "ak-stage-identification",
"flow_info": {
"background": flow.background_url,

View File

@@ -38,10 +38,11 @@ class IdentificationStage(Stage):
help_text=_(
(
"When set, shows a password field, instead of showing the "
"password field as seaprate step."
"password field as separate step."
),
),
)
case_insensitive_matching = models.BooleanField(
default=True,
help_text=_("When enabled, user fields are matched regardless of their casing."),

View File

@@ -64,6 +64,7 @@ class IdentificationChallenge(Challenge):
user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True)
password_fields = BooleanField()
allow_show_password = BooleanField(default=False)
application_pre = CharField(required=False)
flow_designation = ChoiceField(FlowDesignation.choices)
@@ -197,6 +198,8 @@ class IdentificationStageView(ChallengeStageView):
"primary_action": self.get_primary_action(),
"user_fields": current_stage.user_fields,
"password_fields": bool(current_stage.password_stage),
"allow_show_password": bool(current_stage.password_stage)
and current_stage.password_stage.allow_show_password,
"show_source_labels": current_stage.show_source_labels,
"flow_designation": self.executor.flow.designation,
}

View File

@@ -16,6 +16,7 @@ class PasswordStageSerializer(StageSerializer):
"backends",
"configure_flow",
"failed_attempts_before_cancel",
"allow_show_password",
]
@@ -28,6 +29,7 @@ class PasswordStageViewSet(UsedByMixin, ModelViewSet):
"name",
"configure_flow",
"failed_attempts_before_cancel",
"allow_show_password",
]
search_fields = ["name"]
ordering = ["name"]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.0.6 on 2024-07-02 18:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_password", "0008_replace_inbuilt"),
]
operations = [
migrations.AddField(
model_name="passwordstage",
name="allow_show_password",
field=models.BooleanField(
default=False,
help_text="When enabled, provides a 'show password' button with the password input field.",
),
),
]

View File

@@ -43,6 +43,12 @@ class PasswordStage(ConfigurableStage, Stage):
"To lock the user out, use a reputation policy and a user_write stage."
),
)
allow_show_password = models.BooleanField(
default=False,
help_text=_(
"When enabled, provides a 'show password' button with the password input field."
),
)
@property
def serializer(self) -> type[BaseSerializer]:

View File

@@ -9,7 +9,7 @@ from django.http import HttpRequest, HttpResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField
from rest_framework.fields import BooleanField, CharField
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
@@ -76,6 +76,8 @@ class PasswordChallenge(WithUserInfoChallenge):
component = CharField(default="ak-stage-password")
allow_show_password = BooleanField(default=False)
class PasswordChallengeResponse(ChallengeResponse):
"""Password challenge response"""
@@ -134,7 +136,11 @@ class PasswordStageView(ChallengeStageView):
response_class = PasswordChallengeResponse
def get_challenge(self) -> Challenge:
challenge = PasswordChallenge(data={})
challenge = PasswordChallenge(
data={
"allow_show_password": self.executor.current_stage.allow_show_password,
}
)
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
if recovery_flow.exists():
recover_url = reverse(

View File

@@ -6905,7 +6905,7 @@
"password_stage": {
"type": "integer",
"title": "Password stage",
"description": "When set, shows a password field, instead of showing the password field as seaprate step."
"description": "When set, shows a password field, instead of showing the password field as separate step."
},
"case_insensitive_matching": {
"type": "boolean",
@@ -7207,6 +7207,11 @@
"maximum": 2147483647,
"title": "Failed attempts before cancel",
"description": "How many attempts a user has before the flow is canceled. To lock the user out, use a reputation policy and a user_write stage."
},
"allow_show_password": {
"type": "boolean",
"title": "Allow show password",
"description": "When enabled, provides a 'show password' button with the password input field."
}
},
"required": []

2
go.mod
View File

@@ -28,7 +28,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024061.2
goauthentik.io/api/v3 v3.2024061.3
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0

4
go.sum
View File

@@ -293,8 +293,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024061.2 h1:9NHK2wriMENQHUmbYN3uxsdZZIV0QoEEEaGM0JS8XRY=
goauthentik.io/api/v3 v3.2024061.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024061.3 h1:gbP8mhHE2/iCDSZEnAUvRkh9DQhggdTfhsEYKg3sp/U=
goauthentik.io/api/v3 v3.2024061.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

6
poetry.lock generated
View File

@@ -4228,13 +4228,13 @@ websocket-client = ">=1.8.0"
[[package]]
name = "sentry-sdk"
version = "2.9.0"
version = "2.10.0"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = ">=3.6"
files = [
{file = "sentry_sdk-2.9.0-py2.py3-none-any.whl", hash = "sha256:0bea5fa8b564cc0d09f2e6f55893e8f70286048b0ffb3a341d5b695d1af0e6ee"},
{file = "sentry_sdk-2.9.0.tar.gz", hash = "sha256:4c85bad74df9767976afb3eeddc33e0e153300e887d637775a753a35ef99bee6"},
{file = "sentry_sdk-2.10.0-py2.py3-none-any.whl", hash = "sha256:87b3d413c87d8e7f816cc9334bff255a83d8b577db2b22042651c30c19c09190"},
{file = "sentry_sdk-2.10.0.tar.gz", hash = "sha256:545fcc6e36c335faa6d6cda84669b6e17025f31efbf3b2211ec14efe008b75d1"},
]
[package.dependencies]

View File

@@ -29993,6 +29993,10 @@ paths:
operationId: stages_password_list
description: PasswordStage Viewset
parameters:
- in: query
name: allow_show_password
schema:
type: boolean
- in: query
name: configure_flow
schema:
@@ -37067,6 +37071,9 @@ components:
nullable: true
password_fields:
type: boolean
allow_show_password:
type: boolean
default: false
application_pre:
type: string
flow_designation:
@@ -37149,7 +37156,7 @@ components:
format: uuid
nullable: true
description: When set, shows a password field, instead of showing the password
field as seaprate step.
field as separate step.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@@ -37217,7 +37224,7 @@ components:
format: uuid
nullable: true
description: When set, shows a password field, instead of showing the password
field as seaprate step.
field as separate step.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@@ -40953,6 +40960,9 @@ components:
type: string
recovery_url:
type: string
allow_show_password:
type: boolean
default: false
required:
- pending_user
- pending_user_avatar
@@ -41235,6 +41245,10 @@ components:
minimum: -2147483648
description: How many attempts a user has before the flow is canceled. To
lock the user out, use a reputation policy and a user_write stage.
allow_show_password:
type: boolean
description: When enabled, provides a 'show password' button with the password
input field.
required:
- backends
- component
@@ -41271,6 +41285,10 @@ components:
minimum: -2147483648
description: How many attempts a user has before the flow is canceled. To
lock the user out, use a reputation policy and a user_write stage.
allow_show_password:
type: boolean
description: When enabled, provides a 'show password' button with the password
input field.
required:
- backends
- name
@@ -42092,7 +42110,7 @@ components:
format: uuid
nullable: true
description: When set, shows a password field, instead of showing the password
field as seaprate step.
field as separate step.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@@ -42804,6 +42822,10 @@ components:
minimum: -2147483648
description: How many attempts a user has before the flow is canceled. To
lock the user out, use a reputation policy and a user_write stage.
allow_show_password:
type: boolean
description: When enabled, provides a 'show password' button with the password
input field.
PatchedPermissionAssignRequest:
type: object
description: Request to assign a new permission

View File

@@ -10,8 +10,8 @@
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@wdio/cli": "^8.39.1",
"@wdio/local-runner": "^8.39.1",
"@wdio/mocha-framework": "^8.39.0",
@@ -943,16 +943,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
"integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz",
"integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/type-utils": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"@typescript-eslint/scope-manager": "7.16.1",
"@typescript-eslint/type-utils": "7.16.1",
"@typescript-eslint/utils": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -976,15 +976,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
"integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz",
"integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"@typescript-eslint/scope-manager": "7.16.1",
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/typescript-estree": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1",
"debug": "^4.3.4"
},
"engines": {
@@ -1004,13 +1004,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
"integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz",
"integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0"
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1021,13 +1021,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
"integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz",
"integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.1",
"@typescript-eslint/utils": "7.16.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1048,9 +1048,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
"integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz",
"integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==",
"dev": true,
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1061,13 +1061,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
"integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz",
"integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1113,15 +1113,15 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
"integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz",
"integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0"
"@typescript-eslint/scope-manager": "7.16.1",
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/typescript-estree": "7.16.1"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1135,12 +1135,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
"integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz",
"integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/types": "7.16.1",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {

View File

@@ -4,8 +4,8 @@
"type": "module",
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@wdio/cli": "^8.39.1",
"@wdio/local-runner": "^8.39.1",
"@wdio/mocha-framework": "^8.39.0",

View File

@@ -53,7 +53,7 @@ type Pair = [string, string];
// Define a getter for each provider type in the radio button collection.
const providerValues: Pair[] = [
["oauth2provider", "oauth2Provider"],
["oauth2Provider", "oauth2Provider"],
["ldapprovider", "ldapProvider"],
["proxyprovider-proxy", "proxyProviderProxy"],
["proxyprovider-forwardsingle", "proxyProviderForwardsingle"],
@@ -66,7 +66,7 @@ providerValues.forEach(([value, name]: Pair) => {
Object.defineProperties(ApplicationWizardView.prototype, {
[name]: {
get: function () {
return this.providerList.$(`>>>input[value="${value}"]`);
return this.providerList.$(`>>>div[data-testid=wizard-provider-${value}]`);
},
},
});

View File

@@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
export class ForwardProxyForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
'[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
`div*=${selector}`,
);
}

View File

@@ -3,9 +3,9 @@ import Page from "../page.js";
export class LdapForm extends Page {
async setBindFlow(selector: string) {
await this.searchSelect(
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
"[name=authorizationFlow]",
"authorizationFlow",
`button*=${selector}`,
`div*=${selector}`,
);
}
}

View File

@@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
export class OauthForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
'[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
`div*=${selector}`,
);
}

View File

@@ -3,9 +3,9 @@ import Page from "../page.js";
export class RadiusForm extends Page {
async setAuthenticationFlow(selector: string) {
await this.searchSelect(
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
'[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
`div*=${selector}`,
);
}
}

View File

@@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
export class SamlForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
'[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
`div*=${selector}`,
);
}

View File

@@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
export class TransparentProxyForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
'[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
`div*=${selector}`,
);
}

View File

@@ -1,6 +1,7 @@
import Page from "./page.js";
import UserLibraryPage from "./user-library.page.js";
import { $ } from "@wdio/globals";
import { and, or, presenceOf, textToBePresentInElement } from "wdio-wait-for";
/**
* sub page containing specific selectors and methods for a specific page
@@ -48,6 +49,14 @@ class LoginPage extends Page {
await this.pause();
await this.password(password);
await this.pause();
const redirect = await $(">>>a[type=submit]");
await browser.waitUntil(or(presenceOf(redirect), presenceOf(UserLibraryPage.pageHeader)));
if (await redirect.isExisting()) {
await redirect.click();
}
await this.pause(">>>div.header h1");
return UserLibraryPage;
}

View File

@@ -32,10 +32,16 @@ export default class Page {
*/
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
const inputBind = await $(searchSelector);
const controlSelector = `>>>ak-search-select-view${searchSelector}`;
const control = await $(controlSelector);
control.scrollIntoView();
const inputBind = await control.$(">>>input[type=text]");
await inputBind.click();
const searchBlock = await $(`>>>div[data-managed-for="${managedSelector}"]`);
const target = searchBlock.$(buttonSelector);
const interior = searchBlock.$(">>>ul");
interior.scrollIntoView();
const target = interior.$(buttonSelector);
return await target.click();
}

View File

@@ -1,10 +1,7 @@
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
import { GOOD_PASSWORD, GOOD_USERNAME } from "./constants.js";
import { expect } from "@wdio/globals";
export const login = async () => {
await LoginPage.open();
await LoginPage.login(GOOD_USERNAME, GOOD_PASSWORD);
await expect(UserLibraryPage.pageHeader).toHaveText("My applications");
};

View File

@@ -212,7 +212,9 @@ export const config: Options.Testrunner = {
* @param {Array.<String>} specs List of spec file paths that are to be run
* @param {object} browser instance of created browser/device session
*/
before: function (_capabilities, _specs) {},
before: function (_capabilities, _specs) {
browser.setWindowSize(1920, 1080);
},
/**
* Runs before a WebdriverIO command gets executed.
* @param {string} commandName hook command name

View File

@@ -1,30 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:lit/recommended",
"plugin:custom-elements/recommended",
"plugin:storybook/recommended",
"plugin:sonarjs/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "lit", "custom-elements", "sonarjs"],
"rules": {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { "avoidEscape": true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
"sonarjs/cognitive-complexity": ["warn", 9],
"sonarjs/no-duplicate-string": "off",
"sonarjs/no-nested-template-literals": "off"
}
}

375
web/package-lock.json generated
View File

@@ -16,9 +16,10 @@
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/legacy-modes": "^6.4.0",
"@codemirror/theme-one-dark": "^6.1.2",
"@floating-ui/dom": "^1.6.3",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.6.1-1720888668",
"@goauthentik/api": "^2024.6.1-1721092506",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.1",
"@lit/reactive-element": "^2.0.4",
@@ -48,7 +49,7 @@
"yaml": "^2.4.5"
},
"devDependencies": {
"@babel/core": "^7.24.8",
"@babel/core": "^7.24.9",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/plugin-transform-private-methods": "^7.24.7",
@@ -64,13 +65,13 @@
"@lit/localize-tools": "^0.7.2",
"@rollup/plugin-replace": "^5.0.7",
"@spotlightjs/spotlight": "^2.0.0",
"@storybook/addon-essentials": "^8.2.2",
"@storybook/addon-links": "^8.2.2",
"@storybook/addon-essentials": "^8.2.4",
"@storybook/addon-links": "^8.2.4",
"@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.0.8",
"@storybook/manager-api": "^8.2.2",
"@storybook/web-components": "^8.2.2",
"@storybook/web-components-vite": "^8.2.2",
"@storybook/manager-api": "^8.2.4",
"@storybook/web-components": "^8.2.4",
"@storybook/web-components-vite": "^8.2.4",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "5.60.15",
@@ -92,14 +93,14 @@
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.11.0",
"eslint-plugin-sonarjs": "^0.25.1",
"eslint-plugin-sonarjs": "0.25.1",
"eslint-plugin-storybook": "^0.8.0",
"github-slugger": "^2.0.0",
"glob": "^11.0.0",
"lit-analyzer": "^2.0.3",
"lockfile-lint": "^4.14.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.2",
"prettier": "^3.3.3",
"pseudolocale": "^2.1.0",
"react": "^18.2.0",
"react-dom": "^18.3.1",
@@ -168,21 +169,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.8.tgz",
"integrity": "sha512-6AWcmZC/MZCO0yKys4uhg5NlxL0ESF3K6IAaoQ+xSXvPyPyxNWRafP+GDbI88Oh68O7QkJgmEtedWPM9U0pZNg==",
"version": "7.24.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
"@babel/generator": "^7.24.8",
"@babel/generator": "^7.24.9",
"@babel/helper-compilation-targets": "^7.24.8",
"@babel/helper-module-transforms": "^7.24.8",
"@babel/helper-module-transforms": "^7.24.9",
"@babel/helpers": "^7.24.8",
"@babel/parser": "^7.24.8",
"@babel/template": "^7.24.7",
"@babel/traverse": "^7.24.8",
"@babel/types": "^7.24.8",
"@babel/types": "^7.24.9",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -198,12 +199,12 @@
}
},
"node_modules/@babel/generator": {
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.8.tgz",
"integrity": "sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==",
"version": "7.24.9",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.9.tgz",
"integrity": "sha512-G8v3jRg+z8IwY1jHFxvCNhOPYPterE4XljNgdGTYfSTtzzwjIswIzIaSPSLs3R7yFuqnqNeay5rjICfqVr+/6A==",
"dev": true,
"dependencies": {
"@babel/types": "^7.24.8",
"@babel/types": "^7.24.9",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1"
@@ -363,9 +364,9 @@
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.8.tgz",
"integrity": "sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==",
"version": "7.24.9",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz",
"integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.24.7",
@@ -2045,9 +2046,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.8.tgz",
"integrity": "sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==",
"version": "7.24.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz",
"integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.24.8",
@@ -3673,9 +3674,9 @@
"dev": true
},
"node_modules/@goauthentik/api": {
"version": "2024.6.1-1720888668",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1720888668.tgz",
"integrity": "sha512-3CJl/mS3o5W8F+qfCeFUIjFLRY6xtd1BcjlDcTqMelOIW9VHfh7iVrij1uUW0UE49/siVFw9wwqUXdBxx9pcTA=="
"version": "2024.6.1-1721092506",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1721092506.tgz",
"integrity": "sha512-XrRLZz3bsLBuww8FDHqNH4tHr3sqw/B3Bf5LiAexho00ilKSSYqKnuoDVJm16HNeEezc3gVhfZoyVVu/nbGYxA=="
},
"node_modules/@hcaptcha/types": {
"version": "1.0.3",
@@ -6586,9 +6587,9 @@
}
},
"node_modules/@storybook/addon-actions": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.2.2.tgz",
"integrity": "sha512-SN4cSRt3f0qXi5te+yhMseSdQuZntA8lGlASbRmN77YQTpIaGsNiH88xFoky0s9qz531hiRfU1R0ZSMylBwSKw==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.2.4.tgz",
"integrity": "sha512-l1dlzWBBkR/5aullsX8N1ZbYr2bkeHPAaMCRy1jG5BBA8IHbi55JFwmJ8XF2gXkT2GyAZnePzb43RuLXz4KxFQ==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -6602,13 +6603,13 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addon-backgrounds": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.2.2.tgz",
"integrity": "sha512-m/xJe7uKL+kfJx7pQcHwAeIvJ3tdLIpDGrMAVDNDJHcAxfe44cFjIInaV/1HKf3y5Awap+DZFW66ekkxuI9zzA==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.2.4.tgz",
"integrity": "sha512-4oU25rFyr4OgMxHe4RpLJ7lxVwUDfdTi1j/YVyHfYv8koTqjagso8bv0uj0ujP5C3dSsVO0sp3/JOfPDkEUtrA==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -6620,13 +6621,13 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addon-controls": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.2.2.tgz",
"integrity": "sha512-y241aOANGzT5XBADUIvALwG/xF5eC6UItzmWJaFvOzSBCq74GIA0+Hu9atyFdvFQbXOrdvPWC4jR+9iuBFRxAA==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.2.4.tgz",
"integrity": "sha512-e56aUYhxyR8zJJstRAUP3WILhWTcvgRf5bysTtiyjFAL7U47cuCr043+IYEsxLkXhuZTKX2pcYSrjBtT5bYkVA==",
"dev": true,
"dependencies": {
"dequal": "^2.0.2",
@@ -6638,21 +6639,21 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addon-docs": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.2.2.tgz",
"integrity": "sha512-qk/yjAR9RpsSrKLLbeCgb6u58c8TmYqyJSnXgbAozZZNKHBWlIpvZ/hTNYud8qo0coPlxnLdjnZf32TykWGlAg==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.2.4.tgz",
"integrity": "sha512-oyrDw4nGfntu5Hkhr2Qt1wUOyLaVVERQekYyejyir92QhM10UeA7ZarPXNLfCTj7rbTrWmM1Waka9Tsf8TGMrw==",
"dev": true,
"dependencies": {
"@babel/core": "^7.24.4",
"@mdx-js/react": "^3.0.0",
"@storybook/blocks": "8.2.2",
"@storybook/csf-plugin": "8.2.2",
"@storybook/blocks": "8.2.4",
"@storybook/csf-plugin": "8.2.4",
"@storybook/global": "^5.0.0",
"@storybook/react-dom-shim": "8.2.2",
"@storybook/react-dom-shim": "8.2.4",
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"fs-extra": "^11.1.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
@@ -6666,7 +6667,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addon-docs/node_modules/fs-extra": {
@@ -6684,20 +6685,20 @@
}
},
"node_modules/@storybook/addon-essentials": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.2.2.tgz",
"integrity": "sha512-yN//BFMbSvNV0+Sll2hcKmgJX06TUKQDm6pZimUjkXczFtOmK7K/UdDmKjWS+qjhfJdWpxdRoEpxoHvvRmNfsA==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.2.4.tgz",
"integrity": "sha512-4upNauDJAJxauxnoUpUvzDnLo18C2yTVxgg+Id9wrKpt9C+CYH2oXyXzxoYGucYWZEe7zgCO6rWrGrKEisiLPQ==",
"dev": true,
"dependencies": {
"@storybook/addon-actions": "8.2.2",
"@storybook/addon-backgrounds": "8.2.2",
"@storybook/addon-controls": "8.2.2",
"@storybook/addon-docs": "8.2.2",
"@storybook/addon-highlight": "8.2.2",
"@storybook/addon-measure": "8.2.2",
"@storybook/addon-outline": "8.2.2",
"@storybook/addon-toolbars": "8.2.2",
"@storybook/addon-viewport": "8.2.2",
"@storybook/addon-actions": "8.2.4",
"@storybook/addon-backgrounds": "8.2.4",
"@storybook/addon-controls": "8.2.4",
"@storybook/addon-docs": "8.2.4",
"@storybook/addon-highlight": "8.2.4",
"@storybook/addon-measure": "8.2.4",
"@storybook/addon-outline": "8.2.4",
"@storybook/addon-toolbars": "8.2.4",
"@storybook/addon-viewport": "8.2.4",
"ts-dedent": "^2.0.0"
},
"funding": {
@@ -6705,13 +6706,13 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addon-highlight": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.2.2.tgz",
"integrity": "sha512-yDTRzzL+IJAymgY32xoZl09BGBVmPOUV2wVNGYcZkkBLvz2GSQMTfUe1/7F4jAx//+rFBu48/MQzsTC7Bk8kPw==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.2.4.tgz",
"integrity": "sha512-Ll/2y0m/q9ko9jFt40qsiee4fds6vpcwwxi3mPAVwRV/J7PpMzPkoLxM54bKpeHiWdTeGCXRguXNvyeQMQf3pg==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -6721,13 +6722,13 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addon-links": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.2.2.tgz",
"integrity": "sha512-eGh7O7SgTJMtnuXC0HlRPOegu1njcJS2cnVqjbzjvjxsPSBhbHpdYMi9Q9E7al/FKuqMUOjIR9YLIlmK1AJaqA==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.2.4.tgz",
"integrity": "sha512-1FgD6YXdXXSEDrp2aO4LxYt/X7LnBYx7cLlFla+xbn1CZLGqWLLeOT+BFd29wxpzs3u1Tap9r1iz1vRYL5ziyg==",
"dev": true,
"dependencies": {
"@storybook/csf": "0.1.11",
@@ -6740,7 +6741,7 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"storybook": "^8.2.2"
"storybook": "^8.2.4"
},
"peerDependenciesMeta": {
"react": {
@@ -6749,9 +6750,9 @@
}
},
"node_modules/@storybook/addon-measure": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.2.2.tgz",
"integrity": "sha512-3rCo/aMltt5FrBVdr2dYlD8HlE2q9TLKGJZnwh9on4QyL6ArHbdYw0LmyHe/LrFahJ49w1XQZBMSJcAdRkkS7w==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.2.4.tgz",
"integrity": "sha512-bSyE3mGDaaIKoe6Kt/f20YXKsn8WSoJUHrfKA68gbb+H3tegVQaqeS2KY5YzLqvjHe1qSmrO132NJt8RixLOPQ==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -6762,13 +6763,13 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addon-outline": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.2.2.tgz",
"integrity": "sha512-Y+PQtfTNO8GLX5nz+3x5AMfHNvdGvBXazJ29+Rl1ygYN1+Q9ZhRJDE1kAK0wLxb7CG14peAgdYEaQb3Rduv7HQ==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.2.4.tgz",
"integrity": "sha512-1C6NrvSDREgCZ7o/1n7Ca81uDDzrSrzWiOkh4OeA7PPQ/445cAOX2OMvxzNkKDIT9GLCLNi9M5XIVyGxJVS4dQ==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -6779,26 +6780,26 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addon-toolbars": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.2.2.tgz",
"integrity": "sha512-JGOueOc3EPljlCl9dVSQee0aMYoqGNvN0UH+R6wYJ3bDZ+tUG/iYpsZVPUOvS8vzp3Imk5Is1kzQbQYJtzdGLg==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.2.4.tgz",
"integrity": "sha512-iPnSr+hdz40Uoqg2cimyWf01/Y8GdgdMKB+b47TGIxtn9SEFBXck00ZG8ttwBvEsecu9K9CDt20fIOnr6oK5tQ==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addon-viewport": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.2.2.tgz",
"integrity": "sha512-gkZ8bsjGGP0NuevkT2iKC+szezSy+w4BrBDknf490mRU2K/B2e7TGojf/j/AtxzILMzD4IKzKUXbE/zwcqjZvA==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.2.4.tgz",
"integrity": "sha512-58DcoX0xGpWlJfc0iLDjggkVPYzT4JdCZA2ioK9SQXQMsUzGFwR5PAAJv1tivYp7467tNkXvcM3QTb3Q3g8p4g==",
"dev": true,
"dependencies": {
"memoizerific": "^1.11.3"
@@ -6808,7 +6809,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/addons": {
@@ -7101,9 +7102,9 @@
}
},
"node_modules/@storybook/blocks": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.2.2.tgz",
"integrity": "sha512-av0Tryg4toDl2L/d1ABErtsAk9wvM1su6+M4wq5/Go50sk5IjGTldhbZFa9zNOohxLkZwaj0Q5xAgJ1Y+m5KrQ==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.2.4.tgz",
"integrity": "sha512-Hl2Dpg41YiJLSVXxjEJPjgPShrDJM3RY6HEEOjqTcAADsheX1IHAWXMJSJGMmne3Sew6VdJXPuHBIOFV4suZxg==",
"dev": true,
"dependencies": {
"@storybook/csf": "0.1.11",
@@ -7128,7 +7129,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"storybook": "^8.2.2"
"storybook": "^8.2.4"
},
"peerDependenciesMeta": {
"react": {
@@ -7140,12 +7141,12 @@
}
},
"node_modules/@storybook/builder-vite": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.2.2.tgz",
"integrity": "sha512-tyt+CjzLEuRHU2NERZSy7JfnTpTJo10HrRysJcRtzclu3TOzx7bWszUJRHho9ttyypBX6w5+8TPcqXh/vu0tig==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.2.4.tgz",
"integrity": "sha512-hDx0ZLcnFrIJaVoFMu41d9w1uWmwy/DDUuIbSd0T7xHwWyVqgI8lmaQlBIp81/QmSKaUB964UduHcdIjkoWoYA==",
"dev": true,
"dependencies": {
"@storybook/csf-plugin": "8.2.2",
"@storybook/csf-plugin": "8.2.4",
"@types/find-cache-dir": "^3.2.1",
"browser-assert": "^1.2.1",
"es-module-lexer": "^1.5.0",
@@ -7161,7 +7162,7 @@
},
"peerDependencies": {
"@preact/preset-vite": "*",
"storybook": "^8.2.2",
"storybook": "^8.2.4",
"typescript": ">= 4.3.x",
"vite": "^4.0.0 || ^5.0.0",
"vite-plugin-glimmerx": "*"
@@ -7234,15 +7235,15 @@
}
},
"node_modules/@storybook/codemod": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.2.2.tgz",
"integrity": "sha512-wRUVKLHVUhbLJYKW3QOufUxJGwaUT4jTCD8+HOGpHPdJO3NrwXu186xt4tuPZO2Y/NnacPeCQPsaK5ok4O8o7A==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.2.4.tgz",
"integrity": "sha512-QcZdqjX4NvkVcWR3yI9it3PfqmBOCR+3iY6j4PmG7p5IE0j9kXMKBbeFrBRprSijHKlwcjbc3bRx2SnKF6AFEg==",
"dev": true,
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/preset-env": "^7.24.4",
"@babel/types": "^7.24.0",
"@storybook/core": "8.2.2",
"@storybook/core": "8.2.4",
"@storybook/csf": "0.1.11",
"@types/cross-spawn": "^6.0.2",
"cross-spawn": "^7.0.3",
@@ -7332,9 +7333,9 @@
}
},
"node_modules/@storybook/core": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.2.2.tgz",
"integrity": "sha512-L4ojYI+Os/i5bCReDIlFgEDQSS94mbJlNU9WRzEGZpqNC5/hbFEC9Tip7P1MiRx9NrewkzU7b+UCP7mi3e4drQ==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.2.4.tgz",
"integrity": "sha512-jePmsGZT2hhUNQs8ED6+hFVt2m4hrMseO8kkN7Mcsve1MIujzHUS7Gjo4uguBwHJJOtiXB2fw4OSiQCmsXscZA==",
"dev": true,
"dependencies": {
"@storybook/csf": "0.1.11",
@@ -7456,9 +7457,9 @@
}
},
"node_modules/@storybook/csf-plugin": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.2.2.tgz",
"integrity": "sha512-3K2RUpDDvq3DT46qAIj2VBC+fzTTebRUcZUsRfS6G1AzaX9p25iClEHiwcJacFkgQKhkci8A/Ly3Z4JJ3b4Pgw==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.2.4.tgz",
"integrity": "sha512-7V2tmeyAwv4/AQiBpB+7fCpphnY1yhcz+Zv9esUOHKqFn5+7u9FKpEXFFcf6fcbqXr2KoNw2F1EnTv3K/SxXrg==",
"dev": true,
"dependencies": {
"unplugin": "^1.3.1"
@@ -7468,7 +7469,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/global": {
@@ -7489,107 +7490,35 @@
}
},
"node_modules/@storybook/manager-api": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.2.2.tgz",
"integrity": "sha512-v7pbddJO21RAsGyT0+GZMgP25nLCdhQFYnmy+aRCgL6rz+k7bToPwcL+qK0mb5sfng+Ah2MAAK9ZvXWTYAVeqw==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.2.4.tgz",
"integrity": "sha512-ayiOtcGupSeLCi2doEsRpALNPo4MBWYruc+e3jjkeVJQIg9A1ipSogNQh8unuOmq9rezO4/vcNBd6MxLs3xLWg==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/preview-api": {
"version": "8.1.11",
"resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.1.11.tgz",
"integrity": "sha512-8ZChmFV56GKppCJ0hnBd/kNTfGn2gWVq1242kuet13pbJtBpvOhyq4W01e/Yo14tAPXvgz8dSnMvWLbJx4QfhQ==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.2.4.tgz",
"integrity": "sha512-IxOiUYYzNnk1OOz3zQBhsa3P1fsgqeMBZcH7TjiQWs9osuWG20oqsFR6+Z3dxoW8IuQHvpnREGKvAbRsDsThcA==",
"dev": true,
"dependencies": {
"@storybook/channels": "8.1.11",
"@storybook/client-logger": "8.1.11",
"@storybook/core-events": "8.1.11",
"@storybook/csf": "^0.1.7",
"@storybook/global": "^5.0.0",
"@storybook/types": "8.1.11",
"@types/qs": "^6.9.5",
"dequal": "^2.0.2",
"lodash": "^4.17.21",
"memoizerific": "^1.11.3",
"qs": "^6.10.0",
"tiny-invariant": "^1.3.1",
"ts-dedent": "^2.0.0",
"util-deprecate": "^1.0.2"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@storybook/preview-api/node_modules/@storybook/channels": {
"version": "8.1.11",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-8.1.11.tgz",
"integrity": "sha512-fu5FTqo6duOqtJFa6gFzKbiSLJoia+8Tibn3xFfB6BeifWrH81hc+AZq0lTmHo5qax2G5t8ZN8JooHjMw6k2RA==",
"dev": true,
"dependencies": {
"@storybook/client-logger": "8.1.11",
"@storybook/core-events": "8.1.11",
"@storybook/global": "^5.0.0",
"telejson": "^7.2.0",
"tiny-invariant": "^1.3.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@storybook/preview-api/node_modules/@storybook/client-logger": {
"version": "8.1.11",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-8.1.11.tgz",
"integrity": "sha512-DVMh2usz3yYmlqCLCiCKy5fT8/UR9aTh+gSqwyNFkGZrIM4otC5A8eMXajXifzotQLT5SaOEnM3WzHwmpvMIEA==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@storybook/preview-api/node_modules/@storybook/core-events": {
"version": "8.1.11",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-8.1.11.tgz",
"integrity": "sha512-vXaNe2KEW9BGlLrg0lzmf5cJ0xt+suPjWmEODH5JqBbrdZ67X6ApA2nb6WcxDQhykesWCuFN5gp1l+JuDOBi7A==",
"dev": true,
"dependencies": {
"@storybook/csf": "^0.1.7",
"ts-dedent": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@storybook/preview-api/node_modules/@storybook/types": {
"version": "8.1.11",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-8.1.11.tgz",
"integrity": "sha512-k9N5iRuY2+t7lVRL6xeu6diNsxO3YI3lS4Juv3RZ2K4QsE/b3yG5ElfJB8DjHDSHwRH4ORyrU71KkOCUVfvtnw==",
"dev": true,
"dependencies": {
"@storybook/channels": "8.1.11",
"@types/express": "^4.7.0",
"file-system-cache": "2.3.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
"peerDependencies": {
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/react-dom-shim": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.2.2.tgz",
"integrity": "sha512-4fb1/yT9WXHzHjs0In6orIEZxga5eXd9UaXEFGudBgowCjDUVP9LabDdKTbGusz20lfaAkATsRG/W+EcSLoh8w==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.2.4.tgz",
"integrity": "sha512-p2ypPWuKKFY/ij7yYjvdnrOcfdpxnAJd9D4/2Hm2eVioE4y8HQSND54t9OfkW+498Ez7ph4zW9ez005XqzH/+w==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -7598,7 +7527,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/theming": {
@@ -7649,12 +7578,16 @@
}
},
"node_modules/@storybook/web-components": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.2.2.tgz",
"integrity": "sha512-6lFesQw9TmdbzgFRytlNqQDPgEqlCQzOvW91ZDyDBdytN64XXONYfNBJqef0tM3hLcXBv1vNIzlnOsMRDhIhZQ==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.2.4.tgz",
"integrity": "sha512-S1ggBI9x+RjUj/iUCOJuW7emf+PnkslHUrfTpsmmlKqDGdSMJoqH7eZiFRQ0B/p/aT+IU3jRnCSsjF4N5eDHLw==",
"dev": true,
"dependencies": {
"@storybook/components": "^8.2.4",
"@storybook/global": "^5.0.0",
"@storybook/manager-api": "^8.2.4",
"@storybook/preview-api": "^8.2.4",
"@storybook/theming": "^8.2.4",
"tiny-invariant": "^1.3.1",
"ts-dedent": "^2.0.0"
},
@@ -7667,17 +7600,17 @@
},
"peerDependencies": {
"lit": "^2.0.0 || ^3.0.0",
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/web-components-vite": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-8.2.2.tgz",
"integrity": "sha512-e+yGo31hNW2uw+ujl9sNLju/JBwCCvOCaqO1Pk+MDy0Kz3xGbr+4RHRbRQ+DyngLdvUZIzy/gypd/Im4wPyf0Q==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-8.2.4.tgz",
"integrity": "sha512-zJwhlYgoMwPHiM7ySLOgTDuNBDH3qPmi+qrvtdpEGVdrSIvijx5jsQQz4XTP2b6BXyOg1g9VaMfQ5S8LaSZ74A==",
"dev": true,
"dependencies": {
"@storybook/builder-vite": "8.2.2",
"@storybook/web-components": "8.2.2",
"@storybook/builder-vite": "8.2.4",
"@storybook/web-components": "8.2.4",
"magic-string": "^0.30.0"
},
"engines": {
@@ -7688,7 +7621,33 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.2"
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/web-components/node_modules/@storybook/components": {
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.2.4.tgz",
"integrity": "sha512-JLT1RoR/RXX+ZTeFoY85CRHb9Zz3l0PRRUSetEjoIJdnBGeL5C38bs0s9QnYjpCDLUlhdYhTln+GzmbyH8ocpA==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.4"
}
},
"node_modules/@storybook/web-components/node_modules/@storybook/theming": {
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.2.4.tgz",
"integrity": "sha512-B4HQMzTeg1TgV9uPDIoDkMSnP839Y05I9+Tw60cilAD+jTqrCvMlccHfehsTzJk+gioAflunATcbU05TMZoeIQ==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.2.4"
}
},
"node_modules/@swagger-api/apidom-ast": {
@@ -14463,8 +14422,9 @@
},
"node_modules/eslint-plugin-sonarjs": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.25.1.tgz",
"integrity": "sha512-5IOKvj/GMBNqjxBdItfotfRHo7w48496GOu1hxdeXuD0mB1JBlDCViiLHETDTfA8pDAVSBimBEQoetRXYceQEw==",
"dev": true,
"license": "LGPL-3.0-only",
"engines": {
"node": ">=16"
},
@@ -20552,9 +20512,10 @@
}
},
"node_modules/prettier": {
"version": "3.3.2",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -22412,15 +22373,15 @@
"license": "MIT"
},
"node_modules/storybook": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-8.2.2.tgz",
"integrity": "sha512-xDT9gyzAEFQNeK7P+Mj/8bNzN+fbm6/4D6ihdSzmczayjydpNjMs74HDHMY6S4Bfu6tRVyEK2ALPGnr6ZVofBA==",
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-8.2.4.tgz",
"integrity": "sha512-ASavW8vIHiWpFY+4M6ngeqK5oL4OkxqdpmQYxvRqH0gA1G1hfq/vmDw4YC4GnqKwyWPQh2kaV5JFurKZVaeaDQ==",
"dev": true,
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/types": "^7.24.0",
"@storybook/codemod": "8.2.2",
"@storybook/core": "8.2.2",
"@storybook/codemod": "8.2.4",
"@storybook/core": "8.2.4",
"@types/semver": "^7.3.4",
"@yarnpkg/fslib": "2.10.3",
"@yarnpkg/libzip": "2.3.0",

View File

@@ -43,9 +43,10 @@
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/legacy-modes": "^6.4.0",
"@codemirror/theme-one-dark": "^6.1.2",
"@floating-ui/dom": "^1.6.3",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.6.1-1720888668",
"@goauthentik/api": "^2024.6.1-1721092506",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.1",
"@lit/reactive-element": "^2.0.4",
@@ -75,7 +76,7 @@
"yaml": "^2.4.5"
},
"devDependencies": {
"@babel/core": "^7.24.8",
"@babel/core": "^7.24.9",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/plugin-transform-private-methods": "^7.24.7",
@@ -91,13 +92,13 @@
"@lit/localize-tools": "^0.7.2",
"@rollup/plugin-replace": "^5.0.7",
"@spotlightjs/spotlight": "^2.0.0",
"@storybook/addon-essentials": "^8.2.2",
"@storybook/addon-links": "^8.2.2",
"@storybook/addon-essentials": "^8.2.4",
"@storybook/addon-links": "^8.2.4",
"@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.0.8",
"@storybook/manager-api": "^8.2.2",
"@storybook/web-components": "^8.2.2",
"@storybook/web-components-vite": "^8.2.2",
"@storybook/manager-api": "^8.2.4",
"@storybook/web-components": "^8.2.4",
"@storybook/web-components-vite": "^8.2.4",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "5.60.15",
@@ -119,14 +120,14 @@
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.11.0",
"eslint-plugin-sonarjs": "^0.25.1",
"eslint-plugin-sonarjs": "0.25.1",
"eslint-plugin-storybook": "^0.8.0",
"github-slugger": "^2.0.0",
"glob": "^11.0.0",
"lit-analyzer": "^2.0.3",
"lockfile-lint": "^4.14.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.2",
"prettier": "^3.3.3",
"pseudolocale": "^2.1.0",
"react": "^18.2.0",
"react-dom": "^18.3.1",

View File

@@ -5,9 +5,9 @@ import process from "process";
const localizeRules = JSON.parse(fs.readFileSync("./lit-localize.json", "utf-8"));
function compareXlfAndSrc(loc) {
const xlf = path.join("./xliff", `${loc}.xlf`);
const src = path.join("./src/locales", `${loc}.ts`);
function generatedFileIsUpToDateWithXliffSource(loc) {
const xliff = path.join("./xliff", `${loc}.xlf`);
const gened = path.join("./src/locales", `${loc}.ts`);
// Returns false if: the expected XLF file doesn't exist, The expected
// generated file doesn't exist, or the XLF file is newer (has a higher date)
@@ -15,29 +15,28 @@ function compareXlfAndSrc(loc) {
// generates a unique error message and halts the build.
try {
var xlfStat = fs.statSync(xlf);
var xlfStat = fs.statSync(xliff);
} catch (_error) {
console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`);
process.exit(1);
}
// If the generated file doesn't exist, of course it's not up to date.
try {
var srcStat = fs.statSync(src);
var genedStat = fs.statSync(gened);
} catch (_error) {
return false;
}
// if the xlf is newer (greater) than src, it's out of date.
if (xlfStat.mtimeMs > srcStat.mtimeMs) {
return false;
}
return true;
// if the generated file is the same age or older (date is greater) than the xliff file, it's
// presumed to have been generated by that file and is up-to-date.
return genedStat.mtimeMs >= xlfStat.mtimeMs;
}
// For all the expected files, find out if any aren't up-to-date.
const upToDate = localizeRules.targetLocales.reduce(
(acc, loc) => acc && compareXlfAndSrc(loc),
(acc, loc) => acc && generatedFileIsUpToDateWithXliffSource(loc),
true,
);
@@ -61,7 +60,9 @@ if (!upToDate) {
.map((locale) => `Locale '${locale}' has ${counts.get(locale)} missing translations`)
.join("\n");
// eslint-disable-next-line no-console
console.log(`Translation tables rebuilt.\n${report}\n`);
}
// eslint-disable-next-line no-console
console.log("Locale ./src is up-to-date");

View File

@@ -12,4 +12,5 @@ const cmd = [
"-S './src/locales/**' ./src -s",
].join(" ");
// eslint-disable-next-line no-console
console.log(execSync(cmd, { encoding: "utf8" }));

View File

@@ -43,34 +43,53 @@ const eslintConfig = {
},
};
const porcelainV1 = /^(..)\s+(.*$)/;
const gitStatus = execFileSync("git", ["status", "--porcelain", "."], { encoding: "utf8" });
function findChangedFiles() {
const porcelainV1 = /^(..)\s+(.*$)/;
const gitStatus = execFileSync("git", ["status", "--porcelain", "."], { encoding: "utf8" });
const statuses = gitStatus.split("\n").reduce((acc, line) => {
const match = porcelainV1.exec(line.replace("\n"));
if (!match) {
return acc;
}
const [status, path] = Array.from(match).slice(1, 3);
return [...acc, [status, path.split("\x00")[0]]];
}, []);
const statuses = gitStatus.split("\n").reduce((acc, line) => {
const match = porcelainV1.exec(line.replace("\n"));
if (!match) {
return acc;
}
const [status, path] = Array.from(match).slice(1, 3);
return [...acc, [status, path.split("\x00")[0]]];
}, []);
const isModified = /^(M|\?|\s)(M|\?|\s)/;
const modified = (s) => isModified.test(s);
const isModified = /^(M|\?|\s)(M|\?|\s)/;
const modified = (s) => isModified.test(s);
const isCheckable = /\.(ts|js|mjs)$/;
const checkable = (s) => isCheckable.test(s);
const isCheckable = /\.(ts|js|mjs)$/;
const checkable = (s) => isCheckable.test(s);
const ignored = /\/\.storybook\//;
const notIgnored = (s) => !ignored.test(s);
const ignored = /\/\.storybook\//;
const notIgnored = (s) => !ignored.test(s);
const updated = statuses.reduce(
(acc, [status, filename]) =>
modified(status) && checkable(filename) && notIgnored(filename)
? [...acc, path.join(projectRoot, filename)]
: acc,
[],
);
return statuses.reduce(
(acc, [status, filename]) =>
modified(status) && checkable(filename) && notIgnored(filename)
? [...acc, path.join(projectRoot, filename)]
: acc,
[],
);
}
if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] === "--help")) {
// eslint-disable-next-line no-console
console.log(`Run eslint with extra-hard checks
options:
-c, --changed: (default) check only the files that have changed
-n, --nightmare: check all the files in the repository
-h, --help: This help message
`);
process.exit(0);
}
const updated =
process.argv.length > 2 && (process.argv[2] === "-n" || process.argv[2] === "--nightmare")
? ["./src/", "./build.mjs", "./scripts/*.mjs"]
: findChangedFiles();
const eslint = new ESLint(eslintConfig);
const results = await eslint.lintFiles(updated);

View File

@@ -2,7 +2,7 @@
TARGET="./node_modules/@spotlightjs/overlay/dist/index-"[0-9a-f]*.js
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2&>1) ]]; then
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2> /dev/null) ]]; then
patch --forward -V none --no-backup-if-mismatch -p0 $TARGET <<EOF
TARGET=$(find "./node_modules/@spotlightjs/overlay/dist/" -name "index-[0-9a-f]*.js");

View File

@@ -28,9 +28,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2024.6.1-1720888668",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1720888668.tgz",
"integrity": "sha512-3CJl/mS3o5W8F+qfCeFUIjFLRY6xtd1BcjlDcTqMelOIW9VHfh7iVrij1uUW0UE49/siVFw9wwqUXdBxx9pcTA=="
"version": "2024.6.1-1721092506",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1721092506.tgz",
"integrity": "sha512-XrRLZz3bsLBuww8FDHqNH4tHr3sqw/B3Bf5LiAexho00ilKSSYqKnuoDVJm16HNeEezc3gVhfZoyVVu/nbGYxA=="
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",

View File

@@ -151,7 +151,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => {
const properties = Array.isArray(attributes)
? { ".activeWhen": attributes }
: attributes ?? {};
: (attributes ?? {});
if (path) {
properties["path"] = path;
}

View File

@@ -30,6 +30,7 @@ export type LocalTypeCreate = TypeCreate & {
modelName: ProviderModelEnumType;
converter: ModelConverter;
note?: ProviderNote;
testId: string;
renderer: ProviderRenderer;
};
@@ -46,6 +47,7 @@ export const providerModelsList: LocalTypeCreate[] = [
...(provider as OAuth2ProviderRequest),
}),
component: "",
testId: "wizard-provider-oauth2Provider",
iconUrl: "/static/authentik/sources/openidconnect.svg",
},
{
@@ -62,6 +64,7 @@ export const providerModelsList: LocalTypeCreate[] = [
...(provider as LDAPProviderRequest),
}),
component: "",
testId: "wizard-provider-ldapprovider",
iconUrl: "/static/authentik/sources/ldap.png",
},
{
@@ -77,6 +80,7 @@ export const providerModelsList: LocalTypeCreate[] = [
mode: ProxyMode.Proxy,
}),
component: "",
testId: "wizard-provider-proxyprovider-proxy",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
@@ -92,6 +96,7 @@ export const providerModelsList: LocalTypeCreate[] = [
mode: ProxyMode.ForwardSingle,
}),
component: "",
testId: "wizard-provider-proxyprovider-forwardsingle",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
@@ -107,6 +112,7 @@ export const providerModelsList: LocalTypeCreate[] = [
mode: ProxyMode.ForwardDomain,
}),
component: "",
testId: "wizard-provider-proxyprovider-forwarddomain",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
@@ -123,6 +129,7 @@ export const providerModelsList: LocalTypeCreate[] = [
note: () => html`<ak-license-notice></ak-license-notice>`,
requiresEnterprise: true,
component: "",
testId: "wizard-provider-racprovider",
iconUrl: "/static/authentik/sources/rac.svg",
},
{
@@ -137,6 +144,7 @@ export const providerModelsList: LocalTypeCreate[] = [
...(provider as SAMLProviderRequest),
}),
component: "",
testId: "wizard-provider-samlprovider",
iconUrl: "/static/authentik/sources/saml.png",
},
{
@@ -151,6 +159,7 @@ export const providerModelsList: LocalTypeCreate[] = [
...(provider as RadiusProviderRequest),
}),
component: "",
testId: "wizard-provider-radiusprovider",
iconUrl: "/static/authentik/sources/radius.svg",
},
{
@@ -165,6 +174,7 @@ export const providerModelsList: LocalTypeCreate[] = [
...(provider as SCIMProviderRequest),
}),
component: "",
testId: "wizard-provider-scimprovider",
iconUrl: "/static/authentik/sources/scim.png",
},
];

View File

@@ -103,11 +103,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
);
if (!providerModel) {
throw new Error(
`Could not determine provider model from user request: ${JSON.stringify(
this.wizard,
null,
2,
)}`,
`Could not determine provider model from user request: ${JSON.stringify(this.wizard, null, 2)}`,
);
}
@@ -118,7 +114,6 @@ export class ApplicationWizardCommitApplication extends BasePanel {
};
this.send(request);
return;
}
}

View File

@@ -8,7 +8,7 @@ import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { TemplateResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -66,14 +66,11 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
</ak-form-element-horizontal>`;
}
renderExampleButtons(): TemplateResult {
const header = html`<p>${msg("Example context data")}</p>`;
switch (this.mapping?.metaModelName) {
case "authentik_sources_ldap.ldappropertymapping":
return html`${header}${this.renderExampleLDAP()}`;
default:
return html``;
}
renderExampleButtons() {
return this.mapping?.metaModelName === "authentik_sources_ldap.ldappropertymapping"
? html`<p>${msg("Example context data")}</p>
${this.renderExampleLDAP()}`
: nothing;
}
renderExampleLDAP(): TemplateResult {

View File

@@ -1,7 +1,7 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@@ -9,7 +9,6 @@ import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
BackendsEnum,
@@ -72,10 +71,10 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
return html` <span>
${msg("Validate the user's password against the selected backend(s).")}
</span>
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name || "")}"
value="${this.instance?.name || ""}"
class="pf-c-form-control"
required
/>
@@ -158,7 +157,7 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
>
<input
type="number"
value="${first(this.instance?.failedAttemptsBeforeCancel, 5)}"
value="${this.instance?.failedAttemptsBeforeCancel ?? 5}"
class="pf-c-form-control"
required
/>
@@ -168,6 +167,12 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
)}
</p>
</ak-form-element-horizontal>
<ak-switch-input
name="allowShowPassword"
label="Allow Show Password"
?checked=${this.instance?.allowShowPassword ?? false}
help=${msg("Provide users with a 'show password' button.")}
></ak-switch-input>
</div>
</ak-form-group>`;
}

View File

@@ -42,12 +42,11 @@ export function transformCredentialCreateOptions(
user.id = u8arr(b64enc(u8arr(stringId)));
const challenge = u8arr(credentialCreateOptions.challenge.toString());
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
return {
...credentialCreateOptions,
challenge,
user,
});
return transformedCredentialCreateOptions;
};
}
export interface Assertion {
@@ -98,12 +97,11 @@ export function transformCredentialRequestOptions(
},
);
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
return {
...credentialRequestOptions,
challenge,
allowCredentials,
});
return transformedCredentialRequestOptions;
};
}
export interface AuthAssertion {

View File

@@ -1,4 +1,4 @@
import { AKElement } from "@goauthentik/elements/Base";
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { TemplateResult, css, html, nothing } from "lit";
@@ -25,7 +25,7 @@ const selectStyles = css`
* @part select - The select itself, to override the height specified above.
*/
@customElement("ak-multi-select")
export class AkMultiSelect extends AKElement {
export class AkMultiSelect extends AkControlElement {
constructor() {
super();
this.dataset.akControl = "true";

View File

@@ -1,145 +0,0 @@
import { groupBy } from "@goauthentik/common/utils";
import { convertToSlug as slugify } from "@goauthentik/common/utils.js";
import "@goauthentik/elements/forms/SearchSelect/ak-search-select";
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect/ak-search-select";
import { Meta } from "@storybook/web-components";
import { TemplateResult, html } from "lit";
type RawSample = [string, string[]];
type Sample = { name: string; pk: string; season: string[] };
// prettier-ignore
const groupedSamples: RawSample[] = [
["Spring", [
"Apples", "Apricots", "Asparagus", "Avocados", "Bananas", "Broccoli",
"Cabbage", "Carrots", "Celery", "Collard Greens", "Garlic", "Herbs", "Kale", "Kiwifruit", "Lemons",
"Lettuce", "Limes", "Mushrooms", "Onions", "Peas", "Pineapples", "Radishes", "Rhubarb", "Spinach",
"Strawberries", "Swiss Chard", "Turnips"]],
["Summer", [
"Apples", "Apricots", "Avocados", "Bananas", "Beets", "Bell Peppers", "Blackberries", "Blueberries",
"Cantaloupe", "Carrots", "Celery", "Cherries", "Corn", "Cucumbers", "Eggplant", "Garlic",
"Green Beans", "Herbs", "Honeydew Melon", "Lemons", "Lima Beans", "Limes", "Mangos", "Okra", "Peaches",
"Plums", "Raspberries", "Strawberries", "Summer Squash", "Tomatillos", "Tomatoes", "Watermelon",
"Zucchini"]],
["Fall", [
"Apples", "Bananas", "Beets", "Bell Peppers", "Broccoli", "Brussels Sprouts", "Cabbage", "Carrots",
"Cauliflower", "Celery", "Collard Greens", "Cranberries", "Garlic", "Ginger", "Grapes", "Green Beans",
"Herbs", "Kale", "Kiwifruit", "Lemons", "Lettuce", "Limes", "Mangos", "Mushrooms", "Onions",
"Parsnips", "Pears", "Peas", "Pineapples", "Potatoes", "Pumpkin", "Radishes", "Raspberries",
"Rutabagas", "Spinach", "Sweet Potatoes", "Swiss Chard", "Turnips", "Winter Squash"]],
["Winter", [
"Apples", "Avocados", "Bananas", "Beets", "Brussels Sprouts", "Cabbage", "Carrots", "Celery",
"Collard Greens", "Grapefruit", "Herbs", "Kale", "Kiwifruit", "Leeks", "Lemons", "Limes", "Onions",
"Oranges", "Parsnips", "Pears", "Pineapples", "Potatoes", "Pumpkin", "Rutabagas",
"Sweet Potatoes", "Swiss Chard", "Turnips", "Winter Squash"]]
];
// WAAAAY too many lines to turn the arrays above into a Sample of
// { name: "Apricots", pk: "apple", season: ["Spring", "Summer"] }
// but it does the job.
const samples = Array.from(
groupedSamples
.reduce((acc, sample) => {
sample[1].forEach((item) => {
const update = (thing: Sample) => ({
...thing,
season: [...thing.season, sample[0]],
});
acc.set(
item,
update(acc.get(item) || { name: item, pk: slugify(item), season: [] }),
);
return acc;
}, acc);
return acc;
}, new Map<string, Sample>())
.values(),
);
samples.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
// All we need is a promise to return our dataset. It doesn't have to be a class-based method a'la
// the authentik API.
const getSamples = (query = "") =>
Promise.resolve(
samples.filter((s) =>
query !== "" ? s.name.toLowerCase().includes(query.toLowerCase()) : true,
),
);
const metadata: Meta<SearchSelect<Sample>> = {
title: "Elements / Search Select ",
component: "ak-search-select",
parameters: {
docs: {
description: {
component: "An implementation of the Patternfly search select pattern",
},
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
${testItem}
<ul id="message-pad" style="margin-top: 1em"></ul>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const displayChange = (ev: any) => {
document.getElementById("message-pad")!.innerText = `Value selected: ${JSON.stringify(
ev.detail.value,
null,
2,
)}`;
};
export const Default = () => {
return container(
html`<ak-search-select
.fetchObjects=${getSamples}
.renderElement=${(sample: Sample) => sample.name}
.value=${(sample: Sample) => sample.pk}
@ak-change=${displayChange}
></ak-search-select>`,
);
};
export const Grouped = () => {
return container(
html`<ak-search-select
.fetchObjects=${getSamples}
.renderElement=${(sample: Sample) => sample.name}
.value=${(sample: Sample) => sample.pk}
.groupBy=${(samples: Sample[]) =>
groupBy(samples, (sample: Sample) => sample.season[0] ?? "")}
@ak-change=${displayChange}
></ak-search-select>`,
);
};
export const Selected = () => {
return container(
html`<ak-search-select
.fetchObjects=${getSamples}
.renderElement=${(sample: Sample) => sample.name}
.value=${(sample: Sample) => sample.pk}
.selected=${(sample: Sample) => sample.pk === "herbs"}
@ak-change=${displayChange}
></ak-search-select>`,
);
};

View File

@@ -0,0 +1,20 @@
import { AKElement } from "./Base";
/**
* @class - prototype for all of our hand-made input elements
*
* Ensures that the `data-ak-control` property is always set, so that
* scrapers can find it easily, and adds a corresponding method for
* extracting the value.
*
*/
export class AkControlElement extends AKElement {
constructor() {
super();
this.dataset.akControl = "true";
}
json() {
throw new Error("Controllers using this protocol must override this method");
}
}

View File

@@ -92,11 +92,13 @@ export class Tabs extends AKElement {
const pages = Array.from(this.querySelectorAll(":scope > [slot^='page-']"));
if (window.location.hash.includes(ROUTE_SEPARATOR)) {
const params = getURLParams();
if (this.pageIdentifier in params && !this.currentPage) {
if (this.querySelector(`[slot='${params[this.pageIdentifier]}']`) !== null) {
// To update the URL to match with the current slot
this.onClick(params[this.pageIdentifier] as string);
}
if (
this.pageIdentifier in params &&
!this.currentPage &&
this.querySelector(`[slot='${params[this.pageIdentifier]}']`) !== null
) {
// To update the URL to match with the current slot
this.onClick(params[this.pageIdentifier] as string);
}
}
if (!this.currentPage) {

View File

@@ -1,4 +1,4 @@
import { AKElement } from "@goauthentik/elements/Base";
import { AkControlElement } from "@goauthentik/elements/AkControlElement";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize";
@@ -23,7 +23,7 @@ function* kvToPairs(items: CheckboxPair[]): Iterable<CheckboxPr> {
}
}
const AkElementWithCustomEvents = CustomEmitterElement(AKElement);
const AkElementWithCustomEvents = CustomEmitterElement(AkControlElement);
/**
* @element ak-checkbox-group

View File

@@ -1,4 +1,4 @@
import { AKElement } from "@goauthentik/elements/Base";
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
import { debounce } from "@goauthentik/elements/utils/debounce";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
@@ -26,9 +26,8 @@ import type { DataProvider, DualSelectPair } from "./types";
*/
@customElement("ak-dual-select-provider")
export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
/**
* A function that takes a page and returns the DualSelectPair[] collection with which to update
export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) {
/** A function that takes a page and returns the DualSelectPair[] collection with which to update
* the "Available" pane.
*
* @attr
@@ -84,8 +83,6 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
constructor() {
super();
setTimeout(() => this.fetch(1), 0);
// Notify AkForElementHorizontal how to handle this thing.
this.dataset.akControl = "true";
this.onNav = this.onNav.bind(this);
this.onChange = this.onChange.bind(this);
this.onSearch = this.onSearch.bind(this);

View File

@@ -3,7 +3,6 @@ import { MessageLevel } from "@goauthentik/common/messages";
import { camelToSnake, convertToSlug, dateToUTC } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
@@ -74,8 +73,8 @@ export function serializeForm<T extends KeyUnknown>(
return;
}
const inputElement = element.querySelector<HTMLInputElement>("[name]");
if (element.hidden || !inputElement) {
const inputElement = element.querySelector<AkControlElement>("[name]");
if (element.hidden || !inputElement || (element.writeOnly && !element.writeOnlyActivated)) {
return;
}
@@ -84,10 +83,6 @@ export function serializeForm<T extends KeyUnknown>(
return;
}
// Skip elements that are writeOnly where the user hasn't clicked on the value
if (element.writeOnly && !element.writeOnlyActivated) {
return;
}
if (
inputElement.tagName.toLowerCase() === "select" &&
"multiple" in inputElement.attributes
@@ -120,17 +115,6 @@ export function serializeForm<T extends KeyUnknown>(
assignValue(inputElement, inputElement.checked, json);
} else if ("selectedFlow" in inputElement) {
assignValue(inputElement, inputElement.value, json);
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
const select = inputElement as unknown as SearchSelect<unknown>;
try {
const value = select.toForm();
assignValue(inputElement, value, json);
} catch (exc) {
if (exc instanceof PreventFormSubmit) {
throw new PreventFormSubmit(exc.message, element);
}
throw exc;
}
} else {
assignValue(inputElement, inputElement.value, json);
}

View File

@@ -0,0 +1,142 @@
import { bound } from "@goauthentik/elements/decorators/bound.js";
import { match } from "ts-pattern";
import { LitElement, ReactiveController, ReactiveControllerHost } from "lit";
import {
KeyboardControllerCloseEvent,
KeyboardControllerSelectEvent,
} from "./SearchKeyboardControllerEvents.js";
type ReactiveElementHost = Partial<ReactiveControllerHost> & LitElement & { value?: string };
type ValuedHtmlElement = HTMLElement & { value: string };
/**
* @class AkKeyboardController
*
* This reactive controller connects to the host and sets up listeners for keyboard events to manage
* a list of elements. Navigational controls (up, down, home, end) do what you'd expect. Enter and Space
* "select" the current item, which means:
*
* - All other items lose focus and tabIndex
* - The selected item gains focus and tabIndex
* - The value of the selected item is sent to the host as an event
*
* @fires ak-keyboard-controller-select - When an element is selected. Contains the `value` of the
* selected item.
*
* @fires ak-keyboard-controller-close - When `Escape` is pressed. Clients can do with this as they
* wish.
*
*/
export class AkKeyboardController implements ReactiveController {
private host: ReactiveElementHost;
private index: number = 0;
private selector: string;
private highlighter: string;
private items: ValuedHtmlElement[] = [];
/**
* @arg selector: The class identifier (it *must* be a class identifier) of the DOM objects
* that this controller will be working with.
*
* NOTE: The objects identified by the selector *must* have a `value` associated with them, and
* as in all things HTML, that value must be a string.
*
* @arg highlighter: The class identifier that clients *may* use to set an alternative focus
* on the object. Note that the object will always receive focus.
*
*/
constructor(
host: ReactiveElementHost,
selector = ".ak-select-item",
highlighter = ".ak-highlight-item",
) {
this.host = host;
host.addController(this);
this.selector = selector[0] === "." ? selector : `.${selector}`;
this.highlighter = highlighter.replace(/^\./, "");
}
hostUpdated() {
this.items = Array.from(this.host.renderRoot.querySelectorAll(this.selector));
const current = this.items.findIndex((item) => item.value === this.host.value);
if (current >= 0) {
this.index = current;
}
}
hostConnected() {
this.host.addEventListener("keydown", this.onKeydown);
}
hostDisconnected() {
this.host.removeEventListener("keydown", this.onKeydown);
}
hostVisible() {
this.items[this.index].focus();
}
get current() {
return this.items[this.index];
}
get value() {
return this.current?.value;
}
set value(v: string) {
const index = this.items.findIndex((i) => i.value === v);
if (index !== undefined) {
this.index = index;
this.performUpdate();
}
}
private performUpdate() {
const items = this.items;
items.forEach((item) => {
item.classList.remove(this.highlighter);
item.tabIndex = -1;
});
items[this.index].classList.add(this.highlighter);
items[this.index].tabIndex = 0;
items[this.index].focus();
}
@bound
onKeydown(event: KeyboardEvent) {
const key = event.key;
match({ key })
.with({ key: "ArrowDown" }, () => {
this.index = Math.min(this.index + 1, this.items.length - 1);
this.performUpdate();
})
.with({ key: "ArrowUp" }, () => {
this.index = Math.max(this.index - 1, 0);
this.performUpdate();
})
.with({ key: "Home" }, () => {
this.index = 0;
this.performUpdate();
})
.with({ key: "End" }, () => {
this.index = this.items.length - 1;
this.performUpdate();
})
.with({ key: " " }, () => {
this.host.dispatchEvent(new KeyboardControllerSelectEvent(this.value));
})
.with({ key: "Enter" }, () => {
this.host.dispatchEvent(new KeyboardControllerSelectEvent(this.value));
})
.with({ key: "Escape" }, () => {
this.host.dispatchEvent(new KeyboardControllerCloseEvent());
});
}
}

View File

@@ -0,0 +1,20 @@
export class KeyboardControllerSelectEvent extends Event {
value: string | undefined;
constructor(value: string | undefined) {
super("ak-keyboard-controller-select", { composed: true, bubbles: true });
this.value = value;
}
}
export class KeyboardControllerCloseEvent extends Event {
constructor() {
super("ak-keyboard-controller-close", { composed: true, bubbles: true });
}
}
declare global {
interface GlobalEventHandlersEventMap {
"ak-keyboard-controller-select": KeyboardControllerSelectEvent;
"ak-keyboard-controller-close": KeyboardControllerCloseEvent;
}
}

View File

@@ -0,0 +1,63 @@
/**
* class SearchSelectSelectEvent
*
* Intended meaning: the user selected an item from the entire dialogue, either by clicking on it
* with the mouse, or selecting it with the keyboard controls and pressing Enter or Space.
*/
export class SearchSelectSelectEvent extends Event {
value: string | undefined;
constructor(value: string | undefined) {
super("ak-search-select-select", { composed: true, bubbles: true });
this.value = value;
}
}
/**
* class SearchSelectSelectMenuEvent
*
* Intended meaning: the user selected an item from the menu, either by clicking on it with the
* mouse, or selecting it with the keyboard controls and pressing Enter or Space. This is
* intercepted an interpreted internally, usually resulting in a throw of SearchSelectSelectEvent.
* They have to be distinct to avoid an infinite event loop.
*/
export class SearchSelectSelectMenuEvent extends Event {
value: string | undefined;
constructor(value: string | undefined) {
super("ak-search-select-select-menu", { composed: true, bubbles: true });
this.value = value;
}
}
/**
* class SearchSelectCloseEvent
*
* Intended meaning: the user requested that the menu dropdown close. Usually triggered by pressing
* the Escape key.
*/
export class SearchSelectCloseEvent extends Event {
constructor() {
super("ak-search-select-close", { composed: true, bubbles: true });
}
}
/**
* class SearchSelectInputEvent
*
* Intended meaning: the user made a change to the content of the `<input>` field
*/
export class SearchSelectInputEvent extends Event {
value: string | undefined;
constructor(value: string | undefined) {
super("ak-search-select-input", { composed: true, bubbles: true });
this.value = value;
}
}
declare global {
interface GlobalEventHandlersEventMap {
"ak-search-select-select-menu": SearchSelectSelectMenuEvent;
"ak-search-select-select": SearchSelectSelectEvent;
"ak-search-select-input": SearchSelectInputEvent;
"ak-search-select-close": SearchSelectCloseEvent;
}
}

View File

@@ -0,0 +1,185 @@
import { autoUpdate, computePosition, flip, hide } from "@floating-ui/dom";
import { LitElement, html, nothing, render } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Ref, createRef, ref } from "lit/directives/ref.js";
import { KeyboardControllerCloseEvent } from "./SearchKeyboardControllerEvents.js";
import "./ak-search-select-menu.js";
import { type SearchSelectMenu } from "./ak-search-select-menu.js";
import type { SearchOptions } from "./types.js";
/**
* An intermediate class to handle the menu and its position.
*
* It has no rendering of its own, and mostly is just a pass-through for options to the menu.
* DOTADIW: it tracks the top-of-DOM object into which we render our menu, guaranteeing that it
* appears above everything else, and operates the positioning control for it.
*
* - @fires ak-search-select-close - Fired (by the keyboard controller) when the tethered end loses
* focus. Clients can do with this information as they wish.
*/
@customElement("ak-search-select-menu-position")
export class SearchSelectMenuPosition extends LitElement {
/**
* The host to which all relevant events will be routed. Useful for managing floating / tethered
* components.
*
* @prop
*/
@property({ type: Object, attribute: false })
host!: HTMLElement;
/**
* The host element which will be our reference point for rendering.
*
* @prop
*/
@property({ type: Object, attribute: false })
anchor!: HTMLElement;
/**
* Passthrough of the options that we'll be rendering.
*
* @prop
*/
@property({ type: Array, attribute: false })
options: SearchOptions = [];
/**
* Passthrough of the current value
*
* @prop
*/
@property()
value?: string;
/**
* If undefined, there will be no empty option shown
*
* @attr
*/
@property()
emptyOption?: string;
/**
* Whether or not the menu is visible
*
* @attr
*/
@property({ type: Boolean, reflect: true })
open = false;
/**
* The name; used mostly for the management layer.
*
* @attr
*/
@property()
name?: string;
/**
* The tether object.
*/
dropdownContainer!: HTMLDivElement;
public cleanup?: () => void;
connected = false;
/**
*Communicates forward with the menu to detect when the tether has lost focus
*/
menuRef: Ref<SearchSelectMenu> = createRef();
connectedCallback() {
super.connectedCallback();
this.dropdownContainer = document.createElement("div");
this.dropdownContainer.dataset["managedBy"] = "ak-search-select";
if (this.name) {
this.dropdownContainer.dataset["managedFor"] = this.name;
}
document.body.append(this.dropdownContainer);
if (!this.host) {
throw new Error("Tether entrance initialized incorrectly: missing host");
}
this.connected = true;
}
disconnectedCallback(): void {
this.connected = false;
this.dropdownContainer?.remove();
this.cleanup?.();
super.disconnectedCallback();
}
setPosition() {
if (!(this.anchor && this.dropdownContainer)) {
throw new Error("Tether initialized incorrectly: missing anchor or tether destination");
}
this.cleanup = autoUpdate(this.anchor, this.dropdownContainer, async () => {
const { x, y } = await computePosition(this.anchor, this.dropdownContainer, {
placement: "bottom-start",
strategy: "fixed",
middleware: [flip(), hide()],
});
Object.assign(this.dropdownContainer.style, {
"position": "fixed",
"z-index": "9999",
"top": 0,
"left": 0,
"transform": `translate(${x}px, ${y}px)`,
});
});
}
updated() {
if (this.anchor && this.dropdownContainer && !this.hidden) {
this.setPosition();
}
}
hasFocus() {
return (
this.menuRef.value &&
(this.menuRef.value === document.activeElement ||
this.menuRef.value.renderRoot.contains(document.activeElement))
);
}
onFocusOut() {
this.dispatchEvent(new KeyboardControllerCloseEvent());
}
render() {
// The 'hidden' attribute is a little weird and the current Typescript definition for
// it is incompatible with actual implementations, so we drill `open` all the way down,
// but we set the hidden attribute here, and on the actual menu use CSS and the
// the attribute's presence to hide/show as needed.
render(
html`<ak-search-select-menu
.options=${this.options}
value=${ifDefined(this.value)}
.host=${this.host}
.emptyOption=${this.emptyOption}
@focusout=${this.onFocusOut}
?open=${this.open}
?hidden=${!this.open}
${ref(this.menuRef)}
></ak-search-select-menu>`,
this.dropdownContainer,
);
// This is a dummy object that just has to exist to be the communications channel between
// the tethered object and its anchor.
return nothing;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-search-select-menu-position": SearchSelectMenuPosition;
}
}

View File

@@ -0,0 +1,192 @@
import { AKElement } from "@goauthentik/elements/Base.js";
import { bound } from "@goauthentik/elements/decorators/bound.js";
import { PropertyValues, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
import PFSelect from "@patternfly/patternfly/components/Select/select.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { AkKeyboardController } from "./SearchKeyboardController.js";
import {
KeyboardControllerCloseEvent,
KeyboardControllerSelectEvent,
} from "./SearchKeyboardControllerEvents.js";
import { SearchSelectCloseEvent, SearchSelectSelectMenuEvent } from "./SearchSelectEvents.js";
import type { GroupedOptions, SearchGroup, SearchOptions, SearchTuple } from "./types.js";
/**
* @class SearchSelectMenu
* @element ak-search-select-menu
*
* The actual renderer of our components. Intended to be positioned and controlled automatically
* from the outside.
*
* @fires ak-search-select-select - An element has been selected. Contains the `value` of the
* selected item.
*
* @fires ak-search-select-close - The user has triggered the `close` event. Clients can do with this
* as they wish.
*/
@customElement("ak-search-select-menu")
export class SearchSelectMenu extends AKElement {
static get styles() {
return [
PFBase,
PFDropdown,
PFSelect,
css`
:host {
overflow: visible;
z-index: 9999;
}
:host([hidden]) {
display: none;
}
.pf-c-dropdown__menu {
max-height: 50vh;
overflow-y: auto;
}
`,
];
}
/**
* The host to which all relevant events will be routed. Useful for managing floating / tethered
* components.
*/
@property({ type: Object, attribute: false })
host!: HTMLElement;
/**
* See the search options type, described in the `./types` file, for the relevant types.
*/
@property({ type: Array, attribute: false })
options: SearchOptions = [];
@property()
value?: string;
@property()
emptyOption?: string;
@property({ type: Boolean, reflect: true })
open = false;
private keyboardController: AkKeyboardController;
constructor() {
super();
this.keyboardController = new AkKeyboardController(this);
this.addEventListener("ak-keyboard-controller-select", this.onKeySelect);
this.addEventListener("ak-keyboard-controller-close", this.onKeyClose);
}
// Handles the "easy mode" of just passing an array of tuples.
fixedOptions(): GroupedOptions {
return Array.isArray(this.options)
? { grouped: false, options: this.options }
: this.options;
}
@bound
onClick(event: Event, value: string) {
event.stopPropagation();
this.host.dispatchEvent(new SearchSelectSelectMenuEvent(value));
this.value = value;
}
@bound
onEmptyClick(event: Event) {
event.stopPropagation();
this.host.dispatchEvent(new SearchSelectSelectMenuEvent(undefined));
this.value = undefined;
}
@bound
onKeySelect(event: KeyboardControllerSelectEvent) {
event.stopPropagation();
this.value = event.value;
this.host.dispatchEvent(new SearchSelectSelectMenuEvent(this.value));
}
@bound
onKeyClose(event: KeyboardControllerCloseEvent) {
event.stopPropagation();
this.host.dispatchEvent(new SearchSelectCloseEvent());
}
updated(changed: PropertyValues<this>) {
if (changed.has("open") && this.open) {
this.keyboardController.hostVisible();
}
}
renderEmptyMenuItem() {
return html`<li>
<button class="pf-c-dropdown__menu-item" role="option" @click=${this.onEmptyClick}>
${this.emptyOption}
</button>
</li>`;
}
renderMenuItems(options: SearchTuple[]) {
return options.map(
([value, label, desc]: SearchTuple) => html`
<li>
<button
class="pf-c-dropdown__menu-item pf-m-description ak-select-item"
role="option"
value=${value}
@click=${(ev: Event) => {
this.onClick(ev, value);
}}
@keypress=${() => {
/* noop */
}}
>
<div class="pf-c-dropdown__menu-item-main">${label}</div>
${desc
? html`<div class="pf-c-dropdown__menu-item-description">${desc}</div>`
: nothing}
</button>
</li>
`,
);
}
renderMenuGroups(options: SearchGroup[]) {
return options.map(
({ name, options }) => html`
<section class="pf-c-dropdown__group">
<h1 class="pf-c-dropdown__group-title">${name}</h1>
<ul>
${this.renderMenuItems(options)}
</ul>
</section>
`,
);
}
render() {
const options = this.fixedOptions();
return html`<div class="pf-c-dropdown pf-m-expanded">
<ul class="pf-c-dropdown__menu pf-m-static" role="listbox" tabindex="0">
${this.emptyOption !== undefined ? this.renderEmptyMenuItem() : nothing}
${options.grouped
? this.renderMenuGroups(options.options)
: this.renderMenuItems(options.options)}
</ul>
</div> `;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-search-select-menu": SearchSelectMenu;
}
}

View File

@@ -0,0 +1,286 @@
import { AKElement } from "@goauthentik/elements/Base";
import { bound } from "@goauthentik/elements/decorators/bound.js";
import "@goauthentik/elements/forms/SearchSelect/ak-search-select-menu-position.js";
import type { SearchSelectMenuPosition } from "@goauthentik/elements/forms/SearchSelect/ak-search-select-menu-position.js";
import { msg } from "@lit/localize";
import { PropertyValues, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Ref, createRef, ref } from "lit/directives/ref.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFSelect from "@patternfly/patternfly/components/Select/select.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import {
SearchSelectCloseEvent,
SearchSelectInputEvent,
SearchSelectSelectEvent,
SearchSelectSelectMenuEvent,
} from "./SearchSelectEvents.js";
import type { SearchOptions, SearchTuple } from "./types.js";
/**
* @class SearchSelectView
* @element ak-search-select-view
*
* Main component of ak-search-select, renders the <input> object and controls interaction with the
* portaled menu list.
*
* @fires ak-search-select-input - When the user selects an item from the list. A derivative Event
* with the `value` as its payload.
*
* Note that this is more on the HTML / Web Component side of the operational line: the keys which
* represent the values we pass back to clients are always strings here. This component is strictly
* for *rendering* and *interacting* with the items as the user sees them. If the host client is
* not using strings for the values it ultimately keeps inside, it must map them forward to the
* string-based keys we use here (along with the label and description), and map them *back* to
* the object that key references when extracting the value for use.
*
*/
@customElement("ak-search-select-view")
export class SearchSelectView extends AKElement {
/**
* The options collection. The simplest variant is just [key, label, optional<description>]. See
* the `./types.ts` file for variants and how to use them.
*
* @prop
*/
@property({ type: Array, attribute: false })
options: SearchOptions = [];
/**
* The current value. Must be one of the keys in the options group above.
*
* @prop
*/
@property()
value?: string;
/**
* If set to true, this object MAY return undefined in no value is passed in and none is set
* during interaction.
*
* @attr
*/
@property({ type: Boolean })
blankable = false;
/**
* The name of the input, for forms
*
* @attr
*/
@property()
name?: string;
/**
* Whether or not the portal is open
*
* @attr
*/
@property({ type: Boolean, reflect: true })
open = false;
/**
* The textual placeholder for the search's <input> object, if currently empty. Used as the
* native <input> object's `placeholder` field.
*
* @attr
*/
@property()
placeholder: string = msg("Select an object.");
/**
* A textual string representing "The user has affirmed they want to leave the selection blank."
* Only used if `blankable` above is true.
*
* @attr
*/
@property()
emptyOption = "---------";
// Handle the behavior of the drop-down when the :host scrolls off the page.
scrollHandler?: () => void;
observer: IntersectionObserver;
@state()
displayValue = "";
/**
* Permanent identify for the input object, so the floating portal can find where to anchor
* itself.
*/
inputRef: Ref<HTMLInputElement> = createRef();
/**
* Permanent identity with the portal so focus events can be checked.
*/
menuRef: Ref<SearchSelectMenuPosition> = createRef();
/**
* Maps a value from the portal to labels to be put into the <input> field>
*/
optionsMap: Map<string, string> = new Map();
static get styles() {
return [PFBase, PFForm, PFFormControl, PFSelect];
}
constructor() {
super();
this.observer = new IntersectionObserver(() => {
this.open = false;
});
this.observer.observe(this);
/* These can't be attached with the `@` syntax because they're not passed through to the
* menu; the positioner is in the way, and it deliberately renders objects *outside* of the
* path from `document` to this object. That's why we pass the positioner (and its target)
* the `this` (host) object; so they can send messages to this object despite being outside
* the event's bubble path.
*/
this.addEventListener("ak-search-select-select-menu", this.onSelect);
this.addEventListener("ak-search-select-close", this.onClose);
}
disconnectedCallback(): void {
this.observer.disconnect();
super.disconnectedCallback();
}
onOpenEvent(event: Event) {
this.open = true;
if (
this.blankable &&
this.value === this.emptyOption &&
event.target &&
event.target instanceof HTMLInputElement
) {
event.target.value = "";
}
}
@bound
onSelect(event: SearchSelectSelectMenuEvent) {
this.open = false;
this.value = event.value;
this.displayValue = this.value ? (this.optionsMap.get(this.value) ?? this.value ?? "") : "";
this.dispatchEvent(new SearchSelectSelectEvent(this.value));
}
@bound
onClose(event: SearchSelectCloseEvent) {
event.stopPropagation();
this.inputRef.value?.focus();
this.open = false;
}
@bound
onFocus(event: FocusEvent) {
this.onOpenEvent(event);
}
@bound
onClick(event: Event) {
this.onOpenEvent(event);
}
@bound
onInput(_event: InputEvent) {
this.value = this.inputRef?.value?.value ?? "";
this.displayValue = this.value ? (this.optionsMap.get(this.value) ?? this.value ?? "") : "";
this.dispatchEvent(new SearchSelectInputEvent(this.value));
}
@bound
onKeydown(event: KeyboardEvent) {
if (event.key === "Escape") {
event.stopPropagation();
this.open = false;
}
}
@bound
onFocusOut(event: FocusEvent) {
event.stopPropagation();
window.setTimeout(() => {
if (!this.menuRef.value?.hasFocus()) {
this.open = false;
}
}, 0);
}
willUpdate(changed: PropertyValues<this>) {
if (changed.has("options")) {
this.optionsMap = optionsToOptionsMap(this.options);
}
if (changed.has("value")) {
this.displayValue = this.value
? (this.optionsMap.get(this.value) ?? this.value ?? "")
: "";
}
}
updated() {
if (!(this.inputRef?.value && this.inputRef?.value?.value === this.displayValue)) {
this.inputRef.value && (this.inputRef.value.value = this.displayValue);
}
}
render() {
return html`<div class="pf-c-select">
<div class="pf-c-select__toggle pf-m-typeahead">
<div class="pf-c-select__toggle-wrapper">
<input
autocomplete="off"
class="pf-c-form-control pf-c-select__toggle-typeahead"
type="text"
${ref(this.inputRef)}
placeholder=${this.placeholder}
spellcheck="false"
@input=${this.onInput}
@focus=${this.onFocus}
@click=${this.onClick}
@keydown=${this.onKeydown}
@focusout=${this.onFocusOut}
value=${this.displayValue}
/>
</div>
</div>
</div>
<ak-search-select-menu-position
name=${ifDefined(this.name)}
.options=${this.options}
value=${ifDefined(this.value)}
.host=${this}
.anchor=${this.inputRef.value}
.emptyOption=${(this.blankable && this.emptyOption) || undefined}
${ref(this.menuRef)}
?open=${this.open}
></ak-search-select-menu-position> `;
}
}
type Pair = [string, string];
const justThePair = ([key, label]: SearchTuple): Pair => [key, label];
function optionsToOptionsMap(options: SearchOptions): Map<string, string> {
const pairs: Pair[] = Array.isArray(options)
? options.map(justThePair)
: options.grouped
? options.options.reduce(
(acc: Pair[], { options }): Pair[] => [...acc, ...options.map(justThePair)],
[] as Pair[],
)
: options.options.map(justThePair);
return new Map(pairs);
}
declare global {
interface HTMLElementTagNameMap {
"ak-search-select-view": SearchSelectView;
}
}

View File

@@ -1,28 +1,31 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { APIErrorTypes, parseAPIError } from "@goauthentik/common/errors";
import { ascii_letters, digits, groupBy, randomString } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import { groupBy } from "@goauthentik/common/utils";
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg, str } from "@lit/localize";
import { TemplateResult, html, render } from "lit";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { styleMap } from "lit/directives/style-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFSelect from "@patternfly/patternfly/components/Select/select.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ResponseError } from "@goauthentik/api";
import { SearchSelectInputEvent, SearchSelectSelectEvent } from "./SearchSelectEvents.js";
import "./ak-search-select-view.js";
import type { GroupedOptions, SearchGroup, SearchTuple } from "./types.js";
type Group<T> = [string, T[]];
@customElement("ak-search-select")
export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
export class SearchSelect<T> extends CustomEmitterElement(AkControlElement) {
static get styles() {
return [PFBase];
}
// A function which takes the query state object (accepting that it may be empty) and returns a
// new collection of objects.
@property({ attribute: false })
@@ -75,14 +78,10 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
@property({ attribute: false })
selectedObject?: T;
// Not used in this object. No known purpose.
// Used to inform the form of the name of the object
@property()
name?: string;
// Whether or not the dropdown component is visible.
@property({ type: Boolean })
open = false;
// The textual placeholder for the search's <input> object, if currently empty. Used as the
// native <input> object's `placeholder` field.
@property()
@@ -93,46 +92,14 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
@property()
emptyOption = "---------";
// Handle the behavior of the drop-down when the :host scrolls off the page.
scrollHandler?: () => void;
observer: IntersectionObserver;
// Handle communication between the :host and the portal
dropdownUID: string;
dropdownContainer: HTMLDivElement;
isFetchingData = false;
@state()
error?: APIErrorTypes;
static get styles() {
return [PFBase, PFForm, PFFormControl, PFSelect];
}
constructor() {
super();
if (!document.adoptedStyleSheets.includes(PFDropdown)) {
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
ensureCSSStyleSheet(PFDropdown),
];
}
this.dropdownContainer = document.createElement("div");
this.observer = new IntersectionObserver(() => {
this.open = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>(
".pf-c-form-control.pf-c-select__toggle-typeahead",
)
.forEach((input) => {
input.blur();
});
});
this.observer.observe(this);
this.dropdownUID = `dropdown-${randomString(10, ascii_letters + digits)}`;
this.onMenuItemClick = this.onMenuItemClick.bind(this);
this.renderWithMenuGroupTitle = this.renderWithMenuGroupTitle.bind(this);
this.dataset.akControl = "true";
}
toForm(): unknown {
@@ -142,16 +109,16 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
return this.value(this.selectedObject) || "";
}
firstUpdated(): void {
this.updateData();
json() {
return this.toForm();
}
updateData(): void {
updateData() {
if (this.isFetchingData) {
return;
}
this.isFetchingData = true;
this.fetchObjects(this.query)
return this.fetchObjects(this.query)
.then((objects) => {
objects.forEach((obj) => {
if (this.selected && this.selected(obj, objects || [])) {
@@ -173,230 +140,97 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
connectedCallback(): void {
super.connectedCallback();
this.dropdownContainer = document.createElement("div");
this.dropdownContainer.dataset["managedBy"] = "ak-search-select";
if (this.name) {
this.dropdownContainer.dataset["managedFor"] = this.name;
}
document.body.append(this.dropdownContainer);
this.updateData();
this.addEventListener(EVENT_REFRESH, this.updateData);
this.scrollHandler = () => {
this.requestUpdate();
};
window.addEventListener("scroll", this.scrollHandler);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener(EVENT_REFRESH, this.updateData);
if (this.scrollHandler) {
window.removeEventListener("scroll", this.scrollHandler);
}
this.dropdownContainer.remove();
this.observer.disconnect();
}
renderMenuItemWithDescription(obj: T, desc: TemplateResult, index: number) {
return html`
<li>
<button
class="pf-c-dropdown__menu-item pf-m-description"
role="option"
@click=${this.onMenuItemClick(obj)}
tabindex=${index}
>
<div class="pf-c-dropdown__menu-item-main">${this.renderElement(obj)}</div>
<div class="pf-c-dropdown__menu-item-description">${desc}</div>
</button>
</li>
`;
}
renderMenuItemWithoutDescription(obj: T, index: number) {
return html`
<li>
<button
class="pf-c-dropdown__menu-item"
role="option"
@click=${this.onMenuItemClick(obj)}
tabindex=${index}
>
${this.renderElement(obj)}
</button>
</li>
`;
}
renderEmptyMenuItem() {
return html`<li>
<button
class="pf-c-dropdown__menu-item"
role="option"
@click=${this.onMenuItemClick(undefined)}
tabindex="0"
>
${this.emptyOption}
</button>
</li>`;
}
onMenuItemClick(obj: T | undefined) {
return () => {
this.selectedObject = obj;
this.dispatchCustomEvent("ak-change", { value: this.selectedObject });
this.open = false;
};
}
renderMenuGroup(items: T[], tabIndexStart: number) {
const renderedItems = items.map((obj, index) => {
const desc = this.renderDescription ? this.renderDescription(obj) : null;
const tabIndex = index + tabIndexStart;
return desc
? this.renderMenuItemWithDescription(obj, desc, tabIndex)
: this.renderMenuItemWithoutDescription(obj, tabIndex);
});
return html`${renderedItems}`;
}
renderWithMenuGroupTitle([group, items]: Group<T>, idx: number) {
return html`
<section class="pf-c-dropdown__group">
<h1 class="pf-c-dropdown__group-title">${group}</h1>
<ul>
${this.renderMenuGroup(items, idx)}
</ul>
</section>
`;
}
get groupedItems(): [boolean, Group<T>[]] {
const items = this.groupBy(this.objects || []);
if (items.length === 0) {
return [false, [["", []]]];
}
if (items.length === 1 && (items[0].length < 1 || items[0][0] === "")) {
return [false, items];
}
return [true, items];
}
/*
* This is a little bit hacky. Because we mainly want to use this field in modal-based forms,
* rendering this menu inline makes the menu not overlay over top of the modal, and cause
* the modal to scroll.
* Hence, we render the menu into the document root, hide it when this menu isn't open
* and remove it on disconnect
* Also to move it to the correct position we're getting this elements's position and use that
* to position the menu
* The other downside this has is that, since we're rendering outside of a shadow root,
* the pf-c-dropdown CSS needs to be loaded on the body.
*/
renderMenu(): void {
if (!this.objects) {
onSearch(event: SearchSelectInputEvent) {
if (event.value === undefined) {
this.selectedObject = undefined;
return;
}
const [shouldRenderGroups, groupedItems] = this.groupedItems;
const pos = this.getBoundingClientRect();
const position = {
"position": "fixed",
"inset": "0px auto auto 0px",
"z-index": "9999",
"transform": `translate(${pos.x}px, ${pos.y + this.offsetHeight}px)`,
"width": `${pos.width}px`,
...(this.open ? {} : { visibility: "hidden" }),
};
render(
html`<div style=${styleMap(position)} class="pf-c-dropdown pf-m-expanded">
<ul
class="pf-c-dropdown__menu pf-m-static"
role="listbox"
style="max-height:50vh;overflow-y:auto;"
id=${this.dropdownUID}
tabindex="0"
>
${this.blankable ? this.renderEmptyMenuItem() : html``}
${shouldRenderGroups
? html`${groupedItems.map(this.renderWithMenuGroupTitle)}`
: html`${this.renderMenuGroup(groupedItems[0][1], 0)}`}
</ul>
</div>`,
this.dropdownContainer,
{ host: this },
);
this.query = event.value;
this.updateData()?.then(() => {
this.dispatchCustomEvent("ak-change", { value: this.selectedObject });
});
}
get renderedValue() {
onSelect(event: SearchSelectSelectEvent) {
if (event.value === undefined) {
this.selectedObject = undefined;
this.dispatchCustomEvent("ak-change", { value: undefined });
return;
}
const selected = (this.objects ?? []).find((obj) => `${this.value(obj)}` === event.value);
if (!selected) {
console.warn(
`ak-search-select: No corresponding object found for value (${event.value}`,
);
}
this.selectedObject = selected;
this.dispatchCustomEvent("ak-change", { value: this.selectedObject });
}
getGroupedItems(): GroupedOptions {
const items = this.groupBy(this.objects || []);
const makeSearchTuples = (items: T[]): SearchTuple[] =>
items.map((item) => [
`${this.value(item)}`,
this.renderElement(item),
this.renderDescription ? this.renderDescription(item) : undefined,
]);
const makeSearchGroups = (items: Group<T>[]): SearchGroup[] =>
items.map((group) => ({
name: group[0],
options: makeSearchTuples(group[1]),
}));
if (items.length === 0) {
return { grouped: false, options: [] };
}
if (items.length === 1 && (items[0].length < 1 || items[0][0] === "")) {
return {
grouped: false,
options: makeSearchTuples(items[0][1]),
};
}
return {
grouped: true,
options: makeSearchGroups(items),
};
}
render() {
if (this.error) {
return msg(str`Failed to fetch objects: ${this.error.detail}`);
return html`<em>${msg("Failed to fetch objects: ")} ${this.error.detail}</em>`;
}
if (!this.objects) {
return msg("Loading...");
return html`${msg("Loading...")}`;
}
if (this.selectedObject) {
return this.renderElement(this.selectedObject);
}
if (this.blankable) {
return this.emptyOption;
}
return "";
}
render(): TemplateResult {
this.renderMenu();
const options = this.getGroupedItems();
const value = this.selectedObject ? `${this.value(this.selectedObject) ?? ""}` : undefined;
const onFocus = (ev: FocusEvent) => {
this.open = true;
this.renderMenu();
if (this.blankable && this.renderedValue === this.emptyOption) {
if (ev.target && ev.target instanceof HTMLInputElement) {
ev.target.value = "";
}
}
};
const onInput = (ev: InputEvent) => {
this.query = (ev.target as HTMLInputElement).value;
this.updateData();
};
const onBlur = (ev: FocusEvent) => {
// For Safari, we get the <ul> element itself here when clicking on one of
// it's buttons, as the container has tabindex set
if (ev.relatedTarget && (ev.relatedTarget as HTMLElement).id === this.dropdownUID) {
return;
}
// Check if we're losing focus to one of our dropdown items, and if such don't blur
if (ev.relatedTarget instanceof HTMLButtonElement) {
const parentMenu = ev.relatedTarget.closest("ul.pf-c-dropdown__menu.pf-m-static");
if (parentMenu && parentMenu.id === this.dropdownUID) {
return;
}
}
this.open = false;
this.renderMenu();
};
return html`<div class="pf-c-select">
<div class="pf-c-select__toggle pf-m-typeahead">
<div class="pf-c-select__toggle-wrapper">
<input
class="pf-c-form-control pf-c-select__toggle-typeahead"
type="text"
placeholder=${this.placeholder}
spellcheck="false"
@input=${onInput}
@focus=${onFocus}
@blur=${onBlur}
.value=${this.renderedValue}
/>
</div>
</div>
</div>`;
return html`<ak-search-select-view
.options=${options}
.value=${value}
?blankable=${this.blankable}
name=${ifDefined(this.name)}
placeholder=${this.placeholder}
emptyOption=${ifDefined(this.blankable ? this.emptyOption : undefined)}
@ak-search-select-input=${this.onSearch}
@ak-search-select-select=${this.onSelect}
></ak-search-select-view> `;
}
}

View File

@@ -0,0 +1,120 @@
import "@goauthentik/elements/messages/MessageContainer";
import { Meta, StoryObj } from "@storybook/web-components";
import { slug } from "github-slugger";
import { TemplateResult, html } from "lit";
import { SearchSelectSelectMenuEvent } from "../SearchSelectEvents.js";
import "../ak-search-select-menu.js";
import { SearchSelectMenu } from "../ak-search-select-menu.js";
import { groupedSampleData, sampleData } from "./sampleData.js";
const metadata: Meta<SearchSelectMenu> = {
title: "Elements / Search Select / Tethered Menu",
component: "ak-search-select-menu",
parameters: {
docs: {
description: {
component: "The tethered panel containing the scrollable list of selectable items",
},
},
},
argTypes: {
options: {
type: "string",
description: "An array of [key, label, desc] pairs of what to show",
},
},
};
export default metadata;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onClick = (event: SearchSelectSelectMenuEvent) => {
const target = document.querySelector("#action-button-message-pad");
target!.innerHTML = "";
target!.append(
new DOMParser().parseFromString(`<li>${event.value}</li>`, "text/xml").firstChild!,
);
};
const container = (testItem: TemplateResult) => {
window.setTimeout(() => {
const menu = document.getElementById("ak-search-select-menu");
const container = document.getElementById("the-main-event");
if (menu && container) {
container.addEventListener("ak-search-select-select-menu", onClick);
(menu as SearchSelectMenu).host = container;
}
}, 250);
return html` <div
style="background: #fff; padding: 2em; position: relative"
id="the-main-event"
>
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
#the-answer-block {
padding-top: 3em;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<div id="the-answer-block">
<p>Messages received from the menu:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>
</div>`;
};
type Story = StoryObj;
const goodForYouPairs = {
grouped: false,
options: sampleData.slice(0, 20).map(({ produce }) => [slug(produce), produce]),
};
export const Default: Story = {
render: () =>
container(
html` <ak-search-select-menu
id="ak-search-select-menu"
style="top: 1em; left: 1em"
.options=${goodForYouPairs}
></ak-search-select-menu>`,
),
};
const longGoodForYouPairs = {
grouped: false,
options: sampleData.map(({ produce }) => [slug(produce), produce]),
};
export const Scrolling: Story = {
render: () =>
container(
html` <ak-search-select-menu
id="ak-search-select-menu"
style="top: 1em; left: 1em"
.options=${longGoodForYouPairs}
.host=${document}
></ak-search-select-menu>`,
),
};
export const Grouped: Story = {
render: () =>
container(
html` <ak-search-select-menu
id="ak-search-select-menu"
style="top: 1em; left: 1em"
.options=${groupedSampleData}
.host=${document}
></ak-search-select-menu>`,
),
};

View File

@@ -0,0 +1,72 @@
import "@goauthentik/elements/forms/SearchSelect/ak-search-select-view.js";
import { SearchSelectView } from "@goauthentik/elements/forms/SearchSelect/ak-search-select-view.js";
import { Meta } from "@storybook/web-components";
import { slug } from "github-slugger";
import { TemplateResult, html } from "lit";
import { groupedSampleData, sampleData } from "./sampleData.js";
const metadata: Meta<SearchSelectView> = {
title: "Elements / Search Select / View Handler ",
component: "ak-search-select-view",
parameters: {
docs: {
description: {
component: "An implementation of the Patternfly search select pattern",
},
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
${testItem}
<ul id="message-pad" style="margin-top: 1em"></ul>
</div>`;
const longGoodForYouPairs = {
grouped: false,
options: sampleData.map(({ produce }) => [slug(produce), produce]),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const displayChange = (ev: any) => {
document.getElementById("message-pad")!.innerText = `Value selected: ${JSON.stringify(
ev.value,
null,
2,
)}`;
};
export const Default = () => {
return container(
html`<ak-search-select-view
.options=${longGoodForYouPairs}
blankable
@ak-search-select-select=${displayChange}
></ak-search-select-view>`,
);
};
export const DescribedGroups = () => {
return container(
html`<ak-search-select-view
.options=${groupedSampleData}
blankable
@ak-search-select-select=${displayChange}
></ak-search-select-view>`,
);
};

View File

@@ -0,0 +1,103 @@
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/SearchSelect/ak-search-select";
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect/ak-search-select";
import { Meta } from "@storybook/web-components";
import { TemplateResult, html } from "lit";
import { sampleData } from "./sampleData.js";
type Sample = { name: string; pk: string; season: string[] };
const samples = sampleData.map(({ produce, seasons }) => ({
name: produce,
pk: produce.replace(/\s+/, "").toLowerCase(),
season: seasons,
}));
samples.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
// All we need is a promise to return our dataset. It doesn't have to be a class-based method a'la
// the authentik API.
const getSamples = (query = "") => {
if (query === "") {
return Promise.resolve(samples);
}
const check = new RegExp(query);
return Promise.resolve(samples.filter((s) => check.test(s.name)));
};
const metadata: Meta<SearchSelect<Sample>> = {
title: "Elements / Search Select / API Interface",
component: "ak-search-select",
parameters: {
docs: {
description: {
component: "An implementation of the Patternfly search select pattern",
},
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
${testItem}
<ul id="message-pad" style="margin-top: 1em"></ul>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const displayChange = (ev: any) => {
document.getElementById("message-pad")!.innerText = `Value selected: ${JSON.stringify(
ev.detail.value,
null,
2,
)}`;
};
export const Default = () =>
container(
html`<ak-search-select
.fetchObjects=${getSamples}
.renderElement=${(sample: Sample) => sample.name}
.value=${(sample: Sample) => sample.pk}
@ak-change=${displayChange}
></ak-search-select>`,
);
export const Grouped = () => {
return container(
html`<ak-search-select
.fetchObjects=${getSamples}
.renderElement=${(sample: Sample) => sample.name}
.value=${(sample: Sample) => sample.pk}
.groupBy=${(samples: Sample[]) =>
groupBy(samples, (sample: Sample) => sample.season[0] ?? "")}
@ak-change=${displayChange}
></ak-search-select>`,
);
};
export const SelectedAndBlankable = () => {
return container(
html`<ak-search-select
blankable
.fetchObjects=${getSamples}
.renderElement=${(sample: Sample) => sample.name}
.value=${(sample: Sample) => sample.pk}
.selected=${(sample: Sample) => sample.pk === "herbs"}
@ak-change=${displayChange}
></ak-search-select>`,
);
};

View File

@@ -0,0 +1,359 @@
import { slug } from "github-slugger";
import type { TemplateResult } from "lit";
// The descriptions were generated by ChatGPT. Don't blame us.
export type ViewSample = {
produce: string;
seasons: string[];
desc?: string;
};
export const sampleData: ViewSample[] = [
{
produce: "Apples",
seasons: ["Spring", "Summer", "Fall", "Winter"],
desc: "Apples are a sweet and crunchy fruit that can be eaten fresh or used in pies, juice, and ciders.",
},
{
produce: "Apricots",
seasons: ["Spring", "Summer"],
desc: "Apricots are a sweet and tangy stone fruit with a velvety skin that's often orange-yellow in color",
},
{
produce: "Asparagus",
seasons: ["Spring"],
desc: "Asparagus is a delicate and nutritious vegetable with a tender spear-like shape",
},
{
produce: "Avocados",
seasons: ["Spring", "Summer", "Winter"],
desc: "Avocados are a nutritious fruit with a creamy texture and nutty flavor",
},
{
produce: "Bananas",
seasons: ["Spring", "Summer", "Fall", "Winter"],
desc: "Bananas are a type of curved, yellow fruit that grows on banana plants",
},
{
produce: "Beets",
seasons: ["Summer", "Fall", "Winter"],
desc: "Beets are a sweet and earthy root vegetable that can be pickled, roasted, or boiled",
},
{
produce: "Bell Peppers",
seasons: ["Summer", "Fall"],
desc: "Bell peppers are a sweet and crunchy type of pepper that can be green, red, yellow, or orange",
},
{
produce: "Blackberries",
seasons: ["Summer"],
desc: "Blackberries are a type of fruit that are dark purple in color and have a sweet-tart taste",
},
{
produce: "Blueberries",
seasons: ["Summer"],
desc: "Blueberries are small, round, and sweet-tart berries with a powdery coating and a burst of juicy flavor.",
},
{
produce: "Broccoli",
seasons: ["Spring", "Fall"],
desc: "Broccoli is a green, cruciferous vegetable with a tree-like shape and a slightly bitter taste.",
},
{
produce: "Brussels Sprouts",
seasons: ["Fall", "Winter"],
desc: "Brussels sprouts are a cruciferous vegetable that is small, green, and formed like a tiny cabbage head, with a sweet and slightly bitter flavor.",
},
{
produce: "Cabbage",
seasons: ["Spring", "Fall", "Winter"],
desc: "Cabbage is a crunchy, sweet, and slightly bitter vegetable with a dense head of tightly packed leaves.",
},
{
produce: "Cantaloupe",
seasons: ["Summer"],
desc: "Cantaloupe is a sweet and juicy melon with a netted or reticulated rind and yellow-orange flesh.",
},
{
produce: "Carrots",
seasons: ["Spring", "Summer", "Fall", "Winter"],
desc: "Carrots are a crunchy and sweet root vegetable commonly eaten raw or cooked in various dishes.",
},
{
produce: "Cauliflower",
seasons: ["Fall"],
desc: "Cauliflower is a cruciferous vegetable with a white or pale yellow florets resembling tiny trees",
},
{
produce: "Celery",
seasons: ["Spring", "Summer", "Fall", "Winter"],
desc: "Celery is a crunchy, sweet-tasting vegetable with a mild flavor, often used in salads and as a snack.",
},
{
produce: "Cherries",
seasons: ["Summer"],
desc: "Cherries are a sweet and juicy stone fruit that typically range in color from bright red to dark purple.",
},
{
produce: "Collard Greens",
seasons: ["Spring", "Fall", "Winter"],
desc: "Collard greens are a type of leafy green vegetable with a slightly bitter and earthy flavor.",
},
{
produce: "Corn",
seasons: ["Summer"],
desc: "Corn is a sweet and savory grain that can be eaten fresh or used in various dishes, such as soups, salads, and baked goods.",
},
{
produce: "Cranberries",
seasons: ["Fall"],
desc: "Cranberries are a type of small, tart-tasting fruit native to North America",
},
{
produce: "Cucumbers",
seasons: ["Summer"],
desc: "Cucumbers are a long, green vegetable that is commonly consumed raw or pickled",
},
{
produce: "Eggplant",
seasons: ["Summer"],
desc: "Eggplant is a purple vegetable with a spongy texture and a slightly bitter taste.",
},
{
produce: "Garlic",
seasons: ["Spring", "Summer", "Fall"],
desc: "Garlic is a pungent and flavorful herb with a distinctive aroma and taste",
},
{
produce: "Ginger",
seasons: ["Fall"],
desc: "Ginger is a spicy, sweet, and tangy root commonly used in Asian cuisine to add warmth and depth",
},
{
produce: "Grapefruit",
seasons: ["Winter"],
desc: "Grapefruit is a tangy and sweet citrus fruit with a tart flavor profile and a slightly bitter aftertaste.",
},
{
produce: "Grapes",
seasons: ["Fall"],
desc: "Grapes are a type of fruit that grow in clusters on vines and are often eaten fresh or used to make wine, jam, and juice.",
},
{
produce: "Green Beans",
seasons: ["Summer", "Fall"],
desc: "Green beans are a type of long, thin, green vegetable that is commonly eaten as a side dish or used in various recipes.",
},
{
produce: "Herbs",
seasons: ["Spring", "Summer", "Fall", "Winter"],
desc: "Herbs are plant parts, such as leaves, stems, or flowers, used to add flavor or aroma",
},
{
produce: "Honeydew Melon",
seasons: ["Summer"],
desc: "Honeydew melons are sweet and refreshing, with a smooth, pale green rind and juicy, creamy white flesh.",
},
{
produce: "Kale",
seasons: ["Spring", "Fall", "Winter"],
desc: "Kale is a type of leafy green vegetable that is packed with nutrients and has a slightly bitter, earthy flavor.",
},
{
produce: "Kiwifruit",
seasons: ["Spring", "Fall", "Winter"],
desc: "Kiwifruit is a small, oval-shaped fruit with a fuzzy exterior and bright green or yellow flesh that tastes sweet and slightly tart.",
},
{
produce: "Leeks",
seasons: ["Winter"],
desc: "Leeks are a type of vegetable that is similar to onions and garlic, but has a milder flavor and a more delicate texture.",
},
{
produce: "Lemons",
seasons: ["Spring", "Summer", "Fall", "Winter"],
desc: "Lemons are a sour and tangy citrus fruit with a bright yellow color and a strong, distinctive flavor used in cooking, cleaning, and as a natural remedy.",
},
{
produce: "Lettuce",
seasons: ["Spring", "Fall"],
desc: "Lettuce is a crisp and refreshing green leafy vegetable often used in salads.",
},
{
produce: "Lima Beans",
seasons: ["Summer"],
desc: "Lima beans are a type of green legume with a mild flavor and soft, creamy texture.",
},
{
produce: "Limes",
seasons: ["Spring", "Summer", "Fall", "Winter"],
desc: "Limes are small, citrus fruits with a sour taste and a bright green color.",
},
{
produce: "Mangos",
seasons: ["Summer", "Fall"],
desc: "Mangos are sweet and creamy tropical fruits with a velvety texture",
},
{
produce: "Mushrooms",
seasons: ["Spring", "Fall"],
desc: "Mushrooms are a type of fungus that grow underground or on decaying organic matter",
},
{
produce: "Okra",
seasons: ["Summer"],
desc: "Okra is a nutritious, green vegetable with a unique texture and flavor",
},
{
produce: "Onions",
seasons: ["Spring", "Fall", "Winter"],
desc: "Onions are a type of vegetable characterized by their layered, bulbous structure and pungent flavor.",
},
{
produce: "Oranges",
seasons: ["Winter"],
desc: "Oranges are a sweet and juicy citrus fruit with a thick, easy-to-peel skin.",
},
{
produce: "Parsnips",
seasons: ["Fall", "Winter"],
desc: "Parsnips are a type of root vegetable that is sweet and nutty in flavor, with a texture similar to carrots.",
},
{
produce: "Peaches",
seasons: ["Summer"],
desc: "Peaches are sweet and juicy stone fruits with a soft, velvety texture.",
},
{
produce: "Pears",
seasons: ["Fall", "Winter"],
desc: "Pears are a type of sweet and juicy fruit with a smooth, buttery texture and a mild flavor",
},
{
produce: "Peas",
seasons: ["Spring", "Fall"],
desc: "Peas are small, round, sweet-tasting legumes that grow on vines and are often eaten as a side dish or added to various recipes.",
},
{
produce: "Pineapples",
seasons: ["Spring", "Fall", "Winter"],
desc: "Pineapples are a tropical fruit with tough, prickly skin and juicy, sweet flesh.",
},
{
produce: "Plums",
seasons: ["Summer"],
desc: "Plums are a type of stone fruit characterized by their juicy sweetness and rough, dark skin.",
},
{
produce: "Potatoes",
seasons: ["Fall", "Winter"],
desc: "Potatoes are a starchy root vegetable that is often brown on the outside and white or yellow on the inside.",
},
{
produce: "Pumpkin",
seasons: ["Fall", "Winter"],
desc: "Pumpkin is a type of squash that is typically orange in color and is often used to make pies, soups, and other sweet or savory dishes.",
},
{
produce: "Radishes",
seasons: ["Spring", "Fall"],
desc: "Radishes are a pungent, crunchy and spicy root vegetable that can be eaten raw or cooked,",
},
{
produce: "Raspberries",
seasons: ["Summer", "Fall"],
desc: "Raspberries are a type of sweet-tart fruit that grows on thorny bushes and is often eaten fresh or used in jams, preserves, and desserts.",
},
{
produce: "Rhubarb",
seasons: ["Spring"],
desc: "Rhubarb is a perennial vegetable with long, tart stalks that are often used in pies and preserves",
},
{
produce: "Rutabagas",
seasons: ["Fall", "Winter"],
desc: "Rutabagas are a type of root vegetable that is similar to a cross between a cabbage and a turnip",
},
{
produce: "Spinach",
seasons: ["Spring", "Fall"],
desc: "Spinach is a nutritious leafy green vegetable that is rich in iron and vitamins A, C, and K.",
},
{
produce: "Strawberries",
seasons: ["Spring", "Summer"],
desc: "Sweet and juicy, strawberries are a popular type of fruit that grow on low-lying plants with sweet-tasting seeds.",
},
{
produce: "Summer Squash",
seasons: ["Summer"],
desc: "Summer squash is a type of warm-season vegetable that includes varieties like zucchini, yellow crookneck, and straightneck",
},
{
produce: "Sweet Potatoes",
seasons: ["Fall", "Winter"],
desc: "Sweet potatoes are a type of root vegetable with a sweet and nutty flavor, often orange in color",
},
{
produce: "Swiss Chard",
seasons: ["Spring", "Fall", "Winter"],
desc: "Swiss Chard is a leafy green vegetable with a slightly bitter taste and a vibrant red or gold stem",
},
{
produce: "Tomatillos",
seasons: ["Summer"],
desc: "Tomatillos are a type of fruit that is similar to tomatoes, but with a papery husk and a more tart, slightly sweet flavor.",
},
{
produce: "Tomatoes",
seasons: ["Summer"],
desc: "Tomatoes are a juicy, sweet, and tangy fruit that is commonly used in salads, sandwiches, and as a topping for various dishes.",
},
{
produce: "Turnips",
seasons: ["Spring", "Fall", "Winter"],
desc: "Turnips are a root vegetable with a sweet and peppery flavor, often used in soups, stews, and salads.",
},
{
produce: "Watermelon",
seasons: ["Summer"],
desc: "Watermelon is a juicy and refreshing sweet fruit with a green rind and pink or yellow flesh.",
},
{
produce: "Winter Squash",
seasons: ["Fall", "Winter"],
desc: "Winter squash is a type of starchy vegetable that is harvested in the fall and has a hard, dry rind that can be stored for several months.",
},
{
produce: "Zucchini",
seasons: ["Summer"],
desc: "Zucchini is a popular summer squash that is often green or yellow in color and has a mild, slightly sweet flavor.",
},
];
type Seasoned = [string, string, string | TemplateResult];
const reseason = (acc: Seasoned[], { produce, seasons, desc }: ViewSample): Seasoned[] => [
...acc,
...seasons.map((s) => [s, produce, desc] as Seasoned),
];
export const groupedSampleData = (() => {
const seasoned: Seasoned[] = sampleData.reduce(reseason, [] as Seasoned[]);
const grouped = Object.groupBy(seasoned, ([season]) => season);
const ungrouped = ([_season, label, desc]: Seasoned) => [slug(label), label, desc];
if (grouped === undefined) {
throw new Error("Not possible with existing data.");
}
return {
grouped: true,
options: ["Spring", "Summer", "Fall", "Winter"].map((season) => ({
name: season,
options: grouped[season]?.map(ungrouped) ?? [],
})),
};
})();

View File

@@ -0,0 +1,66 @@
import type { TemplateResult } from "lit";
/**
* A search tuple consists of a [key, label, description]
* The description is optional. The key must always be a string.
*
*/
export type SearchTuple = [
key: string,
label: string,
description: undefined | string | TemplateResult,
];
/**
* A search list without groups will always just consist of an array of SearchTuples and the
* `grouped: false` flag. Note that it *is* possible to pass to any of the rendering components an
* array of SearchTuples; they will be automatically mapped to a SearchFlat object.
*
*/
export type SearchFlat = {
grouped: false;
options: SearchTuple[];
};
/**
* A search group consists of a group name and a collection of SearchTuples.
*
*/
export type SearchGroup = { name: string; options: SearchTuple[] };
/**
* A grouped search is an array of SearchGroups, of course!
*
*/
export type SearchGrouped = {
grouped: true;
options: SearchGroup[];
};
/**
* Internally, we only work with these two, but we have the `SearchOptions` variant
* below to support the case where you just want to pass in an array of SearchTuples.
*
*/
export type GroupedOptions = SearchGrouped | SearchFlat;
export type SearchOptions = SearchTuple[] | GroupedOptions;
// These can safely be ignored for now.
export type Group<T> = [string, T[]];
export type ElementRendererBase<T> = (element: T) => string;
export type ElementRenderer<T, S = keyof T> = ElementRendererBase<T> | S;
export type DescriptionRendererBase<T> = (element: T) => TemplateResult | string;
export type DescriptionRenderer<T, S = keyof T> = ElementRendererBase<T> | S;
export type ValueExtractorBase<T> = (element: T | undefined) => keyof T | undefined;
export type ValueExtractor<T, S = keyof T> = ValueExtractorBase<T> | S;
export type ValueSelectorBase<T> = (element: T, elements: T[]) => boolean;
export type ValueSelector<T, S extends keyof T> = S extends S
? ValueSelectorBase<T> | [T, T[S]]
: never;
export type GroupByBase<T> = (elements: T[]) => Group<T>[];
export type GroupBy<T, S = keyof T> = GroupByBase<T> | keyof S;

View File

@@ -117,19 +117,11 @@ export class SidebarItem extends AKElement {
if (!this.path) {
return false;
}
if (this.path) {
const ourPath = this.path.split(";")[0];
if (new RegExp(`^${ourPath}$`).exec(path)) {
return true;
}
}
return this.activeMatchers.some((v) => {
const match = v.exec(path);
if (match !== null) {
return true;
}
return false;
});
const ourPath = this.path.split(";")[0];
const pathIsWholePath = new RegExp(`^${ourPath}$`).test(path);
const pathIsAnActivePath = this.activeMatchers.some((v) => v.test(path));
return pathIsWholePath || pathIsAnActivePath;
}
expandParentRecursive(activePath: string, item: SidebarItem): void {

View File

@@ -5,6 +5,7 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@@ -19,10 +20,12 @@ export enum TypeCreateWizardPageLayouts {
grid = "grid",
}
type TypeCreateWithTestId = TypeCreate & { testId?: string };
@customElement("ak-wizard-page-type-create")
export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false })
types: TypeCreate[] = [];
types: TypeCreateWithTestId[] = [];
@property({ attribute: false })
selectedType?: TypeCreate;
@@ -51,7 +54,7 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
activeCallback = async () => {
this.host.isValid = false;
if (this.selectedType) {
this.selectDispatch(this.selectedType);
@@ -78,6 +81,7 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
: "pf-m-selectable-raised"} ${this.selectedType == type
? "pf-m-selected-raised"
: ""}"
data-testid=${ifDefined(type.testId)}
tabindex=${idx}
@click=${() => {
if (requiresEnterprise) {

View File

@@ -0,0 +1,181 @@
import { AKElement } from "@goauthentik/elements/Base.js";
import "@goauthentik/elements/forms/FormElement";
import { msg } from "@lit/localize";
import { html, nothing, render } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-flow-input-password")
export class InputPassword extends AKElement {
static get styles() {
return [PFBase, PFInputGroup, PFFormControl, PFButton];
}
@property({ type: String, attribute: "input-id" })
inputId = "ak-stage-password-input";
@property({ type: String })
name = "password";
@property({ type: String })
label = msg("Password");
@property({ type: String })
placeholder = msg("Please enter your password");
@property({ type: String, attribute: "prefill" })
passwordPrefill = "";
@property({ type: Object })
errors: Record<string, string> = {};
/**
* Forwarded to the input tag's aria-invalid attribute, if set
* @attr
*/
@property({ type: String })
invalid?: string;
@property({ type: Boolean, attribute: "allow-show-password" })
allowShowPassword = false;
/**
* Automatically grab focus after rendering.
* @attr
*/
@property({ type: Boolean, attribute: "grab-focus" })
grabFocus = false;
timer?: number;
input?: HTMLInputElement;
cleanup(): void {
if (this.timer) {
console.debug("authentik/stages/password: cleared focus timer");
window.clearInterval(this.timer);
this.timer = undefined;
}
}
// Must support both older browsers and shadyDom; we'll keep using this in-line, but it'll still
// be in the scope of the parent element, not an independent shadowDOM.
createRenderRoot() {
return this;
}
// State is saved in the DOM, and read from the DOM. Directly affects the DOM,
// so no `.requestUpdate()` required. Effect is immediately visible.
togglePasswordVisibility(ev: PointerEvent) {
const passwordField = this.renderRoot.querySelector(`#${this.inputId}`) as HTMLInputElement;
ev.stopPropagation();
ev.preventDefault();
if (!passwordField) {
throw new Error("ak-flow-password-input: unable to identify input field");
}
passwordField.type = passwordField.type === "password" ? "text" : "password";
this.renderPasswordVisibilityFeatures(passwordField);
}
// In the unlikely event that we want to make "show password" the _default_ behavior, this
// effect handler is broken out into its own method. The current behavior in the main
// `.render()` method assumes the field is of type "password." To have this effect, er, take
// effect, call it in an `.updated()` method.
renderPasswordVisibilityFeatures(passwordField: HTMLInputElement) {
const toggleId = `#${this.inputId}-visibility-toggle`;
const visibilityToggle = this.renderRoot.querySelector(toggleId) as HTMLButtonElement;
if (!visibilityToggle) {
return;
}
const show = passwordField.type === "password";
visibilityToggle?.setAttribute(
"aria-label",
show ? msg("Show password") : msg("Hide password"),
);
visibilityToggle?.querySelector("i")?.remove();
render(
show
? html`<i class="fas fa-eye" aria-hidden="true"></i>`
: html`<i class="fas fa-eye-slash" aria-hidden="true"></i>`,
visibilityToggle,
);
}
renderInput(): HTMLInputElement {
this.input = document.createElement("input");
this.input.id = `${this.inputId}`;
this.input.type = "password";
this.input.name = this.name;
this.input.placeholder = this.placeholder;
this.input.autofocus = true;
this.input.autocomplete = "current-password";
this.input.classList.add("pf-c-form-control");
this.input.required = true;
this.input.value = this.passwordPrefill ?? "";
if (this.invalid) {
this.input.setAttribute("aria-invalid", this.invalid);
}
// This is somewhat of a crude way to get autofocus, but in most cases the `autofocus` attribute
// isn't enough, due to timing within shadow doms and such.
if (this.grabFocus) {
this.timer = window.setInterval(() => {
if (!this.input) {
return;
}
// Because activeElement behaves differently with shadow dom
// we need to recursively check
const rootEl = document.activeElement;
const isActive = (el: Element | null): boolean => {
if (!rootEl) return false;
if (!("shadowRoot" in rootEl)) return false;
if (rootEl.shadowRoot === null) return false;
if (rootEl.shadowRoot.activeElement === el) return true;
return isActive(rootEl.shadowRoot.activeElement);
};
if (isActive(this.input)) {
this.cleanup();
}
this.input.focus();
}, 10);
console.debug("authentik/stages/password: started focus timer");
}
return this.input;
}
render() {
return html` <ak-form-element
label="${this.label}"
required
class="pf-c-form__group"
.errors=${this.errors}
>
<div class="pf-c-input-group">
${this.renderInput()}
${this.allowShowPassword
? html` <button
class="pf-c-button pf-m-control ak-stage-password-toggle-visibility"
type="button"
aria-label=${msg("Show password")}
@click=${(ev: PointerEvent) => this.togglePasswordVisibility(ev)}
>
<i class="fas fa-eye" aria-hidden="true"></i>
</button>`
: nothing}
</div>
</ak-form-element>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-flow-input-password": InputPassword;
}
}

View File

@@ -79,7 +79,7 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<div class="pf-c-form__group">
<p>${msg("You're about to be redirect to the following URL.")}</p>
<p>${msg("You will now be redirected to the following URL.")}</p>
<code>${this.getURL()}</code>
</div>
<div class="pf-c-form__group pf-m-action">

View File

@@ -2,6 +2,7 @@ import { renderSourceIcon } from "@goauthentik/admin/sources/utils";
import "@goauthentik/elements/Divider";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/components/ak-flow-password-input.js";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg, str } from "@lit/localize";
@@ -12,6 +13,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -45,22 +47,32 @@ export class IdentificationStage extends BaseStage<
form?: HTMLFormElement;
static get styles(): CSSResult[] {
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css`
return [
PFBase,
PFAlert,
PFInputGroup,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
PFButton,
/* login page's icons */
.pf-c-login__main-footer-links-item button {
background-color: transparent;
border: 0;
display: flex;
align-items: stretch;
}
.pf-c-login__main-footer-links-item img {
fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill);
width: 100px;
max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width);
height: 100%;
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
}
`);
css`
.pf-c-login__main-footer-links-item button {
background-color: transparent;
border: 0;
display: flex;
align-items: stretch;
}
.pf-c-login__main-footer-links-item img {
fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill);
width: 100px;
max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width);
height: 100%;
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
}
`,
];
}
updated(changedProperties: PropertyValues<this>) {
@@ -250,22 +262,16 @@ export class IdentificationStage extends BaseStage<
</ak-form-element>
${this.challenge.passwordFields
? html`
<ak-form-element
label="${msg("Password")}"
?required="${true}"
<ak-flow-input-password
label=${msg("Password")}
inputId="ak-stage-identification-password"
required
grab-focus
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {})["password"]}
>
<input
type="password"
name="password"
placeholder="${msg("Password")}"
autocomplete="current-password"
class="pf-c-form-control"
required
value=${PasswordManagerPrefill.password || ""}
/>
</ak-form-element>
.errors=${(this.challenge?.responseErrors || {})["password"]}
?allow-show-password=${this.challenge.allowShowPassword}
prefill=${PasswordManagerPrefill["password"] ?? ""}
></ak-flow-input-password>
`
: nothing}
${"non_field_errors" in (this.challenge?.responseErrors || {})

View File

@@ -1,6 +1,7 @@
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/FormStatic";
import "@goauthentik/flow/components/ak-flow-password-input.js";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
@@ -12,6 +13,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -21,62 +23,14 @@ import { PasswordChallenge, PasswordChallengeResponseRequest } from "@goauthenti
@customElement("ak-stage-password")
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle];
return [PFBase, PFLogin, PFInputGroup, PFForm, PFFormControl, PFButton, PFTitle];
}
input?: HTMLInputElement;
timer?: number;
hasError(field: string): boolean {
const errors = (this.challenge?.responseErrors || {})[field];
return (errors || []).length > 0;
}
renderInput(): HTMLInputElement {
this.input = document.createElement("input");
this.input.type = "password";
this.input.name = "password";
this.input.placeholder = msg("Please enter your password");
this.input.autofocus = true;
this.input.autocomplete = "current-password";
this.input.classList.add("pf-c-form-control");
this.input.required = true;
this.input.value = PasswordManagerPrefill.password || "";
this.input.setAttribute("aria-invalid", this.hasError("password").toString());
// This is somewhat of a crude way to get autofocus, but in most cases the `autofocus` attribute
// isn't enough, due to timing within shadow doms and such.
this.timer = window.setInterval(() => {
if (!this.input) {
return;
}
// Because activeElement behaves differently with shadow dom
// we need to recursively check
const rootEl = document.activeElement;
const isActive = (el: Element | null): boolean => {
if (!rootEl) return false;
if (!("shadowRoot" in rootEl)) return false;
if (rootEl.shadowRoot === null) return false;
if (rootEl.shadowRoot.activeElement === el) return true;
return isActive(rootEl.shadowRoot.activeElement);
};
if (isActive(this.input)) {
this.cleanup();
}
this.input.focus();
}, 10);
console.debug("authentik/stages/password: started focus timer");
return this.input;
}
cleanup(): void {
if (this.timer) {
console.debug("authentik/stages/password: cleared focus timer");
window.clearInterval(this.timer);
this.timer = undefined;
}
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
@@ -109,14 +63,16 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
type="hidden"
value="${this.challenge.pendingUser}"
/>
<ak-form-element
label="${msg("Password")}"
?required="${true}"
<ak-flow-input-password
label=${msg("Password")}
required
grab-focus
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["password"]}
>
${this.renderInput()}
</ak-form-element>
?allow-show-password=${this.challenge.allowShowPassword}
invalid=${this.hasError("password").toString()}
prefill=${PasswordManagerPrefill["password"] ?? ""}
></ak-flow-input-password>
${this.challenge.recoveryUrl
? html`<a href="${this.challenge.recoveryUrl}">

View File

@@ -231,14 +231,11 @@ ${prompt.initialValue}</textarea
shouldRenderInWrapper(prompt: StagePrompt): boolean {
// Special types that aren't rendered in a wrapper
if (
return !(
prompt.type === PromptTypeEnum.Static ||
prompt.type === PromptTypeEnum.Hidden ||
prompt.type === PromptTypeEnum.Separator
) {
return false;
}
return true;
);
}
renderField(prompt: StagePrompt): TemplateResult {

View File

@@ -166,6 +166,6 @@ export class LibraryPageApplicationSearch extends AKElement {
declare global {
interface HTMLElementTagNameMap {
"ak-library-list-search": LibraryPageApplicationList;
"ak-library-list-search": LibraryPageApplicationSearch;
}
}

View File

@@ -70,10 +70,7 @@ export class UserSettingsFlowExecutor
})
.then((data) => {
this.challenge = data;
if (this.challenge.responseErrors) {
return false;
}
return true;
return !this.challenge.responseErrors;
})
.catch((e: Error | ResponseError) => {
this.errorMessage(e);

View File

@@ -10,20 +10,16 @@ import { PromptTypeEnum, StagePrompt } from "@goauthentik/api";
@customElement("ak-user-stage-prompt")
export class UserSettingsPromptStage extends PromptStage {
renderPromptInner(prompt: StagePrompt): TemplateResult {
switch (prompt.type) {
// Checkbox requires slightly different rendering here due to the use of horizontal form elements
case PromptTypeEnum.Checkbox:
return html`<input
type="checkbox"
class="pf-c-check__input"
name="${prompt.fieldKey}"
?checked=${prompt.initialValue !== ""}
?required=${prompt.required}
style="vertical-align: bottom"
/>`;
default:
return super.renderPromptInner(prompt);
}
return prompt.type === PromptTypeEnum.Checkbox
? html`<input
type="checkbox"
class="pf-c-check__input"
name="${prompt.fieldKey}"
?checked=${prompt.initialValue !== ""}
?required=${prompt.required}
style="vertical-align: bottom"
/>`
: super.renderPromptInner(prompt);
}
renderField(prompt: StagePrompt): TemplateResult {

View File